22
33import os
44from enum import Enum
5- from typing import Any , List , Optional
5+ from typing import Any , Dict , List , Optional
66
77from langchain_core .runnables import RunnableConfig
8- from pydantic import BaseModel , Field
8+ from pydantic import BaseModel , Field , model_validator
99
1010
1111class SearchAPI (Enum ):
@@ -16,6 +16,66 @@ class SearchAPI(Enum):
1616 TAVILY = "tavily"
1717 NONE = "none"
1818
19+ class ModelPreset (Enum ):
20+ """Enumeration of available model presets for quick configuration."""
21+
22+ DEEPSEEK_OPENROUTER = "deepseek_openrouter"
23+ GPT4_OPENAI = "gpt4_openai"
24+ CLAUDE_ANTHROPIC = "claude_anthropic"
25+ GEMINI_GOOGLE = "gemini_google"
26+ CUSTOM = "custom"
27+
28+ # Model preset configurations
29+ MODEL_PRESETS : Dict [ModelPreset , Dict [str , Any ]] = {
30+ ModelPreset .DEEPSEEK_OPENROUTER : {
31+ "summarization_model" : "openai:gpt-4o-mini" ,
32+ "research_model" : "openai:deepseek/deepseek-chat" ,
33+ "compression_model" : "openai:deepseek/deepseek-chat" ,
34+ "final_report_model" : "openai:deepseek/deepseek-chat" ,
35+ "summarization_model_max_tokens" : 8192 ,
36+ "research_model_max_tokens" : 10000 ,
37+ "compression_model_max_tokens" : 8192 ,
38+ "final_report_model_max_tokens" : 10000 ,
39+ "description" : "使用 OpenRouter API 的 DeepSeek 模型,成本效益高"
40+ },
41+ ModelPreset .GPT4_OPENAI : {
42+ "summarization_model" : "openai:gpt-4o-mini" ,
43+ "research_model" : "openai:gpt-4o" ,
44+ "compression_model" : "openai:gpt-4o" ,
45+ "final_report_model" : "openai:gpt-4o" ,
46+ "summarization_model_max_tokens" : 8192 ,
47+ "research_model_max_tokens" : 8192 ,
48+ "compression_model_max_tokens" : 8192 ,
49+ "final_report_model_max_tokens" : 8192 ,
50+ "description" : "使用 OpenAI GPT-4o 模型,性能优秀但成本较高"
51+ },
52+ ModelPreset .CLAUDE_ANTHROPIC : {
53+ "summarization_model" : "anthropic:claude-3-5-haiku" ,
54+ "research_model" : "anthropic:claude-3-5-sonnet" ,
55+ "compression_model" : "anthropic:claude-3-5-sonnet" ,
56+ "final_report_model" : "anthropic:claude-3-5-sonnet" ,
57+ "summarization_model_max_tokens" : 8192 ,
58+ "research_model_max_tokens" : 8192 ,
59+ "compression_model_max_tokens" : 8192 ,
60+ "final_report_model_max_tokens" : 8192 ,
61+ "description" : "使用 Anthropic Claude 模型,擅长推理和分析"
62+ },
63+ ModelPreset .GEMINI_GOOGLE : {
64+ "summarization_model" : "google:gemini-1.5-flash" ,
65+ "research_model" : "google:gemini-1.5-pro" ,
66+ "compression_model" : "google:gemini-1.5-pro" ,
67+ "final_report_model" : "google:gemini-1.5-pro" ,
68+ "summarization_model_max_tokens" : 8192 ,
69+ "research_model_max_tokens" : 8192 ,
70+ "compression_model_max_tokens" : 8192 ,
71+ "final_report_model_max_tokens" : 8192 ,
72+ "description" : "使用 Google Gemini 模型,支持长上下文"
73+ },
74+ ModelPreset .CUSTOM : {
75+ "description" : "自定义模型配置,需要手动设置各个模型参数"
76+ }
77+ }
78+
1979class MCPConfig (BaseModel ):
2080 """Configuration for Model Context Protocol (MCP) servers."""
2181
@@ -38,6 +98,25 @@ class MCPConfig(BaseModel):
3898class Configuration (BaseModel ):
3999 """Main configuration class for the Deep Research agent."""
40100
101+ # Model Preset Selection
102+ model_preset : ModelPreset = Field (
103+ default = ModelPreset .DEEPSEEK_OPENROUTER ,
104+ metadata = {
105+ "x_oap_ui_config" : {
106+ "type" : "select" ,
107+ "default" : ModelPreset .DEEPSEEK_OPENROUTER .value ,
108+ "description" : "Choose a model preset for quick configuration. When not CUSTOM, individual model settings will be overridden." ,
109+ "options" : [
110+ {"label" : "DeepSeek (OpenRouter) - 成本效益" , "value" : ModelPreset .DEEPSEEK_OPENROUTER .value },
111+ {"label" : "GPT-4o (OpenAI) - 高性能" , "value" : ModelPreset .GPT4_OPENAI .value },
112+ {"label" : "Claude (Anthropic) - 善于推理" , "value" : ModelPreset .CLAUDE_ANTHROPIC .value },
113+ {"label" : "Gemini (Google) - 长上下文" , "value" : ModelPreset .GEMINI_GOOGLE .value },
114+ {"label" : "Custom - 自定义配置" , "value" : ModelPreset .CUSTOM .value }
115+ ]
116+ }
117+ }
118+ )
119+
41120 # General Configuration
42121 max_structured_output_retries : int = Field (
43122 default = 3 ,
@@ -151,12 +230,12 @@ class Configuration(BaseModel):
151230 }
152231 )
153232 research_model : str = Field (
154- default = "openai:gpt-4.1 " ,
233+ default = "openai:deepseek/deepseek-chat " ,
155234 metadata = {
156235 "x_oap_ui_config" : {
157236 "type" : "text" ,
158- "default" : "openai:gpt-4.1 " ,
159- "description" : "Model for conducting research. NOTE: Make sure your Researcher Model supports the selected search API ."
237+ "default" : "openai:deepseek/deepseek-chat " ,
238+ "description" : "Model for conducting research. Use 'openai:model_name' format for OpenRouter models to explicitly use OpenAI provider ."
160239 }
161240 }
162241 )
@@ -171,12 +250,12 @@ class Configuration(BaseModel):
171250 }
172251 )
173252 compression_model : str = Field (
174- default = "openai:gpt-4.1 " ,
253+ default = "openai:deepseek/deepseek-chat " ,
175254 metadata = {
176255 "x_oap_ui_config" : {
177256 "type" : "text" ,
178- "default" : "openai:gpt-4.1 " ,
179- "description" : "Model for compressing research findings from sub-agents. NOTE: Make sure your Compression Model supports the selected search API ."
257+ "default" : "openai:deepseek/deepseek-chat " ,
258+ "description" : "Model for compressing research findings from sub-agents. Use 'openai:model_name' format for OpenRouter models ."
180259 }
181260 }
182261 )
@@ -191,12 +270,12 @@ class Configuration(BaseModel):
191270 }
192271 )
193272 final_report_model : str = Field (
194- default = "openai:gpt-4.1 " ,
273+ default = "openai:deepseek/deepseek-chat " ,
195274 metadata = {
196275 "x_oap_ui_config" : {
197276 "type" : "text" ,
198- "default" : "openai:gpt-4.1 " ,
199- "description" : "Model for writing the final report from all research findings"
277+ "default" : "openai:deepseek/deepseek-chat " ,
278+ "description" : "Model for writing the final report from all research findings. Use 'openai:model_name' format for OpenRouter models. "
200279 }
201280 }
202281 )
@@ -231,6 +310,61 @@ class Configuration(BaseModel):
231310 }
232311 }
233312 )
313+ apiKeys : Optional [dict [str , str ]] = Field (
314+ default = {
315+ "OPENAI_API_KEY" : os .environ .get ("OPENAI_API_KEY" ),
316+ "ANTHROPIC_API_KEY" : os .environ .get ("ANTHROPIC_API_KEY" ),
317+ "GOOGLE_API_KEY" : os .environ .get ("GOOGLE_API_KEY" ),
318+ "OPENROUTER_API_KEY" : os .environ .get ("OPENROUTER_API_KEY" ),
319+ "TAVILY_API_KEY" : os .environ .get ("TAVILY_API_KEY" )
320+ },
321+ optional = True
322+ )
323+
324+ @model_validator (mode = 'before' )
325+ @classmethod
326+ def apply_model_preset (cls , data : Any ) -> Any :
327+ """Apply model preset configuration if not using custom preset."""
328+ if not isinstance (data , dict ):
329+ return data
330+
331+ # Get the model preset from the data
332+ model_preset = data .get ('model_preset' , ModelPreset .DEEPSEEK_OPENROUTER )
333+
334+ # If using custom preset, don't override the values
335+ if model_preset == ModelPreset .CUSTOM :
336+ return data
337+
338+ # Ensure model_preset is a ModelPreset enum
339+ if isinstance (model_preset , str ):
340+ try :
341+ model_preset = ModelPreset (model_preset )
342+ except ValueError :
343+ model_preset = ModelPreset .DEEPSEEK_OPENROUTER
344+
345+ # Apply preset configuration
346+ preset_config = MODEL_PRESETS .get (model_preset , {})
347+
348+ # Create a copy of data to modify
349+ result = data .copy ()
350+
351+ # Apply preset values for model fields
352+ model_fields = [
353+ 'summarization_model' , 'research_model' , 'compression_model' , 'final_report_model' ,
354+ 'summarization_model_max_tokens' , 'research_model_max_tokens' ,
355+ 'compression_model_max_tokens' , 'final_report_model_max_tokens'
356+ ]
357+
358+ for field_name in model_fields :
359+ if field_name in preset_config :
360+ # Only apply preset if the field is not explicitly set by user
361+ if field_name not in data or data [field_name ] is None :
362+ result [field_name ] = preset_config [field_name ]
363+ # For non-custom presets, always apply the preset (override user values)
364+ else :
365+ result [field_name ] = preset_config [field_name ]
366+
367+ return result
234368
235369
236370 @classmethod
@@ -245,6 +379,31 @@ def from_runnable_config(
245379 for field_name in field_names
246380 }
247381 return cls (** {k : v for k , v in values .items () if v is not None })
382+
383+ def get_preset_description (self ) -> str :
384+ """Get the description of the current model preset."""
385+ preset_config = MODEL_PRESETS .get (self .model_preset , {})
386+ return preset_config .get ("description" , "Unknown preset" )
387+
388+ def get_preset_info (self ) -> Dict [str , Any ]:
389+ """Get detailed information about the current model preset."""
390+ preset_config = MODEL_PRESETS .get (self .model_preset , {})
391+ return {
392+ "preset" : self .model_preset .value ,
393+ "description" : preset_config .get ("description" , "Unknown preset" ),
394+ "models" : {
395+ "summarization" : self .summarization_model ,
396+ "research" : self .research_model ,
397+ "compression" : self .compression_model ,
398+ "final_report" : self .final_report_model
399+ },
400+ "max_tokens" : {
401+ "summarization" : self .summarization_model_max_tokens ,
402+ "research" : self .research_model_max_tokens ,
403+ "compression" : self .compression_model_max_tokens ,
404+ "final_report" : self .final_report_model_max_tokens
405+ }
406+ }
248407
249408 class Config :
250409 """Pydantic configuration."""
0 commit comments