diff --git a/config/profiles/free_tier.yaml b/config/profiles/free_tier.yaml new file mode 100644 index 00000000..087be144 --- /dev/null +++ b/config/profiles/free_tier.yaml @@ -0,0 +1,157 @@ +# Free Tier Agent Profile +# A lightweight configuration for basic agent task execution +# Based on general.yaml with cost-effective model choices + +# Default agent configurations +agents: + # Atomizer: Fast decision-making with Gemini Flash + atomizer: + llm: + model: openrouter/google/gemini-2.5-flash + temperature: 0.4 + max_tokens: 8000 + signature_instructions: "prompt_optimization.prompts.seed_prompts.atomizer_seed:ATOMIZER_PROMPT" + demos: "prompt_optimization.prompts.seed_prompts.atomizer_seed:ATOMIZER_DEMOS" + + # Planner: Strategic planning with Gemini Flash + planner: + llm: + model: openrouter/google/gemini-2.5-flash + temperature: 0.4 + max_tokens: 32000 + signature_instructions: "prompt_optimization.prompts.seed_prompts.planner_seed:PLANNER_PROMPT" + demos: "prompt_optimization.prompts.seed_prompts.planner_seed:PLANNER_DEMOS" + agent_config: + max_subtasks: 12 + + toolkits: + - class_name: WebSearchToolkit + enabled: true + toolkit_config: + model: openrouter/openai/gpt-4o-mini + max_results: 5 + search_context_size: medium + temperature: 1.0 + max_tokens: 16000 + + # Default Executor (fallback for unmapped task types) + executor: + llm: + model: openrouter/google/gemini-2.5-flash + temperature: 0.6 + max_tokens: 64000 + prediction_strategy: react + signature_instructions: "prompt_optimization.prompts.seed_prompts.executor_seed:EXECUTOR_PROMPT" + demos: "prompt_optimization.prompts.seed_prompts.executor_seed:EXECUTOR_DEMOS" + agent_config: + max_executions: 10 + + # Default fundamental toolkits + toolkits: + - class_name: E2BToolkit + enabled: true + toolkit_config: + timeout: 600 + max_lifetime_hours: 23.5 + auto_reinitialize: true + + - class_name: WebSearchToolkit + enabled: true + toolkit_config: + model: openrouter/openai/gpt-4o-mini + max_results: 5 + search_context_size: medium + temperature: 1.0 + max_tokens: 16000 + + - class_name: FileToolkit + enabled: true + toolkit_config: + enable_delete: false + max_file_size: 10485760 # 10MB + + - class_name: CalculatorToolkit + enabled: true + + # Aggregator: Synthesis with Gemini Flash + aggregator: + llm: + model: openrouter/google/gemini-2.5-flash + temperature: 0.0 + max_tokens: 32000 + signature_instructions: "prompt_optimization.prompts.seed_prompts.aggregator_seed:AGGREGATOR_PROMPT" + demos: "prompt_optimization.prompts.seed_prompts.aggregator_seed:AGGREGATOR_DEMOS" + + # Verifier: Validation with Gemini Flash + verifier: + llm: + model: openrouter/google/gemini-2.5-flash + temperature: 0.0 + max_tokens: 16000 + signature_instructions: "prompt_optimization.prompts.seed_prompts.verifier_seed:VERIFIER_PROMPT" + demos: "prompt_optimization.prompts.seed_prompts.verifier_seed:VERIFIER_DEMOS" + +# Runtime configuration +runtime: + max_depth: 6 + verbose: true + enable_logging: true + log_level: INFO + timeout: 700 + +# Resilience +resilience: + retry: + enabled: true + max_attempts: 5 + strategy: exponential_backoff + base_delay: 2.0 + max_delay: 60.0 + + circuit_breaker: + enabled: true + failure_threshold: 5 + recovery_timeout: 120.0 + half_open_max_calls: 3 + + checkpoint: + enabled: true + storage_path: ${oc.env:ROMA_CHECKPOINT_PATH,.checkpoints} + max_checkpoints: 20 + max_age_hours: 48.0 + compress_checkpoints: true + verify_integrity: true + +# Storage +storage: + base_path: ${oc.env:STORAGE_BASE_PATH,/opt/sentient} + max_file_size: 104857600 # 100MB + + postgres: + enabled: ${oc.env:POSTGRES_ENABLED,true} + connection_url: ${oc.env:DATABASE_URL,postgresql+asyncpg://localhost/roma_dspy} + pool_size: 10 + max_overflow: 20 + +# Observability +observability: + mlflow: + enabled: ${oc.env:MLFLOW_ENABLED,false} + tracking_uri: ${oc.env:MLFLOW_TRACKING_URI,http://mlflow:5000} + experiment_name: ROMA-Free-Tier + log_traces: true + log_compiles: true + log_evals: true + +# Logging +logging: + level: ${oc.env:LOG_LEVEL,INFO} + log_dir: ${oc.env:LOG_DIR,logs} + console_format: detailed + file_format: json + serialize: true + rotation: 500 MB + retention: 90 days + colorize: true + backtrace: true + diagnose: false diff --git a/config/profiles/general.yaml b/config/profiles/general.yaml index 63ed6830..cef97960 100644 --- a/config/profiles/general.yaml +++ b/config/profiles/general.yaml @@ -104,7 +104,7 @@ runtime: verbose: true enable_logging: true log_level: INFO - timeout: 120 + timeout: 700 # Resilience resilience: diff --git a/src/roma_dspy/api/execution_service.py b/src/roma_dspy/api/execution_service.py index f315a6e7..3acea921 100644 --- a/src/roma_dspy/api/execution_service.py +++ b/src/roma_dspy/api/execution_service.py @@ -11,6 +11,7 @@ from roma_dspy.core.storage.postgres_storage import PostgresStorage from roma_dspy.core.engine.dag import TaskDAG from roma_dspy.config.manager import ConfigManager +from roma_dspy.config.utils import config_to_dict from roma_dspy.types import ExecutionStatus, TaskStatus @@ -157,9 +158,7 @@ async def start_execution( max_depth=max_depth, profile=config_profile, experiment_name=experiment_name, - config=config.model_dump() - if hasattr(config, "model_dump") - else dict(config), + config=config_to_dict(config), metadata=enhanced_metadata, ) diff --git a/src/roma_dspy/config/utils.py b/src/roma_dspy/config/utils.py index e282e96b..3fd13859 100644 --- a/src/roma_dspy/config/utils.py +++ b/src/roma_dspy/config/utils.py @@ -42,6 +42,40 @@ def interpolate_config(config: DictConfig, context: Dict[str, Any]) -> DictConfi return config_with_context +def _convert_to_serializable(obj: Any) -> Any: + """ + Recursively convert Pydantic models and dataclasses to serializable dicts. + + This handles nested structures that OmegaConf.structured() cannot process. + """ + from dataclasses import is_dataclass, asdict + from pydantic import BaseModel + + if isinstance(obj, BaseModel): + # Pydantic BaseModel - use model_dump + return {k: _convert_to_serializable(v) for k, v in obj.model_dump().items()} + elif is_dataclass(obj) and not isinstance(obj, type): + # Pydantic dataclass or stdlib dataclass instance + try: + # Try to get fields and convert recursively + result = {} + for field_name in obj.__dataclass_fields__: + value = getattr(obj, field_name) + result[field_name] = _convert_to_serializable(value) + return result + except Exception: + # Fallback to asdict if available + return asdict(obj) + elif isinstance(obj, dict): + return {k: _convert_to_serializable(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [_convert_to_serializable(item) for item in obj] + elif isinstance(obj, Path): + return str(obj) + else: + return obj + + def config_to_dict( config: Union[ROMAConfig, DictConfig], resolve: bool = True ) -> Dict[str, Any]: @@ -55,11 +89,12 @@ def config_to_dict( Returns: Plain dictionary representation """ - if isinstance(config, ROMAConfig): - # Convert Pydantic model to OmegaConf first - config = OmegaConf.structured(config) - - return OmegaConf.to_container(config, resolve=resolve) + if isinstance(config, DictConfig): + return OmegaConf.to_container(config, resolve=resolve) + + # For ROMAConfig and other Pydantic/dataclass objects, + # recursively convert to dict to handle nested Pydantic models + return _convert_to_serializable(config) def validate_config_file(config_path: Path) -> bool: diff --git a/tests/unit/test_execution_service.py b/tests/unit/test_execution_service.py index 48bc4879..1bc3fcf1 100644 --- a/tests/unit/test_execution_service.py +++ b/tests/unit/test_execution_service.py @@ -75,10 +75,11 @@ def mock_storage(self): def mock_config_manager(self): """Create mock config manager.""" from unittest.mock import MagicMock + from omegaconf import OmegaConf manager = MagicMock() - mock_config = MagicMock() - mock_config.model_dump.return_value = {"test": "config"} + # Use OmegaConf directly so config_to_dict can serialize it + mock_config = OmegaConf.create({"test": "config", "observability": None}) manager.load_config.return_value = mock_config return manager