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
src/logging/logger.py ADDED
@@ -0,0 +1,709 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Core Logger Implementation
4
+ ==========================
5
+
6
+ Unified logging with consistent format across all modules.
7
+ Format: [Module] Symbol Message
8
+
9
+ Example outputs:
10
+ [Solver] ✓ Ready in 2.3s
11
+ [Research] → Starting deep research...
12
+ [Guide] → Compiling knowledge points
13
+ [Knowledge] ✓ Indexed 150 documents
14
+ """
15
+
16
+ from datetime import datetime
17
+ from enum import Enum
18
+ import json
19
+ import logging
20
+ from pathlib import Path
21
+ import sys
22
+ from typing import Any, List, Optional, Union
23
+
24
+ from src.config.constants import LOG_SYMBOLS, PROJECT_ROOT
25
+
26
+
27
+ class LogLevel(Enum):
28
+ """Log levels with associated symbols"""
29
+
30
+ DEBUG = ("DEBUG", "·") # Dot for debug
31
+ INFO = ("INFO", "●") # Circle for info
32
+ SUCCESS = ("SUCCESS", "✓") # Checkmark for success
33
+ WARNING = ("WARNING", "⚠") # Warning sign
34
+ ERROR = ("ERROR", "✗") # X for error
35
+ CRITICAL = ("CRITICAL", "✗") # X for critical
36
+ PROGRESS = ("INFO", "→") # Arrow for progress
37
+ COMPLETE = ("INFO", "✓") # Checkmark for completion
38
+
39
+
40
+ class ConsoleFormatter(logging.Formatter):
41
+ """
42
+ Clean console formatter with colors and symbols.
43
+ Format: [Module] Symbol Message
44
+ """
45
+
46
+ # ANSI color codes
47
+ COLORS = {
48
+ "DEBUG": "\033[90m", # Gray
49
+ "INFO": "\033[37m", # White
50
+ "SUCCESS": "\033[32m", # Green
51
+ "WARNING": "\033[33m", # Yellow
52
+ "ERROR": "\033[31m", # Red
53
+ "CRITICAL": "\033[35m", # Magenta
54
+ }
55
+ RESET = "\033[0m"
56
+ BOLD = "\033[1m"
57
+ DIM = "\033[2m"
58
+
59
+ # Symbols for different log types
60
+ SYMBOLS = LOG_SYMBOLS
61
+
62
+ def __init__(self):
63
+ super().__init__()
64
+ # Check TTY status once during initialization
65
+ stdout_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
66
+ stderr_tty = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
67
+ self.use_colors = stdout_tty or stderr_tty
68
+
69
+ def format(self, record: logging.LogRecord) -> str:
70
+ # Get module name (padded to 12 chars for alignment)
71
+ module = getattr(record, "module_name", record.name)
72
+ module_padded = f"[{module}]".ljust(14)
73
+ symbol = getattr(record, "symbol", self.SYMBOLS.get(record.levelname, "●"))
74
+ # Use pre-computed TTY status
75
+ use_colors = self.use_colors
76
+ if use_colors:
77
+ # Get color
78
+ level = getattr(record, "display_level", record.levelname)
79
+ color = self.COLORS.get(level, self.COLORS["INFO"])
80
+ dim = self.DIM
81
+ reset = self.RESET
82
+ else:
83
+ # No colors for non-interactive output
84
+ color = ""
85
+ dim = ""
86
+ reset = ""
87
+
88
+ # Format message
89
+ message = record.getMessage()
90
+
91
+ # Build output: [Module] ● Message
92
+ return f"{dim}{module_padded}{reset} {color}{symbol}{reset} {message}"
93
+
94
+
95
+ class FileFormatter(logging.Formatter):
96
+ """
97
+ Detailed file formatter for log files.
98
+ Format: TIMESTAMP [LEVEL] [Module] Message
99
+ """
100
+
101
+ def __init__(self):
102
+ super().__init__(
103
+ fmt="%(asctime)s [%(levelname)-8s] [%(module_name)-12s] %(message)s",
104
+ datefmt="%Y-%m-%d %H:%M:%S",
105
+ )
106
+
107
+ def format(self, record: logging.LogRecord) -> str:
108
+ # Ensure module_name exists
109
+ if not hasattr(record, "module_name"):
110
+ record.module_name = record.name
111
+ return super().format(record)
112
+
113
+
114
+ class Logger:
115
+ """
116
+ Unified logger for DeepTutor.
117
+
118
+ Features:
119
+ - Consistent format across all modules
120
+ - Color-coded console output
121
+ - File logging to user/logs/
122
+ - WebSocket streaming support
123
+ - Success/progress/complete convenience methods
124
+
125
+ Usage:
126
+ logger = Logger("Solver")
127
+ logger.info("Processing...")
128
+ logger.success("Done!", elapsed=2.3)
129
+ logger.progress("Step 1/5")
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ name: str,
135
+ level: str = "INFO",
136
+ console_output: bool = True,
137
+ file_output: bool = True,
138
+ log_dir: Optional[Union[str, Path]] = None,
139
+ ):
140
+ """
141
+ Initialize logger.
142
+
143
+ Args:
144
+ name: Module name (e.g., "Solver", "Research", "Guide")
145
+ level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
146
+ console_output: Whether to output to console
147
+ file_output: Whether to output to file
148
+ log_dir: Log directory (default: ../user/logs/)
149
+ """
150
+ self.name = name
151
+ self.level = getattr(logging, level.upper(), logging.INFO)
152
+
153
+ # Create underlying Python logger
154
+ self.logger = logging.getLogger(f"ai_tutor.{name}")
155
+ self.logger.setLevel(logging.DEBUG) # Capture all, filter at handlers
156
+ self.logger.handlers.clear()
157
+ # Setup log directory
158
+ log_dir_path: Path
159
+ if log_dir is None:
160
+ log_dir_path = PROJECT_ROOT / "data" / "user" / "logs"
161
+ else:
162
+ log_dir_path = Path(log_dir) if isinstance(log_dir, str) else log_dir
163
+ # If relative path, resolve it relative to project root
164
+ if not log_dir_path.is_absolute():
165
+ log_dir_path = PROJECT_ROOT / log_dir_path
166
+
167
+ log_dir_path.mkdir(parents=True, exist_ok=True)
168
+ self.log_dir = log_dir_path
169
+
170
+ # Console handler
171
+ if console_output:
172
+ console_handler = logging.StreamHandler(sys.stdout)
173
+ console_handler.setLevel(self.level)
174
+ console_handler.setFormatter(ConsoleFormatter())
175
+ self.logger.addHandler(console_handler)
176
+
177
+ # File handler
178
+ if file_output:
179
+ timestamp = datetime.now().strftime("%Y%m%d")
180
+ log_file = log_dir_path / f"ai_tutor_{timestamp}.log"
181
+
182
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
183
+ file_handler.setLevel(logging.DEBUG) # Log everything to file
184
+ file_handler.setFormatter(FileFormatter())
185
+ self.logger.addHandler(file_handler)
186
+
187
+ self._log_file = log_file
188
+
189
+ # For backwards compatibility with task-specific logging
190
+ self._task_handlers: List[logging.Handler] = []
191
+
192
+ # Display manager for TUI (optional, used by solve_agents)
193
+ self.display_manager = None
194
+
195
+ def add_task_log_handler(
196
+ self, task_log_file: str, capture_stdout: bool = False, capture_stderr: bool = False
197
+ ):
198
+ """
199
+ Add a task-specific log file handler.
200
+ For backwards compatibility with old SolveAgentLogger.
201
+
202
+ Args:
203
+ task_log_file: Path to the task log file
204
+ capture_stdout: Ignored (kept for API compatibility)
205
+ capture_stderr: Ignored (kept for API compatibility)
206
+ """
207
+ task_path = Path(task_log_file)
208
+ task_path.parent.mkdir(parents=True, exist_ok=True)
209
+
210
+ handler = logging.FileHandler(task_log_file, encoding="utf-8")
211
+ handler.setLevel(logging.DEBUG)
212
+ handler.setFormatter(FileFormatter())
213
+ self.logger.addHandler(handler)
214
+ self._task_handlers.append(handler)
215
+
216
+ def remove_task_log_handlers(self):
217
+ """Remove all task-specific log handlers."""
218
+ for handler in self._task_handlers:
219
+ self.logger.removeHandler(handler)
220
+ handler.close()
221
+ self._task_handlers.clear()
222
+
223
+ def log_stage_progress(self, stage: str, status: str, detail: Optional[str] = None):
224
+ """Backwards compatibility alias for stage()"""
225
+ self.stage(stage, status, detail)
226
+
227
+ def section(self, title: str, char: str = "=", length: int = 60):
228
+ """Print a section header"""
229
+ self.info(char * length)
230
+ self.info(title)
231
+ self.info(char * length)
232
+
233
+ def _log(
234
+ self,
235
+ level: int,
236
+ message: str,
237
+ symbol: Optional[str] = None,
238
+ display_level: Optional[str] = None,
239
+ **kwargs,
240
+ ):
241
+ """Internal logging method with extra attributes."""
242
+ extra = {
243
+ "module_name": self.name,
244
+ "symbol": symbol,
245
+ "display_level": display_level or logging.getLevelName(level),
246
+ }
247
+ # Extract standard logging parameters from kwargs
248
+ log_kwargs = {
249
+ "extra": extra,
250
+ "exc_info": kwargs.get("exc_info", False),
251
+ "stack_info": kwargs.get("stack_info", False),
252
+ "stacklevel": kwargs.get("stacklevel", 1),
253
+ }
254
+ self.logger.log(level, message, **log_kwargs)
255
+
256
+ # Standard logging methods
257
+ def debug(self, message: str, **kwargs):
258
+ """Debug level log (·)"""
259
+ self._log(logging.DEBUG, message, symbol="·", **kwargs)
260
+
261
+ def info(self, message: str, **kwargs):
262
+ """Info level log (●)"""
263
+ self._log(logging.INFO, message, symbol="●", **kwargs)
264
+
265
+ def warning(self, message: str, **kwargs):
266
+ """Warning level log (⚠)"""
267
+ self._log(logging.WARNING, message, symbol="⚠", **kwargs)
268
+
269
+ def error(self, message: str, **kwargs):
270
+ """Error level log (✗)"""
271
+ self._log(logging.ERROR, message, symbol="✗", **kwargs)
272
+
273
+ def critical(self, message: str, **kwargs):
274
+ """Critical level log (✗)"""
275
+ self._log(logging.CRITICAL, message, symbol="✗", **kwargs)
276
+
277
+ def exception(self, message: str, **kwargs):
278
+ """Log exception with traceback"""
279
+ self.logger.exception(
280
+ message, extra={"module_name": self.name, "symbol": "✗", "display_level": "ERROR"}
281
+ )
282
+
283
+ # Convenience methods
284
+ def success(self, message: str, elapsed: Optional[float] = None, **kwargs):
285
+ """Success log with checkmark (✓)"""
286
+ if elapsed is not None:
287
+ message = f"{message} in {elapsed:.1f}s"
288
+ self._log(logging.INFO, message, symbol="✓", display_level="SUCCESS", **kwargs)
289
+
290
+ def progress(self, message: str, **kwargs):
291
+ """Progress log with arrow (→)"""
292
+ self._log(logging.INFO, message, symbol="→", **kwargs)
293
+
294
+ def complete(self, message: str, **kwargs):
295
+ """Completion log with checkmark (✓)"""
296
+ self._log(logging.INFO, message, symbol="✓", display_level="SUCCESS", **kwargs)
297
+
298
+ def stage(self, stage_name: str, status: str = "start", detail: Optional[str] = None):
299
+ """
300
+ Log stage progress.
301
+
302
+ Args:
303
+ stage_name: Name of the stage (e.g., "Analysis", "Synthesis")
304
+ status: One of "start", "running", "complete", "skip", "error"
305
+ detail: Optional detail message
306
+ """
307
+ symbols = {
308
+ "start": "▶",
309
+ "running": "●",
310
+ "complete": "✓",
311
+ "skip": "○",
312
+ "error": "✗",
313
+ "warning": "⚠",
314
+ }
315
+ symbol = symbols.get(status, "●")
316
+
317
+ message = f"{stage_name}"
318
+ if status == "complete":
319
+ message += " completed"
320
+ elif status == "start":
321
+ message += " started"
322
+ elif status == "running":
323
+ message += " running"
324
+ elif status == "skip":
325
+ message += " skipped"
326
+ elif status == "error":
327
+ message += " failed"
328
+
329
+ if detail:
330
+ message += f" | {detail}"
331
+
332
+ level = logging.ERROR if status == "error" else logging.INFO
333
+ display_level = (
334
+ "ERROR" if status == "error" else ("SUCCESS" if status == "complete" else "INFO")
335
+ )
336
+ self._log(level, message, symbol=symbol, display_level=display_level)
337
+
338
+ def tool_call(
339
+ self, tool_name: str, status: str = "success", elapsed_ms: Optional[float] = None, **kwargs
340
+ ):
341
+ """
342
+ Log tool call.
343
+
344
+ Args:
345
+ tool_name: Name of the tool
346
+ status: "success", "error", or "running"
347
+ elapsed_ms: Execution time in milliseconds
348
+ """
349
+ symbol = "✓" if status == "success" else ("✗" if status == "error" else "●")
350
+ display_level = (
351
+ "SUCCESS" if status == "success" else ("ERROR" if status == "error" else "INFO")
352
+ )
353
+
354
+ message = f"Tool: {tool_name}"
355
+ if elapsed_ms is not None:
356
+ message += f" ({elapsed_ms:.0f}ms)"
357
+ if status == "error":
358
+ message += " [FAILED]"
359
+
360
+ self._log(
361
+ logging.INFO if status != "error" else logging.ERROR,
362
+ message,
363
+ symbol=symbol,
364
+ display_level=display_level,
365
+ )
366
+
367
+ def llm_call(
368
+ self,
369
+ model: str,
370
+ agent: Optional[str] = None,
371
+ tokens_in: Optional[int] = None,
372
+ tokens_out: Optional[int] = None,
373
+ elapsed: Optional[float] = None,
374
+ **kwargs,
375
+ ):
376
+ """
377
+ Log LLM API call.
378
+
379
+ Args:
380
+ model: Model name
381
+ agent: Agent making the call
382
+ tokens_in: Input tokens
383
+ tokens_out: Output tokens
384
+ elapsed: Call duration in seconds
385
+ """
386
+ parts = [f"LLM: {model}"]
387
+ if agent:
388
+ parts.append(f"agent={agent}")
389
+ if tokens_in is not None:
390
+ parts.append(f"in={tokens_in}")
391
+ if tokens_out is not None:
392
+ parts.append(f"out={tokens_out}")
393
+ if elapsed is not None:
394
+ parts.append(f"{elapsed:.2f}s")
395
+
396
+ message = " | ".join(parts)
397
+ self._log(logging.DEBUG, message, symbol="◆")
398
+
399
+ def separator(self, char: str = "─", length: int = 50):
400
+ """Print a separator line"""
401
+ self.info(char * length)
402
+
403
+ def log_tool_call(
404
+ self,
405
+ tool_name: str,
406
+ tool_input: Any = None,
407
+ tool_output: Any = None,
408
+ status: str = "success",
409
+ elapsed_ms: Optional[float] = None,
410
+ **kwargs,
411
+ ):
412
+ """
413
+ Log a tool call with input/output details.
414
+ Backwards compatible with old SolveAgentLogger.
415
+
416
+ Args:
417
+ tool_name: Name of the tool
418
+ tool_input: Tool input (logged to file only)
419
+ tool_output: Tool output (logged to file only)
420
+ status: "success", "error", or "running"
421
+ elapsed_ms: Execution time in milliseconds
422
+ """
423
+ symbol = "✓" if status == "success" else ("✗" if status == "error" else "●")
424
+ display_level = (
425
+ "SUCCESS" if status == "success" else ("ERROR" if status == "error" else "INFO")
426
+ )
427
+
428
+ # Console message (brief)
429
+ message = f"Tool: {tool_name}"
430
+ if elapsed_ms is not None:
431
+ message += f" ({elapsed_ms:.0f}ms)"
432
+ if status == "error":
433
+ message += " [FAILED]"
434
+
435
+ self._log(
436
+ logging.INFO if status != "error" else logging.ERROR,
437
+ message,
438
+ symbol=symbol,
439
+ display_level=display_level,
440
+ )
441
+
442
+ # Debug log with full details (file only)
443
+ if tool_input is not None:
444
+ try:
445
+ input_str = (
446
+ json.dumps(tool_input, ensure_ascii=False, indent=2)
447
+ if isinstance(tool_input, (dict, list))
448
+ else str(tool_input)
449
+ )
450
+ self.debug(f"Tool Input: {input_str[:500]}...")
451
+ except:
452
+ pass
453
+ if tool_output is not None:
454
+ try:
455
+ output_str = (
456
+ json.dumps(tool_output, ensure_ascii=False, indent=2)
457
+ if isinstance(tool_output, (dict, list))
458
+ else str(tool_output)
459
+ )
460
+ self.debug(f"Tool Output: {output_str[:500]}...")
461
+ except:
462
+ pass
463
+
464
+ def log_llm_input(
465
+ self,
466
+ agent_name: str,
467
+ stage: str,
468
+ system_prompt: str,
469
+ user_prompt: str,
470
+ metadata: Optional[dict[str, Any]] = None,
471
+ ):
472
+ """Log LLM input (debug level, file only)"""
473
+ self.debug(
474
+ f"LLM Input [{agent_name}:{stage}] system={len(system_prompt)}chars, user={len(user_prompt)}chars"
475
+ )
476
+
477
+ def log_llm_output(
478
+ self, agent_name: str, stage: str, response: str, metadata: Optional[dict[str, Any]] = None
479
+ ):
480
+ """Log LLM output (debug level, file only)"""
481
+ self.debug(f"LLM Output [{agent_name}:{stage}] response={len(response)}chars")
482
+
483
+ def log_llm_call(
484
+ self,
485
+ model: str,
486
+ stage: str,
487
+ system_prompt: str,
488
+ user_prompt: str,
489
+ response: str,
490
+ agent_name: Optional[str] = None,
491
+ input_tokens: Optional[int] = None,
492
+ output_tokens: Optional[int] = None,
493
+ cost: Optional[float] = None,
494
+ level: str = "INFO",
495
+ ):
496
+ """
497
+ Log complete LLM call with formatted output.
498
+
499
+ Args:
500
+ model: Model name
501
+ stage: Stage name (e.g., "generate_question", "validate")
502
+ system_prompt: System prompt content
503
+ user_prompt: User prompt content
504
+ response: LLM response content
505
+ agent_name: Agent name (optional)
506
+ input_tokens: Input token count (optional)
507
+ output_tokens: Output token count (optional)
508
+ cost: Estimated cost (optional)
509
+ level: Log level ("DEBUG" for full details, "INFO" for summary)
510
+ """
511
+ # Build header
512
+ header_parts = ["[LLM-CALL]"]
513
+ if agent_name:
514
+ header_parts.append(f"[Agent: {agent_name}]")
515
+ header_parts.append(f"[Stage: {stage}]")
516
+ header_parts.append(f"[Model: {model}]")
517
+ header = " ".join(header_parts)
518
+
519
+ # Log at appropriate level
520
+ log_level = logging.DEBUG if level == "DEBUG" else logging.INFO
521
+
522
+ if level == "DEBUG":
523
+ # Full detailed output
524
+ self._log(log_level, header, symbol="◆")
525
+ self._log(
526
+ log_level, "┌─ Input ──────────────────────────────────────────────", symbol=" "
527
+ )
528
+ self._log(
529
+ log_level,
530
+ (
531
+ f"System: {system_prompt[:200]}..."
532
+ if len(system_prompt) > 200
533
+ else f"System: {system_prompt}"
534
+ ),
535
+ symbol=" ",
536
+ )
537
+ self._log(
538
+ log_level,
539
+ (
540
+ f"User: {user_prompt[:500]}..."
541
+ if len(user_prompt) > 500
542
+ else f"User: {user_prompt}"
543
+ ),
544
+ symbol=" ",
545
+ )
546
+ self._log(
547
+ log_level, "└──────────────────────────────────────────────────────", symbol=" "
548
+ )
549
+ self._log(
550
+ log_level, "┌─ Output ─────────────────────────────────────────────", symbol=" "
551
+ )
552
+ self._log(
553
+ log_level, f"{response[:1000]}..." if len(response) > 1000 else response, symbol=" "
554
+ )
555
+ self._log(
556
+ log_level, "└──────────────────────────────────────────────────────", symbol=" "
557
+ )
558
+
559
+ # Token and cost info
560
+ token_info_parts = []
561
+ if input_tokens is not None:
562
+ token_info_parts.append(f"in={input_tokens}")
563
+ if output_tokens is not None:
564
+ token_info_parts.append(f"out={output_tokens}")
565
+ if input_tokens is not None and output_tokens is not None:
566
+ token_info_parts.append(f"total={input_tokens + output_tokens}")
567
+ if cost is not None:
568
+ token_info_parts.append(f"cost=${cost:.6f}")
569
+
570
+ if token_info_parts:
571
+ self._log(log_level, f"[Tokens: {' '.join(token_info_parts)}]", symbol=" ")
572
+ else:
573
+ # Summary output
574
+ token_info = ""
575
+ if input_tokens is not None and output_tokens is not None:
576
+ token_info = f" [Tokens: in={input_tokens}, out={output_tokens}, total={input_tokens + output_tokens}]"
577
+ if cost is not None:
578
+ token_info += f" [Cost: ${cost:.6f}]"
579
+
580
+ message = f"{header}{token_info}"
581
+ self._log(log_level, message, symbol="◆")
582
+
583
+ def update_token_stats(self, summary: dict[str, Any]):
584
+ """Update token statistics (for display manager compatibility)"""
585
+ # Log token stats at debug level
586
+ if summary:
587
+ total_tokens = summary.get("total_tokens", 0)
588
+ self.debug(f"Token Stats: {total_tokens} tokens")
589
+
590
+ def shutdown(self):
591
+ """
592
+ Shut down this logger by cleaning up **all** attached handlers.
593
+
594
+ This method iterates over a copy of ``self.logger.handlers``, calls
595
+ ``close()`` on each handler to release any underlying resources
596
+ (such as open file streams or other I/O handles), and then removes
597
+ the handler from the underlying ``logging.Logger`` instance.
598
+
599
+ Note:
600
+ This closes and removes every handler currently attached to this
601
+ logger instance (including any task-specific handlers), not just a
602
+ subset of handlers. Callers that previously relied on only
603
+ task-specific handlers being removed should be aware that this
604
+ method now performs a full cleanup of all handlers.
605
+ """
606
+ # Close all handlers
607
+ for handler in self.logger.handlers[:]:
608
+ handler.close()
609
+ self.logger.removeHandler(handler)
610
+
611
+
612
+ # Global logger registry - key is tuple of (name, level, console_output, file_output, log_dir)
613
+ _loggers: dict[tuple[str, str, bool, bool, Optional[str]], "Logger"] = {}
614
+
615
+
616
+ def get_logger(
617
+ name: str = "Main",
618
+ level: str = "INFO",
619
+ console_output: bool = True,
620
+ file_output: bool = True,
621
+ log_dir: Optional[str] = None,
622
+ ) -> Logger:
623
+ """
624
+ Get or create a logger instance.
625
+
626
+ Args:
627
+ name: Module name
628
+ level: Log level
629
+ console_output: Enable console output
630
+ file_output: Enable file output
631
+ log_dir: Log directory (if None, will try to load from config/main.yaml)
632
+
633
+ Returns:
634
+ Logger instance
635
+ """
636
+ global _loggers
637
+
638
+ # If log_dir not provided, try to load from config
639
+ if log_dir is None:
640
+ try:
641
+ from src.services.config import get_path_from_config, load_config_with_main
642
+
643
+ # Use resolve() to get absolute path, ensuring correct project root regardless of working directory
644
+ config = load_config_with_main(
645
+ "solve_config.yaml", PROJECT_ROOT
646
+ ) # Use any config to get main.yaml
647
+ log_dir = get_path_from_config(config, "user_log_dir") or config.get("paths", {}).get(
648
+ "user_log_dir"
649
+ )
650
+ if log_dir:
651
+ # Convert relative path to absolute based on project root
652
+ log_dir_path = Path(log_dir)
653
+ if not log_dir_path.is_absolute():
654
+ # Remove leading ./ if present
655
+ log_dir_str = str(log_dir_path).lstrip("./")
656
+ log_dir = str(PROJECT_ROOT / log_dir_str)
657
+ else:
658
+ log_dir = str(log_dir_path)
659
+ except Exception:
660
+ # Fallback to default
661
+ pass
662
+ log_dir_key = str(log_dir) if log_dir is not None else None
663
+ # Create a cache key that includes configuration, using a normalized log_dir
664
+ cache_key = (name, level, console_output, file_output, log_dir_key)
665
+
666
+ if cache_key not in _loggers:
667
+ _loggers[cache_key] = Logger(
668
+ name=name,
669
+ level=level,
670
+ console_output=console_output,
671
+ file_output=file_output,
672
+ log_dir=log_dir,
673
+ )
674
+
675
+ return _loggers[cache_key]
676
+
677
+
678
+ def reset_logger(name: Optional[str] = None):
679
+ """
680
+ Reset logger(s).
681
+
682
+ Args:
683
+ name: Logger name to reset, or None to reset all
684
+ """
685
+ global _loggers
686
+
687
+ if name is None:
688
+ keys_to_remove = list(_loggers.keys())
689
+ else:
690
+ # Remove all loggers with the given name, supporting both tuple and string keys
691
+ keys_to_remove = [
692
+ key
693
+ for key in _loggers.keys()
694
+ if (isinstance(key, tuple) and len(key) > 0 and key[0] == name) or key == name
695
+ ]
696
+
697
+ for key in keys_to_remove:
698
+ _loggers.pop(key, None)
699
+
700
+
701
+ def reload_loggers():
702
+ """
703
+ Reload configuration for all cached loggers.
704
+
705
+ This method clears the logger cache, forcing recreation with current config
706
+ on next get_logger() calls.
707
+ """
708
+ global _loggers
709
+ _loggers.clear()