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,394 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ import sys
5
+ import traceback
6
+ from typing import Any
7
+
8
+ from fastapi import APIRouter, WebSocket
9
+ from pydantic import BaseModel
10
+
11
+ from src.agents.research.agents import RephraseAgent
12
+ from src.agents.research.research_pipeline import ResearchPipeline
13
+ from src.api.utils.history import ActivityType, history_manager
14
+ from src.api.utils.task_id_manager import TaskIDManager
15
+ from src.logging import get_logger
16
+ from src.services.config import load_config_with_main
17
+ from src.services.llm import get_llm_config
18
+
19
+ # Force stdout to use utf-8 to prevent encoding errors with emojis on Windows
20
+ if sys.platform == "win32":
21
+ sys.stdout.reconfigure(encoding="utf-8")
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ # Helper to load config (with main.yaml merge)
27
+ def load_config():
28
+ project_root = Path(__file__).parent.parent.parent.parent
29
+ return load_config_with_main("research_config.yaml", project_root)
30
+
31
+
32
+ # Initialize logger with config
33
+ config = load_config()
34
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
35
+ logger = get_logger("ResearchAPI", log_dir=log_dir)
36
+
37
+
38
+ class OptimizeRequest(BaseModel):
39
+ topic: str
40
+ iteration: int = 0
41
+ previous_result: dict[str, Any] | None = None
42
+ kb_name: str | None = "ai_textbook"
43
+
44
+
45
+ @router.post("/optimize_topic")
46
+ async def optimize_topic(request: OptimizeRequest):
47
+ try:
48
+ config = load_config()
49
+
50
+ # Inject API keys
51
+ try:
52
+ llm_config = get_llm_config()
53
+ api_key = llm_config.api_key
54
+ base_url = llm_config.base_url
55
+ except Exception as e:
56
+ return {"error": f"LLM config error: {e!s}"}
57
+
58
+ # Init Agent
59
+ agent = RephraseAgent(config=config, api_key=api_key, base_url=base_url)
60
+
61
+ # Process
62
+ # If iteration > 0, topic is treated as feedback
63
+ if request.iteration == 0:
64
+ result = await agent.process(request.topic, iteration=0)
65
+ else:
66
+ result = await agent.process(
67
+ request.topic, iteration=request.iteration, previous_result=request.previous_result
68
+ )
69
+
70
+ return result
71
+
72
+ except Exception as e:
73
+ traceback.print_exc()
74
+ return {"error": str(e)}
75
+
76
+
77
+ @router.websocket("/run")
78
+ async def websocket_research_run(websocket: WebSocket):
79
+ await websocket.accept()
80
+
81
+ # Get task ID manager
82
+ task_manager = TaskIDManager.get_instance()
83
+
84
+ pusher_task = None
85
+ progress_pusher_task = None
86
+ original_stdout = sys.stdout # Save original stdout at the start
87
+
88
+ try:
89
+ # 1. Wait for config
90
+ data = await websocket.receive_json()
91
+ topic = data.get("topic")
92
+ kb_name = data.get("kb_name", "ai_textbook")
93
+ # New unified parameters
94
+ plan_mode = data.get("plan_mode", "medium") # quick, medium, deep, auto
95
+ enabled_tools = data.get("enabled_tools", ["RAG"]) # RAG, Paper, Web
96
+ skip_rephrase = data.get("skip_rephrase", False)
97
+ # Legacy support
98
+ preset = data.get("preset") # For backward compatibility
99
+ research_mode = data.get("research_mode")
100
+
101
+ if not topic:
102
+ await websocket.send_json({"type": "error", "content": "Topic is required"})
103
+ return
104
+
105
+ # Generate task ID
106
+ task_key = f"research_{kb_name}_{hash(str(topic))}"
107
+ task_id = task_manager.generate_task_id("research", task_key)
108
+
109
+ # Send task ID to frontend
110
+ await websocket.send_json({"type": "task_id", "task_id": task_id})
111
+
112
+ # Use unified logger
113
+ config = load_config()
114
+ try:
115
+ # Get log_dir from config
116
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get(
117
+ "log_dir"
118
+ )
119
+ research_logger = get_logger("Research", log_dir=log_dir)
120
+ research_logger.info(f"[{task_id}] Starting research flow: {topic[:50]}...")
121
+ except Exception as e:
122
+ logger.warning(f"Failed to initialize research logger: {e}")
123
+
124
+ # 2. Initialize Pipeline
125
+ # Initialize nested config structures from research.* (main.yaml structure)
126
+ # This ensures all research module configs are properly inherited from main.yaml
127
+ research_config = config.get("research", {})
128
+
129
+ # Initialize planning config from research.planning
130
+ if "planning" not in config:
131
+ config["planning"] = research_config.get("planning", {}).copy()
132
+ else:
133
+ # Merge with research.planning defaults
134
+ default_planning = research_config.get("planning", {})
135
+ for key, value in default_planning.items():
136
+ if key not in config["planning"]:
137
+ config["planning"][key] = value if not isinstance(value, dict) else value.copy()
138
+ elif isinstance(value, dict) and isinstance(config["planning"][key], dict):
139
+ # Deep merge for nested dicts like decompose, rephrase
140
+ for k, v in value.items():
141
+ if k not in config["planning"][key]:
142
+ config["planning"][key][k] = v
143
+
144
+ # Ensure decompose and rephrase exist
145
+ if "decompose" not in config["planning"]:
146
+ config["planning"]["decompose"] = {}
147
+ if "rephrase" not in config["planning"]:
148
+ config["planning"]["rephrase"] = {}
149
+
150
+ # Initialize researching config from research.researching
151
+ # This ensures execution_mode, max_parallel_topics etc. are properly inherited
152
+ if "researching" not in config:
153
+ config["researching"] = research_config.get("researching", {}).copy()
154
+ else:
155
+ # Merge with research.researching defaults (research.researching has lower priority)
156
+ default_researching = research_config.get("researching", {})
157
+ for key, value in default_researching.items():
158
+ if key not in config["researching"]:
159
+ config["researching"][key] = value
160
+
161
+ # Initialize reporting config from research.reporting
162
+ # This ensures enable_citation_list, enable_inline_citations etc. are properly inherited
163
+ if "reporting" not in config:
164
+ config["reporting"] = research_config.get("reporting", {}).copy()
165
+ else:
166
+ # Merge with research.reporting defaults
167
+ default_reporting = research_config.get("reporting", {})
168
+ for key, value in default_reporting.items():
169
+ if key not in config["reporting"]:
170
+ config["reporting"][key] = value
171
+
172
+ # Apply plan_mode configuration (unified approach affecting both planning and researching)
173
+ # Each mode defines:
174
+ # - Planning: tree depth (subtopics count) and mode (manual/auto)
175
+ # - Researching: max iterations per topic and iteration_mode (fixed/flexible)
176
+ plan_mode_config = {
177
+ "quick": {
178
+ "planning": {"decompose": {"initial_subtopics": 2, "mode": "manual"}},
179
+ "researching": {"max_iterations": 2, "iteration_mode": "fixed"},
180
+ },
181
+ "medium": {
182
+ "planning": {"decompose": {"initial_subtopics": 5, "mode": "manual"}},
183
+ "researching": {"max_iterations": 4, "iteration_mode": "fixed"},
184
+ },
185
+ "deep": {
186
+ "planning": {"decompose": {"initial_subtopics": 8, "mode": "manual"}},
187
+ "researching": {"max_iterations": 7, "iteration_mode": "fixed"},
188
+ },
189
+ "auto": {
190
+ "planning": {"decompose": {"mode": "auto", "auto_max_subtopics": 8}},
191
+ "researching": {"max_iterations": 6, "iteration_mode": "flexible"},
192
+ },
193
+ }
194
+ if plan_mode in plan_mode_config:
195
+ mode_cfg = plan_mode_config[plan_mode]
196
+ # Apply planning configuration
197
+ if "planning" in mode_cfg:
198
+ for key, value in mode_cfg["planning"].items():
199
+ if key not in config["planning"]:
200
+ config["planning"][key] = {}
201
+ config["planning"][key].update(value)
202
+ # Apply researching configuration
203
+ if "researching" in mode_cfg:
204
+ config["researching"].update(mode_cfg["researching"])
205
+
206
+ # Legacy preset support (for backward compatibility)
207
+ if preset and "presets" in config and preset in config["presets"]:
208
+ preset_config = config["presets"][preset]
209
+ for key, value in preset_config.items():
210
+ if key in config and isinstance(value, dict):
211
+ config[key].update(value)
212
+
213
+ # Apply enabled_tools configuration
214
+ # RAG includes: rag_naive, rag_hybrid, query_item
215
+ # Paper includes: paper_search
216
+ # Web includes: web_search
217
+ # run_code is always enabled
218
+ config["researching"]["enable_rag_naive"] = "RAG" in enabled_tools
219
+ config["researching"]["enable_rag_hybrid"] = "RAG" in enabled_tools
220
+ config["researching"]["enable_query_item"] = "RAG" in enabled_tools
221
+ config["researching"]["enable_paper_search"] = "Paper" in enabled_tools
222
+ config["researching"]["enable_web_search"] = "Web" in enabled_tools
223
+ config["researching"]["enable_run_code"] = True # Always enabled
224
+
225
+ # Store enabled_tools for prompt generation
226
+ config["researching"]["enabled_tools"] = enabled_tools
227
+
228
+ # Legacy research_mode support
229
+ if research_mode:
230
+ config["researching"]["research_mode"] = research_mode
231
+
232
+ # If skip_rephrase is True, disable the internal rephrase step
233
+ if skip_rephrase:
234
+ config["planning"]["rephrase"]["enabled"] = False
235
+
236
+ # Define unified output directory
237
+ # Use project root directory user/research as unified output directory
238
+ root_dir = Path(__file__).parent.parent.parent.parent
239
+ output_base = root_dir / "data" / "user" / "research"
240
+
241
+ # Update config with unified output paths
242
+ if "system" not in config:
243
+ config["system"] = {}
244
+
245
+ config["system"]["output_base_dir"] = str(output_base / "cache")
246
+ config["system"]["reports_dir"] = str(output_base / "reports")
247
+
248
+ # Inject API keys from env if not in config
249
+ try:
250
+ llm_config = get_llm_config()
251
+ api_key = llm_config.api_key
252
+ base_url = llm_config.base_url
253
+ api_version = getattr(llm_config, "api_version", None)
254
+ except ValueError as e:
255
+ await websocket.send_json({"error": f"LLM configuration error: {e!s}"})
256
+ await websocket.close()
257
+ return
258
+
259
+ # 3. Setup Queues for log and progress
260
+ log_queue = asyncio.Queue()
261
+ progress_queue = asyncio.Queue()
262
+
263
+ # Progress callback function
264
+ def progress_callback(event: dict[str, Any]):
265
+ """Progress callback function, puts progress events into queue"""
266
+ try:
267
+ asyncio.get_event_loop().call_soon_threadsafe(progress_queue.put_nowait, event)
268
+ except Exception as e:
269
+ logger.error(f"Progress callback error: {e}")
270
+
271
+ pipeline = ResearchPipeline(
272
+ config=config,
273
+ api_key=api_key,
274
+ base_url=base_url,
275
+ api_version=api_version,
276
+ research_id=task_id,
277
+ kb_name=kb_name,
278
+ progress_callback=progress_callback,
279
+ )
280
+
281
+ # 4. Background log pusher
282
+ async def log_pusher():
283
+ while True:
284
+ try:
285
+ log = await log_queue.get()
286
+ if log is None:
287
+ break
288
+ await websocket.send_json({"type": "log", "content": log})
289
+ log_queue.task_done()
290
+ except Exception as e:
291
+ logger.error(f"Log pusher error: {e}")
292
+ break
293
+
294
+ # 5. Background progress pusher
295
+ async def progress_pusher():
296
+ while True:
297
+ try:
298
+ event = await progress_queue.get()
299
+ if event is None:
300
+ break
301
+ await websocket.send_json(event)
302
+ progress_queue.task_done()
303
+ except Exception as e:
304
+ logger.error(f"Progress pusher error: {e}")
305
+ break
306
+
307
+ pusher_task = asyncio.create_task(log_pusher())
308
+ progress_pusher_task = asyncio.create_task(progress_pusher())
309
+
310
+ # 6. Run Pipeline with stdout interception
311
+ class ResearchStdoutInterceptor:
312
+ def __init__(self, queue):
313
+ self.queue = queue
314
+ self.original_stdout = sys.stdout
315
+
316
+ def write(self, message):
317
+ # Write to terminal first to ensure terminal output is not blocked
318
+ self.original_stdout.write(message)
319
+ # Then try to send to frontend (non-blocking, failure doesn't affect terminal output)
320
+ if message.strip():
321
+ try:
322
+ # Use call_soon_threadsafe for thread safety
323
+ loop = asyncio.get_event_loop()
324
+ loop.call_soon_threadsafe(self.queue.put_nowait, message)
325
+ except (asyncio.QueueFull, RuntimeError, AttributeError):
326
+ # Queue full, event loop closed, or no event loop, ignore error, doesn't affect terminal output
327
+ pass
328
+
329
+ def flush(self):
330
+ self.original_stdout.flush()
331
+
332
+ sys.stdout = ResearchStdoutInterceptor(log_queue)
333
+
334
+ try:
335
+ await websocket.send_json(
336
+ {"type": "status", "content": "started", "research_id": pipeline.research_id}
337
+ )
338
+
339
+ result = await pipeline.run(topic)
340
+
341
+ # Send final report content
342
+ with open(result["final_report_path"], encoding="utf-8") as f:
343
+ report_content = f.read()
344
+
345
+ # Save to history
346
+ history_manager.add_entry(
347
+ activity_type=ActivityType.RESEARCH,
348
+ title=topic,
349
+ content={"topic": topic, "report": report_content, "kb_name": kb_name},
350
+ summary=f"Research ID: {result['research_id']}",
351
+ )
352
+
353
+ await websocket.send_json(
354
+ {
355
+ "type": "result",
356
+ "report": report_content,
357
+ "metadata": result["metadata"],
358
+ "research_id": result["research_id"],
359
+ }
360
+ )
361
+
362
+ # Update task status to completed
363
+ try:
364
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get(
365
+ "logging", {}
366
+ ).get("log_dir")
367
+ research_logger = get_logger("Research", log_dir=log_dir)
368
+ research_logger.success(f"[{task_id}] Research flow completed: {topic[:50]}...")
369
+ task_manager.update_task_status(task_id, "completed")
370
+ except Exception as e:
371
+ logger.warning(f"Failed to log completion: {e}")
372
+
373
+ finally:
374
+ sys.stdout = original_stdout # Safely restore using saved reference
375
+
376
+ except Exception as e:
377
+ await websocket.send_json({"type": "error", "content": str(e)})
378
+ logging.error(f"Research error: {e}", exc_info=True)
379
+
380
+ # Update task status to error
381
+ try:
382
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get(
383
+ "log_dir"
384
+ )
385
+ research_logger = get_logger("Research", log_dir=log_dir)
386
+ research_logger.error(f"[{task_id}] Research flow failed: {e}")
387
+ task_manager.update_task_status(task_id, "error", error=str(e))
388
+ except Exception as log_err:
389
+ logger.warning(f"Failed to log error: {log_err}")
390
+ finally:
391
+ if pusher_task:
392
+ pusher_task.cancel()
393
+ if progress_pusher_task:
394
+ progress_pusher_task.cancel()
@@ -0,0 +1,164 @@
1
+ """
2
+ Settings API Router (Simplified)
3
+ ================================
4
+
5
+ Manages basic UI settings: theme, language, sidebar customization.
6
+ Configuration for LLM/Embedding/TTS/Search is handled by the unified config service.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import List, Literal, Optional
12
+
13
+ from fastapi import APIRouter
14
+ from pydantic import BaseModel
15
+
16
+ router = APIRouter()
17
+
18
+ # Settings file path for UI preferences (stored in settings folder with other configs)
19
+ SETTINGS_FILE = (
20
+ Path(__file__).parent.parent.parent.parent / "data" / "user" / "settings" / "interface.json"
21
+ )
22
+
23
+ # Default sidebar navigation order
24
+ DEFAULT_SIDEBAR_NAV_ORDER = {
25
+ "start": ["/", "/history", "/knowledge", "/notebook"],
26
+ "learnResearch": ["/question", "/solver", "/guide", "/ideagen", "/research", "/co_writer"],
27
+ }
28
+
29
+ # Default UI settings
30
+ DEFAULT_UI_SETTINGS = {
31
+ "theme": "light",
32
+ "language": "en",
33
+ "sidebar_description": "✨ Data Intelligence Lab @ HKU",
34
+ "sidebar_nav_order": DEFAULT_SIDEBAR_NAV_ORDER,
35
+ }
36
+
37
+
38
+ class SidebarNavOrder(BaseModel):
39
+ start: List[str]
40
+ learnResearch: List[str]
41
+
42
+
43
+ class UISettings(BaseModel):
44
+ theme: Literal["light", "dark"] = "light"
45
+ language: Literal["zh", "en"] = "en"
46
+ sidebar_description: Optional[str] = None
47
+ sidebar_nav_order: Optional[SidebarNavOrder] = None
48
+
49
+
50
+ class ThemeUpdate(BaseModel):
51
+ theme: Literal["light", "dark"]
52
+
53
+
54
+ class LanguageUpdate(BaseModel):
55
+ language: Literal["zh", "en"]
56
+
57
+
58
+ class SidebarDescriptionUpdate(BaseModel):
59
+ description: str
60
+
61
+
62
+ class SidebarNavOrderUpdate(BaseModel):
63
+ nav_order: SidebarNavOrder
64
+
65
+
66
+ def load_ui_settings() -> dict:
67
+ """Load UI-specific settings from json file"""
68
+ if SETTINGS_FILE.exists():
69
+ try:
70
+ with open(SETTINGS_FILE, encoding="utf-8") as f:
71
+ saved = json.load(f)
72
+ return {**DEFAULT_UI_SETTINGS, **saved}
73
+ except Exception:
74
+ pass
75
+ return DEFAULT_UI_SETTINGS.copy()
76
+
77
+
78
+ def save_ui_settings(settings: dict):
79
+ """Save UI settings"""
80
+ SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
81
+ with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
82
+ json.dump(settings, f, ensure_ascii=False, indent=2)
83
+
84
+
85
+ @router.get("")
86
+ async def get_settings():
87
+ """Get UI settings."""
88
+ return {"ui": load_ui_settings()}
89
+
90
+
91
+ @router.put("/theme")
92
+ async def update_theme(update: ThemeUpdate):
93
+ """Update UI theme"""
94
+ current_ui = load_ui_settings()
95
+ current_ui["theme"] = update.theme
96
+ save_ui_settings(current_ui)
97
+ return {"theme": update.theme}
98
+
99
+
100
+ @router.put("/language")
101
+ async def update_language(update: LanguageUpdate):
102
+ """Update UI language"""
103
+ current_ui = load_ui_settings()
104
+ current_ui["language"] = update.language
105
+ save_ui_settings(current_ui)
106
+ return {"language": update.language}
107
+
108
+
109
+ @router.put("/ui")
110
+ async def update_ui_settings(update: UISettings):
111
+ """Update all UI settings"""
112
+ current_ui = load_ui_settings()
113
+ update_dict = update.model_dump(exclude_none=True)
114
+ current_ui.update(update_dict)
115
+ save_ui_settings(current_ui)
116
+ return current_ui
117
+
118
+
119
+ @router.post("/reset")
120
+ async def reset_settings():
121
+ """Reset UI settings to default"""
122
+ save_ui_settings(DEFAULT_UI_SETTINGS)
123
+ return DEFAULT_UI_SETTINGS
124
+
125
+
126
+ @router.get("/themes")
127
+ async def get_themes():
128
+ """Get available theme list"""
129
+ return {
130
+ "themes": [
131
+ {"id": "light", "name": "Light"},
132
+ {"id": "dark", "name": "Dark"},
133
+ ]
134
+ }
135
+
136
+
137
+ @router.get("/sidebar")
138
+ async def get_sidebar_settings():
139
+ """Get sidebar customization settings"""
140
+ current_ui = load_ui_settings()
141
+ return {
142
+ "description": current_ui.get(
143
+ "sidebar_description", DEFAULT_UI_SETTINGS["sidebar_description"]
144
+ ),
145
+ "nav_order": current_ui.get("sidebar_nav_order", DEFAULT_UI_SETTINGS["sidebar_nav_order"]),
146
+ }
147
+
148
+
149
+ @router.put("/sidebar/description")
150
+ async def update_sidebar_description(update: SidebarDescriptionUpdate):
151
+ """Update sidebar description"""
152
+ current_ui = load_ui_settings()
153
+ current_ui["sidebar_description"] = update.description
154
+ save_ui_settings(current_ui)
155
+ return {"description": update.description}
156
+
157
+
158
+ @router.put("/sidebar/nav-order")
159
+ async def update_sidebar_nav_order(update: SidebarNavOrderUpdate):
160
+ """Update sidebar navigation order"""
161
+ current_ui = load_ui_settings()
162
+ current_ui["sidebar_nav_order"] = update.nav_order.model_dump()
163
+ save_ui_settings(current_ui)
164
+ return {"nav_order": update.nav_order.model_dump()}