77import os
88import re
99import sys
10+ from pathlib import Path
1011from typing import Any , cast
1112
1213import anyio
3435
3536_ALLOWED_OUTPUT_FORMATS = {"markdown" , "yaml" , "hybrid" }
3637_ALLOWED_MENTIONS_MODES = {"controlled" , "required" , "off" }
38+ _GLOBAL_OUTPUT_TEMPLATES_CACHE : dict [str , Any ] | None = None
39+ _AGENT_OUTPUT_CONFIG_CACHE : dict [str , dict [str , Any ]] = {}
3740
3841_OUTPUT_SCHEMA_BLOCK_MARKDOWN = (
3942 "\n \n ## Output Format (required)\n "
@@ -98,16 +101,157 @@ def _normalize_mentions_mode(value: Any) -> str:
98101 return "controlled"
99102
100103
101- def _get_output_preferences (agent_name : str ) -> tuple [str , str ]:
104+ def _get_project_root () -> Path :
105+ return Path .cwd ()
106+
107+
108+ def _load_global_output_templates (root_dir : Path | None = None ) -> dict [str , Any ]:
109+ global _GLOBAL_OUTPUT_TEMPLATES_CACHE
110+ if _GLOBAL_OUTPUT_TEMPLATES_CACHE is not None :
111+ return _GLOBAL_OUTPUT_TEMPLATES_CACHE
112+
113+ root = root_dir or _get_project_root ()
114+ path = root / "config" / "output_templates.yml"
115+ if not path .exists ():
116+ _GLOBAL_OUTPUT_TEMPLATES_CACHE = {}
117+ return _GLOBAL_OUTPUT_TEMPLATES_CACHE
118+ try :
119+ data = yaml .safe_load (path .read_text (encoding = "utf-8" )) or {}
120+ _GLOBAL_OUTPUT_TEMPLATES_CACHE = data if isinstance (data , dict ) else {}
121+ except Exception :
122+ _GLOBAL_OUTPUT_TEMPLATES_CACHE = {}
123+ return _GLOBAL_OUTPUT_TEMPLATES_CACHE
124+
125+
126+ def _load_agent_output_config (agent_name : str , root_dir : Path | None = None ) -> dict [str , Any ]:
127+ key = f"{ root_dir or _get_project_root ()} ::{ agent_name } "
128+ if key in _AGENT_OUTPUT_CONFIG_CACHE :
129+ return _AGENT_OUTPUT_CONFIG_CACHE [key ]
130+
131+ root = root_dir or _get_project_root ()
132+ path = root / "agents" / agent_name / "output_config.yml"
133+ if not path .exists ():
134+ _AGENT_OUTPUT_CONFIG_CACHE [key ] = {}
135+ return _AGENT_OUTPUT_CONFIG_CACHE [key ]
136+ try :
137+ data = yaml .safe_load (path .read_text (encoding = "utf-8" )) or {}
138+ _AGENT_OUTPUT_CONFIG_CACHE [key ] = data if isinstance (data , dict ) else {}
139+ except Exception :
140+ _AGENT_OUTPUT_CONFIG_CACHE [key ] = {}
141+ return _AGENT_OUTPUT_CONFIG_CACHE [key ]
142+
143+
144+ def _resolve_output_template (
145+ agent_name : str , template_id : str | None , * , root_dir : Path | None = None , default_template : str = "review_v1"
146+ ) -> dict [str , Any ] | None :
147+ global_config = _load_global_output_templates (root_dir = root_dir )
148+ global_templates = global_config .get ("templates" , {}) if isinstance (global_config , dict ) else {}
149+ agent_config = _load_agent_output_config (agent_name , root_dir = root_dir )
150+ local_templates = agent_config .get ("templates" , {}) if isinstance (agent_config , dict ) else {}
151+
152+ template_name = template_id
153+ if not template_name :
154+ local_default = agent_config .get ("default_template" )
155+ global_default = global_config .get ("default_template" ) if isinstance (global_config , dict ) else None
156+ if isinstance (local_default , str ) and local_default .strip ():
157+ template_name = local_default .strip ()
158+ elif isinstance (global_default , str ) and global_default .strip ():
159+ template_name = global_default .strip ()
160+ else :
161+ template_name = default_template
162+
163+ if template_name .startswith ("local:" ):
164+ local_name = template_name .split (":" , 1 )[1 ].strip ()
165+ candidate = local_templates .get (local_name )
166+ return candidate if isinstance (candidate , dict ) else None
167+
168+ if (
169+ isinstance (local_templates , dict )
170+ and template_name in local_templates
171+ and isinstance (local_templates [template_name ], dict )
172+ ):
173+ return local_templates [template_name ]
174+ if (
175+ isinstance (global_templates , dict )
176+ and template_name in global_templates
177+ and isinstance (global_templates [template_name ], dict )
178+ ):
179+ return global_templates [template_name ]
180+ return None
181+
182+
183+ def _build_template_instruction (
184+ template : dict [str , Any ], * , mentions_mode : str , output_format : str , section_order_override : list [str ] | None = None
185+ ) -> str | None :
186+ sections = template .get ("sections" )
187+ section_order = section_order_override or template .get ("section_order" )
188+ if not isinstance (sections , dict ) or not isinstance (section_order , list ):
189+ return None
190+
191+ ordered : list [str ] = [str (item ) for item in section_order if isinstance (item , str )]
192+ if not ordered :
193+ return None
194+
195+ lines = ["" , "" , "## Output Format (required)" ]
196+ if output_format == "hybrid" :
197+ lines .append ("优先使用 Markdown;仅在无法稳定输出时允许单个 YAML 代码块。" )
198+ else :
199+ lines .append ("请使用 Markdown 输出,禁止输出 YAML/JSON 代码块。" )
200+ lines .append ("" )
201+ lines .append ("请按以下段落顺序输出:" )
202+
203+ for index , section_key in enumerate (ordered , 1 ):
204+ config = sections .get (section_key )
205+ if not isinstance (config , dict ):
206+ continue
207+ title = str (config .get ("title" ) or f"## { section_key .replace ('_' , ' ' ).title ()} " ).strip ()
208+ guidance = str (config .get ("guidance" ) or "" ).strip ()
209+ if guidance :
210+ lines .append (f"{ index } . `{ title } `:{ guidance } " )
211+ else :
212+ lines .append (f"{ index } . `{ title } `" )
213+
214+ mention_instruction = {
215+ "controlled" : "如需触发协作,仅在文末使用受控区:`---\\ n相关人员: @user1 @user2` 或 `协作请求:` 列表。" ,
216+ "required" : "必须在文末使用受控区输出协作对象:`---\\ n相关人员: @user1 @user2` 或 `协作请求:` 列表。" ,
217+ "off" : "不要输出 `相关人员`/`协作请求` 受控区。" ,
218+ }.get (mentions_mode , "如需触发协作,仅在文末使用受控区:`---\\ n相关人员: @user1 @user2` 或 `协作请求:` 列表。" )
219+ lines .extend (["" , f"- { mention_instruction } " ])
220+ return "\n " .join (lines )
221+
222+
223+ def _get_output_preferences (agent_name : str ) -> tuple [str , str , str | None , list [str ] | None ]:
102224 try :
103225 config = get_agent_config (agent_name ) or {}
104226 except Exception :
105227 config = {}
106- return _normalize_output_format (config .get ("output_format" )), _normalize_mentions_mode (config .get ("mentions_mode" ))
228+
229+ template_id = config .get ("output_template" )
230+ if not isinstance (template_id , str ):
231+ template_id = None
232+
233+ section_order = config .get ("section_order" )
234+ parsed_section_order : list [str ] | None = None
235+ if isinstance (section_order , list ) and all (isinstance (x , str ) for x in section_order ):
236+ parsed_section_order = [str (x ) for x in section_order ]
237+
238+ return (
239+ _normalize_output_format (config .get ("output_format" )),
240+ _normalize_mentions_mode (config .get ("mentions_mode" )),
241+ template_id ,
242+ parsed_section_order ,
243+ )
107244
108245
109246def _append_output_schema (
110- prompt : str , stage_name : str | None = None , * , output_format : str = "markdown" , mentions_mode : str = "controlled"
247+ prompt : str ,
248+ agent_name : str ,
249+ stage_name : str | None = None ,
250+ * ,
251+ output_format : str = "markdown" ,
252+ mentions_mode : str = "controlled" ,
253+ output_template : str | None = None ,
254+ section_order : list [str ] | None = None ,
111255) -> str :
112256 """为 prompt 注入统一输出格式(如果尚未注入)。"""
113257 if "## Output Format (required)" in prompt :
@@ -123,6 +267,15 @@ def _append_output_schema(
123267
124268 if output_format == "yaml" :
125269 return f"{ prompt } { _OUTPUT_SCHEMA_BLOCK_YAML } "
270+
271+ template = _resolve_output_template (agent_name , output_template )
272+ if template :
273+ rendered = _build_template_instruction (
274+ template , mentions_mode = mentions_mode , output_format = output_format , section_order_override = section_order
275+ )
276+ if rendered :
277+ return f"{ prompt } { rendered } "
278+
126279 if output_format == "hybrid" :
127280 return f"{ prompt } { _OUTPUT_SCHEMA_BLOCK_HYBRID } { mention_instruction } "
128281 return f"{ prompt } { _OUTPUT_SCHEMA_BLOCK_MARKDOWN } { mention_instruction } "
@@ -146,7 +299,7 @@ async def run_single_agent(prompt: str, agent_name: str, *, stage_name: str | No
146299 """
147300 logger .info (f"[{ agent_name } ] 开始运行 Agent" )
148301 logger .debug (f"[{ agent_name } ] Prompt 长度: { len (prompt )} 字符" )
149- output_format , mentions_mode = _get_output_preferences (agent_name )
302+ output_format , mentions_mode , output_template , section_order = _get_output_preferences (agent_name )
150303
151304 # 执行信息收集
152305 execution_info : dict [str , Any ] = {
@@ -173,7 +326,13 @@ async def _query_agent():
173326 first_result = True
174327
175328 effective_prompt = _append_output_schema (
176- prompt , stage_name = stage_name , output_format = output_format , mentions_mode = mentions_mode
329+ prompt ,
330+ agent_name ,
331+ stage_name = stage_name ,
332+ output_format = output_format ,
333+ mentions_mode = mentions_mode ,
334+ output_template = output_template ,
335+ section_order = section_order ,
177336 )
178337 async for message in query (prompt = effective_prompt , options = options ):
179338 # AssistantMessage: AI 响应(文本或工具调用)
0 commit comments