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.
- realtimex_deeptutor/__init__.py +67 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
- src/__init__.py +40 -0
- src/agents/__init__.py +24 -0
- src/agents/base_agent.py +657 -0
- src/agents/chat/__init__.py +24 -0
- src/agents/chat/chat_agent.py +435 -0
- src/agents/chat/prompts/en/chat_agent.yaml +35 -0
- src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
- src/agents/chat/session_manager.py +311 -0
- src/agents/co_writer/__init__.py +0 -0
- src/agents/co_writer/edit_agent.py +260 -0
- src/agents/co_writer/narrator_agent.py +423 -0
- src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
- src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
- src/agents/guide/__init__.py +16 -0
- src/agents/guide/agents/__init__.py +11 -0
- src/agents/guide/agents/chat_agent.py +104 -0
- src/agents/guide/agents/interactive_agent.py +223 -0
- src/agents/guide/agents/locate_agent.py +149 -0
- src/agents/guide/agents/summary_agent.py +150 -0
- src/agents/guide/guide_manager.py +500 -0
- src/agents/guide/prompts/en/chat_agent.yaml +41 -0
- src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
- src/agents/guide/prompts/en/locate_agent.yaml +68 -0
- src/agents/guide/prompts/en/summary_agent.yaml +157 -0
- src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
- src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
- src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
- src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
- src/agents/ideagen/__init__.py +12 -0
- src/agents/ideagen/idea_generation_workflow.py +426 -0
- src/agents/ideagen/material_organizer_agent.py +173 -0
- src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
- src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
- src/agents/question/__init__.py +24 -0
- src/agents/question/agents/__init__.py +18 -0
- src/agents/question/agents/generate_agent.py +381 -0
- src/agents/question/agents/relevance_analyzer.py +207 -0
- src/agents/question/agents/retrieve_agent.py +239 -0
- src/agents/question/coordinator.py +718 -0
- src/agents/question/example.py +109 -0
- src/agents/question/prompts/en/coordinator.yaml +75 -0
- src/agents/question/prompts/en/generate_agent.yaml +77 -0
- src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
- src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
- src/agents/question/prompts/zh/coordinator.yaml +75 -0
- src/agents/question/prompts/zh/generate_agent.yaml +77 -0
- src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
- src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
- src/agents/research/agents/__init__.py +23 -0
- src/agents/research/agents/decompose_agent.py +507 -0
- src/agents/research/agents/manager_agent.py +228 -0
- src/agents/research/agents/note_agent.py +180 -0
- src/agents/research/agents/rephrase_agent.py +263 -0
- src/agents/research/agents/reporting_agent.py +1333 -0
- src/agents/research/agents/research_agent.py +714 -0
- src/agents/research/data_structures.py +451 -0
- src/agents/research/main.py +188 -0
- src/agents/research/prompts/en/decompose_agent.yaml +89 -0
- src/agents/research/prompts/en/manager_agent.yaml +24 -0
- src/agents/research/prompts/en/note_agent.yaml +121 -0
- src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/en/reporting_agent.yaml +380 -0
- src/agents/research/prompts/en/research_agent.yaml +173 -0
- src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
- src/agents/research/prompts/zh/manager_agent.yaml +24 -0
- src/agents/research/prompts/zh/note_agent.yaml +121 -0
- src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
- src/agents/research/prompts/zh/research_agent.yaml +173 -0
- src/agents/research/research_pipeline.py +1309 -0
- src/agents/research/utils/__init__.py +60 -0
- src/agents/research/utils/citation_manager.py +799 -0
- src/agents/research/utils/json_utils.py +98 -0
- src/agents/research/utils/token_tracker.py +297 -0
- src/agents/solve/__init__.py +80 -0
- src/agents/solve/analysis_loop/__init__.py +14 -0
- src/agents/solve/analysis_loop/investigate_agent.py +414 -0
- src/agents/solve/analysis_loop/note_agent.py +190 -0
- src/agents/solve/main_solver.py +862 -0
- src/agents/solve/memory/__init__.py +34 -0
- src/agents/solve/memory/citation_memory.py +353 -0
- src/agents/solve/memory/investigate_memory.py +226 -0
- src/agents/solve/memory/solve_memory.py +340 -0
- src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
- src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
- src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
- src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
- src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
- src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
- src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
- src/agents/solve/solve_loop/__init__.py +22 -0
- src/agents/solve/solve_loop/citation_manager.py +74 -0
- src/agents/solve/solve_loop/manager_agent.py +274 -0
- src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
- src/agents/solve/solve_loop/response_agent.py +301 -0
- src/agents/solve/solve_loop/solve_agent.py +325 -0
- src/agents/solve/solve_loop/tool_agent.py +470 -0
- src/agents/solve/utils/__init__.py +64 -0
- src/agents/solve/utils/config_validator.py +313 -0
- src/agents/solve/utils/display_manager.py +223 -0
- src/agents/solve/utils/error_handler.py +363 -0
- src/agents/solve/utils/json_utils.py +98 -0
- src/agents/solve/utils/performance_monitor.py +407 -0
- src/agents/solve/utils/token_tracker.py +541 -0
- src/api/__init__.py +0 -0
- src/api/main.py +240 -0
- src/api/routers/__init__.py +1 -0
- src/api/routers/agent_config.py +69 -0
- src/api/routers/chat.py +296 -0
- src/api/routers/co_writer.py +337 -0
- src/api/routers/config.py +627 -0
- src/api/routers/dashboard.py +18 -0
- src/api/routers/guide.py +337 -0
- src/api/routers/ideagen.py +436 -0
- src/api/routers/knowledge.py +821 -0
- src/api/routers/notebook.py +247 -0
- src/api/routers/question.py +537 -0
- src/api/routers/research.py +394 -0
- src/api/routers/settings.py +164 -0
- src/api/routers/solve.py +305 -0
- src/api/routers/system.py +252 -0
- src/api/run_server.py +61 -0
- src/api/utils/history.py +172 -0
- src/api/utils/log_interceptor.py +21 -0
- src/api/utils/notebook_manager.py +415 -0
- src/api/utils/progress_broadcaster.py +72 -0
- src/api/utils/task_id_manager.py +100 -0
- src/config/__init__.py +0 -0
- src/config/accessors.py +18 -0
- src/config/constants.py +34 -0
- src/config/defaults.py +18 -0
- src/config/schema.py +38 -0
- src/config/settings.py +50 -0
- src/core/errors.py +62 -0
- src/knowledge/__init__.py +23 -0
- src/knowledge/add_documents.py +606 -0
- src/knowledge/config.py +65 -0
- src/knowledge/example_add_documents.py +236 -0
- src/knowledge/extract_numbered_items.py +1039 -0
- src/knowledge/initializer.py +621 -0
- src/knowledge/kb.py +22 -0
- src/knowledge/manager.py +782 -0
- src/knowledge/progress_tracker.py +182 -0
- src/knowledge/start_kb.py +535 -0
- src/logging/__init__.py +103 -0
- src/logging/adapters/__init__.py +17 -0
- src/logging/adapters/lightrag.py +184 -0
- src/logging/adapters/llamaindex.py +141 -0
- src/logging/config.py +80 -0
- src/logging/handlers/__init__.py +20 -0
- src/logging/handlers/console.py +75 -0
- src/logging/handlers/file.py +201 -0
- src/logging/handlers/websocket.py +127 -0
- src/logging/logger.py +709 -0
- src/logging/stats/__init__.py +16 -0
- src/logging/stats/llm_stats.py +179 -0
- src/services/__init__.py +56 -0
- src/services/config/__init__.py +61 -0
- src/services/config/knowledge_base_config.py +210 -0
- src/services/config/loader.py +260 -0
- src/services/config/unified_config.py +603 -0
- src/services/embedding/__init__.py +45 -0
- src/services/embedding/adapters/__init__.py +22 -0
- src/services/embedding/adapters/base.py +106 -0
- src/services/embedding/adapters/cohere.py +127 -0
- src/services/embedding/adapters/jina.py +99 -0
- src/services/embedding/adapters/ollama.py +116 -0
- src/services/embedding/adapters/openai_compatible.py +96 -0
- src/services/embedding/client.py +159 -0
- src/services/embedding/config.py +156 -0
- src/services/embedding/provider.py +119 -0
- src/services/llm/__init__.py +152 -0
- src/services/llm/capabilities.py +313 -0
- src/services/llm/client.py +302 -0
- src/services/llm/cloud_provider.py +530 -0
- src/services/llm/config.py +200 -0
- src/services/llm/error_mapping.py +103 -0
- src/services/llm/exceptions.py +152 -0
- src/services/llm/factory.py +450 -0
- src/services/llm/local_provider.py +347 -0
- src/services/llm/providers/anthropic.py +95 -0
- src/services/llm/providers/base_provider.py +93 -0
- src/services/llm/providers/open_ai.py +83 -0
- src/services/llm/registry.py +71 -0
- src/services/llm/telemetry.py +40 -0
- src/services/llm/types.py +27 -0
- src/services/llm/utils.py +333 -0
- src/services/prompt/__init__.py +25 -0
- src/services/prompt/manager.py +206 -0
- src/services/rag/__init__.py +64 -0
- src/services/rag/components/__init__.py +29 -0
- src/services/rag/components/base.py +59 -0
- src/services/rag/components/chunkers/__init__.py +18 -0
- src/services/rag/components/chunkers/base.py +34 -0
- src/services/rag/components/chunkers/fixed.py +71 -0
- src/services/rag/components/chunkers/numbered_item.py +94 -0
- src/services/rag/components/chunkers/semantic.py +97 -0
- src/services/rag/components/embedders/__init__.py +14 -0
- src/services/rag/components/embedders/base.py +32 -0
- src/services/rag/components/embedders/openai.py +63 -0
- src/services/rag/components/indexers/__init__.py +18 -0
- src/services/rag/components/indexers/base.py +35 -0
- src/services/rag/components/indexers/graph.py +172 -0
- src/services/rag/components/indexers/lightrag.py +156 -0
- src/services/rag/components/indexers/vector.py +146 -0
- src/services/rag/components/parsers/__init__.py +18 -0
- src/services/rag/components/parsers/base.py +35 -0
- src/services/rag/components/parsers/markdown.py +52 -0
- src/services/rag/components/parsers/pdf.py +115 -0
- src/services/rag/components/parsers/text.py +86 -0
- src/services/rag/components/retrievers/__init__.py +18 -0
- src/services/rag/components/retrievers/base.py +34 -0
- src/services/rag/components/retrievers/dense.py +200 -0
- src/services/rag/components/retrievers/hybrid.py +164 -0
- src/services/rag/components/retrievers/lightrag.py +169 -0
- src/services/rag/components/routing.py +286 -0
- src/services/rag/factory.py +234 -0
- src/services/rag/pipeline.py +215 -0
- src/services/rag/pipelines/__init__.py +32 -0
- src/services/rag/pipelines/academic.py +44 -0
- src/services/rag/pipelines/lightrag.py +43 -0
- src/services/rag/pipelines/llamaindex.py +313 -0
- src/services/rag/pipelines/raganything.py +384 -0
- src/services/rag/service.py +244 -0
- src/services/rag/types.py +73 -0
- src/services/search/__init__.py +284 -0
- src/services/search/base.py +87 -0
- src/services/search/consolidation.py +398 -0
- src/services/search/providers/__init__.py +128 -0
- src/services/search/providers/baidu.py +188 -0
- src/services/search/providers/exa.py +194 -0
- src/services/search/providers/jina.py +161 -0
- src/services/search/providers/perplexity.py +153 -0
- src/services/search/providers/serper.py +209 -0
- src/services/search/providers/tavily.py +161 -0
- src/services/search/types.py +114 -0
- src/services/setup/__init__.py +34 -0
- src/services/setup/init.py +285 -0
- src/services/tts/__init__.py +16 -0
- src/services/tts/config.py +99 -0
- src/tools/__init__.py +91 -0
- src/tools/code_executor.py +536 -0
- src/tools/paper_search_tool.py +171 -0
- src/tools/query_item_tool.py +310 -0
- src/tools/question/__init__.py +15 -0
- src/tools/question/exam_mimic.py +616 -0
- src/tools/question/pdf_parser.py +211 -0
- src/tools/question/question_extractor.py +397 -0
- src/tools/rag_tool.py +173 -0
- src/tools/tex_chunker.py +339 -0
- src/tools/tex_downloader.py +253 -0
- src/tools/web_search.py +71 -0
- src/utils/config_manager.py +206 -0
- src/utils/document_validator.py +168 -0
- src/utils/error_rate_tracker.py +111 -0
- src/utils/error_utils.py +82 -0
- src/utils/json_parser.py +110 -0
- 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()
|