From 632a3eda814fc00d0db1c4a148669fce5bac7466 Mon Sep 17 00:00:00 2001 From: brokun Date: Tue, 17 Dec 2024 13:41:40 +0800 Subject: [PATCH] Refactor: magent-ui (#98) * chore: update dependencies * refactor(langchain): core adaptor * feat(example): langchain example * chore: spell * feat(langchain): handle ai message * feat(chat): add rehype raw plugin * feat(langchain): input & output format * feat(example): interperter example * fix: chat bot width * fix: modify the presentation of the agent execution process * feat: launch agent in notebook and display qrcode * fix: add qrcode * docs: tongyi agent demo * fix: mobile view change * chore(langchain): update environment & version * chore(flow): tailwind auto update * docs: readme * chore: fix config * feat: magent-ui-core * refactor: magent-ui-core * fix: lint * docs: update readme * feat(core): magent-core --------- Co-authored-by: xujingli.xjl --- .vscode/settings.json | 2 +- README.md | 7 +- README.zh-CN.md | 12 +- api/pyproject.toml | 3 - examples/langchain-example/.gitignore | 11 + examples/langchain-example/.python-version | 1 + examples/langchain-example/MANIFEST.in | 1 + examples/langchain-example/README.md | 1 + examples/langchain-example/openai_agent.ipynb | 223 ++++++++++ examples/langchain-example/openai_chain.ipynb | 192 +++++++++ .../openai_libro_agent.ipynb | 303 +++++++++++++ examples/langchain-example/openai_llm.ipynb | 202 +++++++++ examples/langchain-example/pyproject.toml | 36 ++ .../src/langchain_example/__init__.py | 0 .../src/langchain_example/dev.py | 13 + .../src/langchain_example/interpreter.py | 59 +++ .../src/langchain_example/libro_agent.py | 82 ++++ .../src/langchain_example/tongyi.py | 8 +- .../tongyi_libro_agent.ipynb | 317 ++++++++++++++ packages/magent-core/.gitignore | 11 + packages/magent-core/.python-version | 1 + packages/magent-core/MANIFEST.in | 1 + packages/magent-core/README.md | 1 + packages/magent-core/pyproject.toml | 24 ++ .../magent-core/src/magent_core/__init__.py | 4 + .../magent-core/src/magent_core/config.py | 182 ++++++++ .../magent_core/executor_adpator/__init__.py | 4 + .../executor_adpator/adaptor_registry.py | 73 ++++ .../executor_adpator/base_adaptor.py | 51 +++ .../executor_adpator/current_executor.py | 19 + .../src/magent_core/executor_adpator/event.py | 46 ++ .../src/magent_core/llm/__init__.py | 0 .../src/magent_core/llm/environment.py | 0 packages/magent-core/src/magent_core/utils.py | 47 ++ packages/magent-ui-core/.gitignore | 11 + packages/magent-ui-core/.python-version | 1 + packages/magent-ui-core/MANIFEST.in | 1 + packages/magent-ui-core/README.md | 1 + packages/magent-ui-core/pyproject.toml | 31 ++ .../src/magent_ui_core/__init__.py | 6 + .../src/magent_ui_core/adaptor_registry.py | 73 ++++ .../magent-ui-core/src/magent_ui_core/app.py | 107 +++++ .../src/magent_ui_core/base_adaptor.py | 51 +++ .../src/magent_ui_core/config.py | 182 ++++++++ .../src/magent_ui_core/current_executor.py | 19 + .../src/magent_ui_core/event.py | 46 ++ .../src/magent_ui_core/utils.py | 52 +++ packages/magent-ui-langchain/MANIFEST.in | 2 +- packages/magent-ui-langchain/README.md | 31 +- packages/magent-ui-langchain/pyproject.toml | 7 +- .../magent_ui_langchain/adaptor/__init__.py | 13 + .../adaptor/langchain_adaptor.py | 164 +++++++ .../adaptor/langchain_openai_executor.py | 71 +++ .../adaptor/langchain_tongyi_executor.py | 45 ++ .../src/magent_ui_langchain/app.py | 67 +-- .../src/magent_ui_langchain/core/__init__.py | 14 - .../core/adapter_registry.py | 36 -- .../core/current_executor.py | 18 - .../src/magent_ui_langchain/core/executor.py | 23 - .../core/langchain_executor.py | 81 ---- .../routers/chat/router.py | 65 ++- .../magent_ui_langchain/templates/index.html | 1 + requirements-dev.lock | 403 +++--------------- requirements.lock | 403 +++--------------- web-apps/ui-langchain/src/views/chat/view.tsx | 2 +- web-packages/magent-chat/package.json | 3 +- .../magent-chat/src/chat-view/view.tsx | 2 + web-packages/magent-flow/src/tailwind.out.css | 52 +-- 68 files changed, 3035 insertions(+), 986 deletions(-) create mode 100644 examples/langchain-example/.gitignore create mode 100644 examples/langchain-example/.python-version create mode 100644 examples/langchain-example/MANIFEST.in create mode 100644 examples/langchain-example/README.md create mode 100644 examples/langchain-example/openai_agent.ipynb create mode 100644 examples/langchain-example/openai_chain.ipynb create mode 100644 examples/langchain-example/openai_libro_agent.ipynb create mode 100644 examples/langchain-example/openai_llm.ipynb create mode 100644 examples/langchain-example/pyproject.toml create mode 100644 examples/langchain-example/src/langchain_example/__init__.py create mode 100644 examples/langchain-example/src/langchain_example/dev.py create mode 100644 examples/langchain-example/src/langchain_example/interpreter.py create mode 100644 examples/langchain-example/src/langchain_example/libro_agent.py rename packages/magent-ui-langchain/src/magent_ui_langchain/dev.py => examples/langchain-example/src/langchain_example/tongyi.py (70%) create mode 100644 examples/langchain-example/tongyi_libro_agent.ipynb create mode 100644 packages/magent-core/.gitignore create mode 100644 packages/magent-core/.python-version create mode 100644 packages/magent-core/MANIFEST.in create mode 100644 packages/magent-core/README.md create mode 100644 packages/magent-core/pyproject.toml create mode 100644 packages/magent-core/src/magent_core/__init__.py create mode 100644 packages/magent-core/src/magent_core/config.py create mode 100644 packages/magent-core/src/magent_core/executor_adpator/__init__.py create mode 100644 packages/magent-core/src/magent_core/executor_adpator/adaptor_registry.py create mode 100644 packages/magent-core/src/magent_core/executor_adpator/base_adaptor.py create mode 100644 packages/magent-core/src/magent_core/executor_adpator/current_executor.py create mode 100644 packages/magent-core/src/magent_core/executor_adpator/event.py create mode 100644 packages/magent-core/src/magent_core/llm/__init__.py create mode 100644 packages/magent-core/src/magent_core/llm/environment.py create mode 100644 packages/magent-core/src/magent_core/utils.py create mode 100644 packages/magent-ui-core/.gitignore create mode 100644 packages/magent-ui-core/.python-version create mode 100644 packages/magent-ui-core/MANIFEST.in create mode 100644 packages/magent-ui-core/README.md create mode 100644 packages/magent-ui-core/pyproject.toml create mode 100644 packages/magent-ui-core/src/magent_ui_core/__init__.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/adaptor_registry.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/app.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/base_adaptor.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/config.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/current_executor.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/event.py create mode 100644 packages/magent-ui-core/src/magent_ui_core/utils.py create mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/__init__.py create mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_adaptor.py create mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_openai_executor.py create mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_tongyi_executor.py delete mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/core/__init__.py delete mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/core/adapter_registry.py delete mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/core/current_executor.py delete mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/core/executor.py delete mode 100644 packages/magent-ui-langchain/src/magent_ui_langchain/core/langchain_executor.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d7b6fa0..96302a6d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -131,7 +131,7 @@ "source.fixAll.eslint": "explicit" } }, - "cSpell.words": ["magent", "uvicorn"], + "cSpell.words": ["llms", "magent", "qwen", "tongyi", "uvicorn"], "python.analysis.typeCheckingMode": "basic", "python.analysis.autoImportCompletions": true } diff --git a/README.md b/README.md index 636c4828..fc6c2b9f 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,9 @@ PEER Multi-Agent Dialogue #### Future Plans -- November - - [ ] Workflow nodes support code nodes - - [ ] Workflow nodes support intent recognition - - [ ] Clearer debugging information +- [ ] Workflow nodes support code nodes +- [ ] Workflow nodes support intent recognition +- [ ] Clearer debugging information #### Configuration diff --git a/README.zh-CN.md b/README.zh-CN.md index 75fb383a..7fdf20a2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,10 @@ ## magent-ui -为本地代码研发提供辅助产品化能力,方便用户调试 +为本地代码研发提供辅助产品化能力,方便用户调试。支持多种 Agent 研发框架。 + +- [agentUniverse](https://github.com/alipay/agentUniverse) +- [langchain](https://github.com/langchain-ai/langchain) ### agentUniverse @@ -21,10 +24,9 @@ PEER 多智能体对话 #### 后续计划 -- 11 月 - - [ ] 工作流节点支持代码节点 - - [ ] 工作流节点支持意图识别 - - [ ] 更清晰的调试信息 +- [ ] 工作流节点支持代码节点 +- [ ] 工作流节点支持意图识别 +- [ ] 更清晰的调试信息 #### 配置 diff --git a/api/pyproject.toml b/api/pyproject.toml index 6bb7146b..c6022c24 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -11,10 +11,7 @@ dependencies = [ "pydantic-settings>=2.3.2", "psycopg2-binary>=2.9.9", "fastapi-pagination>=0.12.25", - "langchain>=0.1.20", - "langchain-community>=0.0.38", "sse-starlette>=2.1.2", - "agentuniverse>=0.0.11", "langchain_openai", ] readme = "README.md" diff --git a/examples/langchain-example/.gitignore b/examples/langchain-example/.gitignore new file mode 100644 index 00000000..556211bb --- /dev/null +++ b/examples/langchain-example/.gitignore @@ -0,0 +1,11 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv + diff --git a/examples/langchain-example/.python-version b/examples/langchain-example/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/examples/langchain-example/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/examples/langchain-example/MANIFEST.in b/examples/langchain-example/MANIFEST.in new file mode 100644 index 00000000..58ee6bd5 --- /dev/null +++ b/examples/langchain-example/MANIFEST.in @@ -0,0 +1 @@ +recursive-include src/magent_ui/static * diff --git a/examples/langchain-example/README.md b/examples/langchain-example/README.md new file mode 100644 index 00000000..8b8d64ec --- /dev/null +++ b/examples/langchain-example/README.md @@ -0,0 +1 @@ +# magent_ui diff --git a/examples/langchain-example/openai_agent.ipynb b/examples/langchain-example/openai_agent.ipynb new file mode 100644 index 00000000..7bbba4e9 --- /dev/null +++ b/examples/langchain-example/openai_agent.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "id": "9bf3d1a7-bbbe-445c-b74d-cd31692ae30e", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T11:05:54.981070Z", + "shell.execute_reply.started": "2024-11-19T11:05:54.911264Z", + "to_execute": "2024-11-19T11:05:54.969Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentExecutor(verbose=True, tags=['zero-shot-react-description'], agent=ZeroShotAgent(llm_chain=LLMChain(verbose=False, prompt=PromptTemplate(input_variables=['agent_scratchpad', 'input'], input_types={}, partial_variables={}, template='Answer the following questions as best you can. You have access to the following tools:\\n\\nhash_string(word: str) - This function to get the hash value of a word.\\n\\n:param input_string: The string to be hashed\\n:return: The hash value of the input string\\n\\nUse the following format:\\n\\nQuestion: the input question you must answer\\nThought: you should always think about what to do\\nAction: the action to take, should be one of [hash_string]\\nAction Input: the input to the action\\nObservation: the result of the action\\n... (this Thought/Action/Action Input/Observation can repeat N times)\\nThought: I now know the final answer\\nFinal Answer: the final answer to the original input question\\n\\nBegin!\\n\\nQuestion: {input}\\nThought:{agent_scratchpad}'), llm=ChatOpenAI(client=, async_client=, root_client=, root_async_client=, model_name='gpt-4o', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********')), output_parser=StrOutputParser(), llm_kwargs={}), output_parser=MRKLOutputParser(), allowed_tools=['hash_string']), tools=[StructuredTool(name='hash_string', description='This function to get the hash value of a word.\\n\\n:param input_string: The string to be hashed\\n:return: The hash value of the input string', args_schema=, func=)])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.tools import tool\n", + "from langchain.agents import initialize_agent\n", + "from langchain.agents import AgentType\n", + "\n", + "\n", + "@tool\n", + "def hash_string(word:str):\n", + " \"\"\"\n", + " This function to get the hash value of a word.\n", + "\n", + " :param input_string: The string to be hashed\n", + " :return: The hash value of the input string\n", + " \"\"\"\n", + " import hashlib\n", + " hash_object = hashlib.sha256()\n", + " hash_object.update(word.encode())\n", + " hash_value = hash_object.hexdigest()\n", + " return hash_value\n", + "\n", + "\n", + "llm = ChatOpenAI(\n", + " model=\"gpt-4o\",\n", + " temperature=0,\n", + " max_tokens=None,\n", + " timeout=None,\n", + " max_retries=2,\n", + ")\n", + "\n", + "tools = [hash_string]\n", + "\n", + "agent = initialize_agent(\n", + " tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)\n", + "\n", + "agent\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5e16c965-cedc-45f8-ac41-d0111abf5b5d", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T11:05:56.472219Z", + "shell.execute_reply.started": "2024-11-19T11:05:56.466182Z", + "to_execute": "2024-11-19T11:05:56.523Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ChatOpenAI(client=, async_client=, root_client=, root_async_client=, model_name='gpt-4o', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.agent.llm_chain.llm" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b70afa86-39c9-4278-96de-da12dedb5ed5", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T11:05:59.907734Z", + "shell.execute_reply.started": "2024-11-19T11:05:57.155502Z", + "to_execute": "2024-11-19T11:05:57.193Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new None chain...\u001b[0m\n", + "{'actions': [AgentAction(tool='hash_string', tool_input='Hello, how are you today?', log='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"')], 'messages': [AIMessage(content='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"', additional_kwargs={}, response_metadata={})]}\n", + "\n", + "--- False\n", + "--- False\n", + "--- False\n", + "--- False\n", + "\u001b[32;1m\u001b[1;3mThe word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\n", + "Action: hash_string\n", + "Action Input: \"Hello, how are you today?\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48\u001b[0m\n", + "Thought:{'steps': [AgentStep(action=AgentAction(tool='hash_string', tool_input='Hello, how are you today?', log='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"'), observation='6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48')], 'messages': [HumanMessage(content='6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48', additional_kwargs={}, response_metadata={})]}\n", + "\n", + "--- False\n", + "--- False\n", + "--- False\n", + "--- False\n", + "\u001b[32;1m\u001b[1;3mI now know the final answer.\n", + "Final Answer: The hash value of the sentence \"Hello, how are you today?\" is 6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "{'output': 'The hash value of the sentence \"Hello, how are you today?\" is 6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48.', 'messages': [AIMessage(content='I now know the final answer.\\nFinal Answer: The hash value of the sentence \"Hello, how are you today?\" is 6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48.', additional_kwargs={}, response_metadata={})]}\n", + "\n", + "--- False\n", + "--- False\n", + "--- False\n", + "--- False\n" + ] + }, + { + "data": { + "text/plain": [ + "{'actions': [AgentAction(tool='hash_string', tool_input='Hello, how are you today?', log='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"')],\n", + " 'messages': [AIMessage(content='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"', additional_kwargs={}, response_metadata={}),\n", + " HumanMessage(content='6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48', additional_kwargs={}, response_metadata={}),\n", + " AIMessage(content='I now know the final answer.\\nFinal Answer: The hash value of the sentence \"Hello, how are you today?\" is 6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48.', additional_kwargs={}, response_metadata={})],\n", + " 'steps': [AgentStep(action=AgentAction(tool='hash_string', tool_input='Hello, how are you today?', log='The word \"你好\" is Chinese and translates to \"Hello\" in English. I will now create a sentence starting with \"Hello\" and then get the hash value of the result.\\nAction: hash_string\\nAction Input: \"Hello, how are you today?\"'), observation='6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48')],\n", + " 'output': 'The hash value of the sentence \"Hello, how are you today?\" is 6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48.'}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msg_iterator=agent.stream('Translate the follow word to English, and then write a sentence starting with the word, and then get the hash value of the result, \"你好\"')\n", + "\n", + "from langchain_core.agents import AgentAction, AgentStep, AgentFinish\n", + "from langchain_core.messages import BaseMessageChunk\n", + "\n", + "first = True\n", + "gathered = None\n", + "for chunk in msg_iterator:\n", + " print(chunk)\n", + " print(type(chunk))\n", + " print('---', isinstance(chunk, BaseMessageChunk))\n", + " print('---', isinstance(chunk, AgentAction))\n", + " print('---', isinstance(chunk, AgentStep))\n", + " print('---', isinstance(chunk, AgentFinish))\n", + " \n", + " if first:\n", + " gathered = chunk\n", + " first = False\n", + " else:\n", + " gathered = gathered + chunk\n", + "gathered\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "c40a71ab-e0d4-4dd1-9fc5-7458eddf1d46", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T16:50:27.226125Z", + "shell.execute_reply.started": "2024-11-19T16:50:27.221123Z", + "to_execute": "2024-11-19T16:50:27.275Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'6e766a49e512e0ba0bc935e2aacd3e5a4a34add17f83afc4c9e669c70241cd48'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gathered.get('steps')[0].observation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "884e869b-8c1f-40f2-8bc6-23b3a29ab545", + "metadata": { + "libroFormatter": "formatter-string" + }, + "outputs": [], + "source": [] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/langchain-example/openai_chain.ipynb b/examples/langchain-example/openai_chain.ipynb new file mode 100644 index 00000000..39835c18 --- /dev/null +++ b/examples/langchain-example/openai_chain.ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "c4cee95e-300c-46f7-80c9-8758fc41aa0b", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T05:45:41.025692Z", + "shell.execute_reply.started": "2024-11-19T05:45:39.651334Z", + "to_execute": "2024-11-19T05:45:39.726Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "RunnableBinding(bound=ChatOpenAI(client=, async_client=, root_client=, root_async_client=, model_name='gpt-4o', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********')), kwargs={'tools': [{'type': 'function', 'function': {'name': 'hash_string', 'description': 'This function to get the hash value of a word.\\n\\n:param input_string: The string to be hashed\\n:return: The hash value of the input string', 'parameters': {'properties': {'word': {'type': 'string'}}, 'required': ['word'], 'type': 'object'}}}]}, config={}, config_factories=[])" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def hash_string(word:str):\n", + " \"\"\"\n", + " This function to get the hash value of a word.\n", + "\n", + " :param input_string: The string to be hashed\n", + " :return: The hash value of the input string\n", + " \"\"\"\n", + " import hashlib\n", + " hash_object = hashlib.sha256()\n", + " hash_object.update(word.encode())\n", + " hash_value = hash_object.hexdigest()\n", + " return hash_value\n", + "\n", + "\n", + "llm = ChatOpenAI(\n", + " model=\"gpt-4o\",\n", + " temperature=0,\n", + " max_tokens=None,\n", + " timeout=None,\n", + " max_retries=2,\n", + ")\n", + "\n", + "\n", + "llm_with_tools = llm.bind_tools(\n", + " [hash_string])\n", + "\n", + "llm_with_tools" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "06bd87c4-d7ee-40b0-b21d-eb2e6834fd2b", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T05:45:41.028736Z", + "shell.execute_reply.started": "2024-11-19T05:45:41.026742Z", + "to_execute": "2024-11-19T05:45:41.034Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ChatOpenAI(client=, async_client=, root_client=, root_async_client=, model_name='gpt-4o', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'))" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "llm_with_tools.bound" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b51a53e-2080-40b0-9cb1-f2533842df26", + "metadata": { + "libroFormatter": "formatter-string" + }, + "outputs": [], + "source": [ + "from langchain_core.messages import BaseMessageChunk\n", + "msg_iterator=llm_with_tools.stream('Get the hash value of the follow word, \"hello\"')\n", + "\n", + "\n", + "first = True\n", + "gathered = None\n", + "for chunk in msg_iterator:\n", + " print(chunk)\n", + " print('---', isinstance(chunk, BaseMessageChunk))\n", + " if first:\n", + " gathered = chunk\n", + " first = False\n", + " else:\n", + " gathered = gathered + chunk\n", + "gathered" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b45d1b14-134a-4b49-a881-ba4f2370e0ed", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T06:16:13.329345Z", + "shell.execute_reply.started": "2024-11-19T06:16:12.257952Z", + "to_execute": "2024-11-19T06:16:12.340Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'function': {'arguments': '', 'name': 'hash_string'}, 'type': 'function'}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' tool_calls=[{'name': 'hash_string', 'args': {}, 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'type': 'tool_call'}] tool_call_chunks=[{'name': 'hash_string', 'args': '', 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': None, 'function': {'arguments': '{\"', 'name': None}, 'type': None}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' tool_calls=[{'name': '', 'args': {}, 'id': None, 'type': 'tool_call'}] tool_call_chunks=[{'name': None, 'args': '{\"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': None, 'function': {'arguments': 'word', 'name': None}, 'type': None}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' invalid_tool_calls=[{'name': None, 'args': 'word', 'id': None, 'error': None, 'type': 'invalid_tool_call'}] tool_call_chunks=[{'name': None, 'args': 'word', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': None, 'function': {'arguments': '\":\"', 'name': None}, 'type': None}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' invalid_tool_calls=[{'name': None, 'args': '\":\"', 'id': None, 'error': None, 'type': 'invalid_tool_call'}] tool_call_chunks=[{'name': None, 'args': '\":\"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': None, 'function': {'arguments': 'hello', 'name': None}, 'type': None}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' invalid_tool_calls=[{'name': None, 'args': 'hello', 'id': None, 'error': None, 'type': 'invalid_tool_call'}] tool_call_chunks=[{'name': None, 'args': 'hello', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': None, 'function': {'arguments': '\"}', 'name': None}, 'type': None}]} response_metadata={} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c' invalid_tool_calls=[{'name': None, 'args': '\"}', 'id': None, 'error': None, 'type': 'invalid_tool_call'}] tool_call_chunks=[{'name': None, 'args': '\"}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]\n", + "--- True\n", + "content='' additional_kwargs={} response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae'} id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c'\n", + "--- True\n" + ] + }, + { + "data": { + "text/plain": [ + "AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'function': {'arguments': '{\"word\":\"hello\"}', 'name': 'hash_string'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae'}, id='run-7713a9de-5723-4bac-8b2d-479d7769ec4c', tool_calls=[{'name': 'hash_string', 'args': {'word': 'hello'}, 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'type': 'tool_call'}], tool_call_chunks=[{'name': 'hash_string', 'args': '{\"word\":\"hello\"}', 'id': 'call_p8hKiMLc4KDG8FKZB4ow7qAH', 'index': 0, 'type': 'tool_call_chunk'}])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import BaseMessageChunk\n", + "msg_iterator=llm_with_tools.stream('Get the hash value of the follow word, \"hello\"')\n", + "\n", + "\n", + "first = True\n", + "gathered = None\n", + "for chunk in msg_iterator:\n", + " print(chunk)\n", + " print('---', isinstance(chunk, BaseMessageChunk))\n", + " if first:\n", + " gathered = chunk\n", + " first = False\n", + " else:\n", + " gathered = gathered + chunk\n", + "gathered" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3a763ab-e086-43db-b879-eba1c0707c1e", + "metadata": { + "libroFormatter": "formatter-string" + }, + "outputs": [], + "source": [] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/langchain-example/openai_libro_agent.ipynb b/examples/langchain-example/openai_libro_agent.ipynb new file mode 100644 index 00000000..302de2bb --- /dev/null +++ b/examples/langchain-example/openai_libro_agent.ipynb @@ -0,0 +1,303 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "889d98c9-bc87-4e10-b4fc-4deb8cc4d992", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.tools import tool\n", + "from langchain.agents import initialize_agent\n", + "from langchain.agents import AgentType\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain.embeddings import OpenAIEmbeddings\n", + "from langchain.vectorstores import FAISS\n", + "from langchain.schema import Document" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "46799807-e779-4482-a90b-36f2c587f8d1", + "metadata": {}, + "outputs": [], + "source": [ + "# 读取网页内容\n", + "def load_webpage(url: str):\n", + " response = requests.get(url)\n", + " response.raise_for_status() # 确保请求成功\n", + " soup = BeautifulSoup(response.text, 'html.parser')\n", + " \n", + " # 提取文本内容(可以根据需要调整)\n", + " paragraphs = soup.find_all('p')\n", + " text = \"\\n\".join([para.get_text() for para in paragraphs])\n", + " return text\n", + "\n", + "# 读取网页链接\n", + "url = \"https://medium.com/@libro.development/libro-a-customizable-ai-notebook-ab31bd4197b9\" # 替换为你想读取的网页链接\n", + "webpage_content = load_webpage(url)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8a9331f2-8116-4a13-ae48-4d9589aa3e3a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple\n", + "Requirement already satisfied: faiss-cpu in /Users/johnny/anaconda3/envs/test/lib/python3.11/site-packages (1.9.0.post1)\n", + "Requirement already satisfied: numpy<3.0,>=1.25.0 in /Users/johnny/anaconda3/envs/test/lib/python3.11/site-packages (from faiss-cpu) (1.26.4)\n", + "Requirement already satisfied: packaging in /Users/johnny/anaconda3/envs/test/lib/python3.11/site-packages (from faiss-cpu) (23.2)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install faiss-cpu" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6b3bec39-2617-4f61-9f1d-d4039828fca9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/np/009dffqj74vbz89c1xghgskh0000gp/T/ipykernel_61692/719554337.py:11: LangChainDeprecationWarning: The class `OpenAIEmbeddings` was deprecated in LangChain 0.0.9 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-openai package and should be used instead. To use it run `pip install -U :class:`~langchain-openai` and import as `from :class:`~langchain_openai import OpenAIEmbeddings``.\n", + " embeddings = OpenAIEmbeddings()\n" + ] + } + ], + "source": [ + "from langchain.schema import Document\n", + "\n", + "# 文本分割\n", + "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)\n", + "chunks = text_splitter.split_text(webpage_content)\n", + "\n", + "# 创建文档对象列表\n", + "documents = [Document(page_content=chunk) for chunk in chunks]\n", + "\n", + "# 创建向量存储\n", + "embeddings = OpenAIEmbeddings()\n", + "vector_store = FAISS.from_documents(documents, embeddings)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b86ccc34-f793-490d-9659-5d6ad4b881b8", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/np/009dffqj74vbz89c1xghgskh0000gp/T/ipykernel_61692/2518069785.py:30: LangChainDeprecationWarning: The function `initialize_agent` was deprecated in LangChain 0.1.0 and will be removed in 1.0. Use :meth:`~Use new agent constructor methods like create_react_agent, create_json_agent, create_structured_chat_agent, etc.` instead.\n", + " agent = initialize_agent(\n" + ] + } + ], + "source": [ + "# 创建一个RAG工具\n", + "@tool\n", + "def rag_query(query: str) -> str:\n", + " \"\"\"\n", + " This function retrieves relevant information from the loaded webpage data\n", + " and generates a response using the language model.\n", + "\n", + " :param query: The user's query\n", + " :return: The generated response based on the retrieved information\n", + " \"\"\"\n", + " # Assuming `vector_store` and `llm` are accessible in the scope\n", + " relevant_docs = vector_store.similarity_search(query)\n", + " context = \"\\n\".join([doc.page_content for doc in relevant_docs])\n", + " response = llm(f\"{context}\\n\\nUser: {query}\\nAssistant:\")\n", + " return response\n", + "\n", + "# 设置语言模型\n", + "llm = ChatOpenAI(\n", + " model=\"gpt-4o-mini\",\n", + " temperature=0,\n", + " max_tokens=None,\n", + " timeout=None,\n", + " max_retries=2,\n", + ")\n", + "\n", + "# 定义工具\n", + "tools = [rag_query]\n", + "\n", + "# 初始化代理\n", + "agent = initialize_agent(\n", + " tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "418b59b7-7ff2-429a-a2bb-cdcd007dcf03", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(\"/Users/johnny/Code/difizen/magent/packages/magent-ui-langchain/src\")\n", + "\n", + "from magent_ui_langchain import launch\n", + "# launch(agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f558ed80-a2fd-4f36-82d9-62f83e613d06", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Current log level is\n", + "INFO: Started server process [61692]\n", + "INFO: Waiting for application startup.\n", + "INFO: Server is running at http://localhost:9563/\n" + ] + }, + { + "data": { + "text/html": [ + "

Server is running at: http://localhost:9563/

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACJ0lEQVR4nO2ZTYrjMBCFvxoLspQhB+ijyDfrM/UNrKP0ARqsZUDmzUJy+mdoSAJOAlO1CI79LR6YKj+9MnFh5T+XkuCoo4466qije6LWKwCsRrYAlO32tLsAR69BkyRpAfII9qqTAYMkSd/RfQQ4eg1aegvZ6/tBUA7tHW39tr8AR29DVyO/VGx6lABHf6vw80Z+qUGUUEW5hwBHb0CjpBmAErCJ1QCQVO8jwNHL0WxmZiOQlkHAIJtYmyW8hwBHL6k2Cb/ET3kcEGU1iN9TqYdrdbSft6bSz1ua46m5jO285Z7wedCvLiPNqyktyNL7QUasQbCa9hTg6A3frdZMB7UYI4+rNb9hI9i0vwBHL6otrRgEcfN/SZLmWNtV//twrY5ukzAu9ASjHCt5/AjAQUYZ9xXg6A29FSuao3owmJZBJFU0x9oQ760nQHtvpbeAQWijUPnlZFCOPSxM834CHL3NEwpWI70da/OEecQgDrI9BTh6W/LUv1uxbhYRIL0HeiT1NFr/d7S/j1iBEtDMas0O5vEuAhy9qNok3KLAoQpORreDx0qbjvN+Ahy9Hj3vjs1GaFvk1mqf4fyzaHX0vDuGEoB4MrKZfX3wNFodPVdaBrUGmxlEWu4twNHf65/dMbGiPAUsaTVRhuoO/lnQc/IkoICyDdWIH4E8fmCf2eHjtTp6TnWBHu325KlbDcBT3WdBf+6O1X+aHbyDAEcdddRRRx29Av0LuyT9iPLdjSAAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Application startup complete.\n", + "INFO: Uvicorn running on http://0.0.0.0:9563 (Press CTRL+C to quit)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 127.0.0.1:60031 - \"GET / HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:60031 - \"GET /app/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:60032 - \"GET / HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:60032 - \"GET /app/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:60204 - \"GET / HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:60204 - \"GET /app/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:60521 - \"POST /api/v1/chat-stream HTTP/1.1\" 200 OK\n", + "\n", + "\n", + "\u001b[1m> Entering new None chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mTo answer the question about the characteristics of \"libro,\" I need to gather relevant information about it. I will perform a query to find out more about the features or characteristics associated with \"libro.\" \n", + "\n", + "Action: rag_query \n", + "Action Input: \"libro 特性\" \u001b[0m" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/np/009dffqj74vbz89c1xghgskh0000gp/T/ipykernel_61692/2518069785.py:14: LangChainDeprecationWarning: The method `BaseChatModel.__call__` was deprecated in langchain-core 0.1.7 and will be removed in 1.0. Use :meth:`~invoke` instead.\n", + " response = llm(f\"{context}\\n\\nUser: {query}\\nAssistant:\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation: \u001b[36;1m\u001b[1;3mcontent='Libro 是一个可定制的笔记本解决方案,专为支持生成式 AI 功能而设计。它提供了商业级的用户体验,能够无缝集成 AI,使其易于融入开发环境,构建前沿的 AI 和数据科学解决方案。以下是 Libro 的一些主要特性:\\n\\n1. **AI 工作流**:\\n - **Prompt Cells**:用户可以将 AI 集成到笔记本的执行逻辑中,实现无缝的 AI 工作流。\\n - **解释器模式**:用户可以使用自然语言与笔记本互动,生成所需的结果。\\n\\n2. **多种产品形式**:\\n - Libro 支持多种用例,既可以作为文档编辑器,也可以作为报告演示工具,随时轻松创建演示和报告。\\n\\n3. **轻松的数据集成**:\\n - 用户可以使用 SQL 单元轻松执行数据操作。简单的配置即可连接到自己的数据库,支持在 SQL 中使用 Python 上下文变量,并将查询结果继续作为数据框使用,确保数据工作流的顺畅和高效。\\n\\n4. **功能齐全的代码编辑器**:\\n - Libro 将继续利用其灵活性和易集成性,探索更多笔记本类产品的用例。将不断扩展对不同运行时的支持,并引入更好的交互体验控件,使 Libro 在用户体验方面成为最佳的笔记本产品。\\n\\n5. **开放框架**:\\n - Libro 提供开放框架,支持自定义 UI 和执行内核,允许在所有模块层进行二次开发和定制。\\n\\n6. **丰富的附加功能**:\\n - Libro 除了基本的笔记本功能外,还提供了丰富的附加功能,增强用户的工作体验。\\n\\n通过这些特性,Libro 旨在更好地满足数据和 AI 工作流的需求,推动基于笔记本的工作方式的未来发展。欢迎开发者参与构建 Libro 项目,更多信息请访问 [GitHub](https://github.com/difizen/libro) 或 [官方网站](https://libro.difizen.net/)。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 468, 'prompt_tokens': 676, 'total_tokens': 1144, 'prompt_tokens_details': {'cached_tokens': 0, 'audio_tokens': 0}, 'completion_tokens_details': {'reasoning_tokens': 0, 'audio_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None} id='run-2295ee3b-7e24-4e40-8c1d-414f0817031e-0' usage_metadata={'input_tokens': 676, 'output_tokens': 468, 'total_tokens': 1144, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI now know the final answer. \n", + "Final Answer: Libro 是一个可定制的笔记本解决方案,具有以下主要特性: \n", + "1. **AI 工作流**:支持将 AI 集成到笔记本的执行逻辑中,用户可以使用自然语言与笔记本互动。 \n", + "2. **多种产品形式**:既可以作为文档编辑器,也可以作为报告演示工具。 \n", + "3. **轻松的数据集成**:支持 SQL 单元执行数据操作,简化数据库连接。 \n", + "4. **功能齐全的代码编辑器**:灵活性和易集成性,支持不同运行时。 \n", + "5. **开放框架**:支持自定义 UI 和执行内核,允许二次开发。 \n", + "6. **丰富的附加功能**:增强用户的工作体验。 \n", + "这些特性旨在满足数据和 AI 工作流的需求,推动基于笔记本的工作方式的发展。\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "INFO: 127.0.0.1:60793 - \"GET / HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:60793 - \"GET /app/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:60794 - \"POST /api/v1/chat-stream HTTP/1.1\" 200 OK\n", + "\n", + "\n", + "\u001b[1m> Entering new None chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mTo answer the question about the size of the model, I need to gather specific information regarding the model's parameters or architecture. \n", + "Action: rag_query\n", + "Action Input: \"模型有多大?\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mcontent='模型的大小可以根据具体的实现和使用的技术而有所不同。一般来说,生成式AI模型的大小可以从几百万参数到数十亿参数不等。例如,较小的模型可能只有几百万参数,而大型模型如GPT-3则有1750亿个参数。具体的模型大小取决于其设计目标和应用场景。如果您有特定的模型或框架在心中,请提供更多信息,以便我能给出更准确的回答。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 105, 'prompt_tokens': 542, 'total_tokens': 647, 'prompt_tokens_details': {'cached_tokens': 0, 'audio_tokens': 0}, 'completion_tokens_details': {'reasoning_tokens': 0, 'audio_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None} id='run-c30c8e7a-77b1-4424-b8e4-3d80a79c0e62-0' usage_metadata={'input_tokens': 542, 'output_tokens': 105, 'total_tokens': 647, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI now know the final answer. \n", + "Final Answer: 模型的大小可以根据具体的实现和使用的技术而有所不同。一般来说,生成式AI模型的大小可以从几百万参数到数十亿参数不等。例如,较小的模型可能只有几百万参数,而大型模型如GPT-3则有1750亿个参数。具体的模型大小取决于其设计目标和应用场景。\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + } + ], + "source": [ + "launch(agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc353f0d-51ad-4a9e-a97d-b71b56a813e8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "test", + "language": "python", + "name": "test" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/langchain-example/openai_llm.ipynb b/examples/langchain-example/openai_llm.ipynb new file mode 100644 index 00000000..5086507a --- /dev/null +++ b/examples/langchain-example/openai_llm.ipynb @@ -0,0 +1,202 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "0d3d6934-fdf9-4671-9cb9-3b0c362ce075", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T05:30:00.667702Z", + "shell.execute_reply.started": "2024-11-19T05:30:00.170539Z", + "to_execute": "2024-11-19T05:30:00.193Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ChatOpenAI(client=, async_client=, root_client=, root_async_client=, model_name='gpt-4o', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'))" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(\n", + " model=\"gpt-4o\",\n", + " temperature=0,\n", + " max_tokens=None,\n", + " timeout=None,\n", + " max_retries=2,\n", + ")\n", + "llm" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "462a8826-21f7-4606-893b-92c15f631a1e", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T05:30:01.135926Z", + "shell.execute_reply.started": "2024-11-19T05:30:01.131959Z", + "to_execute": "2024-11-19T05:30:01.179Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'gpt-4o'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "llm.model_name" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "05628fd8-eb87-48f4-9ce5-cce1aa42f6ec", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T05:45:27.622599Z", + "shell.execute_reply.started": "2024-11-19T05:45:26.592294Z", + "to_execute": "2024-11-19T05:45:26.657Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content='Hello' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content='!' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' How' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' can' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' I' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' assist' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' you' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content=' today' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content='?' additional_kwargs={} response_metadata={} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n", + "content='' additional_kwargs={} response_metadata={'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_7f6be3efb0'} id='run-a6ddf224-d279-44dd-884c-5947e091c11b'\n" + ] + }, + { + "data": { + "text/plain": [ + "AIMessageChunk(content='Hello! How can I assist you today?', additional_kwargs={}, response_metadata={'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_7f6be3efb0'}, id='run-a6ddf224-d279-44dd-884c-5947e091c11b')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msg_iterator = llm.stream('hi')\n", + "\n", + "first = True\n", + "gathered = None\n", + "for chunk in msg_iterator:\n", + " print(chunk)\n", + " if first:\n", + " gathered = chunk\n", + " first = False\n", + " else:\n", + " gathered = gathered + chunk\n", + "gathered" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "0a7d8650-52e8-4589-bc3d-4861c9f2a793", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T07:10:58.824230Z", + "shell.execute_reply.started": "2024-11-19T07:10:57.643149Z", + "to_execute": "2024-11-19T07:10:57.720Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17, 'completion_tokens_details': {'audio_tokens': 0, 'reasoning_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9ee9e968ea', 'finish_reason': 'stop', 'logprobs': None}, id='run-5633a3bf-3ecd-4a6e-a26c-4b6ae3fac2b0-0', usage_metadata={'input_tokens': 8, 'output_tokens': 9, 'total_tokens': 17, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msg = llm.invoke('hi')\n", + "msg" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c7e9afc2-c484-4a67-8e6e-9ffcb3a40353", + "metadata": { + "execution": { + "shell.execute_reply.end": "2024-11-19T02:24:02.555064Z", + "shell.execute_reply.started": "2024-11-19T02:24:01.695757Z", + "to_execute": "2024-11-19T02:24:01.756Z" + }, + "libroFormatter": "formatter-string" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "ChatTongyi(client=, model_name='qwen-vl-max', model_kwargs={}, dashscope_api_key=SecretStr('**********'), max_retries=2)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.chat_models.tongyi import ChatTongyi\n", + "\n", + "tongyi_chat = ChatTongyi(\n", + " model=\"qwen-vl-max\",\n", + " max_retries=2,\n", + ") #\n", + "tongyi_chat" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f19fe055-7850-4d01-9d49-4e93b54eb9b5", + "metadata": { + "execution": {}, + "libroFormatter": "formatter-string" + }, + "outputs": [], + "source": [] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/langchain-example/pyproject.toml b/examples/langchain-example/pyproject.toml new file mode 100644 index 00000000..69e8c365 --- /dev/null +++ b/examples/langchain-example/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "langchain-example" +version = "0.1.0" +description = "" +authors = [{ name = "brokun", email = "brokun0128@gmail.com" }] + +dependencies = [ + "magent-ui-langchain>=0.1.0.dev0", + "langchain>=0.3.7", + "nbformat>=5.10.4", + "langchain-community>=0.3.7", + "jupyter-client>=8.6.3", + "nbclient>=0.10.0", +] +readme = "README.md" +requires-python = ">= 3.10" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true + +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/langchain_example"] + +[tool.rye.scripts] +dev = "python src/langchain_example/dev.py" +interpreter = "python src/langchain_example/interpreter.py" +tongyi = "python src/langchain_example/tongyi.py" diff --git a/examples/langchain-example/src/langchain_example/__init__.py b/examples/langchain-example/src/langchain_example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/langchain-example/src/langchain_example/dev.py b/examples/langchain-example/src/langchain_example/dev.py new file mode 100644 index 00000000..29fb2ce1 --- /dev/null +++ b/examples/langchain-example/src/langchain_example/dev.py @@ -0,0 +1,13 @@ +from langchain_openai import ChatOpenAI +from magent_ui_langchain import launch + +llm = ChatOpenAI( + model="gpt-4o", + temperature=0, + max_tokens=None, + timeout=None, + max_retries=2, +) + + +launch(llm) diff --git a/examples/langchain-example/src/langchain_example/interpreter.py b/examples/langchain-example/src/langchain_example/interpreter.py new file mode 100644 index 00000000..50b9008f --- /dev/null +++ b/examples/langchain-example/src/langchain_example/interpreter.py @@ -0,0 +1,59 @@ +from langchain_openai import ChatOpenAI +from magent_ui_langchain import launch +from langchain_core.tools import tool +from langchain_core.output_parsers import PydanticToolsParser +from pydantic import BaseModel, Field +from langchain.agents import initialize_agent +from langchain.agents import AgentType +import sys + + +sys.path.append( + "/Users/brokun/github/libro-code-interpreter/libro-client/src") +sys.path.append( + "/Users/brokun/github/libro-code-interpreter/libro-code-interpreter/src") + + +@tool +def ipython_executor(code: str): + """A Python code executor. Use this to execute python commands. Input should be a valid python command. + + Args: + code: python code + """ + print(code) + from libro_code_interpreter import execute_ipython + return execute_ipython(code) + + +llm = ChatOpenAI( + model="gpt-4o", + temperature=0, + max_tokens=None, + timeout=None, + max_retries=2, +) + + +class IPythonExecutor(BaseModel): + """A Python code executor. Use this to execute python commands. Input should be a valid python command.""" + + code: str = Field(..., description="Python code") + + # def __init__(self, code: str): + # self.code = code + # print('IPythonExecutor', code) + + +llm_with_tools = llm.bind_tools( + [IPythonExecutor]) + +tools = [ipython_executor] + +agent = initialize_agent( + tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True) + + +# chain = llm_with_tools | PydanticToolsParser(tools=[IPythonExecutor]) + +launch(agent) diff --git a/examples/langchain-example/src/langchain_example/libro_agent.py b/examples/langchain-example/src/langchain_example/libro_agent.py new file mode 100644 index 00000000..7ff9b071 --- /dev/null +++ b/examples/langchain-example/src/langchain_example/libro_agent.py @@ -0,0 +1,82 @@ +import sys +sys.path.append("/Users/johnny/Code/difizen/magent/packages/magent-ui-langchain/src") + + + +import requests +from bs4 import BeautifulSoup +from langchain_openai import ChatOpenAI +from langchain_core.tools import tool +from langchain.agents import initialize_agent +from langchain.agents import AgentType +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain.embeddings import OpenAIEmbeddings +from langchain.vectorstores import FAISS +from langchain.schema import Document + +from magent_ui_langchain import launch + +# 读取网页内容 +def load_webpage(url: str): + response = requests.get(url) + response.raise_for_status() # 确保请求成功 + soup = BeautifulSoup(response.text, 'html.parser') + + # 提取文本内容(可以根据需要调整) + paragraphs = soup.find_all('p') + text = "\n".join([para.get_text() for para in paragraphs]) + return text + +# 读取网页链接 +url = "https://medium.com/@libro.development/libro-a-customizable-ai-notebook-ab31bd4197b9" # 替换为你想读取的网页链接 +webpage_content = load_webpage(url) + +from langchain.schema import Document + +# 文本分割 +text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100) +chunks = text_splitter.split_text(webpage_content) + +# 创建文档对象列表 +documents = [Document(page_content=chunk) for chunk in chunks] + +# 创建向量存储 +embeddings = OpenAIEmbeddings() +vector_store = FAISS.from_documents(documents, embeddings) + +# 创建一个RAG工具 +@tool +def rag_query(query: str) -> str: + """ + This function retrieves relevant information from the loaded webpage data + and generates a response using the language model. + + :param query: The user's query + :return: The generated response based on the retrieved information + """ + # Assuming `vector_store` and `llm` are accessible in the scope + relevant_docs = vector_store.similarity_search(query) + context = "\n".join([doc.page_content for doc in relevant_docs]) + response = llm(f"{context}\n\nUser: {query}\nAssistant:") + return response + +# 设置语言模型 +llm = ChatOpenAI( + model="gpt-4o-mini", + temperature=0, + max_tokens=None, + timeout=None, + max_retries=2, +) + +# 定义工具 +tools = [rag_query] + +# 初始化代理 +agent = initialize_agent( + tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True +) + + + +launch(agent) diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/dev.py b/examples/langchain-example/src/langchain_example/tongyi.py similarity index 70% rename from packages/magent-ui-langchain/src/magent_ui_langchain/dev.py rename to examples/langchain-example/src/langchain_example/tongyi.py index ecd520f7..a40d729c 100644 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/dev.py +++ b/examples/langchain-example/src/langchain_example/tongyi.py @@ -3,15 +3,11 @@ # from langchain_openai import ChatOpenAI from magent_ui_langchain import launch -from langchain_community.chat_models import ChatTongyi +from langchain_community.chat_models.tongyi import ChatTongyi tongyi_chat = ChatTongyi( model="qwen-vl-max", - temperature=0, - max_tokens=None, - timeout=None, max_retries=2, -) +) # type: ignore launch(tongyi_chat) - diff --git a/examples/langchain-example/tongyi_libro_agent.ipynb b/examples/langchain-example/tongyi_libro_agent.ipynb new file mode 100644 index 00000000..ee59cd3c --- /dev/null +++ b/examples/langchain-example/tongyi_libro_agent.ipynb @@ -0,0 +1,317 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "fc30c5bb-db9e-43ca-ba8c-4f49f9811106", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from typing import List\n", + "from langchain_community.chat_models.tongyi import ChatTongyi\n", + "from langchain_community.embeddings import DashScopeEmbeddings\n", + "from langchain_core.tools import tool\n", + "from langchain.agents import initialize_agent\n", + "from langchain.agents import AgentType\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain.vectorstores import FAISS\n", + "from langchain.schema import Document" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "dd437770-53b6-4ff0-8c2a-7ef0ea186f75", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# 读取网页内容\n", + "def load_webpage(url: str) -> str:\n", + " \"\"\"加载网页内容并返回文本\"\"\"\n", + " response = requests.get(url)\n", + " response.raise_for_status()\n", + " soup = BeautifulSoup(response.text, 'html.parser')\n", + " paragraphs = soup.find_all('p')\n", + " text = \"\\n\".join([para.get_text() for para in paragraphs])\n", + " return text\n", + "\n", + "def create_vector_store(text: str, chunk_size: int = 1000, chunk_overlap: int = 100) -> FAISS:\n", + " \"\"\"创建向量存储\"\"\"\n", + " # 文本分割\n", + " text_splitter = RecursiveCharacterTextSplitter(\n", + " chunk_size=chunk_size,\n", + " chunk_overlap=chunk_overlap\n", + " )\n", + " chunks = text_splitter.split_text(text)\n", + " \n", + " # 创建文档对象列表\n", + " documents = [Document(page_content=chunk) for chunk in chunks]\n", + " \n", + " # 创建向量存储\n", + " embeddings = DashScopeEmbeddings(\n", + " model=\"text-embedding-v2\",\n", + " )\n", + " return FAISS.from_documents(documents, embeddings)\n", + "\n", + "# 初始化向量存储\n", + "url = \"https://medium.com/@libro.development/libro-a-customizable-ai-notebook-ab31bd4197b9\"\n", + "webpage_content = load_webpage(url)\n", + "vector_store = create_vector_store(webpage_content)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd4b9f3e-f474-4892-b768-885c5aabc93c", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2e3b8fb0-6be8-4906-9cce-7828a0189495", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/np/009dffqj74vbz89c1xghgskh0000gp/T/ipykernel_659/3884561080.py:37: LangChainDeprecationWarning: The function `initialize_agent` was deprecated in LangChain 0.1.0 and will be removed in 1.0. Use :meth:`~Use new agent constructor methods like create_react_agent, create_json_agent, create_structured_chat_agent, etc.` instead.\n", + " agent = initialize_agent(\n" + ] + } + ], + "source": [ + "\n", + "# 设置语言模型\n", + "llm = ChatTongyi(\n", + " model=\"qwen-max\", # 或选择其他可用模型,如 qwen-turbo, qwen-plus\n", + " temperature=0,\n", + " max_retries=2,\n", + ")\n", + "\n", + "# 创建RAG工具\n", + "@tool\n", + "def rag_query(query: str) -> str:\n", + " \"\"\"\n", + " 从加载的网页数据中检索相关信息并生成响应\n", + " :param query: 用户的查询\n", + " :return: 基于检索信息生成的响应\n", + " \"\"\"\n", + " # 获取相关文档\n", + " relevant_docs = vector_store.similarity_search(query)\n", + " context = \"\\n\".join([doc.page_content for doc in relevant_docs])\n", + " \n", + " # 构建prompt并获取响应\n", + " prompt = f\"\"\"基于以下背景信息回答用户的问题:\n", + " \n", + "背景信息:\n", + "{context}\n", + "\n", + "用户问题:{query}\n", + "\n", + "请提供准确、相关的回答:\"\"\"\n", + " \n", + " response = llm.invoke(prompt)\n", + " return response.content\n", + "\n", + "# 定义工具列表\n", + "tools = [rag_query]\n", + "\n", + "# 初始化agent\n", + "agent = initialize_agent(\n", + " tools=tools,\n", + " llm=llm,\n", + " agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,\n", + " verbose=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5ca58945-0e88-4cad-8c89-6ee591a7b643", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m我需要了解\"libro\"具体指的是什么,因为这可能是特定软件、库或其它技术的名称。但根据我的现有知识,\"libro\"在西班牙语中是“书”的意思。如果这里的\"libro\"是指某种特定的技术或者项目的话,那么我可能需要进一步查询相关信息来给出准确的回答。\n", + "Action: rag_query\n", + "Action Input: libro的特性\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mLibro 是一个高度可定制的笔记本解决方案,旨在支持生成式AI能力,并提供商业级用户体验。它的一些主要特性包括:\n", + "\n", + "1. **灵活的场景自定义**:用户可以根据自己的需求自由组合Libro的原生模块,以实现个性化的应用场景配置。\n", + "\n", + "2. **丰富的附加功能集**:除了基础的功能外,Libro还提供了许多额外的工具和服务来增强用户的开发体验。\n", + "\n", + "3. **开放框架**:对于那些希望进行二次开发或深度定制的用户来说,Libro提供了一个开放框架,允许对UI界面及执行内核等各个层面进行修改。\n", + "\n", + "4. **AI工作流集成**:通过Prompt Cells(提示单元格),用户能够将AI技术无缝地融入到笔记本的执行逻辑中,从而构建出流畅的人工智能工作流程。此外,解释器模式让用户可以通过自然语言与笔记本交互,从而生成所需的结果。\n", + "\n", + "5. **多样的产品形态**:Libro不仅可以用作文档编辑器,还可以作为报告展示工具使用,非常适合快速创建演示文稿或汇报材料。\n", + "\n", + "6. **易于数据整合**:Libro内置了SQL单元格功能,使得直接在笔记本文档中执行数据库查询变得非常简便。只需要简单的设置就可以连接至个人数据库,并且可以在SQL语句中引用Python上下文变量,查询结果也可以直接作为DataFrame对象处理。\n", + "\n", + "7. **全功能代码编辑器**:Libro为用户提供了一款功能齐全的代码编辑环境,有助于提高编程效率并简化代码审查过程。\n", + "\n", + "8. **持续创新与发展**:项目团队致力于探索更多关于大型模型集成的应用场景,努力让Libro成为最优秀的笔记本产品之一,在用户体验方面达到顶尖水平。\n", + "\n", + "9. **开源社区参与**:欢迎来自不同背景的开发者加入Libro项目的建设之中,共同推动其进步和发展。官方GitHub仓库地址为 https://github.com/difizen/libro 。\n", + "\n", + "总之,Libro是一款面向未来、注重用户体验并且紧密融合最新AI技术的高级笔记本应用。\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m我现在知道了Libro的主要特性。它是一个高度可定制的笔记本解决方案,专注于提供生成式AI能力及商业级用户体验。其特点涵盖了从灵活自定义场景到丰富的附加功能集,再到开放框架支持二次开发等多个方面。\n", + "\n", + "Final Answer: Libro 是一个面向未来的高级笔记本应用,它的主要特性包括:灵活的场景自定义、丰富的附加功能集、开放框架允许深度定制、AI工作流集成(如通过Prompt Cells和解释器模式)、多样化的使用形态(既可作为文档编辑器也能用于报告展示)、易于数据整合(内置SQL单元格)、全功能代码编辑器以及持续的产品创新与开源社区的支持。这些特性共同使Libro成为一个强大的工具,适用于需要高效开发环境和强大AI集成能力的各种用户。\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + } + ], + "source": [ + "response = agent.invoke(\"libro有哪些特性?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a52f2803-582c-4212-9225-6e0e237de72a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Current log level is\n", + "INFO: Started server process [659]\n", + "INFO: Waiting for application startup.\n", + "INFO: Server is running at http://localhost:9563/\n" + ] + }, + { + "data": { + "text/html": [ + "

Server is running at: http://localhost:9563/

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACJ0lEQVR4nO2ZTYrjMBCFvxoLspQhB+ijyDfrM/UNrKP0ARqsZUDmzUJy+mdoSAJOAlO1CI79LR6YKj+9MnFh5T+XkuCoo4466qije6LWKwCsRrYAlO32tLsAR69BkyRpAfII9qqTAYMkSd/RfQQ4eg1aegvZ6/tBUA7tHW39tr8AR29DVyO/VGx6lABHf6vw80Z+qUGUUEW5hwBHb0CjpBmAErCJ1QCQVO8jwNHL0WxmZiOQlkHAIJtYmyW8hwBHL6k2Cb/ET3kcEGU1iN9TqYdrdbSft6bSz1ua46m5jO285Z7wedCvLiPNqyktyNL7QUasQbCa9hTg6A3frdZMB7UYI4+rNb9hI9i0vwBHL6otrRgEcfN/SZLmWNtV//twrY5ukzAu9ASjHCt5/AjAQUYZ9xXg6A29FSuao3owmJZBJFU0x9oQ760nQHtvpbeAQWijUPnlZFCOPSxM834CHL3NEwpWI70da/OEecQgDrI9BTh6W/LUv1uxbhYRIL0HeiT1NFr/d7S/j1iBEtDMas0O5vEuAhy9qNok3KLAoQpORreDx0qbjvN+Ahy9Hj3vjs1GaFvk1mqf4fyzaHX0vDuGEoB4MrKZfX3wNFodPVdaBrUGmxlEWu4twNHf65/dMbGiPAUsaTVRhuoO/lnQc/IkoICyDdWIH4E8fmCf2eHjtTp6TnWBHu325KlbDcBT3WdBf+6O1X+aHbyDAEcdddRRRx29Av0LuyT9iPLdjSAAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Application startup complete.\n", + "INFO: Uvicorn running on http://0.0.0.0:9563 (Press CTRL+C to quit)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 127.0.0.1:51743 - \"GET / HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:51743 - \"GET /app/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:51744 - \"POST /api/v1/chat-stream HTTP/1.1\" 200 OK\n", + "\n", + "\n", + "\u001b[1m> Entering new None chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m我需要理解\"libro\"具体指的是什么,因为这个词汇在不同的上下文中可能有不同的含义。它可能是某个特定软件、库或者产品的名称。不过,根据问题的中文表述,“libro”很可能是指一种技术或产品,并且询问的是该技术/产品的特性。由于直接信息不足,我将尝试使用rag_query工具来搜索关于“libro”的相关信息。\n", + "Action: rag_query\n", + "Action Input: libro 特性\u001b[0mINFO: 127.0.0.1:51869 - \"GET /static/stream.js.map HTTP/1.1\" 404 Not Found\n", + "\n", + "Observation: \u001b[36;1m\u001b[1;3mLibro 是一个高度可定制的笔记本解决方案,旨在支持生成式AI能力,并提供商业级用户体验。以下是Libro的一些主要特性:\n", + "\n", + "1. **灵活易集成**:Libro允许用户根据自身需求自由组合其原生模块,具有强大的场景定制能力。它还提供了开放框架用于自定义UI和执行内核,在所有模块层面上支持二次开发和定制。\n", + "\n", + "2. **AI工作流**:\n", + " - **提示单元(Prompt Cells)**:通过提示单元,用户可以将AI集成到笔记本的执行逻辑中,实现无缝的AI工作流。\n", + " - **解释器模式**:这种模式让用户能够使用自然语言与笔记本互动,从而生成所需的结果,极大简化了编程过程。\n", + "\n", + "3. **多种产品形态**:Libro不仅可用作文档编辑器,还能作为报告展示工具,非常适合创建演示文稿或报告。\n", + "\n", + "4. **轻松的数据整合**:通过SQL单元格,Libro让用户能够方便地执行数据操作。只需简单配置即可连接到自己的数据库;并且可以在SQL中使用Python上下文变量,查询结果可以直接作为DataFrame继续处理,确保了一个流畅高效的数据工作流程。\n", + "\n", + "5. **全功能代码编辑器**:虽然背景信息中没有详细说明这一点的具体内容,但提到Libro提供了一个全功能的代码编辑环境,这意味着它很可能包含了语法高亮、自动完成等高级编辑功能,以增强编码效率。\n", + "\n", + "6. **支持大型模型应用**:Libro致力于探索与大型模型结合的应用案例,旨在为用户提供更加智能的编程体验,使得在Libro中的编程就像撰写文档一样简单直观。\n", + "\n", + "7. **社区驱动的发展**:Libro鼓励来自不同应用场景的开发者加入项目共建,共同推动产品的进步与发展。其开源项目托管于GitHub上,任何人都可以参与贡献或直接使用该项目。\n", + "\n", + "综上所述,Libro通过引入创新性的AI集成以及优化传统笔记本的功能,为数据科学和人工智能领域的专业人士提供了一个强大而灵活的工作平台。\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m现在我已经了解到Libro的一些关键特性,包括其灵活性、AI工作流的支持能力、多种产品形态、数据整合的便利性、全功能代码编辑环境以及对大型模型应用的支持等。基于这些信息,我可以给出一个综合性的回答。\n", + "Final Answer: Libro是一款专为生成式AI设计的高度可定制笔记本解决方案,具有以下主要特性:\n", + "\n", + "- **灵活易集成**:允许用户根据需求自由组合原生模块,并支持在所有层面进行二次开发和定制。\n", + "- **AI工作流**:通过提示单元将AI集成进执行逻辑中;解释器模式让用户能够以自然语言与系统互动。\n", + "- **多用途**:既可用作文档编辑器,也能作为报告展示工具使用。\n", + "- **轻松的数据整合**:内置SQL单元格简化了数据库连接及操作过程。\n", + "- **全功能代码编辑器**:提供包括语法高亮在内的高级编辑功能来提高编码效率。\n", + "- **支持大型模型应用**:探索如何更好地与大型模型结合,提升用户体验。\n", + "- **社区驱动的发展**:鼓励社区成员参与项目共建,促进持续改进。\n", + "\n", + "这些特性使得Libro成为了一个非常适合数据科学和人工智能领域专业人士使用的强大平台。\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + } + ], + "source": [ + "import sys\n", + "sys.path.append(\"/Users/johnny/Code/difizen/magent/packages/magent-ui-langchain/src\")\n", + "\n", + "from magent_ui_langchain import launch\n", + "launch(agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a31c577-eca1-4965-a71a-a805aad16fe2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "test", + "language": "python", + "name": "test" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/magent-core/.gitignore b/packages/magent-core/.gitignore new file mode 100644 index 00000000..556211bb --- /dev/null +++ b/packages/magent-core/.gitignore @@ -0,0 +1,11 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv + diff --git a/packages/magent-core/.python-version b/packages/magent-core/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/packages/magent-core/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/packages/magent-core/MANIFEST.in b/packages/magent-core/MANIFEST.in new file mode 100644 index 00000000..9a9064c6 --- /dev/null +++ b/packages/magent-core/MANIFEST.in @@ -0,0 +1 @@ +recursive-include src/magent_core/static * diff --git a/packages/magent-core/README.md b/packages/magent-core/README.md new file mode 100644 index 00000000..f6777015 --- /dev/null +++ b/packages/magent-core/README.md @@ -0,0 +1 @@ +# magent core diff --git a/packages/magent-core/pyproject.toml b/packages/magent-core/pyproject.toml new file mode 100644 index 00000000..c77ada60 --- /dev/null +++ b/packages/magent-core/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "magent-core" +version = "0.1.0.dev" +description = "" +authors = [{ name = "brokun", email = "brokun0128@gmail.com" }] + +dependencies = ["pydantic>=2.7.3"] +readme = "README.md" +requires-python = ">= 3.9" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true + +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/magent_core"] diff --git a/packages/magent-core/src/magent_core/__init__.py b/packages/magent-core/src/magent_core/__init__.py new file mode 100644 index 00000000..7e891b06 --- /dev/null +++ b/packages/magent-core/src/magent_core/__init__.py @@ -0,0 +1,4 @@ +from .executor_adpator import adaptor_registry +__all__ = [ + "adaptor_registry" +] diff --git a/packages/magent-core/src/magent_core/config.py b/packages/magent-core/src/magent_core/config.py new file mode 100644 index 00000000..bfafedaa --- /dev/null +++ b/packages/magent-core/src/magent_core/config.py @@ -0,0 +1,182 @@ +# config_loader.py + +import logging +import os +import importlib.util +from pathlib import Path +logger = logging.getLogger("uvicorn") + +default_config = { + 'thread_worker': 20, + 'host': '0.0.0.0', + 'port': 9563, + 'base_url': None, + 'open_browser': True, + 'log_level': None, + 'root_path': '/' +} + + +def to_uvicorn_config(config: dict): + uvicorn_config = {**config} + # remove unsupported config key + del uvicorn_config['base_url'] + del uvicorn_config['open_browser'] + del uvicorn_config['log_level'] + + # root path has already taken effect + del uvicorn_config['root_path'] + + if uvicorn_config.get('thread_worker', None) is not None: + del uvicorn_config['thread_worker'] + + return uvicorn_config + + +def load_config_from_file(file_path): + spec = importlib.util.spec_from_file_location("user_config", file_path) + if spec is not None: + config_module = importlib.util.module_from_spec(spec) + if spec.loader is not None: + spec.loader.exec_module(config_module) + return {key: getattr(config_module, key) for key in dir(config_module) if not key.startswith('_')} + return {} + + +def load_config_from_env(default_config, prefix='MAGENT_UI_SERVER'): + env_config = {} + for key, value in default_config.items(): + if isinstance(value, dict): + env_config[key] = load_config_from_env( + value, f"{prefix}_{key.upper()}") + else: + env_var = f"{prefix}_{key.upper()}" + env_value = os.getenv(env_var) + if env_value is not None: + if isinstance(value, bool): + env_value = env_value.lower() in ['true', '1', 'yes'] + elif isinstance(value, int): + env_value = int(env_value) + env_config[key] = env_value + return env_config + + +def merge_dicts(default, override): + for key, value in override.items(): + if isinstance(value, dict) and key in default: + default[key] = merge_dicts(default[key], value) + else: + default[key] = value + return default + + +def load_config(config=default_config, project_root_path=None): + config = merge_dicts(default_config, config) + + # 用户目录配置文件路径 + user_config_path = os.path.expanduser('~/.magent/ui_config.py') + + # 加载用户目录配置文件 + if os.path.exists(user_config_path): + user_config = load_config_from_file(user_config_path) + logger.info( + f"Load user config from {user_config_path}.") + config = merge_dicts(config, user_config) + + # 工作目录配置文件路径 + if project_root_path is None: + project_root_path = os.getcwd() + + # 加载项目根目录配置文件 + project_config_path = os.path.join( + project_root_path, 'config/magent_ui_config.py') + if os.path.exists(project_config_path): + project_config = load_config_from_file(project_config_path) + logger.info( + f"Load project config from {project_config_path}.") + config = merge_dicts(config, project_config) + + project_root_config_path = os.path.join( + project_root_path, '.magent_ui_config.py') + if os.path.exists(project_root_config_path): + project_config = load_config_from_file(project_root_config_path) + logger.info( + f"Load project config from {project_config_path}.") + config = merge_dicts(config, project_config) + + # 加载环境变量配置 + env_config = load_config_from_env(default_config) + if len(env_config.keys()): + logger.info("Load env config.") + config = merge_dicts(config, env_config) + + return config + + +api_path = 'api' +static_path = 'static' +resource_path = 'resources' +app_path = 'app' + + +config_count = 0 + + +class AppConfig(): + config: dict + project_root_path: Path + resource_dir_path: Path + + port: int + root_path: str + base_url: str + open_browser: bool + log_level: str + + full_api_path: str + full_static_path: str + full_resource_path: str + + api_url: str + static_url: str + resource_url: str + app_url: str + + thread_worker: int + + def __init__(self): + global config_count + config_count += 1 + + def load_config(self, project_root_path: Path, **kwargs): + config = load_config(kwargs, project_root_path) + self.project_root_path = project_root_path + self.resource_dir_path = self.project_root_path / 'app' / 'resources' + self.config = config + self.port = config.get('port', 9563) + base_root_path = '/' + root_path = config.get('root_path', base_root_path) + self.root_path = root_path + self.base_url = config.get('base_url', None) + self.open_browser = config.get('open_browser', True) + self.log_level = config.get('log_level', None) + self.thread_worker = config.get('thread_worker', None) + if self.base_url is None: + self.base_url = root_path + + if not root_path.startswith('/'): + logger.info('[magent] root_path should start with "/" ', root_path) + root_path = f'/{root_path}' + config['root_path'] = root_path + + self.full_api_path = os.path.join(root_path, api_path) + self.full_static_path = os.path.join(root_path, static_path) + self.full_resource_path = os.path.join(root_path, resource_path) + + self.api_url = os.path.join(self.base_url, api_path) + self.static_url = os.path.join(self.base_url, static_path) + self.resource_url = os.path.join(self.base_url, resource_path) + self.app_url = os.path.join(self.base_url, app_path) + + +app_config = AppConfig() diff --git a/packages/magent-core/src/magent_core/executor_adpator/__init__.py b/packages/magent-core/src/magent_core/executor_adpator/__init__.py new file mode 100644 index 00000000..f1009802 --- /dev/null +++ b/packages/magent-core/src/magent_core/executor_adpator/__init__.py @@ -0,0 +1,4 @@ +from .adaptor_registry import adaptor_registry +__all__ = [ + "adaptor_registry" +] diff --git a/packages/magent-core/src/magent_core/executor_adpator/adaptor_registry.py b/packages/magent-core/src/magent_core/executor_adpator/adaptor_registry.py new file mode 100644 index 00000000..e245d770 --- /dev/null +++ b/packages/magent-core/src/magent_core/executor_adpator/adaptor_registry.py @@ -0,0 +1,73 @@ +from abc import ABCMeta +from typing import Any, Callable, List, Tuple, Type +from .base_adaptor import InvokeAdaptor, InputAdaptor + + +class AdaptorRegistry: + def __init__(self): + self.invoke_adaptors: List[Tuple[int, Callable[[ + Any, str | None], bool], Type[InvokeAdaptor]]] = [] + + self.input_adaptors: List[Tuple[int, Callable[[ + Any, str | None], bool], Type[InputAdaptor]]] = [] + + def register_invoke_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InvokeAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + """ + self.invoke_adaptors.append((priority, recognizer, adaptor_cls)) + # Sort adaptors by priority + self.invoke_adaptors.sort(key=lambda x: x[0]) + + def register_input_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InputAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + """ + self.input_adaptors.append((priority, recognizer, adaptor_cls)) + self.input_adaptors.sort(key=lambda x: x[0]) + + def register_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InputAdaptor | InvokeAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + + :param priority: The priority of the adaptor (lower numbers mean higher priority). + :param recognizer: A function that returns True if the adaptor can handle the object. + :param adaptor_cls: The adaptor class to use if the recognizer returns True. + """ + + if issubclass(adaptor_cls, InvokeAdaptor): + self.invoke_adaptors.append((priority, recognizer, adaptor_cls)) + # Sort adaptors by priority + self.invoke_adaptors.sort(key=lambda x: x[0], reverse=True) + if issubclass(adaptor_cls, InputAdaptor): + self.input_adaptors.append((priority, recognizer, adaptor_cls)) + self.input_adaptors.sort(key=lambda x: x[0], reverse=True) + + def get_invoke_adaptor(self, obj: Any, llm_type: str | None = None) -> InvokeAdaptor: + """ + Get the appropriate adaptor for the given object based on registered recognizers. + + :param obj: The object to be processed. + :return: An instance of the appropriate adaptor. + """ + input_adaptor = self.get_input_adaptor(obj, llm_type) + + for _, recognizer, adaptor_cls in self.invoke_adaptors: + if recognizer(obj, llm_type): + return adaptor_cls(object=obj, input_adaptor=input_adaptor) + raise ValueError(f"No suitable adaptor found for object: {obj}") + + def get_input_adaptor(self, obj: Any, llm_type: str | None = None) -> InputAdaptor: + """ + Get the appropriate adaptor for the given object based on registered recognizers. + + :param obj: The object to be processed. + :return: An instance of the appropriate adaptor. + """ + for _, recognizer, adaptor_cls in self.input_adaptors: + if recognizer(obj, llm_type): + return adaptor_cls(object=obj) + raise ValueError(f"No suitable adaptor found for object: {obj}") + + +adaptor_registry = AdaptorRegistry() diff --git a/packages/magent-core/src/magent_core/executor_adpator/base_adaptor.py b/packages/magent-core/src/magent_core/executor_adpator/base_adaptor.py new file mode 100644 index 00000000..cb9e49aa --- /dev/null +++ b/packages/magent-core/src/magent_core/executor_adpator/base_adaptor.py @@ -0,0 +1,51 @@ +from abc import ABC, abstractmethod +from typing import Any, AsyncIterator +from .event import BaseEvent, BaseOutputMessage + +from pydantic import BaseModel + + +class InputAdaptor(BaseModel): + ''' + A class to format the input message + ''' + object: Any + llm_type: str | None = None + + def to_input_message(self, value, *args, **kwargs): + return value + + +class InvokeAdaptor(BaseModel, ABC): + name: str = "custom" + llm_type: str | None = None + object: Any + input_adaptor: InputAdaptor | None = None + + @abstractmethod + def invoke(self, value, *args, **kwargs) -> BaseOutputMessage | None: + """Chat invoke""" + raise NotImplementedError( + "Each adaptor must implement the `invoke` method.") + + +class StreamInvokeAdaptor(InvokeAdaptor): + @abstractmethod + def invoke_stream(self, value, *args, **kwargs) -> AsyncIterator[BaseEvent] | None: + """Chat invoke""" + raise NotImplementedError( + "Each adaptor must implement the `invoke` method.") + + +# class OutputAdaptor(BaseModel): +# ''' +# A class to format the output message +# ''' +# object: Any +# llm_type: str | None = None + +# def to_output_message(self, value, *args, **kwargs): +# return value + +# def to_output_event(self, value, *args, **kwargs): +# return value diff --git a/packages/magent-core/src/magent_core/executor_adpator/current_executor.py b/packages/magent-core/src/magent_core/executor_adpator/current_executor.py new file mode 100644 index 00000000..5695fe18 --- /dev/null +++ b/packages/magent-core/src/magent_core/executor_adpator/current_executor.py @@ -0,0 +1,19 @@ +from typing import Any, Optional +from .base_adaptor import InvokeAdaptor +from .adaptor_registry import adaptor_registry + + +_current_invoke_adapator: InvokeAdaptor | None = None + +# Function to process any object using the registry + + +def process_object(obj: Any, llm_type: str | None) -> InvokeAdaptor: + global _current_invoke_adapator + _current_invoke_adapator = adaptor_registry.get_invoke_adaptor( + obj, llm_type) + return _current_invoke_adapator + + +def get_current_invoke_adaptor(): + return _current_invoke_adapator diff --git a/packages/magent-core/src/magent_core/executor_adpator/event.py b/packages/magent-core/src/magent_core/executor_adpator/event.py new file mode 100644 index 00000000..d59550c4 --- /dev/null +++ b/packages/magent-core/src/magent_core/executor_adpator/event.py @@ -0,0 +1,46 @@ +from typing import List +from pydantic import BaseModel + + +class BaseOutputMessage(BaseModel): + id: str + output: str + + +class BaseEvent(BaseModel): + type: str + id: str + data: str | BaseModel + + +class ChunkEvent(BaseEvent): + type: str = 'chunk' + id: str + data: str | BaseOutputMessage + + +class ResultEvent(BaseEvent): + type: str = 'result' + id: str + data: str | BaseOutputMessage + + +class ErrorMessage(BaseModel): + error_message: str + + +class ErrorEvent(BaseEvent): + type: str = 'error' + id: str + data: str | ErrorMessage + + +class ActionMessage(BaseModel): + name: str + arguments: List[str] | None + + +class ActionEvent(BaseEvent): + type: str = 'action' + id: str + data: str | ErrorMessage diff --git a/packages/magent-core/src/magent_core/llm/__init__.py b/packages/magent-core/src/magent_core/llm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/magent-core/src/magent_core/llm/environment.py b/packages/magent-core/src/magent_core/llm/environment.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/magent-core/src/magent_core/utils.py b/packages/magent-core/src/magent_core/utils.py new file mode 100644 index 00000000..5b4cd0a4 --- /dev/null +++ b/packages/magent-core/src/magent_core/utils.py @@ -0,0 +1,47 @@ + + +def is_community_installed(): + return attempt_import('langchain_community') is not None + + +def is_in_array_or_has_prefix(string_array, target_string): + """ + Check if the target_string is in the string_array or starts with any of the elements in the string_array. + + :param string_array: List of strings to check against. + :param target_string: The target string to check. + :return: True if target_string is in string_array or starts with any element in string_array, False otherwise. + """ + # Check if the target_string is directly in the string_array + if target_string in string_array: + return True + + # Check if the target_string starts with any string in the string_array + for prefix in string_array: + if target_string.startswith(prefix): + return True + + return False + + +def is_ipython() -> bool: + """ + Check if interface is launching from iPython + :return is_ipython (bool): True or False + """ + is_ipython = False + try: # Check if running interactively using ipython. + from IPython import get_ipython # type: ignore + + if get_ipython() is not None: + is_ipython = True + except (ImportError, NameError): + pass + return is_ipython + + +def attempt_import(module): + try: + return __import__(module) + except ImportError: + return None diff --git a/packages/magent-ui-core/.gitignore b/packages/magent-ui-core/.gitignore new file mode 100644 index 00000000..556211bb --- /dev/null +++ b/packages/magent-ui-core/.gitignore @@ -0,0 +1,11 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv + diff --git a/packages/magent-ui-core/.python-version b/packages/magent-ui-core/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/packages/magent-ui-core/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/packages/magent-ui-core/MANIFEST.in b/packages/magent-ui-core/MANIFEST.in new file mode 100644 index 00000000..7e7ef50f --- /dev/null +++ b/packages/magent-ui-core/MANIFEST.in @@ -0,0 +1 @@ +recursive-include src/magent_ui_core/static * diff --git a/packages/magent-ui-core/README.md b/packages/magent-ui-core/README.md new file mode 100644 index 00000000..910a2cd4 --- /dev/null +++ b/packages/magent-ui-core/README.md @@ -0,0 +1 @@ +# magent ui core diff --git a/packages/magent-ui-core/pyproject.toml b/packages/magent-ui-core/pyproject.toml new file mode 100644 index 00000000..a26d9c72 --- /dev/null +++ b/packages/magent-ui-core/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "magent-ui-core" +version = "0.1.0.dev" +description = "" +authors = [{ name = "brokun", email = "brokun0128@gmail.com" }] + +dependencies = [ + "sse-starlette>=2.1.2", + "jinja2>=3.1.4", + "python-multipart>=0.0.9", + "fastapi>=0.111.0", + "aiofiles>=24.1.0", + "nest-asyncio>=1.6.0", +] +readme = "README.md" +requires-python = ">= 3.9" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true + +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/magent_ui_core"] diff --git a/packages/magent-ui-core/src/magent_ui_core/__init__.py b/packages/magent-ui-core/src/magent_ui_core/__init__.py new file mode 100644 index 00000000..42f25c3e --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/__init__.py @@ -0,0 +1,6 @@ +from .app import launch +from .adaptor_registry import adaptor_registry +__all__ = [ + "launch", + "adaptor_registry" +] diff --git a/packages/magent-ui-core/src/magent_ui_core/adaptor_registry.py b/packages/magent-ui-core/src/magent_ui_core/adaptor_registry.py new file mode 100644 index 00000000..e245d770 --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/adaptor_registry.py @@ -0,0 +1,73 @@ +from abc import ABCMeta +from typing import Any, Callable, List, Tuple, Type +from .base_adaptor import InvokeAdaptor, InputAdaptor + + +class AdaptorRegistry: + def __init__(self): + self.invoke_adaptors: List[Tuple[int, Callable[[ + Any, str | None], bool], Type[InvokeAdaptor]]] = [] + + self.input_adaptors: List[Tuple[int, Callable[[ + Any, str | None], bool], Type[InputAdaptor]]] = [] + + def register_invoke_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InvokeAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + """ + self.invoke_adaptors.append((priority, recognizer, adaptor_cls)) + # Sort adaptors by priority + self.invoke_adaptors.sort(key=lambda x: x[0]) + + def register_input_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InputAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + """ + self.input_adaptors.append((priority, recognizer, adaptor_cls)) + self.input_adaptors.sort(key=lambda x: x[0]) + + def register_adaptor(self, recognizer: Callable[[Any, str | None], bool], adaptor_cls: Type[InputAdaptor | InvokeAdaptor], priority: int = 0, ): + """ + Register a new adaptor with a priority and a recognizer function. + + :param priority: The priority of the adaptor (lower numbers mean higher priority). + :param recognizer: A function that returns True if the adaptor can handle the object. + :param adaptor_cls: The adaptor class to use if the recognizer returns True. + """ + + if issubclass(adaptor_cls, InvokeAdaptor): + self.invoke_adaptors.append((priority, recognizer, adaptor_cls)) + # Sort adaptors by priority + self.invoke_adaptors.sort(key=lambda x: x[0], reverse=True) + if issubclass(adaptor_cls, InputAdaptor): + self.input_adaptors.append((priority, recognizer, adaptor_cls)) + self.input_adaptors.sort(key=lambda x: x[0], reverse=True) + + def get_invoke_adaptor(self, obj: Any, llm_type: str | None = None) -> InvokeAdaptor: + """ + Get the appropriate adaptor for the given object based on registered recognizers. + + :param obj: The object to be processed. + :return: An instance of the appropriate adaptor. + """ + input_adaptor = self.get_input_adaptor(obj, llm_type) + + for _, recognizer, adaptor_cls in self.invoke_adaptors: + if recognizer(obj, llm_type): + return adaptor_cls(object=obj, input_adaptor=input_adaptor) + raise ValueError(f"No suitable adaptor found for object: {obj}") + + def get_input_adaptor(self, obj: Any, llm_type: str | None = None) -> InputAdaptor: + """ + Get the appropriate adaptor for the given object based on registered recognizers. + + :param obj: The object to be processed. + :return: An instance of the appropriate adaptor. + """ + for _, recognizer, adaptor_cls in self.input_adaptors: + if recognizer(obj, llm_type): + return adaptor_cls(object=obj) + raise ValueError(f"No suitable adaptor found for object: {obj}") + + +adaptor_registry = AdaptorRegistry() diff --git a/packages/magent-ui-core/src/magent_ui_core/app.py b/packages/magent-ui-core/src/magent_ui_core/app.py new file mode 100644 index 00000000..302eec33 --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/app.py @@ -0,0 +1,107 @@ +import nest_asyncio +from pathlib import Path +from typing import Any +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +# from fastapi.templating import Jinja2Templates +from contextlib import asynccontextmanager +import webbrowser +import uvicorn +import logging +import os +from uvicorn.config import LOGGING_CONFIG + +from .config import to_uvicorn_config, app_config +from .current_executor import process_object +from .utils import is_ipython + +# 应用 nest_asyncio 以解决事件循环冲突 +nest_asyncio.apply() + +# Use uvicorn's default logging configuration +logging.config.dictConfig(LOGGING_CONFIG) # type: ignore + +# Get the uvicorn logger +logger = logging.getLogger("uvicorn") + +# 挂载 static 目录,使其可以访问静态文件 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +static_dir = os.path.join(BASE_DIR, 'static') +# templates_dir = os.path.join(BASE_DIR, 'templates') + +# templates = Jinja2Templates(directory=templates_dir) + + +def launch(object: Any, llm_type: str | None = None, **kwargs): + ''' + Launch the langchain server. + ''' + process_object(object, llm_type) + + logger.info("Current log level is") + project_root_path = Path.cwd() + + app_config.load_config(project_root_path=project_root_path, **kwargs) + + if app_config.log_level is not None: + logger.setLevel(app_config.log_level) + + @asynccontextmanager + async def lifespan(app: FastAPI): + url = f"http://localhost:{app_config.port}{app_config.root_path}" + logger.info(f"Server is running at {url}") + + if app_config.open_browser: + webbrowser.open(url) + + yield + logger.info('Server finished') + + app = FastAPI(lifespan=lifespan) + + # # api + # app.include_router(api_router, prefix=app_config.full_api_path) + + # static + if os.path.exists(static_dir): + app.mount(app_config.full_static_path, StaticFiles( + directory=static_dir, html=True), name="static") + else: + logger.info('Can not find static directory. ', static_dir) + + # auto redirect to app url + @app.get(app_config.root_path, response_class=HTMLResponse) + async def to_index_page(request: Request): + return RedirectResponse(url=f"{app_config.app_url}/") + + # html as default, app_path included + # html_root = app_config.root_path if app_config.root_path.endswith( + # '/') else f"{app_config.root_path}/" + + # @app.get(html_root+"{path:path}", response_class=HTMLResponse) + # async def to_app_page(request: Request): + # page_config = { + # "baseUrl": app_config.base_url, + # "resourceUrl": app_config.resource_url, + # "apiUrl": app_config.api_url, + # "appUrl": app_config.app_url, + # "staticUrl": app_config.static_url, + # } + # return templates.TemplateResponse(request=request, name="index.html", context={ + # "page_config": page_config, "static_url": app_config.static_url + # }) + + uvicorn_config = to_uvicorn_config(app_config.config) + # 使用 asyncio.run() 运行 uvicorn + # asyncio.run(uvicorn.run(app, log_level='info', + # loop="asyncio", **uvicorn_config)) + + if is_ipython(): + # 在 Jupyter Notebook 中运行 + import asyncio + asyncio.run(uvicorn.run(app, log_level='info', + loop="asyncio", **uvicorn_config)) # type: ignore + else: + uvicorn.run(app, log_level='info', + loop="asyncio", **uvicorn_config) diff --git a/packages/magent-ui-core/src/magent_ui_core/base_adaptor.py b/packages/magent-ui-core/src/magent_ui_core/base_adaptor.py new file mode 100644 index 00000000..cb9e49aa --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/base_adaptor.py @@ -0,0 +1,51 @@ +from abc import ABC, abstractmethod +from typing import Any, AsyncIterator +from .event import BaseEvent, BaseOutputMessage + +from pydantic import BaseModel + + +class InputAdaptor(BaseModel): + ''' + A class to format the input message + ''' + object: Any + llm_type: str | None = None + + def to_input_message(self, value, *args, **kwargs): + return value + + +class InvokeAdaptor(BaseModel, ABC): + name: str = "custom" + llm_type: str | None = None + object: Any + input_adaptor: InputAdaptor | None = None + + @abstractmethod + def invoke(self, value, *args, **kwargs) -> BaseOutputMessage | None: + """Chat invoke""" + raise NotImplementedError( + "Each adaptor must implement the `invoke` method.") + + +class StreamInvokeAdaptor(InvokeAdaptor): + @abstractmethod + def invoke_stream(self, value, *args, **kwargs) -> AsyncIterator[BaseEvent] | None: + """Chat invoke""" + raise NotImplementedError( + "Each adaptor must implement the `invoke` method.") + + +# class OutputAdaptor(BaseModel): +# ''' +# A class to format the output message +# ''' +# object: Any +# llm_type: str | None = None + +# def to_output_message(self, value, *args, **kwargs): +# return value + +# def to_output_event(self, value, *args, **kwargs): +# return value diff --git a/packages/magent-ui-core/src/magent_ui_core/config.py b/packages/magent-ui-core/src/magent_ui_core/config.py new file mode 100644 index 00000000..bfafedaa --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/config.py @@ -0,0 +1,182 @@ +# config_loader.py + +import logging +import os +import importlib.util +from pathlib import Path +logger = logging.getLogger("uvicorn") + +default_config = { + 'thread_worker': 20, + 'host': '0.0.0.0', + 'port': 9563, + 'base_url': None, + 'open_browser': True, + 'log_level': None, + 'root_path': '/' +} + + +def to_uvicorn_config(config: dict): + uvicorn_config = {**config} + # remove unsupported config key + del uvicorn_config['base_url'] + del uvicorn_config['open_browser'] + del uvicorn_config['log_level'] + + # root path has already taken effect + del uvicorn_config['root_path'] + + if uvicorn_config.get('thread_worker', None) is not None: + del uvicorn_config['thread_worker'] + + return uvicorn_config + + +def load_config_from_file(file_path): + spec = importlib.util.spec_from_file_location("user_config", file_path) + if spec is not None: + config_module = importlib.util.module_from_spec(spec) + if spec.loader is not None: + spec.loader.exec_module(config_module) + return {key: getattr(config_module, key) for key in dir(config_module) if not key.startswith('_')} + return {} + + +def load_config_from_env(default_config, prefix='MAGENT_UI_SERVER'): + env_config = {} + for key, value in default_config.items(): + if isinstance(value, dict): + env_config[key] = load_config_from_env( + value, f"{prefix}_{key.upper()}") + else: + env_var = f"{prefix}_{key.upper()}" + env_value = os.getenv(env_var) + if env_value is not None: + if isinstance(value, bool): + env_value = env_value.lower() in ['true', '1', 'yes'] + elif isinstance(value, int): + env_value = int(env_value) + env_config[key] = env_value + return env_config + + +def merge_dicts(default, override): + for key, value in override.items(): + if isinstance(value, dict) and key in default: + default[key] = merge_dicts(default[key], value) + else: + default[key] = value + return default + + +def load_config(config=default_config, project_root_path=None): + config = merge_dicts(default_config, config) + + # 用户目录配置文件路径 + user_config_path = os.path.expanduser('~/.magent/ui_config.py') + + # 加载用户目录配置文件 + if os.path.exists(user_config_path): + user_config = load_config_from_file(user_config_path) + logger.info( + f"Load user config from {user_config_path}.") + config = merge_dicts(config, user_config) + + # 工作目录配置文件路径 + if project_root_path is None: + project_root_path = os.getcwd() + + # 加载项目根目录配置文件 + project_config_path = os.path.join( + project_root_path, 'config/magent_ui_config.py') + if os.path.exists(project_config_path): + project_config = load_config_from_file(project_config_path) + logger.info( + f"Load project config from {project_config_path}.") + config = merge_dicts(config, project_config) + + project_root_config_path = os.path.join( + project_root_path, '.magent_ui_config.py') + if os.path.exists(project_root_config_path): + project_config = load_config_from_file(project_root_config_path) + logger.info( + f"Load project config from {project_config_path}.") + config = merge_dicts(config, project_config) + + # 加载环境变量配置 + env_config = load_config_from_env(default_config) + if len(env_config.keys()): + logger.info("Load env config.") + config = merge_dicts(config, env_config) + + return config + + +api_path = 'api' +static_path = 'static' +resource_path = 'resources' +app_path = 'app' + + +config_count = 0 + + +class AppConfig(): + config: dict + project_root_path: Path + resource_dir_path: Path + + port: int + root_path: str + base_url: str + open_browser: bool + log_level: str + + full_api_path: str + full_static_path: str + full_resource_path: str + + api_url: str + static_url: str + resource_url: str + app_url: str + + thread_worker: int + + def __init__(self): + global config_count + config_count += 1 + + def load_config(self, project_root_path: Path, **kwargs): + config = load_config(kwargs, project_root_path) + self.project_root_path = project_root_path + self.resource_dir_path = self.project_root_path / 'app' / 'resources' + self.config = config + self.port = config.get('port', 9563) + base_root_path = '/' + root_path = config.get('root_path', base_root_path) + self.root_path = root_path + self.base_url = config.get('base_url', None) + self.open_browser = config.get('open_browser', True) + self.log_level = config.get('log_level', None) + self.thread_worker = config.get('thread_worker', None) + if self.base_url is None: + self.base_url = root_path + + if not root_path.startswith('/'): + logger.info('[magent] root_path should start with "/" ', root_path) + root_path = f'/{root_path}' + config['root_path'] = root_path + + self.full_api_path = os.path.join(root_path, api_path) + self.full_static_path = os.path.join(root_path, static_path) + self.full_resource_path = os.path.join(root_path, resource_path) + + self.api_url = os.path.join(self.base_url, api_path) + self.static_url = os.path.join(self.base_url, static_path) + self.resource_url = os.path.join(self.base_url, resource_path) + self.app_url = os.path.join(self.base_url, app_path) + + +app_config = AppConfig() diff --git a/packages/magent-ui-core/src/magent_ui_core/current_executor.py b/packages/magent-ui-core/src/magent_ui_core/current_executor.py new file mode 100644 index 00000000..5695fe18 --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/current_executor.py @@ -0,0 +1,19 @@ +from typing import Any, Optional +from .base_adaptor import InvokeAdaptor +from .adaptor_registry import adaptor_registry + + +_current_invoke_adapator: InvokeAdaptor | None = None + +# Function to process any object using the registry + + +def process_object(obj: Any, llm_type: str | None) -> InvokeAdaptor: + global _current_invoke_adapator + _current_invoke_adapator = adaptor_registry.get_invoke_adaptor( + obj, llm_type) + return _current_invoke_adapator + + +def get_current_invoke_adaptor(): + return _current_invoke_adapator diff --git a/packages/magent-ui-core/src/magent_ui_core/event.py b/packages/magent-ui-core/src/magent_ui_core/event.py new file mode 100644 index 00000000..d59550c4 --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/event.py @@ -0,0 +1,46 @@ +from typing import List +from pydantic import BaseModel + + +class BaseOutputMessage(BaseModel): + id: str + output: str + + +class BaseEvent(BaseModel): + type: str + id: str + data: str | BaseModel + + +class ChunkEvent(BaseEvent): + type: str = 'chunk' + id: str + data: str | BaseOutputMessage + + +class ResultEvent(BaseEvent): + type: str = 'result' + id: str + data: str | BaseOutputMessage + + +class ErrorMessage(BaseModel): + error_message: str + + +class ErrorEvent(BaseEvent): + type: str = 'error' + id: str + data: str | ErrorMessage + + +class ActionMessage(BaseModel): + name: str + arguments: List[str] | None + + +class ActionEvent(BaseEvent): + type: str = 'action' + id: str + data: str | ErrorMessage diff --git a/packages/magent-ui-core/src/magent_ui_core/utils.py b/packages/magent-ui-core/src/magent_ui_core/utils.py new file mode 100644 index 00000000..801ad65d --- /dev/null +++ b/packages/magent-ui-core/src/magent_ui_core/utils.py @@ -0,0 +1,52 @@ +def attempt_import(module): + try: + return __import__(module) + except ImportError: + return None + + +def is_community_installed(): + return attempt_import('langchain_community') is not None + + +def is_in_array_or_has_prefix(string_array, target_string): + """ + Check if the target_string is in the string_array or starts with any of the elements in the string_array. + + :param string_array: List of strings to check against. + :param target_string: The target string to check. + :return: True if target_string is in string_array or starts with any element in string_array, False otherwise. + """ + # Check if the target_string is directly in the string_array + if target_string in string_array: + return True + + # Check if the target_string starts with any string in the string_array + for prefix in string_array: + if target_string.startswith(prefix): + return True + + return False + + +def is_ipython() -> bool: + """ + Check if interface is launching from iPython + :return is_ipython (bool): True or False + """ + is_ipython = False + try: # Check if running interactively using ipython. + from IPython import get_ipython # type: ignore + + if get_ipython() is not None: + is_ipython = True + except (ImportError, NameError): + pass + return is_ipython + + +def attempt_import(module): + try: + return __import__(module) + except ImportError: + return None diff --git a/packages/magent-ui-langchain/MANIFEST.in b/packages/magent-ui-langchain/MANIFEST.in index 58ee6bd5..fe3e2b18 100644 --- a/packages/magent-ui-langchain/MANIFEST.in +++ b/packages/magent-ui-langchain/MANIFEST.in @@ -1 +1 @@ -recursive-include src/magent_ui/static * +recursive-include src/magent_ui_magent/static * diff --git a/packages/magent-ui-langchain/README.md b/packages/magent-ui-langchain/README.md index 050bb81e..f35eda9b 100644 --- a/packages/magent-ui-langchain/README.md +++ b/packages/magent-ui-langchain/README.md @@ -1,30 +1 @@ -# magent_ui - -## agentUniverse - -与 [agentUniverse](https://github.com/alipay/agentUniverse) 联合推出本地研发产品化方案,详见[产品化文档](https://github.com/alipay/agentUniverse/blob/master/docs/guidebook/zh/10_1_1_%E4%BA%A7%E5%93%81%E5%8C%96%E5%B9%B3%E5%8F%B0%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B.md) - -PEER 多智能体对话 -![PEER 多智能体对话](../../docs/assets/au-peer-chat.jpg) - -reAct 智能体开发 -![reAct 智能体开发](../../docs/assets/au-react-dev.jpg) - -### 配置 - -支持 uvicorn 配置 - -```python -from magent_ui import launch -launch(host='0.0.0.0', port=8888, root_path='/') -``` - -#### 配置文件 - -- 用户级配置: ~/.magent/ui_config.py -- 项目级配置: {project_root}/.magent_ui_config.py -- 项目级配置: {project_root}/config/magent_ui_config.py - -#### 环境变量 - -MAGENT_UI_SERVER_XX +# magent ui langchain diff --git a/packages/magent-ui-langchain/pyproject.toml b/packages/magent-ui-langchain/pyproject.toml index c1f260a6..34a2bcd4 100644 --- a/packages/magent-ui-langchain/pyproject.toml +++ b/packages/magent-ui-langchain/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "magent-ui-langchain" -version = "0.1.17.dev1" +version = "0.1.0.dev" description = "" authors = [{ name = "brokun", email = "brokun0128@gmail.com" }] @@ -11,7 +11,7 @@ dependencies = [ "aiofiles>=24.1.0", ] readme = "README.md" -requires-python = ">= 3.10" +requires-python = ">= 3.9" [build-system] requires = ["hatchling"] @@ -27,6 +27,3 @@ allow-direct-references = true [tool.hatch.build.targets.wheel] packages = ["src/magent_ui_langchain"] - -[tool.rye.scripts] -dev = "python src/magent_ui_langchain/dev.py" diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/__init__.py b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/__init__.py new file mode 100644 index 00000000..9e55381f --- /dev/null +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/__init__.py @@ -0,0 +1,13 @@ +from magent_ui_core import adaptor_registry +from .langchain_adaptor import RunnableAdaptor +from .langchain_openai_executor import OpenAIAdaptor +from .langchain_tongyi_executor import TongyiAdaptor + +adaptor_registry.register_adaptor( + RunnableAdaptor.recognizer, RunnableAdaptor) + +adaptor_registry.register_adaptor( + OpenAIAdaptor.recognizer, OpenAIAdaptor, 10) + +adaptor_registry.register_adaptor( + TongyiAdaptor.recognizer, TongyiAdaptor, 10) diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_adaptor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_adaptor.py new file mode 100644 index 00000000..1846e43c --- /dev/null +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_adaptor.py @@ -0,0 +1,164 @@ +from email import message +from typing import Any, AsyncIterator, List, Sequence +from langchain_community.callbacks.manager import get_openai_callback +from langchain_core.runnables import Runnable +from langchain_core.messages import HumanMessage, AIMessage, AIMessageChunk, BaseMessage, BaseMessageChunk +from langchain_core.agents import AgentAction, AgentStep, AgentFinish +from langchain_core.runnables.utils import AddableDict +from numpy import isin +from .base_adaptor import InputAdaptor, StreamInvokeAdaptor +from .event import BaseEvent, BaseOutputMessage, ChunkEvent, ResultEvent + + +def merge_strings(input_list): + """ + Merge all string elements in the input list into a single string. + + Args: + input_list (list): A list potentially containing strings and other types. + + Returns: + str: A single string containing all the merged string elements. + """ + # Create a list to hold string elements + string_elements = [] + + # Iterate through each element in the input list + for element in input_list: + # Check if the element is a string + if isinstance(element, str): + # If it's a string, add it to the list of string elements + string_elements.append(element) + + # Join all the string elements into a single string + # Using space as a separator, you can adjust this as needed + merged_string = ''.join(string_elements) + + return merged_string + + +class RunnableAdaptor(StreamInvokeAdaptor, InputAdaptor): + @staticmethod + def recognizer(object, llm_type: str | None = None): + return isinstance(object, Runnable) + + def to_input_message(self, value, image=None): + message = HumanMessage(content=value) + return [message] + + def get_input_adaptor(self) -> InputAdaptor: + if self.input_adaptor is not None: + return self.input_adaptor + return self + + def invoke(self, value, image=None) -> BaseOutputMessage | None: + if not isinstance(self.object, Runnable): + return None + messages = self.get_input_adaptor().to_input_message(value, image) + message = self.object.invoke(messages) + data_list = [] + id = '' + if isinstance(message, AIMessage): + if message.id is not None: + id = message.id + if isinstance(message.content, str): + data_list.append(message.content) + if isinstance(message.content, list): + data_list.append(merge_strings(message.content)) + else: + print("Can not handle message content", message) + data = data_list.pop() + return BaseOutputMessage(id=id, output=data) + + def aimessages_to_event_data(self, messages: Sequence[BaseMessage | BaseMessageChunk]) -> List[str]: + data_list = [] + for message in messages: + if isinstance(message, AIMessage) or isinstance(message, AIMessageChunk): + if isinstance(message.content, str): + data_list.append(message.content) + if isinstance(message.content, list): + data_list.append(merge_strings(message.content)) + else: + print("Can not handle message content", message) + return data_list + + def steps_to_event_data(self, steps: Sequence[AgentStep]) -> List[str]: + data_list = [] + for step in steps: + if isinstance(step.action, AgentAction) and step.observation is not None: + data_list.append(f"\n{step.observation}\n") + return data_list + + async def to_output_event(self, value: AsyncIterator, *args, **kwargs) -> AsyncIterator[BaseEvent]: + first = True + gathered = None + gather_output = '' + id = '' + async for msg_chunk in value: + # id = msg_chunk.id + # print('------ chunk', msg_chunk) + # print('------ chunk type', type(msg_chunk)) + if first: + gathered = msg_chunk + first = False + else: + gathered = gathered + msg_chunk + data_list = [] + if isinstance(msg_chunk, BaseMessageChunk) and len(msg_chunk.content) > 0: + data_list = self.aimessages_to_event_data([msg_chunk]) + if isinstance(msg_chunk, AddableDict): + # if 'actions' in msg_chunk: + # for action in msg_chunk['actions']: + # if isinstance(action, AgentAction): + # print(f"Action: {action.log}") + # print('--------------------------------') + + # Check and display agent steps + if 'steps' in msg_chunk: + for step in msg_chunk['steps']: + if isinstance(step, AgentStep): + data_list.append(f'{step.action.log}') + gather_output += f'{step.action.log}' + if hasattr(step, 'observation') and step.observation is not None: + # print(f"\nObservation: {step.observation}") + data_list.append(f'\nObservation: {step.observation}') + gather_output += f'\nObservation: {step.observation}' + data_list.append("\nThought: ") + gather_output += "\nThought: " + # print('--------------------------------') + + # Check and display final output + if 'output' in msg_chunk: + for message in msg_chunk['messages']: + # print(f"{message.content}") + data_list.append(f'{message.content}') + gather_output += f'{message.content}' + # print('--------------------------------') + # messages = msg_chunk.get('messages', []) + # data_list = self.aimessages_to_event_data(messages) + # step_list = self.steps_to_event_data( + # msg_chunk.get('steps', [])) + # data_list.extend(step_list) + + if isinstance(msg_chunk, AgentAction) or isinstance(msg_chunk, AgentFinish) or isinstance(msg_chunk, AgentStep): + data_list = self.aimessages_to_event_data(msg_chunk.messages) + for data in data_list: + yield ChunkEvent(id=id, data=BaseOutputMessage(id=id, output=data)) + + # print('------ gathered', gathered) + # print('------ gathered type', type(gathered)) + + if isinstance(gathered, BaseMessageChunk) and len(gathered.content) > 0: + data_list = self.aimessages_to_event_data([gathered]) + yield ResultEvent(id=id, data=BaseOutputMessage(id=id, output=data_list.pop())) + + if isinstance(gathered, AddableDict): + yield ResultEvent(id=id, data=BaseOutputMessage(id=id, output=gather_output)) + + def invoke_stream(self, value, image=None) -> AsyncIterator[BaseEvent] | None: + if not isinstance(self.object, Runnable): + return None + messages = self.get_input_adaptor().to_input_message(value, image) + stream_result = self.object.astream(messages) + output_event = self.to_output_event(stream_result) + return output_event diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_openai_executor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_openai_executor.py new file mode 100644 index 00000000..269c4e33 --- /dev/null +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_openai_executor.py @@ -0,0 +1,71 @@ +from typing import Any, AsyncIterator +from langchain_community.callbacks.manager import get_openai_callback +from langchain_core.runnables import Runnable, RunnableBinding +from langchain.agents.agent import AgentExecutor +from langchain.chains.llm import LLMChain +from langchain.agents.mrkl.base import ZeroShotAgent +from .event import BaseEvent +from .langchain_adaptor import RunnableAdaptor +from .utils import is_community_installed, is_in_array_or_has_prefix, attempt_import + + +openai_models = [ + "gpt-4o", + "gpt-4", + "gpt-3.5", + "text-ada-001", + "ada", + "text-babbage-001", + "babbage", + "text-curie-001", + "curie", + "davinci", + "text-davinci-003", + "text-davinci-002", + "code-davinci-002", + "code-davinci-001", + "code-cushman-002", + "code-cushman-001", +] + + +def is_langchain_openai_installed(): + return attempt_import('langchain_openai') is not None + + +class OpenAIAdaptor(RunnableAdaptor): + @staticmethod + def recognizer(object, llm_type: str | None = None): + if isinstance(object, ZeroShotAgent): + return OpenAIAdaptor.recognizer(object.llm_chain) + + if not isinstance(object, Runnable): + return False + if llm_type == 'openai': + return True + if isinstance(object, RunnableBinding): + return OpenAIAdaptor.recognizer(object.bound) + + if isinstance(object, LLMChain): + return OpenAIAdaptor.recognizer(object.llm) + + if isinstance(object, AgentExecutor): + return OpenAIAdaptor.recognizer(object.agent) + + if is_community_installed(): + from langchain_community.llms.openai import OpenAIChat + if isinstance(object, OpenAIChat): + return is_in_array_or_has_prefix(openai_models, object.model_name) + if is_langchain_openai_installed(): + from langchain_openai import ChatOpenAI + if isinstance(object, ChatOpenAI): + return is_in_array_or_has_prefix(openai_models, object.model_name) + return False + + def invoke(self, value, image=None) -> Any | None: + with get_openai_callback() as cb: + return super().invoke(value, image) + + def invoke_stream(self, value, image=None) -> AsyncIterator[BaseEvent] | None: + with get_openai_callback() as cb: + return super().invoke_stream(value, image) diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_tongyi_executor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_tongyi_executor.py new file mode 100644 index 00000000..2a73fc40 --- /dev/null +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/adaptor/langchain_tongyi_executor.py @@ -0,0 +1,45 @@ +from typing import Any, Dict, Sequence +from langchain_core.messages import HumanMessage +from langchain_core.runnables import Runnable, RunnableBinding +from .langchain_adaptor import RunnableAdaptor +from .utils import is_community_installed, is_in_array_or_has_prefix + + +tongyi_models = [ + "qwen-max", + "qwen-plus", + "qwen-turbo", + "qwen-long", + "qwen-vl-max", + "qwen-vl-plus", + "qwen-vl-ocr", + "qwen-math-plus", + "qwen-audio-turbo", + "qwen-coder-plus", + "qwen-coder-turbo", + "qwen" +] + + +class TongyiAdaptor(RunnableAdaptor): + @staticmethod + def recognizer(object, llm_type: str | None = None): + if not isinstance(object, Runnable): + return False + if llm_type == 'tongyi': + return True + if isinstance(object, RunnableBinding): + return TongyiAdaptor.recognizer(object.bound) + if not is_community_installed(): + return False + from langchain_community.chat_models.tongyi import ChatTongyi + if isinstance(object, ChatTongyi): + return is_in_array_or_has_prefix(tongyi_models, object.model_name) + return False + + def to_input_message(self, value, image=None): + content: Sequence[str | Dict[str, Any]] = [{"text": value}] + if image is not None: + content.append({"image": image}) + message = HumanMessage(content=content) # type: ignore + return [message] diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/app.py b/packages/magent-ui-langchain/src/magent_ui_langchain/app.py index 3a13d5ff..6fcddf2d 100644 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/app.py +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/app.py @@ -1,5 +1,8 @@ +from magent_ui_core.utils import attempt_import, is_ipython +import nest_asyncio +import asyncio from pathlib import Path -from typing import Any +from typing import Any, Optional from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles @@ -9,12 +12,15 @@ import uvicorn import logging import os -from uvicorn.config import LOGGING_CONFIG +from uvicorn.config import LOGGING_CONFIG from magent_ui_langchain.routers.main import api_router from magent_ui_langchain.config import to_uvicorn_config, app_config -from magent_ui_langchain.core.current_executor import process_object +from magent_ui_core.current_executor import process_object + +# 应用 nest_asyncio 以解决事件循环冲突 +nest_asyncio.apply() # Use uvicorn's default logging configuration logging.config.dictConfig(LOGGING_CONFIG) # type: ignore @@ -27,12 +33,14 @@ static_dir = os.path.join(BASE_DIR, 'static') templates_dir = os.path.join(BASE_DIR, 'templates') - templates = Jinja2Templates(directory=templates_dir) -def launch(object: Any, **kwargs): - process_object(object) +def launch(object: Any, llm_type: str | None = None, **kwargs): + ''' + Launch the langchain server. + ''' + process_object(object, llm_type) logger.info("Current log level is") project_root_path = Path.cwd() @@ -44,10 +52,22 @@ def launch(object: Any, **kwargs): @asynccontextmanager async def lifespan(app: FastAPI): + url = f"http://localhost:{app_config.port}{app_config.root_path}" + logger.info(f"Server is running at {url}") + if is_ipython() and attempt_import('qrcode') is not None: + # 生成二维码并在 Jupyter Notebook 中显示 + import qrcode + qr_img = qrcode.make(url) + + from IPython.display import display, HTML, Image # type: ignore + # 在 Jupyter Notebook 的输出区域打印 URL 和二维码 + display( + HTML(f"

Server is running at: {url}

")) + display(qr_img) + if app_config.open_browser: - url = f"http://localhost:{app_config.port}{app_config.root_path}" webbrowser.open(url) - logger.info(f"Server is running at {url}") + yield logger.info('Server finished') @@ -58,20 +78,11 @@ async def lifespan(app: FastAPI): # static if os.path.exists(static_dir): - app.mount(app_config.full_static_path, StaticFiles(directory=static_dir, - html=True), name="static") + app.mount(app_config.full_static_path, StaticFiles( + directory=static_dir, html=True), name="static") else: logger.info('Can not find static directory. ', static_dir) - # resources - # if not app_config.resource_dir_path.exists(): - # logger.info('Resource directory not exist. create at', - # app_config.resource_dir_path) - # os.makedirs(app_config.resource_dir_path) - - # app.mount(app_config.full_resource_path, StaticFiles(directory=app_config.resource_dir_path, - # html=True)) - # auto redirect to app url @app.get(app_config.root_path, response_class=HTMLResponse) async def to_index_page(request: Request): @@ -90,11 +101,17 @@ async def to_app_page(request: Request): "appUrl": app_config.app_url, "staticUrl": app_config.static_url, } - return templates.TemplateResponse( - request=request, name="index.html", context={ - "page_config": page_config, "static_url": app_config.static_url - } - ) + return templates.TemplateResponse(request=request, name="index.html", context={ + "page_config": page_config, "static_url": app_config.static_url + }) uvicorn_config = to_uvicorn_config(app_config.config) - uvicorn.run(app, log_level='info', **uvicorn_config) + + if is_ipython(): + # 在 Jupyter Notebook 中运行 + import asyncio + asyncio.run(uvicorn.run(app, log_level='info', + loop="asyncio", **uvicorn_config)) # type: ignore + else: + uvicorn.run(app, log_level='info', + loop="asyncio", **uvicorn_config) diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/core/__init__.py b/packages/magent-ui-langchain/src/magent_ui_langchain/core/__init__.py deleted file mode 100644 index a6791615..00000000 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/core/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from .adapter_registry import adapter_registry -from .langchain_executor import LLMStreamExecutor, ChainStreamExecutor, RunnableExecutor - -adapter_registry.register_adapter( - LLMStreamExecutor.recognizer, LLMStreamExecutor, 1) - - -adapter_registry.register_adapter( - ChainStreamExecutor.recognizer, ChainStreamExecutor, 1) - - -adapter_registry.register_adapter( - RunnableExecutor.recognizer, RunnableExecutor) - diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/core/adapter_registry.py b/packages/magent-ui-langchain/src/magent_ui_langchain/core/adapter_registry.py deleted file mode 100644 index 505daffe..00000000 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/core/adapter_registry.py +++ /dev/null @@ -1,36 +0,0 @@ -from abc import ABCMeta -from typing import Any, Callable, List, Tuple, Type -from .executor import Executor - - -class AdapterRegistry: - def __init__(self): - self.adapters: List[Tuple[int, Callable[[ - Any], bool], Type[Executor]]] = [] - - def register_adapter(self, recognizer: Callable[[Any], bool], adapter_cls: Type[Executor], priority: int = 0, ): - """ - Register a new adapter with a priority and a recognizer function. - - :param priority: The priority of the adapter (lower numbers mean higher priority). - :param recognizer: A function that returns True if the adapter can handle the object. - :param adapter_cls: The adapter class to use if the recognizer returns True. - """ - self.adapters.append((priority, recognizer, adapter_cls)) - # Sort adapters by priority - self.adapters.sort(key=lambda x: x[0]) - - def get_adapter(self, obj: Any) -> Executor: - """ - Get the appropriate adapter for the given object based on registered recognizers. - - :param obj: The object to be processed. - :return: An instance of the appropriate adapter. - """ - for _, recognizer, adapter_cls in self.adapters: - if recognizer(obj): - return adapter_cls(object=obj) - raise ValueError(f"No suitable adapter found for object: {obj}") - - -adapter_registry = AdapterRegistry() diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/core/current_executor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/core/current_executor.py deleted file mode 100644 index 1a4ef41b..00000000 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/core/current_executor.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any -from .executor import Executor -from .adapter_registry import adapter_registry - - -_current_executor = None - - -# Function to process any object using the registry -def process_object(obj: Any) -> Executor: - global _current_executor - executor = adapter_registry.get_adapter(obj) - _current_executor = executor - return executor - - -def get_current_executor(): - return _current_executor diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/core/executor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/core/executor.py deleted file mode 100644 index 7ba77ef1..00000000 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/core/executor.py +++ /dev/null @@ -1,23 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, AsyncIterator, Iterator - -from pydantic import BaseModel - - -class Executor(BaseModel, ABC): - name: str = "custom" - object: Any - - @abstractmethod - def invoke(self, value) -> Any | None: - """Chat invoke""" - raise NotImplementedError( - "Each adapter must implement the `invoke` method.") - - -class StreamExecutor(Executor): - @abstractmethod - def invoke_stream(self, value) -> AsyncIterator[Any] | None: - """Chat invoke""" - raise NotImplementedError( - "Each adapter must implement the `invoke` method.") diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/core/langchain_executor.py b/packages/magent-ui-langchain/src/magent_ui_langchain/core/langchain_executor.py deleted file mode 100644 index b2b803f8..00000000 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/core/langchain_executor.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Any, AsyncIterator, Iterator -from langchain_community.callbacks.manager import get_openai_callback -from langchain.llms.base import BaseLLM -from langchain_core.runnables import Runnable -from langchain.chains.base import Chain -from .executor import StreamExecutor, Executor -from langchain_core.messages import HumanMessage - -class LLMStreamExecutor(StreamExecutor): - @staticmethod - def recognizer(object): - return isinstance(object, BaseLLM) - - def invoke_stream(self, value) -> AsyncIterator[Any] | None: - if not isinstance(self.object, BaseLLM): - return None - - with get_openai_callback() as cb: - result = self.object.astream(value) - return result - - -class ChainStreamExecutor(StreamExecutor): - @staticmethod - def recognizer(object): - return isinstance(object, Chain) - - def invoke_stream(self, value) -> AsyncIterator[Any] | None: - if not isinstance(self.object, Chain): - return None - with get_openai_callback() as cb: - result = self.object.astream(value) - return result - - -# class RunnableStreamExecutor(StreamExecutor): -# @staticmethod -# def recognizer(object): -# return isinstance(object, Runnable) - -# def invoke(self, value) -> AsyncIterator[Any] | None: -# if not isinstance(self.object, Runnable): -# return None - -# with get_openai_callback() as cb: -# result = self.object.stream(value) -# return result - -class RunnableExecutor(StreamExecutor): - @staticmethod - def recognizer(object): - return isinstance(object, Runnable) - - def invoke(self, value, image=None) -> Any | None: - if not isinstance(self.object, Runnable): - return None - - with get_openai_callback() as cb: - content = [{"text": value}] - - if image is not None: - content.append({"image": image}) - - message = HumanMessage(content=content) - result = self.object.invoke([message]) - return result - - def invoke_stream(self, value, image=None) -> AsyncIterator[Any] | None: - if not isinstance(self.object, Runnable): - return None - - with get_openai_callback() as cb: - content = [{"text": value}] - - if image is not None: - content.append({"image": image}) - - message = HumanMessage(content=content) - result = self.object.astream([message]) - return result - diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/routers/chat/router.py b/packages/magent-ui-langchain/src/magent_ui_langchain/routers/chat/router.py index bc51a44e..ec3c9a90 100644 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/routers/chat/router.py +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/routers/chat/router.py @@ -1,15 +1,11 @@ -import asyncio import enum import json -from typing import AsyncIterable, List -from fastapi import APIRouter, HTTPException +from typing import AsyncIterable +from fastapi import APIRouter from pydantic import BaseModel from sse_starlette import EventSourceResponse, ServerSentEvent - -from langchain_core.messages.ai import AIMessageChunk - -from magent_ui_langchain.core.current_executor import get_current_executor -from magent_ui_langchain.core.executor import StreamExecutor +from magent_ui_langchain.core.current_executor import get_current_invoke_adaptor +from magent_ui_langchain.core.base_adaptor import StreamInvokeAdaptor from typing import Optional @@ -28,6 +24,7 @@ class MessageCreate(BaseModel): input: str image: Optional[str] = None + class MessageOutput(BaseModel): message_id: int conversation_id: str @@ -53,36 +50,30 @@ async def async_generator_from_sync(gen): async def send_message(model: MessageCreate, stream: bool) -> AsyncIterable[ServerSentEvent]: - current = get_current_executor() + current = get_current_invoke_adaptor() + id = model.conversation_id if current is None: yield ServerSentEvent(event=SSEType.ERROR.value, id=model.conversation_id, data=json.dumps({"error_message": "error executor"}, ensure_ascii=False)) return - if isinstance(current, StreamExecutor) and stream: - msg_iterator = current.invoke_stream(model.input, getattr(model, 'image', None)) + if isinstance(current, StreamInvokeAdaptor) and stream: + msg_iterator = current.invoke_stream( + model.input, image=getattr(model, 'image', None)) if msg_iterator is None: - yield ServerSentEvent(event=SSEType.ERROR.value, id=model.conversation_id, data=json.dumps({"error_message": "error stream invoke_stream"}, ensure_ascii=False)) + yield ServerSentEvent(event=SSEType.ERROR.value, id=id, data=json.dumps({"error_message": "error stream invoke_stream"}, ensure_ascii=False)) return - try: - total_content = '' # 存储所有的模型返回,最后一次返回给接口,重要为了兼容qwen的多模态表现和文本态不一致 - async for msg_chunk in msg_iterator: - print(msg_chunk) - content = msg_chunk.content - if isinstance(content, list) and content: - content = content[0].get('text', '') - if not content: - content = '' - total_content += content - if isinstance(msg_chunk, AIMessageChunk): - # 最后一次返回 - if msg_chunk.response_metadata is not None and len(msg_chunk.response_metadata) > 0: - yield ServerSentEvent(event=SSEType.RESULT.value, id=model.conversation_id, data=json.dumps({"output": total_content, "id": msg_chunk.id, "response_metadata": msg_chunk.response_metadata}, ensure_ascii=False)) - else: - yield ServerSentEvent(event=SSEType.CHUNK.value, id=model.conversation_id, data=json.dumps({"output": content, "id": msg_chunk.id}, ensure_ascii=False)) + async for event in msg_iterator: + data = '' + if isinstance(event.data, str): + data = event.data + else: + data = event.data.model_dump_json() + if len(event.id) > 0: + id = event.id + yield ServerSentEvent(event=event.type, id=id, data=data) except Exception as e: - print('Exception', e) - yield ServerSentEvent(event=SSEType.ERROR.value, id=model.conversation_id, data=json.dumps({"error_message": "error in stream execute"}, ensure_ascii=False)) - return + yield ServerSentEvent(event=SSEType.ERROR.value, id=id, data=json.dumps({"error_message": "error in stream execute"}, ensure_ascii=False)) + raise e @router.post("/chat-stream") @@ -92,17 +83,11 @@ async def stream_chat(model: MessageCreate): @router.post("/chat") async def chat(model: MessageCreate): - current = get_current_executor() + current = get_current_invoke_adaptor() if current is None: return {"error_message": "error executor"} - try: - result = current.invoke(model.input, getattr(model, 'image', None)) - content = result.content - if isinstance(msg_chunk.content, list): - content = msg_chunk.content[0]['text'] - output_dict = {'id': result.id, 'output': content, - 'response_metadata': result.response_metadata} - return output_dict + return current.invoke( + model.input, image=getattr(model, 'image', None)) except Exception as e: return {"error_message": "chat execute error"} diff --git a/packages/magent-ui-langchain/src/magent_ui_langchain/templates/index.html b/packages/magent-ui-langchain/src/magent_ui_langchain/templates/index.html index 5c0f0621..5b5be5ae 100644 --- a/packages/magent-ui-langchain/src/magent_ui_langchain/templates/index.html +++ b/packages/magent-ui-langchain/src/magent_ui_langchain/templates/index.html @@ -2,6 +2,7 @@ + ( return (
- + {instance.chat && } {/* diff --git a/web-packages/magent-chat/package.json b/web-packages/magent-chat/package.json index 1227bcde..73ffa140 100644 --- a/web-packages/magent-chat/package.json +++ b/web-packages/magent-chat/package.json @@ -61,14 +61,15 @@ "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "react-zoom-pan-pinch": "^3.6.1", + "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "uuid": "^9.0.0" }, "devDependencies": { + "@types/lodash.debounce": "^4.0.9", "@types/react": "^18.2.25", "@types/react-syntax-highlighter": "^15.5.13", - "@types/lodash.debounce": "^4.0.9", "@types/uuid": "^9.0.2" }, "peerDependencies": { diff --git a/web-packages/magent-chat/src/chat-view/view.tsx b/web-packages/magent-chat/src/chat-view/view.tsx index f2ee24c9..cdfa71fd 100644 --- a/web-packages/magent-chat/src/chat-view/view.tsx +++ b/web-packages/magent-chat/src/chat-view/view.tsx @@ -21,6 +21,7 @@ import classnames from 'classnames'; import type { RefObject } from 'react'; import { forwardRef } from 'react'; import { useEffect, useRef } from 'react'; +import rehypeRaw from 'rehype-raw'; import breaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; @@ -178,6 +179,7 @@ export class ChatView extends BaseView { return { components: { code: CodeBlock, img: ImageModal }, remarkPlugins: [remarkGfm, breaks], + rehypePlugins: [rehypeRaw], }; } diff --git a/web-packages/magent-flow/src/tailwind.out.css b/web-packages/magent-flow/src/tailwind.out.css index 0cdf443c..a7c645b7 100644 --- a/web-packages/magent-flow/src/tailwind.out.css +++ b/web-packages/magent-flow/src/tailwind.out.css @@ -51,7 +51,7 @@ --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; - --tw-contain-style: + --tw-contain-style: } ::backdrop { @@ -105,7 +105,7 @@ --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; - --tw-contain-style: + --tw-contain-style: } .pointer-events-none { @@ -474,12 +474,12 @@ .border-blue-500 { --tw-border-opacity: 1; - border-color: rgb(59 130 246 / var(--tw-border-opacity)) + border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)) } .border-gray-200 { --tw-border-opacity: 1; - border-color: rgb(229 231 235 / var(--tw-border-opacity)) + border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)) } .border-transparent { @@ -488,57 +488,57 @@ .\!bg-gray-50 { --tw-bg-opacity: 1 !important; - background-color: rgb(249 250 251 / var(--tw-bg-opacity)) !important + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)) !important } .bg-blue-100 { --tw-bg-opacity: 1; - background-color: rgb(219 234 254 / var(--tw-bg-opacity)) + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)) } .bg-blue-400 { --tw-bg-opacity: 1; - background-color: rgb(96 165 250 / var(--tw-bg-opacity)) + background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1)) } .bg-blue-50 { --tw-bg-opacity: 1; - background-color: rgb(239 246 255 / var(--tw-bg-opacity)) + background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)) } .bg-gray-100 { --tw-bg-opacity: 1; - background-color: rgb(243 244 246 / var(--tw-bg-opacity)) + background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)) } .bg-gray-200 { --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity)) + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)) } .bg-gray-50 { --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity)) + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)) } .bg-green-50 { --tw-bg-opacity: 1; - background-color: rgb(240 253 244 / var(--tw-bg-opacity)) + background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1)) } .bg-red-50 { --tw-bg-opacity: 1; - background-color: rgb(254 242 242 / var(--tw-bg-opacity)) + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)) } .bg-white { --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)) + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)) } .bg-yellow-50 { --tw-bg-opacity: 1; - background-color: rgb(254 252 232 / var(--tw-bg-opacity)) + background-color: rgb(254 252 232 / var(--tw-bg-opacity, 1)) } .p-1 { @@ -616,52 +616,52 @@ .text-\[\#155EEF\] { --tw-text-opacity: 1; - color: rgb(21 94 239 / var(--tw-text-opacity)) + color: rgb(21 94 239 / var(--tw-text-opacity, 1)) } .text-\[\#2970FF\] { --tw-text-opacity: 1; - color: rgb(41 112 255 / var(--tw-text-opacity)) + color: rgb(41 112 255 / var(--tw-text-opacity, 1)) } .text-\[\#444CE7\] { --tw-text-opacity: 1; - color: rgb(68 76 231 / var(--tw-text-opacity)) + color: rgb(68 76 231 / var(--tw-text-opacity, 1)) } .text-blue-500 { --tw-text-opacity: 1; - color: rgb(59 130 246 / var(--tw-text-opacity)) + color: rgb(59 130 246 / var(--tw-text-opacity, 1)) } .text-gray-300 { --tw-text-opacity: 1; - color: rgb(209 213 219 / var(--tw-text-opacity)) + color: rgb(209 213 219 / var(--tw-text-opacity, 1)) } .text-gray-400 { --tw-text-opacity: 1; - color: rgb(156 163 175 / var(--tw-text-opacity)) + color: rgb(156 163 175 / var(--tw-text-opacity, 1)) } .text-gray-50 { --tw-text-opacity: 1; - color: rgb(249 250 251 / var(--tw-text-opacity)) + color: rgb(249 250 251 / var(--tw-text-opacity, 1)) } .text-gray-700 { --tw-text-opacity: 1; - color: rgb(55 65 81 / var(--tw-text-opacity)) + color: rgb(55 65 81 / var(--tw-text-opacity, 1)) } .text-gray-800 { --tw-text-opacity: 1; - color: rgb(31 41 55 / var(--tw-text-opacity)) + color: rgb(31 41 55 / var(--tw-text-opacity, 1)) } .text-gray-900 { --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity)) + color: rgb(17 24 39 / var(--tw-text-opacity, 1)) } .opacity-30 { @@ -703,7 +703,7 @@ .hover\:bg-gray-50:hover { --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity)) + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)) } .hover\:shadow-2xl:hover {