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,862 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Main Solver - Problem-Solving System Controller
6
+
7
+ Based on Dual-Loop Architecture: Analysis Loop + Solve Loop
8
+ """
9
+
10
+ import asyncio
11
+ from datetime import datetime
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import traceback
16
+ from typing import Any
17
+
18
+ import yaml
19
+
20
+ from ...services.config import parse_language
21
+ from .analysis_loop import InvestigateAgent, NoteAgent
22
+
23
+ # Dual-Loop Architecture
24
+ from .memory import CitationMemory, InvestigateMemory, SolveChainStep, SolveMemory
25
+ from .solve_loop import (
26
+ ManagerAgent,
27
+ PrecisionAnswerAgent,
28
+ ResponseAgent,
29
+ SolveAgent,
30
+ ToolAgent,
31
+ )
32
+ from .utils import ConfigValidator, PerformanceMonitor, SolveAgentLogger
33
+ from .utils.display_manager import get_display_manager
34
+ from .utils.token_tracker import TokenTracker
35
+
36
+
37
+ class MainSolver:
38
+ """Problem-Solving System Controller"""
39
+
40
+ def __init__(
41
+ self,
42
+ config_path: str | None = None,
43
+ api_key: str | None = None,
44
+ base_url: str | None = None,
45
+ api_version: str | None = None,
46
+ kb_name: str = "ai_textbook",
47
+ output_base_dir: str | None = None,
48
+ ):
49
+ """
50
+ Initialize MainSolver with lightweight setup.
51
+ Call ainit() to complete async initialization.
52
+
53
+ Args:
54
+ config_path: Config file path (default: config.yaml in current directory)
55
+ api_key: API key (if not provided, read from environment)
56
+ base_url: API URL (if not provided, read from environment)
57
+ api_version: API version (if not provided, read from environment)
58
+ kb_name: Knowledge base name
59
+ output_base_dir: Output base directory (optional, overrides config)
60
+ """
61
+ # Store initialization parameters
62
+ self._config_path = config_path
63
+ self._api_key = api_key
64
+ self._base_url = base_url
65
+ self._api_version = api_version
66
+ self._kb_name = kb_name
67
+ self._output_base_dir = output_base_dir
68
+
69
+ # Initialize with None - will be set in ainit()
70
+ self.config = None
71
+ self.api_key = None
72
+ self.base_url = None
73
+ self.api_version = None
74
+ self.kb_name = kb_name
75
+ self.logger = None
76
+ self.monitor = None
77
+ self.token_tracker = None
78
+
79
+ async def ainit(self) -> None:
80
+ """
81
+ Complete the asynchronous second phase of MainSolver initialization.
82
+
83
+ This class uses a two-phase initialization pattern:
84
+
85
+ 1. ``__init__`` performs only lightweight, synchronous setup and stores
86
+ constructor arguments. Attributes such as ``config``, ``api_key``,
87
+ ``base_url``, ``api_version``, ``logger``, ``monitor``, and
88
+ ``token_tracker`` are intentionally left as ``None``.
89
+ 2. :meth:`ainit` performs all I/O-bound and asynchronous work required to
90
+ make the instance fully usable (e.g., loading configuration, wiring up
91
+ logging/monitoring, and preparing external-service clients).
92
+
93
+ You **must** call and await this method exactly once after constructing
94
+ ``MainSolver`` and **before** invoking any other methods that rely on
95
+ configuration, logging, metrics, or API access. Using the object prior
96
+ to calling :meth:`ainit` may result in attributes still being ``None``,
97
+ which can lead to confusing runtime errors such as ``AttributeError``,
98
+ misconfigured API calls, missing logs/metrics, or incorrect output paths.
99
+
100
+ This async initialization pattern is used instead of performing all setup
101
+ in ``__init__`` so that object construction remains fast and synchronous,
102
+ while allowing potentially slow operations (disk I/O, network requests,
103
+ validation) to be awaited explicitly by the caller in an async context.
104
+ """
105
+ config_path = self._config_path
106
+ api_key = self._api_key
107
+ base_url = self._base_url
108
+ api_version = self._api_version
109
+ kb_name = self._kb_name
110
+ output_base_dir = self._output_base_dir
111
+
112
+ # Load config from config directory (main.yaml unified config)
113
+ if config_path is None:
114
+ project_root = Path(__file__).parent.parent.parent.parent
115
+ # Load main.yaml (solve_config.yaml is optional and will be merged if exists)
116
+ from ...services.config.loader import load_config_with_main_async
117
+
118
+ full_config = await load_config_with_main_async("main.yaml", project_root)
119
+
120
+ # Extract solve-specific config and build validator-compatible structure
121
+ solve_config = full_config.get("solve", {})
122
+ paths_config = full_config.get("paths", {})
123
+
124
+ # Build config structure expected by ConfigValidator
125
+ self.config = {
126
+ "system": {
127
+ "output_base_dir": paths_config.get("solve_output_dir", "./data/user/solve"),
128
+ "save_intermediate_results": solve_config.get(
129
+ "save_intermediate_results", True
130
+ ),
131
+ "language": full_config.get("system", {}).get("language", "en"),
132
+ },
133
+ "agents": solve_config.get("agents", {}),
134
+ "logging": full_config.get("logging", {}),
135
+ "tools": full_config.get("tools", {}),
136
+ "paths": paths_config,
137
+ # Keep solve-specific settings accessible
138
+ "solve": solve_config,
139
+ }
140
+ else:
141
+ # If custom config path provided, load it directly (for backward compatibility)
142
+ local_config = {}
143
+ if Path(config_path).exists():
144
+ try:
145
+
146
+ def load_local_config(path: str) -> dict:
147
+ with open(path, encoding="utf-8") as f:
148
+ return yaml.safe_load(f) or {}
149
+
150
+ local_config = await asyncio.to_thread(load_local_config, config_path)
151
+ except Exception:
152
+ # Config loading warning will be handled by config_loader
153
+ pass
154
+ self.config = local_config if isinstance(local_config, dict) else {}
155
+
156
+ if self.config is None or not isinstance(self.config, dict):
157
+ self.config = {}
158
+
159
+ # Override output directory config
160
+ if output_base_dir:
161
+ if "system" not in self.config:
162
+ self.config["system"] = {}
163
+ self.config["system"]["output_base_dir"] = str(output_base_dir)
164
+
165
+ # Note: log_dir and performance_log_dir are now in paths section from main.yaml
166
+ # Only override if explicitly needed
167
+
168
+ # Validate config
169
+ validator = ConfigValidator()
170
+ is_valid, errors, warnings = validator.validate(self.config)
171
+ if not is_valid:
172
+ raise ValueError(f"Config validation failed: {errors}")
173
+
174
+ # API config
175
+ if api_key is None or base_url is None or "llm" not in self.config:
176
+ try:
177
+ from ...services.llm.config import get_llm_config_async
178
+
179
+ llm_config = await get_llm_config_async()
180
+ if api_key is None:
181
+ api_key = llm_config.api_key
182
+ if base_url is None:
183
+ base_url = llm_config.base_url
184
+ if api_version is None:
185
+ api_version = getattr(llm_config, "api_version", None)
186
+
187
+ # Ensure LLM config is populated in self.config for agents
188
+ if "llm" not in self.config:
189
+ self.config["llm"] = {}
190
+
191
+ # Update config with complete details (binding, model, etc.)
192
+ from dataclasses import asdict
193
+
194
+ self.config["llm"].update(asdict(llm_config))
195
+
196
+ except ValueError as e:
197
+ raise ValueError(f"LLM config error: {e!s}")
198
+
199
+ # Check if API key is required
200
+ # Local LLM servers (Ollama, LM Studio, etc.) don't need API keys
201
+ from src.services.llm import is_local_llm_server
202
+
203
+ if not api_key and not is_local_llm_server(base_url):
204
+ raise ValueError("API key not set. Provide api_key param or set LLM_API_KEY in .env")
205
+
206
+ # For local servers, use a placeholder key if none provided
207
+ if not api_key and is_local_llm_server(base_url):
208
+ api_key = "sk-no-key-required"
209
+
210
+ self.api_key = api_key
211
+ self.base_url = base_url
212
+ self.api_version = api_version
213
+ self.kb_name = kb_name
214
+
215
+ # Initialize logging system
216
+ logging_config = self.config.get("logging", {})
217
+ # Get log_dir from paths (user_log_dir from main.yaml) or logging config
218
+ log_dir = (
219
+ self.config.get("paths", {}).get("user_log_dir")
220
+ or self.config.get("paths", {}).get("log_dir")
221
+ or logging_config.get("log_dir")
222
+ )
223
+ self.logger = SolveAgentLogger(
224
+ name="Solver",
225
+ level=logging_config.get("level", "INFO"),
226
+ log_dir=log_dir,
227
+ console_output=logging_config.get("console_output", True),
228
+ file_output=logging_config.get("save_to_file", True),
229
+ )
230
+
231
+ # Attach display manager for TUI and frontend status updates
232
+ self.logger.display_manager = get_display_manager()
233
+
234
+ # Initialize performance monitor (disabled by default - performance logging is deprecated)
235
+ monitoring_config = self.config.get("monitoring", {})
236
+ # Disable performance monitor by default to avoid creating performance directory
237
+ self.monitor = PerformanceMonitor(
238
+ enabled=False,
239
+ save_dir=None, # Disabled - performance logging is deprecated
240
+ )
241
+
242
+ # Initialize Token tracker
243
+ self.token_tracker = TokenTracker(prefer_tiktoken=True)
244
+
245
+ # Connect token_tracker to display_manager for real-time updates
246
+ if self.logger.display_manager:
247
+ self.token_tracker.set_on_usage_added_callback(
248
+ self.logger.display_manager.update_token_stats
249
+ )
250
+
251
+ self.logger.section("Dual-Loop Solver Initializing")
252
+ self.logger.info(f"Knowledge Base: {kb_name}")
253
+
254
+ # Initialize Agents
255
+ self._init_agents()
256
+
257
+ self.logger.success("Solver ready")
258
+
259
+ def _deep_merge(self, base: dict, update: dict) -> dict:
260
+ """Deep merge two dictionaries"""
261
+ if base is None:
262
+ base = {}
263
+ if update is None:
264
+ update = {}
265
+
266
+ result = base.copy() if base else {}
267
+ for key, value in update.items():
268
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
269
+ result[key] = self._deep_merge(result[key], value)
270
+ else:
271
+ result[key] = value
272
+ return result
273
+
274
+ def _init_agents(self):
275
+ """Initialize all Agents - Dual-Loop Architecture"""
276
+ self.logger.progress("Initializing agents...")
277
+
278
+ # Analysis Loop Agents
279
+ self.investigate_agent = InvestigateAgent(
280
+ config=self.config,
281
+ api_key=self.api_key,
282
+ base_url=self.base_url,
283
+ api_version=self.api_version,
284
+ token_tracker=self.token_tracker,
285
+ )
286
+ self.logger.info(" InvestigateAgent initialized")
287
+
288
+ self.note_agent = NoteAgent(
289
+ config=self.config,
290
+ api_key=self.api_key,
291
+ base_url=self.base_url,
292
+ api_version=self.api_version,
293
+ token_tracker=self.token_tracker,
294
+ )
295
+ self.logger.info(" NoteAgent initialized")
296
+
297
+ # Solve Loop Agents (lazy initialization)
298
+ self.manager_agent = None
299
+ self.solve_agent = None
300
+ self.tool_agent = None
301
+ self.response_agent = None
302
+ self.precision_answer_agent = None
303
+ self.logger.info(" Solve Loop agents (lazy init)")
304
+
305
+ async def solve(self, question: str, verbose: bool = True) -> dict[str, Any]:
306
+ """
307
+ Main solving process - Dual-Loop Architecture
308
+
309
+ Args:
310
+ question: User question
311
+ verbose: Whether to print detailed info
312
+
313
+ Returns:
314
+ dict: Solving result
315
+ """
316
+ # Create output directory
317
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
318
+ output_base_dir = self.config.get("system", {}).get("output_base_dir", "./user/solve")
319
+ output_dir = os.path.join(output_base_dir, f"solve_{timestamp}")
320
+ os.makedirs(output_dir, exist_ok=True)
321
+
322
+ # Add task log file handler
323
+ task_log_file = os.path.join(output_dir, "task.log")
324
+ self.logger.add_task_log_handler(task_log_file)
325
+
326
+ self.logger.section("Problem Solving Started")
327
+ self.logger.info(f"Question: {question[:100]}{'...' if len(question) > 100 else ''}")
328
+ self.logger.info(f"Output: {output_dir}")
329
+
330
+ try:
331
+ # Execute dual-loop pipeline
332
+ result = await self._run_dual_loop_pipeline(question, output_dir)
333
+
334
+ # Add metadata
335
+ result["metadata"] = {
336
+ "mode": "dual_loop",
337
+ "timestamp": timestamp,
338
+ "output_dir": output_dir,
339
+ }
340
+
341
+ # Save performance report
342
+ if self.config.get("monitoring", {}).get("enabled", True):
343
+ perf_report = self.monitor.generate_report()
344
+ perf_file = os.path.join(output_dir, "performance_report.json")
345
+ with open(perf_file, "w", encoding="utf-8") as f:
346
+ json.dump(perf_report, f, ensure_ascii=False, indent=2)
347
+ self.logger.debug(f"Performance report saved: {perf_file}")
348
+
349
+ # Output cost report
350
+ if self.token_tracker:
351
+ cost_summary = self.token_tracker.get_summary()
352
+ if cost_summary["total_calls"] > 0:
353
+ cost_text = self.token_tracker.format_summary()
354
+ self.logger.info(f"\n{cost_text}")
355
+
356
+ cost_file = os.path.join(output_dir, "cost_report.json")
357
+ self.token_tracker.save(cost_file)
358
+ self.logger.debug(f"Cost report saved: {cost_file}")
359
+
360
+ self.token_tracker.reset()
361
+
362
+ self.logger.success("Problem solving completed")
363
+ self.logger.remove_task_log_handlers()
364
+
365
+ return result
366
+
367
+ except Exception as e:
368
+ self.logger.error(f"Solving failed: {e!s}")
369
+ self.logger.error(traceback.format_exc())
370
+ self.logger.remove_task_log_handlers()
371
+ raise
372
+
373
+ finally:
374
+ if hasattr(self, "logger"):
375
+ self.logger.shutdown()
376
+
377
+ async def _run_dual_loop_pipeline(self, question: str, output_dir: str) -> dict[str, Any]:
378
+ """
379
+ Dual-Loop Pipeline:
380
+ 1) Analysis Loop: Investigate → Note
381
+ 2) Solve Loop: Plan → Manager → Solve → Check → Format
382
+ """
383
+
384
+ self.logger.info("Pipeline: Analysis Loop → Solve Loop")
385
+
386
+ # ========== Analysis Loop ==========
387
+ self.logger.stage("Analysis Loop", "start", "Understanding the question")
388
+
389
+ investigate_memory = InvestigateMemory.load_or_create(
390
+ output_dir=output_dir, user_question=question
391
+ )
392
+
393
+ citation_memory = CitationMemory.load_or_create(output_dir=output_dir)
394
+
395
+ # Read max_iterations from solve.agents.investigate_agent config (authoritative source)
396
+ agent_config = self.config.get("solve", {}).get("agents", {}).get("investigate_agent", {})
397
+ max_analysis_iterations = agent_config.get("max_iterations", 5)
398
+ self.logger.log_stage_progress(
399
+ "AnalysisLoop", "start", f"max_iterations={max_analysis_iterations}"
400
+ )
401
+
402
+ analysis_completed = False
403
+
404
+ # Analysis Loop iterations
405
+ for i in range(max_analysis_iterations):
406
+ self.logger.log_stage_progress("AnalysisLoop", "running", f"round={i + 1}")
407
+
408
+ # 1. Investigate: Generate queries and call tools
409
+ with self.monitor.track(f"analysis_investigate_{i + 1}"):
410
+ investigate_result = await self.investigate_agent.process(
411
+ question=question,
412
+ memory=investigate_memory,
413
+ citation_memory=citation_memory,
414
+ kb_name=self.kb_name,
415
+ output_dir=output_dir,
416
+ verbose=False,
417
+ )
418
+
419
+ knowledge_ids: list[str] = investigate_result.get("knowledge_item_ids", [])
420
+ should_stop = investigate_result.get("should_stop", False)
421
+ reasoning = investigate_result.get("reasoning", "")
422
+ actions = investigate_result.get("actions", [])
423
+
424
+ self.logger.debug(f" [Investigate] Reasoning: {reasoning or 'N/A'}")
425
+
426
+ if hasattr(self, "_send_progress_update"):
427
+ queries = [action.get("query", "") for action in actions if action.get("query")]
428
+ self._send_progress_update("investigate", {"round": i + 1, "queries": queries})
429
+
430
+ if actions:
431
+ for action in actions:
432
+ tool_label = action["tool_type"]
433
+ query = action.get("query") or ""
434
+ cite_id = action.get("cite_id")
435
+ suffix = f" → cite_id={cite_id}" if cite_id else ""
436
+ self.logger.info(f" Tool: {tool_label} | {query[:50]}{suffix}")
437
+ else:
438
+ self.logger.debug(" No queries generated this round")
439
+
440
+ # 2. Note: Generate notes (if new knowledge exists)
441
+ if knowledge_ids:
442
+ self.logger.log_stage_progress("Note", "start")
443
+
444
+ with self.monitor.track(f"analysis_note_{i + 1}"):
445
+ note_result = await self.note_agent.process(
446
+ question=question,
447
+ memory=investigate_memory,
448
+ new_knowledge_ids=knowledge_ids,
449
+ citation_memory=citation_memory,
450
+ output_dir=output_dir,
451
+ verbose=False,
452
+ )
453
+
454
+ if note_result.get("success"):
455
+ processed = note_result.get("processed_items", 0)
456
+ self.logger.info(f" Note: {processed} items processed")
457
+ self.logger.log_stage_progress("Note", "complete")
458
+ else:
459
+ self.logger.warning(f" Note failed: {note_result.get('reason', 'unknown')}")
460
+ self.logger.log_stage_progress("Note", "error")
461
+
462
+ # Update Token stats
463
+ self.logger.update_token_stats(self.token_tracker.get_summary())
464
+
465
+ # 3. Check stop condition
466
+ if should_stop:
467
+ analysis_completed = True
468
+ self.logger.log_stage_progress(
469
+ "AnalysisLoop",
470
+ "complete",
471
+ f"rounds={i + 1}, knowledge={len(investigate_memory.knowledge_chain)}",
472
+ )
473
+ break
474
+
475
+ if not analysis_completed:
476
+ self.logger.log_stage_progress(
477
+ "AnalysisLoop",
478
+ "warning",
479
+ f"max_iterations({max_analysis_iterations}) reached, knowledge={len(investigate_memory.knowledge_chain)}",
480
+ )
481
+
482
+ # Update investigate_memory metadata
483
+ investigate_memory.metadata["total_iterations"] = i + 1
484
+ investigate_memory.metadata["total_knowledge_items"] = len(
485
+ investigate_memory.knowledge_chain
486
+ )
487
+ investigate_memory.reflections.remaining_questions = []
488
+
489
+ if analysis_completed:
490
+ investigate_memory.metadata["coverage_rate"] = 1.0
491
+ investigate_memory.metadata["avg_confidence"] = 0.9
492
+ else:
493
+ coverage = min(
494
+ 1.0, len(investigate_memory.knowledge_chain) / max(1, max_analysis_iterations)
495
+ )
496
+ investigate_memory.metadata["coverage_rate"] = coverage
497
+ investigate_memory.metadata["avg_confidence"] = 0.6
498
+
499
+ investigate_memory.save()
500
+
501
+ # ========== Solve Loop ==========
502
+ self.logger.stage("Solve Loop", "start", "Generating solution")
503
+
504
+ solve_memory = SolveMemory.load_or_create(output_dir=output_dir, user_question=question)
505
+
506
+ # Initialize Solve Loop Agents (if not yet initialized)
507
+ if self.manager_agent is None:
508
+ self.logger.progress("Initializing Solve Loop agents...")
509
+ self.manager_agent = ManagerAgent(
510
+ self.config,
511
+ self.api_key,
512
+ self.base_url,
513
+ api_version=self.api_version,
514
+ token_tracker=self.token_tracker,
515
+ )
516
+ self.solve_agent = SolveAgent(
517
+ self.config,
518
+ self.api_key,
519
+ self.base_url,
520
+ api_version=self.api_version,
521
+ token_tracker=self.token_tracker,
522
+ )
523
+ self.tool_agent = ToolAgent(
524
+ self.config,
525
+ self.api_key,
526
+ self.base_url,
527
+ api_version=self.api_version,
528
+ token_tracker=self.token_tracker,
529
+ )
530
+ self.response_agent = ResponseAgent(
531
+ self.config,
532
+ self.api_key,
533
+ self.base_url,
534
+ api_version=self.api_version,
535
+ token_tracker=self.token_tracker,
536
+ )
537
+
538
+ precision_enabled = (
539
+ self.config.get("agents", {})
540
+ .get("precision_answer_agent", {})
541
+ .get("enabled", False)
542
+ )
543
+ if precision_enabled:
544
+ self.precision_answer_agent = PrecisionAnswerAgent(
545
+ self.config,
546
+ self.api_key,
547
+ self.base_url,
548
+ api_version=self.api_version,
549
+ token_tracker=self.token_tracker,
550
+ )
551
+
552
+ # 1. Plan: Generate solving plan
553
+ self.logger.info("Plan: Generating solution strategy...")
554
+
555
+ plan_result = None
556
+ for attempt in range(2):
557
+ try:
558
+ with self.monitor.track(f"solve_plan_attempt_{attempt + 1}"):
559
+ plan_result = await self.manager_agent.process(
560
+ question=question,
561
+ investigate_memory=investigate_memory,
562
+ solve_memory=solve_memory,
563
+ verbose=(attempt > 0),
564
+ )
565
+ num_steps = plan_result.get("num_steps") or plan_result.get("steps_count", 0)
566
+ self.logger.log_stage_progress("Plan", "complete", f"steps={num_steps}")
567
+ self.logger.update_token_stats(self.token_tracker.get_summary())
568
+ break
569
+ except Exception as e:
570
+ if attempt == 0:
571
+ self.logger.error(f"ManagerAgent attempt {attempt + 1} failed: {e!s}")
572
+ self.logger.warning("Retrying plan generation...")
573
+ solve_memory = SolveMemory.load_or_create(
574
+ output_dir=output_dir, user_question=question
575
+ )
576
+ else:
577
+ self.logger.error(f"ManagerAgent attempt {attempt + 1} also failed")
578
+ raise ValueError(f"ManagerAgent failed after retry: {e!s}")
579
+
580
+ if plan_result is None:
581
+ raise ValueError("ManagerAgent failed to generate plan")
582
+
583
+ # 2. Solve Loop - Execute steps
584
+ self.logger.info("Solve: Executing solution steps...")
585
+ max_correction_iterations = self.config.get("system", {}).get(
586
+ "max_solve_correction_iterations", 3
587
+ )
588
+ total_planned_steps = len(solve_memory.solve_chains)
589
+ self.logger.log_stage_progress(
590
+ "SolveLoop",
591
+ "start",
592
+ f"planned_steps={total_planned_steps}, max_corrections={max_correction_iterations}",
593
+ )
594
+
595
+ for step_index, step in enumerate(solve_memory.solve_chains, 1):
596
+ if step.status in ("waiting_response", "done"):
597
+ continue
598
+
599
+ self.logger.info(f" Step {step_index}: {step.step_id}")
600
+ self.logger.debug(f" Target: {step.step_target[:80]}")
601
+
602
+ if hasattr(self, "_send_progress_update"):
603
+ self._send_progress_update(
604
+ "solve",
605
+ {
606
+ "step_index": step_index,
607
+ "step_id": step.step_id,
608
+ "step_target": step.step_target,
609
+ },
610
+ )
611
+
612
+ self.logger.log_stage_progress("SolveLoop", "running", f"step={step.step_id}")
613
+
614
+ if self._has_pending_tool_calls(step):
615
+ await self._execute_tool_calls(step, solve_memory, citation_memory, output_dir)
616
+
617
+ iteration = 0
618
+ while iteration < max_correction_iterations:
619
+ iteration += 1
620
+ current_step = solve_memory.get_step(step.step_id) or step
621
+
622
+ with self.monitor.track(f"solve_execute_{step.step_id}_iter_{iteration}"):
623
+ solve_result = await self.solve_agent.process(
624
+ question=question,
625
+ current_step=current_step,
626
+ solve_memory=solve_memory,
627
+ investigate_memory=investigate_memory,
628
+ citation_memory=citation_memory,
629
+ kb_name=self.kb_name,
630
+ output_dir=output_dir,
631
+ verbose=False,
632
+ )
633
+
634
+ if solve_result.get("raw_llm_response"):
635
+ self.logger.log_stage_progress(
636
+ "SolveLoop", "running", f"step={step.step_id}, iteration={iteration}"
637
+ )
638
+
639
+ if solve_result.get("requested_calls"):
640
+ await self._execute_tool_calls(
641
+ current_step, solve_memory, citation_memory, output_dir
642
+ )
643
+
644
+ self.logger.update_token_stats(self.token_tracker.get_summary())
645
+
646
+ if solve_result.get("finish_requested"):
647
+ current_step = solve_memory.get_step(step.step_id) or step
648
+ if self._has_pending_tool_calls(current_step):
649
+ self.logger.debug(" Finish triggered but tools pending, continuing...")
650
+ continue
651
+ solve_memory.mark_step_waiting_response(current_step.step_id)
652
+ solve_memory.save()
653
+ self.logger.log_stage_progress(
654
+ "SolveLoop", "complete", f"step={current_step.step_id} ready for response"
655
+ )
656
+ break
657
+ else:
658
+ self.logger.warning(f" Step {step.step_id} max iterations reached")
659
+ solve_memory.mark_step_waiting_response(step.step_id)
660
+ solve_memory.save()
661
+
662
+ pending_steps = [
663
+ s.step_id
664
+ for s in solve_memory.solve_chains
665
+ if s.status not in ("waiting_response", "done")
666
+ ]
667
+ if pending_steps:
668
+ self.logger.warning(f"Steps not ready for response: {', '.join(pending_steps)}")
669
+
670
+ self.logger.log_stage_progress(
671
+ "SolveLoop", "complete", f"steps_processed={total_planned_steps - len(pending_steps)}"
672
+ )
673
+
674
+ # 3. Response: Generate responses for each step
675
+ self.logger.info("Response: Generating step responses...")
676
+ self.logger.log_stage_progress("ResponseLoop", "start", "Generating responses")
677
+
678
+ accumulated_response = ""
679
+ for step in solve_memory.solve_chains:
680
+ if step.status == "done" and step.step_response:
681
+ accumulated_response += step.step_response + "\n\n"
682
+
683
+ for step in solve_memory.solve_chains:
684
+ if step.status != "waiting_response":
685
+ continue
686
+
687
+ original_step_index = next(
688
+ (
689
+ i + 1
690
+ for i, s in enumerate(solve_memory.solve_chains)
691
+ if s.step_id == step.step_id
692
+ ),
693
+ 0,
694
+ )
695
+
696
+ if hasattr(self, "_send_progress_update"):
697
+ self._send_progress_update(
698
+ "response",
699
+ {
700
+ "step_index": original_step_index,
701
+ "step_id": step.step_id,
702
+ "step_target": step.step_target,
703
+ },
704
+ )
705
+
706
+ with self.monitor.track(f"solve_response_{step.step_id}"):
707
+ response_result = await self.response_agent.process(
708
+ question=question,
709
+ step=step,
710
+ solve_memory=solve_memory,
711
+ investigate_memory=investigate_memory,
712
+ citation_memory=citation_memory,
713
+ output_dir=output_dir,
714
+ verbose=False,
715
+ accumulated_response=accumulated_response,
716
+ )
717
+
718
+ step_response = response_result.get("step_response", "")
719
+ if step_response:
720
+ accumulated_response += step_response + "\n\n"
721
+
722
+ if response_result.get("raw_response"):
723
+ self.logger.log_stage_progress(
724
+ "ResponseLoop", "running", f"step={step.step_id} response generated"
725
+ )
726
+
727
+ self.logger.update_token_stats(self.token_tracker.get_summary())
728
+
729
+ self.logger.log_stage_progress("ResponseLoop", "complete", "All responses generated")
730
+
731
+ # 4. Finalize: Compile final answer
732
+ self.logger.info("Finalize: Compiling final answer...")
733
+ self.logger.log_stage_progress("Finalize", "start", "Compiling steps")
734
+
735
+ actual_total_steps = len(solve_memory.solve_chains)
736
+ completed_step_objs = [
737
+ step
738
+ for step in solve_memory.solve_chains
739
+ if step.status == "done" and step.step_response
740
+ ]
741
+ completed_steps = len(completed_step_objs)
742
+
743
+ solve_memory.metadata["total_steps"] = actual_total_steps
744
+ solve_memory.metadata["completed_steps"] = completed_steps
745
+ solve_memory.save()
746
+ self.logger.info(f" Stats: {completed_steps}/{actual_total_steps} steps completed")
747
+
748
+ used_cite_ids = []
749
+ for step in completed_step_objs:
750
+ used_cite_ids.extend(step.used_citations)
751
+ used_cite_ids = list(dict.fromkeys(used_cite_ids))
752
+
753
+ step_responses = [step.step_response for step in completed_step_objs]
754
+ final_answer = "\n\n".join(step_responses)
755
+
756
+ # Get language setting from config (unified in config/main.yaml system.language)
757
+ language = self.config.get("system", {}).get("language", "zh")
758
+ lang_code = parse_language(language)
759
+
760
+ # Check if citations are enabled
761
+ enable_citations = self.config.get("system", {}).get("enable_citations", True)
762
+
763
+ citations_section = ""
764
+ if enable_citations and citation_memory:
765
+ citations_section = citation_memory.format_citations_markdown(
766
+ used_cite_ids=used_cite_ids, language=lang_code
767
+ )
768
+ if citations_section:
769
+ final_answer = f"{final_answer}\n\n---\n\n{citations_section}"
770
+
771
+ format_result = {
772
+ "final_answer": final_answer.strip(),
773
+ "citations": used_cite_ids,
774
+ "metadata": {
775
+ "refined_steps": len(completed_step_objs),
776
+ "total_steps": actual_total_steps,
777
+ "citations_section": bool(citations_section),
778
+ },
779
+ }
780
+
781
+ self.logger.info(f" Final answer: {len(format_result['final_answer'])} chars")
782
+ self.logger.info(f" Citations: {len(format_result['citations'])}")
783
+
784
+ # 5. Precision Answer (if enabled)
785
+ precision_answer_enabled = (
786
+ self.config.get("agents", {}).get("precision_answer_agent", {}).get("enabled", False)
787
+ )
788
+ final_answer_content = format_result["final_answer"]
789
+
790
+ if precision_answer_enabled and self.precision_answer_agent:
791
+ self.logger.info("PrecisionAnswer: Generating concise answer...")
792
+ with self.monitor.track("precision_answer"):
793
+ precision_result = await self.precision_answer_agent.process(
794
+ question=question, detailed_answer=format_result["final_answer"], verbose=False
795
+ )
796
+ if precision_result.get("needs_precision"):
797
+ precision_answer = precision_result.get("precision_answer", "")
798
+ self.logger.info(f" Precision answer: {len(precision_answer)} chars")
799
+ final_answer_content = f"## Concise Answer\n\n{precision_answer}\n\n---\n\n## Detailed Answer\n\n{format_result['final_answer']}"
800
+ else:
801
+ self.logger.debug(" No precision answer needed")
802
+
803
+ # Save final answer
804
+ final_answer_file = Path(output_dir) / "final_answer.md"
805
+ with open(final_answer_file, "w", encoding="utf-8") as f:
806
+ f.write(final_answer_content)
807
+
808
+ self.logger.success(f"Final answer saved: {final_answer_file}")
809
+ self.logger.log_stage_progress("Format", "complete", f"output={final_answer_file}")
810
+
811
+ return {
812
+ "question": question,
813
+ "output_dir": output_dir,
814
+ "final_answer": final_answer_content,
815
+ "output_md": str(final_answer_file),
816
+ "output_json": str(Path(output_dir) / "solve_chain.json"),
817
+ "formatted_solution": final_answer_content,
818
+ "citations": format_result["citations"],
819
+ "pipeline": "reworked",
820
+ "total_steps": solve_memory.metadata["total_steps"],
821
+ "analysis_iterations": investigate_memory.metadata.get("total_iterations", 0),
822
+ "solve_steps": solve_memory.metadata["completed_steps"],
823
+ "metadata": {
824
+ "coverage_rate": investigate_memory.metadata.get("coverage_rate", 0.0),
825
+ "avg_confidence": investigate_memory.metadata.get("avg_confidence", 0.0),
826
+ "total_steps": solve_memory.metadata["total_steps"],
827
+ },
828
+ }
829
+
830
+ async def _execute_tool_calls(
831
+ self,
832
+ step: SolveChainStep,
833
+ solve_memory: SolveMemory,
834
+ citation_memory: CitationMemory,
835
+ output_dir: str | None,
836
+ ) -> dict[str, Any]:
837
+ tool_result = await self.tool_agent.process(
838
+ step=step,
839
+ solve_memory=solve_memory,
840
+ citation_memory=citation_memory,
841
+ kb_name=self.kb_name,
842
+ output_dir=output_dir,
843
+ verbose=False,
844
+ )
845
+ return tool_result
846
+
847
+ @staticmethod
848
+ def _has_pending_tool_calls(step: SolveChainStep) -> bool:
849
+ return any(call.status in {"pending", "running"} for call in step.tool_calls)
850
+
851
+
852
+ if __name__ == "__main__":
853
+ from dotenv import load_dotenv
854
+
855
+ load_dotenv()
856
+
857
+ async def test():
858
+ solver = MainSolver(kb_name="ai_textbook")
859
+ result = await solver.solve(question="What is linear convolution?", verbose=True)
860
+ print(f"Output file: {result['output_md']}")
861
+
862
+ asyncio.run(test())