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,172 @@
1
+ from enum import Enum
2
+ import json
3
+ from pathlib import Path
4
+ import time
5
+
6
+
7
+ class ActivityType(str, Enum):
8
+ SOLVE = "solve"
9
+ QUESTION = "question"
10
+ RESEARCH = "research"
11
+ CHAT = "chat"
12
+
13
+
14
+ class HistoryManager:
15
+ def __init__(self, base_dir: str | None = None):
16
+ """
17
+ History record manager
18
+
19
+ Args:
20
+ base_dir: History record directory. Default fixed to "project root/user",
21
+ at the same level as user/question, user/solve, user/research,
22
+ does not depend on current working directory, avoids path misalignment
23
+ when uvicorn / IDE start differently.
24
+ """
25
+ if base_dir is None:
26
+ # Current file: DeepTutor/src/api/utils/history.py
27
+ # Project root should be three levels up: DeepTutor/
28
+ project_root = Path(__file__).resolve().parents[3]
29
+ base_dir_path = project_root / "data" / "user"
30
+ else:
31
+ base_dir_path = Path(base_dir)
32
+
33
+ self.base_dir = base_dir_path
34
+ self.base_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ self.history_file = self.base_dir / "user_history.json"
37
+ self._ensure_file()
38
+
39
+ def _ensure_file(self):
40
+ """
41
+ Ensure history file exists with correct format.
42
+ If file exists but has wrong format, it will be fixed on next save.
43
+ """
44
+ if not self.history_file.exists():
45
+ # Create file with correct dict format (matching user_dir_init.py)
46
+ initial_history = {"version": "1.0", "created_at": None, "sessions": []}
47
+ try:
48
+ with open(self.history_file, "w", encoding="utf-8") as f:
49
+ json.dump(initial_history, f, indent=2, ensure_ascii=False)
50
+ except Exception:
51
+ # If we can't create the file, that's okay - it will be handled in _load_history
52
+ pass
53
+ else:
54
+ # File exists, verify it's in correct format
55
+ # If not, it will be fixed on next save operation
56
+ try:
57
+ with open(self.history_file, encoding="utf-8") as f:
58
+ data = json.load(f)
59
+ # If file is in old list format, we'll convert it on next save
60
+ if isinstance(data, list):
61
+ # File is in old format, but that's okay - _load_history handles it
62
+ pass
63
+ except Exception:
64
+ # File exists but is corrupted, will be recreated on next save
65
+ pass
66
+
67
+ def _load_history(self) -> list[dict]:
68
+ """
69
+ Load history from file. Handles multiple formats for backward compatibility.
70
+ Returns a list of history entries.
71
+ """
72
+ try:
73
+ if not self.history_file.exists():
74
+ return []
75
+
76
+ with open(self.history_file, encoding="utf-8") as f:
77
+ data = json.load(f)
78
+
79
+ # Handle both list format and dict format (with 'sessions' key)
80
+ if isinstance(data, dict):
81
+ # If it's a dict, try to get 'sessions' field
82
+ sessions = data.get("sessions", [])
83
+ # Ensure sessions is a list
84
+ if isinstance(sessions, list):
85
+ return sessions
86
+ # If sessions is not a list, return empty list and log warning
87
+ return []
88
+ if isinstance(data, list):
89
+ # Legacy format: direct list
90
+ return data
91
+ # Unknown format, return empty list
92
+ return []
93
+ except json.JSONDecodeError:
94
+ # File exists but is corrupted, return empty list
95
+ # Could log this error in production
96
+ return []
97
+ except Exception:
98
+ # Any other error, return empty list
99
+ return []
100
+
101
+ def _save_history(self, history: list[dict]):
102
+ # Load existing file to preserve metadata if it's in dict format
103
+ try:
104
+ with open(self.history_file, encoding="utf-8") as f:
105
+ existing_data = json.load(f)
106
+ if isinstance(existing_data, dict):
107
+ # Preserve dict structure, update sessions
108
+ existing_data["sessions"] = history
109
+ data_to_save = existing_data
110
+ else:
111
+ # If it was a list, convert to dict format
112
+ data_to_save = {
113
+ "version": "1.0",
114
+ "created_at": existing_data[0].get("timestamp") if existing_data else None,
115
+ "sessions": history,
116
+ }
117
+ except Exception:
118
+ # If file doesn't exist or can't be read, create new dict format
119
+ data_to_save = {
120
+ "version": "1.0",
121
+ "created_at": history[0].get("timestamp") if history else None,
122
+ "sessions": history,
123
+ }
124
+
125
+ with open(self.history_file, "w", encoding="utf-8") as f:
126
+ json.dump(data_to_save, f, indent=2, ensure_ascii=False)
127
+
128
+ def add_entry(self, activity_type: ActivityType, title: str, content: dict, summary: str = ""):
129
+ """
130
+ Add a new history entry.
131
+
132
+ Args:
133
+ activity_type: The type of activity (solve, question, research)
134
+ title: A short title (e.g. the question asked, or topic)
135
+ content: The full result/payload
136
+ summary: A short summary if applicable
137
+ """
138
+ entry = {
139
+ "id": str(int(time.time() * 1000)),
140
+ "timestamp": time.time(),
141
+ "type": activity_type,
142
+ "title": title,
143
+ "summary": summary,
144
+ "content": content,
145
+ }
146
+
147
+ history = self._load_history()
148
+ history.insert(0, entry) # Prepend to show latest first
149
+
150
+ # Optional: Limit history size
151
+ if len(history) > 100:
152
+ history = history[:100]
153
+
154
+ self._save_history(history)
155
+ return entry
156
+
157
+ def get_recent(self, limit: int = 10, type_filter: str | None = None) -> list[dict]:
158
+ history = self._load_history()
159
+ if type_filter:
160
+ history = [h for h in history if h["type"] == type_filter]
161
+ return history[:limit]
162
+
163
+ def get_entry(self, entry_id: str) -> dict | None:
164
+ history = self._load_history()
165
+ for entry in history:
166
+ if entry["id"] == entry_id:
167
+ return entry
168
+ return None
169
+
170
+
171
+ # Global instance
172
+ history_manager = HistoryManager()
@@ -0,0 +1,21 @@
1
+ """
2
+ Log Interceptor for WebSocket streaming
3
+ =======================================
4
+
5
+ Re-exports handlers from the unified logging system.
6
+ Kept for backwards compatibility.
7
+ """
8
+
9
+ from src.logging.handlers import (
10
+ JSONFileHandler,
11
+ LogInterceptor,
12
+ WebSocketLogHandler,
13
+ create_task_logger,
14
+ )
15
+
16
+ __all__ = [
17
+ "WebSocketLogHandler",
18
+ "LogInterceptor",
19
+ "JSONFileHandler",
20
+ "create_task_logger",
21
+ ]
@@ -0,0 +1,415 @@
1
+ """
2
+ Notebook Manager - Manages user notebooks and records
3
+ All notebook data is stored in user/notebook/ directory
4
+ """
5
+
6
+ from enum import Enum
7
+ import json
8
+ from pathlib import Path
9
+ import time
10
+ import uuid
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class RecordType(str, Enum):
16
+ """Record type"""
17
+
18
+ SOLVE = "solve"
19
+ QUESTION = "question"
20
+ RESEARCH = "research"
21
+ CO_WRITER = "co_writer"
22
+
23
+
24
+ class NotebookRecord(BaseModel):
25
+ """Single record in notebook"""
26
+
27
+ id: str
28
+ type: RecordType
29
+ title: str
30
+ user_query: str
31
+ output: str
32
+ metadata: dict = {}
33
+ created_at: float
34
+ kb_name: str | None = None
35
+
36
+
37
+ class Notebook(BaseModel):
38
+ """Notebook model"""
39
+
40
+ id: str
41
+ name: str
42
+ description: str = ""
43
+ created_at: float
44
+ updated_at: float
45
+ records: list[NotebookRecord] = []
46
+ color: str = "#3B82F6" # Default blue
47
+ icon: str = "book" # Default icon
48
+
49
+
50
+ class NotebookManager:
51
+ """Notebook manager"""
52
+
53
+ def __init__(self, base_dir: str | None = None):
54
+ """
55
+ Initialize notebook manager
56
+
57
+ Args:
58
+ base_dir: Notebook storage directory, defaults to project root/user/notebook
59
+ """
60
+ if base_dir is None:
61
+ # Current file: DeepTutor/src/api/utils/notebook_manager.py
62
+ # Project root should be three levels up: DeepTutor/
63
+ project_root = Path(__file__).resolve().parents[3]
64
+ base_dir_path = project_root / "data" / "user" / "notebook"
65
+ else:
66
+ base_dir_path = Path(base_dir)
67
+
68
+ self.base_dir = base_dir_path
69
+ self.base_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ # Notebook index file
72
+ self.index_file = self.base_dir / "notebooks_index.json"
73
+ self._ensure_index()
74
+
75
+ def _ensure_index(self):
76
+ """Ensure index file exists"""
77
+ if not self.index_file.exists():
78
+ with open(self.index_file, "w", encoding="utf-8") as f:
79
+ json.dump({"notebooks": []}, f, indent=2, ensure_ascii=False)
80
+
81
+ def _load_index(self) -> dict:
82
+ """Load index"""
83
+ try:
84
+ with open(self.index_file, encoding="utf-8") as f:
85
+ return json.load(f)
86
+ except Exception:
87
+ return {"notebooks": []}
88
+
89
+ def _save_index(self, index: dict):
90
+ """Save index"""
91
+ with open(self.index_file, "w", encoding="utf-8") as f:
92
+ json.dump(index, f, indent=2, ensure_ascii=False)
93
+
94
+ def _get_notebook_file(self, notebook_id: str) -> Path:
95
+ """Get notebook file path"""
96
+ return self.base_dir / f"{notebook_id}.json"
97
+
98
+ def _load_notebook(self, notebook_id: str) -> dict | None:
99
+ """Load single notebook"""
100
+ filepath = self._get_notebook_file(notebook_id)
101
+ if not filepath.exists():
102
+ return None
103
+ try:
104
+ with open(filepath, encoding="utf-8") as f:
105
+ return json.load(f)
106
+ except Exception:
107
+ return None
108
+
109
+ def _save_notebook(self, notebook: dict):
110
+ """Save single notebook"""
111
+ filepath = self._get_notebook_file(notebook["id"])
112
+ with open(filepath, "w", encoding="utf-8") as f:
113
+ json.dump(notebook, f, indent=2, ensure_ascii=False)
114
+
115
+ # === Notebook Operations ===
116
+
117
+ def create_notebook(
118
+ self, name: str, description: str = "", color: str = "#3B82F6", icon: str = "book"
119
+ ) -> dict:
120
+ """
121
+ Create new notebook
122
+
123
+ Args:
124
+ name: Notebook name
125
+ description: Notebook description
126
+ color: Color code
127
+ icon: Icon name
128
+
129
+ Returns:
130
+ Created notebook information
131
+ """
132
+ notebook_id = str(uuid.uuid4())[:8]
133
+ now = time.time()
134
+
135
+ notebook = {
136
+ "id": notebook_id,
137
+ "name": name,
138
+ "description": description,
139
+ "created_at": now,
140
+ "updated_at": now,
141
+ "records": [],
142
+ "color": color,
143
+ "icon": icon,
144
+ }
145
+
146
+ # Save notebook file
147
+ self._save_notebook(notebook)
148
+
149
+ # Update index
150
+ index = self._load_index()
151
+ index["notebooks"].append(
152
+ {
153
+ "id": notebook_id,
154
+ "name": name,
155
+ "description": description,
156
+ "created_at": now,
157
+ "updated_at": now,
158
+ "record_count": 0,
159
+ "color": color,
160
+ "icon": icon,
161
+ }
162
+ )
163
+ self._save_index(index)
164
+
165
+ return notebook
166
+
167
+ def list_notebooks(self) -> list[dict]:
168
+ """
169
+ List all notebooks (summary information)
170
+
171
+ Returns:
172
+ Notebook list
173
+ """
174
+ index = self._load_index()
175
+ notebooks = []
176
+
177
+ for nb_info in index.get("notebooks", []):
178
+ # Get latest information from actual file
179
+ notebook = self._load_notebook(nb_info["id"])
180
+ if notebook:
181
+ notebooks.append(
182
+ {
183
+ "id": notebook["id"],
184
+ "name": notebook["name"],
185
+ "description": notebook.get("description", ""),
186
+ "created_at": notebook["created_at"],
187
+ "updated_at": notebook["updated_at"],
188
+ "record_count": len(notebook.get("records", [])),
189
+ "color": notebook.get("color", "#3B82F6"),
190
+ "icon": notebook.get("icon", "book"),
191
+ }
192
+ )
193
+
194
+ # Sort by update time
195
+ notebooks.sort(key=lambda x: x["updated_at"], reverse=True)
196
+ return notebooks
197
+
198
+ def get_notebook(self, notebook_id: str) -> dict | None:
199
+ """
200
+ Get notebook details (includes all records)
201
+
202
+ Args:
203
+ notebook_id: Notebook ID
204
+
205
+ Returns:
206
+ Notebook details
207
+ """
208
+ return self._load_notebook(notebook_id)
209
+
210
+ def update_notebook(
211
+ self,
212
+ notebook_id: str,
213
+ name: str | None = None,
214
+ description: str | None = None,
215
+ color: str | None = None,
216
+ icon: str | None = None,
217
+ ) -> dict | None:
218
+ """
219
+ Update notebook information
220
+
221
+ Args:
222
+ notebook_id: Notebook ID
223
+ name: New name
224
+ description: New description
225
+ color: New color
226
+ icon: New icon
227
+
228
+ Returns:
229
+ Updated notebook information
230
+ """
231
+ notebook = self._load_notebook(notebook_id)
232
+ if not notebook:
233
+ return None
234
+
235
+ if name is not None:
236
+ notebook["name"] = name
237
+ if description is not None:
238
+ notebook["description"] = description
239
+ if color is not None:
240
+ notebook["color"] = color
241
+ if icon is not None:
242
+ notebook["icon"] = icon
243
+
244
+ notebook["updated_at"] = time.time()
245
+ self._save_notebook(notebook)
246
+
247
+ # Update index
248
+ index = self._load_index()
249
+ for nb_info in index["notebooks"]:
250
+ if nb_info["id"] == notebook_id:
251
+ if name is not None:
252
+ nb_info["name"] = name
253
+ if description is not None:
254
+ nb_info["description"] = description
255
+ if color is not None:
256
+ nb_info["color"] = color
257
+ if icon is not None:
258
+ nb_info["icon"] = icon
259
+ nb_info["updated_at"] = notebook["updated_at"]
260
+ break
261
+ self._save_index(index)
262
+
263
+ return notebook
264
+
265
+ def delete_notebook(self, notebook_id: str) -> bool:
266
+ """
267
+ Delete notebook
268
+
269
+ Args:
270
+ notebook_id: Notebook ID
271
+
272
+ Returns:
273
+ Whether deletion was successful
274
+ """
275
+ filepath = self._get_notebook_file(notebook_id)
276
+ if not filepath.exists():
277
+ return False
278
+
279
+ # Delete file
280
+ filepath.unlink()
281
+
282
+ # Update index
283
+ index = self._load_index()
284
+ index["notebooks"] = [nb for nb in index["notebooks"] if nb["id"] != notebook_id]
285
+ self._save_index(index)
286
+
287
+ return True
288
+
289
+ # === Record Operations ===
290
+
291
+ def add_record(
292
+ self,
293
+ notebook_ids: list[str],
294
+ record_type: RecordType,
295
+ title: str,
296
+ user_query: str,
297
+ output: str,
298
+ metadata: dict = None,
299
+ kb_name: str = None,
300
+ ) -> dict:
301
+ """
302
+ Add record to one or more notebooks
303
+
304
+ Args:
305
+ notebook_ids: Target notebook ID list
306
+ record_type: Record type
307
+ title: Title
308
+ user_query: User input
309
+ output: Output result
310
+ metadata: Additional metadata
311
+ kb_name: Knowledge base name
312
+
313
+ Returns:
314
+ Added record information
315
+ """
316
+ record_id = str(uuid.uuid4())[:8]
317
+ now = time.time()
318
+
319
+ record = {
320
+ "id": record_id,
321
+ "type": record_type,
322
+ "title": title,
323
+ "user_query": user_query,
324
+ "output": output,
325
+ "metadata": metadata or {},
326
+ "created_at": now,
327
+ "kb_name": kb_name,
328
+ }
329
+
330
+ added_to = []
331
+ for notebook_id in notebook_ids:
332
+ notebook = self._load_notebook(notebook_id)
333
+ if notebook:
334
+ notebook["records"].append(record)
335
+ notebook["updated_at"] = now
336
+ self._save_notebook(notebook)
337
+ added_to.append(notebook_id)
338
+
339
+ # Update update time and record count in index
340
+ index = self._load_index()
341
+ for nb_info in index["notebooks"]:
342
+ if nb_info["id"] == notebook_id:
343
+ nb_info["updated_at"] = now
344
+ nb_info["record_count"] = len(notebook["records"])
345
+ break
346
+ self._save_index(index)
347
+
348
+ return {"record": record, "added_to_notebooks": added_to}
349
+
350
+ def remove_record(self, notebook_id: str, record_id: str) -> bool:
351
+ """
352
+ Remove record from notebook
353
+
354
+ Args:
355
+ notebook_id: Notebook ID
356
+ record_id: Record ID
357
+
358
+ Returns:
359
+ Whether deletion was successful
360
+ """
361
+ notebook = self._load_notebook(notebook_id)
362
+ if not notebook:
363
+ return False
364
+
365
+ original_count = len(notebook["records"])
366
+ notebook["records"] = [r for r in notebook["records"] if r["id"] != record_id]
367
+
368
+ if len(notebook["records"]) == original_count:
369
+ return False
370
+
371
+ notebook["updated_at"] = time.time()
372
+ self._save_notebook(notebook)
373
+
374
+ # Update index
375
+ index = self._load_index()
376
+ for nb_info in index["notebooks"]:
377
+ if nb_info["id"] == notebook_id:
378
+ nb_info["updated_at"] = notebook["updated_at"]
379
+ nb_info["record_count"] = len(notebook["records"])
380
+ break
381
+ self._save_index(index)
382
+
383
+ return True
384
+
385
+ def get_statistics(self) -> dict:
386
+ """
387
+ Get notebook statistics
388
+
389
+ Returns:
390
+ Statistics information
391
+ """
392
+ notebooks = self.list_notebooks()
393
+
394
+ total_records = 0
395
+ type_counts = {"solve": 0, "question": 0, "research": 0, "co_writer": 0}
396
+
397
+ for nb_info in notebooks:
398
+ notebook = self._load_notebook(nb_info["id"])
399
+ if notebook:
400
+ for record in notebook.get("records", []):
401
+ total_records += 1
402
+ record_type = record.get("type", "")
403
+ if record_type in type_counts:
404
+ type_counts[record_type] += 1
405
+
406
+ return {
407
+ "total_notebooks": len(notebooks),
408
+ "total_records": total_records,
409
+ "records_by_type": type_counts,
410
+ "recent_notebooks": notebooks[:5],
411
+ }
412
+
413
+
414
+ # Global instance
415
+ notebook_manager = NotebookManager()
@@ -0,0 +1,72 @@
1
+ """
2
+ Progress Broadcaster - Manages WebSocket broadcasting of knowledge base progress
3
+ """
4
+
5
+ import asyncio
6
+ from typing import Optional
7
+
8
+ from fastapi import WebSocket
9
+
10
+
11
+ class ProgressBroadcaster:
12
+ """Manages WebSocket broadcasting of knowledge base progress"""
13
+
14
+ _instance: Optional["ProgressBroadcaster"] = None
15
+ _connections: dict[str, set[WebSocket]] = {} # kb_name -> Set[WebSocket]
16
+ _lock = asyncio.Lock()
17
+
18
+ @classmethod
19
+ def get_instance(cls) -> "ProgressBroadcaster":
20
+ """Get singleton instance"""
21
+ if cls._instance is None:
22
+ cls._instance = cls()
23
+ return cls._instance
24
+
25
+ async def connect(self, kb_name: str, websocket: WebSocket):
26
+ """Connect WebSocket to specified knowledge base"""
27
+ async with self._lock:
28
+ if kb_name not in self._connections:
29
+ self._connections[kb_name] = set()
30
+ self._connections[kb_name].add(websocket)
31
+ print(
32
+ f"[ProgressBroadcaster] Connected WebSocket for KB '{kb_name}' (total: {len(self._connections[kb_name])})"
33
+ )
34
+
35
+ async def disconnect(self, kb_name: str, websocket: WebSocket):
36
+ """Disconnect WebSocket connection"""
37
+ async with self._lock:
38
+ if kb_name in self._connections:
39
+ self._connections[kb_name].discard(websocket)
40
+ if not self._connections[kb_name]:
41
+ del self._connections[kb_name]
42
+ print(f"[ProgressBroadcaster] Disconnected WebSocket for KB '{kb_name}'")
43
+
44
+ async def broadcast(self, kb_name: str, progress: dict):
45
+ """Broadcast progress update to all WebSocket connections for specified knowledge base"""
46
+ async with self._lock:
47
+ if kb_name not in self._connections:
48
+ return
49
+
50
+ # Create list of connections to remove (closed connections)
51
+ to_remove = []
52
+
53
+ for websocket in self._connections[kb_name]:
54
+ try:
55
+ await websocket.send_json({"type": "progress", "data": progress})
56
+ except Exception as e:
57
+ # Connection closed or error, mark for removal
58
+ print(
59
+ f"[ProgressBroadcaster] Error sending to WebSocket for KB '{kb_name}': {e}"
60
+ )
61
+ to_remove.append(websocket)
62
+
63
+ # Remove closed connections
64
+ for ws in to_remove:
65
+ self._connections[kb_name].discard(ws)
66
+
67
+ if not self._connections[kb_name]:
68
+ del self._connections[kb_name]
69
+
70
+ def get_connection_count(self, kb_name: str) -> int:
71
+ """Get connection count for specified knowledge base"""
72
+ return len(self._connections.get(kb_name, set()))