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,470 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ToolAgent - Tool executor
5
+ Responsible for reading tool calls in solve-chain, actually executing tools and producing summary
6
+ """
7
+
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ import time
12
+ from typing import Any
13
+
14
+ project_root = Path(__file__).parent.parent.parent.parent
15
+ if str(project_root) not in sys.path:
16
+ sys.path.insert(0, str(project_root))
17
+
18
+ from src.agents.base_agent import BaseAgent
19
+ from src.tools.code_executor import run_code
20
+ from src.tools.rag_tool import rag_search
21
+ from src.tools.web_search import web_search
22
+
23
+ from ..memory import CitationMemory, SolveChainStep, SolveMemory
24
+ from ..memory.solve_memory import ToolCallRecord
25
+
26
+
27
+ class ToolAgent(BaseAgent):
28
+ """Execute tool calls and generate summary"""
29
+
30
+ def __init__(
31
+ self,
32
+ config: dict[str, Any],
33
+ api_key: str,
34
+ base_url: str,
35
+ api_version: str | None = None,
36
+ token_tracker=None,
37
+ ):
38
+ language = config.get("system", {}).get("language", "zh")
39
+ super().__init__(
40
+ module_name="solve",
41
+ agent_name="tool_agent",
42
+ api_key=api_key,
43
+ base_url=base_url,
44
+ api_version=api_version,
45
+ language=language,
46
+ config=config,
47
+ token_tracker=token_tracker,
48
+ )
49
+
50
+ async def _generate_code_from_intent(self, intent: str) -> str:
51
+ system_prompt = """
52
+ You are a Python code generator.
53
+ Generate ONLY executable Python code.
54
+ Do NOT include explanations.
55
+ Do NOT include markdown fences.
56
+ Do NOT include comments unless necessary.
57
+ The code must be self-contained and runnable.
58
+ """
59
+ user_prompt = f"""
60
+ Task:
61
+ {intent}
62
+
63
+ Rules:
64
+ - Output only Python code
65
+ - No ``` fences
66
+ - No natural language
67
+ """
68
+ code = await self.call_llm(
69
+ system_prompt=system_prompt,
70
+ user_prompt=user_prompt,
71
+ verbose=False,
72
+ )
73
+ if "```" in code:
74
+ raise ValueError("LLM returned markdown code fences, which is forbidden")
75
+ if len(code) > 8000:
76
+ raise ValueError("Generated code too large")
77
+
78
+ return code.strip()
79
+
80
+ async def process(
81
+ self,
82
+ step: SolveChainStep,
83
+ solve_memory: SolveMemory,
84
+ citation_memory: CitationMemory,
85
+ kb_name: str,
86
+ output_dir: str | None = None,
87
+ verbose: bool = True,
88
+ ) -> dict[str, Any]:
89
+ pending = [
90
+ call
91
+ for call in step.tool_calls
92
+ if call.tool_type not in {"none", "finish"} and call.status in {"pending", "running"}
93
+ ]
94
+
95
+ if not pending:
96
+ return {"step_id": step.step_id, "executed": [], "status": "idle"}
97
+
98
+ logs: list[dict[str, Any]] = []
99
+ base_dir = Path(output_dir).resolve() if output_dir else Path().resolve()
100
+ artifacts_dir = base_dir / "artifacts"
101
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
102
+
103
+ self.logger.log_stage_progress(
104
+ "Tool", "start", f"step={step.step_id}, pending_calls={len(pending)}"
105
+ )
106
+
107
+ for record in pending:
108
+ call_label = f"{record.tool_type} | cite={record.cite_id or '-'}"
109
+ self.logger.log_stage_progress(
110
+ "Tool", "running", f"step={step.step_id}, call={call_label}"
111
+ )
112
+ start_ts = time.time()
113
+ try:
114
+ raw_answer, metadata = await self._execute_single_call(
115
+ record=record,
116
+ kb_name=kb_name,
117
+ output_dir=output_dir,
118
+ artifacts_dir=str(artifacts_dir),
119
+ verbose=verbose,
120
+ )
121
+
122
+ # Check if code execution failed
123
+ is_failed = False
124
+ if record.tool_type == "code_execution":
125
+ is_failed = metadata.get("execution_failed", False)
126
+ exit_code = metadata.get("exit_code", 0)
127
+ if exit_code != 0:
128
+ is_failed = True
129
+
130
+ summary = await self._summarize_tool_result(
131
+ tool_type=record.tool_type, query=record.query, raw_answer=raw_answer
132
+ )
133
+
134
+ # Set correct status based on execution result
135
+ status = "failed" if is_failed else "success"
136
+ solve_memory.update_tool_call_result(
137
+ step_id=step.step_id,
138
+ call_id=record.call_id,
139
+ raw_answer=raw_answer,
140
+ summary=summary,
141
+ status=status,
142
+ metadata=metadata, # Pass metadata to ensure artifacts are saved
143
+ )
144
+ citation_memory.update_citation(
145
+ cite_id=record.cite_id,
146
+ raw_result=raw_answer,
147
+ content=summary,
148
+ metadata=metadata,
149
+ step_id=step.step_id,
150
+ )
151
+ elapsed_ms = (time.time() - start_ts) * 1000
152
+ self.logger.log_tool_call(
153
+ tool_name=record.tool_type,
154
+ tool_input={
155
+ "step_id": step.step_id,
156
+ "call_id": record.call_id,
157
+ "query": record.query,
158
+ },
159
+ tool_output=raw_answer,
160
+ status="success",
161
+ elapsed_ms=elapsed_ms,
162
+ step_id=step.step_id,
163
+ cite_id=record.cite_id,
164
+ )
165
+ logs.append(
166
+ {
167
+ "call_id": record.call_id,
168
+ "tool_type": record.tool_type,
169
+ "cite_id": record.cite_id,
170
+ "status": "success",
171
+ "summary": summary,
172
+ }
173
+ )
174
+ except Exception as e:
175
+ error_msg = str(e)
176
+ elapsed_ms = (time.time() - start_ts) * 1000
177
+ solve_memory.update_tool_call_result(
178
+ step_id=step.step_id,
179
+ call_id=record.call_id,
180
+ raw_answer=error_msg,
181
+ summary=error_msg[:200],
182
+ status="failed",
183
+ metadata={"error": True},
184
+ )
185
+ citation_memory.update_citation(
186
+ cite_id=record.cite_id,
187
+ raw_result=error_msg,
188
+ content=error_msg[:200],
189
+ metadata={"error": True},
190
+ step_id=step.step_id,
191
+ )
192
+ self.logger.log_tool_call(
193
+ tool_name=record.tool_type,
194
+ tool_input={
195
+ "step_id": step.step_id,
196
+ "call_id": record.call_id,
197
+ "query": record.query,
198
+ },
199
+ tool_output=error_msg,
200
+ status="failed",
201
+ elapsed_ms=elapsed_ms,
202
+ step_id=step.step_id,
203
+ cite_id=record.cite_id,
204
+ )
205
+ self.logger.log_stage_progress(
206
+ "Tool", "warning", f"step={step.step_id}, call={call_label}, error={error_msg}"
207
+ )
208
+ logs.append(
209
+ {
210
+ "call_id": record.call_id,
211
+ "tool_type": record.tool_type,
212
+ "cite_id": record.cite_id,
213
+ "status": "failed",
214
+ "error": error_msg,
215
+ }
216
+ )
217
+
218
+ solve_memory.save()
219
+ citation_memory.save()
220
+
221
+ self.logger.log_stage_progress(
222
+ "Tool", "complete", f"step={step.step_id}, executed={len(logs)}"
223
+ )
224
+
225
+ return {"step_id": step.step_id, "executed": logs, "status": "completed"}
226
+
227
+ async def _execute_single_call(
228
+ self,
229
+ record: ToolCallRecord,
230
+ kb_name: str,
231
+ output_dir: str | None,
232
+ artifacts_dir: str,
233
+ verbose: bool,
234
+ ) -> tuple[str, dict[str, Any]]:
235
+ tool_type = record.tool_type
236
+ query = record.query
237
+
238
+ if tool_type == "rag_naive":
239
+ result = await rag_search(query=query, kb_name=kb_name, mode="naive")
240
+ answer = result.get("answer", "")
241
+ source, auto_sources = self._infer_sources(answer)
242
+ metadata = {"source": source, "auto_sources": auto_sources, "mode": "naive"}
243
+ return answer, metadata
244
+
245
+ if tool_type == "rag_hybrid":
246
+ result = await rag_search(query=query, kb_name=kb_name, mode="hybrid")
247
+ answer = result.get("answer", "")
248
+ source, auto_sources = self._infer_sources(answer)
249
+ metadata = {"source": source, "auto_sources": auto_sources, "mode": "hybrid"}
250
+ return answer, metadata
251
+
252
+ if tool_type == "web_search":
253
+ result = web_search(query=query, output_dir=output_dir, verbose=verbose)
254
+ answer = result.get("answer") or result.get("summary") or ""
255
+ used_citation_ids = self._extract_answer_citations(answer)
256
+ filtered_citations = self._select_web_citations(used_citation_ids, result)
257
+ metadata = {"result_file": result.get("result_file"), "citations": filtered_citations}
258
+ return answer, metadata
259
+
260
+ if tool_type == "code_execution":
261
+ artifacts_path = Path(artifacts_dir)
262
+ before_snapshot = self._snapshot_image_artifacts(artifacts_path)
263
+
264
+ if not query or not query.strip():
265
+ # If code is empty, directly return failure without execution
266
+ raw_answer = "【⚠️ Code execution failed】\nError: No valid code input received (Code is empty). Please check if [QUERY] contains a markdown code block."
267
+ metadata = {
268
+ "exit_code": 1,
269
+ "artifacts": [],
270
+ "artifact_paths": [],
271
+ "artifact_rel_paths": [],
272
+ "work_dir": artifacts_dir,
273
+ "execution_failed": True,
274
+ }
275
+ return raw_answer, metadata
276
+
277
+ code = await self._generate_code_from_intent(query)
278
+
279
+ exec_result = await run_code(
280
+ language="python",
281
+ code=code,
282
+ timeout=self.agent_config.get("code_timeout", 20),
283
+ assets_dir=artifacts_dir,
284
+ )
285
+ raw_answer = self._format_code_answer(exec_result, artifacts_dir)
286
+ exit_code = exec_result.get("exit_code", 0)
287
+
288
+ # Check if code execution failed
289
+ is_failed = exit_code != 0
290
+ if is_failed:
291
+ stderr = exec_result.get("stderr", "")
292
+ # Add obvious error prefix at the beginning of raw_answer
293
+ error_prefix = "【⚠️ Code execution failed】\n"
294
+ if "FileNotFoundError" in stderr and "artifacts/" in stderr:
295
+ error_prefix += "Path error detected: Code uses 'artifacts/xxx.png', but working directory is already the artifacts directory.\n"
296
+ error_prefix += "Please use 'xxx.png' instead of 'artifacts/xxx.png'.\n\n"
297
+ raw_answer = error_prefix + raw_answer
298
+
299
+ new_image_paths = self._collect_new_image_artifacts(
300
+ artifacts_path=artifacts_path,
301
+ before_snapshot=before_snapshot,
302
+ output_dir=output_dir,
303
+ )
304
+
305
+ metadata = {
306
+ "exit_code": exit_code,
307
+ "artifacts": exec_result.get("artifacts", []),
308
+ "artifact_paths": exec_result.get("artifact_paths", []),
309
+ "artifact_rel_paths": new_image_paths,
310
+ "work_dir": artifacts_dir,
311
+ "execution_failed": is_failed,
312
+ }
313
+ return raw_answer, metadata
314
+
315
+ raise ValueError(f"Unknown tool type: {tool_type}")
316
+
317
+ def _format_code_answer(self, exec_result: dict[str, Any], artifacts_dir: str) -> str:
318
+ stdout = exec_result.get("stdout", "")
319
+ stderr = exec_result.get("stderr", "")
320
+ artifacts = exec_result.get("artifacts", [])
321
+ artifact_paths = exec_result.get("artifact_paths", [])
322
+
323
+ lines = [
324
+ "【Code Execution Result】",
325
+ f"Exit code: {exec_result.get('exit_code')}",
326
+ f"Elapsed time: {exec_result.get('elapsed_ms', 0):.2f} ms",
327
+ f"Working directory: {artifacts_dir}",
328
+ "",
329
+ "stdout:",
330
+ stdout or "(empty)",
331
+ "",
332
+ "stderr:",
333
+ stderr or "(empty)",
334
+ ]
335
+
336
+ if artifacts:
337
+ lines.append("")
338
+ lines.append("Artifacts:")
339
+ for idx, artifact in enumerate(artifacts):
340
+ abs_path = (
341
+ artifact_paths[idx]
342
+ if idx < len(artifact_paths)
343
+ else str(Path(artifacts_dir) / artifact)
344
+ )
345
+ lines.append(f"- {abs_path}")
346
+
347
+ return "\n".join(lines)
348
+
349
+ async def _summarize_tool_result(self, tool_type: str, query: str, raw_answer: str) -> str:
350
+ system_prompt = self.get_prompt("system") if self.has_prompts() else None
351
+ if not system_prompt:
352
+ raise ValueError(
353
+ "ToolAgent missing system prompt, please configure system in prompts/{lang}/solve_loop/tool_agent.yaml"
354
+ )
355
+
356
+ template = self.get_prompt("user_template") if self.has_prompts() else None
357
+ if not template:
358
+ raise ValueError(
359
+ "ToolAgent missing user_template, please configure user_template in prompts/{lang}/solve_loop/tool_agent.yaml"
360
+ )
361
+
362
+ user_prompt = template.format(
363
+ tool_type=tool_type, query=query, raw_answer=raw_answer[:2000]
364
+ )
365
+
366
+ response = await self.call_llm(
367
+ user_prompt=user_prompt, system_prompt=system_prompt, verbose=False
368
+ )
369
+ return response.strip()
370
+
371
+ def _infer_sources(self, text: str) -> tuple[str, list[str]]:
372
+ if not text:
373
+ return "", []
374
+ matches = re.findall(r"(https?://[^\s\)\]]+)", text)
375
+ cleaned = []
376
+ for item in matches:
377
+ normalized = item.strip().strip(".,;:()[]{}")
378
+ if normalized and normalized not in cleaned:
379
+ cleaned.append(normalized)
380
+ return (", ".join(cleaned), cleaned)
381
+
382
+ def _extract_answer_citations(self, answer: str) -> list[str]:
383
+ if not answer:
384
+ return []
385
+ pattern = re.compile(r"\[(\d+)\]")
386
+ ids = pattern.findall(answer)
387
+ unique: list[str] = []
388
+ for cid in ids:
389
+ if cid not in unique:
390
+ unique.append(cid)
391
+ return unique
392
+
393
+ def _select_web_citations(
394
+ self, used_ids: list[str], result: dict[str, Any]
395
+ ) -> list[dict[str, Any]]:
396
+ if not used_ids:
397
+ return []
398
+ raw_citations = result.get("citations") or []
399
+ search_results = result.get("search_results") or []
400
+ search_map = {item.get("url"): item for item in search_results if item.get("url")}
401
+
402
+ selected: list[dict[str, Any]] = []
403
+ for cid in used_ids:
404
+ matched = None
405
+ for raw in raw_citations:
406
+ ref_id = str(raw.get("id")) if raw.get("id") is not None else ""
407
+ ref_token = (raw.get("reference") or "").strip()
408
+ normalized_token = ref_token.strip("[]")
409
+ if cid == ref_id or cid == normalized_token:
410
+ matched = dict(raw)
411
+ break
412
+ if not matched:
413
+ continue
414
+
415
+ url = matched.get("url")
416
+ if url and url in search_map:
417
+ fallback = search_map[url]
418
+ matched.setdefault("title", fallback.get("title", ""))
419
+ matched.setdefault("snippet", fallback.get("snippet", ""))
420
+
421
+ selected.append(
422
+ {
423
+ "id": matched.get("id") or (int(cid) if cid.isdigit() else cid),
424
+ "reference": matched.get("reference") or f"[{cid}]",
425
+ "url": matched.get("url"),
426
+ "title": matched.get("title", ""),
427
+ }
428
+ )
429
+ return selected
430
+
431
+ # ------------------------------------------------------------------ #
432
+ # Artifacts helpers
433
+ # ------------------------------------------------------------------ #
434
+ IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp"}
435
+
436
+ def _snapshot_image_artifacts(self, artifacts_path: Path) -> set:
437
+ if not artifacts_path.exists():
438
+ return set()
439
+ snapshot = set()
440
+ for file_path in artifacts_path.rglob("*"):
441
+ if file_path.is_file() and file_path.suffix.lower() in self.IMAGE_SUFFIXES:
442
+ snapshot.add(file_path.resolve())
443
+ return snapshot
444
+
445
+ def _collect_new_image_artifacts(
446
+ self, artifacts_path: Path, before_snapshot: set, output_dir: str | None
447
+ ) -> list[str]:
448
+ after_snapshot = self._snapshot_image_artifacts(artifacts_path)
449
+ new_files = sorted(after_snapshot - before_snapshot)
450
+ if not new_files:
451
+ return []
452
+
453
+ rel_paths: list[str] = []
454
+ output_base = Path(output_dir).resolve() if output_dir else None
455
+
456
+ for file_path in new_files:
457
+ rel_path: str | None = None
458
+ if output_base:
459
+ try:
460
+ rel_path = str(file_path.relative_to(output_base)).replace("\\", "/")
461
+ except ValueError:
462
+ rel_path = None
463
+ if rel_path is None:
464
+ try:
465
+ rel_path = str(file_path.relative_to(artifacts_path.parent)).replace("\\", "/")
466
+ except ValueError:
467
+ rel_path = str(Path("artifacts") / file_path.name)
468
+ rel_paths.append(rel_path)
469
+
470
+ return rel_paths
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Utility Module
4
+ Contains logging, performance monitoring, config validation, parsers, etc.
5
+ """
6
+
7
+ # Logging system (from unified logs module)
8
+ from pathlib import Path
9
+ import sys
10
+
11
+ # Add project root to path for imports
12
+ project_root = Path(__file__).parent.parent.parent.parent
13
+ if str(project_root) not in sys.path:
14
+ sys.path.insert(0, str(project_root))
15
+
16
+ from src.logging import Logger, LogLevel, get_logger, reset_logger
17
+
18
+ # Backwards compatibility aliases
19
+ # These aliases maintain compatibility with code that imported these names before refactoring
20
+ SolveAgentLogger = Logger # Alias for Logger class
21
+ from .config_validator import ConfigValidator
22
+ from .error_handler import (
23
+ LLMParseError,
24
+ retry_on_parse_error,
25
+ validate_investigate_output,
26
+ validate_none_tool_constraint,
27
+ validate_note_output,
28
+ validate_output,
29
+ validate_reflect_output,
30
+ validate_solve_output,
31
+ )
32
+
33
+ # Backwards compatibility alias
34
+ ParseError = LLMParseError
35
+ from .performance_monitor import PerformanceMonitor
36
+
37
+ # Token tracker
38
+ from .token_tracker import TokenTracker, calculate_cost, get_model_pricing
39
+
40
+ __all__ = [
41
+ # Logging system
42
+ "Logger",
43
+ "get_logger",
44
+ "reset_logger",
45
+ "LogLevel",
46
+ "SolveAgentLogger", # Backwards compatibility
47
+ # Performance monitoring
48
+ "PerformanceMonitor",
49
+ # Config validation
50
+ "ConfigValidator",
51
+ # Token tracker
52
+ "TokenTracker",
53
+ "calculate_cost",
54
+ "get_model_pricing",
55
+ # Error handling
56
+ "ParseError",
57
+ "retry_on_parse_error",
58
+ "validate_output",
59
+ "validate_investigate_output",
60
+ "validate_note_output",
61
+ "validate_none_tool_constraint",
62
+ "validate_reflect_output",
63
+ "validate_solve_output",
64
+ ]