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,305 @@
1
+ """
2
+ Solve API Router
3
+ ================
4
+
5
+ WebSocket endpoint for real-time problem solving with streaming logs.
6
+ """
7
+
8
+ import asyncio
9
+ from pathlib import Path
10
+ import re
11
+ import sys
12
+ from typing import Any
13
+
14
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
15
+
16
+ from src.agents.solve import MainSolver
17
+ from src.api.utils.history import ActivityType, history_manager
18
+ from src.api.utils.log_interceptor import LogInterceptor
19
+ from src.api.utils.task_id_manager import TaskIDManager
20
+
21
+ _project_root = Path(__file__).parent.parent.parent.parent
22
+ sys.path.insert(0, str(_project_root))
23
+ from src.logging import get_logger
24
+ from src.services.config import load_config_with_main
25
+ from src.services.llm import get_llm_config
26
+
27
+ # Initialize logger with config
28
+ project_root = Path(__file__).parent.parent.parent.parent
29
+ config = load_config_with_main("solve_config.yaml", project_root)
30
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
31
+ logger = get_logger("SolveAPI", level="INFO", log_dir=log_dir)
32
+
33
+ router = APIRouter()
34
+
35
+
36
+ @router.websocket("/solve")
37
+ async def websocket_solve(websocket: WebSocket):
38
+ await websocket.accept()
39
+
40
+ task_manager = TaskIDManager.get_instance()
41
+ connection_closed = asyncio.Event()
42
+ log_queue = asyncio.Queue()
43
+ pusher_task = None
44
+
45
+ async def safe_send_json(data: dict[str, Any]):
46
+ """Safely send JSON to WebSocket, checking if connection is closed"""
47
+ if connection_closed.is_set():
48
+ return False
49
+ try:
50
+ await websocket.send_json(data)
51
+ return True
52
+ except (WebSocketDisconnect, RuntimeError, ConnectionError) as e:
53
+ logger.debug(f"WebSocket connection closed: {e}")
54
+ connection_closed.set()
55
+ return False
56
+ except Exception as e:
57
+ logger.debug(f"Error sending WebSocket message: {e}")
58
+ return False
59
+
60
+ async def log_pusher():
61
+ while not connection_closed.is_set():
62
+ try:
63
+ # Use timeout to periodically check if connection is closed
64
+ entry = await asyncio.wait_for(log_queue.get(), timeout=0.5)
65
+ try:
66
+ await websocket.send_json(entry)
67
+ except (WebSocketDisconnect, RuntimeError, ConnectionError) as e:
68
+ # Connection closed, stop pushing
69
+ logger.debug(f"WebSocket connection closed in log_pusher: {e}")
70
+ connection_closed.set()
71
+ log_queue.task_done()
72
+ break
73
+ except Exception as e:
74
+ logger.debug(f"Error sending log entry: {e}")
75
+ # Continue to next entry
76
+ log_queue.task_done()
77
+ except asyncio.TimeoutError:
78
+ # Timeout, check if connection is still open
79
+ continue
80
+ except Exception as e:
81
+ logger.debug(f"Error in log_pusher: {e}")
82
+ break
83
+
84
+ try:
85
+ # 1. Wait for the initial message with the question and config
86
+ data = await websocket.receive_json()
87
+ question = data.get("question")
88
+ kb_name = data.get("kb_name", "ai_textbook")
89
+
90
+ if not question:
91
+ await websocket.send_json({"type": "error", "content": "Question is required"})
92
+ return
93
+
94
+ task_key = f"solve_{kb_name}_{hash(str(question))}"
95
+ task_id = task_manager.generate_task_id("solve", task_key)
96
+
97
+ await websocket.send_json({"type": "task_id", "task_id": task_id})
98
+
99
+ # 2. Initialize Solver
100
+ root_dir = Path(__file__).parent.parent.parent.parent
101
+ output_base = root_dir / "data" / "user" / "solve"
102
+
103
+ try:
104
+ llm_config = get_llm_config()
105
+ api_key = llm_config.api_key
106
+ base_url = llm_config.base_url
107
+ api_version = getattr(llm_config, "api_version", None)
108
+ except Exception as e:
109
+ logger.error(f"Failed to get LLM config: {e}", exc_info=True)
110
+ await websocket.send_json({"type": "error", "content": f"LLM configuration error: {e}"})
111
+ return
112
+
113
+ solver = MainSolver(
114
+ kb_name=kb_name,
115
+ output_base_dir=str(output_base),
116
+ api_key=api_key,
117
+ base_url=base_url,
118
+ api_version=api_version,
119
+ )
120
+
121
+ # Complete async initialization
122
+ await solver.ainit()
123
+
124
+ logger.info(f"[{task_id}] Solving: {question[:50]}...")
125
+
126
+ target_logger = solver.logger.logger
127
+
128
+ # Note: System log forwarder removed - all logs now go to unified log file
129
+ # The main logger already writes to data/user/logs/ai_tutor_YYYYMMDD.log
130
+
131
+ # 3. Setup Log Queue
132
+ # log_queue already initialized
133
+
134
+ # 4. Setup status update mechanism
135
+ display_manager = None
136
+ if hasattr(solver.logger, "display_manager") and solver.logger.display_manager:
137
+ display_manager = solver.logger.display_manager
138
+
139
+ original_set_status = display_manager.set_agent_status
140
+
141
+ def wrapped_set_status(agent_name: str, status: str):
142
+ original_set_status(agent_name, status)
143
+ try:
144
+ log_queue.put_nowait(
145
+ {
146
+ "type": "agent_status",
147
+ "agent": agent_name,
148
+ "status": status,
149
+ "all_agents": display_manager.agents_status.copy(),
150
+ }
151
+ )
152
+ except Exception:
153
+ pass
154
+
155
+ display_manager.set_agent_status = wrapped_set_status
156
+
157
+ original_update_stats = display_manager.update_token_stats
158
+
159
+ def wrapped_update_stats(summary: dict[str, Any]):
160
+ original_update_stats(summary)
161
+ try:
162
+ stats_copy = display_manager.stats.copy()
163
+ logger.debug(
164
+ f"Sending token_stats: model={stats_copy.get('model')}, calls={stats_copy.get('calls')}, cost={stats_copy.get('cost')}"
165
+ )
166
+ log_queue.put_nowait({"type": "token_stats", "stats": stats_copy})
167
+ except Exception as e:
168
+ logger.debug(f"Failed to send token_stats: {e}")
169
+
170
+ display_manager.update_token_stats = wrapped_update_stats
171
+
172
+ # Re-register the callback to use the wrapped method
173
+ # (The callback was set before wrapping in main_solver.py)
174
+ if hasattr(solver, "token_tracker") and solver.token_tracker:
175
+ solver.token_tracker.set_on_usage_added_callback(wrapped_update_stats)
176
+
177
+ def send_progress_update(stage: str, progress: dict[str, Any]):
178
+ """Send progress update to frontend"""
179
+ try:
180
+ log_queue.put_nowait({"type": "progress", "stage": stage, "progress": progress})
181
+ except Exception:
182
+ pass
183
+
184
+ solver._send_progress_update = send_progress_update
185
+
186
+ # 5. Background task to push logs to WebSocket
187
+ pusher_task = asyncio.create_task(log_pusher())
188
+
189
+ # 6. Run Solver within the LogInterceptor context
190
+ interceptor = LogInterceptor(target_logger, log_queue)
191
+ with interceptor:
192
+ await safe_send_json({"type": "status", "content": "started"})
193
+
194
+ if display_manager:
195
+ await safe_send_json(
196
+ {
197
+ "type": "agent_status",
198
+ "agent": "all",
199
+ "status": "initial",
200
+ "all_agents": display_manager.agents_status.copy(),
201
+ }
202
+ )
203
+ await safe_send_json({"type": "token_stats", "stats": display_manager.stats.copy()})
204
+
205
+ logger.progress(f"[{task_id}] Solving started")
206
+
207
+ result = await solver.solve(question, verbose=True)
208
+
209
+ logger.success(f"[{task_id}] Solving completed")
210
+ task_manager.update_task_status(task_id, "completed")
211
+
212
+ # Process Markdown content to fix image paths
213
+ final_answer = result.get("final_answer", "")
214
+ output_dir_str = result.get("output_dir", "")
215
+
216
+ if output_dir_str and final_answer:
217
+ try:
218
+ output_dir = Path(output_dir_str)
219
+
220
+ if not output_dir.is_absolute():
221
+ output_dir = output_dir.resolve()
222
+
223
+ path_str = str(output_dir).replace("\\", "/")
224
+ parts = path_str.split("/")
225
+
226
+ if "user" in parts:
227
+ idx = parts.index("user")
228
+ rel_path = "/".join(parts[idx + 1 :])
229
+ base_url = f"/api/outputs/{rel_path}"
230
+
231
+ pattern = r"\]\(artifacts/([^)]+)\)"
232
+ replacement = rf"]({base_url}/artifacts/\1)"
233
+ final_answer = re.sub(pattern, replacement, final_answer)
234
+ except Exception as e:
235
+ logger.debug(f"Error processing image paths: {e}")
236
+
237
+ # Send final agent status update
238
+ if display_manager:
239
+ final_agent_status = dict.fromkeys(display_manager.agents_status.keys(), "done")
240
+ await safe_send_json(
241
+ {
242
+ "type": "agent_status",
243
+ "agent": "all",
244
+ "status": "complete",
245
+ "all_agents": final_agent_status,
246
+ }
247
+ )
248
+
249
+ # Send final result
250
+ final_res = {
251
+ "type": "result",
252
+ "final_answer": final_answer,
253
+ "output_dir": output_dir_str,
254
+ "metadata": result.get("metadata"),
255
+ }
256
+ await safe_send_json(final_res)
257
+
258
+ # Save to history
259
+ history_manager.add_entry(
260
+ activity_type=ActivityType.SOLVE,
261
+ title=question[:50] + "..." if len(question) > 50 else question,
262
+ content={
263
+ "question": question,
264
+ "answer": result.get("final_answer"),
265
+ "kb_name": kb_name,
266
+ },
267
+ summary=(
268
+ result.get("final_answer")[:100] + "..." if result.get("final_answer") else ""
269
+ ),
270
+ )
271
+
272
+ except Exception as e:
273
+ # Mark connection as closed before sending error (to prevent log_pusher from interfering)
274
+ connection_closed.set()
275
+ await safe_send_json({"type": "error", "content": str(e)})
276
+ logger.error(f"[{task_id if 'task_id' in locals() else 'unknown'}] Solving failed: {e}")
277
+ if "task_id" in locals():
278
+ task_manager.update_task_status(task_id, "error", error=str(e))
279
+ finally:
280
+ # Stop log pusher first
281
+ connection_closed.set()
282
+ if pusher_task:
283
+ pusher_task.cancel()
284
+ try:
285
+ await pusher_task
286
+ except asyncio.CancelledError:
287
+ pass
288
+ except Exception as e:
289
+ logger.debug(f"Error waiting for pusher task: {e}")
290
+
291
+ # Close WebSocket connection
292
+ try:
293
+ # Check if connection is still open before closing
294
+ if hasattr(websocket, "client_state"):
295
+ state = websocket.client_state
296
+ if hasattr(state, "name") and state.name != "DISCONNECTED":
297
+ await websocket.close()
298
+ else:
299
+ # Fallback: try to close anyway
300
+ await websocket.close()
301
+ except (WebSocketDisconnect, RuntimeError, ConnectionError):
302
+ # Connection already closed, ignore
303
+ pass
304
+ except Exception as e:
305
+ logger.debug(f"Error closing WebSocket: {e}")
@@ -0,0 +1,252 @@
1
+ """
2
+ System Status API Router
3
+ Manages system status checks and model connection tests
4
+ """
5
+
6
+ from datetime import datetime
7
+ import time
8
+
9
+ from fastapi import APIRouter
10
+ from pydantic import BaseModel
11
+
12
+ from src.services.embedding import get_embedding_client, get_embedding_config
13
+ from src.services.llm import complete as llm_complete
14
+ from src.services.llm import get_llm_config, get_token_limit_kwargs
15
+ from src.services.tts import get_tts_config
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ class TestResponse(BaseModel):
21
+ success: bool
22
+ message: str
23
+ model: str | None = None
24
+ response_time_ms: float | None = None
25
+ error: str | None = None
26
+
27
+
28
+ @router.get("/status")
29
+ async def get_system_status():
30
+ """
31
+ Get overall system status including backend and model configurations
32
+
33
+ Returns:
34
+ Dictionary containing status of backend, LLM, embeddings, and TTS
35
+ """
36
+ result = {
37
+ "backend": {"status": "online", "timestamp": datetime.now().isoformat()},
38
+ "llm": {"status": "unknown", "model": None, "testable": True},
39
+ "embeddings": {"status": "unknown", "model": None, "testable": True},
40
+ "tts": {"status": "unknown", "model": None, "testable": True},
41
+ }
42
+
43
+ # Check backend status (this endpoint itself proves backend is online)
44
+ result["backend"]["status"] = "online"
45
+
46
+ # Check LLM configuration
47
+ try:
48
+ llm_config = get_llm_config()
49
+ result["llm"]["model"] = llm_config.model
50
+ result["llm"]["status"] = "configured"
51
+ except ValueError as e:
52
+ result["llm"]["status"] = "not_configured"
53
+ result["llm"]["error"] = str(e)
54
+ except Exception as e:
55
+ result["llm"]["status"] = "error"
56
+ result["llm"]["error"] = str(e)
57
+
58
+ # Check Embeddings configuration
59
+ try:
60
+ embedding_config = get_embedding_config()
61
+ result["embeddings"]["model"] = embedding_config.model
62
+ result["embeddings"]["status"] = "configured"
63
+ except ValueError as e:
64
+ result["embeddings"]["status"] = "not_configured"
65
+ result["embeddings"]["error"] = str(e)
66
+ except Exception as e:
67
+ result["embeddings"]["status"] = "error"
68
+ result["embeddings"]["error"] = str(e)
69
+
70
+ # Check TTS configuration
71
+ try:
72
+ tts_config = get_tts_config()
73
+ result["tts"]["model"] = tts_config.get("model")
74
+ result["tts"]["status"] = "configured"
75
+ except ValueError as e:
76
+ result["tts"]["status"] = "not_configured"
77
+ result["tts"]["error"] = str(e)
78
+ except Exception as e:
79
+ result["tts"]["status"] = "error"
80
+ result["tts"]["error"] = str(e)
81
+
82
+ return result
83
+
84
+
85
+ @router.post("/test/llm", response_model=TestResponse)
86
+ async def test_llm_connection():
87
+ """
88
+ Test LLM model connection by sending a simple completion request
89
+
90
+ Returns:
91
+ Test result with success status and response time
92
+ """
93
+ start_time = time.time()
94
+
95
+ try:
96
+ llm_config = get_llm_config()
97
+ model = llm_config.model
98
+ base_url = llm_config.base_url.rstrip("/")
99
+
100
+ # Sanitize Base URL (remove /chat/completions suffix if present)
101
+ for suffix in ["/chat/completions", "/completions"]:
102
+ if base_url.endswith(suffix):
103
+ base_url = base_url[: -len(suffix)]
104
+
105
+ # Handle API Key (inject dummy if missing for local LLMs)
106
+ api_key = llm_config.api_key
107
+ if not api_key:
108
+ api_key = "sk-no-key-required"
109
+
110
+ # Send a minimal test request with a prompt that guarantees output
111
+ test_prompt = "Say 'OK' to confirm you are working. Do not produce long output."
112
+ token_kwargs = get_token_limit_kwargs(model, max_tokens=200)
113
+
114
+ response = await llm_complete(
115
+ model=model,
116
+ prompt=test_prompt,
117
+ system_prompt="You are a helpful assistant. Respond briefly.",
118
+ binding=llm_config.binding,
119
+ api_key=api_key,
120
+ base_url=base_url,
121
+ temperature=0.1,
122
+ **token_kwargs,
123
+ )
124
+
125
+ response_time = (time.time() - start_time) * 1000
126
+
127
+ if response and len(response.strip()) > 0:
128
+ return TestResponse(
129
+ success=True,
130
+ message="LLM connection successful",
131
+ model=model,
132
+ response_time_ms=round(response_time, 2),
133
+ )
134
+ return TestResponse(
135
+ success=False,
136
+ message="LLM connection failed: Empty response",
137
+ model=model,
138
+ error="Empty response from API",
139
+ )
140
+
141
+ except ValueError as e:
142
+ return TestResponse(success=False, message=f"LLM configuration error: {e!s}", error=str(e))
143
+ except Exception as e:
144
+ response_time = (time.time() - start_time) * 1000
145
+ return TestResponse(
146
+ success=False,
147
+ message=f"LLM connection failed: {e!s}",
148
+ response_time_ms=round(response_time, 2),
149
+ error=str(e),
150
+ )
151
+
152
+
153
+ @router.post("/test/embeddings", response_model=TestResponse)
154
+ async def test_embeddings_connection():
155
+ """
156
+ Test Embeddings model connection by sending a simple embedding request
157
+
158
+ Returns:
159
+ Test result with success status and response time
160
+ """
161
+ start_time = time.time()
162
+
163
+ try:
164
+ embedding_config = get_embedding_config()
165
+ embedding_client = get_embedding_client()
166
+
167
+ model = embedding_config.model
168
+ binding = embedding_config.binding
169
+
170
+ # Send a minimal test request using unified client
171
+ test_texts = ["test"]
172
+ embeddings = await embedding_client.embed(test_texts)
173
+
174
+ response_time = (time.time() - start_time) * 1000
175
+
176
+ if embeddings is not None and len(embeddings) > 0 and len(embeddings[0]) > 0:
177
+ return TestResponse(
178
+ success=True,
179
+ message=f"Embeddings connection successful ({binding} provider)",
180
+ model=model,
181
+ response_time_ms=round(response_time, 2),
182
+ )
183
+ return TestResponse(
184
+ success=False,
185
+ message="Embeddings connection failed: Empty response",
186
+ model=model,
187
+ error="Empty embedding vector",
188
+ )
189
+
190
+ except ValueError as e:
191
+ return TestResponse(
192
+ success=False, message=f"Embeddings configuration error: {e!s}", error=str(e)
193
+ )
194
+ except Exception as e:
195
+ response_time = (time.time() - start_time) * 1000
196
+ return TestResponse(
197
+ success=False,
198
+ message=f"Embeddings connection failed: {e!s}",
199
+ response_time_ms=round(response_time, 2),
200
+ error=str(e),
201
+ )
202
+
203
+
204
+ @router.post("/test/tts", response_model=TestResponse)
205
+ async def test_tts_connection():
206
+ """
207
+ Test TTS model connection by checking configuration
208
+
209
+ Note: We don't actually generate audio for testing to avoid unnecessary API calls.
210
+ We only verify the configuration is valid.
211
+
212
+ Returns:
213
+ Test result with success status
214
+ """
215
+ start_time = time.time()
216
+
217
+ try:
218
+ tts_config = get_tts_config()
219
+ model = tts_config["model"]
220
+ api_key = tts_config["api_key"]
221
+ base_url = tts_config["base_url"]
222
+
223
+ # Verify configuration is complete
224
+ if not model or not api_key or not base_url:
225
+ return TestResponse(
226
+ success=False,
227
+ message="TTS configuration incomplete",
228
+ model=model,
229
+ error="Missing required configuration",
230
+ )
231
+
232
+ # For TTS, we just verify the config is valid
233
+ # Actual audio generation would be expensive, so we skip it
234
+ response_time = (time.time() - start_time) * 1000
235
+
236
+ return TestResponse(
237
+ success=True,
238
+ message="TTS configuration valid",
239
+ model=model,
240
+ response_time_ms=round(response_time, 2),
241
+ )
242
+
243
+ except ValueError as e:
244
+ return TestResponse(success=False, message=f"TTS configuration error: {e!s}", error=str(e))
245
+ except Exception as e:
246
+ response_time = (time.time() - start_time) * 1000
247
+ return TestResponse(
248
+ success=False,
249
+ message=f"TTS connection check failed: {e!s}",
250
+ response_time_ms=round(response_time, 2),
251
+ error=str(e),
252
+ )
src/api/run_server.py ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Uvicorn Server Startup Script
4
+ Uses Python API instead of command line to avoid Windows path parsing issues.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+
10
+ # Force unbuffered output
11
+ os.environ["PYTHONUNBUFFERED"] = "1"
12
+ if hasattr(sys.stdout, "reconfigure"):
13
+ sys.stdout.reconfigure(line_buffering=True)
14
+ if hasattr(sys.stderr, "reconfigure"):
15
+ sys.stderr.reconfigure(line_buffering=True)
16
+
17
+ from pathlib import Path
18
+
19
+ import uvicorn
20
+
21
+ if __name__ == "__main__":
22
+ # Get project root directory
23
+ project_root = Path(__file__).parent.parent.parent
24
+
25
+ # Change to project root to ensure correct module imports
26
+ os.chdir(str(project_root))
27
+
28
+ # Ensure project root is in Python path
29
+ if str(project_root) not in sys.path:
30
+ sys.path.insert(0, str(project_root))
31
+
32
+ # Get port from configuration
33
+ from src.services.setup import get_backend_port
34
+
35
+ backend_port = get_backend_port(project_root)
36
+
37
+ # Configure reload_excludes to skip directories that shouldn't trigger reloads
38
+ # Use absolute paths to ensure they're properly resolved
39
+ reload_excludes = [
40
+ str(project_root / "venv"), # Virtual environment
41
+ str(project_root / ".venv"), # Virtual environment (alternative name)
42
+ str(project_root / "data"), # Data directory (includes knowledge_bases, user data, logs)
43
+ str(project_root / "node_modules"), # Node modules (if any at root)
44
+ str(project_root / "web" / "node_modules"), # Web node modules
45
+ str(project_root / "web" / ".next"), # Next.js build
46
+ str(project_root / ".git"), # Git directory
47
+ str(project_root / "scripts"), # Scripts directory - don't reload on launcher changes
48
+ ]
49
+
50
+ # Filter out non-existent directories to avoid warnings
51
+ reload_excludes = [d for d in reload_excludes if Path(d).exists()]
52
+
53
+ # Start uvicorn server with reload enabled
54
+ uvicorn.run(
55
+ "src.api.main:app",
56
+ host="0.0.0.0",
57
+ port=backend_port,
58
+ reload=True,
59
+ reload_excludes=reload_excludes,
60
+ log_level="info",
61
+ )