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,184 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ LightRAG Log Forwarder
4
+ ======================
5
+
6
+ Forwards LightRAG and RAG-Anything logs to DeepTutor's unified logging system.
7
+ """
8
+
9
+ from contextlib import contextmanager
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ class LightRAGLogForwarder(logging.Handler):
16
+ """
17
+ Handler that forwards LightRAG logger messages to DeepTutor logger.
18
+ """
19
+
20
+ def __init__(self, ai_tutor_logger, add_prefix: bool = True):
21
+ """
22
+ Args:
23
+ ai_tutor_logger: DeepTutor Logger instance
24
+ add_prefix: Whether to add [LightRAG] prefix to messages
25
+ """
26
+ super().__init__()
27
+ self.ai_tutor_logger = ai_tutor_logger
28
+ self.add_prefix = add_prefix
29
+ # Capture all log levels
30
+ self.setLevel(logging.DEBUG)
31
+
32
+ def emit(self, record: logging.LogRecord):
33
+ """
34
+ Forward log record to DeepTutor logger.
35
+ All logs are forwarded as info level to maintain consistent format.
36
+ """
37
+ try:
38
+ # Get the original message directly without adding [LightRAG] prefix
39
+ # LightRAG already formats messages appropriately (e.g., "DEBUG: xxx" for debug logs)
40
+ message = record.getMessage()
41
+
42
+ # Use info() for all levels to maintain consistent format
43
+ # This ensures all logs appear as [RAGTool] ... (or [RAGTool] DEBUG: ... for debug)
44
+ self.ai_tutor_logger.info(message)
45
+
46
+ except Exception:
47
+ # Avoid errors in forwarding from affecting main flow
48
+ self.handleError(record)
49
+
50
+
51
+ def get_lightrag_forwarding_config() -> dict:
52
+ """
53
+ Load LightRAG forwarding configuration from main.yaml.
54
+
55
+ Returns:
56
+ dict: Configuration dictionary with defaults if not found
57
+ """
58
+ try:
59
+ from src.services.config import load_config_with_main
60
+
61
+ # Use resolve() to get absolute path, ensuring correct project root regardless of working directory
62
+ project_root = Path(__file__).resolve().parent.parent.parent.parent
63
+ config = load_config_with_main("solve_config.yaml", project_root)
64
+
65
+ forwarding_config = config.get("logging", {}).get("lightrag_forwarding", {})
66
+
67
+ return {
68
+ "enabled": forwarding_config.get("enabled", True),
69
+ "min_level": forwarding_config.get("min_level", "INFO"),
70
+ "add_prefix": forwarding_config.get("add_prefix", True),
71
+ "logger_names": forwarding_config.get(
72
+ "logger_names", {"knowledge_init": "KnowledgeInit", "rag_tool": "RAGTool"}
73
+ ),
74
+ }
75
+ except Exception:
76
+ # Return defaults if config loading fails
77
+ return {
78
+ "enabled": True,
79
+ "min_level": "INFO",
80
+ "add_prefix": True,
81
+ "logger_names": {"knowledge_init": "KnowledgeInit", "rag_tool": "RAGTool"},
82
+ }
83
+
84
+
85
+ @contextmanager
86
+ def LightRAGLogContext(logger_name: Optional[str] = None, scene: Optional[str] = None):
87
+ """
88
+ Context manager for LightRAG log forwarding.
89
+
90
+ Automatically sets up and tears down log forwarding.
91
+
92
+ Args:
93
+ logger_name: Explicit logger name (overrides scene-based lookup)
94
+ scene: Scene name ('knowledge_init' or 'rag_tool') for logger name lookup
95
+
96
+ Usage:
97
+ with LightRAGLogContext("RAGTool"):
98
+ # RAG operations
99
+ rag = RAGAnything(...)
100
+ """
101
+ from ..logger import get_logger
102
+
103
+ # Get configuration
104
+ config = get_lightrag_forwarding_config()
105
+
106
+ # Check if forwarding is enabled
107
+ if not config.get("enabled", True):
108
+ # If disabled, just pass through without forwarding
109
+ yield
110
+ return
111
+
112
+ # Debug: Log that forwarding is being set up (only if we have a logger)
113
+ # This helps verify the context manager is being called
114
+ try:
115
+ debug_logger = get_logger("RAGForward")
116
+ debug_logger.debug(
117
+ f"Setting up LightRAG log forwarding (scene={scene}, logger_name={logger_name})"
118
+ )
119
+ except:
120
+ pass # Ignore if logger setup fails
121
+
122
+ # Determine logger name
123
+ if logger_name is None:
124
+ if scene:
125
+ logger_names = config.get("logger_names", {})
126
+ logger_name = logger_names.get(scene, "Main")
127
+ else:
128
+ logger_name = "Main"
129
+
130
+ # Get DeepTutor logger
131
+ ai_tutor_logger = get_logger(logger_name)
132
+
133
+ # Get forwarding settings
134
+ add_prefix = config.get("add_prefix", True)
135
+ min_level_str = config.get("min_level", "INFO")
136
+ min_level = getattr(logging, min_level_str.upper(), logging.INFO)
137
+
138
+ # Get LightRAG logger
139
+ lightrag_logger = logging.getLogger("lightrag")
140
+
141
+ # Store original handlers and level to restore later if needed
142
+ original_handlers = lightrag_logger.handlers[:] # Copy list
143
+ original_level = lightrag_logger.level
144
+
145
+ # Temporarily remove existing console handlers to avoid duplicate output
146
+ # We'll forward all logs through our handler instead
147
+ console_handlers_to_remove = []
148
+ for handler in original_handlers:
149
+ if isinstance(handler, logging.StreamHandler):
150
+ console_handlers_to_remove.append(handler)
151
+
152
+ for handler in console_handlers_to_remove:
153
+ lightrag_logger.removeHandler(handler)
154
+
155
+ # Ensure LightRAG logger level is set low enough to capture all logs
156
+ # The logger level controls which logs are created, handler level controls which are processed
157
+ # Set to DEBUG to ensure we capture everything, then filter at handler level
158
+ if lightrag_logger.level > logging.DEBUG:
159
+ lightrag_logger.setLevel(logging.DEBUG)
160
+
161
+ # Create and add forwarder
162
+ forwarder = LightRAGLogForwarder(ai_tutor_logger, add_prefix=add_prefix)
163
+ forwarder.setLevel(min_level)
164
+ lightrag_logger.addHandler(forwarder)
165
+
166
+ # Test that forwarding works by sending a test log
167
+ try:
168
+ test_msg = "LightRAG log forwarding enabled"
169
+ lightrag_logger.info(test_msg)
170
+ except:
171
+ pass # Ignore test log errors
172
+
173
+ try:
174
+ yield
175
+ finally:
176
+ # Clean up: remove our forwarder
177
+ if forwarder in lightrag_logger.handlers:
178
+ lightrag_logger.removeHandler(forwarder)
179
+ forwarder.close()
180
+
181
+ # Restore original console handlers if they were removed
182
+ for handler in console_handlers_to_remove:
183
+ if handler not in lightrag_logger.handlers:
184
+ lightrag_logger.addHandler(handler)
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ LlamaIndex Log Forwarder
4
+ ========================
5
+
6
+ Forwards LlamaIndex logs to DeepTutor's unified logging system.
7
+ """
8
+
9
+ from contextlib import contextmanager
10
+ import logging
11
+ from typing import Any, Dict, List, Optional, Tuple
12
+
13
+
14
+ class LlamaIndexLogForwarder(logging.Handler):
15
+ """
16
+ Handler that forwards LlamaIndex logger messages to DeepTutor logger.
17
+ """
18
+
19
+ def __init__(self, ai_tutor_logger, add_prefix: bool = True):
20
+ """
21
+ Args:
22
+ ai_tutor_logger: DeepTutor Logger instance
23
+ add_prefix: Whether to add prefix to messages
24
+ """
25
+ super().__init__()
26
+ self.ai_tutor_logger = ai_tutor_logger
27
+ self.add_prefix = add_prefix
28
+ # Capture all log levels
29
+ self.setLevel(logging.DEBUG)
30
+
31
+ def emit(self, record: logging.LogRecord):
32
+ """
33
+ Forward log record to DeepTutor logger.
34
+ """
35
+ try:
36
+ message = record.getMessage()
37
+
38
+ # Map log levels
39
+ level = record.levelno
40
+ if level >= logging.ERROR:
41
+ self.ai_tutor_logger.error(message)
42
+ elif level >= logging.WARNING:
43
+ self.ai_tutor_logger.warning(message)
44
+ elif level >= logging.INFO:
45
+ self.ai_tutor_logger.info(message)
46
+ else:
47
+ self.ai_tutor_logger.debug(message)
48
+
49
+ except Exception:
50
+ self.handleError(record)
51
+
52
+
53
+ @contextmanager
54
+ def LlamaIndexLogContext(
55
+ logger_name: Optional[str] = None,
56
+ scene: str = "llamaindex",
57
+ min_level: str = "INFO",
58
+ ):
59
+ """
60
+ Context manager for LlamaIndex log forwarding.
61
+
62
+ Automatically sets up and tears down log forwarding.
63
+
64
+ Args:
65
+ logger_name: Explicit logger name (defaults to scene name)
66
+ scene: Scene name for logger identification
67
+ min_level: Minimum log level to forward
68
+
69
+ Usage:
70
+ with LlamaIndexLogContext("VectorSearch"):
71
+ # LlamaIndex operations
72
+ index.query(query)
73
+ """
74
+ from ..logger import get_logger
75
+
76
+ # Determine logger name
77
+ if logger_name is None:
78
+ logger_name = scene.title().replace("_", "")
79
+
80
+ # Get DeepTutor logger
81
+ ai_tutor_logger = get_logger(logger_name)
82
+
83
+ # Get LlamaIndex loggers
84
+ llama_loggers = [
85
+ logging.getLogger("llama_index"),
86
+ logging.getLogger("llama_index.core"),
87
+ logging.getLogger("llama_index.vector_stores"),
88
+ logging.getLogger("llama_index.embeddings"),
89
+ ]
90
+
91
+ # Parse min level
92
+ min_level_int = getattr(logging, min_level.upper(), logging.INFO)
93
+
94
+ # Store original state
95
+ original_states: List[Dict[str, Any]] = []
96
+ forwarders: List[Tuple[logging.Logger, LlamaIndexLogForwarder]] = []
97
+
98
+ for llama_logger in llama_loggers:
99
+ original_states.append(
100
+ {
101
+ "logger": llama_logger,
102
+ "handlers": llama_logger.handlers[:],
103
+ "level": llama_logger.level,
104
+ }
105
+ )
106
+
107
+ # Temporarily remove console handlers
108
+ console_handlers_to_remove = []
109
+ for handler in llama_logger.handlers:
110
+ if isinstance(handler, logging.StreamHandler):
111
+ console_handlers_to_remove.append(handler)
112
+
113
+ for handler in console_handlers_to_remove:
114
+ llama_logger.removeHandler(handler)
115
+
116
+ # Set level
117
+ if llama_logger.level > logging.DEBUG:
118
+ llama_logger.setLevel(logging.DEBUG)
119
+
120
+ # Add forwarder
121
+ forwarder = LlamaIndexLogForwarder(ai_tutor_logger)
122
+ forwarder.setLevel(min_level_int)
123
+ llama_logger.addHandler(forwarder)
124
+ forwarders.append((llama_logger, forwarder))
125
+
126
+ try:
127
+ yield
128
+ finally:
129
+ # Clean up forwarders
130
+ for llama_logger, forwarder in forwarders:
131
+ if forwarder in llama_logger.handlers:
132
+ llama_logger.removeHandler(forwarder)
133
+ forwarder.close()
134
+
135
+ # Restore original state
136
+ for state in original_states:
137
+ llama_logger = state["logger"]
138
+ # Restore handlers that were removed
139
+ for handler in state["handlers"]:
140
+ if handler not in llama_logger.handlers:
141
+ llama_logger.addHandler(handler)
src/logging/config.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ Logging Configuration
3
+ =====================
4
+
5
+ Configuration settings for the logging system.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ @dataclass
14
+ class LoggingConfig:
15
+ """Configuration for the logging system."""
16
+
17
+ # Output settings
18
+ console_output: bool = True
19
+ file_output: bool = True
20
+
21
+ # Log levels
22
+ console_level: str = "INFO"
23
+ file_level: str = "DEBUG"
24
+
25
+ # Log directory (relative to project root or absolute)
26
+ log_dir: Optional[str] = None
27
+
28
+ # File rotation settings
29
+ max_bytes: int = 10 * 1024 * 1024 # 10MB
30
+ backup_count: int = 5
31
+
32
+ # WebSocket streaming
33
+ websocket_enabled: bool = True
34
+ websocket_queue_size: int = 1000
35
+
36
+ # LightRAG forwarding
37
+ lightrag_forwarding_enabled: bool = True
38
+ lightrag_min_level: str = "INFO"
39
+ lightrag_add_prefix: bool = True
40
+
41
+
42
+ def get_default_log_dir() -> Path:
43
+ """Get the default log directory."""
44
+ project_root = Path(__file__).resolve().parent.parent.parent
45
+ return project_root / "data" / "user" / "logs"
46
+
47
+
48
+ def load_logging_config() -> LoggingConfig:
49
+ """
50
+ Load logging configuration from config files.
51
+
52
+ Returns:
53
+ LoggingConfig instance with loaded or default values.
54
+ """
55
+ try:
56
+ from src.services.config import get_path_from_config, load_config_with_main
57
+
58
+ project_root = Path(__file__).resolve().parent.parent.parent
59
+ config = load_config_with_main("solve_config.yaml", project_root)
60
+
61
+ logging_config = config.get("logging", {})
62
+
63
+ return LoggingConfig(
64
+ console_output=logging_config.get("console_output", True),
65
+ file_output=logging_config.get("file_output", True),
66
+ console_level=logging_config.get("console_level", "INFO"),
67
+ file_level=logging_config.get("file_level", "DEBUG"),
68
+ log_dir=get_path_from_config(config, "user_log_dir"),
69
+ lightrag_forwarding_enabled=logging_config.get("lightrag_forwarding", {}).get(
70
+ "enabled", True
71
+ ),
72
+ lightrag_min_level=logging_config.get("lightrag_forwarding", {}).get(
73
+ "min_level", "INFO"
74
+ ),
75
+ lightrag_add_prefix=logging_config.get("lightrag_forwarding", {}).get(
76
+ "add_prefix", True
77
+ ),
78
+ )
79
+ except Exception:
80
+ return LoggingConfig()
@@ -0,0 +1,20 @@
1
+ """
2
+ Log Handlers
3
+ ============
4
+
5
+ Custom logging handlers for various output destinations.
6
+ """
7
+
8
+ from .console import ConsoleHandler
9
+ from .file import FileHandler, JSONFileHandler, RotatingFileHandler, create_task_logger
10
+ from .websocket import LogInterceptor, WebSocketLogHandler
11
+
12
+ __all__ = [
13
+ "ConsoleHandler",
14
+ "FileHandler",
15
+ "JSONFileHandler",
16
+ "RotatingFileHandler",
17
+ "WebSocketLogHandler",
18
+ "LogInterceptor",
19
+ "create_task_logger",
20
+ ]
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Console Log Handler
4
+ ===================
5
+
6
+ Color-coded console output with symbols.
7
+ """
8
+
9
+ import logging
10
+ import sys
11
+
12
+
13
+ class ConsoleFormatter(logging.Formatter):
14
+ """
15
+ Clean console formatter with colors and symbols.
16
+ Format: [Module] Symbol Message
17
+ """
18
+
19
+ # ANSI color codes
20
+ COLORS = {
21
+ "DEBUG": "\033[90m", # Gray
22
+ "INFO": "\033[37m", # White
23
+ "SUCCESS": "\033[32m", # Green
24
+ "WARNING": "\033[33m", # Yellow
25
+ "ERROR": "\033[31m", # Red
26
+ "CRITICAL": "\033[35m", # Magenta
27
+ }
28
+ RESET = "\033[0m"
29
+ BOLD = "\033[1m"
30
+ DIM = "\033[2m"
31
+
32
+ # Symbols for different log types
33
+ SYMBOLS = {
34
+ "DEBUG": "·",
35
+ "INFO": "●",
36
+ "SUCCESS": "✓",
37
+ "WARNING": "⚠",
38
+ "ERROR": "✗",
39
+ "CRITICAL": "✗",
40
+ }
41
+
42
+ def format(self, record: logging.LogRecord) -> str:
43
+ # Get module name (padded to 12 chars for alignment)
44
+ module = getattr(record, "module_name", record.name)
45
+ module_padded = f"[{module}]".ljust(14)
46
+
47
+ # Get symbol (can be overridden via record.symbol)
48
+ symbol = getattr(record, "symbol", self.SYMBOLS.get(record.levelname, "●"))
49
+
50
+ # Get color
51
+ level = getattr(record, "display_level", record.levelname)
52
+ color = self.COLORS.get(level, self.COLORS["INFO"])
53
+
54
+ # Format message
55
+ message = record.getMessage()
56
+
57
+ # Build output: [Module] ● Message
58
+ return f"{self.DIM}{module_padded}{self.RESET} {color}{symbol}{self.RESET} {message}"
59
+
60
+
61
+ class ConsoleHandler(logging.StreamHandler):
62
+ """
63
+ Console handler with color-coded output.
64
+ """
65
+
66
+ def __init__(self, level: int = logging.INFO):
67
+ """
68
+ Initialize console handler.
69
+
70
+ Args:
71
+ level: Minimum log level to display
72
+ """
73
+ super().__init__(sys.stdout)
74
+ self.setLevel(level)
75
+ self.setFormatter(ConsoleFormatter())
@@ -0,0 +1,201 @@
1
+ """
2
+ File Log Handlers
3
+ =================
4
+
5
+ File-based logging with rotation support.
6
+ """
7
+
8
+ import asyncio
9
+ from datetime import datetime
10
+ import json
11
+ import logging
12
+ from logging.handlers import RotatingFileHandler as BaseRotatingFileHandler
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ class FileFormatter(logging.Formatter):
18
+ """
19
+ Detailed file formatter for log files.
20
+ Format: TIMESTAMP [LEVEL] [Module] Message
21
+ """
22
+
23
+ def __init__(self):
24
+ super().__init__(
25
+ fmt="%(asctime)s [%(levelname)-8s] [%(module_name)-12s] %(message)s",
26
+ datefmt="%Y-%m-%d %H:%M:%S",
27
+ )
28
+
29
+ def format(self, record: logging.LogRecord) -> str:
30
+ # Ensure module_name exists
31
+ if not hasattr(record, "module_name"):
32
+ record.module_name = record.name
33
+ return super().format(record)
34
+
35
+
36
+ class FileHandler(logging.FileHandler):
37
+ """
38
+ File handler with detailed formatting.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ filename: str,
44
+ level: int = logging.DEBUG,
45
+ encoding: str = "utf-8",
46
+ ):
47
+ """
48
+ Initialize file handler.
49
+
50
+ Args:
51
+ filename: Path to log file
52
+ level: Minimum log level
53
+ encoding: File encoding
54
+ """
55
+ # Ensure directory exists
56
+ Path(filename).parent.mkdir(parents=True, exist_ok=True)
57
+
58
+ super().__init__(filename, encoding=encoding)
59
+ self.setLevel(level)
60
+ self.setFormatter(FileFormatter())
61
+
62
+
63
+ class RotatingFileHandler(BaseRotatingFileHandler):
64
+ """
65
+ Rotating file handler with size-based rotation.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ filename: str,
71
+ level: int = logging.DEBUG,
72
+ max_bytes: int = 10 * 1024 * 1024, # 10MB
73
+ backup_count: int = 5,
74
+ encoding: str = "utf-8",
75
+ ):
76
+ """
77
+ Initialize rotating file handler.
78
+
79
+ Args:
80
+ filename: Path to log file
81
+ level: Minimum log level
82
+ max_bytes: Maximum file size before rotation
83
+ backup_count: Number of backup files to keep
84
+ encoding: File encoding
85
+ """
86
+ # Ensure directory exists
87
+ Path(filename).parent.mkdir(parents=True, exist_ok=True)
88
+
89
+ super().__init__(
90
+ filename,
91
+ maxBytes=max_bytes,
92
+ backupCount=backup_count,
93
+ encoding=encoding,
94
+ )
95
+ self.setLevel(level)
96
+ self.setFormatter(FileFormatter())
97
+
98
+
99
+ class JSONFileHandler(logging.Handler):
100
+ """
101
+ A logging handler that writes structured JSON logs to a file.
102
+ Each line is a valid JSON object (JSONL format).
103
+
104
+ Useful for:
105
+ - LLM call logging
106
+ - Structured analysis
107
+ - Log parsing and analysis
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ filepath: str,
113
+ level: int = logging.DEBUG,
114
+ encoding: str = "utf-8",
115
+ ):
116
+ """
117
+ Initialize JSON file handler.
118
+
119
+ Args:
120
+ filepath: Path to log file
121
+ level: Minimum log level
122
+ encoding: File encoding
123
+ """
124
+ super().__init__()
125
+
126
+ # Ensure directory exists
127
+ Path(filepath).parent.mkdir(parents=True, exist_ok=True)
128
+
129
+ self.filepath = filepath
130
+ self.encoding = encoding
131
+ self.setLevel(level)
132
+ self.setFormatter(logging.Formatter("%(message)s"))
133
+
134
+ def emit(self, record: logging.LogRecord):
135
+ """Emit a log record as JSON."""
136
+ try:
137
+ # Build JSON entry
138
+ entry = {
139
+ "timestamp": datetime.fromtimestamp(record.created).isoformat(),
140
+ "level": record.levelname,
141
+ "module": getattr(record, "module_name", record.name),
142
+ "message": self.format(record),
143
+ }
144
+
145
+ # Add extra fields if present
146
+ for key in ["symbol", "display_level", "tool_name", "elapsed_ms", "tokens"]:
147
+ if hasattr(record, key):
148
+ entry[key] = getattr(record, key)
149
+
150
+ # Write to file
151
+ with open(self.filepath, "a", encoding=self.encoding) as f:
152
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
153
+
154
+ except Exception:
155
+ self.handleError(record)
156
+
157
+
158
+ def create_task_logger(
159
+ task_id: str,
160
+ module_name: str,
161
+ log_dir: str,
162
+ queue: Optional["asyncio.Queue"] = None,
163
+ ) -> logging.Logger:
164
+ """
165
+ Create a logger for a specific task with file and optional WebSocket output.
166
+
167
+ Args:
168
+ task_id: Unique task identifier
169
+ module_name: Module name (e.g., "Solver", "Research")
170
+ log_dir: Directory for log files
171
+ queue: Optional asyncio.Queue for WebSocket streaming
172
+
173
+ Returns:
174
+ Configured logger
175
+ """
176
+ from .websocket import WebSocketLogHandler
177
+
178
+ # Create log directory
179
+ log_path = Path(log_dir)
180
+ log_path.mkdir(parents=True, exist_ok=True)
181
+
182
+ # Create logger
183
+ logger = logging.getLogger(f"ai_tutor.{module_name}.{task_id}")
184
+ logger.setLevel(logging.DEBUG)
185
+ logger.handlers.clear()
186
+ logger.propagate = False
187
+
188
+ # File handler
189
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
190
+ log_file = log_path / f"{module_name}_{task_id}_{timestamp}.log"
191
+
192
+ file_handler = FileHandler(str(log_file))
193
+ logger.addHandler(file_handler)
194
+
195
+ # WebSocket handler if queue provided
196
+ if queue is not None:
197
+ ws_handler = WebSocketLogHandler(queue)
198
+ ws_handler.setLevel(logging.INFO)
199
+ logger.addHandler(ws_handler)
200
+
201
+ return logger