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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, WebSocket
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from src.agents.research.agents import RephraseAgent
|
|
12
|
+
from src.agents.research.research_pipeline import ResearchPipeline
|
|
13
|
+
from src.api.utils.history import ActivityType, history_manager
|
|
14
|
+
from src.api.utils.task_id_manager import TaskIDManager
|
|
15
|
+
from src.logging import get_logger
|
|
16
|
+
from src.services.config import load_config_with_main
|
|
17
|
+
from src.services.llm import get_llm_config
|
|
18
|
+
|
|
19
|
+
# Force stdout to use utf-8 to prevent encoding errors with emojis on Windows
|
|
20
|
+
if sys.platform == "win32":
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Helper to load config (with main.yaml merge)
|
|
27
|
+
def load_config():
|
|
28
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
29
|
+
return load_config_with_main("research_config.yaml", project_root)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Initialize logger with config
|
|
33
|
+
config = load_config()
|
|
34
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
|
|
35
|
+
logger = get_logger("ResearchAPI", log_dir=log_dir)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OptimizeRequest(BaseModel):
|
|
39
|
+
topic: str
|
|
40
|
+
iteration: int = 0
|
|
41
|
+
previous_result: dict[str, Any] | None = None
|
|
42
|
+
kb_name: str | None = "ai_textbook"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.post("/optimize_topic")
|
|
46
|
+
async def optimize_topic(request: OptimizeRequest):
|
|
47
|
+
try:
|
|
48
|
+
config = load_config()
|
|
49
|
+
|
|
50
|
+
# Inject API keys
|
|
51
|
+
try:
|
|
52
|
+
llm_config = get_llm_config()
|
|
53
|
+
api_key = llm_config.api_key
|
|
54
|
+
base_url = llm_config.base_url
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return {"error": f"LLM config error: {e!s}"}
|
|
57
|
+
|
|
58
|
+
# Init Agent
|
|
59
|
+
agent = RephraseAgent(config=config, api_key=api_key, base_url=base_url)
|
|
60
|
+
|
|
61
|
+
# Process
|
|
62
|
+
# If iteration > 0, topic is treated as feedback
|
|
63
|
+
if request.iteration == 0:
|
|
64
|
+
result = await agent.process(request.topic, iteration=0)
|
|
65
|
+
else:
|
|
66
|
+
result = await agent.process(
|
|
67
|
+
request.topic, iteration=request.iteration, previous_result=request.previous_result
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
traceback.print_exc()
|
|
74
|
+
return {"error": str(e)}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.websocket("/run")
|
|
78
|
+
async def websocket_research_run(websocket: WebSocket):
|
|
79
|
+
await websocket.accept()
|
|
80
|
+
|
|
81
|
+
# Get task ID manager
|
|
82
|
+
task_manager = TaskIDManager.get_instance()
|
|
83
|
+
|
|
84
|
+
pusher_task = None
|
|
85
|
+
progress_pusher_task = None
|
|
86
|
+
original_stdout = sys.stdout # Save original stdout at the start
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# 1. Wait for config
|
|
90
|
+
data = await websocket.receive_json()
|
|
91
|
+
topic = data.get("topic")
|
|
92
|
+
kb_name = data.get("kb_name", "ai_textbook")
|
|
93
|
+
# New unified parameters
|
|
94
|
+
plan_mode = data.get("plan_mode", "medium") # quick, medium, deep, auto
|
|
95
|
+
enabled_tools = data.get("enabled_tools", ["RAG"]) # RAG, Paper, Web
|
|
96
|
+
skip_rephrase = data.get("skip_rephrase", False)
|
|
97
|
+
# Legacy support
|
|
98
|
+
preset = data.get("preset") # For backward compatibility
|
|
99
|
+
research_mode = data.get("research_mode")
|
|
100
|
+
|
|
101
|
+
if not topic:
|
|
102
|
+
await websocket.send_json({"type": "error", "content": "Topic is required"})
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Generate task ID
|
|
106
|
+
task_key = f"research_{kb_name}_{hash(str(topic))}"
|
|
107
|
+
task_id = task_manager.generate_task_id("research", task_key)
|
|
108
|
+
|
|
109
|
+
# Send task ID to frontend
|
|
110
|
+
await websocket.send_json({"type": "task_id", "task_id": task_id})
|
|
111
|
+
|
|
112
|
+
# Use unified logger
|
|
113
|
+
config = load_config()
|
|
114
|
+
try:
|
|
115
|
+
# Get log_dir from config
|
|
116
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get(
|
|
117
|
+
"log_dir"
|
|
118
|
+
)
|
|
119
|
+
research_logger = get_logger("Research", log_dir=log_dir)
|
|
120
|
+
research_logger.info(f"[{task_id}] Starting research flow: {topic[:50]}...")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Failed to initialize research logger: {e}")
|
|
123
|
+
|
|
124
|
+
# 2. Initialize Pipeline
|
|
125
|
+
# Initialize nested config structures from research.* (main.yaml structure)
|
|
126
|
+
# This ensures all research module configs are properly inherited from main.yaml
|
|
127
|
+
research_config = config.get("research", {})
|
|
128
|
+
|
|
129
|
+
# Initialize planning config from research.planning
|
|
130
|
+
if "planning" not in config:
|
|
131
|
+
config["planning"] = research_config.get("planning", {}).copy()
|
|
132
|
+
else:
|
|
133
|
+
# Merge with research.planning defaults
|
|
134
|
+
default_planning = research_config.get("planning", {})
|
|
135
|
+
for key, value in default_planning.items():
|
|
136
|
+
if key not in config["planning"]:
|
|
137
|
+
config["planning"][key] = value if not isinstance(value, dict) else value.copy()
|
|
138
|
+
elif isinstance(value, dict) and isinstance(config["planning"][key], dict):
|
|
139
|
+
# Deep merge for nested dicts like decompose, rephrase
|
|
140
|
+
for k, v in value.items():
|
|
141
|
+
if k not in config["planning"][key]:
|
|
142
|
+
config["planning"][key][k] = v
|
|
143
|
+
|
|
144
|
+
# Ensure decompose and rephrase exist
|
|
145
|
+
if "decompose" not in config["planning"]:
|
|
146
|
+
config["planning"]["decompose"] = {}
|
|
147
|
+
if "rephrase" not in config["planning"]:
|
|
148
|
+
config["planning"]["rephrase"] = {}
|
|
149
|
+
|
|
150
|
+
# Initialize researching config from research.researching
|
|
151
|
+
# This ensures execution_mode, max_parallel_topics etc. are properly inherited
|
|
152
|
+
if "researching" not in config:
|
|
153
|
+
config["researching"] = research_config.get("researching", {}).copy()
|
|
154
|
+
else:
|
|
155
|
+
# Merge with research.researching defaults (research.researching has lower priority)
|
|
156
|
+
default_researching = research_config.get("researching", {})
|
|
157
|
+
for key, value in default_researching.items():
|
|
158
|
+
if key not in config["researching"]:
|
|
159
|
+
config["researching"][key] = value
|
|
160
|
+
|
|
161
|
+
# Initialize reporting config from research.reporting
|
|
162
|
+
# This ensures enable_citation_list, enable_inline_citations etc. are properly inherited
|
|
163
|
+
if "reporting" not in config:
|
|
164
|
+
config["reporting"] = research_config.get("reporting", {}).copy()
|
|
165
|
+
else:
|
|
166
|
+
# Merge with research.reporting defaults
|
|
167
|
+
default_reporting = research_config.get("reporting", {})
|
|
168
|
+
for key, value in default_reporting.items():
|
|
169
|
+
if key not in config["reporting"]:
|
|
170
|
+
config["reporting"][key] = value
|
|
171
|
+
|
|
172
|
+
# Apply plan_mode configuration (unified approach affecting both planning and researching)
|
|
173
|
+
# Each mode defines:
|
|
174
|
+
# - Planning: tree depth (subtopics count) and mode (manual/auto)
|
|
175
|
+
# - Researching: max iterations per topic and iteration_mode (fixed/flexible)
|
|
176
|
+
plan_mode_config = {
|
|
177
|
+
"quick": {
|
|
178
|
+
"planning": {"decompose": {"initial_subtopics": 2, "mode": "manual"}},
|
|
179
|
+
"researching": {"max_iterations": 2, "iteration_mode": "fixed"},
|
|
180
|
+
},
|
|
181
|
+
"medium": {
|
|
182
|
+
"planning": {"decompose": {"initial_subtopics": 5, "mode": "manual"}},
|
|
183
|
+
"researching": {"max_iterations": 4, "iteration_mode": "fixed"},
|
|
184
|
+
},
|
|
185
|
+
"deep": {
|
|
186
|
+
"planning": {"decompose": {"initial_subtopics": 8, "mode": "manual"}},
|
|
187
|
+
"researching": {"max_iterations": 7, "iteration_mode": "fixed"},
|
|
188
|
+
},
|
|
189
|
+
"auto": {
|
|
190
|
+
"planning": {"decompose": {"mode": "auto", "auto_max_subtopics": 8}},
|
|
191
|
+
"researching": {"max_iterations": 6, "iteration_mode": "flexible"},
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
if plan_mode in plan_mode_config:
|
|
195
|
+
mode_cfg = plan_mode_config[plan_mode]
|
|
196
|
+
# Apply planning configuration
|
|
197
|
+
if "planning" in mode_cfg:
|
|
198
|
+
for key, value in mode_cfg["planning"].items():
|
|
199
|
+
if key not in config["planning"]:
|
|
200
|
+
config["planning"][key] = {}
|
|
201
|
+
config["planning"][key].update(value)
|
|
202
|
+
# Apply researching configuration
|
|
203
|
+
if "researching" in mode_cfg:
|
|
204
|
+
config["researching"].update(mode_cfg["researching"])
|
|
205
|
+
|
|
206
|
+
# Legacy preset support (for backward compatibility)
|
|
207
|
+
if preset and "presets" in config and preset in config["presets"]:
|
|
208
|
+
preset_config = config["presets"][preset]
|
|
209
|
+
for key, value in preset_config.items():
|
|
210
|
+
if key in config and isinstance(value, dict):
|
|
211
|
+
config[key].update(value)
|
|
212
|
+
|
|
213
|
+
# Apply enabled_tools configuration
|
|
214
|
+
# RAG includes: rag_naive, rag_hybrid, query_item
|
|
215
|
+
# Paper includes: paper_search
|
|
216
|
+
# Web includes: web_search
|
|
217
|
+
# run_code is always enabled
|
|
218
|
+
config["researching"]["enable_rag_naive"] = "RAG" in enabled_tools
|
|
219
|
+
config["researching"]["enable_rag_hybrid"] = "RAG" in enabled_tools
|
|
220
|
+
config["researching"]["enable_query_item"] = "RAG" in enabled_tools
|
|
221
|
+
config["researching"]["enable_paper_search"] = "Paper" in enabled_tools
|
|
222
|
+
config["researching"]["enable_web_search"] = "Web" in enabled_tools
|
|
223
|
+
config["researching"]["enable_run_code"] = True # Always enabled
|
|
224
|
+
|
|
225
|
+
# Store enabled_tools for prompt generation
|
|
226
|
+
config["researching"]["enabled_tools"] = enabled_tools
|
|
227
|
+
|
|
228
|
+
# Legacy research_mode support
|
|
229
|
+
if research_mode:
|
|
230
|
+
config["researching"]["research_mode"] = research_mode
|
|
231
|
+
|
|
232
|
+
# If skip_rephrase is True, disable the internal rephrase step
|
|
233
|
+
if skip_rephrase:
|
|
234
|
+
config["planning"]["rephrase"]["enabled"] = False
|
|
235
|
+
|
|
236
|
+
# Define unified output directory
|
|
237
|
+
# Use project root directory user/research as unified output directory
|
|
238
|
+
root_dir = Path(__file__).parent.parent.parent.parent
|
|
239
|
+
output_base = root_dir / "data" / "user" / "research"
|
|
240
|
+
|
|
241
|
+
# Update config with unified output paths
|
|
242
|
+
if "system" not in config:
|
|
243
|
+
config["system"] = {}
|
|
244
|
+
|
|
245
|
+
config["system"]["output_base_dir"] = str(output_base / "cache")
|
|
246
|
+
config["system"]["reports_dir"] = str(output_base / "reports")
|
|
247
|
+
|
|
248
|
+
# Inject API keys from env if not in config
|
|
249
|
+
try:
|
|
250
|
+
llm_config = get_llm_config()
|
|
251
|
+
api_key = llm_config.api_key
|
|
252
|
+
base_url = llm_config.base_url
|
|
253
|
+
api_version = getattr(llm_config, "api_version", None)
|
|
254
|
+
except ValueError as e:
|
|
255
|
+
await websocket.send_json({"error": f"LLM configuration error: {e!s}"})
|
|
256
|
+
await websocket.close()
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# 3. Setup Queues for log and progress
|
|
260
|
+
log_queue = asyncio.Queue()
|
|
261
|
+
progress_queue = asyncio.Queue()
|
|
262
|
+
|
|
263
|
+
# Progress callback function
|
|
264
|
+
def progress_callback(event: dict[str, Any]):
|
|
265
|
+
"""Progress callback function, puts progress events into queue"""
|
|
266
|
+
try:
|
|
267
|
+
asyncio.get_event_loop().call_soon_threadsafe(progress_queue.put_nowait, event)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Progress callback error: {e}")
|
|
270
|
+
|
|
271
|
+
pipeline = ResearchPipeline(
|
|
272
|
+
config=config,
|
|
273
|
+
api_key=api_key,
|
|
274
|
+
base_url=base_url,
|
|
275
|
+
api_version=api_version,
|
|
276
|
+
research_id=task_id,
|
|
277
|
+
kb_name=kb_name,
|
|
278
|
+
progress_callback=progress_callback,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# 4. Background log pusher
|
|
282
|
+
async def log_pusher():
|
|
283
|
+
while True:
|
|
284
|
+
try:
|
|
285
|
+
log = await log_queue.get()
|
|
286
|
+
if log is None:
|
|
287
|
+
break
|
|
288
|
+
await websocket.send_json({"type": "log", "content": log})
|
|
289
|
+
log_queue.task_done()
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"Log pusher error: {e}")
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
# 5. Background progress pusher
|
|
295
|
+
async def progress_pusher():
|
|
296
|
+
while True:
|
|
297
|
+
try:
|
|
298
|
+
event = await progress_queue.get()
|
|
299
|
+
if event is None:
|
|
300
|
+
break
|
|
301
|
+
await websocket.send_json(event)
|
|
302
|
+
progress_queue.task_done()
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Progress pusher error: {e}")
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
pusher_task = asyncio.create_task(log_pusher())
|
|
308
|
+
progress_pusher_task = asyncio.create_task(progress_pusher())
|
|
309
|
+
|
|
310
|
+
# 6. Run Pipeline with stdout interception
|
|
311
|
+
class ResearchStdoutInterceptor:
|
|
312
|
+
def __init__(self, queue):
|
|
313
|
+
self.queue = queue
|
|
314
|
+
self.original_stdout = sys.stdout
|
|
315
|
+
|
|
316
|
+
def write(self, message):
|
|
317
|
+
# Write to terminal first to ensure terminal output is not blocked
|
|
318
|
+
self.original_stdout.write(message)
|
|
319
|
+
# Then try to send to frontend (non-blocking, failure doesn't affect terminal output)
|
|
320
|
+
if message.strip():
|
|
321
|
+
try:
|
|
322
|
+
# Use call_soon_threadsafe for thread safety
|
|
323
|
+
loop = asyncio.get_event_loop()
|
|
324
|
+
loop.call_soon_threadsafe(self.queue.put_nowait, message)
|
|
325
|
+
except (asyncio.QueueFull, RuntimeError, AttributeError):
|
|
326
|
+
# Queue full, event loop closed, or no event loop, ignore error, doesn't affect terminal output
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
def flush(self):
|
|
330
|
+
self.original_stdout.flush()
|
|
331
|
+
|
|
332
|
+
sys.stdout = ResearchStdoutInterceptor(log_queue)
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
await websocket.send_json(
|
|
336
|
+
{"type": "status", "content": "started", "research_id": pipeline.research_id}
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
result = await pipeline.run(topic)
|
|
340
|
+
|
|
341
|
+
# Send final report content
|
|
342
|
+
with open(result["final_report_path"], encoding="utf-8") as f:
|
|
343
|
+
report_content = f.read()
|
|
344
|
+
|
|
345
|
+
# Save to history
|
|
346
|
+
history_manager.add_entry(
|
|
347
|
+
activity_type=ActivityType.RESEARCH,
|
|
348
|
+
title=topic,
|
|
349
|
+
content={"topic": topic, "report": report_content, "kb_name": kb_name},
|
|
350
|
+
summary=f"Research ID: {result['research_id']}",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
await websocket.send_json(
|
|
354
|
+
{
|
|
355
|
+
"type": "result",
|
|
356
|
+
"report": report_content,
|
|
357
|
+
"metadata": result["metadata"],
|
|
358
|
+
"research_id": result["research_id"],
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Update task status to completed
|
|
363
|
+
try:
|
|
364
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get(
|
|
365
|
+
"logging", {}
|
|
366
|
+
).get("log_dir")
|
|
367
|
+
research_logger = get_logger("Research", log_dir=log_dir)
|
|
368
|
+
research_logger.success(f"[{task_id}] Research flow completed: {topic[:50]}...")
|
|
369
|
+
task_manager.update_task_status(task_id, "completed")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.warning(f"Failed to log completion: {e}")
|
|
372
|
+
|
|
373
|
+
finally:
|
|
374
|
+
sys.stdout = original_stdout # Safely restore using saved reference
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
await websocket.send_json({"type": "error", "content": str(e)})
|
|
378
|
+
logging.error(f"Research error: {e}", exc_info=True)
|
|
379
|
+
|
|
380
|
+
# Update task status to error
|
|
381
|
+
try:
|
|
382
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get(
|
|
383
|
+
"log_dir"
|
|
384
|
+
)
|
|
385
|
+
research_logger = get_logger("Research", log_dir=log_dir)
|
|
386
|
+
research_logger.error(f"[{task_id}] Research flow failed: {e}")
|
|
387
|
+
task_manager.update_task_status(task_id, "error", error=str(e))
|
|
388
|
+
except Exception as log_err:
|
|
389
|
+
logger.warning(f"Failed to log error: {log_err}")
|
|
390
|
+
finally:
|
|
391
|
+
if pusher_task:
|
|
392
|
+
pusher_task.cancel()
|
|
393
|
+
if progress_pusher_task:
|
|
394
|
+
progress_pusher_task.cancel()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings API Router (Simplified)
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
Manages basic UI settings: theme, language, sidebar customization.
|
|
6
|
+
Configuration for LLM/Embedding/TTS/Search is handled by the unified config service.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Literal, Optional
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
# Settings file path for UI preferences (stored in settings folder with other configs)
|
|
19
|
+
SETTINGS_FILE = (
|
|
20
|
+
Path(__file__).parent.parent.parent.parent / "data" / "user" / "settings" / "interface.json"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Default sidebar navigation order
|
|
24
|
+
DEFAULT_SIDEBAR_NAV_ORDER = {
|
|
25
|
+
"start": ["/", "/history", "/knowledge", "/notebook"],
|
|
26
|
+
"learnResearch": ["/question", "/solver", "/guide", "/ideagen", "/research", "/co_writer"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Default UI settings
|
|
30
|
+
DEFAULT_UI_SETTINGS = {
|
|
31
|
+
"theme": "light",
|
|
32
|
+
"language": "en",
|
|
33
|
+
"sidebar_description": "✨ Data Intelligence Lab @ HKU",
|
|
34
|
+
"sidebar_nav_order": DEFAULT_SIDEBAR_NAV_ORDER,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SidebarNavOrder(BaseModel):
|
|
39
|
+
start: List[str]
|
|
40
|
+
learnResearch: List[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UISettings(BaseModel):
|
|
44
|
+
theme: Literal["light", "dark"] = "light"
|
|
45
|
+
language: Literal["zh", "en"] = "en"
|
|
46
|
+
sidebar_description: Optional[str] = None
|
|
47
|
+
sidebar_nav_order: Optional[SidebarNavOrder] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ThemeUpdate(BaseModel):
|
|
51
|
+
theme: Literal["light", "dark"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LanguageUpdate(BaseModel):
|
|
55
|
+
language: Literal["zh", "en"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SidebarDescriptionUpdate(BaseModel):
|
|
59
|
+
description: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SidebarNavOrderUpdate(BaseModel):
|
|
63
|
+
nav_order: SidebarNavOrder
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_ui_settings() -> dict:
|
|
67
|
+
"""Load UI-specific settings from json file"""
|
|
68
|
+
if SETTINGS_FILE.exists():
|
|
69
|
+
try:
|
|
70
|
+
with open(SETTINGS_FILE, encoding="utf-8") as f:
|
|
71
|
+
saved = json.load(f)
|
|
72
|
+
return {**DEFAULT_UI_SETTINGS, **saved}
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
return DEFAULT_UI_SETTINGS.copy()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def save_ui_settings(settings: dict):
|
|
79
|
+
"""Save UI settings"""
|
|
80
|
+
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
|
82
|
+
json.dump(settings, f, ensure_ascii=False, indent=2)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("")
|
|
86
|
+
async def get_settings():
|
|
87
|
+
"""Get UI settings."""
|
|
88
|
+
return {"ui": load_ui_settings()}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.put("/theme")
|
|
92
|
+
async def update_theme(update: ThemeUpdate):
|
|
93
|
+
"""Update UI theme"""
|
|
94
|
+
current_ui = load_ui_settings()
|
|
95
|
+
current_ui["theme"] = update.theme
|
|
96
|
+
save_ui_settings(current_ui)
|
|
97
|
+
return {"theme": update.theme}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.put("/language")
|
|
101
|
+
async def update_language(update: LanguageUpdate):
|
|
102
|
+
"""Update UI language"""
|
|
103
|
+
current_ui = load_ui_settings()
|
|
104
|
+
current_ui["language"] = update.language
|
|
105
|
+
save_ui_settings(current_ui)
|
|
106
|
+
return {"language": update.language}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.put("/ui")
|
|
110
|
+
async def update_ui_settings(update: UISettings):
|
|
111
|
+
"""Update all UI settings"""
|
|
112
|
+
current_ui = load_ui_settings()
|
|
113
|
+
update_dict = update.model_dump(exclude_none=True)
|
|
114
|
+
current_ui.update(update_dict)
|
|
115
|
+
save_ui_settings(current_ui)
|
|
116
|
+
return current_ui
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.post("/reset")
|
|
120
|
+
async def reset_settings():
|
|
121
|
+
"""Reset UI settings to default"""
|
|
122
|
+
save_ui_settings(DEFAULT_UI_SETTINGS)
|
|
123
|
+
return DEFAULT_UI_SETTINGS
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@router.get("/themes")
|
|
127
|
+
async def get_themes():
|
|
128
|
+
"""Get available theme list"""
|
|
129
|
+
return {
|
|
130
|
+
"themes": [
|
|
131
|
+
{"id": "light", "name": "Light"},
|
|
132
|
+
{"id": "dark", "name": "Dark"},
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.get("/sidebar")
|
|
138
|
+
async def get_sidebar_settings():
|
|
139
|
+
"""Get sidebar customization settings"""
|
|
140
|
+
current_ui = load_ui_settings()
|
|
141
|
+
return {
|
|
142
|
+
"description": current_ui.get(
|
|
143
|
+
"sidebar_description", DEFAULT_UI_SETTINGS["sidebar_description"]
|
|
144
|
+
),
|
|
145
|
+
"nav_order": current_ui.get("sidebar_nav_order", DEFAULT_UI_SETTINGS["sidebar_nav_order"]),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.put("/sidebar/description")
|
|
150
|
+
async def update_sidebar_description(update: SidebarDescriptionUpdate):
|
|
151
|
+
"""Update sidebar description"""
|
|
152
|
+
current_ui = load_ui_settings()
|
|
153
|
+
current_ui["sidebar_description"] = update.description
|
|
154
|
+
save_ui_settings(current_ui)
|
|
155
|
+
return {"description": update.description}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.put("/sidebar/nav-order")
|
|
159
|
+
async def update_sidebar_nav_order(update: SidebarNavOrderUpdate):
|
|
160
|
+
"""Update sidebar navigation order"""
|
|
161
|
+
current_ui = load_ui_settings()
|
|
162
|
+
current_ui["sidebar_nav_order"] = update.nav_order.model_dump()
|
|
163
|
+
save_ui_settings(current_ui)
|
|
164
|
+
return {"nav_order": update.nav_order.model_dump()}
|