diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-10-09 18:37:27 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-09 18:37:27 -0700 |
commit | f09150617ed2454f3074bcf93f53aae5ae637d40 (patch) | |
tree | 5cfe614a64d921dfe58b049f426d67a8b832c71f /server/continuedev/models | |
parent | 985304a213f620cdff3f8f65f74ed7e3b79be29d (diff) | |
download | sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.gz sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.bz2 sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.zip |
Preview (#541)
* Strong typing (#533)
* refactor: :recycle: get rid of continuedev.src.continuedev structure
* refactor: :recycle: switching back to server folder
* feat: :sparkles: make config.py imports shorter
* feat: :bookmark: publish as pre-release vscode extension
* refactor: :recycle: refactor and add more completion params to ui
* build: :building_construction: download from preview S3
* fix: :bug: fix paths
* fix: :green_heart: package:pre-release
* ci: :green_heart: more time for tests
* fix: :green_heart: fix build scripts
* fix: :bug: fix import in run.py
* fix: :bookmark: update version to try again
* ci: 💚 Update package.json version [skip ci]
* refactor: :fire: don't check for old extensions version
* fix: :bug: small bug fixes
* fix: :bug: fix config.py import paths
* ci: 💚 Update package.json version [skip ci]
* ci: :green_heart: platform-specific builds test #1
* feat: :green_heart: ship with binary
* fix: :green_heart: fix copy statement to include.exe for windows
* fix: :green_heart: cd extension before packaging
* chore: :loud_sound: count tokens generated
* fix: :green_heart: remove npm_config_arch
* fix: :green_heart: publish as pre-release!
* chore: :bookmark: update version
* perf: :green_heart: hardcode distro paths
* fix: :bug: fix yaml syntax error
* chore: :bookmark: update version
* fix: :green_heart: update permissions and version
* feat: :bug: kill old server if needed
* feat: :lipstick: update marketplace icon for pre-release
* ci: 💚 Update package.json version [skip ci]
* feat: :sparkles: auto-reload for config.py
* feat: :wrench: update default config.py imports
* feat: :sparkles: codelens in config.py
* feat: :sparkles: select model param count from UI
* ci: 💚 Update package.json version [skip ci]
* feat: :sparkles: more model options, ollama error handling
* perf: :zap: don't show server loading immediately
* fix: :bug: fixing small UI details
* ci: 💚 Update package.json version [skip ci]
* feat: :rocket: headers param on LLM class
* fix: :bug: fix headers for openai.;y
* feat: :sparkles: highlight code on cmd+shift+L
* ci: 💚 Update package.json version [skip ci]
* feat: :lipstick: sticky top bar in gui.tsx
* fix: :loud_sound: websocket logging and horizontal scrollbar
* ci: 💚 Update package.json version [skip ci]
* feat: :sparkles: allow AzureOpenAI Service through GGML
* ci: 💚 Update package.json version [skip ci]
* fix: :bug: fix automigration
* ci: 💚 Update package.json version [skip ci]
* ci: :green_heart: upload binaries in ci, download apple silicon
* chore: :fire: remove notes
* fix: :green_heart: use curl to download binary
* fix: :green_heart: set permissions on apple silicon binary
* fix: :green_heart: testing
* fix: :green_heart: cleanup file
* fix: :green_heart: fix preview.yaml
* fix: :green_heart: only upload once per binary
* fix: :green_heart: install rosetta
* ci: :green_heart: download binary after tests
* ci: 💚 Update package.json version [skip ci]
* ci: :green_heart: prepare ci for merge to main
---------
Co-authored-by: GitHub Action <action@github.com>
Diffstat (limited to 'server/continuedev/models')
-rw-r--r-- | server/continuedev/models/__init__.py | 0 | ||||
-rw-r--r-- | server/continuedev/models/filesystem.py | 398 | ||||
-rw-r--r-- | server/continuedev/models/filesystem_edit.py | 164 | ||||
-rw-r--r-- | server/continuedev/models/generate_json_schema.py | 54 | ||||
-rw-r--r-- | server/continuedev/models/main.py | 229 | ||||
-rw-r--r-- | server/continuedev/models/reference/generate.py | 144 |
6 files changed, 989 insertions, 0 deletions
diff --git a/server/continuedev/models/__init__.py b/server/continuedev/models/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/server/continuedev/models/__init__.py diff --git a/server/continuedev/models/filesystem.py b/server/continuedev/models/filesystem.py new file mode 100644 index 00000000..27244c4b --- /dev/null +++ b/server/continuedev/models/filesystem.py @@ -0,0 +1,398 @@ +import os +from abc import abstractmethod +from typing import Dict, List, Tuple + +from pydantic import BaseModel + +from ..models.main import AbstractModel, Position, Range +from .filesystem_edit import ( + AddDirectory, + AddFile, + DeleteDirectory, + DeleteFile, + EditDiff, + FileEdit, + FileSystemEdit, + RenameDirectory, + RenameFile, + SequentialFileSystemEdit, +) + + +class RangeInFile(BaseModel): + filepath: str + range: Range + + def __hash__(self): + return hash((self.filepath, self.range)) + + @staticmethod + def from_entire_file(filepath: str, content: str) -> "RangeInFile": + range = Range.from_entire_file(content) + return RangeInFile(filepath=filepath, range=range) + + def translated(self, lines: int): + return RangeInFile(filepath=self.filepath, range=self.range.translated(lines)) + + +class RangeInFileWithContents(RangeInFile): + """A range in a file with the contents of the range.""" + + contents: str + + def __hash__(self): + return hash((self.filepath, self.range, self.contents)) + + def union(self, other: "RangeInFileWithContents") -> "RangeInFileWithContents": + assert self.filepath == other.filepath + # Use a placeholder variable for self and swap it with other if other comes before self + first = self + second = other + if other.range.start < self.range.start: + first = other + second = self + + assert first.filepath == second.filepath + + # Calculate union of contents + num_overlapping_lines = first.range.end.line - second.range.start.line + 1 + union_lines = ( + first.contents.splitlines()[:-num_overlapping_lines] + + second.contents.splitlines() + ) + + return RangeInFileWithContents( + filepath=first.filepath, + range=first.range.union(second.range), + contents="\n".join(union_lines), + ) + + @staticmethod + def from_entire_file(filepath: str, content: str) -> "RangeInFileWithContents": + lines = content.splitlines() + if not lines: + return RangeInFileWithContents( + filepath=filepath, range=Range.from_shorthand(0, 0, 0, 0), contents="" + ) + return RangeInFileWithContents( + filepath=filepath, + range=Range.from_shorthand(0, 0, len(lines) - 1, len(lines[-1]) - 1), + contents=content, + ) + + @staticmethod + def from_range_in_file(rif: RangeInFile, content: str) -> "RangeInFileWithContents": + return RangeInFileWithContents( + filepath=rif.filepath, range=rif.range, contents=content + ) + + +class FileSystem(AbstractModel): + """An abstract filesystem that can read/write from a set of files.""" + + @abstractmethod + def read(self, path) -> str: + raise NotImplementedError + + @abstractmethod + def readlines(self, path) -> List[str]: + raise NotImplementedError + + @abstractmethod + def write(self, path, content): + raise NotImplementedError + + @abstractmethod + def exists(self, path) -> bool: + raise NotImplementedError + + @abstractmethod + def read_range_in_file(self, r: RangeInFile) -> str: + raise NotImplementedError + + @abstractmethod + def rename_file(self, filepath: str, new_filepath: str): + raise NotImplementedError + + @abstractmethod + def rename_directory(self, path: str, new_path: str): + raise NotImplementedError + + @abstractmethod + def delete_file(self, filepath: str): + raise NotImplementedError + + @abstractmethod + def delete_directory(self, path: str): + raise NotImplementedError + + @abstractmethod + def add_directory(self, path: str): + raise NotImplementedError + + @abstractmethod + def apply_file_edit(self, edit: FileEdit) -> EditDiff: + raise NotImplementedError + + @abstractmethod + def apply_edit(self, edit: FileSystemEdit) -> EditDiff: + """Apply edit to filesystem, calculate the reverse edit, and return and EditDiff""" + raise NotImplementedError + + @abstractmethod + def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]: + """List the contents of a directory""" + raise NotImplementedError + + @classmethod + def read_range_in_str(self, s: str, r: Range) -> str: + lines = s.split("\n")[r.start.line : r.end.line + 1] + if len(lines) == 0: + return "" + + lines[0] = lines[0][r.start.character :] + lines[-1] = lines[-1][: r.end.character + 1] + return "\n".join(lines) + + @classmethod + def apply_edit_to_str(cls, s: str, edit: FileEdit) -> Tuple[str, EditDiff]: + original = cls.read_range_in_str(s, edit.range) + + # Split lines and deal with some edge cases (could obviously be nicer) + lines = s.splitlines() + if s.startswith("\n"): + lines.insert(0, "") + if s.endswith("\n"): + lines.append("") + + if len(lines) == 0: + lines = [""] + + end = Position(line=edit.range.end.line, character=edit.range.end.character) + if edit.range.end.line == len(lines) and edit.range.end.character == 0: + end = Position( + line=edit.range.end.line - 1, + character=len(lines[min(len(lines) - 1, edit.range.end.line - 1)]), + ) + + before_lines = lines[: edit.range.start.line] + after_lines = lines[end.line + 1 :] + between_str = ( + lines[min(len(lines) - 1, edit.range.start.line)][ + : edit.range.start.character + ] + + edit.replacement + + lines[min(len(lines) - 1, end.line)][end.character + 1 :] + ) + + new_range = Range( + start=edit.range.start, + end=Position( + line=edit.range.start.line + len(edit.replacement.splitlines()) - 1, + character=edit.range.start.character + + len(edit.replacement.splitlines()[-1]) + if edit.replacement != "" + else 0, + ), + ) + + lines = before_lines + between_str.splitlines() + after_lines + return "\n".join(lines), EditDiff( + forward=edit, + backward=FileEdit( + filepath=edit.filepath, range=new_range, replacement=original + ), + ) + + def reverse_edit_on_str(self, s: str, diff: EditDiff) -> str: + lines = s.splitlines() + + replacement_lines = diff.replacement.splitlines() + replacement_d_lines = len(replacement_lines) + replacement_d_chars = len(replacement_lines[-1]) + replacement_range = Range( + start=diff.edit.range.start, + end=Position( + line=diff.edit.range.start + replacement_d_lines, + character=diff.edit.range.start.character + replacement_d_chars, + ), + ) + + before_lines = lines[: replacement_range.start.line] + after_lines = lines[replacement_range.end.line + 1 :] + between_str = ( + lines[replacement_range.start.line][: replacement_range.start.character] + + diff.original + + lines[replacement_range.end.line][replacement_range.end.character + 1 :] + ) + + lines = before_lines + between_str.splitlines() + after_lines + return "\n".join(lines) + + def apply_edit(self, edit: FileSystemEdit) -> EditDiff: + backward = None + if isinstance(edit, FileEdit): + diff = self.apply_file_edit(edit) + backward = diff.backward + elif isinstance(edit, AddFile): + self.write(edit.filepath, edit.content) + backward = DeleteFile(edit.filepath) + elif isinstance(edit, DeleteFile): + contents = self.read(edit.filepath) + backward = AddFile(edit.filepath, contents) + self.delete_file(edit.filepath) + elif isinstance(edit, RenameFile): + self.rename_file(edit.filepath, edit.new_filepath) + backward = RenameFile( + filepath=edit.new_filepath, new_filepath=edit.filepath + ) + elif isinstance(edit, AddDirectory): + self.add_directory(edit.path) + backward = DeleteDirectory(edit.path) + elif isinstance(edit, DeleteDirectory): + # This isn't atomic! + backward_edits = [] + for root, dirs, files in os.walk(edit.path, topdown=False): + for f in files: + path = os.path.join(root, f) + backward_edits.append(self.apply_edit(DeleteFile(path))) + for d in dirs: + path = os.path.join(root, d) + backward_edits.append(self.apply_edit(DeleteDirectory(path))) + + backward_edits.append(self.apply_edit(DeleteDirectory(edit.path))) + backward_edits.reverse() + backward = SequentialFileSystemEdit(edits=backward_edits) + elif isinstance(edit, RenameDirectory): + self.rename_directory(edit.path, edit.new_path) + backward = RenameDirectory(path=edit.new_path, new_path=edit.path) + elif isinstance(edit, FileSystemEdit): + backward_edits = [] + for edit in edit.next_edit(): + backward_edits.append(self.apply_edit(edit)) + backward_edits.reverse() + backward = SequentialFileSystemEdit(edits=backward_edits) + else: + raise TypeError("Unknown FileSystemEdit type: " + str(type(edit))) + + return EditDiff(forward=edit, backward=backward) + + +class RealFileSystem(FileSystem): + """A filesystem that reads/writes from the actual filesystem.""" + + def read(self, path) -> str: + with open(path, "r") as f: + return f.read() + + def readlines(self, path) -> List[str]: + with open(path, "r") as f: + return f.readlines() + + def write(self, path, content): + with open(path, "w") as f: + f.write(content) + + def exists(self, path) -> bool: + return os.path.exists(path) + + def read_range_in_file(self, r: RangeInFile) -> str: + return FileSystem.read_range_in_str(self.read(r.filepath), r.range) + + def rename_file(self, filepath: str, new_filepath: str): + os.rename(filepath, new_filepath) + + def rename_directory(self, path: str, new_path: str): + os.rename(path, new_path) + + def delete_file(self, filepath: str): + os.remove(filepath) + + def delete_directory(self, path: str): + raise NotImplementedError + + def add_directory(self, path: str): + os.makedirs(path) + + def apply_file_edit(self, edit: FileEdit) -> EditDiff: + old_content = self.read(edit.filepath) + new_content, diff = FileSystem.apply_edit_to_str(old_content, edit) + self.write(edit.filepath, new_content) + return diff + + def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]: + """List the contents of a directory""" + if recursive: + # Walk + paths = [] + for root, dirs, files in os.walk(path): + for f in files: + paths.append(os.path.join(root, f)) + + return paths + return list(map(lambda x: os.path.join(path, x), os.listdir(path))) + + +class VirtualFileSystem(FileSystem): + """A simulated filesystem from a mapping of filepath to file contents.""" + + files: Dict[str, str] + + def __init__(self, files: Dict[str, str]): + self.files = files + + def read(self, path) -> str: + return self.files[path] + + def readlines(self, path) -> List[str]: + return self.files[path].splitlines() + + def write(self, path, content): + self.files[path] = content + + def exists(self, path) -> bool: + return path in self.files + + def read_range_in_file(self, r: RangeInFile) -> str: + return FileSystem.read_range_in_str(self.read(r.filepath), r.range) + + def rename_file(self, filepath: str, new_filepath: str): + self.files[new_filepath] = self.files[filepath] + del self.files[filepath] + + def rename_directory(self, path: str, new_path: str): + for filepath in self.files: + if filepath.startswith(path): + new_filepath = new_path + filepath[len(path) :] + self.files[new_filepath] = self.files[filepath] + del self.files[filepath] + + def delete_file(self, filepath: str): + del self.files[filepath] + + def delete_directory(self, path: str): + raise NotImplementedError + + def add_directory(self, path: str): + # For reasons as seen here and in delete_directory, a Dict[str, str] might not be the best representation. Could just preprocess to something better upon __init__ + pass + + def apply_file_edit(self, edit: FileEdit) -> EditDiff: + old_content = self.read(edit.filepath) + new_content, original = FileSystem.apply_edit_to_str(old_content, edit) + self.write(edit.filepath, new_content) + return EditDiff(edit=edit, original=original) + + def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]: + """List the contents of a directory""" + if recursive: + for filepath in self.files: + if filepath.startswith(path): + yield filepath + + for filepath in self.files: + if filepath.startswith(path) and "/" not in filepath[len(path) :]: + yield filepath + + +# TODO: Uniform errors thrown by any FileSystem subclass. diff --git a/server/continuedev/models/filesystem_edit.py b/server/continuedev/models/filesystem_edit.py new file mode 100644 index 00000000..9316ff46 --- /dev/null +++ b/server/continuedev/models/filesystem_edit.py @@ -0,0 +1,164 @@ +import os +from abc import abstractmethod +from typing import Generator, List + +from pydantic import BaseModel + +from ..libs.util.map_path import map_path +from .main import Position, Range + + +class FileSystemEdit(BaseModel): + @abstractmethod + def next_edit(self) -> Generator["FileSystemEdit", None, None]: + raise NotImplementedError + + @abstractmethod + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + raise NotImplementedError + + +class AtomicFileSystemEdit(FileSystemEdit): + def next_edit(self) -> Generator["FileSystemEdit", None, None]: + yield self + + +class FileEdit(AtomicFileSystemEdit): + filepath: str + range: Range + replacement: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return FileEdit( + map_path(self.filepath, orig_root, copy_root), self.range, self.replacement + ) + + @staticmethod + def from_deletion(filepath: str, range: Range) -> "FileEdit": + return FileEdit(filepath=filepath, range=range, replacement="") + + @staticmethod + def from_insertion(filepath: str, position: Position, content: str) -> "FileEdit": + return FileEdit( + filepath=filepath, + range=Range.from_shorthand( + position.line, position.character, position.line, position.character + ), + replacement=content, + ) + + @staticmethod + def from_append( + filepath: str, previous_content: str, appended_content: str + ) -> "FileEdit": + return FileEdit( + filepath=filepath, + range=Range.from_position(Position.from_end_of_file(previous_content)), + replacement=appended_content, + ) + + +class FileEditWithFullContents(BaseModel): + fileEdit: FileEdit + fileContents: str + + +class AddFile(AtomicFileSystemEdit): + filepath: str + content: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return AddFile( + self, map_path(self.filepath, orig_root, copy_root), self.content + ) + + +class DeleteFile(AtomicFileSystemEdit): + filepath: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return DeleteFile(map_path(self.filepath, orig_root, copy_root)) + + +class RenameFile(AtomicFileSystemEdit): + filepath: str + new_filepath: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return RenameFile( + map_path(self.filepath, orig_root, copy_root), + map_path(self.new_filepath, orig_root, copy_root), + ) + + +class AddDirectory(AtomicFileSystemEdit): + path: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return AddDirectory(map_path(self.path, orig_root, copy_root)) + + +class DeleteDirectory(AtomicFileSystemEdit): + path: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return DeleteDirectory(map_path(self.path, orig_root, copy_root)) + + +class RenameDirectory(AtomicFileSystemEdit): + path: str + new_path: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return RenameDirectory( + map_path(self.filepath, orig_root, copy_root), + map_path(self.new_path, orig_root, copy_root), + ) + + +class DeleteDirectoryRecursive(FileSystemEdit): + path: str + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return DeleteDirectoryRecursive(map_path(self.path, orig_root, copy_root)) + + def next_edit(self) -> Generator[FileSystemEdit, None, None]: + yield DeleteDirectory(path=self.path) + for child in os.listdir(self.path): + child_path = os.path.join(self.path, child) + if os.path.isdir(child_path): + yield DeleteDirectoryRecursive(path=child_path) + else: + yield DeleteFile(filepath=child_path) + + +class SequentialFileSystemEdit(FileSystemEdit): + edits: List[FileSystemEdit] + + def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit": + return SequentialFileSystemEdit( + [edit.with_mapped_paths(orig_root, copy_root) for edit in self.edits] + ) + + def next_edit(self) -> Generator["FileSystemEdit", None, None]: + for edit in self.edits: + yield from edit.next_edit() + + +class EditDiff(BaseModel): + """A reversible edit that can be applied to a file.""" + + forward: FileSystemEdit + backward: FileSystemEdit + + @classmethod + def from_sequence(cls, diffs: List["EditDiff"]) -> "EditDiff": + forwards = [] + backwards = [] + for diff in diffs: + forwards.append(diff.forward) + backwards.insert(0, diff.backward) + return cls( + forward=SequentialFileSystemEdit(edits=forwards), + backward=SequentialFileSystemEdit(edits=backwards), + ) diff --git a/server/continuedev/models/generate_json_schema.py b/server/continuedev/models/generate_json_schema.py new file mode 100644 index 00000000..88a1db68 --- /dev/null +++ b/server/continuedev/models/generate_json_schema.py @@ -0,0 +1,54 @@ +import os + +from pydantic import schema_json_of + +from ..core.config import ContinueConfig +from ..core.context import ContextItem +from ..core.main import FullState, History, HistoryNode, SessionInfo +from ..core.models import Models +from ..libs.llm.base import LLM +from .filesystem import FileEdit, RangeInFile +from .filesystem_edit import FileEditWithFullContents +from .main import Position, Range, Traceback, TracebackFrame + +MODELS_TO_GENERATE = ( + [Position, Range, Traceback, TracebackFrame] + + [RangeInFile, FileEdit] + + [FileEditWithFullContents] + + [History, HistoryNode, FullState, SessionInfo] + + [ContinueConfig] + + [ContextItem] + + [Models] + + [LLM] +) + +RENAMES = {"ExampleClass": "RenamedName"} + +SCHEMA_DIR = "../schema/json" + + +def clear_schemas(): + for filename in os.listdir(SCHEMA_DIR): + if filename.endswith(".json"): + os.remove(os.path.join(SCHEMA_DIR, filename)) + + +def main(): + clear_schemas() + for model in MODELS_TO_GENERATE: + title = RENAMES.get(model.__name__, model.__name__) + try: + json = schema_json_of(model, indent=2, title=title) + except Exception as e: + import traceback + + print(f"Failed to generate json schema for {title}: {e}") + traceback.print_exc() + continue # pun intended + + with open(f"{SCHEMA_DIR}/{title}.json", "w") as f: + f.write(json) + + +if __name__ == "__main__": + main() diff --git a/server/continuedev/models/main.py b/server/continuedev/models/main.py new file mode 100644 index 00000000..5519d718 --- /dev/null +++ b/server/continuedev/models/main.py @@ -0,0 +1,229 @@ +from abc import ABC +from functools import total_ordering +from typing import List, Tuple, Union + +from pydantic import BaseModel, root_validator + + +class ContinueBaseModel(BaseModel): + class Config: + underscore_attrs_are_private = True + + +@total_ordering +class Position(BaseModel): + line: int + character: int + + def __hash__(self): + return hash((self.line, self.character)) + + def __eq__(self, other: "Position") -> bool: + return self.line == other.line and self.character == other.character + + def __lt__(self, other: "Position") -> bool: + if self.line < other.line: + return True + elif self.line == other.line: + return self.character < other.character + else: + return False + + @staticmethod + def from_index(string: str, index: int) -> "Position": + """Convert index in string to line and character""" + line = string.count("\n", 0, index) + if line == 0: + character = index + else: + character = index - string.rindex("\n", 0, index) - 1 + + return Position(line=line, character=character) + + @staticmethod + def from_end_of_file(contents: str) -> "Position": + return Position.from_index(contents, len(contents)) + + def to_index(self, string: str) -> int: + """Convert line and character to index in string""" + lines = string.splitlines() + return sum(map(len, lines[: self.line])) + self.character + + +class PositionInFile(BaseModel): + position: Position + filepath: str + + +class Range(BaseModel): + """A range in a file. 0-indexed.""" + + start: Position + end: Position + + def __lt__(self, other: "Range") -> bool: + return self.start < other.start or ( + self.start == other.start and self.end < other.end + ) + + def __eq__(self, other: "Range") -> bool: + return self.start == other.start and self.end == other.end + + def __hash__(self): + return hash((self.start, self.end)) + + def union(self, other: "Range") -> "Range": + return Range( + start=min(self.start, other.start), + end=max(self.end, other.end), + ) + + def is_empty(self) -> bool: + return self.start == self.end + + def indices_in_string(self, string: str) -> Tuple[int, int]: + """Get the start and end indices of this range in the string""" + lines = string.splitlines() + if len(lines) == 0: + return (0, 0) + + start_index = ( + sum([len(line) + 1 for line in lines[: self.start.line]]) + + self.start.character + ) + end_index = ( + sum([len(line) + 1 for line in lines[: self.end.line]]) + self.end.character + ) + return (start_index, end_index) + + def overlaps_with(self, other: "Range") -> bool: + return not (self.end < other.start or self.start > other.end) + + def to_full_lines(self) -> "Range": + return Range( + start=Position(line=self.start.line, character=0), + end=Position(line=self.end.line + 1, character=0), + ) + + def translated(self, lines: int): + return Range( + start=Position( + line=self.start.line + lines, character=self.start.character + ), + end=Position(line=self.end.line + lines, character=self.end.character), + ) + + def contains(self, position: Position) -> bool: + return self.start <= position and position <= self.end + + def merge_with(self, other: "Range") -> "Range": + return Range( + start=min(self.start, other.start).copy(), + end=max(self.end, other.end).copy(), + ) + + @staticmethod + def from_indices(string: str, start_index: int, end_index: int) -> "Range": + return Range( + start=Position.from_index(string, start_index), + end=Position.from_index(string, end_index), + ) + + @staticmethod + def from_shorthand( + start_line: int, start_char: int, end_line: int, end_char: int + ) -> "Range": + return Range( + start=Position(line=start_line, character=start_char), + end=Position(line=end_line, character=end_char), + ) + + @staticmethod + def from_entire_file(content: str) -> "Range": + lines = content.splitlines() + if len(lines) == 0: + return Range.from_shorthand(0, 0, 0, 0) + return Range.from_shorthand(0, 0, len(lines), 0) + + @staticmethod + def from_snippet_in_file(content: str, snippet: str) -> "Range": + start_index = content.index(snippet) + end_index = start_index + len(snippet) + return Range.from_indices(content, start_index, end_index) + + @staticmethod + def from_lines_snippet_in_file(content: str, snippet: str) -> "Range": + # lines is a substring of the content modulo whitespace on each line + content_lines = content.splitlines() + snippet_lines = snippet.splitlines() + + start_line = -1 + end_line = -1 + looking_for_line = 0 + for i in range(len(content_lines)): + if content_lines[i].strip() == snippet_lines[looking_for_line].strip(): + if looking_for_line == len(snippet_lines) - 1: + start_line = i - len(snippet_lines) + 1 + end_line = i + break + looking_for_line += 1 + else: + looking_for_line = 0 + + if start_line == -1 or end_line == -1: + raise ValueError("Snippet not found in content") + + return Range.from_shorthand( + start_line, 0, end_line, len(content_lines[end_line]) - 1 + ) + + @staticmethod + def from_position(position: Position) -> "Range": + return Range(start=position, end=position) + + +class AbstractModel(ABC, BaseModel): + @root_validator(pre=True) + def check_is_subclass(cls, values): + if not issubclass(cls, AbstractModel): + raise TypeError( + "AbstractModel subclasses must be subclasses of AbstractModel" + ) + + +class TracebackFrame(BaseModel): + filepath: str + lineno: int + function: str + code: Union[str, None] + + def __eq__(self, other): + return ( + self.filepath == other.filepath + and self.lineno == other.lineno + and self.function == other.function + ) + + +class Traceback(BaseModel): + frames: List[TracebackFrame] + message: str + error_type: str + full_traceback: Union[str, None] + + @classmethod + def from_tbutil_parsed_exc(cls, tbutil_parsed_exc): + return cls( + frames=[ + TracebackFrame( + filepath=frame["filepath"], + lineno=frame["lineno"], + function=frame["funcname"], + code=frame["source_line"], + ) + for frame in tbutil_parsed_exc.frames + ], + message=tbutil_parsed_exc.exc_msg, + error_type=tbutil_parsed_exc.exc_type, + full_traceback=tbutil_parsed_exc.to_string(), + ) diff --git a/server/continuedev/models/reference/generate.py b/server/continuedev/models/reference/generate.py new file mode 100644 index 00000000..74912f75 --- /dev/null +++ b/server/continuedev/models/reference/generate.py @@ -0,0 +1,144 @@ +import html +import importlib +import json +from textwrap import dedent + +LLM_MODULES = [ + ("openai", "OpenAI"), + ("anthropic", "AnthropicLLM"), + ("ggml", "GGML"), + ("llamacpp", "LlamaCpp"), + ("text_gen_interface", "TextGenUI"), + ("ollama", "Ollama"), + ("replicate", "ReplicateLLM"), + ("together", "TogetherLLM"), + ("hf_inference_api", "HuggingFaceInferenceAPI"), + ("hf_tgi", "HuggingFaceTGI"), + ("openai_free_trial", "OpenAIFreeTrial"), + ("google_palm_api", "GooglePaLMAPI"), + ("queued", "QueuedLLM"), +] + +CONTEXT_PROVIDER_MODULES = [ + ("diff", "DiffContextProvider"), + ("file", "FileContextProvider"), + ("filetree", "FileTreeContextProvider"), + ("github", "GitHubIssuesContextProvider"), + ("google", "GoogleContextProvider"), + ("search", "SearchContextProvider"), + ("terminal", "TerminalContextProvider"), + ("url", "URLContextProvider"), +] + + +def import_llm_module(module_name, module_title): + module_name = f"continuedev.libs.llm.{module_name}" + module = importlib.import_module(module_name) + obj = getattr(module, module_title) + return obj + + +def import_context_provider_module(module_name, module_title): + module_name = f"continuedev.plugins.context_providers.{module_name}" + module = importlib.import_module(module_name) + obj = getattr(module, module_title) + return obj + + +def docs_from_schema(schema, filepath, ignore_properties=[], inherited=[]): + # Generate markdown docs + properties = "" + inherited_properties = "" + + def add_property(prop, details, only_required): + required = prop in schema.get("required", []) + if only_required != required or prop in ignore_properties: + return "" + required = "true" if required else "false" + return f"""<ClassPropertyRef name='{prop}' details='{html.escape(json.dumps(details))}' required={{{required}}} default="{html.escape(str(details.get("default", "")))}"/>\n""" + + for prop, details in schema["properties"].items(): + property = add_property(prop, details, True) + if prop in inherited: + inherited_properties += property + else: + properties += property + + for prop, details in schema["properties"].items(): + property = add_property(prop, details, False) + if prop in inherited: + inherited_properties += property + else: + properties += property + + return dedent( + f"""\ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# {schema['title']} + +{dedent(schema.get("description", ""))} + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/{filepath}) + +## Properties + +{properties} + +### Inherited Properties + +{inherited_properties}""" + ) + + +llm_module = importlib.import_module("continuedev.libs.llm.base") +ctx_obj = getattr(llm_module, "LLM") +schema = ctx_obj.schema() +ctx_properties = schema["properties"].keys() + +for module_name, module_title in LLM_MODULES: + obj = import_llm_module(module_name, module_title) + schema = obj.schema() + markdown_docs = docs_from_schema( + schema, f"libs/llm/{module_name}.py", inherited=ctx_properties + ) + with open(f"docs/docs/reference/Models/{module_title.lower()}.md", "w") as f: + f.write(markdown_docs) + +config_module = importlib.import_module("continuedev.core.config") +config_obj = getattr(config_module, "ContinueConfig") +schema = config_obj.schema() +markdown_docs = docs_from_schema(schema, "core/config.py") +with open("docs/docs/reference/config.md", "w") as f: + f.write(markdown_docs) + +ctx_module = importlib.import_module("continuedev.core.context") +ctx_obj = getattr(ctx_module, "ContextProvider") +schema = ctx_obj.schema() +ctx_properties = schema["properties"].keys() +for module_name, module_title in CONTEXT_PROVIDER_MODULES: + obj = import_context_provider_module(module_name, module_title) + schema = obj.schema() + markdown_docs = docs_from_schema( + schema, + f"plugins/context_providers/{module_name}.py", + ignore_properties=[ + "sdk", + "updated_documents", + "delete_documents", + "selected_items", + "ignore_patterns", + ], + inherited=ctx_properties, + ) + with open( + f"docs/docs/reference/Context Providers/{module_title.lower()}.md", "w" + ) as f: + f.write(markdown_docs) + +# sdk_module = importlib.import_module("continuedev.core.sdk") +# sdk_obj = getattr(sdk_module, "ContinueSDK") +# schema = sdk_obj.schema() +# markdown_docs = docs_from_schema(schema, "sdk", ignore_properties=[]) +# with open("docs/docs/reference/ContinueSDK.md", "w") as f: +# f.write(markdown_docs) |