diff --git a/examples/react_agent/weather_assistant.md b/examples/react_agent/weather_assistant.md index 9a7e30bf544d93af88caf4121e8ee0175ac24a17..d0d5cbc363b466174faefb6fbac3f7c7cd1772c3 100644 --- a/examples/react_agent/weather_assistant.md +++ b/examples/react_agent/weather_assistant.md @@ -166,6 +166,11 @@ def create_react_agent(agent_config: ReActAgentConfig, result = await react_agent.invoke({"query": "查询杭州的天气"}) ``` +执行天气查询助手成功后,会得到如下的结果: +```text +{'output': '\n\n当前杭州的天气情况如下:\n- 天气现象:小雨\n- 实时温度:30.78℃\n- 体感温度:37.78℃\n- 空气湿度:74%\n- 风速:0.77米/秒(约2.8公里/小时)\n\n建议外出时携带雨具,注意防雨防滑。需要其他天气信息可以随时告诉我哦~'} +``` + ReActAgent的invoke方法实现了ReAct规划流程,代码如下: ```python class ReActAgent(Agent): @@ -195,10 +200,5 @@ class ReActAgent(Agent): return dict(output=self._state.final_result) ``` -执行天气查询助手成功后,会得到如下的结果: -```text -{'output': '\n\n当前杭州的天气情况如下:\n- 天气现象:小雨\n- 实时温度:30.78℃\n- 体感温度:37.78℃\n- 空气湿度:74%\n- 风速:0.77米/秒(约2.8公里/小时)\n\n建议外出时携带雨具,注意防雨防滑。需要其他天气信息可以随时告诉我哦~'} -``` - 恭喜你,成功搭建了第一个ReAct Agent! --- diff --git a/jiuwen/core/component/questioner_comp.py b/jiuwen/core/component/questioner_comp.py index 7e8cbdfa6bb1faee56682677ed5a4ebfa5fa7c8c..661d15f58635ce420bd7d8a0a3d8374fb3785029 100644 --- a/jiuwen/core/component/questioner_comp.py +++ b/jiuwen/core/component/questioner_comp.py @@ -399,6 +399,12 @@ class QuestionerExecutable(Executable): return self async def invoke(self, inputs: Input, context: Context) -> Output: + tracer = context.tracer + if tracer: + tracer.trigger("tracer_workflow", "on_invoke", invoke_id=context.executable_id, + parent_node_id=context.parent_id, + on_invoke_data={"on_invoke_data": "extra trace data"}) + state_from_context = self._load_state_from_context(context) if state_from_context.is_undergoing_interaction(): self._state = state_from_context diff --git a/tests/system_tests/agent/test_react_agent.py b/tests/system_tests/agent/test_react_agent.py index 61e89c3e198bf8166c8192aec537b2bdb37176db..9a8a1ecc2d4be369f45fda90a5197aa7dee0ecba 100644 --- a/tests/system_tests/agent/test_react_agent.py +++ b/tests/system_tests/agent/test_react_agent.py @@ -1,86 +1,25 @@ import os import unittest -from unittest.mock import patch, AsyncMock -from jiuwen.agent.common.schema import WorkflowSchema, PluginSchema +from jiuwen.agent.common.schema import PluginSchema from jiuwen.agent.react_agent import create_react_agent_config, create_react_agent, ReActAgent from jiuwen.core.component.common.configs.model_config import ModelConfig -from jiuwen.core.component.llm_comp import LLMCompConfig, LLMComponent from jiuwen.core.utils.llm.base import BaseModelInfo from jiuwen.core.utils.tool.service_api.param import Param from jiuwen.core.utils.tool.service_api.restful_api import RestfulApi -from jiuwen.core.workflow.base import Workflow -from jiuwen.core.workflow.workflow_config import WorkflowConfig, WorkflowMetadata -from jiuwen.graph.pregel.graph import PregelGraph -from tests.unit_tests.workflow.test_mock_node import MockStartNode, MockEndNode + API_BASE = os.getenv("API_BASE", "") API_KEY = os.getenv("API_KEY", "") MODEL_NAME = os.getenv("MODEL_NAME", "") MODEL_PROVIDER = os.getenv("MODEL_PROVIDER", "") -USER_PROMPT_FOR_TRIP_PLANNING = "帮我生成一份{{location}}的旅行攻略,同时旅行时间为{{duration}}。注意:若未明确指定旅行时间,则默认为一天!" - class ReActAgentTest(unittest.IsolatedAsyncioTestCase): # ① 关键改动 DEFAULT_TEMPLATE = [ dict(role="system", content="你是一个AI助手,在适当的时候调用合适的工具,帮助我完成任务!") ] - @patch("jiuwen.core.utils.tool.service_api.restful_api.RestfulApi.invoke") - async def test_react_agent_invoke(self, mock_restfulapi_invoke): - mock_restfulapi_invoke.return_value = {"result": "杭州今天天气晴,温度35度;注意局部地区有雷阵雨"} - - # 下面所有代码无需再改动,已经是协程环境 - tools_schema = [self._create_tool_schema()] - model_config = self._create_model() - prompt_template = self.DEFAULT_TEMPLATE - - react_agent_config = create_react_agent_config( - agent_id="react_agent_123", - agent_version="0.0.1", - description="AI助手", - plugins=tools_schema, - workflows=[], - model=model_config, - prompt_template=prompt_template - ) - - tool = self._create_tool() - react_agent = create_react_agent( - agent_config=react_agent_config, - workflows=[], - tools=[tool] - ) - inputs = {"query": "查询杭州的天气"} - - result = await react_agent.invoke(inputs) # ③ 已经是 await - print(f"ReActAgent 最终输出结果:{result}") - - async def test_react_agent_invoke_with_real_plugin(self): - tools_schema = [self._create_tool_schema()] - model_config = self._create_model() - prompt_template = self.DEFAULT_TEMPLATE - - react_agent_config = create_react_agent_config( - agent_id="react_agent_123", - agent_version="0.0.1", - description="AI助手", - plugins=tools_schema, - workflows=[], - model=model_config, - prompt_template=prompt_template - ) - - react_agent: ReActAgent = create_react_agent( - agent_config=react_agent_config, - workflows=[], - tools=[self._create_tool()] - ) - - result = await react_agent.invoke({"query": "查询杭州的天气"}) - print(f"ReActAgent 最终输出结果:{result}") - @staticmethod def _create_model(): return ModelConfig(model_provider=MODEL_PROVIDER, @@ -93,75 +32,6 @@ class ReActAgentTest(unittest.IsolatedAsyncioTestCase): # ① 关键改动 timeout=30 # 添加超时设置 )) - @staticmethod - def _create_workflow_schema(): - workflow_inputs = { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "旅行的目的地", - "required": True - }, - "duration": { - "type": "string", - "description": "旅行时长", - "required": False - } - } - } - return WorkflowSchema(id="workflow_123", name="TripPlanning", description="旅行攻略查询工作流", - version="1.0.0", inputs=workflow_inputs) - - @staticmethod - def _create_workflow(): - workflow = Workflow( - workflow_config=WorkflowConfig( - metadata=WorkflowMetadata(id="workflow_123", name="TripPlanning", version="1.0.0")), - graph=PregelGraph()) - start_component = MockStartNode("start") - llm_component = ReActAgentTest._create_llm_component() - end_component = MockEndNode("end") - workflow.set_start_comp("start", start_component, - inputs_schema={ - "location": "${userFields.location}", - "duration": "${userFields.duration}" - }) - - workflow.add_workflow_comp("llm", llm_component, - inputs_schema={ - "userFields": { - "location": "${start.location}", - "duration": "{start.duration}" - } - }) - workflow.set_end_comp("end", end_component, - inputs_schema={ - "output": "${llm.userFields}" - }) - workflow.add_connection("start", "llm") - workflow.add_connection("llm", "end") - return workflow - - @staticmethod - def _create_llm_component(): - model_config = ModelConfig(model_provider=MODEL_PROVIDER, - model_info=BaseModelInfo( - model=MODEL_NAME, - api_base=API_BASE, - api_key=API_KEY, - temperature=0.7, - top_p=0.9, - timeout=30 # 添加超时设置 - )) - config = LLMCompConfig( - model=model_config, - template_content=[{"role": "user", "content": USER_PROMPT_FOR_TRIP_PLANNING}], - response_format={"type": "text"}, - output_config={"result": {"type": "string", "required": True}}, - ) - return LLMComponent(config) - @staticmethod def _create_tool(): weather_plugin = RestfulApi( @@ -194,3 +64,27 @@ class ReActAgentTest(unittest.IsolatedAsyncioTestCase): # ① 关键改动 } ) return tool_info + + async def test_react_agent_invoke_with_real_plugin(self): + tools_schema = [self._create_tool_schema()] + model_config = self._create_model() + prompt_template = self.DEFAULT_TEMPLATE + + react_agent_config = create_react_agent_config( + agent_id="react_agent_123", + agent_version="0.0.1", + description="AI助手", + plugins=tools_schema, + workflows=[], + model=model_config, + prompt_template=prompt_template + ) + + react_agent: ReActAgent = create_react_agent( + agent_config=react_agent_config, + workflows=[], + tools=[self._create_tool()] + ) + + result = await react_agent.invoke({"query": "查询杭州的天气"}) + print(f"ReActAgent 最终输出结果:{result}") diff --git a/tests/unit_tests/workflow/test_questioner_comp.py b/tests/unit_tests/workflow/test_questioner_comp.py index 45dc3dac8e3c95db962f025d24e9ca14036daebf..f192d42c47176d5954d6b21e28d4e7f09dca9921 100644 --- a/tests/unit_tests/workflow/test_questioner_comp.py +++ b/tests/unit_tests/workflow/test_questioner_comp.py @@ -8,6 +8,10 @@ from jiuwen.core.component.questioner_comp import QuestionerInteractState, Field from jiuwen.core.context.config import Config from jiuwen.core.context.context import Context from jiuwen.core.context.memory.base import InMemoryState +from jiuwen.core.stream.emitter import StreamEmitter +from jiuwen.core.stream.manager import StreamWriterManager +from jiuwen.core.stream.writer import TraceSchema +from jiuwen.core.tracer.tracer import Tracer from jiuwen.core.utils.prompt.template.template import Template from jiuwen.core.workflow.base import Workflow from tests.unit_tests.workflow.test_mock_node import MockStartNode, MockEndNode @@ -18,6 +22,10 @@ class MockLLMModel: pass class QuestionerTest(unittest.TestCase): + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + def invoke_workflow(self, inputs: dict, context: Context, flow: Workflow): loop = asyncio.get_event_loop() feature = asyncio.ensure_future(flow.invoke(inputs=inputs, context=context)) @@ -28,8 +36,8 @@ class QuestionerTest(unittest.TestCase): @patch("jiuwen.core.component.questioner_comp.QuestionerDirectReplyHandler._build_llm_inputs") @patch("jiuwen.core.component.questioner_comp.QuestionerExecutable._init_prompt") @patch("jiuwen.core.utils.llm.model_utils.model_factory.ModelFactory.get_model") - def test_questioner_component_in_workflow_initial_ask(self, mock_get_model, mock_init_prompt, mock_llm_inputs, - mock_extraction): + def test_invoke_questioner_component_in_workflow_initial_ask(self, mock_get_model, mock_init_prompt, mock_llm_inputs, + mock_extraction): mock_get_model.return_value = MockLLMModel() mock_prompt_template = [ dict(role="system", content="系统提示词"), @@ -39,7 +47,7 @@ class QuestionerTest(unittest.TestCase): mock_llm_inputs.return_value = mock_prompt_template mock_extraction.return_value = dict(location="hangzhou") - context = Context(config=Config(), state=InMemoryState(), store=None, tracer=None) + context = Context(config=Config(), state=InMemoryState(), store=None) flow = create_flow() key_fields = [ @@ -60,14 +68,15 @@ class QuestionerTest(unittest.TestCase): ) questioner_component = QuestionerComponent(questioner_comp_config=questioner_config) - flow.set_start_comp("s", start_component) - flow.set_end_comp("e", end_component) - flow.add_workflow_comp("questioner", questioner_component) + flow.set_start_comp("s", start_component, inputs_schema={"query": "${query}"}) + flow.set_end_comp("e", end_component, inputs_schema={"output": "${questioner.userFields.key_fields}"}) + flow.add_workflow_comp("questioner", questioner_component, inputs_schema={"query": "${start.query}"}) flow.add_connection("s", "questioner") flow.add_connection("questioner", "e") - result = self.invoke_workflow({}, context, flow) + result = self.invoke_workflow({"query": "查询杭州的天气"}, context, flow) + assert result == {'output': {'location': 'hangzhou', 'time': 'today'}} @patch("jiuwen.core.component.questioner_comp.QuestionerExecutable._load_state_from_context") @@ -75,8 +84,8 @@ class QuestionerTest(unittest.TestCase): @patch("jiuwen.core.component.questioner_comp.QuestionerDirectReplyHandler._build_llm_inputs") @patch("jiuwen.core.component.questioner_comp.QuestionerExecutable._init_prompt") @patch("jiuwen.core.utils.llm.model_utils.model_factory.ModelFactory.get_model") - def test_questioner_component_in_workflow_repeat_ask(self, mock_get_model, mock_init_prompt, mock_llm_inputs, - mock_extraction, mock_state_from_context): + def test_invoke_questioner_component_in_workflow_repeat_ask(self, mock_get_model, mock_init_prompt, mock_llm_inputs, + mock_extraction, mock_state_from_context): mock_get_model.return_value = MockLLMModel() mock_prompt_template = [ dict(role="system", content="系统提示词"), @@ -88,7 +97,7 @@ class QuestionerTest(unittest.TestCase): state = QuestionerInteractState(extracted_key_fields=dict(location="hangzhou")) mock_state_from_context.return_value = state - context = Context(config=Config(), state=InMemoryState(), store=None, tracer=None) + context = Context(config=Config(), state=InMemoryState(), store=None) flow = create_flow() key_fields = [ @@ -116,4 +125,71 @@ class QuestionerTest(unittest.TestCase): flow.add_connection("s", "questioner") flow.add_connection("questioner", "e") - result = self.invoke_workflow({}, context, flow) + result = self.invoke_workflow({"query": "查询杭州的天气"}, context, flow) + assert result == {'output': {'location': 'hangzhou', 'time': 'today'}} + + @patch("jiuwen.core.component.questioner_comp.QuestionerDirectReplyHandler._invoke_llm_for_extraction") + @patch("jiuwen.core.component.questioner_comp.QuestionerDirectReplyHandler._build_llm_inputs") + @patch("jiuwen.core.component.questioner_comp.QuestionerExecutable._init_prompt") + @patch("jiuwen.core.utils.llm.model_utils.model_factory.ModelFactory.get_model") + def test_stream_questioner_component_in_workflow_initial_ask_with_tracer(self, mock_get_model, mock_init_prompt, + mock_llm_inputs, mock_extraction): + ''' + tracer使用问题记录: + 1. 必须调用workflow、agent的 stream方法,才能获取到tracer的数据帧 + 2. workflow组件的输入输出,tracer都已经记录了,组件只需要关注额外的数据 + 3. agent用agent_tracer,workflow用workflow_tracer + 4. event有定义,参考handler.py的@trigger_event + 5. on_invoke_data是固定的结构,结构为 dict(on_invoke_data={"on_invoke_data": "extra trace data"}) + ''' + + mock_get_model.return_value = MockLLMModel() + mock_prompt_template = [ + dict(role="system", content="系统提示词"), + dict(role="user", content="你是一个AI助手") + ] + mock_init_prompt.return_value = Template(name="test", content=mock_prompt_template) + mock_llm_inputs.return_value = mock_prompt_template + mock_extraction.return_value = dict(location="hangzhou") + + context = Context(config=Config(), state=InMemoryState(), store=None) + context.set_stream_writer_manager(StreamWriterManager(StreamEmitter())) + tracer = Tracer() + tracer.init(context.stream_writer_manager, context.callback_manager) + context.set_tracer(tracer) + flow = create_flow() + + key_fields = [ + FieldInfo(field_name="location", description="地点", required=True), + FieldInfo(field_name="time", description="时间", required=True, default_value="today") + ] + + start_component = MockStartNode("s") + end_component = MockEndNode("e") + + model_config = ModelConfig(model_provider="openai") + questioner_config = QuestionerConfig( + model=model_config, + question_content="", + extract_fields_from_response=True, + field_names=key_fields, + with_chat_history=False + ) + questioner_component = QuestionerComponent(questioner_comp_config=questioner_config) + + flow.set_start_comp("s", start_component, inputs_schema={"query": "${query}"}) + flow.set_end_comp("e", end_component, inputs_schema={"output": "${questioner.userFields.key_fields}"}) + flow.add_workflow_comp("questioner", questioner_component, inputs_schema={"query": "${start.query}"}) + + flow.add_connection("s", "questioner") + flow.add_connection("questioner", "e") + + async def _async_stream_workflow_for_tracer(_flow, _inputs, _context, _tracer_chunks): + async for chunk in flow.stream(_inputs, _context): + if isinstance(chunk, TraceSchema): + _tracer_chunks.append(chunk) + + tracer_chunks = [] + self.loop.run_until_complete(_async_stream_workflow_for_tracer(flow, {"query": "查询杭州的天气"}, context, + tracer_chunks)) + print(tracer_chunks)