From 0ade2eb074e8c21cedf6b4f835861b7e6f1edb4c Mon Sep 17 00:00:00 2001 From: xszyou Date: Wed, 4 Dec 2024 22:20:51 +0800 Subject: [PATCH] =?UTF-8?q?fay=E5=B9=B4=E7=BF=BB=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 消灭langchain警告信息; - 修复与funasr对接的包兼容性; - 推送给数字人的消息区分交互和自动播放; - 上传ReWoo agent测试代码。 --- core/fay_core.py | 2 +- llm/agent/fay_agent.py | 6 +- llm/nlp_langchain.py | 8 +- qa.csv | 2 +- requirements.txt | 2 +- test/test_auto_play_server.py | 4 +- ...chain.ipynb => test_langchain_react.ipynb} | 0 test/test_langchain_rewoo.ipynb | 675 ++++++++++++++++++ 8 files changed, 687 insertions(+), 12 deletions(-) rename test/{test_langchain.ipynb => test_langchain_react.ipynb} (100%) create mode 100644 test/test_langchain_rewoo.ipynb diff --git a/core/fay_core.py b/core/fay_core.py index ec4874b..f5ea029 100644 --- a/core/fay_core.py +++ b/core/fay_core.py @@ -436,7 +436,7 @@ class FeiFei: #发送音频给数字人接口 if wsa_server.get_instance().is_connected(interact.data.get("user")): - content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'HttpValue': f'http://{cfg.fay_url}:5000/audio/' + os.path.basename(file_url), 'Text': text, 'Time': audio_length, 'Type': 'interact'}, 'Username' : interact.data.get('user')} + content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'HttpValue': f'http://{cfg.fay_url}:5000/audio/' + os.path.basename(file_url), 'Text': text, 'Time': audio_length, 'Type': 'interact' if interact.interact_type == 1 else 'auto_play'}, 'Username' : interact.data.get('user')} #计算lips if platform.system() == "Windows": try: diff --git a/llm/agent/fay_agent.py b/llm/agent/fay_agent.py index c06992a..15f3f13 100644 --- a/llm/agent/fay_agent.py +++ b/llm/agent/fay_agent.py @@ -10,7 +10,7 @@ from llm.agent.tools.WebPageRetriever import WebPageRetriever from llm.agent.tools.WebPageScraper import WebPageScraper from llm.agent.tools.ToRemind import ToRemind from langgraph.prebuilt import create_react_agent -from langchain_openai import ChatOpenAI +from langchain_community.chat_models import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver import utils.config_util as cfg from utils import util @@ -26,8 +26,8 @@ class FayAgentCore(): os.environ["OPENAI_API_BASE"] = cfg.gpt_base_url os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com" - os.environ["LANGCHAIN_API_KEY"] = "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" - os.environ["LANGCHAIN_PROJECT"] = "pr-best-artist-21" + os.environ["LANGCHAIN_API_KEY"] = "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" + os.environ["LANGCHAIN_PROJECT"] = "my-agent" #创建llm self.llm = ChatOpenAI(model=cfg.gpt_model_engine) diff --git a/llm/nlp_langchain.py b/llm/nlp_langchain.py index e21523b..98a9f21 100644 --- a/llm/nlp_langchain.py +++ b/llm/nlp_langchain.py @@ -1,11 +1,11 @@ import hashlib import os -from langchain.document_loaders import PyPDFLoader -from langchain.embeddings.openai import OpenAIEmbeddings +from langchain_community.document_loaders import PyPDFLoader +from langchain_community.embeddings import OpenAIEmbeddings from langchain.indexes.vectorstore import VectorstoreIndexCreator, VectorStoreIndexWrapper -from langchain.vectorstores.chroma import Chroma -from langchain.chat_models import ChatOpenAI +from langchain_community.vectorstores import Chroma +from langchain_community.chat_models import ChatOpenAI from utils import config_util as cfg from utils import util diff --git a/qa.csv b/qa.csv index 6fd3715..6fb0747 100644 --- a/qa.csv +++ b/qa.csv @@ -1 +1 @@ -你好,你好!有什么我可以帮助你的吗? \ No newline at end of file +问题,答案,执行的脚本 diff --git a/requirements.txt b/requirements.txt index e6abf64..41a8e53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ requests numpy pyaudio~=0.2.11 -websockets~=10.2 +websockets~=10.4 ws4py~=0.5.1 PyQt5==5.15.10 PyQt5-sip==12.13.0 diff --git a/test/test_auto_play_server.py b/test/test_auto_play_server.py index e03071b..9c7b111 100644 --- a/test/test_auto_play_server.py +++ b/test/test_auto_play_server.py @@ -10,8 +10,8 @@ app = Flask(__name__) @app.route('/get_auto_play_item', methods=['POST']) def get_wav(): # 获取用户标识(例如,通过POST请求中的JSON数据) - data = request.json - user = data.get('user', 'User') + # data = request.json + # user = data.get('user', 'User') # 模拟WAV文件的URL(这里假设是某个静态文件服务的URL) wav_url = ""#f"http://120.79.187.154:5000/audio/sample-1729231423801.wav" diff --git a/test/test_langchain.ipynb b/test/test_langchain_react.ipynb similarity index 100% rename from test/test_langchain.ipynb rename to test/test_langchain_react.ipynb diff --git a/test/test_langchain_rewoo.ipynb b/test/test_langchain_rewoo.ipynb new file mode 100644 index 0000000..b4b0cb5 --- /dev/null +++ b/test/test_langchain_rewoo.ipynb @@ -0,0 +1,675 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "1、安装依赖。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: langchain in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (0.3.7)\n", + "Requirement already satisfied: langchain-openai in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (0.2.6)\n", + "Collecting langchain-openai\n", + " Downloading langchain_openai-0.2.8-py3-none-any.whl.metadata (2.6 kB)\n", + "Requirement already satisfied: langgraph in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (0.2.45)\n", + "Collecting langgraph\n", + " Downloading langgraph-0.2.50-py3-none-any.whl.metadata (15 kB)\n", + "Requirement already satisfied: langsmith in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (0.1.142)\n", + "Collecting langsmith\n", + " Downloading langsmith-0.1.143-py3-none-any.whl.metadata (13 kB)\n", + "Requirement already satisfied: PyYAML>=5.3 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (6.0.2)\n", + "Requirement already satisfied: SQLAlchemy<3,>=1.4 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (2.0.35)\n", + "Requirement already satisfied: aiohttp<4.0.0,>=3.8.3 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (3.10.10)\n", + "Requirement already satisfied: langchain-core<0.4.0,>=0.3.15 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (0.3.15)\n", + "Requirement already satisfied: langchain-text-splitters<0.4.0,>=0.3.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (0.3.2)\n", + "Requirement already satisfied: numpy<2.0.0,>=1.26.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (1.26.4)\n", + "Requirement already satisfied: pydantic<3.0.0,>=2.7.4 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (2.9.2)\n", + "Requirement already satisfied: requests<3,>=2 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (2.32.3)\n", + "Requirement already satisfied: tenacity!=8.4.0,<10,>=8.1.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain) (8.2.3)\n", + "Collecting langchain-core<0.4.0,>=0.3.15 (from langchain)\n", + " Downloading langchain_core-0.3.19-py3-none-any.whl.metadata (6.3 kB)\n", + "Requirement already satisfied: openai<2.0.0,>=1.54.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain-openai) (1.54.3)\n", + "Requirement already satisfied: tiktoken<1,>=0.7 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain-openai) (0.8.0)\n", + "Collecting langgraph-checkpoint<3.0.0,>=2.0.4 (from langgraph)\n", + " Downloading langgraph_checkpoint-2.0.4-py3-none-any.whl.metadata (4.6 kB)\n", + "Requirement already satisfied: langgraph-sdk<0.2.0,>=0.1.32 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langgraph) (0.1.35)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langsmith) (0.27.2)\n", + "Requirement already satisfied: orjson<4.0.0,>=3.9.14 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langsmith) (3.10.11)\n", + "Requirement already satisfied: requests-toolbelt<2.0.0,>=1.0.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langsmith) (1.0.0)\n", + "Requirement already satisfied: aiohappyeyeballs>=2.3.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (2.4.3)\n", + "Requirement already satisfied: aiosignal>=1.1.2 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.3.1)\n", + "Requirement already satisfied: attrs>=17.3.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (24.2.0)\n", + "Requirement already satisfied: frozenlist>=1.1.1 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.5.0)\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (6.1.0)\n", + "Requirement already satisfied: yarl<2.0,>=1.12.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.17.1)\n", + "Requirement already satisfied: anyio in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpx<1,>=0.23.0->langsmith) (4.6.2.post1)\n", + "Requirement already satisfied: certifi in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpx<1,>=0.23.0->langsmith) (2024.8.30)\n", + "Requirement already satisfied: httpcore==1.* in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpx<1,>=0.23.0->langsmith) (1.0.6)\n", + "Requirement already satisfied: idna in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpx<1,>=0.23.0->langsmith) (3.10)\n", + "Requirement already satisfied: sniffio in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpx<1,>=0.23.0->langsmith) (1.3.1)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from httpcore==1.*->httpx<1,>=0.23.0->langsmith) (0.14.0)\n", + "Requirement already satisfied: jsonpatch<2.0,>=1.33 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain-core<0.4.0,>=0.3.15->langchain) (1.33)\n", + "Requirement already satisfied: packaging<25,>=23.2 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain-core<0.4.0,>=0.3.15->langchain) (24.1)\n", + "Requirement already satisfied: typing-extensions>=4.7 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langchain-core<0.4.0,>=0.3.15->langchain) (4.12.2)\n", + "Requirement already satisfied: msgpack<2.0.0,>=1.1.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langgraph-checkpoint<3.0.0,>=2.0.4->langgraph) (1.1.0)\n", + "Requirement already satisfied: httpx-sse>=0.4.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from langgraph-sdk<0.2.0,>=0.1.32->langgraph) (0.4.0)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from openai<2.0.0,>=1.54.0->langchain-openai) (1.9.0)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from openai<2.0.0,>=1.54.0->langchain-openai) (0.7.0)\n", + "Requirement already satisfied: tqdm>4 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from openai<2.0.0,>=1.54.0->langchain-openai) (4.67.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from pydantic<3.0.0,>=2.7.4->langchain) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.23.4 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from pydantic<3.0.0,>=2.7.4->langchain) (2.23.4)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from requests<3,>=2->langchain) (3.4.0)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from requests<3,>=2->langchain) (2.2.3)\n", + "Requirement already satisfied: greenlet!=0.4.17 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from SQLAlchemy<3,>=1.4->langchain) (3.1.1)\n", + "Requirement already satisfied: regex>=2022.1.18 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from tiktoken<1,>=0.7->langchain-openai) (2024.11.6)\n", + "Requirement already satisfied: jsonpointer>=1.9 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from jsonpatch<2.0,>=1.33->langchain-core<0.4.0,>=0.3.15->langchain) (3.0.0)\n", + "Requirement already satisfied: colorama in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from tqdm>4->openai<2.0.0,>=1.54.0->langchain-openai) (0.4.6)\n", + "Requirement already satisfied: propcache>=0.2.0 in d:\\anaconda3\\envs\\fay312-2\\lib\\site-packages (from yarl<2.0,>=1.12.0->aiohttp<4.0.0,>=3.8.3->langchain) (0.2.0)\n", + "Downloading langchain_openai-0.2.8-py3-none-any.whl (50 kB)\n", + "Downloading langgraph-0.2.50-py3-none-any.whl (124 kB)\n", + "Downloading langsmith-0.1.143-py3-none-any.whl (306 kB)\n", + "Downloading langchain_core-0.3.19-py3-none-any.whl (409 kB)\n", + "Downloading langgraph_checkpoint-2.0.4-py3-none-any.whl (23 kB)\n", + "Installing collected packages: langsmith, langchain-core, langgraph-checkpoint, langchain-openai, langgraph\n", + " Attempting uninstall: langsmith\n", + " Found existing installation: langsmith 0.1.142\n", + " Uninstalling langsmith-0.1.142:\n", + " Successfully uninstalled langsmith-0.1.142\n", + " Attempting uninstall: langchain-core\n", + " Found existing installation: langchain-core 0.3.15\n", + " Uninstalling langchain-core-0.3.15:\n", + " Successfully uninstalled langchain-core-0.3.15\n", + " Attempting uninstall: langgraph-checkpoint\n", + " Found existing installation: langgraph-checkpoint 2.0.2\n", + " Uninstalling langgraph-checkpoint-2.0.2:\n", + " Successfully uninstalled langgraph-checkpoint-2.0.2\n", + " Attempting uninstall: langchain-openai\n", + " Found existing installation: langchain-openai 0.2.6\n", + " Uninstalling langchain-openai-0.2.6:\n", + " Successfully uninstalled langchain-openai-0.2.6\n", + " Attempting uninstall: langgraph\n", + " Found existing installation: langgraph 0.2.45\n", + " Uninstalling langgraph-0.2.45:\n", + " Successfully uninstalled langgraph-0.2.45\n", + "Successfully installed langchain-core-0.3.19 langchain-openai-0.2.8 langgraph-0.2.50 langgraph-checkpoint-2.0.4 langsmith-0.1.143\n" + ] + } + ], + "source": [ + "!pip install -U langchain langchain-openai langgraph langsmith" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2、配置环境变量。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"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\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"rewoo\"\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \n", + "os.environ[\"OPENAI_API_BASE\"] = " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "3、定义llm model。" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello! How can I assist you today?'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "llm = ChatOpenAI(model=\"gpt-4\")\n", + "llm.invoke(\"Hello, world!\").content#test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "4、定义工具。" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "现在时间是:23:54 星期二 2024年12月03日\n" + ] + } + ], + "source": [ + "import abc\n", + "from typing import Any\n", + "from datetime import datetime\n", + "from langchain.tools import BaseTool\n", + "\n", + "class QueryTime(BaseTool, abc.ABC):\n", + " name: str = \"QueryTime\"\n", + " description: str = \"用于查询当前日期、星期几及时间\" \n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + "\n", + " async def _arun(self, *args: Any, **kwargs: Any) -> Any:\n", + " # 用例中没有用到 arun,不予具体实现\n", + " pass\n", + "\n", + " def _run(self, para) -> str:\n", + " # 获取当前时间\n", + " now = datetime.now()\n", + " # 获取当前日期\n", + " today = now.date()\n", + " # 获取星期几的信息\n", + " week_day = today.strftime(\"%A\")\n", + " # 将星期几的英文名称转换为中文\n", + " week_day_zh = {\n", + " \"Monday\": \"星期一\",\n", + " \"Tuesday\": \"星期二\",\n", + " \"Wednesday\": \"星期三\",\n", + " \"Thursday\": \"星期四\",\n", + " \"Friday\": \"星期五\",\n", + " \"Saturday\": \"星期六\",\n", + " \"Sunday\": \"星期日\",\n", + " }.get(week_day, \"未知\")\n", + " # 将日期格式化为字符串\n", + " date_str = today.strftime(\"%Y年%m月%d日\")\n", + " # 将时间格式化为字符串\n", + " time_str = now.strftime(\"%H:%M\")\n", + "\n", + " return f\"现在时间是:{time_str} {week_day_zh} {date_str}\"\n", + "\n", + "if __name__ == \"__main__\":\n", + " tool = QueryTime()\n", + " result = tool.run(\"\")\n", + " print(result)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "天气预报信息:今天天气:clear sky,气温:23.19摄氏度,风速:1.74 m/s。\n" + ] + } + ], + "source": [ + "from typing import Any\n", + "\n", + "import requests\n", + "from langchain.tools import BaseTool\n", + "from urllib.parse import quote\n", + "\n", + "class Weather(BaseTool):\n", + " name: str = \"Weather\"\n", + " description: str = \"此工具用于获取天气预报信息,需传入英文的城市名,参数格式:Guangzhou\"\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + "\n", + " async def _arun(self, *args: Any, **kwargs: Any) -> Any:\n", + " # 用例中没有用到 arun 不予具体实现\n", + " pass\n", + "\n", + "\n", + " def _run(self, para: str) -> str:\n", + " try:\n", + " if not para:\n", + " return \"参数不能为空\"\n", + " encoded_city = quote(para)\n", + "\n", + " api_url = f\"http://api.openweathermap.org/data/2.5/weather?q={encoded_city}&appid=272fcb70d2c4e6f5134c2dce7d091df6\"\n", + " response = requests.get(api_url)\n", + " if response.status_code == 200:\n", + " weather_data = response.json()\n", + " # 提取天气信息\n", + " temperature_kelvin = weather_data['main']['temp']\n", + " temperature_celsius = temperature_kelvin - 273.15\n", + " min_temperature_kelvin = weather_data['main']['temp_min']\n", + " max_temperature_kelvin = weather_data['main']['temp_max']\n", + " min_temperature_celsius = min_temperature_kelvin - 273.15\n", + " max_temperature_celsius = max_temperature_kelvin - 273.15\n", + " description = weather_data['weather'][0]['description']\n", + " wind_speed = weather_data['wind']['speed']\n", + "\n", + " # 构建天气描述\n", + " weather_description = f\"今天天气:{description},气温:{temperature_celsius:.2f}摄氏度,风速:{wind_speed} m/s。\"\n", + "\n", + " return f\"天气预报信息:{weather_description}\"\n", + " else:\n", + " return f\"无法获取天气预报信息,状态码:{response.status_code}\"\n", + " except Exception as e:\n", + " return f\"发生错误:{str(e)}\"\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " weather_tool = Weather()\n", + " weather_info = weather_tool.run(\"Guangzhou\")\n", + " print(weather_info)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "quertTime = QueryTime()\n", + "weather = Weather()\n", + "tools = [quertTime, weather]\n", + "toolsStr = \"\"\n", + "for tool in tools:\n", + " toolsStr += f\"{tool.name}[input]:{tool.description}\\n\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "5、定义状态" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List\n", + "from typing_extensions import TypedDict\n", + "from langgraph.graph import MessagesState\n", + "\n", + "class ReWOO(MessagesState):\n", + " task: str\n", + " plan_string: str\n", + " steps: List\n", + " results: dict\n", + " result: str\n", + " summary: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "6、定义Planner 节点" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"For the following task, make plans that can solve the problem step by step. For each plan, indicate \\\n", + "which external tool together with tool input to retrieve evidence. You can store the evidence into a \\\n", + "variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)\n", + "\n", + "Tools can be one of the following:\n", + "(1) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general\n", + "world knowledge and common sense. Prioritize it when you are confident in solving the problem\n", + "yourself. Input can be any instruction.\n", + "{tools}\n", + "\n", + "For example,\n", + "Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x\n", + "hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours\n", + "less than Toby. How many hours did Rebecca work?\n", + "Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve\n", + "with Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x − 10) + ((2x − 10) − 8) = 157]\n", + "Plan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]\n", + "Plan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 ∗ #E2 − 10) − 8]\n", + "\n", + "Begin! \n", + "Describe your plans with rich details. Each Plan should be followed by only one #E.\n", + "\n", + "Task: {task}\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "\n", + "def get_plan(state: ReWOO):\n", + " regex_pattern = r\"Plan:\\s*(.+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]\"\n", + " prompt_template = ChatPromptTemplate.from_messages([SystemMessage(\"你的名字叫张三,18岁。\"), HumanMessage(content=prompt, name=\"User\")])#TODO 这里补充历史消息记录及总结\n", + " planner = prompt_template | llm\n", + " task = state[\"task\"]\n", + " result = planner.invoke({\"task\": task, \"tools\" : toolsStr})\n", + " # Find all matches in the sample text\n", + " matches = re.findall(regex_pattern, result.content)\n", + " return {\"steps\": matches, \"plan_string\": result.content}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "7、定义工具执行节点" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "def _get_current_task(state: ReWOO):\n", + " if \"results\" not in state or state[\"results\"] is None:\n", + " return 1\n", + " if len(state[\"results\"]) == len(state[\"steps\"]):\n", + " return None\n", + " else:\n", + " return len(state[\"results\"]) + 1\n", + "\n", + "\n", + "def tool_execution(state: ReWOO):\n", + " \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n", + " _step = _get_current_task(state)\n", + " _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n", + " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", + " for k, v in _results.items():\n", + " tool_input = tool_input.replace(k, v)\n", + " if tool == \"QueryTime\":\n", + " result = quertTime.invoke(tool_input)\n", + " elif tool == \"Weather\":\n", + " result = weather.invoke(tool_input)\n", + " elif tool == \"LLM\":\n", + " result = llm.invoke(tool_input)\n", + " else:\n", + " raise ValueError\n", + " _results[step_name] = str(result)\n", + " return {\"results\": _results}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "8、定义求解节点" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "solve_prompt = \"\"\"Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \\\n", + "retrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \\\n", + "contain irrelevant information.\n", + "\n", + "{plan}\n", + "\n", + "Now solve the question or task according to provided Evidence above. Respond with the answer\n", + "directly with no extra words.\n", + "\n", + "Task: {task}\n", + "Response:\"\"\"\n", + "\n", + "\n", + "def solve(state: ReWOO):\n", + " plan = \"\"\n", + " for _plan, step_name, tool, tool_input in state[\"steps\"]:\n", + " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", + " for k, v in _results.items():\n", + " tool_input = tool_input.replace(k, v)\n", + " step_name = step_name.replace(k, v)\n", + " plan += f\"Plan: {_plan}\\n{step_name} = {tool}[{tool_input}]\"\n", + " prompt = solve_prompt.format(plan=plan, task=state[\"task\"])\n", + " result = llm.invoke(prompt)\n", + " return {\"result\": result.content, \"results\" : None}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "9、定义图形" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "def _route(state):\n", + " _step = _get_current_task(state)\n", + " if _step is None:\n", + " # We have executed all tasks\n", + " return \"solve\"\n", + " else:\n", + " # We are still executing tasks, loop back to the \"tool\" node\n", + " return \"tool\"\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "graph = StateGraph(ReWOO)\n", + "graph.add_node(\"plan\", get_plan)\n", + "graph.add_node(\"tool\", tool_execution)\n", + "graph.add_node(\"solve\", solve)\n", + "graph.add_edge(\"plan\", \"tool\")\n", + "graph.add_edge(\"solve\", END)\n", + "graph.add_conditional_edges(\"tool\", _route)\n", + "graph.add_edge(START, \"plan\")\n", + "memory = MemorySaver()\n", + "app = graph.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "10、执行" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [],\n", + " 'task': '哈哈',\n", + " 'plan_string': \"Task: A company is planning to launch a new product and wants to analyze the sentiment of potential customers on social media platforms to gauge possible reception. They need to collect, process, and analyze relevant data to make informed decisions.\\n\\nPlan: Identify the social media platforms that are most relevant for the target audience of the new product. #E1 = LLM[List the most popular social media platforms for demographic X]\\n\\nPlan: Design a data collection strategy to scrape or extract posts, comments, and reactions related to keywords associated with the new product. #E2 = LLM[Design a data collection strategy for extracting information from platforms identified in #E1 using keywords 'new product']\\n\\nPlan: Use a sentiment analysis tool to process the collected data to determine the overall sentiment (positive, negative, neutral) expressed by the users. #E3 = LLM[Recommend a sentiment analysis tool and explain how to apply it to the data collected in #E2]\\n\\nPlan: Analyze the sentiment data to identify key themes or issues that are frequently mentioned in relation to the new product. #E4 = LLM[How to analyze sentiment data to find common themes or issues]\\n\\nPlan: Summarize the findings and prepare a detailed report that includes data visualizations to present to the company's decision-makers. #E5 = LLM[Guide on how to summarize sentiment analysis findings and prepare a report with data visualizations]\",\n", + " 'steps': [('Identify the social media platforms that are most relevant for the target audience of the new product. ',\n", + " '#E1',\n", + " 'LLM',\n", + " 'List the most popular social media platforms for demographic X'),\n", + " ('Design a data collection strategy to scrape or extract posts, comments, and reactions related to keywords associated with the new product. ',\n", + " '#E2',\n", + " 'LLM',\n", + " \"Design a data collection strategy for extracting information from platforms identified in #E1 using keywords 'new product'\"),\n", + " ('Use a sentiment analysis tool to process the collected data to determine the overall sentiment (positive, negative, neutral) expressed by the users. ',\n", + " '#E3',\n", + " 'LLM',\n", + " 'Recommend a sentiment analysis tool and explain how to apply it to the data collected in #E2'),\n", + " ('Analyze the sentiment data to identify key themes or issues that are frequently mentioned in relation to the new product. ',\n", + " '#E4',\n", + " 'LLM',\n", + " 'How to analyze sentiment data to find common themes or issues'),\n", + " (\"Summarize the findings and prepare a detailed report that includes data visualizations to present to the company's decision-makers. \",\n", + " '#E5',\n", + " 'LLM',\n", + " 'Guide on how to summarize sentiment analysis findings and prepare a report with data visualizations')],\n", + " 'results': None,\n", + " 'result': \"Sorry, I can't assist with that.\"}" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "app.invoke({\"task\": \"哈哈\"}, config=config)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [],\n", + " 'task': '今天天气怎么样',\n", + " 'plan_string': 'Task: Calculate the volume of a cylinder given its height is 10 cm and its radius is 5 cm.\\n\\nPlan: Identify the formula for calculating the volume of a cylinder.\\n#E1 = LLM[What is the formula for calculating the volume of a cylinder?]\\n\\nPlan: Apply the given values (height = 10 cm and radius = 5 cm) to the formula to calculate the volume.\\n#E2 = LLM[Using the formula V = πr^2h, where r is the radius and h is the height, calculate the volume of the cylinder with r = 5 cm and h = 10 cm.]\\n\\nPlan: Convert the calculated volume into the appropriate unit and confirm the final answer.\\n#E3 = LLM[Convert the volume calculated in #E2 into cubic centimeters if not already and confirm the final volume of the cylinder.]',\n", + " 'steps': [['Identify the formula for calculating the volume of a cylinder.',\n", + " '#E1',\n", + " 'LLM',\n", + " 'What is the formula for calculating the volume of a cylinder?'],\n", + " ['Apply the given values (height = 10 cm and radius = 5 cm) to the formula to calculate the volume.',\n", + " '#E2',\n", + " 'LLM',\n", + " 'Using the formula V = πr^2h, where r is the radius and h is the height, calculate the volume of the cylinder with r = 5 cm and h = 10 cm.'],\n", + " ['Convert the calculated volume into the appropriate unit and confirm the final answer.',\n", + " '#E3',\n", + " 'LLM',\n", + " 'Convert the volume calculated in #E2 into cubic centimeters if not already and confirm the final volume of the cylinder.']],\n", + " 'results': {'#E1': \"content='The formula for calculating the volume of a cylinder is:\\\\n\\\\n\\\\\\\\[ V = \\\\\\\\pi r^2 h \\\\\\\\]\\\\n\\\\nwhere \\\\\\\\( V \\\\\\\\) is the volume, \\\\\\\\( r \\\\\\\\) is the radius of the circular base, \\\\\\\\( h \\\\\\\\) is the height of the cylinder, and \\\\\\\\( \\\\\\\\pi \\\\\\\\) (pi) is approximately 3.14159.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 71, 'prompt_tokens': 19, 'total_tokens': 90, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': None, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': None}}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_04751d0b65', 'finish_reason': 'stop', 'logprobs': None} id='run-b41f8f2f-b030-42b2-9a25-4fedc469ab06-0' usage_metadata={'input_tokens': 19, 'output_tokens': 71, 'total_tokens': 90, 'input_token_details': {}, 'output_token_details': {}}\",\n", + " '#E2': \"content='To calculate the volume of the cylinder using the formula \\\\\\\\( V = \\\\\\\\pi r^2 h \\\\\\\\), where \\\\\\\\( r \\\\\\\\) is the radius and \\\\\\\\( h \\\\\\\\) is the height, we can substitute the given values into the formula:\\\\n\\\\nGiven:\\\\n- \\\\\\\\( r = 5 \\\\\\\\) cm\\\\n- \\\\\\\\( h = 10 \\\\\\\\) cm\\\\n\\\\nSubstitute these values into the formula:\\\\n\\\\n\\\\\\\\[\\\\nV = \\\\\\\\pi \\\\\\\\times (5 \\\\\\\\text{ cm})^2 \\\\\\\\times 10 \\\\\\\\text{ cm}\\\\n\\\\\\\\]\\\\n\\\\nCalculate \\\\\\\\( (5 \\\\\\\\text{ cm})^2 \\\\\\\\):\\\\n\\\\n\\\\\\\\[\\\\n(5 \\\\\\\\text{ cm})^2 = 25 \\\\\\\\text{ cm}^2\\\\n\\\\\\\\]\\\\n\\\\nNow substitute back into the formula:\\\\n\\\\n\\\\\\\\[\\\\nV = \\\\\\\\pi \\\\\\\\times 25 \\\\\\\\text{ cm}^2 \\\\\\\\times 10 \\\\\\\\text{ cm}\\\\n\\\\\\\\]\\\\n\\\\n\\\\\\\\[\\\\nV = \\\\\\\\pi \\\\\\\\times 250 \\\\\\\\text{ cm}^3\\\\n\\\\\\\\]\\\\n\\\\nUsing the approximation \\\\\\\\( \\\\\\\\pi \\\\\\\\approx 3.14159 \\\\\\\\):\\\\n\\\\n\\\\\\\\[\\\\nV \\\\\\\\approx 3.14159 \\\\\\\\times 250 \\\\\\\\text{ cm}^3\\\\n\\\\\\\\]\\\\n\\\\n\\\\\\\\[\\\\nV \\\\\\\\approx 785.3975 \\\\\\\\text{ cm}^3\\\\n\\\\\\\\]\\\\n\\\\nTherefore, the volume of the cylinder is approximately \\\\\\\\( 785.40 \\\\\\\\text{ cm}^3 \\\\\\\\).' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 280, 'prompt_tokens': 48, 'total_tokens': 328, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': None, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': None}}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_04751d0b65', 'finish_reason': 'stop', 'logprobs': None} id='run-6885084e-c251-4368-956a-29321f9c1169-0' usage_metadata={'input_tokens': 48, 'output_tokens': 280, 'total_tokens': 328, 'input_token_details': {}, 'output_token_details': {}}\",\n", + " '#E3': \"content='The volume of the cylinder has already been calculated in cubic centimeters. The volume was found using the formula:\\\\n\\\\n\\\\\\\\[ V = \\\\\\\\pi r^2 h \\\\\\\\]\\\\n\\\\nWith the given values:\\\\n- \\\\\\\\( r = 5 \\\\\\\\) cm\\\\n- \\\\\\\\( h = 10 \\\\\\\\) cm\\\\n\\\\nThe calculation was done as follows:\\\\n\\\\n1. Calculate \\\\\\\\( (5 \\\\\\\\text{ cm})^2 \\\\\\\\):\\\\n \\\\\\\\[ (5 \\\\\\\\text{ cm})^2 = 25 \\\\\\\\text{ cm}^2 \\\\\\\\]\\\\n\\\\n2. Substitute into the formula:\\\\n \\\\\\\\[ V = \\\\\\\\pi \\\\\\\\times 25 \\\\\\\\text{ cm}^2 \\\\\\\\times 10 \\\\\\\\text{ cm} \\\\\\\\]\\\\n\\\\n3. Simplify further:\\\\n \\\\\\\\[ V = \\\\\\\\pi \\\\\\\\times 250 \\\\\\\\text{ cm}^3 \\\\\\\\]\\\\n\\\\n4. Use the approximation \\\\\\\\( \\\\\\\\pi \\\\\\\\approx 3.14159 \\\\\\\\):\\\\n \\\\\\\\[ V \\\\\\\\approx 3.14159 \\\\\\\\times 250 \\\\\\\\text{ cm}^3 \\\\\\\\]\\\\n\\\\n5. Calculate the approximate volume:\\\\n \\\\\\\\[ V \\\\\\\\approx 785.3975 \\\\\\\\text{ cm}^3 \\\\\\\\]\\\\n\\\\nRounding to two decimal places, the volume of the cylinder is approximately \\\\\\\\( 785.40 \\\\\\\\text{ cm}^3 \\\\\\\\).\\\\n\\\\nTherefore, the final volume of the cylinder is indeed \\\\\\\\( 785.40 \\\\\\\\text{ cm}^3 \\\\\\\\), and it is already expressed in cubic centimeters.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 294, 'prompt_tokens': 539, 'total_tokens': 833, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': None, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': None}}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_04751d0b65', 'finish_reason': 'stop', 'logprobs': None} id='run-73de4bfa-4f7b-40e5-9678-cbfdb1d6a0fb-0' usage_metadata={'input_tokens': 539, 'output_tokens': 294, 'total_tokens': 833, 'input_token_details': {}, 'output_token_details': {}}\"},\n", + " 'result': '抱歉,我无法提供当前的天气信息。建议您查阅当地的天气预报或使用天气应用来获取最新信息。'}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "app.get_state(config).values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "11、查看agent graph。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(app.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fay312-2", + "language": "python", + "name": "python3" + }, + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}