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