From e31849559351e3988033ad8c95301a833ca38be0 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Wed, 7 Feb 2024 11:13:45 +0800 Subject: [PATCH 1/5] Add new files and modules --- devchat/chatmark/.gitignore | 1 + devchat/chatmark/README.md | 5 + devchat/chatmark/__init__.py | 12 + devchat/chatmark/chatmark_example/README.md | 16 + devchat/chatmark/chatmark_example/main.py | 163 ++++++++ devchat/chatmark/form.py | 95 +++++ devchat/chatmark/iobase.py | 43 +++ devchat/chatmark/step.py | 28 ++ devchat/chatmark/widgets.py | 397 ++++++++++++++++++++ devchat/ide/__init__.py | 5 + devchat/ide/rpc.py | 37 ++ devchat/ide/service.py | 93 +++++ devchat/ide/types.py | 25 ++ devchat/ide/vscode_services.py | 143 +++++++ devchat/llm/__init__.py | 19 + devchat/llm/chat.py | 109 ++++++ devchat/llm/openai.py | 197 ++++++++++ devchat/llm/pipeline.py | 94 +++++ devchat/llm/text_confirm.py | 57 +++ devchat/llm/tools_call.py | 220 +++++++++++ devchat/memory/__init__.py | 0 devchat/memory/base.py | 32 ++ devchat/memory/fixsize_memory.py | 51 +++ 23 files changed, 1842 insertions(+) create mode 100644 devchat/chatmark/.gitignore create mode 100644 devchat/chatmark/README.md create mode 100644 devchat/chatmark/__init__.py create mode 100644 devchat/chatmark/chatmark_example/README.md create mode 100644 devchat/chatmark/chatmark_example/main.py create mode 100644 devchat/chatmark/form.py create mode 100644 devchat/chatmark/iobase.py create mode 100644 devchat/chatmark/step.py create mode 100644 devchat/chatmark/widgets.py create mode 100644 devchat/ide/__init__.py create mode 100644 devchat/ide/rpc.py create mode 100644 devchat/ide/service.py create mode 100644 devchat/ide/types.py create mode 100644 devchat/ide/vscode_services.py create mode 100644 devchat/llm/__init__.py create mode 100644 devchat/llm/chat.py create mode 100644 devchat/llm/openai.py create mode 100644 devchat/llm/pipeline.py create mode 100644 devchat/llm/text_confirm.py create mode 100644 devchat/llm/tools_call.py create mode 100644 devchat/memory/__init__.py create mode 100644 devchat/memory/base.py create mode 100644 devchat/memory/fixsize_memory.py diff --git a/devchat/chatmark/.gitignore b/devchat/chatmark/.gitignore new file mode 100644 index 00000000..c0363794 --- /dev/null +++ b/devchat/chatmark/.gitignore @@ -0,0 +1 @@ +tmp/ \ No newline at end of file diff --git a/devchat/chatmark/README.md b/devchat/chatmark/README.md new file mode 100644 index 00000000..94d95af4 --- /dev/null +++ b/devchat/chatmark/README.md @@ -0,0 +1,5 @@ +# ChatMark + +ChatMark is a markup language for user interaction in chat message. + +This module provides python implementation for common widgets in ChatMark. diff --git a/devchat/chatmark/__init__.py b/devchat/chatmark/__init__.py new file mode 100644 index 00000000..d78551f6 --- /dev/null +++ b/devchat/chatmark/__init__.py @@ -0,0 +1,12 @@ +from .form import Form +from .step import Step +from .widgets import Button, Checkbox, Radio, TextEditor + +__all__ = [ + "Checkbox", + "TextEditor", + "Radio", + "Button", + "Form", + "Step", +] diff --git a/devchat/chatmark/chatmark_example/README.md b/devchat/chatmark/chatmark_example/README.md new file mode 100644 index 00000000..8246f91f --- /dev/null +++ b/devchat/chatmark/chatmark_example/README.md @@ -0,0 +1,16 @@ +# chatmark_exmaple + +This is an example of how to use the chatmark module. + +Usage: + +1. Copy the `chatmark_example` folder under `~/.chat/workflow/org` +2. Create `command.yml` under `~/.chat/workflow/org/chatmark_example` with the following content: +```yaml +description: chatmark examples +steps: + - run: $command_python $command_path/main.py + +``` +3. Use the command `/chatmark_example` in devchat vscode plugin. + diff --git a/devchat/chatmark/chatmark_example/main.py b/devchat/chatmark/chatmark_example/main.py new file mode 100644 index 00000000..7204a61f --- /dev/null +++ b/devchat/chatmark/chatmark_example/main.py @@ -0,0 +1,163 @@ +import os +import sys +import time + +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "libs")) +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "libs")) + +from chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # noqa: E402 + + +def main(): + print("\n\n---\n\n") + + # Step + print("\n\n# Step Example\n\n") + with Step("Something is running..."): + print("Will sleep for 5 seconds...", flush=True) + time.sleep(5) + print("Done", flush=True) + + print("\n\n# Step Example with exception\n\n") + try: + with Step("Something is running (will raise exception)..."): + print("Will sleep for 5 seconds...", flush=True) + time.sleep(5) + raise Exception("oops!") + + except Exception: + pass + + # Button + print("\n\n# Button Example\n\n") + button = Button( + [ + "Yes", + "Or", + "No", + ], + ) + button.render() + + idx = button.clicked + print("\n\nButton result\n\n") + print(f"\n\n{idx}: {button.buttons[idx]}\n\n") + + print("\n\n---\n\n") + + # Checkbox + print("\n\n# Checkbox Example\n\n") + checkbox = Checkbox( + [ + "A", + "B", + "C", + "D", + ], + [True, False, False, True], + ) + checkbox.render() + + print(f"\n\ncheckbox.selections: {checkbox.selections}\n\n") + for idx in checkbox.selections: + print(f"\n\n{idx}: {checkbox.options[idx]}\n\n") + + print("\n\n---\n\n") + + # TextEditor + print("\n\n# TextEditor Example\n\n") + text_editor = TextEditor( + "hello world\nnice to meet you", + ) + + text_editor.render() + + print(f"\n\ntext_editor.new_text:\n\n{text_editor.new_text}\n\n") + + print("\n\n---\n\n") + + # Radio + print("\n\n# Radio Example\n\n") + radio = Radio( + [ + "Sun", + "Moon", + "Star", + ], + ) + radio.render() + + print(f"\n\nradio.selection: {radio.selection}\n\n") + if radio.selection is not None: + print(f"\n\nradio.options[radio.selection]: {radio.options[radio.selection]}\n\n") + + print("\n\n---\n\n") + + # Form + print("\n\n# Form Example\n\n") + checkbox_1 = Checkbox( + [ + "Sprint", + "Summer", + "Autumn", + "Winter", + ] + ) + checkbox_2 = Checkbox( + [ + "金", + "木", + "水", + "火", + "土", + ], + ) + radio_1 = Radio( + [ + "Up", + "Down", + ], + ) + radio_2 = Radio( + [ + "Left", + "Center", + "Right", + ], + ) + text_editor_1 = TextEditor( + "hello world\nnice to meet you", + ) + text_editor_2 = TextEditor( + "hihihihihi", + ) + + form = Form( + [ + "Some string in a form", + checkbox_1, + "Another string in a form", + radio_1, + "the third string in a form", + checkbox_2, + "the fourth string in a form", + radio_2, + "the fifth string in a form", + text_editor_1, + "the last string in a form", + text_editor_2, + ], + ) + + form.render() + + print(f"\n\ncheckbox_1.selections: {checkbox_1.selections}\n\n") + print(f"\n\ncheckbox_2.selections: {checkbox_2.selections}\n\n") + print(f"\n\nradio_1.selection: {radio_1.selection}\n\n") + print(f"\n\nradio_2.selection: {radio_2.selection}\n\n") + print(f"\n\ntext_editor_1.new_text:\n\n{text_editor_1.new_text}\n\n") + print(f"\n\ntext_editor_2.new_text:\n\n{text_editor_2.new_text}\n\n") + + +if __name__ == "__main__": + main() diff --git a/devchat/chatmark/form.py b/devchat/chatmark/form.py new file mode 100644 index 00000000..facbf62a --- /dev/null +++ b/devchat/chatmark/form.py @@ -0,0 +1,95 @@ +from typing import Dict, List, Optional, Union + +from .iobase import pipe_interaction +from .widgets import Button, Widget + + +class Form: + """ + A container for different widgets + + Syntax: + """ + + def __init__( + self, + components: List[Union[Widget, str]], + title: Optional[str] = None, + submit_button_name: Optional[str] = None, + cancel_button_name: Optional[str] = None, + ): + """ + components: components in the form, can be widgets (except Button) or strings + title: title of the form + """ + assert ( + any(isinstance(c, Button) for c in components) is False + ), "Button is not allowed in Form" + + self._components = components + self._title = title + + self._rendered = False + self._submit = submit_button_name + self._cancel = cancel_button_name + + @property + def components(self) -> List[Union[Widget, str]]: + """ + Return the components + """ + + return self._components + + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for all components + """ + lines = [] + + if self._title: + lines.append(self._title) + + for c in self.components: + if isinstance(c, str): + lines.append(c) + elif isinstance(c, Widget): + lines.append(c._in_chatmark()) + else: + raise ValueError(f"Invalid component {c}") + + return "\n".join(lines) + + def _parse_response(self, response: Dict): + """ + Parse response from user input + """ + for c in self.components: + if isinstance(c, Widget): + c._parse_response(response) + + def render(self): + """ + Render to receive user input + """ + if self._rendered: + # already rendered once + # not sure if the constraint is necessary + # could be removed if re-rendering is needed + raise RuntimeError("Widget can only be rendered once") + + self._rendered = True + + chatmark_header = "```chatmark" + chatmark_header += f" submit={self._submit}" if self._submit else "" + chatmark_header += f" cancel={self._cancel}" if self._cancel else "" + + lines = [ + chatmark_header, + self._in_chatmark(), + "```", + ] + + chatmark = "\n".join(lines) + response = pipe_interaction(chatmark) + self._parse_response(response) diff --git a/devchat/chatmark/iobase.py b/devchat/chatmark/iobase.py new file mode 100644 index 00000000..571e940e --- /dev/null +++ b/devchat/chatmark/iobase.py @@ -0,0 +1,43 @@ +import yaml + + +def _send_message(message): + out_data = f"""\n{message}\n""" + print(out_data, flush=True) + + +def _parse_chatmark_response(response): + # resonse looks like: + """ + ``` some_name + some key name 1: value1 + some key name 2: value2 + ``` + """ + # parse key values + lines = response.strip().split("\n") + if len(lines) <= 2: + return {} + + data = yaml.safe_load("\n".join(lines[1:-1])) + return data + + +def pipe_interaction(message: str): + _send_message(message) + + lines = [] + while True: + try: + line = input() + if line.strip().startswith("```yaml"): + lines = [] + elif line.strip() == "```": + lines.append(line) + break + lines.append(line) + except EOFError: + pass + + response = "\n".join(lines) + return _parse_chatmark_response(response) diff --git a/devchat/chatmark/step.py b/devchat/chatmark/step.py new file mode 100644 index 00000000..e81d8aad --- /dev/null +++ b/devchat/chatmark/step.py @@ -0,0 +1,28 @@ +from contextlib import AbstractContextManager, contextmanager + + +class Step(AbstractContextManager): + """ + Show a running step in the TUI. + + ChatMark syntax: + + ```Step + # Something is running... + some details... + ``` + + Usage: + with Step("Something is running..."): + print("some details...") + """ + + def __init__(self, title: str): + self.title = title + + def __enter__(self): + print(f"\n```Step\n# {self.title}", flush=True) + + def __exit__(self, exc_type, exc_val, exc_tb): + # close the step + print(f"\n```", flush=True) diff --git a/devchat/chatmark/widgets.py b/devchat/chatmark/widgets.py new file mode 100644 index 00000000..e1e99878 --- /dev/null +++ b/devchat/chatmark/widgets.py @@ -0,0 +1,397 @@ +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Tuple +from uuid import uuid4 + +from .iobase import pipe_interaction + + +class Widget(ABC): + """ + Abstract base class for widgets + """ + + def __init__(self, submit: Optional[str] = None, cancel: Optional[str] = None): + self._rendered = False + # Prefix for IDs/keys in the widget + self._id_prefix = self.gen_id_prefix() + self._submit = submit + self._cancel = cancel + + @abstractmethod + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for the widget + """ + pass + + @abstractmethod + def _parse_response(self, response: Dict) -> None: + """ + Parse ChatMark response from user input + """ + pass + + def render(self) -> None: + """ + Render the widget to receive user input + """ + if self._rendered: + # already rendered once + # not sure if the constraint is necessary + # could be removed if re-rendering is needed + raise RuntimeError("Widget can only be rendered once") + + self._rendered = True + + chatmark_header = "```chatmark" + chatmark_header += f" submit={self._submit}" if self._submit else "" + chatmark_header += f" cancel={self._cancel}" if self._cancel else "" + + lines = [ + chatmark_header, + self._in_chatmark(), + "```", + ] + + chatmark = "\n".join(lines) + response = pipe_interaction(chatmark) + self._parse_response(response) + + @staticmethod + def gen_id_prefix() -> str: + return uuid4().hex + + @staticmethod + def gen_id(id_prefix: str, index: int) -> str: + return f"{id_prefix}_{index}" + + @staticmethod + def parse_id(a_id: str) -> Tuple[Optional[str], Optional[int]]: + try: + id_prefix, index = a_id.split("_") + return id_prefix, int(index) + except Exception: + return None, None + + +class Checkbox(Widget): + """ + ChatMark syntax: + ```chatmark + Which files would you like to commit? I've suggested a few. + > [x](file1) devchat/engine/prompter.py + > [x](file2) devchat/prompt.py + > [](file3) tests/test_cli_prompt.py + ``` + + Response: + ```yaml + file1: checked + file3: checked + ``` + """ + + def __init__( + self, + options: List[str], + check_states: Optional[List[bool]] = None, + title: Optional[str] = None, + submit_button_name: str = "Submit", + cancel_button_name: str = "Cancel", + ): + """ + options: options to be selected + check_states: initial check states of options, default to all False + title: title of the widget + """ + super().__init__(submit_button_name, cancel_button_name) + + if check_states is not None: + assert len(options) == len(check_states) + else: + check_states = [False for _ in options] + + self._options = options + self._states = check_states + self._title = title + + self._selections: Optional[List[int]] = None + + @property + def selections(self) -> Optional[List[int]]: + """ + Get the indices of selected options + """ + return self._selections + + @property + def options(self) -> List[str]: + """ + Get the options + """ + return self._options + + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for checkbox options + Use the index of option to generate id/key + """ + lines = [] + + if self._title: + lines.append(self._title) + + for idx, (option, state) in enumerate(zip(self._options, self._states)): + mark = "[x]" if state else "[]" + key = self.gen_id(self._id_prefix, idx) + lines.append(f"> {mark}({key}) {option}") + + text = "\n".join(lines) + return text + + def _parse_response(self, response: Dict): + selections = [] + for key, value in response.items(): + prefix, index = self.parse_id(key) + # check if the prefix is the same as the widget's + if prefix != self._id_prefix: + continue + + if value == "checked": + selections.append(index) + + self._selections = selections + + +class TextEditor(Widget): + """ + ChatMark syntax: + ```chatmark + I've drafted a commit message for you as below. Feel free to modify it. + + > | (ID) + > fix: prevent racing of requests + > + > Introduce a request id and a reference to latest request. Dismiss + > incoming responses other than from latest request. + > + > Reviewed-by: Z + > Refs: #123 + ``` + + Response: + ```yaml + ID: | + fix: prevent racing of requests + + Introduce a request ID and a reference to latest request. Dismiss + incoming responses other than from latest request. + + Reviewed-by: Z + Refs: #123 + ``` + """ + + def __init__( + self, + text: str, + title: Optional[str] = None, + submit_button_name: str = "Submit", + cancel_button_name: str = "Cancel", + ): + super().__init__(submit_button_name, cancel_button_name) + + self._title = title + self._text = text + + self._editor_key = self.gen_id(self._id_prefix, 0) + self._new_text: Optional[str] = None + + @property + def new_text(self): + return self._new_text + + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for text editor + Use _editor_key as id + """ + lines = self._text.split("\n") + new_lines = [] + + if self._title: + new_lines.append(self._title) + + new_lines.append(f"> | ({self._editor_key})") + new_lines.extend([f"> {line}" for line in lines]) + + text = "\n".join(new_lines) + return text + + def _parse_response(self, response: Dict): + self._new_text = response.get(self._editor_key, None) + + +class Radio(Widget): + """ + ChatMark syntax: + ```chatmark + How would you like to make the change? + > - (insert) Insert the new code. + > - (new) Put the code in a new file. + > - (replace) Replace the current code. + ``` + + Reponse: + ```yaml + replace: checked + ``` + """ + + def __init__( + self, + options: List[str], + default_selected: Optional[int] = None, + title: Optional[str] = None, + submit_button_name: str = "Submit", + cancel_button_name: str = "Cancel", + ) -> None: + """ + options: options to be selected + default_selected: index of the option to be selected by default, default to None + title: title of the widget + """ + if default_selected is not None: + assert 0 <= default_selected < len(options) + + super().__init__(submit_button_name, cancel_button_name) + + self._options = options + self._title = title + + self._selection: Optional[int] = default_selected + + @property + def options(self) -> List[str]: + """ + Return the options + """ + return self._options + + @property + def selection(self) -> Optional[int]: + """ + Return the index of the selected option + """ + return self._selection + + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for options + Use the index of option to generate id/key + """ + lines = [] + + if self._title: + lines.append(self._title) + + for idx, option in enumerate(self._options): + key = self.gen_id(self._id_prefix, idx) + if self._selection is not None and self._selection == idx: + lines.append(f"> x ({key}) {option}") + else: + lines.append(f"> - ({key}) {option}") + + text = "\n".join(lines) + return text + + def _parse_response(self, response: Dict): + selected = None + for key, value in response.items(): + prefix, idx = self.parse_id(key) + # check if the prefix is the same as the widget's + if prefix != self._id_prefix: + continue + + if value == "checked": + selected = idx + break + + self._selection = selected + + +class Button(Widget): + """ + ChatMark syntax: + ```chatmark + Would you like to pay $0.02 for this LLM query? + > (Confirm) Yes, go ahead! + > (Cancel) No, let's skip this. + ``` + + ```yaml + Confirm: clicked + ``` + + # NOTE: almost the same as Radio essentially + """ + + def __init__( + self, + buttons: List[str], + title: Optional[str] = None, + ) -> None: + """ + buttons: button names to show + title: title of the widget + """ + super().__init__() + + self._buttons = buttons + self._title = title + + self._clicked: Optional[int] = None + + @property + def clicked(self) -> Optional[int]: + """ + Return the index of the clicked button + """ + return self._clicked + + @property + def buttons(self) -> List[str]: + """ + Return the buttons + """ + return self._buttons + + def _in_chatmark(self) -> str: + """ + Generate ChatMark syntax for options + Use the index of button to generate id/key + """ + lines = [] + + if self._title: + lines.append(self._title) + + for idx, button in enumerate(self._buttons): + key = self.gen_id(self._id_prefix, idx) + lines.append(f"> ({key}) {button}") + + text = "\n".join(lines) + return text + + def _parse_response(self, response: Dict[str, str]): + clicked = None + for key, value in response.items(): + prefix, idx = self.parse_id(key) + # check if the prefix is the same as the widget's + if prefix != self._id_prefix: + continue + + if value == "clicked": + clicked = idx + break + self._clicked = clicked diff --git a/devchat/ide/__init__.py b/devchat/ide/__init__.py new file mode 100644 index 00000000..dbfd10ad --- /dev/null +++ b/devchat/ide/__init__.py @@ -0,0 +1,5 @@ +from .service import IDEService + +__all__ = [ + "IDEService", +] diff --git a/devchat/ide/rpc.py b/devchat/ide/rpc.py new file mode 100644 index 00000000..eced9c3d --- /dev/null +++ b/devchat/ide/rpc.py @@ -0,0 +1,37 @@ +import os +from functools import wraps + +import requests + +BASE_SERVER_URL = os.environ.get("DEVCHAT_IDE_SERVICE_URL", "http://localhost:3000") + + +def rpc_call(f): + @wraps(f) + def wrapper(*args, **kwargs): + if os.environ.get("DEVCHAT_IDE_SERVICE_URL", "") == "": + # maybe in a test, user don't want to mock services functions + return + + try: + function_name = f.__name__ + url = f"{BASE_SERVER_URL}/{function_name}" + + data = dict(zip(f.__code__.co_varnames, args)) + data.update(kwargs) + headers = {"Content-Type": "application/json"} + + response = requests.post(url, json=data, headers=headers) + + if response.status_code != 200: + raise Exception(f"Server error: {response.status_code}") + + response_data = response.json() + if "error" in response_data: + raise Exception(f"Server returned an error: {response_data['error']}") + return response_data.get("result", None) + except ConnectionError as err: + # TODO + raise err + + return wrapper diff --git a/devchat/ide/service.py b/devchat/ide/service.py new file mode 100644 index 00000000..c9652317 --- /dev/null +++ b/devchat/ide/service.py @@ -0,0 +1,93 @@ +import os +from functools import wraps +from typing import List + +import requests + +from .types import Location, SymbolNode + +BASE_SERVER_URL = os.environ.get("DEVCHAT_IDE_SERVICE_URL", "http://localhost:3000") + + +def rpc_method(f): + """ + Decorator for Service methods + """ + + @wraps(f) + def wrapper(self, *args, **kwargs): + if os.environ.get("DEVCHAT_IDE_SERVICE_URL", "") == "": + # maybe in a test, user don't want to mock services functions + return + + try: + function_name = f.__name__ + url = f"{BASE_SERVER_URL}/{function_name}" + + data = dict(zip(f.__code__.co_varnames[1:], args)) # Exclude "self" + data.update(kwargs) + headers = {"Content-Type": "application/json"} + + response = requests.post(url, json=data, headers=headers) + + if response.status_code != 200: + raise Exception(f"Server error: {response.status_code}") + + response_data = response.json() + if "error" in response_data: + raise Exception(f"Server returned an error: {response_data['error']}") + + # Store the result in the _result attribute of the instance + self._result = response_data.get("result", None) + return f(self, *args, **kwargs) + + except ConnectionError as err: + # TODO + raise err + + return wrapper + + +class IDEService: + """ + Client for IDE service + + Usage: + client = IDEService() + res = client.ide_language() + res = client.ide_logging("info", "some message") + """ + + def __init__(self): + self._result = None + + @rpc_method + def get_lsp_brige_port(self) -> str: + return self._result + + @rpc_method + def install_python_env(self, command_name: str, requirements_file: str) -> str: + return self._result + + @rpc_method + def update_slash_commands(self) -> bool: + return self._result + + @rpc_method + def ide_language(self) -> str: + return self._result + + @rpc_method + def ide_logging(self, level: str, message: str) -> bool: + """ + level: "info" | "warn" | "error" | "debug" + """ + return self._result + + @rpc_method + def get_document_symbols(self, abspath: str) -> List[SymbolNode]: + return [SymbolNode.parse_obj(node) for node in self._result] + + @rpc_method + def find_type_def_locations(self, abspath: str, line: int, character: int) -> List[Location]: + return [Location.parse_obj(loc) for loc in self._result] diff --git a/devchat/ide/types.py b/devchat/ide/types.py new file mode 100644 index 00000000..dbd7ed46 --- /dev/null +++ b/devchat/ide/types.py @@ -0,0 +1,25 @@ +from typing import List + +from pydantic import BaseModel + + +class Position(BaseModel): + line: int # 0-based + character: int # 0-based + + +class Range(BaseModel): + start: Position + end: Position + + +class Location(BaseModel): + abspath: str + range: Range + + +class SymbolNode(BaseModel): + name: str + kind: str + range: Range + children: List["SymbolNode"] diff --git a/devchat/ide/vscode_services.py b/devchat/ide/vscode_services.py new file mode 100644 index 00000000..c57dfac2 --- /dev/null +++ b/devchat/ide/vscode_services.py @@ -0,0 +1,143 @@ +import os +import sys + +from .rpc import rpc_call + + +@rpc_call +def run_code(code: str): + pass + + +@rpc_call +def diff_apply(filepath, content): + pass + + +@rpc_call +def get_symbol_defines_in_selected_code(): + pass + + +def find_symbol(command, abspath, line, col): + code = ( + f"const position = new vscode.Position({line}, {col});" + f"const absPath = vscode.Uri.file('{abspath}');" + f"return await vscode.commands.executeCommand('{command}', absPath, position);" + ) + result = run_code(code=code) + return result + + +def find_definition(abspath: str, line: int, col: int): + return find_symbol("vscode.executeDefinitionProvider", abspath, line, col) + + +def find_type_definition(abspath: str, line: int, col: int): + return find_symbol("vscode.executeTypeDefinitionProvider", abspath, line, col) + + +def find_declaration(abspath: str, line: int, col: int): + return find_symbol("vscode.executeDeclarationProvider", abspath, line, col) + + +def find_implementation(abspath: str, line: int, col: int): + return find_symbol("vscode.executeImplementationProvider", abspath, line, col) + + +def find_reference(abspath: str, line: int, col: int): + return find_symbol("vscode.executeReferenceProvider", abspath, line, col) + + +def document_symbols(abspath: str): + code = ( + f"const fileUri = vscode.Uri.file('{abspath}');" + "return await vscode.commands.executeCommand(" + "'vscode.executeDocumentSymbolProvider', fileUri);" + ) + symbols = run_code(code=code) + return symbols + + +def workspace_symbols(query: str): + code = ( + "return await vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider'," + f" '{query}');" + ) + return run_code(code=code) + + +def active_text_editor(): + code = "return vscode.window.activeTextEditor;" + return run_code(code=code) + + +def open_folder(folder: str): + folder = folder.replace("\\", "/") + code = ( + f"const folderUri = vscode.Uri.file('{folder}');" + "vscode.commands.executeCommand(`vscode.openFolder`, folderUri);" + ) + run_code(code=code) + + +def visible_lines(): + active_document = active_text_editor() + fail_result = { + "filePath": "", + "visibleText": "", + "visibleRange": [-1, -1], + } + + if not active_document: + return fail_result + if not os.path.exists(active_document["document"]["uri"]["fsPath"]): + return fail_result + + file_path = active_document["document"]["uri"]["fsPath"] + start_line = active_document["visibleRanges"][0][0]["line"] + end_line = active_document["visibleRanges"][0][1]["line"] + + # read file lines from start_line to end_line + with open(file_path, "r") as file: + lines = file.readlines() + selected_lines = lines[start_line : end_line + 1] + + # continue with the rest of the function + return { + "filePath": file_path, + "visibleText": "".join(selected_lines), + "visibleRange": [start_line, end_line], + } + + +def selected_lines(): + active_document = active_text_editor() + fail_result = { + "filePath": "", + "selectedText": "", + "selectedRange": [-1, -1, -1, -1], + } + + if not active_document: + return fail_result + if not os.path.exists(active_document["document"]["uri"]["fsPath"]): + return fail_result + + file_path = active_document["document"]["uri"]["fsPath"] + start_line = active_document["selection"]["start"]["line"] + start_col = active_document["selection"]["start"]["character"] + end_line = active_document["selection"]["end"]["line"] + end_col = active_document["selection"]["end"]["character"] + + # read file lines from start_line to end_line + with open(file_path, "r") as file: + lines = file.readlines() + selected_lines = lines[start_line : end_line + 1] + + # continue with the rest of the function + return { + "filePath": "", + "selectedText": "".join(selected_lines), + "selectedRange": [start_line, start_col, end_line, end_col], + } diff --git a/devchat/llm/__init__.py b/devchat/llm/__init__.py new file mode 100644 index 00000000..2b63face --- /dev/null +++ b/devchat/llm/__init__.py @@ -0,0 +1,19 @@ +from .chat import chat, chat_json +from .memory.base import ChatMemory +from .memory.fixsize_memory import FixSizeChatMemory +from .openai import chat_completion_no_stream_return_json, chat_completion_stream +from .text_confirm import llm_edit_confirm +from .tools_call import chat_tools, llm_func, llm_param + +__all__ = [ + "chat_completion_stream", + "chat_completion_no_stream_return_json", + "chat_json", + "chat", + "llm_edit_confirm", + "llm_func", + "llm_param", + "chat_tools", + "ChatMemory", + "FixSizeChatMemory", +] diff --git a/devchat/llm/chat.py b/devchat/llm/chat.py new file mode 100644 index 00000000..31718992 --- /dev/null +++ b/devchat/llm/chat.py @@ -0,0 +1,109 @@ +import json +import os +import sys +from functools import wraps + +import openai + +from .memory.base import ChatMemory +from .openai import ( + chat_completion_no_stream_return_json, + chat_completion_stream, + chat_completion_stream_commit, + chunks_content, + retry_timeout, + stream_out_chunk, + to_dict_content_and_call, +) +from .pipeline import exception_handle, pipeline, retry + +chat_completion_stream_out = exception_handle( + retry( + pipeline( + chat_completion_stream_commit, + retry_timeout, + stream_out_chunk, + chunks_content, + to_dict_content_and_call, + ), + times=3, + ), + lambda err: { + "content": None, + "function_name": None, + "parameters": "", + "error": err.type if isinstance(err, openai.APIError) else err, + }, +) + + +def chat( + prompt, + memory: ChatMemory = None, + stream_out: bool = False, + model: str = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106"), + **llm_config, +): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal prompt, memory, model, llm_config + prompt = prompt.format(**kwargs) + messages = memory.contexts() if memory else [] + if not any(item["content"] == prompt for item in messages) and prompt: + messages.append({"role": "user", "content": prompt}) + if "__user_request__" in kwargs: + messages.append(kwargs["__user_request__"]) + del kwargs["__user_request__"] + + llm_config["model"] = model + if not stream_out: + response = chat_completion_stream(messages, llm_config=llm_config) + else: + response = chat_completion_stream_out(messages, llm_config=llm_config) + if not response.get("content", None): + print(f"call {func.__name__} failed:", response["error"], file=sys.stderr) + return None + + if memory: + memory.append( + {"role": "user", "content": prompt}, + {"role": "assistant", "content": response["content"]}, + ) + return response["content"] + + return wrapper + + return decorator + + +def chat_json( + prompt, + memory: ChatMemory = None, + model: str = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106"), + **llm_config, +): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal prompt, memory, model, llm_config + prompt = prompt.format(**kwargs) + messages = memory.contexts() if memory else [] + if not any(item["content"] == prompt for item in messages): + messages.append({"role": "user", "content": prompt}) + + llm_config["model"] = model + response = chat_completion_no_stream_return_json(messages, llm_config=llm_config) + if not response: + print(f"call {func.__name__} failed.", file=sys.stderr) + + if memory: + memory.append( + {"role": "user", "content": prompt}, + {"role": "assistant", "content": json.dumps(response)}, + ) + return response + + return wrapper + + return decorator diff --git a/devchat/llm/openai.py b/devchat/llm/openai.py new file mode 100644 index 00000000..e732537b --- /dev/null +++ b/devchat/llm/openai.py @@ -0,0 +1,197 @@ +# flake8: noqa: E402 +import json +import os +import re +import sys +from functools import wraps +from typing import Dict, List, Optional + +import openai + +from .pipeline import ( + RetryException, + exception_err, + exception_handle, + exception_output_handle, + parallel, + pipeline, + retry, +) + + +def _try_remove_markdown_block_flag(content): + """ + 如果content是一个markdown块,则删除它的头部```xxx和尾部``` + """ + # 定义正则表达式模式,用于匹配markdown块的头部和尾部 + pattern = r"^\s*```\s*(\w+)\s*\n(.*?)\n\s*```\s*$" + + # 使用re模块进行匹配 + match = re.search(pattern, content, re.DOTALL | re.MULTILINE) + + if match: + # 如果匹配成功,则提取出markdown块的内容并返回 + _ = match.group(1) # language + markdown_content = match.group(2) + return markdown_content.strip() + else: + # 如果匹配失败,则返回原始内容 + return content + + +def chat_completion_stream_commit( + messages: List[Dict], # [{"role": "user", "content": "hello"}] + llm_config: Dict, # {"model": "...", ...} +): + client = openai.OpenAI( + api_key=os.environ.get("OPENAI_API_KEY", None), + base_url=os.environ.get("OPENAI_API_BASE", None), + ) + + llm_config["stream"] = True + llm_config["timeout"] = 60 + return client.chat.completions.create(messages=messages, **llm_config) + + +def chat_completion_stream_raw(**kwargs): + client = openai.OpenAI( + api_key=os.environ.get("OPENAI_API_KEY", None), + base_url=os.environ.get("OPENAI_API_BASE", None), + ) + + kwargs["stream"] = True + kwargs["timeout"] = 60 + return client.chat.completions.create(**kwargs) + + +def stream_out_chunk(chunks): + for chunk in chunks: + chunk_dict = chunk.dict() + delta = chunk_dict["choices"][0]["delta"] + if delta.get("content", None): + print(delta["content"], end="", flush=True) + yield chunk + + +def retry_timeout(chunks): + try: + for chunk in chunks: + yield chunk + except (openai.APIConnectionError, openai.APITimeoutError) as err: + raise RetryException(err) from err + + +def chunk_list(chunks): + return [chunk for chunk in chunks] + + +def chunks_content(chunks): + content = None + for chunk in chunks: + chunk_dict = chunk.dict() + delta = chunk_dict["choices"][0]["delta"] + if delta.get("content", None): + if content is None: + content = "" + content += delta["content"] + return content + + +def chunks_call(chunks): + tool_calls = [] + + for chunk in chunks: + chunk = chunk.dict() + delta = chunk["choices"][0]["delta"] + if "tool_calls" in delta and delta["tool_calls"]: + tool_call = delta["tool_calls"][0]["function"] + if delta["tool_calls"][0].get("index", None) is not None: + index = delta["tool_calls"][0]["index"] + if index >= len(tool_calls): + tool_calls.append({"name": None, "arguments": ""}) + if tool_call.get("name", None): + tool_calls[-1]["name"] = tool_call["name"] + if tool_call.get("arguments", None): + tool_calls[-1]["arguments"] += tool_call["arguments"] + return tool_calls + + +def content_to_json(content): + try: + # json will format as ```json ... ``` in 1106 model + response_content = _try_remove_markdown_block_flag(content) + response_obj = json.loads(response_content) + return response_obj + except json.JSONDecodeError as err: + raise RetryException(err) from err + except Exception as err: + raise err + + +def to_dict_content_and_call(content, tool_calls=[]): + return { + "content": content, + "function_name": tool_calls[0]["name"] if tool_calls else None, + "parameters": tool_calls[0]["arguments"] if tool_calls else "", + "tool_calls": tool_calls, + } + + +chat_completion_content = retry( + pipeline(chat_completion_stream_commit, retry_timeout, chunks_content), times=3 +) + +chat_completion_stream_content = retry( + pipeline(chat_completion_stream_commit, retry_timeout, stream_out_chunk, chunks_content), + times=3, +) + +chat_completion_call = retry( + pipeline(chat_completion_stream_commit, retry_timeout, chunks_call), times=3 +) + +chat_completion_no_stream_return_json = exception_handle( + retry( + pipeline(chat_completion_stream_commit, retry_timeout, chunks_content, content_to_json), + times=3, + ), + exception_output_handle(lambda err: None), +) + +chat_completion_stream = exception_handle( + retry( + pipeline( + chat_completion_stream_commit, + retry_timeout, + chunks_content, + to_dict_content_and_call, + ), + times=3, + ), + lambda err: { + "content": None, + "function_name": None, + "parameters": "", + "error": err.type if isinstance(err, openai.APIError) else err, + }, +) + +chat_call_completion_stream = exception_handle( + retry( + pipeline( + chat_completion_stream_commit, + retry_timeout, + chunk_list, + parallel(chunks_content, chunks_call), + to_dict_content_and_call, + ), + times=3, + ), + lambda err: { + "content": None, + "function_name": None, + "parameters": "", + "tool_calls": [], + "error": err.type if isinstance(err, openai.APIError) else err, + }, +) diff --git a/devchat/llm/pipeline.py b/devchat/llm/pipeline.py new file mode 100644 index 00000000..4df5a898 --- /dev/null +++ b/devchat/llm/pipeline.py @@ -0,0 +1,94 @@ +import sys +from typing import Dict + +import openai + + +class RetryException(Exception): + def __init__(self, err): + self.error = err + + +def retry(func, times): + def wrapper(*args, **kwargs): + for index in range(times): + try: + return func(*args, **kwargs) + except RetryException as err: + if index + 1 == times: + raise err.error + continue + except Exception as err: + raise err + raise err.error + + return wrapper + + +def exception_err(func): + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + return True, result + except Exception as err: + return False, err + + return wrapper + + +def exception_output_handle(func): + def wrapper(err): + if isinstance(err, openai.APIError): + print(err.type, file=sys.stderr, flush=True) + else: + print(err, file=sys.stderr, flush=True) + return func(err) + + return wrapper + + +def exception_output_handle(func): + def wrapper(err): + if isinstance(err, openai.APIError): + print(err.type, file=sys.stderr, flush=True) + else: + print(err, file=sys.stderr, flush=True) + return func(err) + + return wrapper + + +def exception_handle(func, handler): + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + return result + except Exception as err: + return handler(err) + + return wrapper + + +def pipeline(*funcs): + def wrapper(*args, **kwargs): + for index, func in enumerate(funcs): + if index > 0: + if isinstance(args, Dict) and args.get("__type__", None) == "parallel": + args = func(*args["value"]) + else: + args = func(args) + else: + args = func(*args, **kwargs) + return args + + return wrapper + + +def parallel(*funcs): + def wrapper(args): + results = {"__type__": "parallel", "value": []} + for func in funcs: + results["value"].append(func(args)) + return results + + return wrapper diff --git a/devchat/llm/text_confirm.py b/devchat/llm/text_confirm.py new file mode 100644 index 00000000..c8722cac --- /dev/null +++ b/devchat/llm/text_confirm.py @@ -0,0 +1,57 @@ +# flake8: noqa: E402 +import os +import sys +from functools import wraps + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + + +from chatmark import Checkbox, Form, TextEditor # noqa: #402 + + +class MissEditConfirmFieldException(Exception): + pass + + +def edit_confirm(response): + need_regenerate = Checkbox(["Need Regenerate"]) + edit_text = TextEditor(response) + feedback_text = TextEditor("") + confirmation_form = Form( + [ + "Edit AI Response:", + edit_text, + "Need Regenerate?", + need_regenerate, + "Feedback if Regenerate:", + feedback_text, + ] + ) + confirmation_form.render() + if len(need_regenerate.selections) > 0: + return True, feedback_text.new_text + return False, edit_text.new_text + + +def llm_edit_confirm(edit_confirm_fun=edit_confirm): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal edit_confirm_fun + if not edit_confirm_fun: + raise MissEditConfirmFieldException() + + while True: + response = func(*args, **kwargs) + if not response: + return response + + do_regenerate, new_response = edit_confirm_fun(response) + if do_regenerate: + kwargs["__user_request__"] = {"role": "user", "content": new_response} + else: + return new_response if new_response else response + + return wrapper + + return decorator diff --git a/devchat/llm/tools_call.py b/devchat/llm/tools_call.py new file mode 100644 index 00000000..e271fc29 --- /dev/null +++ b/devchat/llm/tools_call.py @@ -0,0 +1,220 @@ +import json +import os +import sys +from functools import wraps + +from .memory.base import ChatMemory +from .openai import chat_call_completion_stream + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from chatmark import Checkbox, Form, Radio, TextEditor # noqa: #402 +from ide_services import IDEService # noqa: #402 + + +class MissToolsFieldException(Exception): + pass + + +def openai_tool_schema(name, description, parameters, required): + return { + "type": "function", + "function": { + "name": name, + "description": description, + "parameters": {"type": "object", "properties": parameters, "required": required}, + }, + } + + +def openai_function_schema(name, description, properties, required): + return { + "name": name, + "description": description, + "parameters": {"type": "object", "properties": properties, "required": required}, + } + + +def llm_func(name, description, schema_fun=openai_tool_schema): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + if not hasattr(func, "llm_metadata"): + func.llm_metadata = {"properties": {}, "required": []} + + wrapper.function_name = name + wrapper.json_schema = lambda: schema_fun( + name, + description, + func.llm_metadata.get("properties", {}), + func.llm_metadata.get("required", []), + ) + return wrapper + + return decorator + + +def llm_param(name, description, dtype, **kwargs): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + if hasattr(func, "llm_metadata"): + wrapper.llm_metadata = func.llm_metadata + else: + wrapper.llm_metadata = {"properties": {}, "required": []} + + wrapper.llm_metadata["properties"][name] = { + "type": dtype, + "description": description, + **kwargs, # Add any additional keyword arguments + } + wrapper.llm_metadata["required"].append(name) + + return wrapper + + return decorator + + +def call_confirm(response): + """ + Prompt the user to confirm if a function call should be allowed. + + This function is responsible for asking the user to confirm whether the AI's + intention to call a function is permissible. It prints out the response content + and the details of the function calls that the AI intends to make. The user is + then presented with a choice to either allow or deny the function call. + + Parameters: + response (dict): A dictionary containing the 'content' and 'all_calls' keys. + 'content' is a string representing the AI's response, and + 'all_calls' is a list of dictionaries, each representing a + function call with 'function_name' and 'parameters' keys. + + Returns: + tuple: A tuple containing a boolean and a string. The boolean indicates whether + the function call is allowed (True) or not (False). The string contains + additional input from the user if the function call is not allowed. + """ + + def display_response_and_calls(response): + if response["content"]: + print(f"AI Response: {response['content']}", end="\n\n", flush=True) + print("Function Call Requests:", end="\n\n", flush=True) + for call_request in response["all_calls"]: + print( + f"Function: {call_request['function_name']}, " + f"Parameters: {call_request['parameters']}", + end="\n\n", + flush=True, + ) + + def prompt_user_confirmation(): + function_call_radio = Radio(["Allow function call", "Block function call"]) + user_feedback_input = TextEditor("") + confirmation_form = Form( + [ + "Permission to proceed with function call?", + function_call_radio, + "Provide feedback if blocked:", + user_feedback_input, + ] + ) + confirmation_form.render() + user_allowed_call = function_call_radio.selection == 0 + user_feedback = user_feedback_input.new_text + return user_allowed_call, user_feedback + + display_response_and_calls(response) + return prompt_user_confirmation() + + +def chat_tools( + prompt, + memory: ChatMemory = None, + model: str = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106"), + tools=None, + call_confirm_fun=call_confirm, + **llm_config, +): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal prompt, memory, model, tools, call_confirm_fun, llm_config + prompt = prompt.format(**kwargs) + if not tools: + raise MissToolsFieldException() + + messages = memory.contexts() if memory else [] + if not any(item["content"] == prompt for item in messages): + messages.append({"role": "user", "content": prompt}) + + tool_schemas = [fun.json_schema() for fun in tools] if tools else [] + + llm_config["model"] = model + llm_config["tools"] = tool_schemas + + user_request = {"role": "user", "content": prompt} + while True: + response = chat_call_completion_stream(messages, llm_config=llm_config) + if not response.get("content", None) and not response.get("function_name", None): + print(f"call {func.__name__} failed:", response["error"], file=sys.stderr) + return response + + response_content = ( + f"{response.get('content', '') or ''}\n\n" + f"call function {response.get('function_name', '')} with arguments:" + f"{response.get('parameters', '')}" + ) + if memory: + memory.append(user_request, {"role": "assistant", "content": response_content}) + messages.append({"role": "assistant", "content": response_content}) + + if not response.get("function_name", None): + return response + if not response.get("all_calls", None): + response["all_calls"] = [ + { + "function_name": response["function_name"], + "parameters": response["parameters"], + } + ] + + do_call = True + if call_confirm_fun: + do_call, fix_prompt = call_confirm_fun(response) + + if do_call: + # call function + functions = {tool.function_name: tool for tool in tools} + for call in response["all_calls"]: + IDEService().ide_logging( + "info", + f"try to call function tool: {call['function_name']} " + f"with {call['parameters']}", + ) + tool = functions[call["function_name"]] + result = tool(**json.loads(call["parameters"])) + messages.append( + { + "role": "function", + "content": f"function has called, this is the result: {result}", + "name": call["function_name"], + } + ) + user_request = { + "role": "function", + "content": f"function has called, this is the result: {result}", + "name": call["function_name"], + } + else: + # update prompt + messages.append({"role": "user", "content": fix_prompt}) + user_request = {"role": "user", "content": fix_prompt} + + return wrapper + + return decorator diff --git a/devchat/memory/__init__.py b/devchat/memory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/devchat/memory/base.py b/devchat/memory/base.py new file mode 100644 index 00000000..7d1f3104 --- /dev/null +++ b/devchat/memory/base.py @@ -0,0 +1,32 @@ +class ChatMemory: + """ + ChatMemory is the base class for all chat memory classes. + """ + + def __init__(self): + pass + + def append(self, request, response): + """ + Append a request and response to the memory. + """ + # it must implemented in sub class + pass + + def append_request(self, request): + """ + Append a request to the memory. + """ + pass + + def append_response(self, response): + """ + Append a request to the memory. + """ + pass + + def contexts(self): + """ + Return the contexts of the memory. + """ + pass diff --git a/devchat/memory/fixsize_memory.py b/devchat/memory/fixsize_memory.py new file mode 100644 index 00000000..36e9c2ed --- /dev/null +++ b/devchat/memory/fixsize_memory.py @@ -0,0 +1,51 @@ +from .base import ChatMemory + + +class FixSizeChatMemory(ChatMemory): + """ + FixSizeChatMemory is a memory class that stores up + to a fixed number of requests and responses. + """ + + def __init__(self, max_size: int = 5, messages=[], system_prompt=None): + """ + init the memory + """ + super().__init__() + self._max_size = max_size + # store last max_size messages + self._messages = messages[-max_size * 2 :] + self._system_prompt = system_prompt + + def append(self, request, response): + """ + Append a request and response to the memory. + """ + self._messages.append(request) + self._messages.append(response) + if len(self._messages) > self._max_size * 2: + self._messages = self._messages[-self._max_size * 2 :] + + def append_request(self, request): + """ + Append a request to the memory. + """ + self._messages.append(request) + + def append_response(self, response): + """ + Append a response to the memory. + """ + self._messages.append(response) + if len(self._messages) > self._max_size * 2: + self._messages = self._messages[-self._max_size * 2 :] + + def contexts(self): + """ + Return the contexts of the memory. + """ + messages = self._messages.copy() + # insert system prompt at the beginning + if self._system_prompt: + messages = [{"role": "system", "content": self._system_prompt}] + messages + return messages From 4632aea0eb58384eb56a20d42549459f8fd3b8bf Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Wed, 7 Feb 2024 11:34:13 +0800 Subject: [PATCH 2/5] refactor: streamline import paths and cleanup - Updated import paths to use full paths for better clarity - Removed redundant sys.path.append calls throughout the codebase - Consolidated memory-related imports into devchat/memory module --- devchat/chatmark/chatmark_example/main.py | 6 +----- devchat/llm/__init__.py | 4 ---- devchat/llm/chat.py | 3 ++- devchat/llm/openai.py | 5 +---- devchat/llm/pipeline.py | 11 ----------- devchat/llm/text_confirm.py | 7 +------ devchat/llm/tools_call.py | 10 ++++------ devchat/memory/__init__.py | 8 ++++++++ 8 files changed, 17 insertions(+), 37 deletions(-) diff --git a/devchat/chatmark/chatmark_example/main.py b/devchat/chatmark/chatmark_example/main.py index 7204a61f..d8eb7bdc 100644 --- a/devchat/chatmark/chatmark_example/main.py +++ b/devchat/chatmark/chatmark_example/main.py @@ -1,11 +1,7 @@ -import os -import sys import time -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "libs")) -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "libs")) -from chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # noqa: E402 +from devchat.chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # noqa: E402 def main(): diff --git a/devchat/llm/__init__.py b/devchat/llm/__init__.py index 2b63face..22e36837 100644 --- a/devchat/llm/__init__.py +++ b/devchat/llm/__init__.py @@ -1,6 +1,4 @@ from .chat import chat, chat_json -from .memory.base import ChatMemory -from .memory.fixsize_memory import FixSizeChatMemory from .openai import chat_completion_no_stream_return_json, chat_completion_stream from .text_confirm import llm_edit_confirm from .tools_call import chat_tools, llm_func, llm_param @@ -14,6 +12,4 @@ "llm_func", "llm_param", "chat_tools", - "ChatMemory", - "FixSizeChatMemory", ] diff --git a/devchat/llm/chat.py b/devchat/llm/chat.py index 31718992..f1bbcf77 100644 --- a/devchat/llm/chat.py +++ b/devchat/llm/chat.py @@ -5,7 +5,8 @@ import openai -from .memory.base import ChatMemory +from devchat.memory import ChatMemory + from .openai import ( chat_completion_no_stream_return_json, chat_completion_stream, diff --git a/devchat/llm/openai.py b/devchat/llm/openai.py index e732537b..1cb3a97b 100644 --- a/devchat/llm/openai.py +++ b/devchat/llm/openai.py @@ -2,15 +2,12 @@ import json import os import re -import sys -from functools import wraps -from typing import Dict, List, Optional +from typing import Dict, List import openai from .pipeline import ( RetryException, - exception_err, exception_handle, exception_output_handle, parallel, diff --git a/devchat/llm/pipeline.py b/devchat/llm/pipeline.py index 4df5a898..a4b9e5f8 100644 --- a/devchat/llm/pipeline.py +++ b/devchat/llm/pipeline.py @@ -47,17 +47,6 @@ def wrapper(err): return wrapper -def exception_output_handle(func): - def wrapper(err): - if isinstance(err, openai.APIError): - print(err.type, file=sys.stderr, flush=True) - else: - print(err, file=sys.stderr, flush=True) - return func(err) - - return wrapper - - def exception_handle(func, handler): def wrapper(*args, **kwargs): try: diff --git a/devchat/llm/text_confirm.py b/devchat/llm/text_confirm.py index c8722cac..7d877b5e 100644 --- a/devchat/llm/text_confirm.py +++ b/devchat/llm/text_confirm.py @@ -1,12 +1,7 @@ # flake8: noqa: E402 -import os -import sys from functools import wraps -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - - -from chatmark import Checkbox, Form, TextEditor # noqa: #402 +from devchat.chatmark import Checkbox, Form, TextEditor class MissEditConfirmFieldException(Exception): diff --git a/devchat/llm/tools_call.py b/devchat/llm/tools_call.py index e271fc29..49d267d4 100644 --- a/devchat/llm/tools_call.py +++ b/devchat/llm/tools_call.py @@ -3,13 +3,11 @@ import sys from functools import wraps -from .memory.base import ChatMemory -from .openai import chat_call_completion_stream - -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +from devchat.memory import ChatMemory +from devchat.chatmark import Form, Radio, TextEditor +from devchat.ide import IDEService -from chatmark import Checkbox, Form, Radio, TextEditor # noqa: #402 -from ide_services import IDEService # noqa: #402 +from .openai import chat_call_completion_stream class MissToolsFieldException(Exception): diff --git a/devchat/memory/__init__.py b/devchat/memory/__init__.py index e69de29b..4a5905cc 100644 --- a/devchat/memory/__init__.py +++ b/devchat/memory/__init__.py @@ -0,0 +1,8 @@ + +from .base import ChatMemory +from .fixsize_memory import FixSizeChatMemory + +__all__ = [ + "ChatMemory", + "FixSizeChatMemory", +] From 271a8d7db570b9bf24d84abccfb7eae6796c37fc Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Wed, 7 Feb 2024 16:00:26 +0800 Subject: [PATCH 3/5] Fix linting issues and remove unused code --- devchat/chatmark/form.py | 2 ++ devchat/chatmark/step.py | 4 ++-- devchat/chatmark/widgets.py | 2 -- devchat/ide/rpc.py | 4 ++++ devchat/ide/service.py | 9 +++++++++ devchat/ide/vscode_services.py | 21 ++++++++++----------- devchat/llm/chat.py | 4 ++-- devchat/llm/openai.py | 11 ++++++----- devchat/llm/pipeline.py | 4 ++-- devchat/llm/tools_call.py | 2 +- devchat/memory/base.py | 4 ---- devchat/memory/fixsize_memory.py | 5 ++++- 12 files changed, 42 insertions(+), 30 deletions(-) diff --git a/devchat/chatmark/form.py b/devchat/chatmark/form.py index facbf62a..8324f872 100644 --- a/devchat/chatmark/form.py +++ b/devchat/chatmark/form.py @@ -1,3 +1,5 @@ +# noqa: C0103 +# noqa: W0212 from typing import Dict, List, Optional, Union from .iobase import pipe_interaction diff --git a/devchat/chatmark/step.py b/devchat/chatmark/step.py index e81d8aad..9e0a8c49 100644 --- a/devchat/chatmark/step.py +++ b/devchat/chatmark/step.py @@ -1,4 +1,4 @@ -from contextlib import AbstractContextManager, contextmanager +from contextlib import AbstractContextManager class Step(AbstractContextManager): @@ -25,4 +25,4 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): # close the step - print(f"\n```", flush=True) + print("\n```", flush=True) diff --git a/devchat/chatmark/widgets.py b/devchat/chatmark/widgets.py index e1e99878..ac04f103 100644 --- a/devchat/chatmark/widgets.py +++ b/devchat/chatmark/widgets.py @@ -22,14 +22,12 @@ def _in_chatmark(self) -> str: """ Generate ChatMark syntax for the widget """ - pass @abstractmethod def _parse_response(self, response: Dict) -> None: """ Parse ChatMark response from user input """ - pass def render(self) -> None: """ diff --git a/devchat/ide/rpc.py b/devchat/ide/rpc.py index eced9c3d..cd5ef67b 100644 --- a/devchat/ide/rpc.py +++ b/devchat/ide/rpc.py @@ -1,3 +1,7 @@ +# noqa: C0103 +# noqa: W3101 +# noqa: W0719 +# noqa: R1710 import os from functools import wraps diff --git a/devchat/ide/service.py b/devchat/ide/service.py index c9652317..b929547d 100644 --- a/devchat/ide/service.py +++ b/devchat/ide/service.py @@ -1,3 +1,12 @@ +# disable pylint +# noqa: W0613 +# noqa: E1133 +# noqa: R1710 +# noqa: W0212 +# noqa: W0719 +# noqa: W3101 +# noqa: C0103 + import os from functools import wraps from typing import List diff --git a/devchat/ide/vscode_services.py b/devchat/ide/vscode_services.py index c57dfac2..0b9706c4 100644 --- a/devchat/ide/vscode_services.py +++ b/devchat/ide/vscode_services.py @@ -1,16 +1,15 @@ import os -import sys from .rpc import rpc_call @rpc_call -def run_code(code: str): +def run_code(code: str): # pylint: disable=unused-argument pass @rpc_call -def diff_apply(filepath, content): +def diff_apply(filepath, content): # pylint: disable=unused-argument pass @@ -99,14 +98,14 @@ def visible_lines(): end_line = active_document["visibleRanges"][0][1]["line"] # read file lines from start_line to end_line - with open(file_path, "r") as file: - lines = file.readlines() - selected_lines = lines[start_line : end_line + 1] + with open(file_path, "r", encoding="utf-8") as file: + _lines = file.readlines() + _visible_lines = _lines[start_line : end_line + 1] # continue with the rest of the function return { "filePath": file_path, - "visibleText": "".join(selected_lines), + "visibleText": "".join(_visible_lines), "visibleRange": [start_line, end_line], } @@ -131,13 +130,13 @@ def selected_lines(): end_col = active_document["selection"]["end"]["character"] # read file lines from start_line to end_line - with open(file_path, "r") as file: - lines = file.readlines() - selected_lines = lines[start_line : end_line + 1] + with open(file_path, "r", encoding="utf-8") as file: + _lines = file.readlines() + _selected_lines = _lines[start_line : end_line + 1] # continue with the rest of the function return { "filePath": "", - "selectedText": "".join(selected_lines), + "selectedText": "".join(_selected_lines), "selectedRange": [start_line, start_col, end_line, end_col], } diff --git a/devchat/llm/chat.py b/devchat/llm/chat.py index f1bbcf77..fc3e80a2 100644 --- a/devchat/llm/chat.py +++ b/devchat/llm/chat.py @@ -47,7 +47,7 @@ def chat( ): def decorator(func): @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # pylint: disable=unused-argument nonlocal prompt, memory, model, llm_config prompt = prompt.format(**kwargs) messages = memory.contexts() if memory else [] @@ -86,7 +86,7 @@ def chat_json( ): def decorator(func): @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # pylint: disable=unused-argument nonlocal prompt, memory, model, llm_config prompt = prompt.format(**kwargs) messages = memory.contexts() if memory else [] diff --git a/devchat/llm/openai.py b/devchat/llm/openai.py index 1cb3a97b..80b67add 100644 --- a/devchat/llm/openai.py +++ b/devchat/llm/openai.py @@ -31,9 +31,8 @@ def _try_remove_markdown_block_flag(content): _ = match.group(1) # language markdown_content = match.group(2) return markdown_content.strip() - else: - # 如果匹配失败,则返回原始内容 - return content + # 如果匹配失败,则返回原始内容 + return content def chat_completion_stream_commit( @@ -79,7 +78,7 @@ def retry_timeout(chunks): def chunk_list(chunks): - return [chunk for chunk in chunks] + return [chunk for chunk in chunks] # noqa: R1721 def chunks_content(chunks): @@ -125,7 +124,9 @@ def content_to_json(content): raise err -def to_dict_content_and_call(content, tool_calls=[]): +def to_dict_content_and_call(content, tool_calls=None): + if tool_calls is None: + tool_calls = [] return { "content": content, "function_name": tool_calls[0]["name"] if tool_calls else None, diff --git a/devchat/llm/pipeline.py b/devchat/llm/pipeline.py index a4b9e5f8..418266a2 100644 --- a/devchat/llm/pipeline.py +++ b/devchat/llm/pipeline.py @@ -62,8 +62,8 @@ def pipeline(*funcs): def wrapper(*args, **kwargs): for index, func in enumerate(funcs): if index > 0: - if isinstance(args, Dict) and args.get("__type__", None) == "parallel": - args = func(*args["value"]) + if isinstance(args, Dict) and args.get("__type__", None) == "parallel": # noqa: E1101 + args = func(*args["value"]) # noqa: E1126 else: args = func(args) else: diff --git a/devchat/llm/tools_call.py b/devchat/llm/tools_call.py index 49d267d4..01a189e1 100644 --- a/devchat/llm/tools_call.py +++ b/devchat/llm/tools_call.py @@ -140,7 +140,7 @@ def chat_tools( ): def decorator(func): @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # pylint: disable=unused-argument nonlocal prompt, memory, model, tools, call_confirm_fun, llm_config prompt = prompt.format(**kwargs) if not tools: diff --git a/devchat/memory/base.py b/devchat/memory/base.py index 7d1f3104..4c73687d 100644 --- a/devchat/memory/base.py +++ b/devchat/memory/base.py @@ -11,22 +11,18 @@ def append(self, request, response): Append a request and response to the memory. """ # it must implemented in sub class - pass def append_request(self, request): """ Append a request to the memory. """ - pass def append_response(self, response): """ Append a request to the memory. """ - pass def contexts(self): """ Return the contexts of the memory. """ - pass diff --git a/devchat/memory/fixsize_memory.py b/devchat/memory/fixsize_memory.py index 36e9c2ed..ab514132 100644 --- a/devchat/memory/fixsize_memory.py +++ b/devchat/memory/fixsize_memory.py @@ -7,13 +7,16 @@ class FixSizeChatMemory(ChatMemory): to a fixed number of requests and responses. """ - def __init__(self, max_size: int = 5, messages=[], system_prompt=None): + def __init__(self, max_size: int = 5, messages=None, system_prompt=None): """ init the memory """ super().__init__() self._max_size = max_size # store last max_size messages + if messages is None: + messages = [] + self._messages = messages[-max_size * 2 :] self._system_prompt = system_prompt From e44e81dd95887c4ea2222c07c7648fa35c39d1a7 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Wed, 7 Feb 2024 16:07:02 +0800 Subject: [PATCH 4/5] Fix pylint warnings in code --- devchat/chatmark/chatmark_example/main.py | 2 +- devchat/chatmark/form.py | 4 ++-- devchat/ide/rpc.py | 8 ++++---- devchat/ide/service.py | 14 +++++++------- devchat/llm/openai.py | 2 +- devchat/llm/pipeline.py | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/devchat/chatmark/chatmark_example/main.py b/devchat/chatmark/chatmark_example/main.py index d8eb7bdc..f834c02c 100644 --- a/devchat/chatmark/chatmark_example/main.py +++ b/devchat/chatmark/chatmark_example/main.py @@ -1,7 +1,7 @@ import time -from devchat.chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # noqa: E402 +from devchat.chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # pylint: disable=E402 def main(): diff --git a/devchat/chatmark/form.py b/devchat/chatmark/form.py index 8324f872..f610358f 100644 --- a/devchat/chatmark/form.py +++ b/devchat/chatmark/form.py @@ -1,5 +1,5 @@ -# noqa: C0103 -# noqa: W0212 +# pylint: disable=C0103 +# pylint: disable=W0212 from typing import Dict, List, Optional, Union from .iobase import pipe_interaction diff --git a/devchat/ide/rpc.py b/devchat/ide/rpc.py index cd5ef67b..23eb2caf 100644 --- a/devchat/ide/rpc.py +++ b/devchat/ide/rpc.py @@ -1,7 +1,7 @@ -# noqa: C0103 -# noqa: W3101 -# noqa: W0719 -# noqa: R1710 +# pylint: disable=C0103 +# pylint: disable=W3101 +# pylint: disable=W0719 +# pylint: disable=R1710 import os from functools import wraps diff --git a/devchat/ide/service.py b/devchat/ide/service.py index b929547d..8eaaa359 100644 --- a/devchat/ide/service.py +++ b/devchat/ide/service.py @@ -1,11 +1,11 @@ # disable pylint -# noqa: W0613 -# noqa: E1133 -# noqa: R1710 -# noqa: W0212 -# noqa: W0719 -# noqa: W3101 -# noqa: C0103 +# pylint: disable=W0613 +# pylint: disable=E1133 +# pylint: disable=R1710 +# pylint: disable=W0212 +# pylint: disable=W0719 +# pylint: disable=W3101 +# pylint: disable=C0103 import os from functools import wraps diff --git a/devchat/llm/openai.py b/devchat/llm/openai.py index 80b67add..d6664be0 100644 --- a/devchat/llm/openai.py +++ b/devchat/llm/openai.py @@ -78,7 +78,7 @@ def retry_timeout(chunks): def chunk_list(chunks): - return [chunk for chunk in chunks] # noqa: R1721 + return [chunk for chunk in chunks] # pylint: disable=R1721 def chunks_content(chunks): diff --git a/devchat/llm/pipeline.py b/devchat/llm/pipeline.py index 418266a2..5b683286 100644 --- a/devchat/llm/pipeline.py +++ b/devchat/llm/pipeline.py @@ -62,8 +62,8 @@ def pipeline(*funcs): def wrapper(*args, **kwargs): for index, func in enumerate(funcs): if index > 0: - if isinstance(args, Dict) and args.get("__type__", None) == "parallel": # noqa: E1101 - args = func(*args["value"]) # noqa: E1126 + if isinstance(args, Dict) and args.get("__type__", None) == "parallel": # pylint: disable=E1101 + args = func(*args["value"]) # pylint: disable=E1126 else: args = func(args) else: From e6c69dc111ca21ce086d77045eb6c7f64bebc527 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Wed, 7 Feb 2024 16:07:35 +0800 Subject: [PATCH 5/5] Refactor pipeline function to handle parallel execution --- devchat/llm/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devchat/llm/pipeline.py b/devchat/llm/pipeline.py index 5b683286..2a4c867f 100644 --- a/devchat/llm/pipeline.py +++ b/devchat/llm/pipeline.py @@ -62,7 +62,8 @@ def pipeline(*funcs): def wrapper(*args, **kwargs): for index, func in enumerate(funcs): if index > 0: - if isinstance(args, Dict) and args.get("__type__", None) == "parallel": # pylint: disable=E1101 + # pylint: disable=E1101 + if isinstance(args, Dict) and args.get("__type__", None) == "parallel": args = func(*args["value"]) # pylint: disable=E1126 else: args = func(args)