From 0266ec65cca7745f70fd8e087ba4294837750305 Mon Sep 17 00:00:00 2001 From: Rasic2 <1051987201@qq.com> Date: Mon, 19 Jan 2026 16:05:04 +0800 Subject: [PATCH 1/3] feat: implement multi-plan generation and update agent naming for clarity and modularity --- agents/matmaster_agent/flow_agents/agent.py | 95 ++++++++++++------- .../matmaster_agent/flow_agents/constant.py | 1 + .../flow_agents/expand_agent/__init__.py | 0 .../flow_agents/expand_agent/constant.py | 1 + .../flow_agents/intent_agent/constant.py | 1 + .../plan_confirm_agent/__init__.py | 0 .../plan_confirm_agent/constant.py | 1 + .../flow_agents/plan_confirm_agent/prompt.py | 6 ++ .../flow_agents/plan_confirm_agent/schema.py | 1 + .../flow_agents/plan_info_agent/callback.py | 55 +++++++++++ .../flow_agents/plan_info_agent/prompt.py | 13 --- .../flow_agents/plan_make_agent/agent.py | 56 ++++++----- .../flow_agents/plan_make_agent/callback.py | 53 +++++++++++ .../flow_agents/plan_make_agent/prompt.py | 24 +++-- .../flow_agents/plan_make_agent/schema.py | 38 ++++++++ .../flow_agents/scene_agent/constant.py | 1 + agents/matmaster_agent/flow_agents/schema.py | 19 ---- agents/matmaster_agent/flow_agents/utils.py | 38 +++----- agents/matmaster_agent/locales.py | 2 + 19 files changed, 281 insertions(+), 124 deletions(-) create mode 100644 agents/matmaster_agent/flow_agents/expand_agent/__init__.py create mode 100644 agents/matmaster_agent/flow_agents/expand_agent/constant.py create mode 100644 agents/matmaster_agent/flow_agents/intent_agent/constant.py create mode 100644 agents/matmaster_agent/flow_agents/plan_confirm_agent/__init__.py create mode 100644 agents/matmaster_agent/flow_agents/plan_confirm_agent/constant.py create mode 100644 agents/matmaster_agent/flow_agents/plan_info_agent/callback.py create mode 100644 agents/matmaster_agent/flow_agents/plan_make_agent/callback.py create mode 100644 agents/matmaster_agent/flow_agents/plan_make_agent/schema.py create mode 100644 agents/matmaster_agent/flow_agents/scene_agent/constant.py diff --git a/agents/matmaster_agent/flow_agents/agent.py b/agents/matmaster_agent/flow_agents/agent.py index b2bec4ed..6db83090 100644 --- a/agents/matmaster_agent/flow_agents/agent.py +++ b/agents/matmaster_agent/flow_agents/agent.py @@ -32,34 +32,50 @@ ChatAgentGlobalInstruction, ChatAgentInstruction, ) +from agents.matmaster_agent.flow_agents.constant import MATMASTER_FLOW from agents.matmaster_agent.flow_agents.execution_agent.agent import ( MatMasterSupervisorAgent, ) from agents.matmaster_agent.flow_agents.expand_agent.agent import ExpandAgent +from agents.matmaster_agent.flow_agents.expand_agent.constant import EXPAND_AGENT from agents.matmaster_agent.flow_agents.expand_agent.prompt import EXPAND_INSTRUCTION from agents.matmaster_agent.flow_agents.expand_agent.schema import ExpandSchema from agents.matmaster_agent.flow_agents.handle_upload_agent.agent import ( HandleUploadAgent, ) +from agents.matmaster_agent.flow_agents.intent_agent.constant import INTENT_AGENT from agents.matmaster_agent.flow_agents.intent_agent.model import IntentEnum from agents.matmaster_agent.flow_agents.intent_agent.prompt import INTENT_INSTRUCTION from agents.matmaster_agent.flow_agents.intent_agent.schema import IntentSchema +from agents.matmaster_agent.flow_agents.plan_confirm_agent.constant import ( + PLAN_CONFIRM_AGENT, +) from agents.matmaster_agent.flow_agents.plan_confirm_agent.prompt import ( PlanConfirmInstruction, ) from agents.matmaster_agent.flow_agents.plan_confirm_agent.schema import ( PlanConfirmSchema, ) +from agents.matmaster_agent.flow_agents.plan_info_agent.callback import ( + filter_plan_info_llm_contents, +) from agents.matmaster_agent.flow_agents.plan_info_agent.prompt import ( - get_plan_info_instruction, + PLAN_INFO_INSTRUCTION, ) from agents.matmaster_agent.flow_agents.plan_make_agent.agent import PlanMakeAgent +from agents.matmaster_agent.flow_agents.plan_make_agent.callback import ( + filter_plan_make_llm_contents, +) from agents.matmaster_agent.flow_agents.plan_make_agent.prompt import ( get_plan_make_instruction, ) +from agents.matmaster_agent.flow_agents.plan_make_agent.schema import ( + create_dynamic_multi_plans_schema, +) +from agents.matmaster_agent.flow_agents.scene_agent.constant import SCENE_AGENT from agents.matmaster_agent.flow_agents.scene_agent.prompt import SCENE_INSTRUCTION from agents.matmaster_agent.flow_agents.scene_agent.schema import SceneSchema -from agents.matmaster_agent.flow_agents.schema import FlowStatusEnum, PlanSchema +from agents.matmaster_agent.flow_agents.schema import FlowStatusEnum from agents.matmaster_agent.flow_agents.step_title_agent.callback import ( filter_llm_contents, ) @@ -75,7 +91,6 @@ ) from agents.matmaster_agent.flow_agents.utils import ( check_plan, - create_dynamic_plan_schema, get_tools_list, should_bypass_confirmation, ) @@ -129,7 +144,7 @@ def after_init(self): ) self._intent_agent = DisallowTransferAndContentLimitSchemaAgent( - name='intent_agent', + name=INTENT_AGENT, model=MatMasterLlmConfig.tool_schema_model, description='识别用户的意图', instruction=INTENT_INSTRUCTION, @@ -138,7 +153,7 @@ def after_init(self): ) self._expand_agent = ExpandAgent( - name='expand_agent', + name=EXPAND_AGENT, model=MatMasterLlmConfig.tool_schema_model, description='扩写用户的问题', instruction=EXPAND_INSTRUCTION, @@ -147,7 +162,7 @@ def after_init(self): ) self._scene_agent = DisallowTransferAndContentLimitSchemaAgent( - name='scene_agent', + name=SCENE_AGENT, model=MatMasterLlmConfig.tool_schema_model, description='把用户的问题划分到特定的场景', instruction=SCENE_INSTRUCTION, @@ -159,12 +174,12 @@ def after_init(self): name='plan_make_agent', model=MatMasterLlmConfig.tool_schema_model, description='根据用户的问题依据现有工具执行计划,如果没有工具可用,告知用户,不要自己制造工具或幻想', - output_schema=PlanSchema, - state_key='plan', + state_key='multi_plans', + before_model_callback=filter_plan_make_llm_contents, ) self._plan_confirm_agent = DisallowTransferAndContentLimitSchemaAgent( - name='plan_confirm_agent', + name=PLAN_CONFIRM_AGENT, model=MatMasterLlmConfig.tool_schema_model, description='判断用户对计划是否认可', instruction=PlanConfirmInstruction, @@ -175,8 +190,10 @@ def after_init(self): self._plan_info_agent = DisallowTransferAndContentLimitLlmAgent( name='plan_info_agent', model=MatMasterLlmConfig.default_litellm_model, + global_instruction=GLOBAL_INSTRUCTION, description='根据 materials_plan 返回的计划进行总结', - # instruction=PLAN_INFO_INSTRUCTION, + instruction=PLAN_INFO_INSTRUCTION, + before_model_callback=filter_plan_info_llm_contents, ) # execution_agent @@ -406,6 +423,16 @@ async def _run_async_impl( ) plan_confirm = ctx.session.state['plan_confirm'].get('flag', False) + if plan_confirm: + selected_plan_id = ctx.session.state['plan_confirm'][ + 'selected_plan_id' + ] + selected_plan = ctx.session.state['multi_plans']['plans'][ + selected_plan_id + ] + yield update_state_event( + ctx, state_delta={'plan': selected_plan} + ) # 判断要不要制定计划(1. 无计划;2. 计划未通过;3. 计划已完成) if ( @@ -444,8 +471,8 @@ async def _run_async_impl( + UPDATE_USER_CONTENT + TOOLCHAIN_EXAMPLES_PROMPT ) - self.plan_make_agent.output_schema = create_dynamic_plan_schema( - available_tools + self.plan_make_agent.output_schema = ( + create_dynamic_multi_plans_schema(available_tools) ) async for plan_event in self.plan_make_agent.run_async(ctx): yield plan_event @@ -454,7 +481,7 @@ async def _run_async_impl( for matmaster_flow_event in context_function_event( ctx, self.name, - 'matmaster_flow', + MATMASTER_FLOW, None, ModelRole, { @@ -466,15 +493,6 @@ async def _run_async_impl( }, ): yield matmaster_flow_event - plan_steps = ctx.session.state['plan'].get('steps', []) - tool_names = [ - step.get('tool_name') - for step in plan_steps - if step.get('tool_name') - ] - self.plan_info_agent.instruction = get_plan_info_instruction( - tool_names - ) async for plan_summary_event in self.plan_info_agent.run_async( ctx ): @@ -482,7 +500,7 @@ async def _run_async_impl( for matmaster_flow_event in context_function_event( ctx, self.name, - 'matmaster_flow', + MATMASTER_FLOW, None, ModelRole, { @@ -496,16 +514,21 @@ async def _run_async_impl( yield matmaster_flow_event # 更新计划为可执行的计划 - update_plan = copy.deepcopy(ctx.session.state['plan']) - origin_steps = ctx.session.state['plan']['steps'] - actual_steps = [] - for step in origin_steps: - if step.get('tool_name'): - actual_steps.append(step) - else: - break - update_plan['steps'] = actual_steps - yield update_state_event(ctx, state_delta={'plan': update_plan}) + update_multi_plans = copy.deepcopy( + ctx.session.state['multi_plans'] + ) + for update_plan in update_multi_plans['plans']: + origin_steps = update_plan['steps'] + actual_steps = [] + for step in origin_steps: + if step.get('tool_name'): + actual_steps.append(step) + else: + break + update_plan['steps'] = actual_steps + yield update_state_event( + ctx, state_delta={'multi_plans': update_multi_plans} + ) # 检查是否应该跳过用户确认步骤 if should_bypass_confirmation(ctx): @@ -520,6 +543,7 @@ async def _run_async_impl( }, ) else: + multi_plans = ctx.session.state['multi_plans']['plans'] for generate_plan_confirm_event in context_function_event( ctx, self.name, @@ -532,7 +556,10 @@ async def _run_async_impl( 'invocation_id': ctx.invocation_id, 'title': i18n.t('PlanOperation'), 'list': [ - i18n.t('ConfirmPlan'), + f'{i18n.t("Plan")} {id+1}' + for id in range(len(multi_plans)) + ] + + [ i18n.t('RePlan'), ], } diff --git a/agents/matmaster_agent/flow_agents/constant.py b/agents/matmaster_agent/flow_agents/constant.py index c6b09c40..adfd37fc 100644 --- a/agents/matmaster_agent/flow_agents/constant.py +++ b/agents/matmaster_agent/flow_agents/constant.py @@ -1 +1,2 @@ MATMASTER_SUPERVISOR_AGENT = 'matmaster_supervisor_agent' +MATMASTER_FLOW = 'matmaster_flow' diff --git a/agents/matmaster_agent/flow_agents/expand_agent/__init__.py b/agents/matmaster_agent/flow_agents/expand_agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/matmaster_agent/flow_agents/expand_agent/constant.py b/agents/matmaster_agent/flow_agents/expand_agent/constant.py new file mode 100644 index 00000000..7dbdad1b --- /dev/null +++ b/agents/matmaster_agent/flow_agents/expand_agent/constant.py @@ -0,0 +1 @@ +EXPAND_AGENT = 'expand_agent' diff --git a/agents/matmaster_agent/flow_agents/intent_agent/constant.py b/agents/matmaster_agent/flow_agents/intent_agent/constant.py new file mode 100644 index 00000000..0f5e8b78 --- /dev/null +++ b/agents/matmaster_agent/flow_agents/intent_agent/constant.py @@ -0,0 +1 @@ +INTENT_AGENT = 'intent_agent' diff --git a/agents/matmaster_agent/flow_agents/plan_confirm_agent/__init__.py b/agents/matmaster_agent/flow_agents/plan_confirm_agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/matmaster_agent/flow_agents/plan_confirm_agent/constant.py b/agents/matmaster_agent/flow_agents/plan_confirm_agent/constant.py new file mode 100644 index 00000000..439c26ef --- /dev/null +++ b/agents/matmaster_agent/flow_agents/plan_confirm_agent/constant.py @@ -0,0 +1 @@ +PLAN_CONFIRM_AGENT = 'plan_confirm_agent' diff --git a/agents/matmaster_agent/flow_agents/plan_confirm_agent/prompt.py b/agents/matmaster_agent/flow_agents/plan_confirm_agent/prompt.py index d3b50a8f..6a2d1ad5 100644 --- a/agents/matmaster_agent/flow_agents/plan_confirm_agent/prompt.py +++ b/agents/matmaster_agent/flow_agents/plan_confirm_agent/prompt.py @@ -16,10 +16,16 @@ **Output Format:** Return a valid JSON object with the following structure: {{ "flag": true | false, + "selected_plan_id": 0, "reason": "A concise explanation citing the specific words or phrases from the user's response that led to this judgment." }} **Critical Instructions:** - Your analysis should be reasonable but strict. Assume lack of approval unless there is clear indication of acceptance. +- `selected_plan_id` must be a **0-based index** into the presented plans: + - If the user says "方案1" / "plan 1" / "option 1", then `selected_plan_id` = 0 + - If the user says "方案2" / "plan 2" / "option 2", then `selected_plan_id` = 1 + - And so on. +- If `flag` is `false` or no plan is clearly selected, set `selected_plan_id` to 0. - Return **only** the raw JSON object. Do not include any other text, commentary, or formatting outside the JSON structure. """ diff --git a/agents/matmaster_agent/flow_agents/plan_confirm_agent/schema.py b/agents/matmaster_agent/flow_agents/plan_confirm_agent/schema.py index b26464b3..eef87ffa 100644 --- a/agents/matmaster_agent/flow_agents/plan_confirm_agent/schema.py +++ b/agents/matmaster_agent/flow_agents/plan_confirm_agent/schema.py @@ -3,4 +3,5 @@ class PlanConfirmSchema(BaseModel): flag: bool + selected_plan_id: int reason: str diff --git a/agents/matmaster_agent/flow_agents/plan_info_agent/callback.py b/agents/matmaster_agent/flow_agents/plan_info_agent/callback.py new file mode 100644 index 00000000..eae95e95 --- /dev/null +++ b/agents/matmaster_agent/flow_agents/plan_info_agent/callback.py @@ -0,0 +1,55 @@ +import logging +from typing import Optional + +from google.adk.agents.callback_context import CallbackContext +from google.adk.models import LlmRequest, LlmResponse +from google.genai.types import Content, Part + +from agents.matmaster_agent.constant import MATMASTER_AGENT_NAME, ModelRole +from agents.matmaster_agent.flow_agents.constant import MATMASTER_FLOW +from agents.matmaster_agent.flow_agents.expand_agent.constant import EXPAND_AGENT +from agents.matmaster_agent.flow_agents.intent_agent.constant import INTENT_AGENT +from agents.matmaster_agent.flow_agents.plan_confirm_agent.constant import ( + PLAN_CONFIRM_AGENT, +) +from agents.matmaster_agent.flow_agents.scene_agent.constant import SCENE_AGENT +from agents.matmaster_agent.flow_agents.utils import is_content_has_keywords +from agents.matmaster_agent.logger import PrefixFilter + +logger = logging.getLogger(__name__) +logger.addFilter(PrefixFilter(MATMASTER_AGENT_NAME)) +logger.setLevel(logging.INFO) + + +async def filter_plan_info_llm_contents( + callback_context: CallbackContext, llm_request: LlmRequest +) -> Optional[LlmResponse]: + contents = [] + for content in llm_request.contents[::-1]: + if is_content_has_keywords( + content, + [ + PLAN_CONFIRM_AGENT, + SCENE_AGENT, + INTENT_AGENT, + EXPAND_AGENT.replace('_agent', '_schema'), + MATMASTER_FLOW, + ], + ): + continue + else: + contents.insert(0, content) + + logger.info( + f'{callback_context.session.id} {callback_context.agent_name} contents = {contents}' + ) + + if not contents: + contents = [ + Content( + role=ModelRole, + parts=[Part(text='Default Text')], + ) + ] + + llm_request.contents = contents diff --git a/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py b/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py index c216f4be..c1b822a5 100644 --- a/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py +++ b/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py @@ -31,17 +31,4 @@ Important: - Do not end with any question or prompt for user action. - Avoid using the exact title "计划总结" at the beginning. You may use titles like "任务执行方案", "处理概要", or similar alternatives to present the summary more naturally. - -Response in {{target_language}}. """ - - -def get_plan_info_instruction(tool_name: list) -> str: - if len(tool_name) == 1 and tool_name[0] == 'llm_tool': - return """ -You are a plan summary agent. Now there are no available tools. `llm_tool` will directly call the LLM to generate the answer. Tell user the current situation in one sentence. Could refer to this template in {target_language}: -``` -In response to your query, there are no available tools. The following answer is generated by the general LLM. -``` - """ - return PLAN_INFO_INSTRUCTION diff --git a/agents/matmaster_agent/flow_agents/plan_make_agent/agent.py b/agents/matmaster_agent/flow_agents/plan_make_agent/agent.py index 227755a7..27d94c4d 100644 --- a/agents/matmaster_agent/flow_agents/plan_make_agent/agent.py +++ b/agents/matmaster_agent/flow_agents/plan_make_agent/agent.py @@ -22,31 +22,35 @@ async def _run_events(self, ctx: InvocationContext) -> AsyncGenerator[Event, Non async for event in super()._run_events(ctx): yield event - logger.info(f'{ctx.session.id} plan = {ctx.session.state["plan"]}') + logger.info( + f'{ctx.session.id} multi_plans = {ctx.session.state["multi_plans"]}' + ) + # 计算 feasibility - update_plan = ctx.session.state['plan'] - update_plan['feasibility'] = 'null' - total_steps = len(update_plan.get('steps', [])) - exist_step = 0 - update_plan_steps = [] - for step in update_plan.get('steps', []): - if not step['tool_name']: - step['tool_name'] = 'llm_tool' - update_plan_steps.append(step) - update_plan['steps'] = update_plan_steps - - for index, step in enumerate(update_plan['steps']): - if index == 0 and not step['tool_name']: - break - if step['tool_name']: - exist_step += 1 + update_multi_plans = ctx.session.state['multi_plans'] + for update_plan in update_multi_plans['plans']: + update_plan['feasibility'] = 'null' + total_steps = len(update_plan.get('steps', [])) + exist_step = 0 + update_plan_steps = [] + for step in update_plan.get('steps', []): + if not step['tool_name']: + step['tool_name'] = 'llm_tool' + update_plan_steps.append(step) + update_plan['steps'] = update_plan_steps + + for index, step in enumerate(update_plan['steps']): + if index == 0 and not step['tool_name']: + break + if step['tool_name']: + exist_step += 1 + else: + break + if not exist_step: + pass + elif exist_step != total_steps: + update_plan['feasibility'] = 'part' else: - break - if not exist_step: - pass - elif exist_step != total_steps: - update_plan['feasibility'] = 'part' - else: - update_plan['feasibility'] = 'full' - - yield update_state_event(ctx, state_delta={'plan': update_plan}) + update_plan['feasibility'] = 'full' + + yield update_state_event(ctx, state_delta={'multi_plans': update_multi_plans}) diff --git a/agents/matmaster_agent/flow_agents/plan_make_agent/callback.py b/agents/matmaster_agent/flow_agents/plan_make_agent/callback.py new file mode 100644 index 00000000..251d4daa --- /dev/null +++ b/agents/matmaster_agent/flow_agents/plan_make_agent/callback.py @@ -0,0 +1,53 @@ +import logging +from typing import Optional + +from google.adk.agents.callback_context import CallbackContext +from google.adk.models import LlmRequest, LlmResponse +from google.genai.types import Content, Part + +from agents.matmaster_agent.constant import MATMASTER_AGENT_NAME, ModelRole +from agents.matmaster_agent.flow_agents.expand_agent.constant import EXPAND_AGENT +from agents.matmaster_agent.flow_agents.intent_agent.constant import INTENT_AGENT +from agents.matmaster_agent.flow_agents.plan_confirm_agent.constant import ( + PLAN_CONFIRM_AGENT, +) +from agents.matmaster_agent.flow_agents.scene_agent.constant import SCENE_AGENT +from agents.matmaster_agent.flow_agents.utils import is_content_has_keywords +from agents.matmaster_agent.logger import PrefixFilter + +logger = logging.getLogger(__name__) +logger.addFilter(PrefixFilter(MATMASTER_AGENT_NAME)) +logger.setLevel(logging.INFO) + + +async def filter_plan_make_llm_contents( + callback_context: CallbackContext, llm_request: LlmRequest +) -> Optional[LlmResponse]: + contents = [] + for content in llm_request.contents[::-1]: + if is_content_has_keywords( + content, + [ + PLAN_CONFIRM_AGENT, + SCENE_AGENT, + INTENT_AGENT, + EXPAND_AGENT.replace('_agent', '_schema'), + ], + ): + continue + else: + contents.insert(0, content) + + logger.info( + f'{callback_context.session.id} {callback_context.agent_name} contents = {contents}' + ) + + if not contents: + contents = [ + Content( + role=ModelRole, + parts=[Part(text='Default Text')], + ) + ] + + llm_request.contents = contents diff --git a/agents/matmaster_agent/flow_agents/plan_make_agent/prompt.py b/agents/matmaster_agent/flow_agents/plan_make_agent/prompt.py index 314e28c0..bb5fbf36 100644 --- a/agents/matmaster_agent/flow_agents/plan_make_agent/prompt.py +++ b/agents/matmaster_agent/flow_agents/plan_make_agent/prompt.py @@ -8,14 +8,25 @@ def get_plan_make_instruction(available_tools_with_info: str): ### RE-PLANNING LOGIC: If the input contains errors from previous steps, analyze the failure and adjust the current plan (e.g., fix parameters or change tools) to resolve the issue. Mention the fix in the "description". +### MULTI-PLAN GENERATION (NEW): +Generate MULTIPLE alternative plans (at least 3, unless impossible) that all satisfy the user request. +Each plan MUST use a DIFFERENT tool orchestration strategy (i.e., different tool choices and/or different step ordering). +If there is only one feasible orchestration due to tool constraints, still output multiple plans and clearly explain in each plan's "feasibility" why divergence is not possible. + Return a JSON structure with the following format: {{ - "steps": [ + "plans": [ {{ - "tool_name": , // Name of the tool to use (exact match from available list). Use null if no suitable tool exists - "description": , // Clear explanation of what this tool call will accomplish - "feasibility", // Clear evidence that the preceding step or user input supports the execution of the step. Or why is there no tool support for this step - "status": "plan" // Always return "plan" + "plan_id": , + "strategy": , // brief summary of how this plan differs (tooling/order) + "steps": [ + {{ + "tool_name": , // Name of the tool to use (exact match from available list). Use null if no suitable tool exists + "description": , // Clear explanation of what this tool call will accomplish + "feasibility": , // Evidence input/preceding steps support this step, or why no tool support exists + "status": "plan" // Always return "plan" + }} + ] }} ] }} @@ -28,7 +39,8 @@ def get_plan_make_instruction(available_tools_with_info: str): 5. Use null for tool_name only when no appropriate tool exists in the available tools list 6. Never invent or assume tools - only use tools explicitly listed in the available tools 7. Match tools precisely to requirements - if functionality doesn't align exactly, use null -8. Ensure steps array represents the complete execution sequence for the request +8. Ensure each plan’s steps array represents a complete execution sequence for the request +9. Across different plans, avoid producing identical step lists; vary tooling and/or ordering whenever feasible. EXECUTION PRINCIPLES: - Make sure that the previous steps can provide the input information required for the current step, such as the file URL diff --git a/agents/matmaster_agent/flow_agents/plan_make_agent/schema.py b/agents/matmaster_agent/flow_agents/plan_make_agent/schema.py new file mode 100644 index 00000000..c5917ed4 --- /dev/null +++ b/agents/matmaster_agent/flow_agents/plan_make_agent/schema.py @@ -0,0 +1,38 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel, create_model + +from agents.matmaster_agent.flow_agents.model import PlanStepStatusEnum + + +def create_dynamic_multi_plans_schema(available_tools: list): + # 动态创建 PlanStepSchema + DynamicPlanStepSchema = create_model( + 'DynamicPlanStepSchema', + tool_name=(Optional[Literal[tuple(available_tools)]], None), + description=(str, ...), + feasibility=(str, ...), + status=( + Literal[tuple(PlanStepStatusEnum.__members__.values())], + PlanStepStatusEnum.PLAN.value, + ), + __base__=BaseModel, + ) + + # 动态创建 PlanSchema + DynamicPlanSchema = create_model( + 'DynamicPlanSchema', + plan_id=(str, ...), + strategy=(str, ...), + steps=(List[DynamicPlanStepSchema], ...), + __base__=BaseModel, + ) + + # 动态创建 MultiPlansSchema + DynamicMultiPlansSchema = create_model( + 'DynamicMultiPlansSchema', + plans=(List[DynamicPlanSchema], ...), + __base__=BaseModel, + ) + + return DynamicMultiPlansSchema diff --git a/agents/matmaster_agent/flow_agents/scene_agent/constant.py b/agents/matmaster_agent/flow_agents/scene_agent/constant.py new file mode 100644 index 00000000..4d81cf98 --- /dev/null +++ b/agents/matmaster_agent/flow_agents/scene_agent/constant.py @@ -0,0 +1 @@ +SCENE_AGENT = 'scene_agent' diff --git a/agents/matmaster_agent/flow_agents/schema.py b/agents/matmaster_agent/flow_agents/schema.py index 90de81d5..a15acf81 100644 --- a/agents/matmaster_agent/flow_agents/schema.py +++ b/agents/matmaster_agent/flow_agents/schema.py @@ -1,23 +1,4 @@ from enum import Enum -from typing import List, Literal, Optional - -from pydantic import BaseModel - -from agents.matmaster_agent.flow_agents.model import PlanStepStatusEnum -from agents.matmaster_agent.sub_agents.mapping import ALL_AGENT_TOOLS_LIST - - -class PlanStepSchema(BaseModel): - tool_name: Optional[Literal[tuple(ALL_AGENT_TOOLS_LIST)]] = None - description: str - feasibility: str - status: Literal[tuple(PlanStepStatusEnum.__members__.values())] = ( - PlanStepStatusEnum.PLAN - ) - - -class PlanSchema(BaseModel): - steps: List[PlanStepSchema] class FlowStatusEnum(str, Enum): diff --git a/agents/matmaster_agent/flow_agents/utils.py b/agents/matmaster_agent/flow_agents/utils.py index b0736a67..afbeba0b 100644 --- a/agents/matmaster_agent/flow_agents/utils.py +++ b/agents/matmaster_agent/flow_agents/utils.py @@ -1,8 +1,8 @@ import logging -from typing import List, Literal, Optional +from typing import List from google.adk.agents import InvocationContext -from pydantic import BaseModel, create_model +from google.genai.types import Content from agents.matmaster_agent.flow_agents.model import PlanStepStatusEnum from agents.matmaster_agent.flow_agents.scene_agent.model import SceneEnum @@ -80,30 +80,6 @@ def check_plan(ctx: InvocationContext): return FlowStatusEnum.PROCESS -def create_dynamic_plan_schema(available_tools: list): - # 动态创建 PlanStepSchema - DynamicPlanStepSchema = create_model( - 'DynamicPlanStepSchema', - tool_name=(Optional[Literal[tuple(available_tools)]], None), - description=(str, ...), - feasibility=(str, ...), - status=( - Literal[tuple(PlanStepStatusEnum.__members__.values())], - PlanStepStatusEnum.PLAN.value, - ), - __base__=BaseModel, - ) - - # 动态创建 PlanSchema - DynamicPlanSchema = create_model( - 'DynamicPlanSchema', - steps=(List[DynamicPlanStepSchema], ...), - __base__=BaseModel, - ) - - return DynamicPlanSchema - - def should_bypass_confirmation(ctx: InvocationContext) -> bool: """Determine whether to skip plan confirmation based on the tools in the plan.""" plan_steps = ctx.session.state['plan'].get('steps', []) @@ -148,3 +124,13 @@ def get_self_check(current_tool_name: str) -> bool: if not tool: return False return tool.get('self_check', False) + + +def is_content_has_keywords(content: Content, keywords: List[str]) -> bool: + tokens = [f'[{k}]' if k.endswith('agent') else f'`{k}`' for k in keywords] + + return any( + isinstance((text := getattr(part, 'text', None)), str) + and any(token in text for token in tokens) + for part in content.parts + ) diff --git a/agents/matmaster_agent/locales.py b/agents/matmaster_agent/locales.py index f902aecd..c7032e15 100644 --- a/agents/matmaster_agent/locales.py +++ b/agents/matmaster_agent/locales.py @@ -22,6 +22,7 @@ 'RePlanMake': 'Re-Planning', 'PlanMake': 'Planning', 'PlanSummary': 'Plan Summary', + 'Plan': 'Plan', 'NoFoundStructure': 'No eligible structures found.', 'WalletNoFee': 'Insufficient wallet balance', 'WalletNoFeeAction': 'Insufficient wallet balance. Please top up your account on [this page](https://www.bohrium.com/consume?menu=cash) and try again.', @@ -47,6 +48,7 @@ 'RePlanMake': '重新制定计划', 'PlanMake': '制定计划', 'PlanSummary': '计划汇总概要', + 'Plan': '方案', 'NoFoundStructure': '未找到符合条件的结构', 'WalletNoFee': '钱包余额不足', 'WalletNoFeeAction': '钱包余额不足,请在[此页面](https://www.bohrium.com/consume?menu=cash)充值后重试。', From 8f2de00200aa1775385cf7851e32d4fa80675a4b Mon Sep 17 00:00:00 2001 From: Rasic2 <1051987201@qq.com> Date: Tue, 20 Jan 2026 16:49:37 +0800 Subject: [PATCH 2/3] feat: implement JSON output schema for plan_info_agent to enhance data consistency --- agents/matmaster_agent/flow_agents/agent.py | 56 ++++++++++--------- .../flow_agents/plan_info_agent/prompt.py | 34 +++++++++-- .../flow_agents/plan_info_agent/schema.py | 9 +++ 3 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 agents/matmaster_agent/flow_agents/plan_info_agent/schema.py diff --git a/agents/matmaster_agent/flow_agents/agent.py b/agents/matmaster_agent/flow_agents/agent.py index 6db83090..3dafdb70 100644 --- a/agents/matmaster_agent/flow_agents/agent.py +++ b/agents/matmaster_agent/flow_agents/agent.py @@ -62,6 +62,7 @@ from agents.matmaster_agent.flow_agents.plan_info_agent.prompt import ( PLAN_INFO_INSTRUCTION, ) +from agents.matmaster_agent.flow_agents.plan_info_agent.schema import PlanInfoSchema from agents.matmaster_agent.flow_agents.plan_make_agent.agent import PlanMakeAgent from agents.matmaster_agent.flow_agents.plan_make_agent.callback import ( filter_plan_make_llm_contents, @@ -187,12 +188,14 @@ def after_init(self): state_key='plan_confirm', ) - self._plan_info_agent = DisallowTransferAndContentLimitLlmAgent( + self._plan_info_agent = DisallowTransferAndContentLimitSchemaAgent( name='plan_info_agent', - model=MatMasterLlmConfig.default_litellm_model, + model=MatMasterLlmConfig.tool_schema_model, global_instruction=GLOBAL_INSTRUCTION, description='根据 materials_plan 返回的计划进行总结', instruction=PLAN_INFO_INSTRUCTION, + output_schema=PlanInfoSchema, + state_key='plan_info', before_model_callback=filter_plan_info_llm_contents, ) @@ -497,6 +500,30 @@ async def _run_async_impl( ctx ): yield plan_summary_event + plan_info = ctx.session.state['plan_info'] + intro = plan_info['intro'] + plans = plan_info['plans'] + overall = plan_info['overall'] + + for matmaster_flow_plans_event in context_function_event( + ctx, + self.name, + 'matmaster_flow_plans', + None, + ModelRole, + { + 'plans_result': json.dumps( + { + 'invocation_id': ctx.invocation_id, + 'intro': intro, + 'plans': plans, + 'overall': overall, + } + ) + }, + ): + yield matmaster_flow_plans_event + for matmaster_flow_event in context_function_event( ctx, self.name, @@ -542,31 +569,6 @@ async def _run_async_impl( } }, ) - else: - multi_plans = ctx.session.state['multi_plans']['plans'] - for generate_plan_confirm_event in context_function_event( - ctx, - self.name, - 'matmaster_generate_follow_up', - {}, - ModelRole, - { - 'follow_up_result': json.dumps( - { - 'invocation_id': ctx.invocation_id, - 'title': i18n.t('PlanOperation'), - 'list': [ - f'{i18n.t("Plan")} {id+1}' - for id in range(len(multi_plans)) - ] - + [ - i18n.t('RePlan'), - ], - } - ), - }, - ): - yield generate_plan_confirm_event # 计划未确认,暂停往下执行 if ctx.session.state['plan_confirm']['flag']: diff --git a/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py b/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py index c1b822a5..fc96d20d 100644 --- a/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py +++ b/agents/matmaster_agent/flow_agents/plan_info_agent/prompt.py @@ -12,10 +12,10 @@ {{ "tool_name": "", // Tool name without "functions" prefix; null if no tool available "description": "", // Description of what this step does - "status": "plan" // Planning + "status": "plan" }} ], - "feasibility": "" // "full" = all steps have tools, "part" = some have tools, "null" = no tools available + "feasibility": "" // "full" | "part" | "null" }} When the user proposes: "[USER'S QUERY HERE]" @@ -26,9 +26,35 @@ - Overall feasibility assessment - Clear indication of whether the plan can be fully executed, partially executed, or cannot be executed -Format your response to be user-friendly and actionable. +Additionally, your output MUST be a JSON object that conforms to this schema (no extra keys, no surrounding text): +{{ + "intro": "", + "plans": ["", "..."], + "overall": "" +}} + +Where: +- intro: a brief, user-friendly overview including total steps (do not start with the exact title "计划总结"). +- plans: list of plan strings. IMPORTANT: each plan string MUST be a multi-line, human-friendly “方案” block (not a single sentence), similar to: + + 方案 i:<方案标题(根据步骤内容自动概括)> + * 共 步 + 1. 使用 **** <更具体的动作描述(补全“做什么/为了什么/产出是什么”)> + 2. 使用 **** <更具体的动作描述(补全“做什么/为了什么/产出是什么”)> + ... + + Requirements for each step line: + - Must include the tool in bold: **tool_name** (or **no tool**). + - Must be explicit and concrete: include what is done + purpose/output (e.g., “并保存为 xxx / 得到 xxx / 提取出 xxx 字段”). + - Preserve step order and step count exactly as in input steps. + - Use Chinese numbering format “1.” “2.” ... as shown. + + Note: even though the input is a single plan with steps, still format it as “方案 1 …”. If in the future multiple alternative plans are provided, put each alternative as one item in the `plans` list. + +- overall: overall feasibility assessment; must clearly state fully/partially/cannot be executed (对应 full/part/null). Important: +- Return JSON only (no markdown/code fences). - Do not end with any question or prompt for user action. -- Avoid using the exact title "计划总结" at the beginning. You may use titles like "任务执行方案", "处理概要", or similar alternatives to present the summary more naturally. +- Avoid using the exact title "计划总结" at the beginning. """ diff --git a/agents/matmaster_agent/flow_agents/plan_info_agent/schema.py b/agents/matmaster_agent/flow_agents/plan_info_agent/schema.py new file mode 100644 index 00000000..6b35192f --- /dev/null +++ b/agents/matmaster_agent/flow_agents/plan_info_agent/schema.py @@ -0,0 +1,9 @@ +from typing import List + +from pydantic import BaseModel + + +class PlanInfoSchema(BaseModel): + intro: str + plans: List[str] + overall: str From 8c5ee3f95c206b5bfc8da63bf76cb04951710edb Mon Sep 17 00:00:00 2001 From: Rasic2 <1051987201@qq.com> Date: Tue, 20 Jan 2026 16:56:23 +0800 Subject: [PATCH 3/3] fix: add checks to correct plan_confirm flag when misjudged --- agents/matmaster_agent/flow_agents/agent.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/agents/matmaster_agent/flow_agents/agent.py b/agents/matmaster_agent/flow_agents/agent.py index 3dafdb70..33c757e3 100644 --- a/agents/matmaster_agent/flow_agents/agent.py +++ b/agents/matmaster_agent/flow_agents/agent.py @@ -411,6 +411,7 @@ async def _run_async_impl( ) in self.plan_confirm_agent.run_async(ctx): yield plan_confirm_event + # 用户说确认计划,但 plan_confirm 误判为 False if ctx.user_content.parts[ 0 ].text == '确认计划' and not ctx.session.state[ @@ -424,6 +425,16 @@ async def _run_async_impl( yield update_state_event( ctx, state_delta={'plan_confirm': {'flag': True}} ) + # 没有计划,但 plan_confirm 误判为 True + elif ctx.session.state['plan_confirm'].get( + 'flag', False + ) and not ctx.session.state.get('multi_plans', {}): + logger.warning( + f'{ctx.session.id} plan_confirm = True, but no multi_plans, manually setting plan_confirm -> False' + ) + yield update_state_event( + ctx, state_delta={'plan_confirm': {'flag': False}} + ) plan_confirm = ctx.session.state['plan_confirm'].get('flag', False) if plan_confirm: