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
src/api/main.py ADDED
@@ -0,0 +1,240 @@
1
+ from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ from src.api.routers import (
9
+ agent_config,
10
+ chat,
11
+ co_writer,
12
+ config,
13
+ dashboard,
14
+ guide,
15
+ ideagen,
16
+ knowledge,
17
+ notebook,
18
+ question,
19
+ research,
20
+ settings,
21
+ solve,
22
+ system,
23
+ )
24
+ from src.logging import get_logger
25
+
26
+ logger = get_logger("API")
27
+
28
+ CONFIG_DRIFT_ERROR_TEMPLATE = (
29
+ "Configuration Drift Detected: Tools {drift} found in agents.yaml "
30
+ "investigate.valid_tools but missing from main.yaml solve.valid_tools. "
31
+ "Add these tools to main.yaml solve.valid_tools or remove them from "
32
+ "agents.yaml investigate.valid_tools."
33
+ )
34
+
35
+
36
+ def validate_tool_consistency():
37
+ """
38
+ Validate that the tools configured for agents are consistent with the main application
39
+ configuration.
40
+
41
+ This function loads the main configuration (``main.yaml``) and the agents configuration
42
+ (``agents.yaml``) from the project root and compares:
43
+
44
+ * ``solve.valid_tools`` in ``main.yaml``
45
+ * ``investigate.valid_tools`` in ``agents.yaml``
46
+
47
+ All tools referenced by agents must be present in the main configuration. If any tools are
48
+ defined for agents that are not listed in the main configuration, a ``RuntimeError`` is
49
+ raised describing the drift. The error is logged and re-raised, which causes the FastAPI
50
+ application startup to fail when this function is called from the ``lifespan`` handler.
51
+
52
+ Impact on startup
53
+ ------------------
54
+ This validation runs during application startup. Any configuration drift will:
55
+
56
+ * Be logged as an error with details about the unknown tools.
57
+ * Prevent the API from starting until the configuration is corrected.
58
+
59
+ How to resolve configuration drift
60
+ ----------------------------------
61
+ If startup fails with a configuration drift error:
62
+
63
+ 1. Inspect the set of tools reported in the error message.
64
+ 2. Either:
65
+ * Add the missing tools to ``solve.valid_tools`` in ``main.yaml``, **or**
66
+ * Remove or rename the offending tools from ``investigate.valid_tools`` in ``agents.yaml``.
67
+ 3. Restart the application after updating the configuration files.
68
+
69
+ Example of aligned configuration
70
+ --------------------------------
71
+ ``main.yaml``::
72
+
73
+ solve:
74
+ valid_tools:
75
+ - web_search
76
+ - code_execution
77
+
78
+ ``agents.yaml``::
79
+
80
+ investigate:
81
+ valid_tools:
82
+ - web_search
83
+
84
+ In this case, validation passes because ``investigate.valid_tools`` is a subset of
85
+ ``solve.valid_tools``.
86
+
87
+ Example of configuration drift
88
+ ------------------------------
89
+ ``agents.yaml``::
90
+
91
+ investigate:
92
+ valid_tools:
93
+ - web_search
94
+ - unknown_tool
95
+
96
+ Here, ``unknown_tool`` is not present in ``solve.valid_tools`` in ``main.yaml``, so
97
+ validation will fail and prevent the application from starting until the configurations
98
+ are aligned.
99
+ """
100
+ try:
101
+ from src.services.config import load_config_with_main
102
+
103
+ project_root = Path(__file__).parent.parent.parent
104
+ main_config = load_config_with_main("main.yaml", project_root)
105
+ agent_config_data = load_config_with_main("agents.yaml", project_root)
106
+
107
+ main_tools = set(main_config.get("solve", {}).get("valid_tools", []))
108
+ agent_tools = set(agent_config_data.get("investigate", {}).get("valid_tools", []))
109
+
110
+ if not agent_tools.issubset(main_tools):
111
+ drift = agent_tools - main_tools
112
+ raise RuntimeError(CONFIG_DRIFT_ERROR_TEMPLATE.format(drift=drift))
113
+ except RuntimeError:
114
+ logger.exception("Configuration validation failed")
115
+ raise
116
+ except Exception:
117
+ logger.exception("Failed to load configuration for validation")
118
+ raise
119
+
120
+
121
+ @asynccontextmanager
122
+ async def lifespan(app: FastAPI):
123
+ """
124
+ Application lifecycle management
125
+ Gracefully handle startup and shutdown events, avoid CancelledError
126
+ """
127
+ # Execute on startup
128
+ logger.info("Application startup")
129
+
130
+ # Validate configuration consistency
131
+ validate_tool_consistency()
132
+
133
+ yield
134
+ # Execute on shutdown
135
+ logger.info("Application shutdown")
136
+
137
+
138
+ app = FastAPI(
139
+ title="DeepTutor API",
140
+ version="1.0.0",
141
+ lifespan=lifespan,
142
+ # Disable automatic trailing slash redirects to prevent protocol downgrade issues
143
+ # when deployed behind HTTPS reverse proxies (e.g., nginx).
144
+ # Without this, FastAPI's 307 redirects may change HTTPS to HTTP.
145
+ # See: https://github.com/HKUDS/DeepTutor/issues/112
146
+ redirect_slashes=False,
147
+ )
148
+
149
+ # Configure CORS
150
+ app.add_middleware(
151
+ CORSMiddleware,
152
+ allow_origins=["*"], # In production, replace with specific frontend origin
153
+ allow_credentials=True,
154
+ allow_methods=["*"],
155
+ allow_headers=["*"],
156
+ )
157
+
158
+ # Mount user directory as static root for generated artifacts
159
+ # This allows frontend to access generated artifacts (images, PDFs, etc.)
160
+ # URL: /api/outputs/solve/solve_xxx/artifacts/image.png
161
+ # Physical Path: DeepTutor/data/user/solve/solve_xxx/artifacts/image.png
162
+ project_root = Path(__file__).parent.parent.parent
163
+ user_dir = project_root / "data" / "user"
164
+
165
+ # Initialize user directories on startup
166
+ try:
167
+ from src.services.setup import init_user_directories
168
+
169
+ init_user_directories(project_root)
170
+ except Exception:
171
+ # Fallback: just create the main directory if it doesn't exist
172
+ if not user_dir.exists():
173
+ user_dir.mkdir(parents=True)
174
+
175
+ app.mount("/api/outputs", StaticFiles(directory=str(user_dir)), name="outputs")
176
+
177
+ # Include routers
178
+ app.include_router(solve.router, prefix="/api/v1", tags=["solve"])
179
+ app.include_router(chat.router, prefix="/api/v1", tags=["chat"])
180
+ app.include_router(question.router, prefix="/api/v1/question", tags=["question"])
181
+ app.include_router(research.router, prefix="/api/v1/research", tags=["research"])
182
+ app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
183
+ app.include_router(dashboard.router, prefix="/api/v1/dashboard", tags=["dashboard"])
184
+ app.include_router(co_writer.router, prefix="/api/v1/co_writer", tags=["co_writer"])
185
+ app.include_router(notebook.router, prefix="/api/v1/notebook", tags=["notebook"])
186
+ app.include_router(guide.router, prefix="/api/v1/guide", tags=["guide"])
187
+ app.include_router(ideagen.router, prefix="/api/v1/ideagen", tags=["ideagen"])
188
+ app.include_router(settings.router, prefix="/api/v1/settings", tags=["settings"])
189
+ app.include_router(system.router, prefix="/api/v1/system", tags=["system"])
190
+ app.include_router(config.router, prefix="/api/v1/config", tags=["config"])
191
+ app.include_router(agent_config.router, prefix="/api/v1/agent-config", tags=["agent-config"])
192
+
193
+
194
+ @app.get("/")
195
+ async def root():
196
+ return {"message": "Welcome to DeepTutor API"}
197
+
198
+
199
+ if __name__ == "__main__":
200
+ from pathlib import Path
201
+
202
+ import uvicorn
203
+
204
+ # Get project root directory
205
+ project_root = Path(__file__).parent.parent.parent
206
+
207
+ # Ensure project root is in Python path
208
+ import sys
209
+
210
+ if str(project_root) not in sys.path:
211
+ sys.path.insert(0, str(project_root))
212
+
213
+ # Get port from configuration
214
+ from src.services.setup import get_backend_port
215
+
216
+ backend_port = get_backend_port(project_root)
217
+
218
+ # Configure reload_excludes with absolute paths to properly exclude directories
219
+ venv_dir = project_root / "venv"
220
+ data_dir = project_root / "data"
221
+ reload_excludes = [
222
+ str(d)
223
+ for d in [
224
+ venv_dir,
225
+ project_root / ".venv",
226
+ data_dir,
227
+ project_root / "web" / "node_modules",
228
+ project_root / "web" / ".next",
229
+ project_root / ".git",
230
+ ]
231
+ if d.exists()
232
+ ]
233
+
234
+ uvicorn.run(
235
+ "api.main:app",
236
+ host="0.0.0.0",
237
+ port=backend_port,
238
+ reload=True,
239
+ reload_excludes=reload_excludes,
240
+ )
@@ -0,0 +1 @@
1
+ # Init file for routers
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Agent Configuration API - Provides agent metadata for data-driven UI.
4
+ """
5
+
6
+ from fastapi import APIRouter
7
+
8
+ router = APIRouter()
9
+
10
+ # Agent registry - single source of truth for agent UI metadata
11
+ AGENT_REGISTRY = {
12
+ "solve": {
13
+ "icon": "HelpCircle",
14
+ "color": "blue",
15
+ "label_key": "Problem Solved",
16
+ },
17
+ "question": {
18
+ "icon": "FileText",
19
+ "color": "purple",
20
+ "label_key": "Question Generated",
21
+ },
22
+ "research": {
23
+ "icon": "Search",
24
+ "color": "emerald",
25
+ "label_key": "Research Report",
26
+ },
27
+ "co_writer": {
28
+ "icon": "PenTool",
29
+ "color": "amber",
30
+ "label_key": "Co-Writer",
31
+ },
32
+ "guide": {
33
+ "icon": "BookOpen",
34
+ "color": "indigo",
35
+ "label_key": "Guided Learning",
36
+ },
37
+ "ideagen": {
38
+ "icon": "Lightbulb",
39
+ "color": "yellow",
40
+ "label_key": "Idea Generated",
41
+ },
42
+ }
43
+
44
+
45
+ @router.get("/agents")
46
+ async def get_agent_config():
47
+ """
48
+ Get agent UI configuration.
49
+
50
+ Returns:
51
+ Dict mapping agent type to UI metadata (icon, color, label_key)
52
+ """
53
+ return AGENT_REGISTRY
54
+
55
+
56
+ @router.get("/agents/{agent_type}")
57
+ async def get_single_agent_config(agent_type: str):
58
+ """
59
+ Get UI configuration for a specific agent.
60
+
61
+ Args:
62
+ agent_type: Agent type (solve, question, research, etc.)
63
+
64
+ Returns:
65
+ Agent UI metadata or 404 if not found
66
+ """
67
+ if agent_type in AGENT_REGISTRY:
68
+ return AGENT_REGISTRY[agent_type]
69
+ return {"error": f"Agent type '{agent_type}' not found"}
@@ -0,0 +1,296 @@
1
+ """
2
+ Chat API Router
3
+ ================
4
+
5
+ WebSocket endpoint for lightweight chat with session management.
6
+ REST endpoints for session operations.
7
+ """
8
+
9
+ from pathlib import Path
10
+ import sys
11
+
12
+ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
13
+
14
+ _project_root = Path(__file__).parent.parent.parent.parent
15
+ sys.path.insert(0, str(_project_root))
16
+
17
+ from src.agents.chat import ChatAgent, SessionManager
18
+ from src.logging import get_logger
19
+ from src.services.config import load_config_with_main
20
+ from src.services.llm.config import get_llm_config
21
+
22
+ # Initialize logger
23
+ project_root = Path(__file__).parent.parent.parent.parent
24
+ config = load_config_with_main("solve_config.yaml", project_root)
25
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
26
+ logger = get_logger("ChatAPI", level="INFO", log_dir=log_dir)
27
+
28
+ router = APIRouter()
29
+
30
+ # Initialize session manager
31
+ session_manager = SessionManager()
32
+
33
+
34
+ # =============================================================================
35
+ # REST Endpoints for Session Management
36
+ # =============================================================================
37
+
38
+
39
+ @router.get("/chat/sessions")
40
+ async def list_sessions(limit: int = 20):
41
+ """
42
+ List recent chat sessions.
43
+
44
+ Args:
45
+ limit: Maximum number of sessions to return
46
+
47
+ Returns:
48
+ List of session summaries
49
+ """
50
+ return session_manager.list_sessions(limit=limit, include_messages=False)
51
+
52
+
53
+ @router.get("/chat/sessions/{session_id}")
54
+ async def get_session(session_id: str):
55
+ """
56
+ Get a specific chat session with full message history.
57
+
58
+ Args:
59
+ session_id: Session identifier
60
+
61
+ Returns:
62
+ Complete session data including messages
63
+ """
64
+ session = session_manager.get_session(session_id)
65
+ if not session:
66
+ raise HTTPException(status_code=404, detail="Session not found")
67
+ return session
68
+
69
+
70
+ @router.delete("/chat/sessions/{session_id}")
71
+ async def delete_session(session_id: str):
72
+ """
73
+ Delete a chat session.
74
+
75
+ Args:
76
+ session_id: Session identifier
77
+
78
+ Returns:
79
+ Success message
80
+ """
81
+ if session_manager.delete_session(session_id):
82
+ return {"status": "deleted", "session_id": session_id}
83
+ raise HTTPException(status_code=404, detail="Session not found")
84
+
85
+
86
+ # =============================================================================
87
+ # WebSocket Endpoint for Chat
88
+ # =============================================================================
89
+
90
+
91
+ @router.websocket("/chat")
92
+ async def websocket_chat(websocket: WebSocket):
93
+ """
94
+ WebSocket endpoint for chat with session and context management.
95
+
96
+ Message format:
97
+ {
98
+ "message": str, # User message
99
+ "session_id": str | null, # Session ID (null for new session)
100
+ "history": [...] | null, # Optional: explicit history override
101
+ "kb_name": str, # Knowledge base name (for RAG)
102
+ "enable_rag": bool, # Enable RAG retrieval
103
+ "enable_web_search": bool # Enable Web Search
104
+ }
105
+
106
+ Response format:
107
+ - {"type": "session", "session_id": str} # Session ID (new or existing)
108
+ - {"type": "status", "stage": str, "message": str} # Status updates
109
+ - {"type": "stream", "content": str} # Streaming response chunks
110
+ - {"type": "sources", "rag": list, "web": list} # Source citations
111
+ - {"type": "result", "content": str} # Final complete response
112
+ - {"type": "error", "message": str} # Error message
113
+ """
114
+ await websocket.accept()
115
+
116
+ # Get system language for agent
117
+ language = config.get("system", {}).get("language", "en")
118
+
119
+ try:
120
+ while True:
121
+ # Receive message
122
+ data = await websocket.receive_json()
123
+ message = data.get("message", "").strip()
124
+ session_id = data.get("session_id")
125
+ explicit_history = data.get("history") # Optional override
126
+ kb_name = data.get("kb_name", "")
127
+ enable_rag = data.get("enable_rag", False)
128
+ enable_web_search = data.get("enable_web_search", False)
129
+
130
+ if not message:
131
+ await websocket.send_json({"type": "error", "message": "Message is required"})
132
+ continue
133
+
134
+ logger.info(
135
+ f"Chat request: session={session_id}, "
136
+ f"message={message[:50]}..., rag={enable_rag}, web={enable_web_search}"
137
+ )
138
+
139
+ try:
140
+ # Get or create session
141
+ if session_id:
142
+ session = session_manager.get_session(session_id)
143
+ if not session:
144
+ # Session not found, create new one
145
+ session = session_manager.create_session(
146
+ title=message[:50] + ("..." if len(message) > 50 else ""),
147
+ settings={
148
+ "kb_name": kb_name,
149
+ "enable_rag": enable_rag,
150
+ "enable_web_search": enable_web_search,
151
+ },
152
+ )
153
+ session_id = session["session_id"]
154
+ else:
155
+ # Create new session
156
+ session = session_manager.create_session(
157
+ title=message[:50] + ("..." if len(message) > 50 else ""),
158
+ settings={
159
+ "kb_name": kb_name,
160
+ "enable_rag": enable_rag,
161
+ "enable_web_search": enable_web_search,
162
+ },
163
+ )
164
+ session_id = session["session_id"]
165
+
166
+ # Send session ID to frontend
167
+ await websocket.send_json(
168
+ {
169
+ "type": "session",
170
+ "session_id": session_id,
171
+ }
172
+ )
173
+
174
+ # Build history from session or explicit override
175
+ if explicit_history is not None:
176
+ history = explicit_history
177
+ else:
178
+ # Get history from session messages
179
+ history = [
180
+ {"role": msg["role"], "content": msg["content"]}
181
+ for msg in session.get("messages", [])
182
+ ]
183
+
184
+ # Add user message to session
185
+ session_manager.add_message(
186
+ session_id=session_id,
187
+ role="user",
188
+ content=message,
189
+ )
190
+
191
+ # Initialize ChatAgent
192
+ try:
193
+ llm_config = get_llm_config()
194
+ api_key = llm_config.api_key
195
+ base_url = llm_config.base_url
196
+ api_version = getattr(llm_config, "api_version", None)
197
+ except Exception:
198
+ api_key = None
199
+ base_url = None
200
+ api_version = None
201
+
202
+ agent = ChatAgent(
203
+ language=language,
204
+ config=config,
205
+ api_key=api_key,
206
+ base_url=base_url,
207
+ api_version=api_version,
208
+ )
209
+
210
+ # Send status updates
211
+ if enable_rag and kb_name:
212
+ await websocket.send_json(
213
+ {
214
+ "type": "status",
215
+ "stage": "rag",
216
+ "message": f"Searching knowledge base: {kb_name}...",
217
+ }
218
+ )
219
+
220
+ if enable_web_search:
221
+ await websocket.send_json(
222
+ {
223
+ "type": "status",
224
+ "stage": "web",
225
+ "message": "Searching the web...",
226
+ }
227
+ )
228
+
229
+ await websocket.send_json(
230
+ {
231
+ "type": "status",
232
+ "stage": "generating",
233
+ "message": "Generating response...",
234
+ }
235
+ )
236
+
237
+ # Process with streaming
238
+ full_response = ""
239
+ sources = {"rag": [], "web": []}
240
+
241
+ stream_generator = await agent.process(
242
+ message=message,
243
+ history=history,
244
+ kb_name=kb_name,
245
+ enable_rag=enable_rag,
246
+ enable_web_search=enable_web_search,
247
+ stream=True,
248
+ )
249
+
250
+ async for chunk_data in stream_generator:
251
+ if chunk_data["type"] == "chunk":
252
+ await websocket.send_json(
253
+ {
254
+ "type": "stream",
255
+ "content": chunk_data["content"],
256
+ }
257
+ )
258
+ full_response += chunk_data["content"]
259
+ elif chunk_data["type"] == "complete":
260
+ full_response = chunk_data["response"]
261
+ sources = chunk_data.get("sources", {"rag": [], "web": []})
262
+
263
+ # Send sources if any
264
+ if sources.get("rag") or sources.get("web"):
265
+ await websocket.send_json({"type": "sources", **sources})
266
+
267
+ # Send final result
268
+ await websocket.send_json(
269
+ {
270
+ "type": "result",
271
+ "content": full_response,
272
+ }
273
+ )
274
+
275
+ # Save assistant message to session
276
+ session_manager.add_message(
277
+ session_id=session_id,
278
+ role="assistant",
279
+ content=full_response,
280
+ sources=sources if (sources.get("rag") or sources.get("web")) else None,
281
+ )
282
+
283
+ logger.info(f"Chat completed: session={session_id}, {len(full_response)} chars")
284
+
285
+ except Exception as e:
286
+ logger.error(f"Chat processing error: {e}")
287
+ await websocket.send_json({"type": "error", "message": str(e)})
288
+
289
+ except WebSocketDisconnect:
290
+ logger.debug("Client disconnected from chat")
291
+ except Exception as e:
292
+ logger.error(f"WebSocket error: {e}")
293
+ try:
294
+ await websocket.send_json({"type": "error", "message": str(e)})
295
+ except Exception:
296
+ pass