diff --git a/alias/README.md b/alias/README.md index ea58955..d4ec513 100644 --- a/alias/README.md +++ b/alias/README.md @@ -288,7 +288,7 @@ The backend server will: - Create the initial superuser account (if not exists) - Start on `http://localhost:8000` (or the port specified in `.env`) -Verify the server is running by visiting `http://localhost:8000/api/v1/monitor/health`. +Verify the server is running by visiting `http://localhost:8000/api/v1/health`. #### Start the Frontend @@ -361,7 +361,7 @@ Once both servers are running: - **Frontend UI**: Open `http://localhost:5173` in your browser - **Backend API**: Available at `http://localhost:8000` - **API Documentation**: Available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/api/v1/openapi.json` (OpenAPI JSON) -- **Health Check**: `http://localhost:8000/api/v1/monitor/health` +- **Health Check**: `http://localhost:8000/api/v1/health` #### Default Login Credentials diff --git a/alias/README_ZH.md b/alias/README_ZH.md index 58d1aa6..a3ff850 100644 --- a/alias/README_ZH.md +++ b/alias/README_ZH.md @@ -289,7 +289,7 @@ python -m uvicorn alias.server.main:app --host 0.0.0.0 --port 8000 --reload - 创建初始超级用户账户(如果不存在) - 在 `http://localhost:8000` 启动(或 `.env` 中指定的端口) -通过访问 `http://localhost:8000/api/v1/monitor/health` 来验证服务器是否正在运行。 +通过访问 `http://localhost:8000/api/v1/health` 来验证服务器是否正在运行。 #### 启动前端 @@ -362,7 +362,7 @@ bash script/start_memory_service.sh - **前端 UI**:在浏览器中打开 `http://localhost:5173` - **后端 API**:可在 `http://localhost:8000` 访问 - **API 文档**:可在 `http://localhost:8000/docs` (Swagger UI) 或 `http://localhost:8000/api/v1/openapi.json` (OpenAPI JSON) 访问 -- **健康检查**:`http://localhost:8000/api/v1/monitor/health` +- **健康检查**:`http://localhost:8000/api/v1/health` #### 默认登录凭据 diff --git a/alias/pyproject.toml b/alias/pyproject.toml index 75f29f7..e9a1a28 100644 --- a/alias/pyproject.toml +++ b/alias/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "elasticsearch>=9.0.0", "jinja2>=3.1.6", "bcrypt==4.0.1", - "agentscope[full]==1.0.7", + "agentscope[full]==1.0.11", "tenacity>=8.5.0", "apscheduler>=3.11.0", "chardet>=5.2.0", @@ -42,7 +42,7 @@ dependencies = [ "alembic>=1.16.1", "openpyxl>=3.1.5", "sentry-sdk[fastapi]===2.30.0", - "agentscope-runtime==0.2.0", + "agentscope-runtime>=1.0.0", "aiosqlite>=0.21.0", "asyncpg>=0.30.0", "itsdangerous>=2.2.0" diff --git a/alias/src/alias/agent/agents/_alias_agent_base.py b/alias/src/alias/agent/agents/_alias_agent_base.py index bfb32fd..25ca1df 100644 --- a/alias/src/alias/agent/agents/_alias_agent_base.py +++ b/alias/src/alias/agent/agents/_alias_agent_base.py @@ -11,7 +11,7 @@ from agentscope.agent import ReActAgent from agentscope.model import ChatModelBase from agentscope.formatter import FormatterBase from agentscope.memory import MemoryBase, LongTermMemoryBase -from agentscope.message import Msg, TextBlock, ToolUseBlock, ToolResultBlock +from agentscope.message import Msg, ToolUseBlock, ToolResultBlock from alias.agent.tools import AliasToolkit from alias.agent.utils.constants import DEFAULT_PLANNER_NAME @@ -23,24 +23,6 @@ from alias.agent.utils.constants import DEFAULT_BROWSER_WORKER_NAME from alias.agent.utils.constants import MODEL_MAX_RETRIES -def alias_agent_post_reply_hook( - self: "AliasAgentBase", - kwargs: dict[str, Any], # pylint: disable=unused-argument - output: Any, -): - """ - This is a monkey patch to ensure that when the agent is interrupted in - a tool call, the control returns to user - """ - if ( - self.tool_call_interrupt_return - and isinstance(output, Msg) - and output.metadata - and output.metadata.get("is_interrupted", False) - ): - raise asyncio.CancelledError() - - class AliasAgentBase(ReActAgent): def __init__( self, @@ -53,7 +35,6 @@ class AliasAgentBase(ReActAgent): state_saving_dir: Optional[str] = None, sys_prompt: Optional[str] = None, max_iters: int = 10, - tool_call_interrupt_return: bool = True, long_term_memory: Optional[LongTermMemoryBase] = None, long_term_memory_mode: Literal[ "agent_control", @@ -77,14 +58,7 @@ class AliasAgentBase(ReActAgent): self.message_sending_mapping = {} self.state_saving_dir = state_saving_dir self.agent_stop_function_names = [self.finish_function_name] - self.tool_call_interrupt_return = tool_call_interrupt_return - # interrupted if the - self.register_instance_hook( - "post_reply", - "alias_agent_post_reply_hook", - alias_agent_post_reply_hook, - ) # for message output to backend self.register_instance_hook( "post_print", @@ -92,7 +66,16 @@ class AliasAgentBase(ReActAgent): alias_post_print_hook, ) - async def _reasoning(self): + # register finish_function_name + if self.finish_function_name not in self.toolkit.tools: + self.toolkit.register_tool_function( + getattr(self, self.finish_function_name), + ) + + async def _reasoning( + self, + tool_choice: Literal["auto", "none", "required"] | None = None, + ): """Override _reasoning to add retry logic.""" # Call the parent class's _reasoning method directly to @@ -109,7 +92,8 @@ class AliasAgentBase(ReActAgent): if hasattr(original_method, "__wrapped__"): # This is the wrapped version, get the original original_method = original_method.__wrapped__ - return await original_method(self) + + return await original_method(self, tool_choice=tool_choice) for i in range(MODEL_MAX_RETRIES - 1): try: @@ -132,21 +116,18 @@ class AliasAgentBase(ReActAgent): # final attempt await call_parent_reasoning() - async def _acting(self, tool_call: ToolUseBlock) -> Msg | None: - """Perform the acting process. - - TODO: (part 2) - this is just a monkey patch for AS when not support interruption - during tool call; can be remove when AS framework updated + async def _acting(self, tool_call: ToolUseBlock) -> dict | None: + """Perform the acting process, and return the structured output if + it's generated and verified in the finish function call. Args: tool_call (`ToolUseBlock`): The tool use block to be executed. Returns: - `Union[Msg, None]`: - Return a message to the user if the `_finish_function` is - called, otherwise return `None`. + `Union[dict, None]`: + Return the structured output if it's verified in the finish + function call. """ tool_res_msg = Msg( @@ -165,7 +146,6 @@ class AliasAgentBase(ReActAgent): # Execute the tool call tool_res = await self.toolkit.call_tool_function(tool_call) - response_msg = None # Async generator handling async for chunk in tool_res: # Turn into a tool result block @@ -191,28 +171,26 @@ class AliasAgentBase(ReActAgent): tool_call["name"] != self.finish_function_name or ( tool_call["name"] == self.finish_function_name + and chunk.metadata and not chunk.metadata.get("success") ) ): await self.print(tool_res_msg, chunk.is_last) # Return message if generate_response is called successfully - if tool_call[ - "name" - ] in self.agent_stop_function_names and chunk.metadata.get( - "success", - True, + if ( + tool_call["name"] in self.agent_stop_function_names + and chunk.metadata + and chunk.metadata.get( + "success", + True, + ) ): - response_msg = chunk.metadata.get("response_msg") + return chunk.metadata.get("structured_output") elif chunk.is_interrupted: - # TODO: monkey patch happens here - response_msg = tool_res_msg - if response_msg.metadata is None: - response_msg.metadata = {"is_interrupted": True} - else: - response_msg.metadata["is_interrupted"] = True + raise asyncio.CancelledError - return response_msg + return None finally: # Record the tool result message in the memory await self.memory.add(tool_res_msg) @@ -228,16 +206,16 @@ class AliasAgentBase(ReActAgent): """ response_msg = Msg( self.name, - content=[ - TextBlock( - type="text", - text="I got interrupted by the user. " - "Pivot to handle the user's new request.", - ), - ], - role="assistant", - metadata={}, + "I noticed that you have interrupted me. What can I " + "do for you?", + "assistant", + metadata={ + # Expose this field to indicate the interruption + "_is_interrupted": True, + }, ) + + await self.print(response_msg, True) await self.memory.add(response_msg) # update and save agent states diff --git a/alias/src/alias/agent/agents/_browser_agent.py b/alias/src/alias/agent/agents/_browser_agent.py index 3463f4a..ad79454 100644 --- a/alias/src/alias/agent/agents/_browser_agent.py +++ b/alias/src/alias/agent/agents/_browser_agent.py @@ -10,7 +10,7 @@ import os import json import inspect from functools import wraps -from typing import Type, Optional, Any +from typing import Type, Optional, Any, Literal import asyncio import copy from loguru import logger @@ -104,6 +104,10 @@ with open( _BROWSER_AGENT_SUMMARIZE_TASK_PROMPT = f.read() +class EmptyModel(BaseModel): + pass + + async def browser_pre_reply_hook( self, kwargs: dict[str, Any], @@ -150,6 +154,7 @@ async def browser_post_acting_hook( ] = self._filter_execution_text(return_json["text"]) if tool_call["name"] != self.finish_function_name or ( tool_call["name"] == self.finish_function_name + and tool_res_msg.metadata and not tool_res_msg.metadata.get("success") ): await self.print(tool_res_msg) @@ -343,6 +348,7 @@ class BrowserAgent(AliasAgentBase): or "gpt-5" in self.model.model_name ) + # pylint: disable=R0912,R0915 async def reply( self, msg: Msg | list[Msg] | None = None, @@ -370,20 +376,38 @@ class BrowserAgent(AliasAgentBase): else "" ) + if structured_model is None: + structured_model = EmptyModel + + tool_choice: Literal["auto", "none", "required"] | None = None + self._required_structured_model = structured_model # Record structured output model if provided if structured_model: + # Register generate_response tool only when structured output + # is required + if self.finish_function_name not in self.toolkit.tools: + self.toolkit.register_tool_function( + getattr(self, self.finish_function_name), + ) + self.toolkit.set_extended_model( self.finish_function_name, structured_model, ) + tool_choice = "required" + else: + # Remove generate_response tool if no structured output is required + self.toolkit.remove_tool_function(self.finish_function_name) + # The reasoning-acting loop + structured_output = None reply_msg = None for iter_n in range(self.max_iters): self.iter_n = iter_n + 1 await self._summarize_mem() - msg_reasoning = await self._pure_reasoning() + msg_reasoning = await self._pure_reasoning(tool_choice) tool_calls = msg_reasoning.get_content_blocks("tool_use") if tool_calls and tool_calls[0]["name"] == "browser_snapshot": msg_reasoning = await self._reasoning_with_observation() @@ -397,27 +421,69 @@ class BrowserAgent(AliasAgentBase): # Parallel tool calls or not if self.parallel_tool_calls: - acting_responses = await asyncio.gather(*futures) + structured_outputs = await asyncio.gather(*futures) else: # Sequential tool calls - acting_responses = [await _ for _ in futures] + structured_outputs = [await _ for _ in futures] - # Find the first non-None replying message from the acting - for acting_msg in acting_responses: - reply_msg = reply_msg or acting_msg + # -------------- Check for exit condition -------------- + # If structured output is still not satisfied + if self._required_structured_model: + # Remove None results + structured_outputs = [_ for _ in structured_outputs if _] - if reply_msg: + msg_hint = None + # If the acting step generates structured outputs + if structured_outputs: + # Cache the structured output data + structured_output = structured_outputs[-1] + + reply_msg = Msg( + self.name, + structured_output.get("subtask_progress_summary", ""), + "assistant", + metadata=structured_output, + ) + break + + if not msg_reasoning.has_content_blocks("tool_use"): + # If structured output is required but no tool call is + # made, remind the llm to go on the task + msg_hint = Msg( + "user", + "Structured output is " + f"required, go on to finish your task or call " + f"'{self.finish_function_name}' to generate the " + f"required structured output.", + "user", + ) + await self._reasoning_hint_msgs.add(msg_hint) + # Require tool call in the next reasoning step + tool_choice = "required" + + if msg_hint and self.print_hint_msg: + await self.print(msg_hint) + + elif not msg_reasoning.has_content_blocks("tool_use"): + # Exit the loop when no structured output is required (or + # already satisfied) and only text response is generated + msg_reasoning.metadata = structured_output + reply_msg = msg_reasoning break - # When the maximum iterations are reached - if not reply_msg: - reply_msg = await self._summarizing() - await self.memory.add(reply_msg) + # When the maximum iterations are reached + # and no reply message is generated + if reply_msg is None: + reply_msg = await self._summarizing() + reply_msg.metadata = structured_output + await self.memory.add(reply_msg) + return reply_msg async def _pure_reasoning( self, + tool_choice: Literal["auto", "none", "required"] | None = None, ) -> Msg: msg = Msg( "user", @@ -433,12 +499,18 @@ class BrowserAgent(AliasAgentBase): Msg("system", self.sys_prompt, "system"), *await self.memory.get_memory(), msg, + # The hint messages to guide the agent's behavior, maybe empty + *await self._reasoning_hint_msgs.get_memory(), ], ) + # Clear the hint messages after use + await self._reasoning_hint_msgs.clear() + res = await self.model( prompt, tools=self.no_screenshot_tool_list, + tool_choice=tool_choice, ) # handle output from the model interrupted_by_user = False @@ -1260,15 +1332,6 @@ class BrowserAgent(AliasAgentBase): subtask_progress_summary=summary_text, generated_files={}, ) - - response_msg = Msg( - self.name, - content=[ - TextBlock(type="text", text=summary_text), - ], - role="assistant", - metadata=structure_response.model_dump(), - ) return ToolResponse( content=[ TextBlock( @@ -1278,7 +1341,7 @@ class BrowserAgent(AliasAgentBase): ], metadata={ "success": True, - "response_msg": response_msg, + "structured_output": structure_response.model_dump(), }, is_last=True, ) @@ -1290,7 +1353,7 @@ class BrowserAgent(AliasAgentBase): text=f"Here is a summary of current status:\n{summary_text}\nPlease continue.\n Following steps \n {finish_status}", ), ], - metadata={"success": False, "response_msg": None}, + metadata={"success": False, "structured_output": None}, is_last=True, ) except Exception as e: @@ -1310,8 +1373,8 @@ class BrowserAgent(AliasAgentBase): sys_prompt = ( "You are an expert in task validation. " "Your job is to determine if the agent has completed its task" - " based on the provided summary. If finished, strictly reply " - '"BROWSER_AGENT_TASK_FINISHED", otherwise return the remaining ' + " based on the provided summary. If the summary is `NO_ANSWER`, this task is not over. If finished, strictly reply " + '"BROWSER_AGENT_TASK_FINISHED" and your reason, otherwise return the remaining ' "tasks or next steps." ) # Extract user question from memory diff --git a/alias/src/alias/agent/agents/_data_science_agent.py b/alias/src/alias/agent/agents/_data_science_agent.py index 005e00c..8ac192d 100644 --- a/alias/src/alias/agent/agents/_data_science_agent.py +++ b/alias/src/alias/agent/agents/_data_science_agent.py @@ -4,10 +4,9 @@ import asyncio import json import os from functools import partial -from typing import List, Dict, Optional, Any, Type, cast +from typing import List, Dict, Optional, Any, Type, cast, Literal import uuid -import shortuuid from agentscope.formatter import FormatterBase from agentscope.memory import MemoryBase from agentscope.message import Msg, TextBlock, ToolUseBlock, ToolResultBlock @@ -15,7 +14,7 @@ from agentscope.model import ChatModelBase from agentscope.tool import ToolResponse from agentscope.tracing import trace_reply from loguru import logger -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, ValidationError, Field from tenacity import retry, stop_after_attempt, wait_fixed from alias.agent.agents import AliasAgentBase @@ -37,6 +36,13 @@ from .ds_agent_utils import ( from .ds_agent_utils.ds_config import PROMPT_DS_BASE_PATH +class DefaultStructuredResponse(BaseModel): + response: str = Field( + description="Just a placeholder. " + "Enter any character to trigger report generation", + ) + + class DataScienceAgent(AliasAgentBase): def __init__( self, @@ -194,27 +200,140 @@ class DataScienceAgent(AliasAgentBase): "pre_reply", "files_filter_pre_reply_hook", ) - return await super().reply(msg, structured_model) + + if structured_model is None: + structured_model = DefaultStructuredResponse + + # Record the input message(s) in the memory + await self.memory.add(msg) + + # -------------- Retrieval process -------------- + # Retrieve relevant records from the long-term memory if activated + await self._retrieve_from_long_term_memory(msg) + # Retrieve relevant documents from the knowledge base(s) if any + await self._retrieve_from_knowledge(msg) + + # Control if LLM generates tool calls in each reasoning step + tool_choice: Literal["auto", "none", "required"] | None = None + + # -------------- Structured output management -------------- + self._required_structured_model = structured_model + + # Register generate_response tool only when structured output + # is required + if self.finish_function_name not in self.toolkit.tools: + self.toolkit.register_tool_function( + getattr(self, self.finish_function_name), + ) + + # Set the structured output model + self.toolkit.set_extended_model( + self.finish_function_name, + structured_model, + ) + tool_choice = "required" + + # -------------- The reasoning-acting loop -------------- + # Cache the structured output generated in the finish function call + structured_output = None + reply_msg = None + for _ in range(self.max_iters): + # -------------- The reasoning process -------------- + msg_reasoning = await self._reasoning(tool_choice) + + # -------------- The acting process -------------- + futures = [ + self._acting(tool_call) + for tool_call in msg_reasoning.get_content_blocks( + "tool_use", + ) + ] + # Parallel tool calls or not + if self.parallel_tool_calls: + structured_outputs = await asyncio.gather(*futures) + else: + # Sequential tool calls + structured_outputs = [await _ for _ in futures] + + # -------------- Check for exit condition -------------- + # Remove None results + structured_outputs = [_ for _ in structured_outputs if _] + + msg_hint = None + # If the acting step generates structured outputs + if structured_outputs: + # Cache the structured output data + structured_output = structured_outputs[-1] + + reply_msg = Msg( + self.name, + structured_output.get("response"), + "assistant", + metadata=structured_output, + ) + break + + if not msg_reasoning.has_content_blocks("tool_use"): + # If structured output is required but no tool call is + # made, remind the llm to go on the task + msg_hint = Msg( + "user", + "Structured output is " + f"required, go on to finish your task or call " + f"'{self.finish_function_name}' to generate the " + f"required structured output.", + "user", + ) + await self._reasoning_hint_msgs.add(msg_hint) + + if msg_hint and self.print_hint_msg: + await self.print(msg_hint) + + # When the maximum iterations are reached + # and no reply message is generated + if reply_msg is None: + reply_msg = await self._summarizing() + reply_msg.metadata = structured_output + await self.memory.add(reply_msg) + + # Post-process the memory, long-term memory + if self._static_control: + await self.long_term_memory.record( + [ + *([*msg] if isinstance(msg, list) else [msg]), + *await self.memory.get_memory(), + reply_msg, + ], + ) + + return reply_msg @retry(stop=stop_after_attempt(10), wait=wait_fixed(5), reraise=True) async def _reasoning( self, + tool_choice: str = "required", ) -> Msg: """Perform the reasoning process.""" prompt = await self.formatter.format( msgs=[ Msg("system", self.sys_prompt, "system"), *await self.memory.get_memory(), + # The hint messages to guide the agent's behavior, maybe empty + *await self._reasoning_hint_msgs.get_memory(), ], ) + # Clear the hint messages after use + await self._reasoning_hint_msgs.clear() + try: res = await self.model( prompt, tools=self.toolkit.get_json_schemas(), + tool_choice=tool_choice, ) except Exception as e: - print(str(e)) + logger.debug("Error while calling model in _reasoning: {}", e) # handle output from the model interrupted_by_user = False @@ -238,18 +357,6 @@ class DataScienceAgent(AliasAgentBase): raise e from None finally: - if msg and not msg.has_content_blocks("tool_use"): - # Turn plain text response into a tool call of the finish - # function - msg.content = [ - ToolUseBlock( - id=shortuuid.uuid(), - type="tool_use", - name=self.think_function_name, - input={"response": msg.get_text_content()}, - ), - ] - # None will be ignored by the memory await self.memory.add(msg) @@ -279,17 +386,10 @@ class DataScienceAgent(AliasAgentBase): # pylint: disable=invalid-overridden-method, unused-argument async def generate_response( self, - response: str, **kwargs: Any, ) -> ToolResponse: - """Call this function when you have either completed the task - or cannot continue due to insurmountable reasons. - Provide in the `response` argument any information you believe - the user needs to be informed of. - - Args: - response (`str`): - Your response to the user. + """ + Generate required structured output by this function and return it """ memory = await self.memory.get_memory() memory_log = "\n\n".join( @@ -350,20 +450,15 @@ class DataScienceAgent(AliasAgentBase): f"{self.detailed_report_path}." ) - response_msg = Msg( - self.name, - response, - "assistant", - ) - - await self.print(response_msg, True) + kwargs["response"] = response + structured_output = {} # Prepare structured output if self._required_structured_model: try: # Use the metadata field of the message to store the # structured output - response_msg.metadata = ( + structured_output = ( self._required_structured_model.model_validate( kwargs, ).model_dump() @@ -379,10 +474,18 @@ class DataScienceAgent(AliasAgentBase): ], metadata={ "success": False, - "response_msg": None, + "structured_output": {}, }, ) + await self.print( + Msg( + name=self.name, + content=response, + role="assistant", + ), + True, + ) return ToolResponse( content=[ TextBlock( @@ -392,7 +495,7 @@ class DataScienceAgent(AliasAgentBase): ], metadata={ "success": True, - "response_msg": response_msg, + "structured_output": structured_output, }, is_last=True, ) diff --git a/alias/src/alias/agent/agents/_deep_research_agent_v2.py b/alias/src/alias/agent/agents/_deep_research_agent_v2.py index 232ad97..6e3cc75 100644 --- a/alias/src/alias/agent/agents/_deep_research_agent_v2.py +++ b/alias/src/alias/agent/agents/_deep_research_agent_v2.py @@ -107,6 +107,8 @@ class DeepResearchAgent(AliasAgentBase): x, x.get("task_type", "general"), ) + if x + else None ), ) self.node_level_report = node_level_report diff --git a/alias/src/alias/agent/agents/_meta_planner.py b/alias/src/alias/agent/agents/_meta_planner.py index 095b66c..b2e65ed 100644 --- a/alias/src/alias/agent/agents/_meta_planner.py +++ b/alias/src/alias/agent/agents/_meta_planner.py @@ -245,12 +245,19 @@ class MetaPlanner(AliasAgentBase): long_term_memory.tool_memory_retrieve, ) + # register finish_function_name + if not self.toolkit.tools.get(self.finish_function_name): + self.toolkit.register_tool_function( + self.finish_function_name, + ) + + response_func = self.toolkit.tools.get(self.finish_function_name) + # adjust ReActAgent parameters if enable_clarification: self._required_structured_model = ( MetaPlannerResponseWithClarification ) - response_func = self.toolkit.tools.get(self.finish_function_name) response_func.json_schema["function"][ "description" ] = response_func.json_schema["function"].get( @@ -265,7 +272,6 @@ class MetaPlanner(AliasAgentBase): self._required_structured_model = ( MetaPlannerResponseNoClarification ) - response_func = self.toolkit.tools.get(self.finish_function_name) response_func.json_schema["function"][ "description" ] = response_func.json_schema["function"].get( diff --git a/alias/src/alias/agent/agents/_react_worker.py b/alias/src/alias/agent/agents/_react_worker.py index 0b77dd0..2176138 100644 --- a/alias/src/alias/agent/agents/_react_worker.py +++ b/alias/src/alias/agent/agents/_react_worker.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- # pylint: disable=C2801, W0611, W0212 -from typing import Optional, Any +from typing import Optional, Any, Callable +from functools import partial from agentscope.model import ChatModelBase from agentscope.formatter import FormatterBase from agentscope.memory import MemoryBase -from agentscope.tool import ToolResponse -from agentscope.message import ( - Msg, - TextBlock, -) from dotenv import load_dotenv from alias.agent.agents import AliasAgentBase @@ -50,21 +46,22 @@ class ReActWorker(AliasAgentBase): state_saving_dir=state_saving_dir, ) + self._required_structured_model = WorkerResponse + self.reply: Callable = partial( + self.reply, + structured_model=self._required_structured_model, + ) + self.max_iters: int = max(self.max_iters, WORKER_MAX_ITER) def generate_response( self, - response: str = "", - task_done: bool = True, - subtask_progress_summary: str = "", - generated_files: dict[str, str] = None, - ) -> ToolResponse: + **kwargs, + ): # pylint: disable=useless-parent-delegation """ Generate a response summarizing the execution progress of the given subtask. Args: - response (str): - The response text (compatible with AgentScope finish function). task_done (bool): REQUIRED! Whether the subtask was done or not. subtask_progress_summary (str): @@ -76,37 +73,4 @@ class ReActWorker(AliasAgentBase): paths of generated files (e.g. '/FULL/PATH/OF/FILE_1.md') and the values are short descriptions about the generated files. """ - if generated_files is None: - generated_files = {} - - # If only response is provided, - # use it as subtask_progress_summary - if not subtask_progress_summary and response: - subtask_progress_summary = response - - structure_response = WorkerResponse( - task_done=task_done, - subtask_progress_summary=subtask_progress_summary, - generated_files=generated_files, - ) - response_msg = Msg( - self.name, - content=[ - TextBlock(type="text", text=subtask_progress_summary), - ], - role="assistant", - metadata=structure_response.model_dump(), - ) - return ToolResponse( - content=[ - TextBlock( - type="text", - text="Successfully generated response.", - ), - ], - metadata={ - "success": True, - "response_msg": response_msg, - }, - is_last=True, - ) + return super().generate_response(**kwargs) diff --git a/alias/src/alias/agent/agents/common_agent_utils/_common_agent_hooks.py b/alias/src/alias/agent/agents/common_agent_utils/_common_agent_hooks.py index b07f395..b1f2de9 100644 --- a/alias/src/alias/agent/agents/common_agent_utils/_common_agent_hooks.py +++ b/alias/src/alias/agent/agents/common_agent_utils/_common_agent_hooks.py @@ -106,7 +106,7 @@ async def save_post_reasoning_state( async def save_post_action_state( self: AliasAgentBase, action_input: dict[str, Any], # pylint: disable=W0613 - tool_output: Optional[Msg], # pylint: disable=W0613 + tool_output: Optional[dict], # pylint: disable=W0613 ) -> None: """Hook func for save state after action step""" await _update_and_save_state_with_session(self) @@ -115,23 +115,23 @@ async def save_post_action_state( async def generate_response_post_action_hook( self: AliasAgentBase, action_input: dict[str, Any], # pylint: disable=W0613 - tool_output: Optional[Msg], # pylint: disable=W0613 + tool_output: Optional[dict], # pylint: disable=W0613 ) -> None: """Hook func for printing clarification""" if not (hasattr(self, "session_service") and self.session_service): return - if isinstance(tool_output, Msg): - if tool_output.metadata and tool_output.metadata.get( + if isinstance(tool_output, dict): + if tool_output.get( "require_clarification", False, ): clarification_dict = { - "clarification_question": tool_output.metadata.get( + "clarification_question": tool_output.get( "clarification_question", "", ), - "clarification_options": tool_output.metadata.get( + "clarification_options": tool_output.get( "clarification_options", "", ), @@ -144,7 +144,7 @@ async def generate_response_post_action_hook( indent=4, ), role="assistant", - metadata=tool_output.metadata, + metadata=tool_output, ) await self.print(msg, last=True) diff --git a/alias/src/alias/agent/agents/common_agent_utils/_common_models.py b/alias/src/alias/agent/agents/common_agent_utils/_common_models.py index 5cfc265..01a8db2 100644 --- a/alias/src/alias/agent/agents/common_agent_utils/_common_models.py +++ b/alias/src/alias/agent/agents/common_agent_utils/_common_models.py @@ -76,14 +76,15 @@ class WorkerResponse(BaseModel): """ subtask_progress_summary: str = Field( - ..., + default="", description=WORKER_PROGRESS_SUMMARY, ) generated_files: dict = Field( - ..., + default=dict, description=WORKER_FILE_COLLECTION_INSTRUCTION, ) task_done: bool = Field( - ..., - description="Whether task is done or it require addition effort", + default=True, + description="Whether the task is done or " + "it requires additional effort", ) diff --git a/alias/src/alias/agent/agents/ds_agent_utils/__init__.py b/alias/src/alias/agent/agents/ds_agent_utils/__init__.py index 7db8eff..1c40afc 100644 --- a/alias/src/alias/agent/agents/ds_agent_utils/__init__.py +++ b/alias/src/alias/agent/agents/ds_agent_utils/__init__.py @@ -9,7 +9,9 @@ from .utils import ( ) from .ds_toolkit import add_ds_specific_tool from .prompt_selector import LLMPromptSelector -from .agent_hook import files_filter_pre_reply_hook +from .agent_hook import ( + files_filter_pre_reply_hook, +) __all__ = [ "ReportGenerator", diff --git a/alias/src/alias/agent/agents/meta_planner_utils/_meta_planner_hooks.py b/alias/src/alias/agent/agents/meta_planner_utils/_meta_planner_hooks.py index 63b2f01..baa61f8 100644 --- a/alias/src/alias/agent/agents/meta_planner_utils/_meta_planner_hooks.py +++ b/alias/src/alias/agent/agents/meta_planner_utils/_meta_planner_hooks.py @@ -128,7 +128,7 @@ async def _planner_save_plan_with_session( async def planner_save_post_action_state( self: MetaPlanner, action_input: dict[str, Any], # pylint: disable=W0613 - tool_output: Optional[Msg], # pylint: disable=W0613 + tool_output: Optional[dict], # pylint: disable=W0613 ) -> None: """Hook func for save state after action step""" await _update_and_save_state_with_session(self) diff --git a/alias/src/alias/server/api/v1/__init__.py b/alias/src/alias/server/api/v1/__init__.py index 3c7472f..0dce93f 100644 --- a/alias/src/alias/server/api/v1/__init__.py +++ b/alias/src/alias/server/api/v1/__init__.py @@ -10,6 +10,7 @@ from alias.server.api.v1.file import router as file_router from alias.server.api.v1.inner import router as inner_router from alias.server.api.v1.share import router as share_router from alias.server.api.v1.user import router as user_router +from alias.server.api.v1.monitor import router as monitor_router router = APIRouter() router.include_router(user_router) @@ -19,3 +20,4 @@ router.include_router(chat_router) router.include_router(file_router) router.include_router(inner_router) router.include_router(share_router) +router.include_router(monitor_router) diff --git a/alias/src/alias/server/services/conversation_service.py b/alias/src/alias/server/services/conversation_service.py index effed83..6a72160 100644 --- a/alias/src/alias/server/services/conversation_service.py +++ b/alias/src/alias/server/services/conversation_service.py @@ -80,6 +80,7 @@ class ConversationService(BaseService[Conversation]): base_url=settings.SANDBOX_URL, bearer_token=settings.SANDBOX_BEARER_TOKEN, ) + sandbox.__enter__() conversation_data = Conversation( user_id=user_id,