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,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Telemetry
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
Basic telemetry tracking for LLM calls.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def track_llm_call(provider_name: str):
|
|
16
|
+
"""
|
|
17
|
+
Decorator to track LLM calls for telemetry.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
provider_name: Name of the provider being called
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Decorator function
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def decorator(func: Callable) -> Callable:
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
async def wrapper(*args, **kwargs) -> Any:
|
|
29
|
+
logger.debug(f"LLM call to {provider_name}: {func.__name__}")
|
|
30
|
+
try:
|
|
31
|
+
result = await func(*args, **kwargs)
|
|
32
|
+
logger.debug(f"LLM call to {provider_name} completed successfully")
|
|
33
|
+
return result
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logger.warning(f"LLM call to {provider_name} failed: {e}")
|
|
36
|
+
raise
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
39
|
+
|
|
40
|
+
return decorator
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, AsyncGenerator, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TutorResponse(BaseModel):
|
|
7
|
+
content: str
|
|
8
|
+
raw_response: Dict[str, Any]
|
|
9
|
+
usage: Dict[str, int] = Field(
|
|
10
|
+
default_factory=lambda: {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
|
11
|
+
)
|
|
12
|
+
provider: str
|
|
13
|
+
model: str
|
|
14
|
+
finish_reason: Optional[str] = None
|
|
15
|
+
cost_estimate: float = 0.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TutorStreamChunk(BaseModel):
|
|
19
|
+
content: str
|
|
20
|
+
delta: str
|
|
21
|
+
provider: str
|
|
22
|
+
model: str
|
|
23
|
+
is_complete: bool = False
|
|
24
|
+
usage: Optional[Dict[str, int]] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AsyncStreamGenerator = AsyncGenerator[TutorStreamChunk, None]
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Utilities
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
Utility functions for LLM service:
|
|
6
|
+
- URL handling for local and cloud servers
|
|
7
|
+
- Response content extraction
|
|
8
|
+
- Thinking tags cleaning
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
# Known cloud provider domains (should never be treated as local)
|
|
15
|
+
CLOUD_DOMAINS = [
|
|
16
|
+
".openai.com",
|
|
17
|
+
".anthropic.com",
|
|
18
|
+
".deepseek.com",
|
|
19
|
+
".openrouter.ai",
|
|
20
|
+
".azure.com",
|
|
21
|
+
".googleapis.com",
|
|
22
|
+
".cohere.ai",
|
|
23
|
+
".mistral.ai",
|
|
24
|
+
".together.ai",
|
|
25
|
+
".fireworks.ai",
|
|
26
|
+
".groq.com",
|
|
27
|
+
".perplexity.ai",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Common local server ports
|
|
31
|
+
LOCAL_PORTS = [
|
|
32
|
+
":1234", # LM Studio
|
|
33
|
+
":11434", # Ollama
|
|
34
|
+
":8000", # vLLM
|
|
35
|
+
":8080", # llama.cpp
|
|
36
|
+
":5000", # Common dev port
|
|
37
|
+
":3000", # Common dev port
|
|
38
|
+
":8001", # Alternative vLLM
|
|
39
|
+
":5001", # Alternative dev port
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Local hostname indicators
|
|
43
|
+
LOCAL_HOSTS = [
|
|
44
|
+
"localhost",
|
|
45
|
+
"127.0.0.1",
|
|
46
|
+
"0.0.0.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Ports that need /v1 suffix for OpenAI compatibility
|
|
50
|
+
V1_SUFFIX_PORTS = {
|
|
51
|
+
":11434", # Ollama
|
|
52
|
+
":1234", # LM Studio
|
|
53
|
+
":8000", # vLLM
|
|
54
|
+
":8001", # Alternative vLLM
|
|
55
|
+
":8080", # llama.cpp
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_local_llm_server(base_url: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if the given URL points to a local LLM server.
|
|
62
|
+
|
|
63
|
+
Detects local servers by:
|
|
64
|
+
1. Checking for local hostnames (localhost, 127.0.0.1, 0.0.0.0)
|
|
65
|
+
2. Checking for common local LLM server ports
|
|
66
|
+
3. Excluding known cloud provider domains
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
base_url: The base URL to check
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if the URL appears to be a local LLM server
|
|
73
|
+
"""
|
|
74
|
+
if not base_url:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
base_url_lower = base_url.lower()
|
|
78
|
+
|
|
79
|
+
# First, exclude known cloud providers
|
|
80
|
+
for domain in CLOUD_DOMAINS:
|
|
81
|
+
if domain in base_url_lower:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# Check for local hostname indicators
|
|
85
|
+
for host in LOCAL_HOSTS:
|
|
86
|
+
if host in base_url_lower:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
# Check for common local server ports
|
|
90
|
+
for port in LOCAL_PORTS:
|
|
91
|
+
if port in base_url_lower:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _needs_v1_suffix(url: str) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if the URL needs /v1 suffix for OpenAI compatibility.
|
|
100
|
+
|
|
101
|
+
Most local LLM servers (Ollama, LM Studio, vLLM, llama.cpp) expose
|
|
102
|
+
OpenAI-compatible endpoints at /v1.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
url: The URL to check
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if /v1 should be appended
|
|
109
|
+
"""
|
|
110
|
+
if not url:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
url_lower = url.lower()
|
|
114
|
+
|
|
115
|
+
# Skip if already has /v1
|
|
116
|
+
if url_lower.endswith("/v1"):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Only add /v1 for local servers with known ports that need it
|
|
120
|
+
if not is_local_llm_server(url):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Check if URL contains any port that needs /v1 suffix
|
|
124
|
+
# Also check for "ollama" in URL (but not ollama.com cloud service)
|
|
125
|
+
is_ollama = "ollama" in url_lower and "ollama.com" not in url_lower
|
|
126
|
+
if is_ollama:
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
return any(port in url_lower for port in V1_SUFFIX_PORTS)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def sanitize_url(base_url: str, model: str = "") -> str:
|
|
133
|
+
"""
|
|
134
|
+
Sanitize base URL for OpenAI-compatible APIs, with special handling for local LLM servers.
|
|
135
|
+
|
|
136
|
+
Handles:
|
|
137
|
+
- Ollama (port 11434)
|
|
138
|
+
- LM Studio (port 1234)
|
|
139
|
+
- vLLM (port 8000)
|
|
140
|
+
- llama.cpp (port 8080)
|
|
141
|
+
- Other localhost OpenAI-compatible servers
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
base_url: The base URL to sanitize
|
|
145
|
+
model: Optional model name (unused, kept for API compatibility)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Sanitized URL string
|
|
149
|
+
"""
|
|
150
|
+
if not base_url:
|
|
151
|
+
return base_url
|
|
152
|
+
|
|
153
|
+
url = base_url.rstrip("/")
|
|
154
|
+
|
|
155
|
+
# Ensure URL has a protocol (default to http for local servers)
|
|
156
|
+
if url and not url.startswith(("http://", "https://")):
|
|
157
|
+
url = "http://" + url
|
|
158
|
+
|
|
159
|
+
# Standard OpenAI client library is strict about URLs:
|
|
160
|
+
# - No trailing slashes
|
|
161
|
+
# - No /chat/completions or /completions/messages/embeddings suffixes
|
|
162
|
+
# (it adds these automatically)
|
|
163
|
+
for suffix in ["/chat/completions", "/completions", "/messages", "/embeddings"]:
|
|
164
|
+
if url.endswith(suffix):
|
|
165
|
+
url = url[: -len(suffix)]
|
|
166
|
+
url = url.rstrip("/")
|
|
167
|
+
|
|
168
|
+
# For local LLM servers, ensure /v1 is present for OpenAI compatibility
|
|
169
|
+
if _needs_v1_suffix(url):
|
|
170
|
+
url = url.rstrip("/") + "/v1"
|
|
171
|
+
|
|
172
|
+
return url
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def clean_thinking_tags(
|
|
176
|
+
content: str,
|
|
177
|
+
binding: Optional[str] = None,
|
|
178
|
+
model: Optional[str] = None,
|
|
179
|
+
) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Remove thinking tags from model output.
|
|
182
|
+
|
|
183
|
+
Some reasoning models (DeepSeek, Qwen, etc.) include <think>...</think> blocks
|
|
184
|
+
that should be stripped from the final response.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
content: Raw model output
|
|
188
|
+
binding: Provider binding name (optional, for capability check)
|
|
189
|
+
model: Model name (optional, for capability check)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Cleaned content without thinking tags
|
|
193
|
+
"""
|
|
194
|
+
if not content:
|
|
195
|
+
return content
|
|
196
|
+
|
|
197
|
+
# Check if model produces thinking tags (if binding/model provided)
|
|
198
|
+
if binding:
|
|
199
|
+
# Lazy import to avoid circular dependency
|
|
200
|
+
from .capabilities import has_thinking_tags
|
|
201
|
+
|
|
202
|
+
if not has_thinking_tags(binding, model):
|
|
203
|
+
return content
|
|
204
|
+
|
|
205
|
+
# Remove <think>...</think> blocks
|
|
206
|
+
if "<think>" in content:
|
|
207
|
+
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
|
|
208
|
+
|
|
209
|
+
return content.strip()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_chat_url(
|
|
213
|
+
base_url: str,
|
|
214
|
+
api_version: Optional[str] = None,
|
|
215
|
+
binding: Optional[str] = None,
|
|
216
|
+
) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Build the full chat completions endpoint URL.
|
|
219
|
+
|
|
220
|
+
Handles:
|
|
221
|
+
- Adding /chat/completions suffix for OpenAI-compatible endpoints
|
|
222
|
+
- Adding /messages suffix for Anthropic endpoints
|
|
223
|
+
- Adding api-version query parameter for Azure OpenAI
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
base_url: Base URL (should be sanitized first)
|
|
227
|
+
api_version: API version for Azure OpenAI (optional)
|
|
228
|
+
binding: Provider binding name (optional, for Anthropic detection)
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Full endpoint URL
|
|
232
|
+
"""
|
|
233
|
+
if not base_url:
|
|
234
|
+
return base_url
|
|
235
|
+
|
|
236
|
+
url = base_url.rstrip("/")
|
|
237
|
+
|
|
238
|
+
# Anthropic uses /messages endpoint
|
|
239
|
+
binding_lower = (binding or "").lower()
|
|
240
|
+
if binding_lower in ["anthropic", "claude"]:
|
|
241
|
+
if not url.endswith("/messages"):
|
|
242
|
+
url += "/messages"
|
|
243
|
+
else:
|
|
244
|
+
# OpenAI-compatible endpoints use /chat/completions
|
|
245
|
+
if not url.endswith("/chat/completions"):
|
|
246
|
+
url += "/chat/completions"
|
|
247
|
+
|
|
248
|
+
# Add api-version for Azure OpenAI
|
|
249
|
+
if api_version:
|
|
250
|
+
separator = "&" if "?" in url else "?"
|
|
251
|
+
url += f"{separator}api-version={api_version}"
|
|
252
|
+
|
|
253
|
+
return url
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def extract_response_content(message: dict[str, Any]) -> str:
|
|
257
|
+
"""
|
|
258
|
+
Extract content from LLM response message.
|
|
259
|
+
|
|
260
|
+
Handles different response formats from various models:
|
|
261
|
+
- Standard content field
|
|
262
|
+
- Reasoning models that use reasoning_content, reasoning, or thought fields
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
message: Message dict from LLM response (e.g., choices[0].message)
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Extracted content string
|
|
269
|
+
"""
|
|
270
|
+
if not message:
|
|
271
|
+
return ""
|
|
272
|
+
|
|
273
|
+
content = message.get("content", "")
|
|
274
|
+
|
|
275
|
+
# Handle reasoning models that return content in different fields
|
|
276
|
+
if not content:
|
|
277
|
+
content = (
|
|
278
|
+
message.get("reasoning_content")
|
|
279
|
+
or message.get("reasoning")
|
|
280
|
+
or message.get("thought")
|
|
281
|
+
or ""
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return content
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def build_auth_headers(
|
|
288
|
+
api_key: Optional[str],
|
|
289
|
+
binding: Optional[str] = None,
|
|
290
|
+
) -> dict[str, str]:
|
|
291
|
+
"""
|
|
292
|
+
Build authentication headers for LLM API requests.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
api_key: API key
|
|
296
|
+
binding: Provider binding name (for provider-specific headers)
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Headers dict
|
|
300
|
+
"""
|
|
301
|
+
headers = {"Content-Type": "application/json"}
|
|
302
|
+
|
|
303
|
+
if not api_key:
|
|
304
|
+
return headers
|
|
305
|
+
|
|
306
|
+
binding_lower = (binding or "").lower()
|
|
307
|
+
|
|
308
|
+
if binding_lower in ["anthropic", "claude"]:
|
|
309
|
+
headers["x-api-key"] = api_key
|
|
310
|
+
headers["anthropic-version"] = "2023-06-01"
|
|
311
|
+
elif binding_lower == "azure_openai":
|
|
312
|
+
headers["api-key"] = api_key
|
|
313
|
+
else:
|
|
314
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
315
|
+
|
|
316
|
+
return headers
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
__all__ = [
|
|
320
|
+
# URL utilities
|
|
321
|
+
"sanitize_url",
|
|
322
|
+
"is_local_llm_server",
|
|
323
|
+
"build_chat_url",
|
|
324
|
+
"build_auth_headers",
|
|
325
|
+
# Content utilities
|
|
326
|
+
"clean_thinking_tags",
|
|
327
|
+
"extract_response_content",
|
|
328
|
+
# Constants
|
|
329
|
+
"CLOUD_DOMAINS",
|
|
330
|
+
"LOCAL_PORTS",
|
|
331
|
+
"LOCAL_HOSTS",
|
|
332
|
+
"V1_SUFFIX_PORTS",
|
|
333
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt Service
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
Unified prompt management for all DeepTutor modules.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from src.services.prompt import get_prompt_manager, PromptManager
|
|
9
|
+
|
|
10
|
+
# Get singleton manager
|
|
11
|
+
pm = get_prompt_manager()
|
|
12
|
+
|
|
13
|
+
# Load prompts for an agent
|
|
14
|
+
prompts = pm.load_prompts("guide", "tutor_agent", language="en")
|
|
15
|
+
|
|
16
|
+
# Get specific prompt
|
|
17
|
+
system_prompt = pm.get_prompt(prompts, "system", "base")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .manager import PromptManager, get_prompt_manager
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PromptManager",
|
|
24
|
+
"get_prompt_manager",
|
|
25
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Unified Prompt Manager - Single source of truth for all prompt loading.
|
|
4
|
+
Supports multi-language, caching, and language fallbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from src.services.config import PROJECT_ROOT, parse_language
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PromptManager:
|
|
16
|
+
"""Unified prompt manager with singleton pattern and global caching."""
|
|
17
|
+
|
|
18
|
+
_instance: "PromptManager | None" = None
|
|
19
|
+
_cache: dict[str, dict[str, Any]] = {}
|
|
20
|
+
|
|
21
|
+
# Language fallback chain: if primary language not found, try alternatives
|
|
22
|
+
LANGUAGE_FALLBACKS = {
|
|
23
|
+
"zh": ["zh", "en"],
|
|
24
|
+
"en": ["en", "zh"],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Supported modules
|
|
28
|
+
MODULES = ["research", "solve", "guide", "question", "ideagen", "co_writer"]
|
|
29
|
+
|
|
30
|
+
def __new__(cls) -> "PromptManager":
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
cls._instance = super().__new__(cls)
|
|
33
|
+
return cls._instance
|
|
34
|
+
|
|
35
|
+
def load_prompts(
|
|
36
|
+
self,
|
|
37
|
+
module_name: str,
|
|
38
|
+
agent_name: str,
|
|
39
|
+
language: str = "zh",
|
|
40
|
+
subdirectory: str | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""
|
|
43
|
+
Load prompts for an agent.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
module_name: Module name (research, solve, guide, question, ideagen, co_writer)
|
|
47
|
+
agent_name: Agent name (filename without .yaml)
|
|
48
|
+
language: Language code ('zh' or 'en')
|
|
49
|
+
subdirectory: Optional subdirectory (e.g., 'solve_loop' for solve module)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Loaded prompt configuration dictionary
|
|
53
|
+
"""
|
|
54
|
+
lang_code = parse_language(language)
|
|
55
|
+
cache_key = self._build_cache_key(module_name, agent_name, lang_code, subdirectory)
|
|
56
|
+
|
|
57
|
+
if cache_key in self._cache:
|
|
58
|
+
return self._cache[cache_key]
|
|
59
|
+
|
|
60
|
+
prompts = self._load_with_fallback(module_name, agent_name, lang_code, subdirectory)
|
|
61
|
+
self._cache[cache_key] = prompts
|
|
62
|
+
return prompts
|
|
63
|
+
|
|
64
|
+
def _build_cache_key(
|
|
65
|
+
self,
|
|
66
|
+
module_name: str,
|
|
67
|
+
agent_name: str,
|
|
68
|
+
lang_code: str,
|
|
69
|
+
subdirectory: str | None,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Build unique cache key."""
|
|
72
|
+
subdir_part = f"_{subdirectory}" if subdirectory else ""
|
|
73
|
+
return f"{module_name}_{agent_name}_{lang_code}{subdir_part}"
|
|
74
|
+
|
|
75
|
+
def _load_with_fallback(
|
|
76
|
+
self,
|
|
77
|
+
module_name: str,
|
|
78
|
+
agent_name: str,
|
|
79
|
+
lang_code: str,
|
|
80
|
+
subdirectory: str | None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Load prompt file with language fallback."""
|
|
83
|
+
prompts_dir = PROJECT_ROOT / "src" / "agents" / module_name / "prompts"
|
|
84
|
+
fallback_chain = self.LANGUAGE_FALLBACKS.get(lang_code, ["en"])
|
|
85
|
+
|
|
86
|
+
for lang in fallback_chain:
|
|
87
|
+
prompt_file = self._resolve_prompt_path(prompts_dir, lang, agent_name, subdirectory)
|
|
88
|
+
if prompt_file and prompt_file.exists():
|
|
89
|
+
try:
|
|
90
|
+
with open(prompt_file, encoding="utf-8") as f:
|
|
91
|
+
return yaml.safe_load(f) or {}
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Warning: Failed to load {prompt_file}: {e}")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
print(f"Warning: No prompt file found for {module_name}/{agent_name}")
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
def _resolve_prompt_path(
|
|
100
|
+
self,
|
|
101
|
+
prompts_dir: Path,
|
|
102
|
+
lang: str,
|
|
103
|
+
agent_name: str,
|
|
104
|
+
subdirectory: str | None,
|
|
105
|
+
) -> Path | None:
|
|
106
|
+
"""Resolve prompt file path, supporting subdirectory and recursive search."""
|
|
107
|
+
lang_dir = prompts_dir / lang
|
|
108
|
+
|
|
109
|
+
if not lang_dir.exists():
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# If subdirectory specified, look there first
|
|
113
|
+
if subdirectory:
|
|
114
|
+
direct_path = lang_dir / subdirectory / f"{agent_name}.yaml"
|
|
115
|
+
if direct_path.exists():
|
|
116
|
+
return direct_path
|
|
117
|
+
|
|
118
|
+
# Try direct path
|
|
119
|
+
direct_path = lang_dir / f"{agent_name}.yaml"
|
|
120
|
+
if direct_path.exists():
|
|
121
|
+
return direct_path
|
|
122
|
+
|
|
123
|
+
# Recursive search in subdirectories
|
|
124
|
+
found = list(lang_dir.rglob(f"{agent_name}.yaml"))
|
|
125
|
+
if found:
|
|
126
|
+
return found[0]
|
|
127
|
+
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def get_prompt(
|
|
131
|
+
self,
|
|
132
|
+
prompts: dict[str, Any],
|
|
133
|
+
section: str,
|
|
134
|
+
field: str | None = None,
|
|
135
|
+
fallback: str = "",
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Safely get prompt from loaded configuration.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prompts: Loaded prompt dictionary
|
|
142
|
+
section: Top-level section name
|
|
143
|
+
field: Optional nested field name
|
|
144
|
+
fallback: Default value if not found
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Prompt string or fallback
|
|
148
|
+
"""
|
|
149
|
+
if section not in prompts:
|
|
150
|
+
return fallback
|
|
151
|
+
|
|
152
|
+
value = prompts[section]
|
|
153
|
+
|
|
154
|
+
if field is None:
|
|
155
|
+
return value if isinstance(value, str) else fallback
|
|
156
|
+
|
|
157
|
+
if isinstance(value, dict) and field in value:
|
|
158
|
+
result = value[field]
|
|
159
|
+
return result if isinstance(result, str) else fallback
|
|
160
|
+
|
|
161
|
+
return fallback
|
|
162
|
+
|
|
163
|
+
def clear_cache(self, module_name: str | None = None) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Clear cached prompts.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
module_name: If provided, only clear cache for this module
|
|
169
|
+
"""
|
|
170
|
+
if module_name:
|
|
171
|
+
keys_to_remove = [k for k in self._cache if k.startswith(f"{module_name}_")]
|
|
172
|
+
for key in keys_to_remove:
|
|
173
|
+
del self._cache[key]
|
|
174
|
+
else:
|
|
175
|
+
self._cache.clear()
|
|
176
|
+
|
|
177
|
+
def reload_prompts(
|
|
178
|
+
self,
|
|
179
|
+
module_name: str,
|
|
180
|
+
agent_name: str,
|
|
181
|
+
language: str = "zh",
|
|
182
|
+
subdirectory: str | None = None,
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
"""Force reload prompts, bypassing cache."""
|
|
185
|
+
lang_code = parse_language(language)
|
|
186
|
+
cache_key = self._build_cache_key(module_name, agent_name, lang_code, subdirectory)
|
|
187
|
+
|
|
188
|
+
if cache_key in self._cache:
|
|
189
|
+
del self._cache[cache_key]
|
|
190
|
+
|
|
191
|
+
return self.load_prompts(module_name, agent_name, language, subdirectory)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Global singleton instance
|
|
195
|
+
_prompt_manager: PromptManager | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_prompt_manager() -> PromptManager:
|
|
199
|
+
"""Get the global PromptManager instance."""
|
|
200
|
+
global _prompt_manager
|
|
201
|
+
if _prompt_manager is None:
|
|
202
|
+
_prompt_manager = PromptManager()
|
|
203
|
+
return _prompt_manager
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
__all__ = ["PromptManager", "get_prompt_manager"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RAG Service
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
Unified RAG pipeline service for DeepTutor.
|
|
6
|
+
|
|
7
|
+
Provides:
|
|
8
|
+
- RAGService: Unified entry point for all RAG operations
|
|
9
|
+
- Composable RAG pipelines
|
|
10
|
+
- Pre-configured pipelines (RAGAnything, LightRAG, LlamaIndex, Academic)
|
|
11
|
+
- Modular components (parsers, chunkers, embedders, indexers, retrievers)
|
|
12
|
+
- Factory for pipeline creation
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
# Recommended: Use RAGService for all operations
|
|
16
|
+
from src.services.rag import RAGService
|
|
17
|
+
|
|
18
|
+
service = RAGService(provider="llamaindex")
|
|
19
|
+
await service.initialize("kb_name", ["doc1.txt", "doc2.txt"])
|
|
20
|
+
result = await service.search("query", "kb_name")
|
|
21
|
+
|
|
22
|
+
# Alternative: Use factory directly
|
|
23
|
+
from src.services.rag import get_pipeline
|
|
24
|
+
|
|
25
|
+
pipeline = get_pipeline("raganything")
|
|
26
|
+
await pipeline.initialize("kb_name", ["doc1.pdf"])
|
|
27
|
+
result = await pipeline.search("query", "kb_name")
|
|
28
|
+
|
|
29
|
+
# Or build custom pipeline
|
|
30
|
+
from src.services.rag import RAGPipeline
|
|
31
|
+
from src.services.rag.components import TextParser, SemanticChunker
|
|
32
|
+
|
|
33
|
+
custom = (
|
|
34
|
+
RAGPipeline("custom")
|
|
35
|
+
.parser(TextParser())
|
|
36
|
+
.chunker(SemanticChunker())
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from .factory import get_pipeline, has_pipeline, list_pipelines, register_pipeline
|
|
41
|
+
from .pipeline import RAGPipeline
|
|
42
|
+
|
|
43
|
+
# Import pipeline classes for convenience
|
|
44
|
+
from .pipelines.raganything import RAGAnythingPipeline
|
|
45
|
+
from .service import RAGService
|
|
46
|
+
from .types import Chunk, Document, SearchResult
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Service (recommended entry point)
|
|
50
|
+
"RAGService",
|
|
51
|
+
# Types
|
|
52
|
+
"Document",
|
|
53
|
+
"Chunk",
|
|
54
|
+
"SearchResult",
|
|
55
|
+
# Pipeline
|
|
56
|
+
"RAGPipeline",
|
|
57
|
+
# Factory
|
|
58
|
+
"get_pipeline",
|
|
59
|
+
"list_pipelines",
|
|
60
|
+
"register_pipeline",
|
|
61
|
+
"has_pipeline",
|
|
62
|
+
# Pipeline implementations
|
|
63
|
+
"RAGAnythingPipeline",
|
|
64
|
+
]
|