realtimex-deeptutor 0.5.0.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. realtimex_deeptutor/__init__.py +67 -0
  2. realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
  3. realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
  4. realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
  5. realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
  6. realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
  7. realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
  8. src/__init__.py +40 -0
  9. src/agents/__init__.py +24 -0
  10. src/agents/base_agent.py +657 -0
  11. src/agents/chat/__init__.py +24 -0
  12. src/agents/chat/chat_agent.py +435 -0
  13. src/agents/chat/prompts/en/chat_agent.yaml +35 -0
  14. src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
  15. src/agents/chat/session_manager.py +311 -0
  16. src/agents/co_writer/__init__.py +0 -0
  17. src/agents/co_writer/edit_agent.py +260 -0
  18. src/agents/co_writer/narrator_agent.py +423 -0
  19. src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
  20. src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
  21. src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
  22. src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
  23. src/agents/guide/__init__.py +16 -0
  24. src/agents/guide/agents/__init__.py +11 -0
  25. src/agents/guide/agents/chat_agent.py +104 -0
  26. src/agents/guide/agents/interactive_agent.py +223 -0
  27. src/agents/guide/agents/locate_agent.py +149 -0
  28. src/agents/guide/agents/summary_agent.py +150 -0
  29. src/agents/guide/guide_manager.py +500 -0
  30. src/agents/guide/prompts/en/chat_agent.yaml +41 -0
  31. src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
  32. src/agents/guide/prompts/en/locate_agent.yaml +68 -0
  33. src/agents/guide/prompts/en/summary_agent.yaml +157 -0
  34. src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
  35. src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
  36. src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
  37. src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
  38. src/agents/ideagen/__init__.py +12 -0
  39. src/agents/ideagen/idea_generation_workflow.py +426 -0
  40. src/agents/ideagen/material_organizer_agent.py +173 -0
  41. src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
  42. src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
  43. src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
  44. src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
  45. src/agents/question/__init__.py +24 -0
  46. src/agents/question/agents/__init__.py +18 -0
  47. src/agents/question/agents/generate_agent.py +381 -0
  48. src/agents/question/agents/relevance_analyzer.py +207 -0
  49. src/agents/question/agents/retrieve_agent.py +239 -0
  50. src/agents/question/coordinator.py +718 -0
  51. src/agents/question/example.py +109 -0
  52. src/agents/question/prompts/en/coordinator.yaml +75 -0
  53. src/agents/question/prompts/en/generate_agent.yaml +77 -0
  54. src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
  55. src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
  56. src/agents/question/prompts/zh/coordinator.yaml +75 -0
  57. src/agents/question/prompts/zh/generate_agent.yaml +77 -0
  58. src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
  59. src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
  60. src/agents/research/agents/__init__.py +23 -0
  61. src/agents/research/agents/decompose_agent.py +507 -0
  62. src/agents/research/agents/manager_agent.py +228 -0
  63. src/agents/research/agents/note_agent.py +180 -0
  64. src/agents/research/agents/rephrase_agent.py +263 -0
  65. src/agents/research/agents/reporting_agent.py +1333 -0
  66. src/agents/research/agents/research_agent.py +714 -0
  67. src/agents/research/data_structures.py +451 -0
  68. src/agents/research/main.py +188 -0
  69. src/agents/research/prompts/en/decompose_agent.yaml +89 -0
  70. src/agents/research/prompts/en/manager_agent.yaml +24 -0
  71. src/agents/research/prompts/en/note_agent.yaml +121 -0
  72. src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
  73. src/agents/research/prompts/en/reporting_agent.yaml +380 -0
  74. src/agents/research/prompts/en/research_agent.yaml +173 -0
  75. src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
  76. src/agents/research/prompts/zh/manager_agent.yaml +24 -0
  77. src/agents/research/prompts/zh/note_agent.yaml +121 -0
  78. src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
  79. src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
  80. src/agents/research/prompts/zh/research_agent.yaml +173 -0
  81. src/agents/research/research_pipeline.py +1309 -0
  82. src/agents/research/utils/__init__.py +60 -0
  83. src/agents/research/utils/citation_manager.py +799 -0
  84. src/agents/research/utils/json_utils.py +98 -0
  85. src/agents/research/utils/token_tracker.py +297 -0
  86. src/agents/solve/__init__.py +80 -0
  87. src/agents/solve/analysis_loop/__init__.py +14 -0
  88. src/agents/solve/analysis_loop/investigate_agent.py +414 -0
  89. src/agents/solve/analysis_loop/note_agent.py +190 -0
  90. src/agents/solve/main_solver.py +862 -0
  91. src/agents/solve/memory/__init__.py +34 -0
  92. src/agents/solve/memory/citation_memory.py +353 -0
  93. src/agents/solve/memory/investigate_memory.py +226 -0
  94. src/agents/solve/memory/solve_memory.py +340 -0
  95. src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
  96. src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
  97. src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
  98. src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
  99. src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
  100. src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
  101. src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
  102. src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
  103. src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
  104. src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
  105. src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
  106. src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
  107. src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
  108. src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
  109. src/agents/solve/solve_loop/__init__.py +22 -0
  110. src/agents/solve/solve_loop/citation_manager.py +74 -0
  111. src/agents/solve/solve_loop/manager_agent.py +274 -0
  112. src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
  113. src/agents/solve/solve_loop/response_agent.py +301 -0
  114. src/agents/solve/solve_loop/solve_agent.py +325 -0
  115. src/agents/solve/solve_loop/tool_agent.py +470 -0
  116. src/agents/solve/utils/__init__.py +64 -0
  117. src/agents/solve/utils/config_validator.py +313 -0
  118. src/agents/solve/utils/display_manager.py +223 -0
  119. src/agents/solve/utils/error_handler.py +363 -0
  120. src/agents/solve/utils/json_utils.py +98 -0
  121. src/agents/solve/utils/performance_monitor.py +407 -0
  122. src/agents/solve/utils/token_tracker.py +541 -0
  123. src/api/__init__.py +0 -0
  124. src/api/main.py +240 -0
  125. src/api/routers/__init__.py +1 -0
  126. src/api/routers/agent_config.py +69 -0
  127. src/api/routers/chat.py +296 -0
  128. src/api/routers/co_writer.py +337 -0
  129. src/api/routers/config.py +627 -0
  130. src/api/routers/dashboard.py +18 -0
  131. src/api/routers/guide.py +337 -0
  132. src/api/routers/ideagen.py +436 -0
  133. src/api/routers/knowledge.py +821 -0
  134. src/api/routers/notebook.py +247 -0
  135. src/api/routers/question.py +537 -0
  136. src/api/routers/research.py +394 -0
  137. src/api/routers/settings.py +164 -0
  138. src/api/routers/solve.py +305 -0
  139. src/api/routers/system.py +252 -0
  140. src/api/run_server.py +61 -0
  141. src/api/utils/history.py +172 -0
  142. src/api/utils/log_interceptor.py +21 -0
  143. src/api/utils/notebook_manager.py +415 -0
  144. src/api/utils/progress_broadcaster.py +72 -0
  145. src/api/utils/task_id_manager.py +100 -0
  146. src/config/__init__.py +0 -0
  147. src/config/accessors.py +18 -0
  148. src/config/constants.py +34 -0
  149. src/config/defaults.py +18 -0
  150. src/config/schema.py +38 -0
  151. src/config/settings.py +50 -0
  152. src/core/errors.py +62 -0
  153. src/knowledge/__init__.py +23 -0
  154. src/knowledge/add_documents.py +606 -0
  155. src/knowledge/config.py +65 -0
  156. src/knowledge/example_add_documents.py +236 -0
  157. src/knowledge/extract_numbered_items.py +1039 -0
  158. src/knowledge/initializer.py +621 -0
  159. src/knowledge/kb.py +22 -0
  160. src/knowledge/manager.py +782 -0
  161. src/knowledge/progress_tracker.py +182 -0
  162. src/knowledge/start_kb.py +535 -0
  163. src/logging/__init__.py +103 -0
  164. src/logging/adapters/__init__.py +17 -0
  165. src/logging/adapters/lightrag.py +184 -0
  166. src/logging/adapters/llamaindex.py +141 -0
  167. src/logging/config.py +80 -0
  168. src/logging/handlers/__init__.py +20 -0
  169. src/logging/handlers/console.py +75 -0
  170. src/logging/handlers/file.py +201 -0
  171. src/logging/handlers/websocket.py +127 -0
  172. src/logging/logger.py +709 -0
  173. src/logging/stats/__init__.py +16 -0
  174. src/logging/stats/llm_stats.py +179 -0
  175. src/services/__init__.py +56 -0
  176. src/services/config/__init__.py +61 -0
  177. src/services/config/knowledge_base_config.py +210 -0
  178. src/services/config/loader.py +260 -0
  179. src/services/config/unified_config.py +603 -0
  180. src/services/embedding/__init__.py +45 -0
  181. src/services/embedding/adapters/__init__.py +22 -0
  182. src/services/embedding/adapters/base.py +106 -0
  183. src/services/embedding/adapters/cohere.py +127 -0
  184. src/services/embedding/adapters/jina.py +99 -0
  185. src/services/embedding/adapters/ollama.py +116 -0
  186. src/services/embedding/adapters/openai_compatible.py +96 -0
  187. src/services/embedding/client.py +159 -0
  188. src/services/embedding/config.py +156 -0
  189. src/services/embedding/provider.py +119 -0
  190. src/services/llm/__init__.py +152 -0
  191. src/services/llm/capabilities.py +313 -0
  192. src/services/llm/client.py +302 -0
  193. src/services/llm/cloud_provider.py +530 -0
  194. src/services/llm/config.py +200 -0
  195. src/services/llm/error_mapping.py +103 -0
  196. src/services/llm/exceptions.py +152 -0
  197. src/services/llm/factory.py +450 -0
  198. src/services/llm/local_provider.py +347 -0
  199. src/services/llm/providers/anthropic.py +95 -0
  200. src/services/llm/providers/base_provider.py +93 -0
  201. src/services/llm/providers/open_ai.py +83 -0
  202. src/services/llm/registry.py +71 -0
  203. src/services/llm/telemetry.py +40 -0
  204. src/services/llm/types.py +27 -0
  205. src/services/llm/utils.py +333 -0
  206. src/services/prompt/__init__.py +25 -0
  207. src/services/prompt/manager.py +206 -0
  208. src/services/rag/__init__.py +64 -0
  209. src/services/rag/components/__init__.py +29 -0
  210. src/services/rag/components/base.py +59 -0
  211. src/services/rag/components/chunkers/__init__.py +18 -0
  212. src/services/rag/components/chunkers/base.py +34 -0
  213. src/services/rag/components/chunkers/fixed.py +71 -0
  214. src/services/rag/components/chunkers/numbered_item.py +94 -0
  215. src/services/rag/components/chunkers/semantic.py +97 -0
  216. src/services/rag/components/embedders/__init__.py +14 -0
  217. src/services/rag/components/embedders/base.py +32 -0
  218. src/services/rag/components/embedders/openai.py +63 -0
  219. src/services/rag/components/indexers/__init__.py +18 -0
  220. src/services/rag/components/indexers/base.py +35 -0
  221. src/services/rag/components/indexers/graph.py +172 -0
  222. src/services/rag/components/indexers/lightrag.py +156 -0
  223. src/services/rag/components/indexers/vector.py +146 -0
  224. src/services/rag/components/parsers/__init__.py +18 -0
  225. src/services/rag/components/parsers/base.py +35 -0
  226. src/services/rag/components/parsers/markdown.py +52 -0
  227. src/services/rag/components/parsers/pdf.py +115 -0
  228. src/services/rag/components/parsers/text.py +86 -0
  229. src/services/rag/components/retrievers/__init__.py +18 -0
  230. src/services/rag/components/retrievers/base.py +34 -0
  231. src/services/rag/components/retrievers/dense.py +200 -0
  232. src/services/rag/components/retrievers/hybrid.py +164 -0
  233. src/services/rag/components/retrievers/lightrag.py +169 -0
  234. src/services/rag/components/routing.py +286 -0
  235. src/services/rag/factory.py +234 -0
  236. src/services/rag/pipeline.py +215 -0
  237. src/services/rag/pipelines/__init__.py +32 -0
  238. src/services/rag/pipelines/academic.py +44 -0
  239. src/services/rag/pipelines/lightrag.py +43 -0
  240. src/services/rag/pipelines/llamaindex.py +313 -0
  241. src/services/rag/pipelines/raganything.py +384 -0
  242. src/services/rag/service.py +244 -0
  243. src/services/rag/types.py +73 -0
  244. src/services/search/__init__.py +284 -0
  245. src/services/search/base.py +87 -0
  246. src/services/search/consolidation.py +398 -0
  247. src/services/search/providers/__init__.py +128 -0
  248. src/services/search/providers/baidu.py +188 -0
  249. src/services/search/providers/exa.py +194 -0
  250. src/services/search/providers/jina.py +161 -0
  251. src/services/search/providers/perplexity.py +153 -0
  252. src/services/search/providers/serper.py +209 -0
  253. src/services/search/providers/tavily.py +161 -0
  254. src/services/search/types.py +114 -0
  255. src/services/setup/__init__.py +34 -0
  256. src/services/setup/init.py +285 -0
  257. src/services/tts/__init__.py +16 -0
  258. src/services/tts/config.py +99 -0
  259. src/tools/__init__.py +91 -0
  260. src/tools/code_executor.py +536 -0
  261. src/tools/paper_search_tool.py +171 -0
  262. src/tools/query_item_tool.py +310 -0
  263. src/tools/question/__init__.py +15 -0
  264. src/tools/question/exam_mimic.py +616 -0
  265. src/tools/question/pdf_parser.py +211 -0
  266. src/tools/question/question_extractor.py +397 -0
  267. src/tools/rag_tool.py +173 -0
  268. src/tools/tex_chunker.py +339 -0
  269. src/tools/tex_downloader.py +253 -0
  270. src/tools/web_search.py +71 -0
  271. src/utils/config_manager.py +206 -0
  272. src/utils/document_validator.py +168 -0
  273. src/utils/error_rate_tracker.py +111 -0
  274. src/utils/error_utils.py +82 -0
  275. src/utils/json_parser.py +110 -0
  276. src/utils/network/circuit_breaker.py +79 -0
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ManagerAgent - Manager (Refactored: directly plans step-level solution steps)
5
+ Based on user question and knowledge chain, plans solution steps
6
+ """
7
+
8
+ from pathlib import Path
9
+ import sys
10
+ from typing import Any
11
+
12
+ # Add project root to path
13
+ project_root = Path(__file__).parent.parent.parent.parent
14
+ sys.path.insert(0, str(project_root))
15
+
16
+ from src.agents.base_agent import BaseAgent
17
+
18
+ from ..memory import InvestigateMemory, SolveChainStep, SolveMemory
19
+ from ..utils.json_utils import extract_json_from_text
20
+
21
+
22
+ class ManagerAgent(BaseAgent):
23
+ """Manager Agent - Plans solution steps"""
24
+
25
+ def __init__(
26
+ self,
27
+ config: dict[str, Any],
28
+ api_key: str,
29
+ base_url: str,
30
+ api_version: str | None = None,
31
+ token_tracker=None,
32
+ ):
33
+ language = config.get("system", {}).get("language", "zh")
34
+ super().__init__(
35
+ module_name="solve",
36
+ agent_name="manager_agent",
37
+ api_key=api_key,
38
+ base_url=base_url,
39
+ api_version=api_version,
40
+ language=language,
41
+ config=config,
42
+ token_tracker=token_tracker,
43
+ )
44
+
45
+ async def process(
46
+ self,
47
+ question: str,
48
+ investigate_memory: InvestigateMemory,
49
+ solve_memory: SolveMemory,
50
+ verbose: bool = True,
51
+ ) -> dict[str, Any]:
52
+ """
53
+ Process management workflow - plan solution steps
54
+
55
+ Args:
56
+ question: User question
57
+ investigate_memory: Investigation memory (contains knowledge chain)
58
+ solve_memory: Solve memory
59
+ verbose: Whether to print detailed information
60
+
61
+ Returns:
62
+ dict: Management result
63
+ """
64
+ stage_label = "Plan"
65
+ self.logger.log_stage_progress(
66
+ stage_label, "start", f"question={question[:60]}{'...' if len(question) > 60 else ''}"
67
+ )
68
+
69
+ # 1. Check if steps already exist
70
+ if solve_memory.solve_chains:
71
+ steps_count = len(solve_memory.solve_chains)
72
+ self.logger.log_stage_progress(stage_label, "skip", f"Already has {steps_count} steps")
73
+ return {
74
+ "has_steps": True,
75
+ "steps_count": steps_count,
76
+ "num_steps": steps_count, # Maintain compatibility
77
+ "message": "Steps already exist, skipping planning",
78
+ }
79
+
80
+ # 2. Build context
81
+ context = self._build_context(question=question, investigate_memory=investigate_memory)
82
+
83
+ # 3. Build Prompt
84
+ system_prompt = self._build_system_prompt()
85
+ user_prompt = self._build_user_prompt(context)
86
+
87
+ # 4. Call LLM (requires JSON format output)
88
+ response = await self.call_llm(
89
+ user_prompt=user_prompt,
90
+ system_prompt=system_prompt,
91
+ verbose=verbose,
92
+ stage=stage_label,
93
+ response_format={"type": "json_object"}, # Force JSON
94
+ )
95
+
96
+ # 5. Parse output and create StepItem
97
+ steps = self._parse_response(response, investigate_memory)
98
+
99
+ # 6. Add steps to solve_memory
100
+ solve_memory.create_chains(steps)
101
+ solve_memory.save()
102
+
103
+ steps_count = len(steps)
104
+ self.logger.log_stage_progress(stage_label, "complete", f"Generated {steps_count} steps")
105
+ return {
106
+ "has_steps": True,
107
+ "steps_count": steps_count,
108
+ "num_steps": steps_count, # Maintain compatibility
109
+ "message": f"Generated {steps_count} steps",
110
+ }
111
+
112
+ def _build_context(
113
+ self, question: str, investigate_memory: InvestigateMemory
114
+ ) -> dict[str, Any]:
115
+ """Build context"""
116
+ # Get knowledge chain information (cite_id + summary)
117
+ knowledge_info = []
118
+ for knowledge in investigate_memory.knowledge_chain:
119
+ if knowledge.summary: # Only use knowledge with summary
120
+ knowledge_info.append(
121
+ {
122
+ "cite_id": knowledge.cite_id,
123
+ "tool_type": knowledge.tool_type,
124
+ "query": knowledge.query,
125
+ "summary": knowledge.summary,
126
+ }
127
+ )
128
+
129
+ knowledge_text = ""
130
+ for info in knowledge_info:
131
+ knowledge_text += f"\n{info['cite_id']} [{info['tool_type']}]\n"
132
+ knowledge_text += f" Query: {info['query']}\n"
133
+ knowledge_text += f" Summary: {info['summary']}\n"
134
+
135
+ remaining_questions = []
136
+ if investigate_memory and getattr(investigate_memory, "reflections", None):
137
+ remaining_questions = investigate_memory.reflections.remaining_questions or []
138
+
139
+ reflections_summary = (
140
+ "\n".join(f"- {q}" for q in remaining_questions)
141
+ if remaining_questions
142
+ else "(No remaining questions)"
143
+ )
144
+
145
+ knowledge_summary_text = knowledge_text if knowledge_text else "(No research information)"
146
+
147
+ return {
148
+ "question": question,
149
+ "knowledge_info": knowledge_info,
150
+ "knowledge_text": knowledge_summary_text,
151
+ "knowledge_chain_summary": knowledge_summary_text,
152
+ "reflections_summary": reflections_summary,
153
+ }
154
+
155
+ def _build_system_prompt(self) -> str:
156
+ """Build system prompt"""
157
+ prompt = self.get_prompt("system") if self.has_prompts() else None
158
+ if not prompt:
159
+ raise ValueError(
160
+ "ManagerAgent missing system prompt, please configure system section in prompts/zh/solve_loop/manager_agent.yaml."
161
+ )
162
+ return prompt
163
+
164
+ def _build_user_prompt(self, context: dict[str, Any]) -> str:
165
+ """Build user prompt"""
166
+ template = self.get_prompt("user_template") if self.has_prompts() else None
167
+ if not template:
168
+ raise ValueError(
169
+ "ManagerAgent missing user prompt template, please configure user_template in prompts/zh/solve_loop/manager_agent.yaml."
170
+ )
171
+ return template.format(**context)
172
+
173
+ def _parse_response(
174
+ self, response: str, investigate_memory: InvestigateMemory
175
+ ) -> list[SolveChainStep]:
176
+ """Parse LLM output (JSON format), create solve-chain steps"""
177
+ steps: list[SolveChainStep] = []
178
+ knowledge_ids = {k.cite_id for k in investigate_memory.knowledge_chain}
179
+
180
+ # Use json_utils to extract JSON
181
+ parsed_data = extract_json_from_text(response)
182
+
183
+ if not parsed_data or not isinstance(parsed_data, dict):
184
+ raise ValueError(
185
+ f"Failed to parse valid JSON object from LLM output. Original output: {response[:200]}..."
186
+ )
187
+
188
+ steps_data = parsed_data.get("steps", [])
189
+ if not isinstance(steps_data, list):
190
+ raise ValueError(f"'steps' field in JSON is not an array. Parsed result: {parsed_data}")
191
+
192
+ if not steps_data:
193
+ raise ValueError("'steps' array in JSON is empty, please check LLM output")
194
+
195
+ # Parse each step
196
+ for idx, step_data in enumerate(steps_data, 1):
197
+ if not isinstance(step_data, dict):
198
+ self.logger.warning(
199
+ f"[ManagerAgent] Skipping invalid step data (index {idx}): {step_data}"
200
+ )
201
+ continue
202
+
203
+ # Get step_id
204
+ step_id = step_data.get("step_id", "").strip()
205
+ if not step_id:
206
+ step_id = f"S{idx}"
207
+ elif not step_id.upper().startswith("S"):
208
+ step_id = f"S{step_id}"
209
+
210
+ # Get role and target
211
+ role = step_data.get("role", "").strip()
212
+ target = step_data.get("target", "").strip()
213
+
214
+ # If target already contains role, use directly; otherwise combine
215
+ if target:
216
+ if ":" in target or ":" in target:
217
+ step_target = target
218
+ elif role:
219
+ step_target = f"{role}:{target}"
220
+ else:
221
+ step_target = target
222
+ else:
223
+ raise ValueError(f"Step {step_id} missing 'target' field")
224
+
225
+ # Get cite_ids
226
+ cite_ids_raw = step_data.get("cite_ids", [])
227
+ if not isinstance(cite_ids_raw, list):
228
+ # Compatible with string format
229
+ if isinstance(cite_ids_raw, str):
230
+ cite_ids_raw = [cite_ids_raw] if cite_ids_raw and cite_ids_raw != "none" else []
231
+ else:
232
+ cite_ids_raw = []
233
+
234
+ # Clean and normalize cite_ids
235
+ filtered_cites = []
236
+ for cite in cite_ids_raw:
237
+ if not cite or cite == "none":
238
+ continue
239
+ # Ensure format is [xxx]
240
+ cleaned = str(cite).strip()
241
+ if not cleaned.startswith("["):
242
+ cleaned = f"[{cleaned.strip('[] ')}]"
243
+
244
+ # Filter invalid cite
245
+ if cleaned in knowledge_ids:
246
+ filtered_cites.append(cleaned)
247
+ else:
248
+ self.logger.warning(
249
+ f"[ManagerAgent] Skipping unknown cite_id {cleaned} (not in knowledge chain)"
250
+ )
251
+
252
+ # Create step
253
+ steps.append(
254
+ SolveChainStep(
255
+ step_id=step_id,
256
+ step_target=step_target,
257
+ available_cite=list(dict.fromkeys(filtered_cites)), # Remove duplicates
258
+ status="undone",
259
+ )
260
+ )
261
+
262
+ if not steps:
263
+ raise ValueError("Failed to parse any valid steps, please check LLM output format")
264
+
265
+ logger = getattr(self, "logger", None)
266
+ if logger is not None:
267
+ logger.info(f"[ManagerAgent._parse_response] Parsed {len(steps)} solve-chain steps")
268
+ for step in steps:
269
+ logger.info(f" - {step.step_id}: {step.step_target}")
270
+ logger.info(
271
+ f" Available citations: {', '.join(step.available_cite) or '(none)'}"
272
+ )
273
+
274
+ return steps
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrecisionAnswerAgent - Precision answer generator (two-stage)
5
+ """
6
+
7
+ from pathlib import Path
8
+ import sys
9
+ from typing import Any, Dict
10
+
11
+ project_root = Path(__file__).parent.parent.parent.parent
12
+ if str(project_root) not in sys.path:
13
+ sys.path.insert(0, str(project_root))
14
+
15
+ from src.agents.base_agent import BaseAgent
16
+
17
+
18
+ class PrecisionAnswerAgent(BaseAgent):
19
+ """Staged precision answer"""
20
+
21
+ def __init__(
22
+ self,
23
+ config: Dict[str, Any],
24
+ api_key: str,
25
+ base_url: str,
26
+ api_version: str | None = None,
27
+ token_tracker=None,
28
+ ):
29
+ language = config.get("system", {}).get("language", "zh")
30
+ super().__init__(
31
+ module_name="solve",
32
+ agent_name="precision_answer_agent",
33
+ api_key=api_key,
34
+ base_url=base_url,
35
+ api_version=api_version,
36
+ language=language,
37
+ config=config,
38
+ token_tracker=token_tracker,
39
+ )
40
+
41
+ async def process(
42
+ self, question: str, detailed_answer: str, verbose: bool = True
43
+ ) -> Dict[str, Any]:
44
+ decision = await self._should_generate(question, verbose)
45
+ if not decision["needs_precision"]:
46
+ return {
47
+ "needs_precision": False,
48
+ "precision_answer": "",
49
+ "final_answer": detailed_answer,
50
+ }
51
+
52
+ precision_answer = await self._generate_precision_answer(
53
+ question=question, detailed_answer=detailed_answer, verbose=verbose
54
+ )
55
+ return {
56
+ "needs_precision": True,
57
+ "precision_answer": precision_answer,
58
+ "final_answer": detailed_answer,
59
+ }
60
+
61
+ async def _should_generate(self, question: str, verbose: bool) -> Dict[str, Any]:
62
+ system_prompt = self.get_prompt("decision_system") if self.has_prompts() else None
63
+ if not system_prompt:
64
+ raise ValueError(
65
+ "PrecisionAnswerAgent missing decision_system prompt, please configure decision_system in prompts/{lang}/solve_loop/precision_answer_agent.yaml"
66
+ )
67
+ template = self.get_prompt("decision_user_template") if self.has_prompts() else None
68
+ if not template:
69
+ raise ValueError(
70
+ "PrecisionAnswerAgent missing decision_user_template, please configure decision_user_template in prompts/{lang}/solve_loop/precision_answer_agent.yaml"
71
+ )
72
+ user_prompt = template.format(question=question)
73
+ response = await self.call_llm(
74
+ user_prompt=user_prompt, system_prompt=system_prompt, verbose=verbose
75
+ )
76
+ needs_precision = response.strip().upper().startswith("Y")
77
+ return {"needs_precision": needs_precision, "raw_decision": response.strip()}
78
+
79
+ async def _generate_precision_answer(
80
+ self, question: str, detailed_answer: str, verbose: bool
81
+ ) -> str:
82
+ system_prompt = self.get_prompt("precision_system") if self.has_prompts() else None
83
+ if not system_prompt:
84
+ raise ValueError(
85
+ "PrecisionAnswerAgent missing precision_system prompt, please configure precision_system in prompts/{lang}/solve_loop/precision_answer_agent.yaml"
86
+ )
87
+ template = self.get_prompt("precision_user_template") if self.has_prompts() else None
88
+ if not template:
89
+ raise ValueError(
90
+ "PrecisionAnswerAgent missing precision_user_template, please configure precision_user_template in prompts/{lang}/solve_loop/precision_answer_agent.yaml"
91
+ )
92
+ user_prompt = template.format(question=question, detailed_answer=detailed_answer)
93
+ response = await self.call_llm(
94
+ user_prompt=user_prompt, system_prompt=system_prompt, verbose=verbose
95
+ )
96
+ return response.strip()
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ResponseAgent - Step response generator
5
+ Based on materials in solve-chain, generates formal response for current step
6
+ """
7
+
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ from typing import Any
12
+
13
+ project_root = Path(__file__).parent.parent.parent.parent
14
+ if str(project_root) not in sys.path:
15
+ sys.path.insert(0, str(project_root))
16
+
17
+ from src.agents.base_agent import BaseAgent
18
+
19
+ from ..memory import CitationMemory, InvestigateMemory, SolveChainStep, SolveMemory
20
+
21
+
22
+ class ResponseAgent(BaseAgent):
23
+ """Response generator Agent"""
24
+
25
+ def __init__(
26
+ self,
27
+ config: dict[str, Any],
28
+ api_key: str,
29
+ base_url: str,
30
+ api_version: str | None = None,
31
+ token_tracker=None,
32
+ ):
33
+ language = config.get("system", {}).get("language", "zh")
34
+ super().__init__(
35
+ module_name="solve",
36
+ agent_name="response_agent",
37
+ api_key=api_key,
38
+ base_url=base_url,
39
+ api_version=api_version,
40
+ language=language,
41
+ config=config,
42
+ token_tracker=token_tracker,
43
+ )
44
+ # Store citation configuration
45
+ self.enable_citations = config.get("system", {}).get("enable_citations", True)
46
+
47
+ async def process(
48
+ self,
49
+ question: str,
50
+ step: SolveChainStep,
51
+ solve_memory: SolveMemory,
52
+ investigate_memory: InvestigateMemory,
53
+ citation_memory: CitationMemory,
54
+ output_dir: str | None = None,
55
+ verbose: bool = True,
56
+ accumulated_response: str = "",
57
+ ) -> dict[str, Any]:
58
+ if not step:
59
+ return {"step_response": "(No pending step)"}
60
+
61
+ context = self._build_context(
62
+ question=question,
63
+ step=step,
64
+ solve_memory=solve_memory,
65
+ investigate_memory=investigate_memory,
66
+ citation_memory=citation_memory,
67
+ output_dir=output_dir,
68
+ accumulated_response=accumulated_response,
69
+ )
70
+
71
+ system_prompt = self._build_system_prompt(context["image_materials"])
72
+ user_prompt = self._build_user_prompt(context)
73
+
74
+ response = await self.call_llm(
75
+ user_prompt=user_prompt, system_prompt=system_prompt, verbose=verbose
76
+ )
77
+
78
+ # Directly use LLM's raw output as step_response, no parsing
79
+ step_response = response.strip() if response else ""
80
+ used_citations = self._extract_used_citations(step_response, step)
81
+ solve_memory.submit_step_response(
82
+ step_id=step.step_id, response=step_response, used_citations=used_citations
83
+ )
84
+ solve_memory.save()
85
+
86
+ return {
87
+ "step_id": step.step_id,
88
+ "step_response": step_response,
89
+ "used_citations": used_citations,
90
+ "raw_response": response,
91
+ }
92
+
93
+ # ------------------------------------------------------------------ #
94
+ # Prompt Building
95
+ # ------------------------------------------------------------------ #
96
+ def _build_context(
97
+ self,
98
+ question: str,
99
+ step: SolveChainStep,
100
+ solve_memory: SolveMemory,
101
+ investigate_memory: InvestigateMemory,
102
+ citation_memory: CitationMemory,
103
+ output_dir: str | None,
104
+ accumulated_response: str = "",
105
+ ) -> dict[str, Any]:
106
+ available_cite_details = self._format_available_cite(step, investigate_memory)
107
+ tool_materials, image_materials = self._format_tool_materials(step, output_dir)
108
+ citation_details = self._format_citation_details(step, citation_memory)
109
+
110
+ return {
111
+ "question": question,
112
+ "step_id": step.step_id,
113
+ "step_target": step.step_target,
114
+ "available_cite_details": available_cite_details,
115
+ "tool_materials": tool_materials,
116
+ "citation_details": citation_details,
117
+ "image_materials": image_materials,
118
+ "previous_context": accumulated_response
119
+ or "(No previous content, this is the first step)",
120
+ }
121
+
122
+ def _build_system_prompt(self, image_materials: list[str]) -> str:
123
+ base_prompt = self.get_prompt("system") if self.has_prompts() else None
124
+ if not base_prompt:
125
+ raise ValueError(
126
+ "ResponseAgent missing system prompt, please configure in prompts/zh/solve_loop/response_agent.yaml."
127
+ )
128
+
129
+ # Add citation disable instruction if citations are disabled
130
+ citation_instruction = ""
131
+ if not self.enable_citations:
132
+ citation_instruction_yaml = self.get_prompt("citation_instruction_disabled")
133
+ if citation_instruction_yaml:
134
+ citation_instruction = citation_instruction_yaml
135
+ else:
136
+ citation_instruction = "\n\n**Important: Citation Feature Disabled**\n"
137
+
138
+ if image_materials:
139
+ image_list = "\n".join([f" - {img}" for img in image_materials])
140
+ image_instruction_template = self.get_prompt("image_instruction")
141
+ if image_instruction_template:
142
+ image_instruction = image_instruction_template.format(image_list=image_list)
143
+ else:
144
+ image_instruction = f"\n\n**Image files to insert**:\n{image_list}\n"
145
+ return base_prompt + citation_instruction + image_instruction
146
+
147
+ return base_prompt + citation_instruction
148
+
149
+ def _build_user_prompt(self, context: dict[str, Any]) -> str:
150
+ template = self.get_prompt("user_template") if self.has_prompts() else None
151
+ if not template:
152
+ raise ValueError(
153
+ "ResponseAgent missing user_template, please configure user_template in prompts/{lang}/solve_loop/response_agent.yaml"
154
+ )
155
+
156
+ # Format image_materials as clear text list
157
+ image_materials = context.get("image_materials", [])
158
+ if image_materials:
159
+ image_text = "\n".join([f"- {img}" for img in image_materials])
160
+ else:
161
+ image_text = "(No image files)"
162
+
163
+ # Create new context, format image_materials as text
164
+ formatted_context = context.copy()
165
+ formatted_context["image_materials"] = image_text
166
+
167
+ return template.format(**formatted_context)
168
+
169
+ # ------------------------------------------------------------------ #
170
+ # Material Organization
171
+ # ------------------------------------------------------------------ #
172
+ def _format_available_cite(
173
+ self, step: SolveChainStep, investigate_memory: InvestigateMemory
174
+ ) -> str:
175
+ if not step.available_cite:
176
+ return "(No available knowledge chain)"
177
+ lines: list[str] = []
178
+ for cite in step.available_cite:
179
+ knowledge = next(
180
+ (k for k in investigate_memory.knowledge_chain if k.cite_id == cite), None
181
+ )
182
+ if not knowledge:
183
+ continue
184
+ summary = knowledge.summary or knowledge.raw_result[:300]
185
+ lines.append(
186
+ f"{cite} [{knowledge.tool_type}]\n"
187
+ f" Query: {knowledge.query}\n"
188
+ f" Summary: {summary}\n"
189
+ f" Raw: {knowledge.raw_result[:300]}..."
190
+ )
191
+ return "\n".join(lines) if lines else "(No matching knowledge)"
192
+
193
+ def _format_tool_materials(
194
+ self, step: SolveChainStep, output_dir: str | None
195
+ ) -> tuple[str, list[str]]:
196
+ if not step.tool_calls:
197
+ return "(No tool calls yet)", []
198
+
199
+ lines: list[str] = []
200
+ images: list[str] = []
201
+ seen_images: set[str] = set()
202
+
203
+ def _append_image(path_str: str):
204
+ normalized = str(path_str).replace("\\", "/")
205
+ if normalized and normalized not in seen_images:
206
+ images.append(normalized)
207
+ seen_images.add(normalized)
208
+
209
+ for call in step.tool_calls:
210
+ summary = call.summary or "(Summary pending)"
211
+ raw_preview = (call.raw_answer or "")[:500]
212
+ lines.append(
213
+ f"{call.tool_type} | cite_id={call.cite_id} | Status={call.status}\n"
214
+ f"Query: {call.query}\n"
215
+ f"Summary: {summary}\n"
216
+ f"Raw excerpt: {raw_preview}"
217
+ )
218
+ # Priority: use recorded relative paths, then absolute paths, finally fall back to original artifacts list
219
+ artifact_rel_paths = call.metadata.get("artifact_rel_paths") if call.metadata else None
220
+ artifact_paths = call.metadata.get("artifact_paths") if call.metadata else None
221
+ artifacts = call.metadata.get("artifacts") if call.metadata else None
222
+
223
+ if artifact_rel_paths:
224
+ for rel_path in artifact_rel_paths:
225
+ _append_image(rel_path)
226
+
227
+ if artifact_paths:
228
+ # Use absolute paths, but convert to relative paths relative to output_dir for display
229
+ for abs_path in artifact_paths:
230
+ path = Path(abs_path)
231
+ if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp"}:
232
+ # If output_dir exists, calculate relative path; otherwise use absolute path
233
+ if output_dir:
234
+ try:
235
+ rel_path = path.relative_to(Path(output_dir))
236
+ _append_image(str(rel_path))
237
+ except ValueError:
238
+ # If cannot calculate relative path, use filename
239
+ _append_image(path.name)
240
+ else:
241
+ _append_image(path.name)
242
+ elif artifacts:
243
+ # Fall back to using artifacts list
244
+ for artifact in artifacts:
245
+ path = Path(artifact)
246
+ if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp"}:
247
+ # Build relative path to artifacts directory
248
+ rel_path = Path("artifacts") / path.name
249
+ _append_image(str(rel_path))
250
+ return "\n\n".join(lines), images
251
+
252
+ def _format_citation_details(
253
+ self, step: SolveChainStep, citation_memory: CitationMemory
254
+ ) -> str:
255
+ # If citations are disabled, return empty string
256
+ if not self.enable_citations:
257
+ return "(Citations disabled)"
258
+
259
+ cite_ids = list(
260
+ dict.fromkeys(
261
+ step.available_cite + [tc.cite_id for tc in step.tool_calls if tc.cite_id]
262
+ )
263
+ )
264
+ if not cite_ids:
265
+ return "(No citations)"
266
+ lines: list[str] = []
267
+ for cite_id in cite_ids:
268
+ citation = citation_memory.get_citation(cite_id)
269
+ if not citation:
270
+ continue
271
+ summary = citation.content or citation.raw_result[:200]
272
+ lines.append(f"- {cite_id} [{citation.tool_type}] Query: {citation.query}")
273
+ if summary:
274
+ lines.append(f" Summary: {summary[:300]}")
275
+ return "\n".join(lines) if lines else "(Citation information missing)"
276
+
277
+ # ------------------------------------------------------------------ #
278
+ # Citation Extraction
279
+ # ------------------------------------------------------------------ #
280
+ def _extract_used_citations(self, content: str, step: SolveChainStep) -> list[str]:
281
+ # If citations are disabled, return empty list
282
+ if not self.enable_citations:
283
+ return []
284
+
285
+ if not content:
286
+ return []
287
+ # Allow standard English brackets [cite], also tolerate some error formats (like Chinese full-width brackets【cite】), uniformly normalize to [cite]
288
+ pattern = re.compile(r"\[([^\]\[]+)\](?!\()|【([^】\[]+)】")
289
+ matches = pattern.findall(content)
290
+ normalized: list[str] = []
291
+ for match in matches:
292
+ candidate = match[0] or match[1]
293
+ if not candidate:
294
+ continue
295
+ normalized.append(f"[{candidate.strip()}]")
296
+ allowed = set(step.available_cite + [tc.cite_id for tc in step.tool_calls if tc.cite_id])
297
+ ordered: list[str] = []
298
+ for cite in normalized:
299
+ if cite in allowed and cite not in ordered:
300
+ ordered.append(cite)
301
+ return ordered