realtimex-deeptutor 0.5.0.post1__py3-none-any.whl → 0.5.0.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/METADATA +24 -17
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/RECORD +143 -123
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/WHEEL +1 -1
- realtimex_deeptutor-0.5.0.post3.dist-info/entry_points.txt +4 -0
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/top_level.txt +1 -0
- scripts/__init__.py +1 -0
- scripts/audit_prompts.py +179 -0
- scripts/check_install.py +460 -0
- scripts/generate_roster.py +327 -0
- scripts/install_all.py +653 -0
- scripts/migrate_kb.py +655 -0
- scripts/start.py +807 -0
- scripts/start_web.py +632 -0
- scripts/sync_prompts_from_en.py +147 -0
- src/__init__.py +2 -2
- src/agents/ideagen/material_organizer_agent.py +2 -0
- src/agents/solve/__init__.py +6 -0
- src/agents/solve/main_solver.py +9 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +9 -7
- src/agents/solve/session_manager.py +345 -0
- src/api/main.py +14 -0
- src/api/routers/chat.py +3 -3
- src/api/routers/co_writer.py +12 -7
- src/api/routers/config.py +1 -0
- src/api/routers/guide.py +3 -1
- src/api/routers/ideagen.py +7 -0
- src/api/routers/knowledge.py +64 -12
- src/api/routers/question.py +2 -0
- src/api/routers/realtimex.py +137 -0
- src/api/routers/research.py +9 -0
- src/api/routers/solve.py +120 -2
- src/cli/__init__.py +13 -0
- src/cli/start.py +209 -0
- src/config/constants.py +11 -9
- src/knowledge/add_documents.py +453 -213
- src/knowledge/extract_numbered_items.py +9 -10
- src/knowledge/initializer.py +102 -101
- src/knowledge/manager.py +251 -74
- src/knowledge/progress_tracker.py +43 -2
- src/knowledge/start_kb.py +11 -2
- src/logging/__init__.py +5 -0
- src/logging/adapters/__init__.py +1 -0
- src/logging/adapters/lightrag.py +25 -18
- src/logging/adapters/llamaindex.py +1 -0
- src/logging/config.py +30 -27
- src/logging/handlers/__init__.py +1 -0
- src/logging/handlers/console.py +7 -50
- src/logging/handlers/file.py +5 -20
- src/logging/handlers/websocket.py +23 -19
- src/logging/logger.py +161 -126
- src/logging/stats/__init__.py +1 -0
- src/logging/stats/llm_stats.py +37 -17
- src/services/__init__.py +17 -1
- src/services/config/__init__.py +1 -0
- src/services/config/knowledge_base_config.py +1 -0
- src/services/config/loader.py +1 -1
- src/services/config/unified_config.py +211 -4
- src/services/embedding/__init__.py +1 -0
- src/services/embedding/adapters/__init__.py +3 -0
- src/services/embedding/adapters/base.py +1 -0
- src/services/embedding/adapters/cohere.py +1 -0
- src/services/embedding/adapters/jina.py +1 -0
- src/services/embedding/adapters/ollama.py +1 -0
- src/services/embedding/adapters/openai_compatible.py +1 -0
- src/services/embedding/adapters/realtimex.py +125 -0
- src/services/embedding/client.py +27 -0
- src/services/embedding/config.py +3 -0
- src/services/embedding/provider.py +1 -0
- src/services/llm/__init__.py +17 -3
- src/services/llm/capabilities.py +47 -0
- src/services/llm/client.py +32 -0
- src/services/llm/cloud_provider.py +21 -4
- src/services/llm/config.py +36 -2
- src/services/llm/error_mapping.py +1 -0
- src/services/llm/exceptions.py +30 -0
- src/services/llm/factory.py +55 -16
- src/services/llm/local_provider.py +1 -0
- src/services/llm/providers/anthropic.py +1 -0
- src/services/llm/providers/base_provider.py +1 -0
- src/services/llm/providers/open_ai.py +1 -0
- src/services/llm/realtimex_provider.py +240 -0
- src/services/llm/registry.py +1 -0
- src/services/llm/telemetry.py +1 -0
- src/services/llm/types.py +1 -0
- src/services/llm/utils.py +1 -0
- src/services/prompt/__init__.py +1 -0
- src/services/prompt/manager.py +3 -2
- src/services/rag/__init__.py +27 -5
- src/services/rag/components/__init__.py +1 -0
- src/services/rag/components/base.py +1 -0
- src/services/rag/components/chunkers/__init__.py +1 -0
- src/services/rag/components/chunkers/base.py +1 -0
- src/services/rag/components/chunkers/fixed.py +1 -0
- src/services/rag/components/chunkers/numbered_item.py +1 -0
- src/services/rag/components/chunkers/semantic.py +1 -0
- src/services/rag/components/embedders/__init__.py +1 -0
- src/services/rag/components/embedders/base.py +1 -0
- src/services/rag/components/embedders/openai.py +1 -0
- src/services/rag/components/indexers/__init__.py +1 -0
- src/services/rag/components/indexers/base.py +1 -0
- src/services/rag/components/indexers/graph.py +5 -44
- src/services/rag/components/indexers/lightrag.py +5 -44
- src/services/rag/components/indexers/vector.py +1 -0
- src/services/rag/components/parsers/__init__.py +1 -0
- src/services/rag/components/parsers/base.py +1 -0
- src/services/rag/components/parsers/markdown.py +1 -0
- src/services/rag/components/parsers/pdf.py +1 -0
- src/services/rag/components/parsers/text.py +1 -0
- src/services/rag/components/retrievers/__init__.py +1 -0
- src/services/rag/components/retrievers/base.py +1 -0
- src/services/rag/components/retrievers/dense.py +1 -0
- src/services/rag/components/retrievers/hybrid.py +5 -44
- src/services/rag/components/retrievers/lightrag.py +5 -44
- src/services/rag/components/routing.py +48 -0
- src/services/rag/factory.py +112 -46
- src/services/rag/pipeline.py +1 -0
- src/services/rag/pipelines/__init__.py +27 -18
- src/services/rag/pipelines/lightrag.py +1 -0
- src/services/rag/pipelines/llamaindex.py +99 -0
- src/services/rag/pipelines/raganything.py +67 -100
- src/services/rag/pipelines/raganything_docling.py +368 -0
- src/services/rag/service.py +5 -12
- src/services/rag/types.py +1 -0
- src/services/rag/utils/__init__.py +17 -0
- src/services/rag/utils/image_migration.py +279 -0
- src/services/search/__init__.py +1 -0
- src/services/search/base.py +1 -0
- src/services/search/consolidation.py +1 -0
- src/services/search/providers/__init__.py +1 -0
- src/services/search/providers/baidu.py +1 -0
- src/services/search/providers/exa.py +1 -0
- src/services/search/providers/jina.py +1 -0
- src/services/search/providers/perplexity.py +1 -0
- src/services/search/providers/serper.py +1 -0
- src/services/search/providers/tavily.py +1 -0
- src/services/search/types.py +1 -0
- src/services/settings/__init__.py +1 -0
- src/services/settings/interface_settings.py +78 -0
- src/services/setup/__init__.py +1 -0
- src/services/tts/__init__.py +1 -0
- src/services/tts/config.py +1 -0
- src/utils/realtimex.py +284 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +0 -2
- src/services/rag/pipelines/academic.py +0 -44
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Semi-automatic sync of prompt structure from prompts/en to prompts/zh|prompts/cn.
|
|
6
|
+
|
|
7
|
+
Behavior (safe by default):
|
|
8
|
+
- Dry-run: prints what would be added
|
|
9
|
+
- With --write: adds missing keys to zh/cn files without overwriting existing values
|
|
10
|
+
- With --create-missing-files: creates missing zh/cn files using the en structure
|
|
11
|
+
|
|
12
|
+
NOTE: This tool does NOT translate. It inserts TODO markers to be manually rewritten in Chinese.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
AGENTS_DIR = PROJECT_ROOT / "src" / "agents"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_yaml(path: Path) -> Any:
|
|
29
|
+
with open(path, encoding="utf-8") as f:
|
|
30
|
+
return yaml.safe_load(f) or {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _dump_yaml(path: Path, obj: Any) -> None:
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
36
|
+
yaml.safe_dump(obj, f, allow_unicode=True, sort_keys=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _merge_missing(en_obj: Any, zh_obj: Any) -> tuple[Any, int]:
|
|
40
|
+
"""
|
|
41
|
+
Add missing keys from en_obj into zh_obj without overwriting existing zh content.
|
|
42
|
+
Returns (new_obj, added_count).
|
|
43
|
+
"""
|
|
44
|
+
added = 0
|
|
45
|
+
|
|
46
|
+
if isinstance(en_obj, dict):
|
|
47
|
+
if not isinstance(zh_obj, dict):
|
|
48
|
+
zh_obj = {}
|
|
49
|
+
for k, v in en_obj.items():
|
|
50
|
+
if k not in zh_obj:
|
|
51
|
+
added += 1
|
|
52
|
+
if isinstance(v, str):
|
|
53
|
+
zh_obj[k] = f"<<TODO_TRANSLATE>> {v}"
|
|
54
|
+
else:
|
|
55
|
+
# For non-string nodes, insert scaffold recursively
|
|
56
|
+
zh_obj[k], inc = _merge_missing(
|
|
57
|
+
v, {} if isinstance(v, dict) else [] if isinstance(v, list) else None
|
|
58
|
+
)
|
|
59
|
+
added += inc
|
|
60
|
+
else:
|
|
61
|
+
zh_obj[k], inc = _merge_missing(v, zh_obj[k])
|
|
62
|
+
added += inc
|
|
63
|
+
return zh_obj, added
|
|
64
|
+
|
|
65
|
+
if isinstance(en_obj, list):
|
|
66
|
+
# Do not attempt to merge list structures; keep existing zh list.
|
|
67
|
+
return zh_obj, 0
|
|
68
|
+
|
|
69
|
+
# Primitive leaf: nothing to do
|
|
70
|
+
return zh_obj, 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main() -> int:
|
|
74
|
+
parser = argparse.ArgumentParser()
|
|
75
|
+
parser.add_argument("--write", action="store_true", help="write changes to disk")
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--create-missing-files",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="create missing zh/cn files from en structure with TODO markers",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--target",
|
|
83
|
+
choices=["zh", "cn", "both"],
|
|
84
|
+
default="both",
|
|
85
|
+
help="which target language directory to sync",
|
|
86
|
+
)
|
|
87
|
+
args = parser.parse_args()
|
|
88
|
+
|
|
89
|
+
if not AGENTS_DIR.exists():
|
|
90
|
+
print(f"Agents directory not found: {AGENTS_DIR}", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
total_added = 0
|
|
94
|
+
total_files = 0
|
|
95
|
+
|
|
96
|
+
for module_dir in sorted([p for p in AGENTS_DIR.iterdir() if p.is_dir()]):
|
|
97
|
+
prompts_dir = module_dir / "prompts"
|
|
98
|
+
en_dir = prompts_dir / "en"
|
|
99
|
+
if not en_dir.exists():
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
targets: list[tuple[str, Path]] = []
|
|
103
|
+
if args.target in ("zh", "both"):
|
|
104
|
+
targets.append(("zh", prompts_dir / "zh"))
|
|
105
|
+
if args.target in ("cn", "both"):
|
|
106
|
+
targets.append(("cn", prompts_dir / "cn"))
|
|
107
|
+
|
|
108
|
+
en_files = [p for p in en_dir.rglob("*.yaml") if p.is_file()]
|
|
109
|
+
for en_file in sorted(en_files):
|
|
110
|
+
rel = en_file.relative_to(en_dir)
|
|
111
|
+
en_obj = _load_yaml(en_file)
|
|
112
|
+
for lang_name, lang_dir in targets:
|
|
113
|
+
zh_file = lang_dir / rel
|
|
114
|
+
if not zh_file.exists():
|
|
115
|
+
if not args.create_missing_files:
|
|
116
|
+
print(f"[MISSING {lang_name}] {module_dir.name}: {rel.as_posix()}")
|
|
117
|
+
continue
|
|
118
|
+
zh_obj = {}
|
|
119
|
+
else:
|
|
120
|
+
zh_obj = _load_yaml(zh_file)
|
|
121
|
+
|
|
122
|
+
new_obj, added = _merge_missing(en_obj, zh_obj)
|
|
123
|
+
if added == 0:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
total_added += added
|
|
127
|
+
total_files += 1
|
|
128
|
+
print(f"[SYNC {lang_name}] {module_dir.name}: {rel.as_posix()} (+{added} keys)")
|
|
129
|
+
|
|
130
|
+
if args.write:
|
|
131
|
+
_dump_yaml(zh_file, new_obj)
|
|
132
|
+
|
|
133
|
+
if total_files == 0:
|
|
134
|
+
print("No changes needed.")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
if args.write:
|
|
138
|
+
print(f"Updated {total_files} file(s), added {total_added} key(s).")
|
|
139
|
+
else:
|
|
140
|
+
print(
|
|
141
|
+
f"Dry-run: would update {total_files} file(s), add {total_added} key(s). Use --write to apply."
|
|
142
|
+
)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
raise SystemExit(main())
|
src/__init__.py
CHANGED
|
@@ -17,7 +17,7 @@ Core Modules:
|
|
|
17
17
|
Usage:
|
|
18
18
|
# Import as a package for integration with other services
|
|
19
19
|
from src.api.main import app
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
# Or use individual modules
|
|
22
22
|
from src.services.rag import RAGService
|
|
23
23
|
from src.agents import ChatAgent
|
|
@@ -30,7 +30,7 @@ __package_name__ = "realtimex-deeptutor"
|
|
|
30
30
|
# These are imported lazily to avoid circular imports and heavy startup
|
|
31
31
|
__all__ = [
|
|
32
32
|
"agents",
|
|
33
|
-
"api",
|
|
33
|
+
"api",
|
|
34
34
|
"services",
|
|
35
35
|
"knowledge",
|
|
36
36
|
"logging",
|
|
@@ -25,6 +25,7 @@ class MaterialOrganizerAgent(BaseAgent):
|
|
|
25
25
|
language: str = "en",
|
|
26
26
|
api_key: str | None = None,
|
|
27
27
|
base_url: str | None = None,
|
|
28
|
+
api_version: str | None = None,
|
|
28
29
|
model: str | None = None,
|
|
29
30
|
):
|
|
30
31
|
super().__init__(
|
|
@@ -32,6 +33,7 @@ class MaterialOrganizerAgent(BaseAgent):
|
|
|
32
33
|
agent_name="material_organizer",
|
|
33
34
|
api_key=api_key,
|
|
34
35
|
base_url=base_url,
|
|
36
|
+
api_version=api_version,
|
|
35
37
|
model=model,
|
|
36
38
|
language=language,
|
|
37
39
|
)
|
src/agents/solve/__init__.py
CHANGED
|
@@ -42,6 +42,9 @@ from .memory import (
|
|
|
42
42
|
ToolCallRecord,
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
# Session management
|
|
46
|
+
from .session_manager import SolverSessionManager, get_solver_session_manager
|
|
47
|
+
|
|
45
48
|
# Solve loop
|
|
46
49
|
from .solve_loop import (
|
|
47
50
|
ManagerAgent,
|
|
@@ -77,4 +80,7 @@ __all__ = [
|
|
|
77
80
|
"ToolAgent",
|
|
78
81
|
# Main Controller
|
|
79
82
|
"MainSolver",
|
|
83
|
+
# Session Management
|
|
84
|
+
"SolverSessionManager",
|
|
85
|
+
"get_solver_session_manager",
|
|
80
86
|
]
|
src/agents/solve/main_solver.py
CHANGED
|
@@ -43,6 +43,7 @@ class MainSolver:
|
|
|
43
43
|
api_key: str | None = None,
|
|
44
44
|
base_url: str | None = None,
|
|
45
45
|
api_version: str | None = None,
|
|
46
|
+
language: str | None = None,
|
|
46
47
|
kb_name: str = "ai_textbook",
|
|
47
48
|
output_base_dir: str | None = None,
|
|
48
49
|
):
|
|
@@ -55,6 +56,7 @@ class MainSolver:
|
|
|
55
56
|
api_key: API key (if not provided, read from environment)
|
|
56
57
|
base_url: API URL (if not provided, read from environment)
|
|
57
58
|
api_version: API version (if not provided, read from environment)
|
|
59
|
+
language: Preferred language for prompts ("en"/"zh"/"cn")
|
|
58
60
|
kb_name: Knowledge base name
|
|
59
61
|
output_base_dir: Output base directory (optional, overrides config)
|
|
60
62
|
"""
|
|
@@ -63,6 +65,7 @@ class MainSolver:
|
|
|
63
65
|
self._api_key = api_key
|
|
64
66
|
self._base_url = base_url
|
|
65
67
|
self._api_version = api_version
|
|
68
|
+
self._language = language
|
|
66
69
|
self._kb_name = kb_name
|
|
67
70
|
self._output_base_dir = output_base_dir
|
|
68
71
|
|
|
@@ -108,6 +111,7 @@ class MainSolver:
|
|
|
108
111
|
api_version = self._api_version
|
|
109
112
|
kb_name = self._kb_name
|
|
110
113
|
output_base_dir = self._output_base_dir
|
|
114
|
+
language = self._language
|
|
111
115
|
|
|
112
116
|
# Load config from config directory (main.yaml unified config)
|
|
113
117
|
if config_path is None:
|
|
@@ -156,6 +160,11 @@ class MainSolver:
|
|
|
156
160
|
if self.config is None or not isinstance(self.config, dict):
|
|
157
161
|
self.config = {}
|
|
158
162
|
|
|
163
|
+
# Override system language from UI if provided
|
|
164
|
+
if language:
|
|
165
|
+
self.config.setdefault("system", {})
|
|
166
|
+
self.config["system"]["language"] = parse_language(language)
|
|
167
|
+
|
|
159
168
|
# Override output directory config
|
|
160
169
|
if output_base_dir:
|
|
161
170
|
if "system" not in self.config:
|
|
@@ -11,18 +11,19 @@ system: |
|
|
|
11
11
|
4. **聚焦核心**:只关注解题必须的"硬知识"(定义、公式、定理)。除非用户明确要求,否则不要查询应用案例或背景故事。
|
|
12
12
|
|
|
13
13
|
# 工具使用指南
|
|
14
|
-
- `
|
|
14
|
+
- `query_item`: **编号条目优先**。当用户问题中**明确提到具体编号条目**(例如“公式 2.1.2 / Equation (2.1.2)”、“定理 3.1 / Theorem 3.1”、“图 1.2 / Figure 1.2”)时,**必须优先使用该工具(ALWAYS use this first)**。请从问题中提取标识符并直接查询(常见格式:方程用 `(X.X.X)`,图用 `Figure X.X`,定理用 `Theorem X.X`)。这是检索编号内容最直接、最准确的方式。
|
|
15
|
+
- `rag_naive`: **通用查询首选**。用于查询明确的术语定义、核心公式,或验证某个概念是否存在。速度快。
|
|
15
16
|
- `rag_hybrid`: 用于需要综合多个概念、探究复杂实体关系或深层原理的场景。
|
|
16
17
|
- `web_search`: **慎用**。仅当信息极有可能超出教材范围(如最新新闻、特定技术参数、开源库用法)时使用。
|
|
17
|
-
- `query_item`: **专用**。仅当你明确知道需要获取某个特定编号(如 "Fig 1.2", "Theorem 3.1")的内容时使用。
|
|
18
18
|
- `none`: 当现有信息足以支撑解题,或缺口无法通过检索填补(如需要用户提供)时,返回此状态。
|
|
19
19
|
|
|
20
20
|
# 思考路径
|
|
21
|
-
1.
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
24
|
-
4.
|
|
25
|
-
5.
|
|
21
|
+
1. **识别编号条目**:**首先扫描用户问题中是否存在明确的编号引用**(例如“公式 2.1.2 / Equation (2.1.2)”、“定理 3.1 / Theorem 3.1”、“图 1.2 / Figure 1.2”)。若存在,提取标识符并立即使用 `query_item`。编号条目优先于语义检索。
|
|
22
|
+
2. **解析需求**:解题真正缺的是什么?是某个变量的定义?是某个定理的公式?还是某个常数的数值?
|
|
23
|
+
3. **核查存量**:查看`已有查询内容`。答案是否已经存在?或者是否隐含在已知信息中?
|
|
24
|
+
4. **构建查询**:如果确有缺口,构建原子化的、互不重叠的查询请求。
|
|
25
|
+
5. **判断终止**:如果核心定义和公式都已齐备,果断停止。
|
|
26
|
+
6. **⚠️ 关键 - 及时止损(Know When to Cut Losses)**:如果某话题的第一轮查询没有返回有用信息(结果为空、不相关或质量低),通常意味着知识库不包含相关内容。**你必须立即放弃该话题**,改查其他话题,或直接进入下一阶段。**不要对同一个话题反复查询**——这会浪费资源并拖慢解题。
|
|
26
27
|
|
|
27
28
|
# 输出格式
|
|
28
29
|
直接输出 JSON 对象(无 Markdown 格式):
|
|
@@ -46,6 +47,7 @@ user_template: |
|
|
|
46
47
|
|
|
47
48
|
## 任务
|
|
48
49
|
分析是否存在阻碍解题的知识缺口。
|
|
50
|
+
- **编号条目优先**:若问题提到具体编号条目(公式/定理/图/方程编号如“2.1.2”“3.1”等),**立即使用 `query_item`** 并传入提取的标识符(例如方程用 `(2.1.2)`,图用 `Figure 2.1`)。
|
|
49
51
|
- 检查重复:新查询不得与已有内容重叠。
|
|
50
52
|
- 灵活决策:如果前序工具查询出错,考虑通过其他工具来达到相同的目的。
|
|
51
53
|
- 及时止损:如果某话题的第一轮查询没有返回有用信息,不要反复查询该话题,应该换其他话题或进入下一阶段。
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
SolverSessionManager - Solver session persistence and management.
|
|
4
|
+
|
|
5
|
+
This module handles:
|
|
6
|
+
- Creating new solver sessions
|
|
7
|
+
- Updating sessions with new messages
|
|
8
|
+
- Retrieving session history
|
|
9
|
+
- Listing recent sessions
|
|
10
|
+
- Deleting sessions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SolverSessionManager:
|
|
21
|
+
"""
|
|
22
|
+
Manages persistent storage of solver sessions.
|
|
23
|
+
|
|
24
|
+
Sessions are stored in a JSON file at data/user/solver_sessions.json.
|
|
25
|
+
Each session contains:
|
|
26
|
+
- session_id: Unique identifier
|
|
27
|
+
- title: Session title (usually first user question)
|
|
28
|
+
- messages: List of messages with role, content, outputDir, timestamp
|
|
29
|
+
- kb_name: Knowledge base used
|
|
30
|
+
- token_stats: Cost and token usage statistics
|
|
31
|
+
- created_at: Creation timestamp
|
|
32
|
+
- updated_at: Last update timestamp
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, base_dir: str | None = None):
|
|
36
|
+
"""
|
|
37
|
+
Initialize SolverSessionManager.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_dir: Base directory for session storage.
|
|
41
|
+
Defaults to project_root/data/user
|
|
42
|
+
"""
|
|
43
|
+
if base_dir is None:
|
|
44
|
+
# Current file: src/agents/solve/session_manager.py
|
|
45
|
+
# Project root: 4 levels up
|
|
46
|
+
project_root = Path(__file__).resolve().parents[3]
|
|
47
|
+
base_dir_path = project_root / "data" / "user"
|
|
48
|
+
else:
|
|
49
|
+
base_dir_path = Path(base_dir)
|
|
50
|
+
|
|
51
|
+
self.base_dir = base_dir_path
|
|
52
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
self.sessions_file = self.base_dir / "solver_sessions.json"
|
|
55
|
+
self._ensure_file()
|
|
56
|
+
|
|
57
|
+
def _ensure_file(self):
|
|
58
|
+
"""Ensure the sessions file exists with correct format."""
|
|
59
|
+
if not self.sessions_file.exists():
|
|
60
|
+
initial_data = {
|
|
61
|
+
"version": "1.0",
|
|
62
|
+
"sessions": [],
|
|
63
|
+
}
|
|
64
|
+
self._save_data(initial_data)
|
|
65
|
+
|
|
66
|
+
def _load_data(self) -> dict[str, Any]:
|
|
67
|
+
"""Load sessions data from file."""
|
|
68
|
+
try:
|
|
69
|
+
with open(self.sessions_file, encoding="utf-8") as f:
|
|
70
|
+
return json.load(f)
|
|
71
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
72
|
+
return {"version": "1.0", "sessions": []}
|
|
73
|
+
|
|
74
|
+
def _save_data(self, data: dict[str, Any]):
|
|
75
|
+
"""Save sessions data to file."""
|
|
76
|
+
with open(self.sessions_file, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
78
|
+
|
|
79
|
+
def _get_sessions(self) -> list[dict[str, Any]]:
|
|
80
|
+
"""Get list of all sessions."""
|
|
81
|
+
data = self._load_data()
|
|
82
|
+
return data.get("sessions", [])
|
|
83
|
+
|
|
84
|
+
def _save_sessions(self, sessions: list[dict[str, Any]]):
|
|
85
|
+
"""Save sessions list."""
|
|
86
|
+
data = self._load_data()
|
|
87
|
+
data["sessions"] = sessions
|
|
88
|
+
self._save_data(data)
|
|
89
|
+
|
|
90
|
+
def create_session(
|
|
91
|
+
self,
|
|
92
|
+
title: str = "New Solver Session",
|
|
93
|
+
kb_name: str = "",
|
|
94
|
+
token_stats: dict[str, Any] | None = None,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
"""
|
|
97
|
+
Create a new solver session.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
title: Session title
|
|
101
|
+
kb_name: Knowledge base name
|
|
102
|
+
token_stats: Optional token usage statistics
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
New session dict with session_id
|
|
106
|
+
"""
|
|
107
|
+
session_id = f"solver_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|
108
|
+
now = time.time()
|
|
109
|
+
|
|
110
|
+
session = {
|
|
111
|
+
"session_id": session_id,
|
|
112
|
+
"title": title[:100], # Limit title length
|
|
113
|
+
"messages": [],
|
|
114
|
+
"kb_name": kb_name,
|
|
115
|
+
"token_stats": token_stats
|
|
116
|
+
or {
|
|
117
|
+
"model": "Unknown",
|
|
118
|
+
"calls": 0,
|
|
119
|
+
"tokens": 0,
|
|
120
|
+
"input_tokens": 0,
|
|
121
|
+
"output_tokens": 0,
|
|
122
|
+
"cost": 0.0,
|
|
123
|
+
},
|
|
124
|
+
"created_at": now,
|
|
125
|
+
"updated_at": now,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sessions = self._get_sessions()
|
|
129
|
+
sessions.insert(0, session) # Add to front (newest first)
|
|
130
|
+
|
|
131
|
+
# Limit total sessions to prevent file bloat
|
|
132
|
+
max_sessions = 100
|
|
133
|
+
if len(sessions) > max_sessions:
|
|
134
|
+
sessions = sessions[:max_sessions]
|
|
135
|
+
|
|
136
|
+
self._save_sessions(sessions)
|
|
137
|
+
|
|
138
|
+
return session
|
|
139
|
+
|
|
140
|
+
def get_session(self, session_id: str) -> dict[str, Any] | None:
|
|
141
|
+
"""
|
|
142
|
+
Get a session by ID.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
session_id: Session identifier
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Session dict or None if not found
|
|
149
|
+
"""
|
|
150
|
+
sessions = self._get_sessions()
|
|
151
|
+
for session in sessions:
|
|
152
|
+
if session.get("session_id") == session_id:
|
|
153
|
+
return session
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def update_session(
|
|
157
|
+
self,
|
|
158
|
+
session_id: str,
|
|
159
|
+
messages: list[dict[str, Any]] | None = None,
|
|
160
|
+
title: str | None = None,
|
|
161
|
+
kb_name: str | None = None,
|
|
162
|
+
token_stats: dict[str, Any] | None = None,
|
|
163
|
+
) -> dict[str, Any] | None:
|
|
164
|
+
"""
|
|
165
|
+
Update a session with new data.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
session_id: Session identifier
|
|
169
|
+
messages: New messages list (replaces existing)
|
|
170
|
+
title: New title (optional)
|
|
171
|
+
kb_name: New knowledge base name (optional)
|
|
172
|
+
token_stats: New token stats (optional)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Updated session or None if not found
|
|
176
|
+
"""
|
|
177
|
+
sessions = self._get_sessions()
|
|
178
|
+
|
|
179
|
+
for i, session in enumerate(sessions):
|
|
180
|
+
if session.get("session_id") == session_id:
|
|
181
|
+
if messages is not None:
|
|
182
|
+
session["messages"] = messages
|
|
183
|
+
if title is not None:
|
|
184
|
+
session["title"] = title[:100]
|
|
185
|
+
if kb_name is not None:
|
|
186
|
+
session["kb_name"] = kb_name
|
|
187
|
+
if token_stats is not None:
|
|
188
|
+
session["token_stats"] = token_stats
|
|
189
|
+
|
|
190
|
+
session["updated_at"] = time.time()
|
|
191
|
+
|
|
192
|
+
# Move to front (most recently updated)
|
|
193
|
+
sessions.pop(i)
|
|
194
|
+
sessions.insert(0, session)
|
|
195
|
+
|
|
196
|
+
self._save_sessions(sessions)
|
|
197
|
+
return session
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def add_message(
|
|
202
|
+
self,
|
|
203
|
+
session_id: str,
|
|
204
|
+
role: str,
|
|
205
|
+
content: str,
|
|
206
|
+
output_dir: str | None = None,
|
|
207
|
+
) -> dict[str, Any] | None:
|
|
208
|
+
"""
|
|
209
|
+
Add a single message to a session.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
session_id: Session identifier
|
|
213
|
+
role: Message role ('user' or 'assistant')
|
|
214
|
+
content: Message content
|
|
215
|
+
output_dir: Optional output directory (for assistant messages)
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Updated session or None if not found
|
|
219
|
+
"""
|
|
220
|
+
session = self.get_session(session_id)
|
|
221
|
+
if not session:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
message = {
|
|
225
|
+
"role": role,
|
|
226
|
+
"content": content,
|
|
227
|
+
"timestamp": time.time(),
|
|
228
|
+
}
|
|
229
|
+
if output_dir:
|
|
230
|
+
message["output_dir"] = output_dir
|
|
231
|
+
|
|
232
|
+
messages = session.get("messages", [])
|
|
233
|
+
messages.append(message)
|
|
234
|
+
|
|
235
|
+
# Update title from first user message if still default
|
|
236
|
+
if session.get("title") == "New Solver Session" and role == "user":
|
|
237
|
+
new_title = content[:50] + ("..." if len(content) > 50 else "")
|
|
238
|
+
return self.update_session(session_id, messages=messages, title=new_title)
|
|
239
|
+
|
|
240
|
+
return self.update_session(session_id, messages=messages)
|
|
241
|
+
|
|
242
|
+
def update_token_stats(
|
|
243
|
+
self,
|
|
244
|
+
session_id: str,
|
|
245
|
+
token_stats: dict[str, Any],
|
|
246
|
+
) -> dict[str, Any] | None:
|
|
247
|
+
"""
|
|
248
|
+
Update token stats for a session.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
session_id: Session identifier
|
|
252
|
+
token_stats: Token usage statistics
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Updated session or None if not found
|
|
256
|
+
"""
|
|
257
|
+
return self.update_session(session_id, token_stats=token_stats)
|
|
258
|
+
|
|
259
|
+
def list_sessions(
|
|
260
|
+
self,
|
|
261
|
+
limit: int = 20,
|
|
262
|
+
include_messages: bool = False,
|
|
263
|
+
) -> list[dict[str, Any]]:
|
|
264
|
+
"""
|
|
265
|
+
List recent sessions.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
limit: Maximum number of sessions to return
|
|
269
|
+
include_messages: Whether to include full message history
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of session dicts (newest first)
|
|
273
|
+
"""
|
|
274
|
+
sessions = self._get_sessions()[:limit]
|
|
275
|
+
|
|
276
|
+
if not include_messages:
|
|
277
|
+
# Return summary only (without full messages)
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
"session_id": s.get("session_id"),
|
|
281
|
+
"title": s.get("title"),
|
|
282
|
+
"message_count": len(s.get("messages", [])),
|
|
283
|
+
"kb_name": s.get("kb_name"),
|
|
284
|
+
"token_stats": s.get("token_stats"),
|
|
285
|
+
"created_at": s.get("created_at"),
|
|
286
|
+
"updated_at": s.get("updated_at"),
|
|
287
|
+
# Include preview of last message
|
|
288
|
+
"last_message": (
|
|
289
|
+
s.get("messages", [])[-1].get("content", "")[:100]
|
|
290
|
+
if s.get("messages")
|
|
291
|
+
else ""
|
|
292
|
+
),
|
|
293
|
+
}
|
|
294
|
+
for s in sessions
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
return sessions
|
|
298
|
+
|
|
299
|
+
def delete_session(self, session_id: str) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
Delete a session.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
session_id: Session identifier
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if deleted, False if not found
|
|
308
|
+
"""
|
|
309
|
+
sessions = self._get_sessions()
|
|
310
|
+
original_count = len(sessions)
|
|
311
|
+
|
|
312
|
+
sessions = [s for s in sessions if s.get("session_id") != session_id]
|
|
313
|
+
|
|
314
|
+
if len(sessions) < original_count:
|
|
315
|
+
self._save_sessions(sessions)
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def clear_all_sessions(self) -> int:
|
|
321
|
+
"""
|
|
322
|
+
Delete all sessions.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Number of sessions deleted
|
|
326
|
+
"""
|
|
327
|
+
sessions = self._get_sessions()
|
|
328
|
+
count = len(sessions)
|
|
329
|
+
self._save_sessions([])
|
|
330
|
+
return count
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# Singleton instance for convenience
|
|
334
|
+
_solver_session_manager: SolverSessionManager | None = None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_solver_session_manager() -> SolverSessionManager:
|
|
338
|
+
"""Get or create the global SolverSessionManager instance."""
|
|
339
|
+
global _solver_session_manager
|
|
340
|
+
if _solver_session_manager is None:
|
|
341
|
+
_solver_session_manager = SolverSessionManager()
|
|
342
|
+
return _solver_session_manager
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
__all__ = ["SolverSessionManager", "get_solver_session_manager"]
|
src/api/main.py
CHANGED
|
@@ -16,6 +16,7 @@ from src.api.routers import (
|
|
|
16
16
|
knowledge,
|
|
17
17
|
notebook,
|
|
18
18
|
question,
|
|
19
|
+
realtimex,
|
|
19
20
|
research,
|
|
20
21
|
settings,
|
|
21
22
|
solve,
|
|
@@ -23,6 +24,7 @@ from src.api.routers import (
|
|
|
23
24
|
)
|
|
24
25
|
from src.logging import get_logger
|
|
25
26
|
|
|
27
|
+
# Note: Don't set service_prefix here - start_web.py already adds [Backend] prefix
|
|
26
28
|
logger = get_logger("API")
|
|
27
29
|
|
|
28
30
|
CONFIG_DRIFT_ERROR_TEMPLATE = (
|
|
@@ -130,6 +132,17 @@ async def lifespan(app: FastAPI):
|
|
|
130
132
|
# Validate configuration consistency
|
|
131
133
|
validate_tool_consistency()
|
|
132
134
|
|
|
135
|
+
# Initialize LLM client early to set environment variables for LightRAG
|
|
136
|
+
# LightRAG reads OPENAI_API_KEY from os.environ internally, so we must
|
|
137
|
+
# set it before any RAG operations can happen
|
|
138
|
+
try:
|
|
139
|
+
from src.services.llm import get_llm_client
|
|
140
|
+
|
|
141
|
+
llm_client = get_llm_client()
|
|
142
|
+
logger.info(f"LLM client initialized: model={llm_client.config.model}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning(f"Failed to initialize LLM client at startup: {e}")
|
|
145
|
+
|
|
133
146
|
yield
|
|
134
147
|
# Execute on shutdown
|
|
135
148
|
logger.info("Application shutdown")
|
|
@@ -189,6 +202,7 @@ app.include_router(settings.router, prefix="/api/v1/settings", tags=["settings"]
|
|
|
189
202
|
app.include_router(system.router, prefix="/api/v1/system", tags=["system"])
|
|
190
203
|
app.include_router(config.router, prefix="/api/v1/config", tags=["config"])
|
|
191
204
|
app.include_router(agent_config.router, prefix="/api/v1/agent-config", tags=["agent-config"])
|
|
205
|
+
app.include_router(realtimex.router, prefix="/api/v1", tags=["realtimex"])
|
|
192
206
|
|
|
193
207
|
|
|
194
208
|
@app.get("/")
|
src/api/routers/chat.py
CHANGED
|
@@ -18,6 +18,7 @@ from src.agents.chat import ChatAgent, SessionManager
|
|
|
18
18
|
from src.logging import get_logger
|
|
19
19
|
from src.services.config import load_config_with_main
|
|
20
20
|
from src.services.llm.config import get_llm_config
|
|
21
|
+
from src.services.settings.interface_settings import get_ui_language
|
|
21
22
|
|
|
22
23
|
# Initialize logger
|
|
23
24
|
project_root = Path(__file__).parent.parent.parent.parent
|
|
@@ -113,13 +114,12 @@ async def websocket_chat(websocket: WebSocket):
|
|
|
113
114
|
"""
|
|
114
115
|
await websocket.accept()
|
|
115
116
|
|
|
116
|
-
# Get system language for agent
|
|
117
|
-
language = config.get("system", {}).get("language", "en")
|
|
118
|
-
|
|
119
117
|
try:
|
|
120
118
|
while True:
|
|
121
119
|
# Receive message
|
|
122
120
|
data = await websocket.receive_json()
|
|
121
|
+
# Use current UI language (fallback to config/main.yaml system.language)
|
|
122
|
+
language = get_ui_language(default=config.get("system", {}).get("language", "en"))
|
|
123
123
|
message = data.get("message", "").strip()
|
|
124
124
|
session_id = data.get("session_id")
|
|
125
125
|
explicit_history = data.get("history") # Optional override
|