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,436 @@
1
+ """
2
+ IdeaGen API Router
3
+ Used to generate research ideas from notebook content
4
+ """
5
+
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
11
+ from pydantic import BaseModel
12
+
13
+ # Ensure project modules can be imported
14
+ project_root = Path(__file__).parent.parent.parent.parent
15
+ if str(project_root) not in sys.path:
16
+ sys.path.insert(0, str(project_root))
17
+
18
+ from src.agents.base_agent import BaseAgent
19
+ from src.agents.ideagen.idea_generation_workflow import IdeaGenerationWorkflow
20
+ from src.agents.ideagen.material_organizer_agent import MaterialOrganizerAgent
21
+ from src.api.utils.notebook_manager import NotebookManager
22
+ from src.api.utils.task_id_manager import TaskIDManager
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
+ router = APIRouter()
28
+
29
+ # Initialize logger with config
30
+ project_root = Path(__file__).parent.parent.parent.parent
31
+ config = load_config_with_main("solve_config.yaml", project_root) # Use any config to get main.yaml
32
+ log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
33
+ logger = get_logger("IdeaGen", level="INFO", log_dir=log_dir)
34
+
35
+
36
+ class IdeaGenRequest(BaseModel):
37
+ notebook_id: str
38
+ record_ids: list[str] | None = None # If None, use all records
39
+
40
+
41
+ # Define status constants to make state flow clearer
42
+ class IdeaGenStage:
43
+ """IdeaGen status stages"""
44
+
45
+ INIT = "init" # Initialization
46
+ EXTRACTING = "extracting" # Extracting knowledge points
47
+ KNOWLEDGE_EXTRACTED = "knowledge_extracted" # Knowledge points extraction completed
48
+ FILTERING = "filtering" # Loose filtering
49
+ FILTERED = "filtered" # Filtering completed
50
+ EXPLORING = "exploring" # Exploring research ideas
51
+ EXPLORED = "explored" # Exploration completed
52
+ STRICT_FILTERING = "strict_filtering" # Strict filtering
53
+ GENERATING = "generating" # Generating statement
54
+ IDEA_READY = "idea_ready" # Single idea ready
55
+ COMPLETE = "complete" # All completed
56
+ ERROR = "error" # Error
57
+
58
+
59
+ async def send_status(
60
+ websocket: WebSocket, stage: str, message: str, data: dict = None, task_id: str = None
61
+ ):
62
+ """Unified status sending function"""
63
+ payload = {
64
+ "type": "status",
65
+ "stage": stage,
66
+ "message": message,
67
+ "timestamp": datetime.now().isoformat(),
68
+ }
69
+ if data:
70
+ payload["data"] = data
71
+
72
+ await websocket.send_json(payload)
73
+
74
+ # Log to file
75
+ log_msg = f"[{stage}] {message}"
76
+ if task_id:
77
+ log_msg = f"[{task_id}] {log_msg}"
78
+ logger.info(log_msg)
79
+
80
+
81
+ @router.websocket("/generate")
82
+ async def websocket_ideagen(websocket: WebSocket):
83
+ """
84
+ WebSocket endpoint: Execute idea generation workflow
85
+
86
+ Status flow:
87
+ 1. init -> Initialization
88
+ 2. extracting -> Extract knowledge points
89
+ 3. knowledge_extracted -> Knowledge points extraction completed
90
+ 4. filtering -> Loose filtering
91
+ 5. filtered -> Filtering completed
92
+ 6. exploring -> Explore research ideas (loop)
93
+ 7. explored -> Exploration completed
94
+ 8. strict_filtering -> Strict filtering
95
+ 9. generating -> Generate statement
96
+ 10. idea_ready -> Single idea ready
97
+ 11. complete -> All completed
98
+
99
+ Request format:
100
+ {
101
+ "notebook_id": "string", // Optional, single notebook mode
102
+ "record_ids": ["id1", "id2"], // Optional, specify specific records
103
+ "records": [...], // Optional, cross-notebook mode directly provide records
104
+ "user_thoughts": "string" // Optional, user additional thoughts
105
+ }
106
+ """
107
+ await websocket.accept()
108
+ logger.info("=" * 60)
109
+ logger.info("WebSocket connection accepted")
110
+ logger.info("=" * 60)
111
+
112
+ # Get task ID manager
113
+ task_manager = TaskIDManager.get_instance()
114
+ task_id = None
115
+
116
+ try:
117
+ # Receive request data
118
+ data = await websocket.receive_json()
119
+ notebook_id = data.get("notebook_id")
120
+ record_ids = data.get("record_ids")
121
+ direct_records = data.get("records")
122
+ user_thoughts = data.get("user_thoughts", "")
123
+
124
+ logger.info(
125
+ f"Received request: notebook_id={notebook_id}, record_ids={record_ids}, direct_records_count={len(direct_records) if direct_records else 0}"
126
+ )
127
+
128
+ # Generate task ID
129
+ task_key = (
130
+ f"ideagen_{notebook_id or 'cross_notebook'}_{hash(str(direct_records or record_ids))}"
131
+ )
132
+ task_id = task_manager.generate_task_id("ideagen", task_key)
133
+
134
+ # Send task ID to frontend
135
+ await websocket.send_json({"type": "task_id", "task_id": task_id})
136
+ logger.info(f"Task ID: {task_id}")
137
+
138
+ # ========== Stage 1: INIT ==========
139
+ await send_status(
140
+ websocket,
141
+ IdeaGenStage.INIT,
142
+ "Initializing idea generation workflow...",
143
+ task_id=task_id,
144
+ )
145
+
146
+ # Reset LLM stats for this session
147
+ BaseAgent.reset_stats("ideagen")
148
+
149
+ # Get LLM configuration
150
+ llm_config = get_llm_config()
151
+
152
+ # Get records
153
+ records = []
154
+
155
+ if direct_records and isinstance(direct_records, list):
156
+ records = direct_records
157
+ logger.info(f"Using {len(records)} direct records")
158
+ elif notebook_id:
159
+ nb_manager = NotebookManager()
160
+ notebook = nb_manager.get_notebook(notebook_id)
161
+ if not notebook:
162
+ await send_status(
163
+ websocket, IdeaGenStage.ERROR, "Notebook not found", task_id=task_id
164
+ )
165
+ await websocket.close()
166
+ return
167
+
168
+ records = notebook.get("records", [])
169
+ if record_ids:
170
+ records = [r for r in records if r.get("id") in record_ids]
171
+ logger.info(f"Loaded {len(records)} records from notebook")
172
+
173
+ # Check if we have either records or user_thoughts
174
+ if not records and not user_thoughts:
175
+ await send_status(
176
+ websocket,
177
+ IdeaGenStage.ERROR,
178
+ "Please provide notebook records or describe your research topic",
179
+ task_id=task_id,
180
+ )
181
+ await websocket.close()
182
+ return
183
+
184
+ # ========== Stage 2: EXTRACTING ==========
185
+ # If we have records, extract knowledge points from them
186
+ # If only user_thoughts, create a virtual knowledge point from the text
187
+ if records:
188
+ await send_status(
189
+ websocket,
190
+ IdeaGenStage.EXTRACTING,
191
+ f"Extracting knowledge points from {len(records)} records...",
192
+ {"record_count": len(records)},
193
+ task_id=task_id,
194
+ )
195
+
196
+ organizer = MaterialOrganizerAgent(
197
+ api_key=llm_config.api_key,
198
+ base_url=llm_config.base_url,
199
+ api_version=getattr(llm_config, "api_version", None),
200
+ model=llm_config.model,
201
+ )
202
+
203
+ knowledge_points = await organizer.process(
204
+ records, user_thoughts if user_thoughts else None
205
+ )
206
+ logger.info(f"Extracted {len(knowledge_points)} knowledge points")
207
+ else:
208
+ # Text-only mode: create virtual knowledge point from user_thoughts
209
+ await send_status(
210
+ websocket,
211
+ IdeaGenStage.EXTRACTING,
212
+ "Processing your research topic description...",
213
+ {"record_count": 0, "text_only_mode": True},
214
+ task_id=task_id,
215
+ )
216
+
217
+ # Create a virtual knowledge point from user_thoughts
218
+ knowledge_points = [
219
+ {
220
+ "knowledge_point": "User Research Topic",
221
+ "description": user_thoughts.strip(),
222
+ }
223
+ ]
224
+ logger.info("Created virtual knowledge point from user thoughts (text-only mode)")
225
+
226
+ # ========== Stage 3: KNOWLEDGE_EXTRACTED ==========
227
+ await send_status(
228
+ websocket,
229
+ IdeaGenStage.KNOWLEDGE_EXTRACTED,
230
+ f"Extracted {len(knowledge_points)} knowledge points",
231
+ {"knowledge_points": knowledge_points, "count": len(knowledge_points)},
232
+ task_id=task_id,
233
+ )
234
+
235
+ if not knowledge_points:
236
+ await send_status(
237
+ websocket,
238
+ IdeaGenStage.COMPLETE,
239
+ "No valid knowledge points extracted from notes",
240
+ {"ideas": [], "count": 0},
241
+ task_id=task_id,
242
+ )
243
+ await websocket.close()
244
+ return
245
+
246
+ # ========== Stage 4: FILTERING (Loose Filter) ==========
247
+ await send_status(
248
+ websocket,
249
+ IdeaGenStage.FILTERING,
250
+ f"Filtering {len(knowledge_points)} knowledge points (loose criteria)...",
251
+ {"total": len(knowledge_points)},
252
+ task_id=task_id,
253
+ )
254
+
255
+ workflow = IdeaGenerationWorkflow(
256
+ api_key=llm_config.api_key,
257
+ base_url=llm_config.base_url,
258
+ api_version=getattr(llm_config, "api_version", None),
259
+ model=llm_config.model,
260
+ progress_callback=None, # We manually manage status here
261
+ )
262
+
263
+ filtered_points = await workflow.loose_filter(knowledge_points)
264
+ logger.info(
265
+ f"Loose filter: {len(knowledge_points)} -> {len(filtered_points)} knowledge points"
266
+ )
267
+
268
+ # ========== Stage 5: FILTERED ==========
269
+ await send_status(
270
+ websocket,
271
+ IdeaGenStage.FILTERED,
272
+ f"Filtered to {len(filtered_points)} knowledge points",
273
+ {
274
+ "filtered_points": filtered_points,
275
+ "original": len(knowledge_points),
276
+ "filtered": len(filtered_points),
277
+ },
278
+ task_id=task_id,
279
+ )
280
+
281
+ if not filtered_points:
282
+ await send_status(
283
+ websocket,
284
+ IdeaGenStage.COMPLETE,
285
+ "All knowledge points were filtered out",
286
+ {"ideas": [], "count": 0},
287
+ task_id=task_id,
288
+ )
289
+ await websocket.close()
290
+ return
291
+
292
+ # ========== Stage 6-10: Process each knowledge point ==========
293
+ all_ideas = []
294
+ total_points = len(filtered_points)
295
+
296
+ for idx, point in enumerate(filtered_points):
297
+ point_name = point.get("knowledge_point", f"Point {idx + 1}")
298
+ logger.info(f"Processing knowledge point {idx + 1}/{total_points}: {point_name}")
299
+
300
+ # ========== Stage 6: EXPLORING ==========
301
+ await send_status(
302
+ websocket,
303
+ IdeaGenStage.EXPLORING,
304
+ f"Exploring research ideas for: {point_name} ({idx + 1}/{total_points})",
305
+ {"index": idx + 1, "total": total_points, "knowledge_point": point_name},
306
+ task_id=task_id,
307
+ )
308
+
309
+ research_ideas = await workflow.explore_ideas(point)
310
+ logger.info(f"Generated {len(research_ideas)} research ideas")
311
+
312
+ # ========== Stage 7: EXPLORED ==========
313
+ await send_status(
314
+ websocket,
315
+ IdeaGenStage.EXPLORED,
316
+ f"Generated {len(research_ideas)} research ideas for: {point_name}",
317
+ {
318
+ "index": idx + 1,
319
+ "ideas_count": len(research_ideas),
320
+ "knowledge_point": point_name,
321
+ },
322
+ task_id=task_id,
323
+ )
324
+
325
+ if not research_ideas:
326
+ logger.warning("No ideas generated, skipping")
327
+ continue
328
+
329
+ # ========== Stage 8: STRICT_FILTERING ==========
330
+ await send_status(
331
+ websocket,
332
+ IdeaGenStage.STRICT_FILTERING,
333
+ f"Strictly filtering {len(research_ideas)} ideas for: {point_name}",
334
+ {
335
+ "index": idx + 1,
336
+ "ideas_count": len(research_ideas),
337
+ "knowledge_point": point_name,
338
+ },
339
+ task_id=task_id,
340
+ )
341
+
342
+ kept_ideas = await workflow.strict_filter(point, research_ideas)
343
+ logger.info(f"Kept {len(kept_ideas)} ideas after strict filter")
344
+
345
+ if not kept_ideas:
346
+ logger.warning("No ideas kept, skipping")
347
+ continue
348
+
349
+ # ========== Stage 9: GENERATING ==========
350
+ await send_status(
351
+ websocket,
352
+ IdeaGenStage.GENERATING,
353
+ f"Generating statement for: {point_name}",
354
+ {"index": idx + 1, "kept_ideas": len(kept_ideas), "knowledge_point": point_name},
355
+ task_id=task_id,
356
+ )
357
+
358
+ statement = await workflow.generate_statement(point, kept_ideas)
359
+ logger.info(f"Statement generated ({len(statement)} chars)")
360
+
361
+ idea_result = {
362
+ "id": f"idea-{idx}",
363
+ "knowledge_point": point_name,
364
+ "description": point.get("description", ""),
365
+ "research_ideas": kept_ideas,
366
+ "statement": statement,
367
+ "expanded": False,
368
+ }
369
+ all_ideas.append(idea_result)
370
+
371
+ # ========== Stage 10: IDEA_READY ==========
372
+ # Send status message
373
+ await send_status(
374
+ websocket,
375
+ IdeaGenStage.IDEA_READY,
376
+ f"Research idea ready: {point_name}",
377
+ {"index": idx + 1, "total": total_points},
378
+ task_id=task_id,
379
+ )
380
+
381
+ # Important: Also send type="idea" message, frontend needs this to render ideas
382
+ await websocket.send_json({"type": "idea", "data": idea_result})
383
+ logger.info(f"Sent idea to frontend: {point_name}")
384
+
385
+ # ========== Stage 11: COMPLETE ==========
386
+ logger.success(
387
+ f"Workflow complete: generated {len(all_ideas)} ideas from {total_points} knowledge points"
388
+ )
389
+ await send_status(
390
+ websocket,
391
+ IdeaGenStage.COMPLETE,
392
+ f"Successfully generated {len(all_ideas)} research ideas",
393
+ {"ideas": all_ideas, "count": len(all_ideas)},
394
+ task_id=task_id,
395
+ )
396
+
397
+ # Print LLM usage stats
398
+ BaseAgent.print_stats("ideagen")
399
+
400
+ # Update task status
401
+ task_manager.update_task_status(task_id, "completed")
402
+ logger.success(f"Task {task_id} completed")
403
+
404
+ except WebSocketDisconnect:
405
+ logger.info(f"WebSocket disconnected (task_id={task_id})")
406
+ except Exception as e:
407
+ logger.error(f"ERROR: {e}")
408
+
409
+ logger.exception("Exception details:")
410
+
411
+ if task_id:
412
+ task_manager.update_task_status(task_id, "error", error=str(e))
413
+
414
+ try:
415
+ await send_status(
416
+ websocket,
417
+ IdeaGenStage.ERROR,
418
+ f"Error: {e!s}",
419
+ {"error": str(e)},
420
+ task_id=task_id,
421
+ )
422
+ except (RuntimeError, WebSocketDisconnect, ConnectionError):
423
+ pass # Connection already closed
424
+ finally:
425
+ try:
426
+ await websocket.close()
427
+ logger.info("WebSocket closed")
428
+ except (RuntimeError, WebSocketDisconnect, ConnectionError):
429
+ pass # Connection already closed
430
+ logger.info("=" * 60)
431
+
432
+
433
+ @router.get("/test")
434
+ async def test_ideagen():
435
+ """Test endpoint"""
436
+ return {"status": "ok", "message": "IdeaGen API is working"}