realtimex-deeptutor 0.5.0.post1__py3-none-any.whl → 0.5.0.post3__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 (145) hide show
  1. {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/METADATA +24 -17
  2. {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/RECORD +143 -123
  3. {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/WHEEL +1 -1
  4. realtimex_deeptutor-0.5.0.post3.dist-info/entry_points.txt +4 -0
  5. {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/top_level.txt +1 -0
  6. scripts/__init__.py +1 -0
  7. scripts/audit_prompts.py +179 -0
  8. scripts/check_install.py +460 -0
  9. scripts/generate_roster.py +327 -0
  10. scripts/install_all.py +653 -0
  11. scripts/migrate_kb.py +655 -0
  12. scripts/start.py +807 -0
  13. scripts/start_web.py +632 -0
  14. scripts/sync_prompts_from_en.py +147 -0
  15. src/__init__.py +2 -2
  16. src/agents/ideagen/material_organizer_agent.py +2 -0
  17. src/agents/solve/__init__.py +6 -0
  18. src/agents/solve/main_solver.py +9 -0
  19. src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +9 -7
  20. src/agents/solve/session_manager.py +345 -0
  21. src/api/main.py +14 -0
  22. src/api/routers/chat.py +3 -3
  23. src/api/routers/co_writer.py +12 -7
  24. src/api/routers/config.py +1 -0
  25. src/api/routers/guide.py +3 -1
  26. src/api/routers/ideagen.py +7 -0
  27. src/api/routers/knowledge.py +64 -12
  28. src/api/routers/question.py +2 -0
  29. src/api/routers/realtimex.py +137 -0
  30. src/api/routers/research.py +9 -0
  31. src/api/routers/solve.py +120 -2
  32. src/cli/__init__.py +13 -0
  33. src/cli/start.py +209 -0
  34. src/config/constants.py +11 -9
  35. src/knowledge/add_documents.py +453 -213
  36. src/knowledge/extract_numbered_items.py +9 -10
  37. src/knowledge/initializer.py +102 -101
  38. src/knowledge/manager.py +251 -74
  39. src/knowledge/progress_tracker.py +43 -2
  40. src/knowledge/start_kb.py +11 -2
  41. src/logging/__init__.py +5 -0
  42. src/logging/adapters/__init__.py +1 -0
  43. src/logging/adapters/lightrag.py +25 -18
  44. src/logging/adapters/llamaindex.py +1 -0
  45. src/logging/config.py +30 -27
  46. src/logging/handlers/__init__.py +1 -0
  47. src/logging/handlers/console.py +7 -50
  48. src/logging/handlers/file.py +5 -20
  49. src/logging/handlers/websocket.py +23 -19
  50. src/logging/logger.py +161 -126
  51. src/logging/stats/__init__.py +1 -0
  52. src/logging/stats/llm_stats.py +37 -17
  53. src/services/__init__.py +17 -1
  54. src/services/config/__init__.py +1 -0
  55. src/services/config/knowledge_base_config.py +1 -0
  56. src/services/config/loader.py +1 -1
  57. src/services/config/unified_config.py +211 -4
  58. src/services/embedding/__init__.py +1 -0
  59. src/services/embedding/adapters/__init__.py +3 -0
  60. src/services/embedding/adapters/base.py +1 -0
  61. src/services/embedding/adapters/cohere.py +1 -0
  62. src/services/embedding/adapters/jina.py +1 -0
  63. src/services/embedding/adapters/ollama.py +1 -0
  64. src/services/embedding/adapters/openai_compatible.py +1 -0
  65. src/services/embedding/adapters/realtimex.py +125 -0
  66. src/services/embedding/client.py +27 -0
  67. src/services/embedding/config.py +3 -0
  68. src/services/embedding/provider.py +1 -0
  69. src/services/llm/__init__.py +17 -3
  70. src/services/llm/capabilities.py +47 -0
  71. src/services/llm/client.py +32 -0
  72. src/services/llm/cloud_provider.py +21 -4
  73. src/services/llm/config.py +36 -2
  74. src/services/llm/error_mapping.py +1 -0
  75. src/services/llm/exceptions.py +30 -0
  76. src/services/llm/factory.py +55 -16
  77. src/services/llm/local_provider.py +1 -0
  78. src/services/llm/providers/anthropic.py +1 -0
  79. src/services/llm/providers/base_provider.py +1 -0
  80. src/services/llm/providers/open_ai.py +1 -0
  81. src/services/llm/realtimex_provider.py +240 -0
  82. src/services/llm/registry.py +1 -0
  83. src/services/llm/telemetry.py +1 -0
  84. src/services/llm/types.py +1 -0
  85. src/services/llm/utils.py +1 -0
  86. src/services/prompt/__init__.py +1 -0
  87. src/services/prompt/manager.py +3 -2
  88. src/services/rag/__init__.py +27 -5
  89. src/services/rag/components/__init__.py +1 -0
  90. src/services/rag/components/base.py +1 -0
  91. src/services/rag/components/chunkers/__init__.py +1 -0
  92. src/services/rag/components/chunkers/base.py +1 -0
  93. src/services/rag/components/chunkers/fixed.py +1 -0
  94. src/services/rag/components/chunkers/numbered_item.py +1 -0
  95. src/services/rag/components/chunkers/semantic.py +1 -0
  96. src/services/rag/components/embedders/__init__.py +1 -0
  97. src/services/rag/components/embedders/base.py +1 -0
  98. src/services/rag/components/embedders/openai.py +1 -0
  99. src/services/rag/components/indexers/__init__.py +1 -0
  100. src/services/rag/components/indexers/base.py +1 -0
  101. src/services/rag/components/indexers/graph.py +5 -44
  102. src/services/rag/components/indexers/lightrag.py +5 -44
  103. src/services/rag/components/indexers/vector.py +1 -0
  104. src/services/rag/components/parsers/__init__.py +1 -0
  105. src/services/rag/components/parsers/base.py +1 -0
  106. src/services/rag/components/parsers/markdown.py +1 -0
  107. src/services/rag/components/parsers/pdf.py +1 -0
  108. src/services/rag/components/parsers/text.py +1 -0
  109. src/services/rag/components/retrievers/__init__.py +1 -0
  110. src/services/rag/components/retrievers/base.py +1 -0
  111. src/services/rag/components/retrievers/dense.py +1 -0
  112. src/services/rag/components/retrievers/hybrid.py +5 -44
  113. src/services/rag/components/retrievers/lightrag.py +5 -44
  114. src/services/rag/components/routing.py +48 -0
  115. src/services/rag/factory.py +112 -46
  116. src/services/rag/pipeline.py +1 -0
  117. src/services/rag/pipelines/__init__.py +27 -18
  118. src/services/rag/pipelines/lightrag.py +1 -0
  119. src/services/rag/pipelines/llamaindex.py +99 -0
  120. src/services/rag/pipelines/raganything.py +67 -100
  121. src/services/rag/pipelines/raganything_docling.py +368 -0
  122. src/services/rag/service.py +5 -12
  123. src/services/rag/types.py +1 -0
  124. src/services/rag/utils/__init__.py +17 -0
  125. src/services/rag/utils/image_migration.py +279 -0
  126. src/services/search/__init__.py +1 -0
  127. src/services/search/base.py +1 -0
  128. src/services/search/consolidation.py +1 -0
  129. src/services/search/providers/__init__.py +1 -0
  130. src/services/search/providers/baidu.py +1 -0
  131. src/services/search/providers/exa.py +1 -0
  132. src/services/search/providers/jina.py +1 -0
  133. src/services/search/providers/perplexity.py +1 -0
  134. src/services/search/providers/serper.py +1 -0
  135. src/services/search/providers/tavily.py +1 -0
  136. src/services/search/types.py +1 -0
  137. src/services/settings/__init__.py +1 -0
  138. src/services/settings/interface_settings.py +78 -0
  139. src/services/setup/__init__.py +1 -0
  140. src/services/tts/__init__.py +1 -0
  141. src/services/tts/config.py +1 -0
  142. src/utils/realtimex.py +284 -0
  143. realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +0 -2
  144. src/services/rag/pipelines/academic.py +0 -44
  145. {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/licenses/LICENSE +0 -0
@@ -23,6 +23,7 @@ from src.agents.co_writer.edit_agent import (
23
23
  from src.agents.co_writer.narrator_agent import NarratorAgent
24
24
  from src.logging import get_logger
25
25
  from src.services.config import load_config_with_main
26
+ from src.services.settings.interface_settings import get_ui_language
26
27
  from src.services.tts import get_tts_config
27
28
 
28
29
  router = APIRouter()
@@ -33,15 +34,17 @@ config = load_config_with_main("solve_config.yaml", project_root) # Use any con
33
34
  log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
34
35
  logger = get_logger("CoWriter", level="INFO", log_dir=log_dir)
35
36
 
36
- # Get system language for agent
37
- _system_language = config.get("system", {}).get("language", "en")
38
-
39
37
  # Singleton agent instances - use refresh_config() before each request
40
38
  # to pick up any configuration changes from Settings
41
39
  _edit_agent: EditAgent | None = None
42
40
  _narrator_agent: NarratorAgent | None = None
43
41
 
44
42
 
43
+ def _current_language() -> str:
44
+ # Prefer UI settings, fall back to main.yaml system.language
45
+ return get_ui_language(default=config.get("system", {}).get("language", "en"))
46
+
47
+
45
48
  def get_edit_agent() -> EditAgent:
46
49
  """
47
50
  Get the singleton EditAgent instance with refreshed configuration.
@@ -51,8 +54,9 @@ def get_edit_agent() -> EditAgent:
51
54
  2. Latest LLM configuration from Settings is always used
52
55
  """
53
56
  global _edit_agent
54
- if _edit_agent is None:
55
- _edit_agent = EditAgent(language=_system_language)
57
+ lang = _current_language()
58
+ if _edit_agent is None or getattr(_edit_agent, "language", None) != lang:
59
+ _edit_agent = EditAgent(language=lang)
56
60
  # Refresh config to pick up any changes from Settings
57
61
  _edit_agent.refresh_config()
58
62
  return _edit_agent
@@ -67,8 +71,9 @@ def get_narrator_agent() -> NarratorAgent:
67
71
  2. Latest LLM configuration from Settings is always used
68
72
  """
69
73
  global _narrator_agent
70
- if _narrator_agent is None:
71
- _narrator_agent = NarratorAgent(language=_system_language)
74
+ lang = _current_language()
75
+ if _narrator_agent is None or getattr(_narrator_agent, "language", None) != lang:
76
+ _narrator_agent = NarratorAgent(language=lang)
72
77
  # Refresh config to pick up any changes from Settings
73
78
  _narrator_agent.refresh_config()
74
79
  return _narrator_agent
src/api/routers/config.py CHANGED
@@ -168,6 +168,7 @@ async def get_config_status():
168
168
  "provider": active.get("provider") if active else None,
169
169
  "env_configured": env_status,
170
170
  "total_configs": len(configs),
171
+ "source": active.get("source") if active else None, # "realtimex" when using RTX
171
172
  }
172
173
 
173
174
  return ConfigStatusResponse(
src/api/routers/guide.py CHANGED
@@ -22,6 +22,7 @@ from src.api.utils.task_id_manager import TaskIDManager
22
22
  from src.logging import get_logger
23
23
  from src.services.config import load_config_with_main
24
24
  from src.services.llm import get_llm_config
25
+ from src.services.settings.interface_settings import get_ui_language
25
26
 
26
27
  router = APIRouter()
27
28
 
@@ -76,11 +77,12 @@ def get_guide_manager():
76
77
  except Exception as e:
77
78
  raise HTTPException(status_code=500, detail=f"LLM config error: {e!s}")
78
79
 
80
+ ui_language = get_ui_language(default=config.get("system", {}).get("language", "en"))
79
81
  return GuideManager(
80
82
  api_key=api_key,
81
83
  base_url=base_url,
82
84
  api_version=api_version,
83
- language=None,
85
+ language=ui_language,
84
86
  binding=binding,
85
87
  ) # Read from config file
86
88
 
@@ -23,6 +23,7 @@ from src.api.utils.task_id_manager import TaskIDManager
23
23
  from src.logging import get_logger
24
24
  from src.services.config import load_config_with_main
25
25
  from src.services.llm import get_llm_config
26
+ from src.services.settings.interface_settings import get_ui_language
26
27
 
27
28
  router = APIRouter()
28
29
 
@@ -148,6 +149,7 @@ async def websocket_ideagen(websocket: WebSocket):
148
149
 
149
150
  # Get LLM configuration
150
151
  llm_config = get_llm_config()
152
+ ui_language = get_ui_language(default=config.get("system", {}).get("language", "en"))
151
153
 
152
154
  # Get records
153
155
  records = []
@@ -198,6 +200,7 @@ async def websocket_ideagen(websocket: WebSocket):
198
200
  base_url=llm_config.base_url,
199
201
  api_version=getattr(llm_config, "api_version", None),
200
202
  model=llm_config.model,
203
+ language=ui_language,
201
204
  )
202
205
 
203
206
  knowledge_points = await organizer.process(
@@ -258,6 +261,7 @@ async def websocket_ideagen(websocket: WebSocket):
258
261
  api_version=getattr(llm_config, "api_version", None),
259
262
  model=llm_config.model,
260
263
  progress_callback=None, # We manually manage status here
264
+ language=ui_language,
261
265
  )
262
266
 
263
267
  filtered_points = await workflow.loose_filter(knowledge_points)
@@ -412,6 +416,9 @@ async def websocket_ideagen(websocket: WebSocket):
412
416
  task_manager.update_task_status(task_id, "error", error=str(e))
413
417
 
414
418
  try:
419
+ # Send unified error message via send_status
420
+ # Note: send_status sends {"type": "status", "stage": "error", ...}
421
+ # which is the standard format for this WebSocket protocol
415
422
  await send_status(
416
423
  websocket,
417
424
  IdeaGenStage.ERROR,
@@ -142,8 +142,19 @@ async def run_upload_processing_task(
142
142
  base_url: str,
143
143
  uploaded_file_paths: list[str],
144
144
  rag_provider: str = None,
145
+ folder_id: str = None,
145
146
  ):
146
- """Background task for processing uploaded files"""
147
+ """Background task for processing uploaded files.
148
+
149
+ Args:
150
+ kb_name: Knowledge base name
151
+ base_dir: Base directory for knowledge bases
152
+ api_key: LLM API key
153
+ base_url: LLM API base URL
154
+ uploaded_file_paths: List of file paths to process
155
+ rag_provider: RAG provider (ignored - we use the one from KB metadata)
156
+ folder_id: Optional folder ID for sync state update
157
+ """
147
158
  task_manager = TaskIDManager.get_instance()
148
159
  task_key = f"{kb_name}_upload_{len(uploaded_file_paths)}"
149
160
  task_id = task_manager.generate_task_id("kb_upload", task_key)
@@ -169,8 +180,22 @@ async def run_upload_processing_task(
169
180
  rag_provider=rag_provider,
170
181
  )
171
182
 
172
- new_files = [Path(path) for path in uploaded_file_paths]
173
- processed_files = await adder.process_new_documents(new_files)
183
+ # Stage files and check for duplicates
184
+ staged_files = adder.add_documents(uploaded_file_paths, allow_duplicates=False)
185
+
186
+ if not staged_files:
187
+ logger.info(f"[{task_id}] No new files to process (all duplicates or invalid)")
188
+ progress_tracker.update(
189
+ ProgressStage.COMPLETED,
190
+ "No new files to process (all duplicates or invalid)",
191
+ current=0,
192
+ total=0,
193
+ )
194
+ task_manager.update_task_status(task_id, "completed")
195
+ return
196
+
197
+ # Process staged files
198
+ processed_files = await adder.process_new_documents(staged_files)
174
199
 
175
200
  if processed_files:
176
201
  progress_tracker.update(
@@ -181,16 +206,28 @@ async def run_upload_processing_task(
181
206
  )
182
207
  adder.extract_numbered_items_for_new_docs(processed_files, batch_size=20)
183
208
 
184
- adder.update_metadata(len(new_files))
209
+ adder.update_metadata(len(processed_files) if processed_files else 0)
210
+
211
+ # Update folder sync state if this was a folder sync
212
+ if folder_id and processed_files:
213
+ try:
214
+ manager = get_kb_manager()
215
+ manager.update_folder_sync_state(
216
+ kb_name, folder_id, [str(f) for f in processed_files]
217
+ )
218
+ logger.info(f"[{task_id}] Updated folder sync state for folder '{folder_id}'")
219
+ except Exception as sync_err:
220
+ logger.warning(f"[{task_id}] Failed to update folder sync state: {sync_err}")
185
221
 
222
+ num_processed = len(processed_files) if processed_files else 0
186
223
  progress_tracker.update(
187
224
  ProgressStage.COMPLETED,
188
- f"Successfully processed {len(processed_files)} files!",
189
- current=len(processed_files),
190
- total=len(processed_files),
225
+ f"Successfully processed {num_processed} files!",
226
+ current=num_processed,
227
+ total=num_processed,
191
228
  )
192
229
 
193
- logger.success(f"[{task_id}] Processed {len(processed_files)} files to KB '{kb_name}'")
230
+ logger.success(f"[{task_id}] Processed {num_processed} files to KB '{kb_name}'")
194
231
  task_manager.update_task_status(task_id, "completed")
195
232
  except Exception as e:
196
233
  error_msg = f"Upload processing failed (KB '{kb_name}'): {e}"
@@ -531,13 +568,28 @@ async def create_knowledge_base(
531
568
  except ValueError as e:
532
569
  raise HTTPException(status_code=500, detail=f"LLM config error: {e!s}")
533
570
 
534
- progress_tracker = ProgressTracker(name, _kb_base_dir)
535
-
536
571
  logger.info(f"Creating KB: {name}")
537
572
 
538
- progress_tracker.update(
539
- ProgressStage.INITIALIZING, "Initializing knowledge base...", current=0, total=0
573
+ # Register KB to kb_config.json immediately with "initializing" status
574
+ # This ensures the KB appears in the list right away
575
+ manager.update_kb_status(
576
+ name=name,
577
+ status="initializing",
578
+ progress={
579
+ "stage": "initializing",
580
+ "message": "Initializing knowledge base...",
581
+ "percent": 0,
582
+ "current": 0,
583
+ "total": len(files),
584
+ },
540
585
  )
586
+ # Also store rag_provider in config (reload and update)
587
+ manager.config = manager._load_config()
588
+ if name in manager.config.get("knowledge_bases", {}):
589
+ manager.config["knowledge_bases"][name]["rag_provider"] = rag_provider
590
+ manager._save_config()
591
+
592
+ progress_tracker = ProgressTracker(name, _kb_base_dir)
541
593
 
542
594
  initializer = KnowledgeBaseInitializer(
543
595
  kb_name=name,
@@ -23,6 +23,7 @@ sys.path.insert(0, str(project_root))
23
23
  from src.logging import get_logger
24
24
  from src.services.config import load_config_with_main
25
25
  from src.services.llm.config import get_llm_config
26
+ from src.services.settings.interface_settings import get_ui_language
26
27
 
27
28
  # Setup module logger with unified logging system (from config)
28
29
  project_root = Path(__file__).parent.parent.parent.parent
@@ -382,6 +383,7 @@ async def websocket_question_generate(websocket: WebSocket):
382
383
  base_url=base_url,
383
384
  api_version=api_version,
384
385
  kb_name=kb_name,
386
+ language=get_ui_language(default=config.get("system", {}).get("language", "en")),
385
387
  max_rounds=10,
386
388
  output_dir=str(output_base),
387
389
  )
@@ -0,0 +1,137 @@
1
+ """
2
+ RealTimeX SDK Status Router
3
+ ============================
4
+
5
+ Provides status information about RealTimeX SDK connection and environment.
6
+ """
7
+
8
+ from fastapi import APIRouter
9
+
10
+ from src.utils.realtimex import get_realtimex_sdk, should_use_realtimex_sdk
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.get("/realtimex/status")
16
+ async def get_realtimex_status():
17
+ """
18
+ Get RealTimeX SDK connection status.
19
+
20
+ Returns:
21
+ {
22
+ "connected": bool,
23
+ "mode": str | null, # "development" or "production"
24
+ "appId": str | null,
25
+ "timestamp": str | null,
26
+ "error": str | null
27
+ }
28
+ """
29
+ try:
30
+ # Check if RealTimeX is detected
31
+ is_detected = should_use_realtimex_sdk()
32
+
33
+ if not is_detected:
34
+ return {
35
+ "connected": False,
36
+ "mode": None,
37
+ "appId": None,
38
+ "timestamp": None,
39
+ "error": None,
40
+ }
41
+
42
+ # Get SDK instance and ping (async)
43
+ try:
44
+ sdk = get_realtimex_sdk()
45
+ # Use async ping() instead of ping_sync() to avoid event loop conflict
46
+ ping_result = await sdk.ping()
47
+
48
+ return {
49
+ "connected": ping_result.get("success", False),
50
+ "mode": ping_result.get("mode"),
51
+ "appId": ping_result.get("appId"),
52
+ "timestamp": ping_result.get("timestamp"),
53
+ "error": None,
54
+ }
55
+ except Exception as e:
56
+ return {
57
+ "connected": False,
58
+ "mode": None,
59
+ "appId": None,
60
+ "timestamp": None,
61
+ "error": str(e),
62
+ }
63
+
64
+ except Exception as e:
65
+ return {
66
+ "connected": False,
67
+ "mode": None,
68
+ "appId": None,
69
+ "timestamp": None,
70
+ "error": f"Detection failed: {str(e)}",
71
+ }
72
+
73
+
74
+ @router.get("/realtimex/providers")
75
+ async def get_providers():
76
+ """
77
+ Get available providers from RealTimeX SDK.
78
+ Returns empty lists if SDK not enabled, allowing frontend to use defaults.
79
+ """
80
+ from src.utils.realtimex import get_cached_providers
81
+
82
+ return await get_cached_providers()
83
+
84
+
85
+ from pydantic import BaseModel
86
+
87
+
88
+ class RTXConfigApplyRequest(BaseModel):
89
+ config_type: str # "llm" or "embedding"
90
+ provider: str
91
+ model: str
92
+
93
+
94
+ @router.post("/realtimex/config/apply")
95
+ async def apply_rtx_config(request: RTXConfigApplyRequest):
96
+ """
97
+ Apply RTX provider/model selection.
98
+
99
+ Saves the selection to rtx_active.json and sets the active config
100
+ to 'rtx' in the unified config manager.
101
+ """
102
+ from fastapi import HTTPException
103
+
104
+ from src.services.config.unified_config import ConfigType, get_config_manager
105
+ from src.utils.realtimex import set_rtx_active_config, should_use_realtimex_sdk
106
+
107
+ if not should_use_realtimex_sdk():
108
+ raise HTTPException(400, "RealTimeX SDK not available")
109
+
110
+ try:
111
+ # Validate config type
112
+ if request.config_type == "llm":
113
+ config_type_enum = ConfigType.LLM
114
+ elif request.config_type == "embedding":
115
+ config_type_enum = ConfigType.EMBEDDING
116
+ else:
117
+ raise HTTPException(400, f"Invalid config type: {request.config_type}")
118
+
119
+ # Save RTX selection to rtx_active.json
120
+ if not set_rtx_active_config(request.config_type, request.provider, request.model):
121
+ raise HTTPException(500, "Failed to save RTX configuration")
122
+
123
+ # Set 'rtx' as the active config in unified config manager
124
+ manager = get_config_manager()
125
+ manager.set_active_config(config_type_enum, "rtx")
126
+
127
+ return {
128
+ "success": True,
129
+ "config_type": request.config_type,
130
+ "provider": request.provider,
131
+ "model": request.model,
132
+ }
133
+
134
+ except HTTPException:
135
+ raise
136
+ except Exception as e:
137
+ raise HTTPException(500, f"Failed to apply configuration: {str(e)}")
@@ -15,6 +15,7 @@ from src.api.utils.task_id_manager import TaskIDManager
15
15
  from src.logging import get_logger
16
16
  from src.services.config import load_config_with_main
17
17
  from src.services.llm import get_llm_config
18
+ from src.services.settings.interface_settings import get_ui_language
18
19
 
19
20
  # Force stdout to use utf-8 to prevent encoding errors with emojis on Windows
20
21
  if sys.platform == "win32":
@@ -46,6 +47,10 @@ class OptimizeRequest(BaseModel):
46
47
  async def optimize_topic(request: OptimizeRequest):
47
48
  try:
48
49
  config = load_config()
50
+ config.setdefault("system", {})
51
+ config["system"]["language"] = get_ui_language(
52
+ default=config.get("system", {}).get("language", "en")
53
+ )
49
54
 
50
55
  # Inject API keys
51
56
  try:
@@ -111,6 +116,10 @@ async def websocket_research_run(websocket: WebSocket):
111
116
 
112
117
  # Use unified logger
113
118
  config = load_config()
119
+ config.setdefault("system", {})
120
+ config["system"]["language"] = get_ui_language(
121
+ default=config.get("system", {}).get("language", "en")
122
+ )
114
123
  try:
115
124
  # Get log_dir from config
116
125
  log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get(
src/api/routers/solve.py CHANGED
@@ -11,9 +11,9 @@ import re
11
11
  import sys
12
12
  from typing import Any
13
13
 
14
- from fastapi import APIRouter, WebSocket, WebSocketDisconnect
14
+ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
15
15
 
16
- from src.agents.solve import MainSolver
16
+ from src.agents.solve import MainSolver, SolverSessionManager
17
17
  from src.api.utils.history import ActivityType, history_manager
18
18
  from src.api.utils.log_interceptor import LogInterceptor
19
19
  from src.api.utils.task_id_manager import TaskIDManager
@@ -23,6 +23,7 @@ sys.path.insert(0, str(_project_root))
23
23
  from src.logging import get_logger
24
24
  from src.services.config import load_config_with_main
25
25
  from src.services.llm import get_llm_config
26
+ from src.services.settings.interface_settings import get_ui_language
26
27
 
27
28
  # Initialize logger with config
28
29
  project_root = Path(__file__).parent.parent.parent.parent
@@ -32,6 +33,66 @@ logger = get_logger("SolveAPI", level="INFO", log_dir=log_dir)
32
33
 
33
34
  router = APIRouter()
34
35
 
36
+ # Initialize session manager
37
+ solver_session_manager = SolverSessionManager()
38
+
39
+
40
+ # =============================================================================
41
+ # REST Endpoints for Session Management
42
+ # =============================================================================
43
+
44
+
45
+ @router.get("/solve/sessions")
46
+ async def list_solver_sessions(limit: int = 20):
47
+ """
48
+ List recent solver sessions.
49
+
50
+ Args:
51
+ limit: Maximum number of sessions to return
52
+
53
+ Returns:
54
+ List of session summaries
55
+ """
56
+ return solver_session_manager.list_sessions(limit=limit, include_messages=False)
57
+
58
+
59
+ @router.get("/solve/sessions/{session_id}")
60
+ async def get_solver_session(session_id: str):
61
+ """
62
+ Get a specific solver session with full message history.
63
+
64
+ Args:
65
+ session_id: Session identifier
66
+
67
+ Returns:
68
+ Complete session data including messages
69
+ """
70
+ session = solver_session_manager.get_session(session_id)
71
+ if not session:
72
+ raise HTTPException(status_code=404, detail="Session not found")
73
+ return session
74
+
75
+
76
+ @router.delete("/solve/sessions/{session_id}")
77
+ async def delete_solver_session(session_id: str):
78
+ """
79
+ Delete a solver session.
80
+
81
+ Args:
82
+ session_id: Session identifier
83
+
84
+ Returns:
85
+ Success message
86
+ """
87
+ if solver_session_manager.delete_session(session_id):
88
+ return {"status": "deleted", "session_id": session_id}
89
+ raise HTTPException(status_code=404, detail="Session not found")
90
+
91
+
92
+ # =============================================================================
93
+ # WebSocket Endpoint for Solving
94
+ # =============================================================================
95
+
35
96
 
36
97
  @router.websocket("/solve")
37
98
  async def websocket_solve(websocket: WebSocket):
@@ -81,16 +142,47 @@ async def websocket_solve(websocket: WebSocket):
81
142
  logger.debug(f"Error in log_pusher: {e}")
82
143
  break
83
144
 
145
+ session_id = None # Track session for this connection
146
+
84
147
  try:
85
148
  # 1. Wait for the initial message with the question and config
86
149
  data = await websocket.receive_json()
87
150
  question = data.get("question")
88
151
  kb_name = data.get("kb_name", "ai_textbook")
152
+ session_id = data.get("session_id") # Optional session ID
89
153
 
90
154
  if not question:
91
155
  await websocket.send_json({"type": "error", "content": "Question is required"})
92
156
  return
93
157
 
158
+ # Get or create session
159
+ if session_id:
160
+ session = solver_session_manager.get_session(session_id)
161
+ if not session:
162
+ # Session not found, create new one
163
+ session = solver_session_manager.create_session(
164
+ title=question[:50] + ("..." if len(question) > 50 else ""),
165
+ kb_name=kb_name,
166
+ )
167
+ session_id = session["session_id"]
168
+ else:
169
+ # Create new session
170
+ session = solver_session_manager.create_session(
171
+ title=question[:50] + ("..." if len(question) > 50 else ""),
172
+ kb_name=kb_name,
173
+ )
174
+ session_id = session["session_id"]
175
+
176
+ # Send session ID to frontend
177
+ await websocket.send_json({"type": "session", "session_id": session_id})
178
+
179
+ # Add user message to session
180
+ solver_session_manager.add_message(
181
+ session_id=session_id,
182
+ role="user",
183
+ content=question,
184
+ )
185
+
94
186
  task_key = f"solve_{kb_name}_{hash(str(question))}"
95
187
  task_id = task_manager.generate_task_id("solve", task_key)
96
188
 
@@ -110,12 +202,14 @@ async def websocket_solve(websocket: WebSocket):
110
202
  await websocket.send_json({"type": "error", "content": f"LLM configuration error: {e}"})
111
203
  return
112
204
 
205
+ ui_language = get_ui_language(default=config.get("system", {}).get("language", "en"))
113
206
  solver = MainSolver(
114
207
  kb_name=kb_name,
115
208
  output_base_dir=str(output_base),
116
209
  api_key=api_key,
117
210
  base_url=base_url,
118
211
  api_version=api_version,
212
+ language=ui_language,
119
213
  )
120
214
 
121
215
  # Complete async initialization
@@ -247,14 +341,37 @@ async def websocket_solve(websocket: WebSocket):
247
341
  )
248
342
 
249
343
  # Send final result
344
+ # Extract relative path from output_dir for frontend use
345
+ dir_name = ""
346
+ if output_dir_str:
347
+ parts = output_dir_str.replace("\\", "/").split("/")
348
+ dir_name = parts[-1] if parts else ""
349
+
250
350
  final_res = {
251
351
  "type": "result",
352
+ "session_id": session_id,
252
353
  "final_answer": final_answer,
253
354
  "output_dir": output_dir_str,
355
+ "output_dir_name": dir_name,
254
356
  "metadata": result.get("metadata"),
255
357
  }
256
358
  await safe_send_json(final_res)
257
359
 
360
+ # Save assistant message to session
361
+ if session_id:
362
+ solver_session_manager.add_message(
363
+ session_id=session_id,
364
+ role="assistant",
365
+ content=final_answer,
366
+ output_dir=dir_name,
367
+ )
368
+ # Update token stats in session
369
+ if display_manager:
370
+ solver_session_manager.update_token_stats(
371
+ session_id=session_id,
372
+ token_stats=display_manager.stats.copy(),
373
+ )
374
+
258
375
  # Save to history
259
376
  history_manager.add_entry(
260
377
  activity_type=ActivityType.SOLVE,
@@ -263,6 +380,7 @@ async def websocket_solve(websocket: WebSocket):
263
380
  "question": question,
264
381
  "answer": result.get("final_answer"),
265
382
  "kb_name": kb_name,
383
+ "session_id": session_id,
266
384
  },
267
385
  summary=(
268
386
  result.get("final_answer")[:100] + "..." if result.get("final_answer") else ""
src/cli/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Command-line interface for DeepTutor.
3
+
4
+ Provides entry points for:
5
+ - Full-stack startup (backend + frontend)
6
+ - Backend-only mode (future)
7
+ - Frontend-only mode (future)
8
+ - Config management (future)
9
+ """
10
+
11
+ from .start import main as start_main
12
+
13
+ __all__ = ["start_main"]