diff --git a/.gitmodules b/.gitmodules index f3b2ca07..4cae8177 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "llm_docker_setting_pub"] path = llm_docker_setting_pub url = https://github.com/nobu007/llm_docker_setting_pub.git +[submodule "GuiAgentLoopCore"] + path = GuiAgentLoopCore + url = https://github.com/nobu007/GuiAgentLoopCore.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..0bcc5c33 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + # Using this mirror lets us use mypyc-compiled black, which is 2x faster + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.10.1 + hooks: + - id: black + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.11 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort diff --git a/GuiAgentLoopCore b/GuiAgentLoopCore new file mode 160000 index 00000000..f92616af --- /dev/null +++ b/GuiAgentLoopCore @@ -0,0 +1 @@ +Subproject commit f92616af75ba8e43714a0570eb072f8dcd045b79 diff --git a/envsetup.sh b/envsetup.sh index 7854cabd..87109ff0 100755 --- a/envsetup.sh +++ b/envsetup.sh @@ -1,27 +1,2 @@ # AUTO DEV SETUP -# check if rye is installed -if ! command -v rye &>/dev/null; then - echo "rye could not be found: installing now ..." - curl -sSf https://rye-up.com/get | RYE_INSTALL_OPTION="--yes" bash - echo "Check the rye docs for more info: https://rye-up.com/" -fi - -export PATH="$HOME/.rye/shims:$PATH" - -cd /app -source "$HOME/.rye/env" -echo 'source "$HOME/.rye/env"' >> ~/.bashrc -echo 'source "$HOME/.rye/env"' >> ~/.profile - - -echo "SYNC: setup .venv" -rye sync - -rye add pytest pre-commit - -echo "ACTIVATE: activate .venv" -source .venv/bin/activate - -echo "SETUP: install pre-commit hooks" -pre-commit install diff --git a/examples/anthropic_claude.py b/examples/anthropic_claude.py index 89a69ecb..7ecfd203 100644 --- a/examples/anthropic_claude.py +++ b/examples/anthropic_claude.py @@ -1,7 +1,5 @@ from codeinterpreterapi import CodeInterpreterSession -with CodeInterpreterSession(model="claude-2") as session: - result = session.generate_response( - "Plot the nvidea stock vs microsoft stock over the last 6 months." - ) +with CodeInterpreterSession(model="claude-3-haiku-20240307") as session: + result = session.generate_response("Plot the nvidea stock vs microsoft stock over the last 6 months.") result.show() diff --git a/examples/chat_cli.py b/examples/chat_cli.py index bcdbd32b..a70801c7 100644 --- a/examples/chat_cli.py +++ b/examples/chat_cli.py @@ -2,12 +2,7 @@ settings.MODEL = "gpt-4" -print( - "AI: Hello, I am the " - "code interpreter agent.\n" - "Ask me todo something and " - "I will use python to do it!\n" -) +print("AI: Hello, I am the " "code interpreter agent.\n" "Ask me todo something and " "I will use python to do it!\n") with CodeInterpreterSession() as session: while True: diff --git a/examples/chat_history_backend.py b/examples/chat_history_backend.py index 14bc6f46..b5d35d80 100644 --- a/examples/chat_history_backend.py +++ b/examples/chat_history_backend.py @@ -6,11 +6,14 @@ from codeinterpreterapi import CodeInterpreterSession # noqa: E402 -def main() -> None: +def main(is_local=True) -> None: session_id = None session = CodeInterpreterSession() - session.start() + if is_local: + session.start_local() + else: + session.start() print("Session ID:", session.session_id) session_id = session.session_id diff --git a/examples/frontend/app.py b/examples/frontend/app.py index 8654cc60..a94ae4c5 100644 --- a/examples/frontend/app.py +++ b/examples/frontend/app.py @@ -2,8 +2,9 @@ import sys import streamlit as st + from codeinterpreterapi import File -from utils import get_images # type: ignore +from codeinterpreterapi.config import settings # Page configuration st.set_page_config(layout="wide") diff --git a/examples/frontend/chainlitui.py b/examples/frontend/chainlitui.py index 268dbe59..b6dbf177 100644 --- a/examples/frontend/chainlitui.py +++ b/examples/frontend/chainlitui.py @@ -1,4 +1,5 @@ import chainlit as cl # type: ignore + from codeinterpreterapi import CodeInterpreterSession from codeinterpreterapi import File as CIFile @@ -11,9 +12,7 @@ async def on_action(action: cl.Action) -> None: # Wait for the user to upload a file while files is None: - files = await cl.AskFileMessage( - content="Please upload a text file to begin!", accept=["text/csv"] - ).send() + files = await cl.AskFileMessage(content="Please upload a text file to begin!", accept=["text/csv"]).send() # Decode the file text_file = files[0] text = text_file.content.decode("utf-8") @@ -21,27 +20,24 @@ async def on_action(action: cl.Action) -> None: UPLOADED_FILES.append(text_file) # Let the user know that the system is ready - await cl.Message( - content=f"`{text_file.name}` uploaded, it contains {len(text)} characters!" - ).send() + await cl.Message(content=f"`{text_file.name}` uploaded, it contains {len(text)} characters!").send() await action.remove() @cl.on_chat_start async def start_chat() -> None: - actions = [ - cl.Action(name="upload_file", value="example_value", description="Upload file") - ] + actions = [cl.Action(name="upload_file", value="example_value", description="Upload file")] - await cl.Message( - content="Hello, How can I assist you today", actions=actions - ).send() + await cl.Message(content="Hello, How can I assist you today", actions=actions).send() @cl.on_message -async def run_conversation(user_message: str) -> None: +async def run_conversation(user_message: str, is_local=True) -> None: session = CodeInterpreterSession() - await session.astart() + if is_local: + await session.astart_local() + else: + await session.astart() files = [CIFile(name=it.name, content=it.content) for it in UPLOADED_FILES] @@ -54,9 +50,7 @@ async def run_conversation(user_message: str) -> None: ) for file in response.files ] - actions = [ - cl.Action(name="upload_file", value="example_value", description="Upload file") - ] + actions = [cl.Action(name="upload_file", value="example_value", description="Upload file")] await cl.Message( content=response.content, elements=elements, diff --git a/examples/frontend/utils.py b/examples/frontend/utils.py index b33792df..5e3f195c 100644 --- a/examples/frontend/utils.py +++ b/examples/frontend/utils.py @@ -4,6 +4,7 @@ from typing import Optional import streamlit as st + from codeinterpreterapi import CodeInterpreterSession @@ -21,7 +22,7 @@ async def get_images(prompt: str, files: Optional[list] = None) -> list: with st.chat_message("user"): # type: ignore st.write(prompt) with st.spinner(): - async with CodeInterpreterSession(model="gpt-3.5-turbo") as session: + async with CodeInterpreterSession(model="claude-3-haiku-20240307") as session: response = await session.agenerate_response(prompt, files=files) with st.chat_message("assistant"): # type: ignore diff --git a/examples/plot_sin_wave.py b/examples/plot_sin_wave.py index 31ed3748..a0af570b 100644 --- a/examples/plot_sin_wave.py +++ b/examples/plot_sin_wave.py @@ -3,9 +3,7 @@ async def main() -> None: async with CodeInterpreterSession() as session: - response = await session.agenerate_response( - "Plot a sin wave and show it to me." - ) + response = await session.agenerate_response("Plot a sin wave and show it to me.") response.show() diff --git a/examples/show_bitcoin_chart.py b/examples/show_bitcoin_chart.py index 62e14342..7fddf2ae 100644 --- a/examples/show_bitcoin_chart.py +++ b/examples/show_bitcoin_chart.py @@ -7,9 +7,7 @@ def main() -> None: with CodeInterpreterSession(local=True) as session: currentdate = datetime.now().strftime("%Y-%m-%d") - response = session.generate_response( - f"Plot the bitcoin chart of 2023 YTD (today is {currentdate})" - ) + response = session.generate_response(f"Plot the bitcoin chart of 2023 YTD (today is {currentdate})") # prints the text and shows the image response.show() diff --git a/examples/use_additional_tools.py b/examples/use_additional_tools.py index 96cf0915..cf5d261d 100644 --- a/examples/use_additional_tools.py +++ b/examples/use_additional_tools.py @@ -4,13 +4,15 @@ so it can download the bitcoin chart from yahoo finance and plot it for you """ + import csv import io from typing import Any -from codeinterpreterapi import CodeInterpreterSession from langchain_core.tools import BaseTool +from codeinterpreterapi import CodeInterpreterSession + class ExampleKnowledgeBaseTool(BaseTool): name: str = "salary_database" @@ -31,12 +33,8 @@ async def _arun(self, *args: Any, **kwargs: Any) -> Any: async def main() -> None: - async with CodeInterpreterSession( - additional_tools=[ExampleKnowledgeBaseTool()] - ) as session: - response = await session.agenerate_response( - "Plot chart of company employee salaries" - ) + async with CodeInterpreterSession(additional_tools=[ExampleKnowledgeBaseTool()]) as session: + response = await session.agenerate_response("Plot chart of company employee salaries") response.show() diff --git a/pyproject.toml b/pyproject.toml index 406d943b..4b0f5f08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,60 +1,68 @@ [project] -name = "codeinterpreterapi" -version = "0.1.17" -description = "CodeInterpreterAPI is an (unofficial) open source python interface for the ChatGPT CodeInterpreter." -authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] +authors = [{name = "Shroominic", email = "contact@shroominic.com"}] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] dependencies = [ - "langchain-openai>=0.1.1", - "codeboxapi>=0.1.19", - "langchain>=0.1.14", - "pyzmq==25.1.2", + "langchain-openai>=0.1.1", + "codeboxapi>=0.1.19", + "langchain>=0.1.14", #TODO: remove + "pyzmq==25.1.2", + "invoke>=2.2.0", + "langchain>=0.1.16", + "langchain-anthropic>=0.1.11", + "langchain_experimental>=0.0.57", + "langchain-google-genai>=1.0.3", + "langchain_community>=0.0.34", + "langchain_experimental>=0.0.57", + "langchain-core>=0.1.46", ] -license = { file = "LICENSE" } -readme = "README.md" -requires-python = ">= 3.9.7, <3.13" +description = "CodeInterpreterAPI is an (unofficial) open source python interface for the ChatGPT CodeInterpreter." keywords = [ - "codeinterpreter", - "chatgpt", - "codeinterpreterapi", - "api", - "langchain", - "codeboxapi", -] -classifiers = [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering :: Artificial Intelligence", + "codeinterpreter", + "chatgpt", + "codeinterpreterapi", + "api", + "langchain", + "codeboxapi", ] +license = {file = "LICENSE"} +name = "codeinterpreterapi" +readme = "README.md" +requires-python = ">= 3.9.7, <3.13" +version = "0.1.17" [project.urls] Code = "https://github.com/shroominic/codeinterpreter-api" Docs = "https://shroominic.github.io/codeinterpreter-api" [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = ["hatchling"] [tool.rye] -managed = true dev-dependencies = [ - "ruff", - "mypy", - "isort", - "pytest", - "ipython", - "pre-commit", - "codeinterpreterapi[all]", - "mkdocs-material>=9.4", + "ruff", + "mypy", + "isort", + "pytest", + "ipython", + "pre-commit", + "codeinterpreterapi[all]", + "mkdocs-material>=9.4", ] +managed = true [project.optional-dependencies] -localbox = ["codeboxapi[local_support]"] +all = ["codeboxapi[all]", "codeinterpreterapi[frontend]"] frontend = ["streamlit"] image_support = ["codeboxapi[image_support]"] -all = ["codeboxapi[all]", "codeinterpreterapi[frontend]"] +localbox = ["codeboxapi[local_support]"] [tool.hatch.metadata] allow-direct-references = true @@ -63,19 +71,50 @@ allow-direct-references = true addopts = "-p no:warnings" [tool.isort] -multi_line_output = 3 -include_trailing_comma = true +ensure_newline_before_comments = true force_grid_wrap = 0 +include_trailing_comma = true line_length = 120 +multi_line_output = 3 +skip_gitignore = true +use_parentheses = true +# you can skip files as below +#skip_glob = docs/conf.py [tool.flake8] max-line-length = 120 [tool.mypy] -ignore_missing_imports = true -disallow_untyped_defs = true -disallow_untyped_calls = true disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +ignore_missing_imports = true [tool.ruff.lint] select = ["E", "F", "I"] + +[tool.pylint.messages_control] +disable = [ + "global-statement", + "missing-docstring", +] + +[tool.black] +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' +include = '\.pyi?$' +line-length = 120 +skip-string-normalization = true +target-version = ['py36', 'py37', 'py38', 'py39'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..7cb810c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# This is additional packages +invoke +langchain_anthropic +langchain_community +langchain_experimental diff --git a/server.py b/server.py index 22f9b148..8a8cfae6 100644 --- a/server.py +++ b/server.py @@ -1,21 +1,113 @@ -from interpreter.core.core import OpenInterpreter -from ui.server_impl import server +import traceback -interpreter = OpenInterpreter( - auto_run=True, +from gui_agent_loop_core.connector_impl.core_to_agent.connector_impl_codeinterpreter_api import ( + ConnectorImplCodeinterpreterApi, ) -print("interpreter.sync_computer=", interpreter.sync_computer) -interpreter.llm.model = "claude-3-haiku-20240307" -interpreter.llm.max_tokens = 2000 -interpreter.verbose = True -interpreter.sync_computer = True -interpreter.system_message += """ -Your workdir is "/app/work". You should save any input and output files in this directory. -If you lost previous work, you should check this directory and result from files. -""" -print("system_message=", interpreter.system_message) - -computer = interpreter.computer -computer.debug = True -computer.verbose = True -server(interpreter) +from gui_agent_loop_core.gui_agent_loop_core import GuiAgentLoopCore +from gui_agent_loop_core.schema.schema import ( + GuiAgentInterpreterChatMessage, + GuiAgentInterpreterChatMessages, + GuiAgentInterpreterChatResponse, + GuiAgentInterpreterChatResponseAny, +) + +from codeinterpreterapi import CodeInterpreterSession + + +class CodeInterpreter(ConnectorImplCodeinterpreterApi): + def __init__(self): + model = "claude-3-haiku-20240307" + # model = "gemini-1.5-pro-latest" + # model = "gemini-1.0-pro" + self.session = CodeInterpreterSession(model=model, verbose=True) + self.status = self.session.start_local() + + # def chat_core( + # self, + # message: GuiAgentInterpreterChatMessages, + # display: bool = False, + # stream: bool = False, + # blocking: bool = False, + # ) -> GuiAgentInterpreterChatResponseAny: + # try: + # message = """SVG画像を自動生成するプログラムの要件を以下のように定義します。 + + # 目的: + + # 電子書籍のヘッダ画像を自動生成すること + # 別のコンテンツ生成プログラムが出力したSVGファイルを入力として受け取ること + # 入力SVGファイルを指定の要件に従って加工し、新たなSVGファイルとして出力すること + # 機能要件: + + # グリッドレイアウト機能の実装 + + # 指定したグリッドサイズ(行数、列数)に基づいて要素を配置できるようにする + # グリッドの各セルに対して要素を割り当てられるようにする + # グリッドのサイズや間隔を柔軟に設定できるようにする + # SVG要素の配置と編集 + + # グリッド上の指定した位置にSVG要素(テキスト、図形、画像など)を配置できるようにする + # 配置する要素の属性(サイズ、色、フォントなど)を柔軟に設定できるようにする + # 既存のSVG要素を削除、移動、変更できるようにする + # 外部画像ファイルの読み込みと配置 + + # PNGやJPEGなどの外部画像ファイルを読み込んでSVGファイルに埋め込めるようにする + # 読み込んだ画像をグリッド上の指定した位置に配置できるようにする + # 画像のサイズを変更できるようにする + # SVGファイルの入出力 + + # SVGファイルを入力として読み込み、加工後のSVGファイルを出力できるようにする + # 出力ファイルのファイル名やパスを指定できるようにする + # 非機能要件: + + # Python3とsvgwriteライブラリを使用して実装すること + # コードはモジュール化し、再利用性と保守性を高めること + # エラーハンドリングを適切に行い、ログ出力を行うこと + # コードにはコメントを付けて可読性を高めること + # 実装の進め方: + + # svgwriteを使ったSVGファイルの基本的な読み込み、編集、出力の機能を実装する + # グリッドレイアウト機能を実装し、要素を配置できるようにする + # 外部画像ファイルの読み込みと配置機能を実装する + # 入力SVGファイルを読み込んで、指定の要件に従って加工し、新たなSVGファイルを出力する一連の処理を実装する + # 細かい仕様について検討し、機能を拡張する + # テストを行い、不具合を修正する + # ドキュメントを整備し、コードをリファクタリングする + # まずはこの要件定義に基づいて、各機能の実装に着手してください。実装方法や詳細な手順は、要件に合わせて適宜ご判断ください。 + + # 作業フォルダは/app/workを使ってください。 + + # 全ての処理は自動で実施して結果とプログラムだけ報告してください。 + + # """ + + # is_stream = False + # if is_stream: + # # ChainExecutorのエラーが出て動かない + # """ + # process_messages_gradio response_chunks= + # llm stream start + # server chat chunk_response= type= role= content="Error in CodeInterpreterSession: AttributeError - 'ChainExecutor' object has no attribute 'stream'" format='' code='' start=False end=False + # process_messages_gradio response= type= role= content="Error in CodeInterpreterSession: AttributeError - 'ChainExecutor' object has no attribute 'stream'" format='' code='' start=False end=False + # memory.save_context full_response= Error in CodeInterpr + # """ + # for chunk_str in self.session.generate_response_stream(message): + # chunk_response = GuiAgentInterpreterChatResponse() + # chunk_response.content = chunk_str + # print("server chat chunk_response=", chunk_response) + # yield chunk_response + # else: + # response = self.session.generate_response(message) + # print("server chat response(no stream)=", response) + # yield response + + # except Exception as e: + # print(e) + # traceback.print_exc() + # error_response = {"role": GuiAgentInterpreterChatMessage.Role.ASSISTANT, "content": str(e)} + # yield error_response + + +interpreter = CodeInterpreter() +core = GuiAgentLoopCore() +core.launch_server(interpreter) diff --git a/src/codeinterpreterapi/__init__.py b/src/codeinterpreterapi/__init__.py index 2afd77d1..6c6061c3 100644 --- a/src/codeinterpreterapi/__init__.py +++ b/src/codeinterpreterapi/__init__.py @@ -1,9 +1,8 @@ -from . import _patch_parser # noqa - from codeinterpreterapi.config import settings from codeinterpreterapi.schema import File from codeinterpreterapi.session import CodeInterpreterSession +from . import _patch_parser # noqa __all__ = [ "CodeInterpreterSession", diff --git a/src/codeinterpreterapi/_patch_parser.py b/src/codeinterpreterapi/_patch_parser.py index 244ae59e..ac6de14d 100644 --- a/src/codeinterpreterapi/_patch_parser.py +++ b/src/codeinterpreterapi/_patch_parser.py @@ -3,15 +3,15 @@ from json import JSONDecodeError from typing import List, Union -from langchain.agents.agent import AgentOutputParser from langchain.agents.openai_functions_agent import base +from langchain_community.output_parsers.rail_parser import GuardrailsOutputParser from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish from langchain_core.exceptions import OutputParserException from langchain_core.messages import AIMessage, BaseMessage from langchain_core.outputs import ChatGeneration, Generation -class OpenAIFunctionsAgentOutputParser(AgentOutputParser): +class OpenAIFunctionsAgentOutputParser(GuardrailsOutputParser): """Parses a message into agent action/finish. Is meant to be used with OpenAI models, as it relies on the specific @@ -52,8 +52,7 @@ def _parse_ai_message(message: BaseMessage) -> Union[AgentAction, AgentFinish]: } else: raise OutputParserException( - f"Could not parse tool input: {function_call} because " - f"the `arguments` is not valid JSON." + f"Could not parse tool input: {function_call} because " f"the `arguments` is not valid JSON." ) # HACK HACK HACK: @@ -76,13 +75,9 @@ def _parse_ai_message(message: BaseMessage) -> Union[AgentAction, AgentFinish]: message_log=[message], ) - return AgentFinish( - return_values={"output": message.content}, log=str(message.content) - ) + return AgentFinish(return_values={"output": message.content}, log=str(message.content)) - def parse_result( - self, result: List[Generation], *, partial: bool = False - ) -> Union[AgentAction, AgentFinish]: + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Union[AgentAction, AgentFinish]: if not isinstance(result[0], ChatGeneration): raise ValueError("This output parser only works on ChatGeneration output") message = result[0].message @@ -91,9 +86,7 @@ def parse_result( async def aparse_result( self, result: List[Generation], *, partial: bool = False ) -> Union[AgentAction, AgentFinish]: - return await asyncio.get_running_loop().run_in_executor( - None, self.parse_result, result - ) + return await asyncio.get_running_loop().run_in_executor(None, self.parse_result, result) def parse(self, text: str) -> Union[AgentAction, AgentFinish]: raise ValueError("Can only parse messages") diff --git a/src/codeinterpreterapi/agents/agents.py b/src/codeinterpreterapi/agents/agents.py new file mode 100644 index 00000000..db31a9f3 --- /dev/null +++ b/src/codeinterpreterapi/agents/agents.py @@ -0,0 +1,86 @@ +import pprint + +from langchain.agents import AgentExecutor, BaseSingleActionAgent, ConversationalAgent, ConversationalChatAgent +from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent +from langchain.chat_models.base import BaseChatModel +from langchain.memory.buffer import ConversationBufferMemory +from langchain_core.prompts.chat import MessagesPlaceholder +from langchain_experimental.plan_and_execute import PlanAndExecute, load_agent_executor, load_chat_planner +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_openai import AzureChatOpenAI, ChatOpenAI + +from codeinterpreterapi.config import settings + + +class CodeInterpreterAgent: + @staticmethod + def choose_single_chat_agent( + llm, + tools, + ) -> BaseSingleActionAgent: + if isinstance(llm, ChatOpenAI) or isinstance(llm, AzureChatOpenAI): + print("choose_agent OpenAIFunctionsAgent") + return OpenAIFunctionsAgent.from_llm_and_tools( + llm=llm, + tools=tools, + system_message=settings.SYSTEM_MESSAGE, + extra_prompt_messages=[MessagesPlaceholder(variable_name="chat_history")], + ) + elif isinstance(llm, ChatGoogleGenerativeAI): + print("choose_agent ConversationalChatAgent(ANTHROPIC)") + return ConversationalChatAgent.from_llm_and_tools( + llm=llm, + tools=tools, + system_message=settings.SYSTEM_MESSAGE.content.__str__(), + ) + elif isinstance(llm, ChatGoogleGenerativeAI): + print("choose_agent ChatGoogleGenerativeAI(gemini-pro)") + return ConversationalChatAgent.from_llm_and_tools( + llm=llm, + tools=tools, + system_message=settings.SYSTEM_MESSAGE.content.__str__(), + ) + else: + print("choose_agent ConversationalAgent(default)") + return ConversationalAgent.from_llm_and_tools( + llm=llm, + tools=tools, + prefix=settings.SYSTEM_MESSAGE.content.__str__(), + ) + + @staticmethod + def create_agent_and_executor(llm, tools, verbose, chat_memory, callbacks) -> AgentExecutor: + # agent + agent = CodeInterpreterAgent.choose_single_chat_agent(llm, tools) + print("create_agent_and_executor agent=", str(type(agent))) + # pprint.pprint(agent) + + # agent_executor + agent_executor = load_agent_executor( + agent=agent, + max_iterations=settings.MAX_ITERATIONS, + tools=tools, + verbose=verbose, + memory=ConversationBufferMemory( + memory_key="chat_history", + return_messages=True, + chat_memory=chat_memory, + ), + callbacks=callbacks, + ) + print("create_agent_and_executor agent_executor tools:") + for tool in agent_executor.tools: + pprint.pprint(tool) + + return agent_executor + + @staticmethod + def create_agent_and_executor_experimental(llm, tools, verbose) -> AgentExecutor: + # agent + agent = CodeInterpreterAgent.choose_single_chat_agent(llm, tools) + print("create_agent_and_executor agent=", str(type(agent))) + + # agent_executor + agent_executor = load_agent_executor(llm, tools, verbose=verbose) + + return agent_executor diff --git a/src/codeinterpreterapi/chains/rm_dl_link.py b/src/codeinterpreterapi/chains/rm_dl_link.py index 0c5d72db..57e856e0 100644 --- a/src/codeinterpreterapi/chains/rm_dl_link.py +++ b/src/codeinterpreterapi/chains/rm_dl_link.py @@ -10,9 +10,7 @@ def remove_download_link( input_response: str, llm: BaseLanguageModel, ) -> str: - messages = remove_dl_link_prompt.format_prompt( - input_response=input_response - ).to_messages() + messages = remove_dl_link_prompt.format_prompt(input_response=input_response).to_messages() message = llm.invoke(messages) if not isinstance(message, AIMessage): @@ -26,9 +24,7 @@ async def aremove_download_link( input_response: str, llm: BaseLanguageModel, ) -> str: - messages = remove_dl_link_prompt.format_prompt( - input_response=input_response - ).to_messages() + messages = remove_dl_link_prompt.format_prompt(input_response=input_response).to_messages() message = await llm.ainvoke(messages) if not isinstance(message, AIMessage): @@ -39,12 +35,9 @@ async def aremove_download_link( def test() -> None: - llm = ChatOpenAI(model="gpt-3.5-turbo-0613") # type: ignore + llm = ChatOpenAI(model="claude-3-haiku-20240307") # type: ignore - example = ( - "I have created the plot to your dataset.\n\n" - "Link to the file [here](sandbox:/plot.png)." - ) + example = "I have created the plot to your dataset.\n\n" "Link to the file [here](sandbox:/plot.png)." print(remove_download_link(example, llm)) diff --git a/src/codeinterpreterapi/config.py b/src/codeinterpreterapi/config.py index c917839d..e800c9bd 100644 --- a/src/codeinterpreterapi/config.py +++ b/src/codeinterpreterapi/config.py @@ -19,10 +19,11 @@ class CodeInterpreterAPISettings(BaseSettings): AZURE_API_BASE: Optional[str] = None AZURE_API_VERSION: Optional[str] = None AZURE_DEPLOYMENT_NAME: Optional[str] = None + GEMINI_API_KEY: Optional[SecretStr] = None ANTHROPIC_API_KEY: Optional[SecretStr] = None # LLM Settings - MODEL: str = "gpt-3.5-turbo" + MODEL: str = "claude-3-haiku-20240307" TEMPERATURE: float = 0.03 DETAILED_ERROR: bool = True SYSTEM_MESSAGE: SystemMessage = code_interpreter_system_message diff --git a/src/codeinterpreterapi/invoke_tasks/__init__.py b/src/codeinterpreterapi/invoke_tasks/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/codeinterpreterapi/invoke_tasks/__init__.py @@ -0,0 +1 @@ + diff --git a/src/codeinterpreterapi/invoke_tasks/python.py b/src/codeinterpreterapi/invoke_tasks/python.py new file mode 100644 index 00000000..f5cdd817 --- /dev/null +++ b/src/codeinterpreterapi/invoke_tasks/python.py @@ -0,0 +1,116 @@ +import asyncio + +from invoke import Context, task + + +@task +def run_code(c, code): + """ + LLMから生成されたPythonコードを非同期で実行し、結果をパースする + """ + loop = asyncio.get_event_loop() + exitcode, stdout, stderr = loop.run_until_complete(execute_code(code)) + ret = "" + ret += f"Exit Code: {exitcode}\n" + ret += f"Output: \n{stdout.decode()}\n" + if stderr: + ret += f"Error: \n{stderr.decode()}\n" + + # 一時ファイルを削除 + c.run("rm temp.py") + + return ret + + +@task +def run_code_file(c, code_file): + """ + LLMから生成されたPythonコードを保存したファイルを非同期で実行し、結果をパースする + """ + loop = asyncio.get_event_loop() + exitcode, stdout, stderr = loop.run_until_complete(execute_code_file(code_file)) + ret = "" + ret += f"Exit Code: {exitcode}\n" + ret += f"Output: \n{stdout.decode()}\n" + if stderr: + ret += f"Error: \n{stderr.decode()}\n" + print(ret) + + return ret + + +async def execute_code(code): + """ + Pythonコードを非同期で実行する + """ + # コードを一時ファイルに保存 + with open("temp.py", "w") as f: + f.write(code) + + # Pythonコードを非同期で実行し、出力をキャプチャ + proc = await asyncio.create_subprocess_exec( + "python", + "temp.py", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await proc.communicate() + exitcode = proc.returncode + + return exitcode, stdout, stderr + + +async def execute_code_file(code_file): + """ + Pythonファイルを非同期で実行する + """ + + # Pythonコードを非同期で実行し、出力をキャプチャ + proc = await asyncio.create_subprocess_exec( + "python", + code_file, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await proc.communicate() + exitcode = proc.returncode + + return exitcode, stdout, stderr + + +@task +def main(c, code=None): + """ + LLMから生成されたPythonコードを実行・修正を繰り返す + """ + loop = asyncio.get_event_loop() + + try: + print("Current code:") + print(code) + + exitcode, stdout, stderr = loop.run_until_complete(execute_code(code)) + ret = "" + ret += f"Exit Code: {exitcode}\n" + ret += f"Output: \n{stdout.decode()}\n" + ret += f"Error: \n{stderr.decode()}\n" + print(f"Exit Code: {exitcode}") + print(f"Output: \n{stdout.decode()}") + print(f"Error: \n{stderr.decode()}") + + except Exception as e: + print(f"An error occurred: {e}") + + finally: + c.run("rm temp.py") + + +# execute_code コルーチンは前と同様 + +if __name__ == "__main__": + # 初期コードを引数で受け取る (オプション) + initial_code = "print('ddd')" + run_code(Context(), code=initial_code) + main(Context(), code=initial_code) diff --git a/src/codeinterpreterapi/llm/llm.py b/src/codeinterpreterapi/llm/llm.py new file mode 100644 index 00000000..0a7ea1c0 --- /dev/null +++ b/src/codeinterpreterapi/llm/llm.py @@ -0,0 +1,57 @@ +from langchain.chat_models.base import BaseChatModel + +from codeinterpreterapi.config import settings + + +class CodeInterpreterLlm: + @classmethod + def get_llm(cls) -> BaseChatModel: + model = settings.MODEL + if ( + settings.AZURE_OPENAI_API_KEY + and settings.AZURE_API_BASE + and settings.AZURE_API_VERSION + and settings.AZURE_DEPLOYMENT_NAME + ): + from langchain_openai import AzureChatOpenAI + + return AzureChatOpenAI( + temperature=0.03, + base_url=settings.AZURE_API_BASE, + api_version=settings.AZURE_API_VERSION, + azure_deployment=settings.AZURE_DEPLOYMENT_NAME, + api_key=settings.AZURE_OPENAI_API_KEY, + max_retries=settings.MAX_RETRY, + timeout=settings.REQUEST_TIMEOUT, + ) # type: ignore + if settings.OPENAI_API_KEY: + from langchain_openai import ChatGoogleGenerativeAI + + return ChatGoogleGenerativeAI( + model=model, + api_key=settings.GEMINI_API_KEY, + timeout=settings.REQUEST_TIMEOUT, + temperature=settings.TEMPERATURE, + max_retries=settings.MAX_RETRY, + ) # type: ignore + if settings.GEMINI_API_KEY and "gemini" in model: + from langchain_google_genai import ChatGoogleGenerativeAI # type: ignore + + if "gemini" not in model: + print("Please set the gemini model in the settings.") + return ChatGoogleGenerativeAI( + model=model, + temperature=settings.TEMPERATURE, + google_api_key=settings.GEMINI_API_KEY, + ) + if settings.ANTHROPIC_API_KEY and "claude" in model: + from langchain_anthropic import ChatAnthropic # type: ignore + + if "claude" not in model: + print("Please set the claude model in the settings.") + return ChatAnthropic( + model_name=model, + temperature=settings.TEMPERATURE, + anthropic_api_key=settings.ANTHROPIC_API_KEY, + ) + raise ValueError("Please set the API key for the LLM you want to use.") diff --git a/src/codeinterpreterapi/planners/__init__.py b/src/codeinterpreterapi/planners/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/codeinterpreterapi/planners/__init__.py @@ -0,0 +1 @@ + diff --git a/src/codeinterpreterapi/planners/planners.py b/src/codeinterpreterapi/planners/planners.py new file mode 100644 index 00000000..2ef625c8 --- /dev/null +++ b/src/codeinterpreterapi/planners/planners.py @@ -0,0 +1,12 @@ +from langchain.base_language import BaseLanguageModel +from langchain_experimental.plan_and_execute import load_chat_planner +from langchain_experimental.plan_and_execute.planners.base import LLMPlanner + + +class CodeInterpreterPlanner: + @staticmethod + def choose_planner( + llm: BaseLanguageModel, + ) -> LLMPlanner: + planner = load_chat_planner(llm) + return planner diff --git a/src/codeinterpreterapi/schema.py b/src/codeinterpreterapi/schema.py index 1cdf0fcd..f06a50c3 100644 --- a/src/codeinterpreterapi/schema.py +++ b/src/codeinterpreterapi/schema.py @@ -50,11 +50,7 @@ def get_image(self) -> Any: try: from PIL import Image # type: ignore except ImportError: - print( - "Please install it with " - "`pip install 'codeinterpreterapi[image_support]'`" - " to display images." - ) + print("Please install it with " "`pip install 'codeinterpreterapi[image_support]'`" " to display images.") exit(1) from io import BytesIO diff --git a/src/codeinterpreterapi/session.py b/src/codeinterpreterapi/session.py index 6cf742a2..1b2f9017 100644 --- a/src/codeinterpreterapi/session.py +++ b/src/codeinterpreterapi/session.py @@ -1,5 +1,7 @@ import base64 import re +import subprocess +import tempfile import traceback from io import BytesIO from types import TracebackType @@ -8,26 +10,24 @@ from codeboxapi import CodeBox # type: ignore from codeboxapi.schema import CodeBoxOutput # type: ignore -from langchain.agents import ( - AgentExecutor, - BaseSingleActionAgent, - ConversationalAgent, - ConversationalChatAgent, +from gui_agent_loop_core.schema.schema import ( + GuiAgentInterpreterChatMessage, + GuiAgentInterpreterChatMessageList, + GuiAgentInterpreterChatResponse, + GuiAgentInterpreterChatResponseGenerator, + GuiAgentInterpreterChatResponseStr, + GuiAgentInterpreterManagerBase, + InterpreterState, ) -from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent +from langchain.agents import AgentExecutor from langchain.callbacks.base import Callbacks -from langchain.chat_models.base import BaseChatModel -from langchain.memory.buffer import ConversationBufferMemory from langchain_community.chat_message_histories.in_memory import ChatMessageHistory -from langchain_community.chat_message_histories.postgres import ( - PostgresChatMessageHistory, -) +from langchain_community.chat_message_histories.postgres import PostgresChatMessageHistory from langchain_community.chat_message_histories.redis import RedisChatMessageHistory from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseLanguageModel -from langchain_core.prompts.chat import MessagesPlaceholder -from langchain_core.tools import BaseTool, StructuredTool -from langchain_openai import AzureChatOpenAI, ChatOpenAI +from langchain_core.tools import BaseTool +from langchain_experimental.plan_and_execute.planners.base import LLMPlanner from codeinterpreterapi.chains import ( aget_file_modifications, @@ -37,13 +37,13 @@ ) from codeinterpreterapi.chat_history import CodeBoxChatMessageHistory from codeinterpreterapi.config import settings -from codeinterpreterapi.schema import ( - CodeInput, - CodeInterpreterResponse, - File, - SessionStatus, - UserRequest, -) +from codeinterpreterapi.schema import CodeInterpreterResponse, File, SessionStatus, UserRequest + +from .agents.agents import CodeInterpreterAgent +from .llm.llm import CodeInterpreterLlm +from .planners.planners import CodeInterpreterPlanner +from .supervisors.supervisors import CodeInterpreterSupervisor, MySupervisorChain +from .tools.tools import CodeInterpreterTools def _handle_deprecated_kwargs(kwargs: dict) -> None: @@ -61,15 +61,26 @@ def __init__( llm: Optional[BaseLanguageModel] = None, additional_tools: list[BaseTool] = [], callbacks: Callbacks = None, + is_local: bool = True, **kwargs: Any, ) -> None: _handle_deprecated_kwargs(kwargs) + self.is_local = is_local self.codebox = CodeBox(requirements=settings.CUSTOM_PACKAGES) self.verbose = kwargs.get("verbose", settings.DEBUG) - self.tools: list[BaseTool] = self._tools(additional_tools) - self.llm: BaseLanguageModel = llm or self._choose_llm() + run_handler_func = self._run_handler + arun_handler_func = self._arun_handler + if self.is_local: + run_handler_func = self._run_handler_local + arun_handler_func = self._arun_handler_local + self.tools: list[BaseTool] = CodeInterpreterTools.get_all(additional_tools, run_handler_func, arun_handler_func) + self.llm: BaseLanguageModel = llm or CodeInterpreterLlm.get_llm() + self.log("self.llm=" + str(self.llm)) + self.callbacks = callbacks self.agent_executor: Optional[AgentExecutor] = None + self.llm_planner: Optional[LLMPlanner] = None + self.supervisor: Optional[MySupervisorChain] = None self.input_files: list[File] = [] self.output_files: list[File] = [] self.code_log: list[tuple[str, str]] = [] @@ -78,147 +89,92 @@ def __init__( def from_id(cls, session_id: UUID, **kwargs: Any) -> "CodeInterpreterSession": session = cls(**kwargs) session.codebox = CodeBox.from_id(session_id) - session.agent_executor = session._agent_executor() + session.agent_executor = CodeInterpreterAgent.create_agent_and_executor() return session @property def session_id(self) -> Optional[UUID]: return self.codebox.session_id + def initialize(self): + self.initialize_agent_executor() + self.initialize_llm_planner() + self.initialize_supervisor() + + def initialize_agent_executor(self): + # self.agent_executor = CodeInterpreterAgent.create_agent_and_executor( + # llm=self.llm, + # tools=self.tools, + # verbose=self.verbose, + # chat_memory=self._history_backend(), + # callbacks=self.callbacks, + # ) + self.agent_executor = CodeInterpreterAgent.create_agent_and_executor_experimental( + llm=self.llm, + tools=self.tools, + verbose=self.verbose, + ) + + def initialize_llm_planner(self): + self.llm_planner = CodeInterpreterPlanner.choose_planner( + llm=self.llm, + ) + + def initialize_supervisor(self): + self.supervisor = CodeInterpreterSupervisor.choose_supervisor( + planner=self.llm_planner, + executor=self.agent_executor, + ) + def start(self) -> SessionStatus: + print("start") status = SessionStatus.from_codebox_status(self.codebox.start()) - self.agent_executor = self._agent_executor() + self.initialize() self.codebox.run( f"!pip install -q {' '.join(settings.CUSTOM_PACKAGES)}", ) return status async def astart(self) -> SessionStatus: + print("astart") status = SessionStatus.from_codebox_status(await self.codebox.astart()) - self.agent_executor = self._agent_executor() + self.initialize() await self.codebox.arun( f"!pip install -q {' '.join(settings.CUSTOM_PACKAGES)}", ) return status - def _tools(self, additional_tools: list[BaseTool]) -> list[BaseTool]: - return additional_tools + [ - StructuredTool( - name="python", - description="Input a string of code to a ipython interpreter. " - "Write the entire code in a single string. This string can " - "be really long, so you can use the `;` character to split lines. " - "Start your code on the same line as the opening quote. " - "Do not start your code with a line break. " - "For example, do 'import numpy', not '\\nimport numpy'." - "Variables are preserved between runs. " - + ( - ( - "You can use all default python packages " - f"specifically also these: {settings.CUSTOM_PACKAGES}" - ) - if settings.CUSTOM_PACKAGES - else "" - ), # TODO: or include this in the system message - func=self._run_handler, - coroutine=self._arun_handler, - args_schema=CodeInput, # type: ignore - ), - ] - - def _choose_llm(self) -> BaseChatModel: - if ( - settings.AZURE_OPENAI_API_KEY - and settings.AZURE_API_BASE - and settings.AZURE_API_VERSION - and settings.AZURE_DEPLOYMENT_NAME - ): - self.log("Using Azure Chat OpenAI") - return AzureChatOpenAI( - temperature=0.03, - base_url=settings.AZURE_API_BASE, - api_version=settings.AZURE_API_VERSION, - azure_deployment=settings.AZURE_DEPLOYMENT_NAME, - api_key=settings.AZURE_OPENAI_API_KEY, - max_retries=settings.MAX_RETRY, - timeout=settings.REQUEST_TIMEOUT, - ) # type: ignore - if settings.OPENAI_API_KEY: - from langchain_openai import ChatOpenAI - - return ChatOpenAI( - model=settings.MODEL, - api_key=settings.OPENAI_API_KEY, - timeout=settings.REQUEST_TIMEOUT, - temperature=settings.TEMPERATURE, - max_retries=settings.MAX_RETRY, - ) # type: ignore - if settings.ANTHROPIC_API_KEY: - from langchain_anthropic import ChatAnthropic # type: ignore - - if "claude" not in settings.MODEL: - print("Please set the claude model in the settings.") - self.log("Using Chat Anthropic") - return ChatAnthropic( - model_name=settings.MODEL, - temperature=settings.TEMPERATURE, - anthropic_api_key=settings.ANTHROPIC_API_KEY, - ) - raise ValueError("Please set the API key for the LLM you want to use.") + def start_local(self) -> SessionStatus: + print("start_local") + self.initialize() + status = SessionStatus(status="started") + return status - def _choose_agent(self) -> BaseSingleActionAgent: - return ( - OpenAIFunctionsAgent.from_llm_and_tools( - llm=self.llm, - tools=self.tools, - system_message=settings.SYSTEM_MESSAGE, - extra_prompt_messages=[ - MessagesPlaceholder(variable_name="chat_history") - ], - ) - if isinstance(self.llm, ChatOpenAI) or isinstance(self.llm, AzureChatOpenAI) - else ConversationalChatAgent.from_llm_and_tools( - llm=self.llm, - tools=self.tools, - system_message=settings.SYSTEM_MESSAGE.content.__str__(), - ) - if isinstance(self.llm, BaseChatModel) - else ConversationalAgent.from_llm_and_tools( - llm=self.llm, - tools=self.tools, - prefix=settings.SYSTEM_MESSAGE.content.__str__(), - ) - ) + async def astart_local(self) -> SessionStatus: + print("astart_local") + status = self.start_local() + self.initialize() + return status def _history_backend(self) -> BaseChatMessageHistory: return ( CodeBoxChatMessageHistory(codebox=self.codebox) if settings.HISTORY_BACKEND == "codebox" - else RedisChatMessageHistory( - session_id=str(self.session_id), - url=settings.REDIS_URL, - ) - if settings.HISTORY_BACKEND == "redis" - else PostgresChatMessageHistory( - session_id=str(self.session_id), - connection_string=settings.POSTGRES_URL, + else ( + RedisChatMessageHistory( + session_id=str(self.session_id), + url=settings.REDIS_URL, + ) + if settings.HISTORY_BACKEND == "redis" + else ( + PostgresChatMessageHistory( + session_id=str(self.session_id), + connection_string=settings.POSTGRES_URL, + ) + if settings.HISTORY_BACKEND == "postgres" + else ChatMessageHistory() + ) ) - if settings.HISTORY_BACKEND == "postgres" - else ChatMessageHistory() - ) - - def _agent_executor(self) -> AgentExecutor: - return AgentExecutor.from_agent_and_tools( - agent=self._choose_agent(), - max_iterations=settings.MAX_ITERATIONS, - tools=self.tools, - verbose=self.verbose, - memory=ConversationBufferMemory( - memory_key="chat_history", - return_messages=True, - chat_memory=self._history_backend(), - ), - callbacks=self.callbacks, ) def show_code(self, code: str) -> None: @@ -230,6 +186,34 @@ async def ashow_code(self, code: str) -> None: if self.verbose: print(code) + def _get_handler_local_command(self, code: str): + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".py") as temp_file: + temp_file.write(code) + temp_file_path = temp_file.name + + command = f"cd src/codeinterpreterapi/invoke_tasks && invoke -c python run-code-file '{temp_file_path}'" + return command + + def _run_handler_local(self, code: str): + command = self._get_handler_local_command(code) + try: + output_content = subprocess.check_output(command, shell=True, universal_newlines=True) + self.code_log.append((code, output_content)) + return output_content + except subprocess.CalledProcessError as e: + print(f"An error occurred: {e}") + return None + + async def _arun_handler_local(self, code: str): + command = self._get_handler_local_command(code) + try: + output_content = await subprocess.check_output(command, shell=True, universal_newlines=True) + self.code_log.append((code, output_content)) + return output_content + except subprocess.CalledProcessError as e: + print(f"An error occurred: {e}") + return None + def _run_handler(self, code: str) -> str: """Run code in container and send the output to the user""" self.show_code(code) @@ -253,10 +237,7 @@ def _run_handler(self, code: str) -> str: output.content, ): self.codebox.install(package.group(1)) - return ( - f"{package.group(1)} was missing but " - "got installed now. Please try again." - ) + return f"{package.group(1)} was missing but " "got installed now. Please try again." else: # TODO: pre-analyze error to optimize next code generation pass @@ -272,9 +253,7 @@ def _run_handler(self, code: str) -> str: continue file_buffer = BytesIO(fileb.content) file_buffer.name = filename - self.output_files.append( - File(name=filename, content=file_buffer.read()) - ) + self.output_files.append(File(name=filename, content=file_buffer.read())) return output.content @@ -301,10 +280,7 @@ async def _arun_handler(self, code: str) -> str: output.content, ): await self.codebox.ainstall(package.group(1)) - return ( - f"{package.group(1)} was missing but " - "got installed now. Please try again." - ) + return f"{package.group(1)} was missing but " "got installed now. Please try again." else: # TODO: pre-analyze error to optimize next code generation pass @@ -320,9 +296,7 @@ async def _arun_handler(self, code: str) -> str: continue file_buffer = BytesIO(fileb.content) file_buffer.name = filename - self.output_files.append( - File(name=filename, content=file_buffer.read()) - ) + self.output_files.append(File(name=filename, content=file_buffer.read())) return output.content @@ -331,9 +305,7 @@ def _input_handler(self, request: UserRequest) -> None: if not request.files: return if not request.content: - request.content = ( - "I uploaded, just text me back and confirm that you got the file(s)." - ) + request.content = "I uploaded, just text me back and confirm that you got the file(s)." assert isinstance(request.content, str), "TODO: implement image support" request.content += "\n**The user uploaded the following files: **\n" for file in request.files: @@ -348,9 +320,7 @@ async def _ainput_handler(self, request: UserRequest) -> None: if not request.files: return if not request.content: - request.content = ( - "I uploaded, just text me back and confirm that you got the file(s)." - ) + request.content = "I uploaded, just text me back and confirm that you got the file(s)." assert isinstance(request.content, str), "TODO: implement image support" request.content += "\n**The user uploaded the following files: **\n" for file in request.files: @@ -378,9 +348,8 @@ def _output_handler(self, final_response: str) -> CodeInterpreterResponse: self.output_files = [] self.code_log = [] - return CodeInterpreterResponse( - content=final_response, files=output_files, code_log=code_log - ) + response = CodeInterpreterResponse(content=final_response, files=output_files, code_log=code_log) + return response async def _aoutput_handler(self, final_response: str) -> CodeInterpreterResponse: """Embed images in the response""" @@ -401,9 +370,8 @@ async def _aoutput_handler(self, final_response: str) -> CodeInterpreterResponse self.output_files = [] self.code_log = [] - return CodeInterpreterResponse( - content=final_response, files=output_files, code_log=code_log - ) + response = CodeInterpreterResponse(content=final_response, files=output_files, code_log=code_log) + return response def generate_response_sync( self, @@ -426,15 +394,25 @@ def generate_response( try: self._input_handler(user_request) assert self.agent_executor, "Session not initialized." - response = self.agent_executor.run(input=user_request.content) - return self._output_handler(response) + print("user_request.content=", user_request.content) + + # ======= ↓↓↓↓ LLM invoke ↓↓↓↓ #======= + # response = self.agent_executor.invoke(input=user_request.content) + response = self.supervisor.invoke(input=user_request, verbose=True) + # ======= ↑↑↑↑ LLM invoke ↑↑↑↑ #======= + print("response(type)=", type(response)) + print("response=", response) + + output = response["output"] + print("generate_response agent_executor.invoke output=", output) + return self._output_handler(output) + # return output except Exception as e: if self.verbose: traceback.print_exc() if settings.DETAILED_ERROR: return CodeInterpreterResponse( - content="Error in CodeInterpreterSession: " - f"{e.__class__.__name__} - {e}" + content="Error in CodeInterpreterSession: " f"{e.__class__.__name__} - {e}" ) else: return CodeInterpreterResponse( @@ -452,15 +430,20 @@ async def agenerate_response( try: await self._ainput_handler(user_request) assert self.agent_executor, "Session not initialized." - response = await self.agent_executor.arun(input=user_request.content) - return await self._aoutput_handler(response) + + # ======= ↓↓↓↓ LLM invoke ↓↓↓↓ #======= + response = await self.agent_executor.ainvoke(input=user_request.content) + # ======= ↑↑↑↑ LLM invoke ↑↑↑↑ #======= + + output = response["output"] + print("agenerate_response agent_executor.ainvoke output=", output) + return await self._aoutput_handler(output) except Exception as e: if self.verbose: traceback.print_exc() if settings.DETAILED_ERROR: return CodeInterpreterResponse( - content="Error in CodeInterpreterSession: " - f"{e.__class__.__name__} - {e}" + content="Error in CodeInterpreterSession(agenerate_response): " f"{e.__class__.__name__} - {e}" ) else: return CodeInterpreterResponse( @@ -468,6 +451,81 @@ async def agenerate_response( "Please try again or restart the session." ) + def generate_response_stream( + self, + user_msg: str, + files: list[File] = None, + ) -> GuiAgentInterpreterChatResponseStr: + """Generate a Code Interpreter response based on the user's input.""" + if files is None: + files = [] + user_request = UserRequest(content=user_msg, files=files) + try: + self._input_handler(user_request) + assert self.agent_executor, "Session not initialized." + print("llm stream start") + # ======= ↓↓↓↓ LLM invoke ↓↓↓↓ #======= + response = self.agent_executor.stream(input=user_request.content) + # ======= ↑↑↑↑ LLM invoke ↑↑↑↑ #======= + print("llm stream response(type)=", type(response)) + print("llm stream response=", response) + + full_output = "" + for chunk in response: + yield chunk + full_output += chunk["output"] + + print("generate_response_stream agent_executor.stream full_output=", full_output) + self._aoutput_handler(full_output) + except Exception as e: + if self.verbose: + traceback.print_exc() + if settings.DETAILED_ERROR: + yield "Error in CodeInterpreterSession(generate_response_stream): " f"{e.__class__.__name__} - {e}" + else: + yield "Sorry, something went while generating your response." + "Please try again or restart the session." + + async def agenerate_response_stream( + self, + user_msg: str, + files: list[File] = None, + ) -> CodeInterpreterResponse: + """Generate a Code Interpreter response based on the user's input.""" + if files is None: + files = [] + user_request = UserRequest(content=user_msg, files=files) + try: + await self._ainput_handler(user_request) + assert self.agent_executor, "Session not initialized." + + print("llm astream start") + # ======= ↓↓↓↓ LLM invoke ↓↓↓↓ #======= + response = self.agent_executor.astream(input=user_request.content) + # ======= ↑↑↑↑ LLM invoke ↑↑↑↑ #======= + print("llm astream response(type)=", type(response)) + print("llm astream response=", response) + + full_output = "" + async for chunk in response: + yield chunk + full_output += chunk["output"] + + print("agent_executor.astream full_output=", full_output) + await self._aoutput_handler(full_output) + except Exception as e: + if self.verbose: + traceback.print_exc() + if settings.DETAILED_ERROR: + yield CodeInterpreterResponse( + content="Error in CodeInterpreterSession(agenerate_response_stream): " + f"{e.__class__.__name__} - {e}" + ) + else: + yield CodeInterpreterResponse( + content="Sorry, something went while generating your response." + "Please try again or restart the session." + ) + def is_running(self) -> bool: return self.codebox.status() == "running" @@ -485,7 +543,10 @@ async def astop(self) -> SessionStatus: return SessionStatus.from_codebox_status(await self.codebox.astop()) def __enter__(self) -> "CodeInterpreterSession": - self.start() + if self.is_local: + self.start_local() + else: + self.start() return self def __exit__( diff --git a/src/codeinterpreterapi/supervisors/__init__.py b/src/codeinterpreterapi/supervisors/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/codeinterpreterapi/supervisors/__init__.py @@ -0,0 +1 @@ + diff --git a/src/codeinterpreterapi/supervisors/supervisors.py b/src/codeinterpreterapi/supervisors/supervisors.py new file mode 100644 index 00000000..a27d27b7 --- /dev/null +++ b/src/codeinterpreterapi/supervisors/supervisors.py @@ -0,0 +1,15 @@ +from langchain.agents import AgentExecutor +from langchain.chains.base import Chain +from langchain_experimental.plan_and_execute.agent_executor import PlanAndExecute +from langchain_experimental.plan_and_execute.planners.base import LLMPlanner + + +class MySupervisorChain(Chain): + pass + + +class CodeInterpreterSupervisor: + @staticmethod + def choose_supervisor(planner: LLMPlanner, executor: AgentExecutor) -> MySupervisorChain: + supervisor = PlanAndExecute(planner=planner, executor=executor, verbose=True) + return supervisor diff --git a/src/codeinterpreterapi/tools/tools.py b/src/codeinterpreterapi/tools/tools.py new file mode 100644 index 00000000..f558a290 --- /dev/null +++ b/src/codeinterpreterapi/tools/tools.py @@ -0,0 +1,49 @@ +from langchain_community.tools.shell.tool import ShellTool +from langchain_community.tools.tavily_search import TavilySearchResults +from langchain_core.tools import BaseTool, StructuredTool + +from codeinterpreterapi.config import settings +from codeinterpreterapi.schema import CodeInput + + +class CodeInterpreterTools: + @staticmethod + def get_all(additional_tools: list[BaseTool], run_handler_func, arun_handler_func) -> list[BaseTool]: + additional_tools = CodeInterpreterTools.get_python(additional_tools, run_handler_func, arun_handler_func) + additional_tools = CodeInterpreterTools.get_shell(additional_tools) + additional_tools = CodeInterpreterTools.get_web_search(additional_tools) + return additional_tools + + @staticmethod + def get_python(additional_tools: list[BaseTool], run_handler_func, arun_handler_func) -> list[BaseTool]: + return additional_tools + [ + StructuredTool( + name="python", + description="Input a string of code to a ipython interpreter. " + "Write the entire code in a single string. This string can " + "be really long, so you can use the `;` character to split lines. " + "Start your code on the same line as the opening quote. " + "Do not start your code with a line break. " + "For example, do 'import numpy', not '\\nimport numpy'." + "Variables are preserved between runs. " + + ( + ("You can use all default python packages " f"specifically also these: {settings.CUSTOM_PACKAGES}") + if settings.CUSTOM_PACKAGES + else "" + ), # TODO: or include this in the system message + func=run_handler_func, + coroutine=arun_handler_func, + args_schema=CodeInput, # type: ignore + ), + ] + + @staticmethod + def get_shell(additional_tools: list[BaseTool]) -> list[BaseTool]: + tools = [ShellTool()] + return additional_tools + tools + + @staticmethod + def get_web_search(additional_tools: list[BaseTool]) -> list[BaseTool]: + # TODO: use ShellInput + tools = [TavilySearchResults(max_results=1)] + return additional_tools + tools diff --git a/tests/chain_test.py b/tests/chain_test.py index bf301562..2a8b0a3a 100644 --- a/tests/chain_test.py +++ b/tests/chain_test.py @@ -1,18 +1,18 @@ from asyncio import run as _await +from langchain_openai import ChatOpenAI + from codeinterpreterapi.chains import ( aget_file_modifications, aremove_download_link, get_file_modifications, remove_download_link, ) -from langchain_openai import ChatOpenAI -llm = ChatOpenAI(model="gpt-3.5-turbo") +llm = ChatOpenAI("claude-3-haiku-20240307") remove_download_link_example = ( - "I have created the plot to your dataset.\n\n" - "Link to the file [here](sandbox:/plot.png)." + "I have created the plot to your dataset.\n\n" "Link to the file [here](sandbox:/plot.png)." ) base_code = """ diff --git a/tests/general_test.py b/tests/general_test.py index af4079f7..064f78aa 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -6,17 +6,13 @@ def test_codebox() -> None: session = CodeInterpreterSession() assert run_sync(session), "Failed to run sync CodeInterpreterSession remotely" - assert asyncio.run( - run_async(session) - ), "Failed to run async CodeInterpreterSession remotely" + assert asyncio.run(run_async(session)), "Failed to run async CodeInterpreterSession remotely" def test_localbox() -> None: session = CodeInterpreterSession(local=True) assert run_sync(session), "Failed to run sync CodeInterpreterSession locally" - assert asyncio.run( - run_async(session) - ), "Failed to run async CodeInterpreterSession locally" + assert asyncio.run(run_async(session)), "Failed to run async CodeInterpreterSession locally" def run_sync(session: CodeInterpreterSession) -> bool: @@ -26,8 +22,7 @@ def run_sync(session: CodeInterpreterSession) -> bool: assert ( "3.1" in session.generate_response( - "Compute pi using Monte Carlo simulation in " - "Python and show me the result." + "Compute pi using Monte Carlo simulation in " "Python and show me the result." ).content ) @@ -64,8 +59,7 @@ async def run_async(session: CodeInterpreterSession) -> bool: "3.1" in ( await session.agenerate_response( - "Compute pi using Monte Carlo simulation in " - "Python and show me the result." + "Compute pi using Monte Carlo simulation in " "Python and show me the result." ) ).content ) diff --git a/tests/one_test.py b/tests/one_test.py new file mode 100644 index 00000000..b4699a3f --- /dev/null +++ b/tests/one_test.py @@ -0,0 +1,28 @@ +from codeinterpreterapi import CodeInterpreterSession + + +def test(): + model = "claude-3-haiku-20240307" + # model = "gemini-1.0-pro" + test_message = "pythonで円周率を表示するプログラムを実行してください。" + verbose = False + is_streaming = False + print("test_message=", test_message) + session = CodeInterpreterSession(model=model, verbose=verbose) + status = session.start_local() + print("status=", status) + if is_streaming: + # response_inner: CodeInterpreterResponse + response_inner = session.generate_response_stream(test_message) + for response_inner_chunk in response_inner: + print("response_inner_chunk.content=", response_inner.content) + print("response_inner_chunk code_log=", response_inner.code_log) + else: + # response_inner: CodeInterpreterResponse + response_inner = session.generate_response(test_message) + print("response_inner.content=", response_inner.content) + print("response_inner code_log=", response_inner.code_log) + + +if __name__ == "__main__": + test()