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,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider Capabilities
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
Centralized configuration for LLM provider capabilities.
|
|
6
|
+
This replaces scattered hardcoded checks throughout the codebase.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from src.services.llm.capabilities import get_capability, supports_response_format
|
|
10
|
+
|
|
11
|
+
# Check if a provider supports response_format
|
|
12
|
+
if supports_response_format(binding, model):
|
|
13
|
+
kwargs["response_format"] = {"type": "json_object"}
|
|
14
|
+
|
|
15
|
+
# Generic capability check
|
|
16
|
+
if get_capability(binding, "streaming", default=True):
|
|
17
|
+
# use streaming
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
# Provider capabilities configuration
|
|
23
|
+
# Keys are binding names (lowercase), values are capability dictionaries
|
|
24
|
+
PROVIDER_CAPABILITIES: dict[str, dict[str, Any]] = {
|
|
25
|
+
# OpenAI and OpenAI-compatible providers
|
|
26
|
+
"openai": {
|
|
27
|
+
"supports_response_format": True,
|
|
28
|
+
"supports_streaming": True,
|
|
29
|
+
"supports_tools": True,
|
|
30
|
+
"system_in_messages": True, # System prompt goes in messages array
|
|
31
|
+
"newer_models_use_max_completion_tokens": True,
|
|
32
|
+
},
|
|
33
|
+
"azure_openai": {
|
|
34
|
+
"supports_response_format": True,
|
|
35
|
+
"supports_streaming": True,
|
|
36
|
+
"supports_tools": True,
|
|
37
|
+
"system_in_messages": True,
|
|
38
|
+
"newer_models_use_max_completion_tokens": True,
|
|
39
|
+
"requires_api_version": True,
|
|
40
|
+
},
|
|
41
|
+
# Anthropic
|
|
42
|
+
"anthropic": {
|
|
43
|
+
"supports_response_format": False, # Anthropic uses different format
|
|
44
|
+
"supports_streaming": True,
|
|
45
|
+
"supports_tools": True,
|
|
46
|
+
"system_in_messages": False, # System is a separate parameter
|
|
47
|
+
"has_thinking_tags": False,
|
|
48
|
+
},
|
|
49
|
+
"claude": { # Alias for anthropic
|
|
50
|
+
"supports_response_format": False,
|
|
51
|
+
"supports_streaming": True,
|
|
52
|
+
"supports_tools": True,
|
|
53
|
+
"system_in_messages": False,
|
|
54
|
+
"has_thinking_tags": False,
|
|
55
|
+
},
|
|
56
|
+
# DeepSeek
|
|
57
|
+
"deepseek": {
|
|
58
|
+
"supports_response_format": False, # DeepSeek doesn't support strict JSON schema yet
|
|
59
|
+
"supports_streaming": True,
|
|
60
|
+
"supports_tools": True,
|
|
61
|
+
"system_in_messages": True,
|
|
62
|
+
"has_thinking_tags": True, # DeepSeek reasoner has thinking tags
|
|
63
|
+
},
|
|
64
|
+
# OpenRouter (aggregator, generally OpenAI-compatible)
|
|
65
|
+
"openrouter": {
|
|
66
|
+
"supports_response_format": True, # Depends on underlying model
|
|
67
|
+
"supports_streaming": True,
|
|
68
|
+
"supports_tools": True,
|
|
69
|
+
"system_in_messages": True,
|
|
70
|
+
},
|
|
71
|
+
# Groq (fast inference)
|
|
72
|
+
"groq": {
|
|
73
|
+
"supports_response_format": True,
|
|
74
|
+
"supports_streaming": True,
|
|
75
|
+
"supports_tools": True,
|
|
76
|
+
"system_in_messages": True,
|
|
77
|
+
},
|
|
78
|
+
# Together AI
|
|
79
|
+
"together": {
|
|
80
|
+
"supports_response_format": True,
|
|
81
|
+
"supports_streaming": True,
|
|
82
|
+
"supports_tools": True,
|
|
83
|
+
"system_in_messages": True,
|
|
84
|
+
},
|
|
85
|
+
"together_ai": { # Alias
|
|
86
|
+
"supports_response_format": True,
|
|
87
|
+
"supports_streaming": True,
|
|
88
|
+
"supports_tools": True,
|
|
89
|
+
"system_in_messages": True,
|
|
90
|
+
},
|
|
91
|
+
# Mistral
|
|
92
|
+
"mistral": {
|
|
93
|
+
"supports_response_format": True,
|
|
94
|
+
"supports_streaming": True,
|
|
95
|
+
"supports_tools": True,
|
|
96
|
+
"system_in_messages": True,
|
|
97
|
+
},
|
|
98
|
+
# Local providers (generally OpenAI-compatible)
|
|
99
|
+
"ollama": {
|
|
100
|
+
"supports_response_format": True, # Ollama supports JSON mode
|
|
101
|
+
"supports_streaming": True,
|
|
102
|
+
"supports_tools": False, # Limited tool support
|
|
103
|
+
"system_in_messages": True,
|
|
104
|
+
},
|
|
105
|
+
"lm_studio": {
|
|
106
|
+
"supports_response_format": True,
|
|
107
|
+
"supports_streaming": True,
|
|
108
|
+
"supports_tools": False,
|
|
109
|
+
"system_in_messages": True,
|
|
110
|
+
},
|
|
111
|
+
"vllm": {
|
|
112
|
+
"supports_response_format": True,
|
|
113
|
+
"supports_streaming": True,
|
|
114
|
+
"supports_tools": False,
|
|
115
|
+
"system_in_messages": True,
|
|
116
|
+
},
|
|
117
|
+
"llama_cpp": {
|
|
118
|
+
"supports_response_format": True, # llama.cpp server supports JSON grammar
|
|
119
|
+
"supports_streaming": True,
|
|
120
|
+
"supports_tools": False,
|
|
121
|
+
"system_in_messages": True,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Default capabilities for unknown providers (assume OpenAI-compatible)
|
|
126
|
+
DEFAULT_CAPABILITIES: dict[str, Any] = {
|
|
127
|
+
"supports_response_format": True,
|
|
128
|
+
"supports_streaming": True,
|
|
129
|
+
"supports_tools": False,
|
|
130
|
+
"system_in_messages": True,
|
|
131
|
+
"has_thinking_tags": False,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Model-specific overrides
|
|
135
|
+
# Format: {model_pattern: {capability: value}}
|
|
136
|
+
# Patterns are matched with case-insensitive startswith
|
|
137
|
+
MODEL_OVERRIDES: dict[str, dict[str, Any]] = {
|
|
138
|
+
"deepseek": {
|
|
139
|
+
"supports_response_format": False,
|
|
140
|
+
"has_thinking_tags": True,
|
|
141
|
+
},
|
|
142
|
+
"deepseek-reasoner": {
|
|
143
|
+
"supports_response_format": False,
|
|
144
|
+
"has_thinking_tags": True,
|
|
145
|
+
},
|
|
146
|
+
"qwen": {
|
|
147
|
+
# Qwen models may have thinking tags
|
|
148
|
+
"has_thinking_tags": True,
|
|
149
|
+
},
|
|
150
|
+
"qwq": {
|
|
151
|
+
# QwQ is Qwen's reasoning model with thinking tags
|
|
152
|
+
"has_thinking_tags": True,
|
|
153
|
+
},
|
|
154
|
+
# Claude models through OpenRouter or other providers
|
|
155
|
+
"claude": {
|
|
156
|
+
"supports_response_format": False,
|
|
157
|
+
"system_in_messages": False,
|
|
158
|
+
},
|
|
159
|
+
# Anthropic models
|
|
160
|
+
"anthropic/": {
|
|
161
|
+
"supports_response_format": False,
|
|
162
|
+
"system_in_messages": False,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_capability(
|
|
168
|
+
binding: str,
|
|
169
|
+
capability: str,
|
|
170
|
+
model: Optional[str] = None,
|
|
171
|
+
default: Any = None,
|
|
172
|
+
) -> Any:
|
|
173
|
+
"""
|
|
174
|
+
Get a capability value for a provider/model combination.
|
|
175
|
+
|
|
176
|
+
Checks in order:
|
|
177
|
+
1. Model-specific overrides (matched by prefix)
|
|
178
|
+
2. Provider/binding capabilities
|
|
179
|
+
3. Default capabilities for unknown providers
|
|
180
|
+
4. Explicit default value
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
binding: Provider binding name (e.g., "openai", "anthropic", "deepseek")
|
|
184
|
+
capability: Capability name (e.g., "supports_response_format")
|
|
185
|
+
model: Optional model name for model-specific overrides
|
|
186
|
+
default: Default value if capability is not defined
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Capability value or default
|
|
190
|
+
"""
|
|
191
|
+
binding_lower = (binding or "openai").lower()
|
|
192
|
+
|
|
193
|
+
# 1. Check model-specific overrides first
|
|
194
|
+
if model:
|
|
195
|
+
model_lower = model.lower()
|
|
196
|
+
# Sort by pattern length descending to match most specific first
|
|
197
|
+
for pattern, overrides in sorted(MODEL_OVERRIDES.items(), key=lambda x: -len(x[0])):
|
|
198
|
+
if model_lower.startswith(pattern):
|
|
199
|
+
if capability in overrides:
|
|
200
|
+
return overrides[capability]
|
|
201
|
+
|
|
202
|
+
# 2. Check provider capabilities
|
|
203
|
+
provider_caps = PROVIDER_CAPABILITIES.get(binding_lower, {})
|
|
204
|
+
if capability in provider_caps:
|
|
205
|
+
return provider_caps[capability]
|
|
206
|
+
|
|
207
|
+
# 3. Check default capabilities for unknown providers
|
|
208
|
+
if capability in DEFAULT_CAPABILITIES:
|
|
209
|
+
return DEFAULT_CAPABILITIES[capability]
|
|
210
|
+
|
|
211
|
+
# 4. Return explicit default
|
|
212
|
+
return default
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def supports_response_format(binding: str, model: Optional[str] = None) -> bool:
|
|
216
|
+
"""
|
|
217
|
+
Check if the provider/model supports response_format parameter.
|
|
218
|
+
|
|
219
|
+
This is a convenience function for the most common capability check.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
binding: Provider binding name
|
|
223
|
+
model: Optional model name for model-specific overrides
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if response_format is supported
|
|
227
|
+
"""
|
|
228
|
+
return get_capability(binding, "supports_response_format", model, default=True)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def supports_streaming(binding: str, model: Optional[str] = None) -> bool:
|
|
232
|
+
"""
|
|
233
|
+
Check if the provider/model supports streaming responses.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
binding: Provider binding name
|
|
237
|
+
model: Optional model name
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if streaming is supported
|
|
241
|
+
"""
|
|
242
|
+
return get_capability(binding, "supports_streaming", model, default=True)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def system_in_messages(binding: str, model: Optional[str] = None) -> bool:
|
|
246
|
+
"""
|
|
247
|
+
Check if system prompt should be in messages array (OpenAI style)
|
|
248
|
+
or as a separate parameter (Anthropic style).
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
binding: Provider binding name
|
|
252
|
+
model: Optional model name
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if system prompt goes in messages array
|
|
256
|
+
"""
|
|
257
|
+
return get_capability(binding, "system_in_messages", model, default=True)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def has_thinking_tags(binding: str, model: Optional[str] = None) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Check if the model output may contain thinking tags (<think>...</think>).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
binding: Provider binding name
|
|
266
|
+
model: Optional model name
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
True if thinking tags should be filtered
|
|
270
|
+
"""
|
|
271
|
+
return get_capability(binding, "has_thinking_tags", model, default=False)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def supports_tools(binding: str, model: Optional[str] = None) -> bool:
|
|
275
|
+
"""
|
|
276
|
+
Check if the provider/model supports function calling / tools.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
binding: Provider binding name
|
|
280
|
+
model: Optional model name
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if tools/function calling is supported
|
|
284
|
+
"""
|
|
285
|
+
return get_capability(binding, "supports_tools", model, default=False)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def requires_api_version(binding: str, model: Optional[str] = None) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Check if the provider requires an API version parameter (e.g., Azure OpenAI).
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
binding: Provider binding name
|
|
294
|
+
model: Optional model name
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if api_version is required
|
|
298
|
+
"""
|
|
299
|
+
return get_capability(binding, "requires_api_version", model, default=False)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
__all__ = [
|
|
303
|
+
"PROVIDER_CAPABILITIES",
|
|
304
|
+
"MODEL_OVERRIDES",
|
|
305
|
+
"DEFAULT_CAPABILITIES",
|
|
306
|
+
"get_capability",
|
|
307
|
+
"supports_response_format",
|
|
308
|
+
"supports_streaming",
|
|
309
|
+
"system_in_messages",
|
|
310
|
+
"has_thinking_tags",
|
|
311
|
+
"supports_tools",
|
|
312
|
+
"requires_api_version",
|
|
313
|
+
]
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Client
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
Unified LLM client for all DeepTutor services.
|
|
6
|
+
|
|
7
|
+
Note: This is a legacy interface. Prefer using the factory functions directly:
|
|
8
|
+
from src.services.llm import complete, stream
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from src.logging import get_logger
|
|
14
|
+
|
|
15
|
+
from .capabilities import system_in_messages
|
|
16
|
+
from .config import LLMConfig, get_llm_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LLMClient:
|
|
20
|
+
"""
|
|
21
|
+
Unified LLM client for all services.
|
|
22
|
+
|
|
23
|
+
Wraps the LLM Factory with a class-based interface.
|
|
24
|
+
Prefer using factory functions (complete, stream) directly for new code.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: Optional[LLMConfig] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize LLM client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config: LLM configuration. If None, loads from environment.
|
|
33
|
+
"""
|
|
34
|
+
self.config = config or get_llm_config()
|
|
35
|
+
self.logger = get_logger("LLMClient")
|
|
36
|
+
|
|
37
|
+
async def complete(
|
|
38
|
+
self,
|
|
39
|
+
prompt: str,
|
|
40
|
+
system_prompt: Optional[str] = None,
|
|
41
|
+
history: Optional[List[Dict[str, str]]] = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Call LLM completion via Factory.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prompt: User prompt
|
|
49
|
+
system_prompt: Optional system prompt
|
|
50
|
+
history: Optional conversation history
|
|
51
|
+
**kwargs: Additional arguments passed to the API
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
LLM response text
|
|
55
|
+
"""
|
|
56
|
+
from . import factory
|
|
57
|
+
|
|
58
|
+
# Delegate to factory for unified routing and retry handling
|
|
59
|
+
return await factory.complete(
|
|
60
|
+
prompt=prompt,
|
|
61
|
+
system_prompt=system_prompt or "You are a helpful assistant.",
|
|
62
|
+
model=self.config.model,
|
|
63
|
+
api_key=self.config.api_key,
|
|
64
|
+
base_url=self.config.base_url,
|
|
65
|
+
api_version=getattr(self.config, "api_version", None),
|
|
66
|
+
binding=getattr(self.config, "binding", "openai"),
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def complete_sync(
|
|
71
|
+
self,
|
|
72
|
+
prompt: str,
|
|
73
|
+
system_prompt: Optional[str] = None,
|
|
74
|
+
history: Optional[List[Dict[str, str]]] = None,
|
|
75
|
+
**kwargs: Any,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Synchronous wrapper for complete().
|
|
79
|
+
|
|
80
|
+
Use this when you need to call from non-async context.
|
|
81
|
+
"""
|
|
82
|
+
import asyncio
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
asyncio.get_running_loop()
|
|
86
|
+
except RuntimeError:
|
|
87
|
+
# No running event loop -> safe to run synchronously.
|
|
88
|
+
return asyncio.run(self.complete(prompt, system_prompt, history, **kwargs))
|
|
89
|
+
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
"LLMClient.complete_sync() cannot be called from a running event loop. "
|
|
92
|
+
"Use `await llm.complete(...)` instead."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def get_model_func(self):
|
|
96
|
+
"""
|
|
97
|
+
Get a function compatible with LightRAG's llm_model_func parameter.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Callable that can be used as llm_model_func
|
|
101
|
+
"""
|
|
102
|
+
binding = getattr(self.config, "binding", "openai")
|
|
103
|
+
|
|
104
|
+
# Use capabilities to determine if provider uses OpenAI-style messages
|
|
105
|
+
uses_openai_style = system_in_messages(binding, self.config.model)
|
|
106
|
+
|
|
107
|
+
# For non-OpenAI-compatible providers (e.g., Anthropic), use Factory
|
|
108
|
+
if not uses_openai_style:
|
|
109
|
+
from . import factory
|
|
110
|
+
|
|
111
|
+
def llm_model_func_via_factory(
|
|
112
|
+
prompt: str,
|
|
113
|
+
system_prompt: Optional[str] = None,
|
|
114
|
+
history_messages: Optional[List[Dict]] = None,
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
):
|
|
117
|
+
return factory.complete(
|
|
118
|
+
prompt=prompt,
|
|
119
|
+
system_prompt=system_prompt or "You are a helpful assistant.",
|
|
120
|
+
model=self.config.model,
|
|
121
|
+
api_key=self.config.api_key,
|
|
122
|
+
base_url=self.config.base_url,
|
|
123
|
+
binding=binding,
|
|
124
|
+
history_messages=history_messages,
|
|
125
|
+
**kwargs,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return llm_model_func_via_factory
|
|
129
|
+
|
|
130
|
+
# OpenAI-compatible bindings use lightrag (has caching)
|
|
131
|
+
from lightrag.llm.openai import openai_complete_if_cache
|
|
132
|
+
|
|
133
|
+
def llm_model_func(
|
|
134
|
+
prompt: str,
|
|
135
|
+
system_prompt: Optional[str] = None,
|
|
136
|
+
history_messages: Optional[List[Dict]] = None,
|
|
137
|
+
**kwargs: Any,
|
|
138
|
+
):
|
|
139
|
+
# Only pass api_version if set (for Azure OpenAI)
|
|
140
|
+
lightrag_kwargs = {
|
|
141
|
+
"system_prompt": system_prompt,
|
|
142
|
+
"history_messages": history_messages or [],
|
|
143
|
+
"api_key": self.config.api_key,
|
|
144
|
+
"base_url": self.config.base_url,
|
|
145
|
+
**kwargs,
|
|
146
|
+
}
|
|
147
|
+
api_version = getattr(self.config, "api_version", None)
|
|
148
|
+
if api_version:
|
|
149
|
+
lightrag_kwargs["api_version"] = api_version
|
|
150
|
+
return openai_complete_if_cache(
|
|
151
|
+
self.config.model,
|
|
152
|
+
prompt,
|
|
153
|
+
**lightrag_kwargs,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return llm_model_func
|
|
157
|
+
|
|
158
|
+
def get_vision_model_func(self):
|
|
159
|
+
"""
|
|
160
|
+
Get a function compatible with RAG-Anything's vision_model_func parameter.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Callable that can be used as vision_model_func
|
|
164
|
+
"""
|
|
165
|
+
binding = getattr(self.config, "binding", "openai")
|
|
166
|
+
|
|
167
|
+
# Use capabilities to determine if provider uses OpenAI-style messages
|
|
168
|
+
uses_openai_style = system_in_messages(binding, self.config.model)
|
|
169
|
+
|
|
170
|
+
# For non-OpenAI-compatible providers, use Factory
|
|
171
|
+
if not uses_openai_style:
|
|
172
|
+
from . import factory
|
|
173
|
+
|
|
174
|
+
def vision_model_func_via_factory(
|
|
175
|
+
prompt: str,
|
|
176
|
+
system_prompt: Optional[str] = None,
|
|
177
|
+
history_messages: Optional[List[Dict]] = None,
|
|
178
|
+
image_data: Optional[str] = None,
|
|
179
|
+
messages: Optional[List[Dict]] = None,
|
|
180
|
+
**kwargs: Any,
|
|
181
|
+
):
|
|
182
|
+
# Use factory for unified handling
|
|
183
|
+
return factory.complete(
|
|
184
|
+
prompt=prompt,
|
|
185
|
+
system_prompt=system_prompt or "You are a helpful assistant.",
|
|
186
|
+
model=self.config.model,
|
|
187
|
+
api_key=self.config.api_key,
|
|
188
|
+
base_url=self.config.base_url,
|
|
189
|
+
binding=binding,
|
|
190
|
+
messages=messages,
|
|
191
|
+
history_messages=history_messages,
|
|
192
|
+
image_data=image_data,
|
|
193
|
+
**kwargs,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return vision_model_func_via_factory
|
|
197
|
+
|
|
198
|
+
# OpenAI-compatible bindings
|
|
199
|
+
from lightrag.llm.openai import openai_complete_if_cache
|
|
200
|
+
|
|
201
|
+
# Get api_version once for reuse
|
|
202
|
+
api_version = getattr(self.config, "api_version", None)
|
|
203
|
+
|
|
204
|
+
def vision_model_func(
|
|
205
|
+
prompt: str,
|
|
206
|
+
system_prompt: Optional[str] = None,
|
|
207
|
+
history_messages: Optional[List[Dict]] = None,
|
|
208
|
+
image_data: Optional[str] = None,
|
|
209
|
+
messages: Optional[List[Dict]] = None,
|
|
210
|
+
**kwargs: Any,
|
|
211
|
+
):
|
|
212
|
+
# Handle multimodal messages
|
|
213
|
+
if messages:
|
|
214
|
+
clean_kwargs = {
|
|
215
|
+
k: v
|
|
216
|
+
for k, v in kwargs.items()
|
|
217
|
+
if k not in ["messages", "prompt", "system_prompt", "history_messages"]
|
|
218
|
+
}
|
|
219
|
+
lightrag_kwargs = {
|
|
220
|
+
"messages": messages,
|
|
221
|
+
"api_key": self.config.api_key,
|
|
222
|
+
"base_url": self.config.base_url,
|
|
223
|
+
**clean_kwargs,
|
|
224
|
+
}
|
|
225
|
+
if api_version:
|
|
226
|
+
lightrag_kwargs["api_version"] = api_version
|
|
227
|
+
return openai_complete_if_cache(
|
|
228
|
+
self.config.model,
|
|
229
|
+
prompt="",
|
|
230
|
+
**lightrag_kwargs,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Handle image data
|
|
234
|
+
if image_data:
|
|
235
|
+
# Build image message
|
|
236
|
+
image_message = {
|
|
237
|
+
"role": "user",
|
|
238
|
+
"content": [
|
|
239
|
+
{"type": "text", "text": prompt},
|
|
240
|
+
{
|
|
241
|
+
"type": "image_url",
|
|
242
|
+
"image_url": {"url": f"data:image/jpeg;base64,{image_data}"},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}
|
|
246
|
+
lightrag_kwargs = {
|
|
247
|
+
"messages": [image_message],
|
|
248
|
+
"api_key": self.config.api_key,
|
|
249
|
+
"base_url": self.config.base_url,
|
|
250
|
+
**kwargs,
|
|
251
|
+
}
|
|
252
|
+
if api_version:
|
|
253
|
+
lightrag_kwargs["api_version"] = api_version
|
|
254
|
+
return openai_complete_if_cache(
|
|
255
|
+
self.config.model,
|
|
256
|
+
prompt="",
|
|
257
|
+
**lightrag_kwargs,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Fallback to regular completion
|
|
261
|
+
lightrag_kwargs = {
|
|
262
|
+
"system_prompt": system_prompt,
|
|
263
|
+
"history_messages": history_messages or [],
|
|
264
|
+
"api_key": self.config.api_key,
|
|
265
|
+
"base_url": self.config.base_url,
|
|
266
|
+
**kwargs,
|
|
267
|
+
}
|
|
268
|
+
if api_version:
|
|
269
|
+
lightrag_kwargs["api_version"] = api_version
|
|
270
|
+
return openai_complete_if_cache(
|
|
271
|
+
self.config.model,
|
|
272
|
+
prompt,
|
|
273
|
+
**lightrag_kwargs,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return vision_model_func
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Singleton instance
|
|
280
|
+
_client: Optional[LLMClient] = None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_llm_client(config: Optional[LLMConfig] = None) -> LLMClient:
|
|
284
|
+
"""
|
|
285
|
+
Get or create the singleton LLM client.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
config: Optional configuration. Only used on first call.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
LLMClient instance
|
|
292
|
+
"""
|
|
293
|
+
global _client
|
|
294
|
+
if _client is None:
|
|
295
|
+
_client = LLMClient(config)
|
|
296
|
+
return _client
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def reset_llm_client():
|
|
300
|
+
"""Reset the singleton LLM client."""
|
|
301
|
+
global _client
|
|
302
|
+
_client = None
|