|
|
import json |
|
|
import threading |
|
|
import tomllib |
|
|
from pathlib import Path |
|
|
from typing import Dict, List, Optional |
|
|
|
|
|
from pydantic import BaseModel, Field |
|
|
|
|
|
|
|
|
def get_project_root() -> Path: |
|
|
"""Get the project root directory""" |
|
|
return Path(__file__).resolve().parent.parent |
|
|
|
|
|
|
|
|
PROJECT_ROOT = get_project_root() |
|
|
WORKSPACE_ROOT = PROJECT_ROOT / "workspace" |
|
|
|
|
|
|
|
|
class LLMSettings(BaseModel): |
|
|
model: str = Field(..., description="Model name") |
|
|
base_url: str = Field(..., description="API base URL") |
|
|
api_key: str = Field(..., description="API key") |
|
|
max_tokens: int = Field(4096, description="Maximum number of tokens per request") |
|
|
max_input_tokens: Optional[int] = Field( |
|
|
None, |
|
|
description="Maximum input tokens to use across all requests (None for unlimited)", |
|
|
) |
|
|
temperature: float = Field(1.0, description="Sampling temperature") |
|
|
api_type: str = Field(..., description="Azure, Openai, or Ollama") |
|
|
api_version: str = Field(..., description="Azure Openai version if AzureOpenai") |
|
|
|
|
|
|
|
|
class ProxySettings(BaseModel): |
|
|
server: str = Field(None, description="Proxy server address") |
|
|
username: Optional[str] = Field(None, description="Proxy username") |
|
|
password: Optional[str] = Field(None, description="Proxy password") |
|
|
|
|
|
|
|
|
class SearchSettings(BaseModel): |
|
|
engine: str = Field(default="Google", description="Search engine the llm to use") |
|
|
fallback_engines: List[str] = Field( |
|
|
default_factory=lambda: ["DuckDuckGo", "Baidu", "Bing"], |
|
|
description="Fallback search engines to try if the primary engine fails", |
|
|
) |
|
|
retry_delay: int = Field( |
|
|
default=60, |
|
|
description="Seconds to wait before retrying all engines again after they all fail", |
|
|
) |
|
|
max_retries: int = Field( |
|
|
default=3, |
|
|
description="Maximum number of times to retry all engines when all fail", |
|
|
) |
|
|
lang: str = Field( |
|
|
default="en", |
|
|
description="Language code for search results (e.g., en, zh, fr)", |
|
|
) |
|
|
country: str = Field( |
|
|
default="us", |
|
|
description="Country code for search results (e.g., us, cn, uk)", |
|
|
) |
|
|
|
|
|
|
|
|
class RunflowSettings(BaseModel): |
|
|
use_data_analysis_agent: bool = Field( |
|
|
default=False, description="Enable data analysis agent in run flow" |
|
|
) |
|
|
|
|
|
|
|
|
class BrowserSettings(BaseModel): |
|
|
headless: bool = Field(False, description="Whether to run browser in headless mode") |
|
|
disable_security: bool = Field( |
|
|
True, description="Disable browser security features" |
|
|
) |
|
|
extra_chromium_args: List[str] = Field( |
|
|
default_factory=list, description="Extra arguments to pass to the browser" |
|
|
) |
|
|
chrome_instance_path: Optional[str] = Field( |
|
|
None, description="Path to a Chrome instance to use" |
|
|
) |
|
|
wss_url: Optional[str] = Field( |
|
|
None, description="Connect to a browser instance via WebSocket" |
|
|
) |
|
|
cdp_url: Optional[str] = Field( |
|
|
None, description="Connect to a browser instance via CDP" |
|
|
) |
|
|
proxy: Optional[ProxySettings] = Field( |
|
|
None, description="Proxy settings for the browser" |
|
|
) |
|
|
max_content_length: int = Field( |
|
|
2000, description="Maximum length for content retrieval operations" |
|
|
) |
|
|
|
|
|
|
|
|
class SandboxSettings(BaseModel): |
|
|
"""Configuration for the execution sandbox""" |
|
|
|
|
|
use_sandbox: bool = Field(False, description="Whether to use the sandbox") |
|
|
image: str = Field("python:3.12-slim", description="Base image") |
|
|
work_dir: str = Field("/workspace", description="Container working directory") |
|
|
memory_limit: str = Field("512m", description="Memory limit") |
|
|
cpu_limit: float = Field(1.0, description="CPU limit") |
|
|
timeout: int = Field(300, description="Default command timeout (seconds)") |
|
|
network_enabled: bool = Field( |
|
|
False, description="Whether network access is allowed" |
|
|
) |
|
|
|
|
|
|
|
|
class DaytonaSettings(BaseModel): |
|
|
daytona_api_key: str |
|
|
daytona_server_url: Optional[str] = Field( |
|
|
"https://app.daytona.io/api", description="" |
|
|
) |
|
|
daytona_target: Optional[str] = Field("us", description="enum ['eu', 'us']") |
|
|
sandbox_image_name: Optional[str] = Field("whitezxj/sandbox:0.1.0", description="") |
|
|
sandbox_entrypoint: Optional[str] = Field( |
|
|
"/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf", |
|
|
description="", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
VNC_password: Optional[str] = Field( |
|
|
"123456", description="VNC password for the vnc service in sandbox" |
|
|
) |
|
|
|
|
|
|
|
|
class MCPServerConfig(BaseModel): |
|
|
"""Configuration for a single MCP server""" |
|
|
|
|
|
type: str = Field(..., description="Server connection type (sse or stdio)") |
|
|
url: Optional[str] = Field(None, description="Server URL for SSE connections") |
|
|
command: Optional[str] = Field(None, description="Command for stdio connections") |
|
|
args: List[str] = Field( |
|
|
default_factory=list, description="Arguments for stdio command" |
|
|
) |
|
|
|
|
|
|
|
|
class MCPSettings(BaseModel): |
|
|
"""Configuration for MCP (Model Context Protocol)""" |
|
|
|
|
|
server_reference: str = Field( |
|
|
"app.mcp.server", description="Module reference for the MCP server" |
|
|
) |
|
|
servers: Dict[str, MCPServerConfig] = Field( |
|
|
default_factory=dict, description="MCP server configurations" |
|
|
) |
|
|
|
|
|
@classmethod |
|
|
def load_server_config(cls) -> Dict[str, MCPServerConfig]: |
|
|
"""Load MCP server configuration from JSON file""" |
|
|
config_path = PROJECT_ROOT / "config" / "mcp.json" |
|
|
|
|
|
try: |
|
|
config_file = config_path if config_path.exists() else None |
|
|
if not config_file: |
|
|
return {} |
|
|
|
|
|
with config_file.open() as f: |
|
|
data = json.load(f) |
|
|
servers = {} |
|
|
|
|
|
for server_id, server_config in data.get("mcpServers", {}).items(): |
|
|
servers[server_id] = MCPServerConfig( |
|
|
type=server_config["type"], |
|
|
url=server_config.get("url"), |
|
|
command=server_config.get("command"), |
|
|
args=server_config.get("args", []), |
|
|
) |
|
|
return servers |
|
|
except Exception as e: |
|
|
raise ValueError(f"Failed to load MCP server config: {e}") |
|
|
|
|
|
|
|
|
class AppConfig(BaseModel): |
|
|
llm: Dict[str, LLMSettings] |
|
|
sandbox: Optional[SandboxSettings] = Field( |
|
|
None, description="Sandbox configuration" |
|
|
) |
|
|
browser_config: Optional[BrowserSettings] = Field( |
|
|
None, description="Browser configuration" |
|
|
) |
|
|
search_config: Optional[SearchSettings] = Field( |
|
|
None, description="Search configuration" |
|
|
) |
|
|
mcp_config: Optional[MCPSettings] = Field(None, description="MCP configuration") |
|
|
run_flow_config: Optional[RunflowSettings] = Field( |
|
|
None, description="Run flow configuration" |
|
|
) |
|
|
daytona_config: Optional[DaytonaSettings] = Field( |
|
|
None, description="Daytona configuration" |
|
|
) |
|
|
|
|
|
class Config: |
|
|
arbitrary_types_allowed = True |
|
|
|
|
|
|
|
|
class Config: |
|
|
_instance = None |
|
|
_lock = threading.Lock() |
|
|
_initialized = False |
|
|
|
|
|
def __new__(cls): |
|
|
if cls._instance is None: |
|
|
with cls._lock: |
|
|
if cls._instance is None: |
|
|
cls._instance = super().__new__(cls) |
|
|
return cls._instance |
|
|
|
|
|
def __init__(self): |
|
|
if not self._initialized: |
|
|
with self._lock: |
|
|
if not self._initialized: |
|
|
self._config = None |
|
|
self._load_initial_config() |
|
|
self._initialized = True |
|
|
|
|
|
@staticmethod |
|
|
def _get_config_path() -> Path: |
|
|
root = PROJECT_ROOT |
|
|
config_path = root / "config" / "config.toml" |
|
|
if config_path.exists(): |
|
|
return config_path |
|
|
example_path = root / "config" / "config.example.toml" |
|
|
if example_path.exists(): |
|
|
return example_path |
|
|
raise FileNotFoundError("No configuration file found in config directory") |
|
|
|
|
|
def _load_config(self) -> dict: |
|
|
config_path = self._get_config_path() |
|
|
with config_path.open("rb") as f: |
|
|
return tomllib.load(f) |
|
|
|
|
|
def _load_initial_config(self): |
|
|
raw_config = self._load_config() |
|
|
base_llm = raw_config.get("llm", {}) |
|
|
llm_overrides = { |
|
|
k: v for k, v in raw_config.get("llm", {}).items() if isinstance(v, dict) |
|
|
} |
|
|
|
|
|
default_settings = { |
|
|
"model": base_llm.get("model"), |
|
|
"base_url": base_llm.get("base_url"), |
|
|
"api_key": base_llm.get("api_key"), |
|
|
"max_tokens": base_llm.get("max_tokens", 4096), |
|
|
"max_input_tokens": base_llm.get("max_input_tokens"), |
|
|
"temperature": base_llm.get("temperature", 1.0), |
|
|
"api_type": base_llm.get("api_type", ""), |
|
|
"api_version": base_llm.get("api_version", ""), |
|
|
} |
|
|
|
|
|
|
|
|
browser_config = raw_config.get("browser", {}) |
|
|
browser_settings = None |
|
|
|
|
|
if browser_config: |
|
|
|
|
|
proxy_config = browser_config.get("proxy", {}) |
|
|
proxy_settings = None |
|
|
|
|
|
if proxy_config and proxy_config.get("server"): |
|
|
proxy_settings = ProxySettings( |
|
|
**{ |
|
|
k: v |
|
|
for k, v in proxy_config.items() |
|
|
if k in ["server", "username", "password"] and v |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
valid_browser_params = { |
|
|
k: v |
|
|
for k, v in browser_config.items() |
|
|
if k in BrowserSettings.__annotations__ and v is not None |
|
|
} |
|
|
|
|
|
|
|
|
if proxy_settings: |
|
|
valid_browser_params["proxy"] = proxy_settings |
|
|
|
|
|
|
|
|
if valid_browser_params: |
|
|
browser_settings = BrowserSettings(**valid_browser_params) |
|
|
|
|
|
search_config = raw_config.get("search", {}) |
|
|
search_settings = None |
|
|
if search_config: |
|
|
search_settings = SearchSettings(**search_config) |
|
|
sandbox_config = raw_config.get("sandbox", {}) |
|
|
if sandbox_config: |
|
|
sandbox_settings = SandboxSettings(**sandbox_config) |
|
|
else: |
|
|
sandbox_settings = SandboxSettings() |
|
|
daytona_config = raw_config.get("daytona", {}) |
|
|
if daytona_config: |
|
|
daytona_settings = DaytonaSettings(**daytona_config) |
|
|
else: |
|
|
daytona_settings = DaytonaSettings() |
|
|
|
|
|
mcp_config = raw_config.get("mcp", {}) |
|
|
mcp_settings = None |
|
|
if mcp_config: |
|
|
|
|
|
mcp_config["servers"] = MCPSettings.load_server_config() |
|
|
mcp_settings = MCPSettings(**mcp_config) |
|
|
else: |
|
|
mcp_settings = MCPSettings(servers=MCPSettings.load_server_config()) |
|
|
|
|
|
run_flow_config = raw_config.get("runflow") |
|
|
if run_flow_config: |
|
|
run_flow_settings = RunflowSettings(**run_flow_config) |
|
|
else: |
|
|
run_flow_settings = RunflowSettings() |
|
|
config_dict = { |
|
|
"llm": { |
|
|
"default": default_settings, |
|
|
**{ |
|
|
name: {**default_settings, **override_config} |
|
|
for name, override_config in llm_overrides.items() |
|
|
}, |
|
|
}, |
|
|
"sandbox": sandbox_settings, |
|
|
"browser_config": browser_settings, |
|
|
"search_config": search_settings, |
|
|
"mcp_config": mcp_settings, |
|
|
"run_flow_config": run_flow_settings, |
|
|
"daytona_config": daytona_settings, |
|
|
} |
|
|
|
|
|
self._config = AppConfig(**config_dict) |
|
|
|
|
|
@property |
|
|
def llm(self) -> Dict[str, LLMSettings]: |
|
|
return self._config.llm |
|
|
|
|
|
@property |
|
|
def sandbox(self) -> SandboxSettings: |
|
|
return self._config.sandbox |
|
|
|
|
|
@property |
|
|
def daytona(self) -> DaytonaSettings: |
|
|
return self._config.daytona_config |
|
|
|
|
|
@property |
|
|
def browser_config(self) -> Optional[BrowserSettings]: |
|
|
return self._config.browser_config |
|
|
|
|
|
@property |
|
|
def search_config(self) -> Optional[SearchSettings]: |
|
|
return self._config.search_config |
|
|
|
|
|
@property |
|
|
def mcp_config(self) -> MCPSettings: |
|
|
"""Get the MCP configuration""" |
|
|
return self._config.mcp_config |
|
|
|
|
|
@property |
|
|
def run_flow_config(self) -> RunflowSettings: |
|
|
"""Get the Run Flow configuration""" |
|
|
return self._config.run_flow_config |
|
|
|
|
|
@property |
|
|
def workspace_root(self) -> Path: |
|
|
"""Get the workspace root directory""" |
|
|
return WORKSPACE_ROOT |
|
|
|
|
|
@property |
|
|
def root_path(self) -> Path: |
|
|
"""Get the root path of the application""" |
|
|
return PROJECT_ROOT |
|
|
|
|
|
|
|
|
config = Config() |
|
|
|