Add Code category with individual examples for external CLI integrations

- Add Code category in Key Features table (like Workflows)
- Add individual examples for each external agent:
  - claude_code_example.py
  - gemini_cli_example.py
  - codex_cli_example.py
  - cursor_cli_example.py
  - external_agents_example.py (all integrations)
  - code_editing_example.py
- Link to individual docs pages for each integration
- Organize code-related features under Code category
This commit is contained in:
Mervin Praison committed 2025-12-19 11:07:25 +00:00
1 parent 3161389341
commit bc9579b497
12 files changed
+1785 -14

No files matched your search

+8 -3
View File
@@ -191,9 +191,14 @@ npm install praisonai
| 📚 Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| 🧠 Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| 📄 Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
| 💻 Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| ✏️ AI Code Editing Tools | [Example](#ai-code-editing) | [📖](https://docs.praison.ai/code/editing) |
| 🤖 External Agents (Claude/Gemini/Codex/Cursor) | [Example](#external-agents-cli) | [📖](https://docs.praison.ai/code/external-agents) |
| **💻 Code** | | |
| ↳ Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| ↳ AI Code Editing Tools | [Example](examples/python/code/code_editing_example.py) | [📖](https://docs.praison.ai/code/editing) |
| ↳ External Agents (All) | [Example](examples/python/code/external_agents_example.py) | [📖](https://docs.praison.ai/code/external-agents) |
| ↳ Claude Code CLI | [Example](examples/python/code/claude_code_example.py) | [📖](https://docs.praison.ai/code/claude-code) |
| ↳ Gemini CLI | [Example](examples/python/code/gemini_cli_example.py) | [📖](https://docs.praison.ai/code/gemini-cli) |
| ↳ Codex CLI | [Example](examples/python/code/codex_cli_example.py) | [📖](https://docs.praison.ai/code/codex-cli) |
| ↳ Cursor CLI | [Example](examples/python/code/cursor_cli_example.py) | [📖](https://docs.praison.ai/code/cursor-cli) |
| 📚 RAG Agents | [Example](examples/python/concepts/rag-agents.py) | [📖](https://docs.praison.ai/features/rag) |
| 🤔 Async & Parallel Processing | [Example](examples/python/general/async_example.py) | [📖](https://docs.praison.ai/features/async) |
| 🔢 Math Agents | [Example](examples/python/agents/math-agent.py) | [📖](https://docs.praison.ai/features/mathagent) |
@@ -0,0 +1,34 @@
"""
Claude Code CLI Integration Example
Demonstrates how to use Claude Code CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import ClaudeCodeIntegration
async def main():
# Create Claude Code integration
claude = ClaudeCodeIntegration(
workspace=".",
output_format="json",
skip_permissions=True,
system_prompt="Be concise and follow best practices",
allowed_tools=["Read", "Write", "Bash"]
)
# Check availability
print(f"Claude CLI available: {claude.is_available}")
print(f"Claude SDK available: {claude.sdk_available}")
# Execute a coding task
result = await claude.execute("List all Python files in the current directory")
print(f"Result: {result}")
# Stream output
print("\nStreaming output:")
async for event in claude.stream("Explain the project structure"):
print(event)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,60 @@
"""
Code Editing Tools Example
Demonstrates how to use PraisonAI's code editing tools with SEARCH/REPLACE diffs.
"""
from praisonai.code import (
set_workspace,
code_read_file,
code_write_file,
code_apply_diff,
code_list_files,
code_execute_command,
CODE_TOOLS
)
# Set workspace
set_workspace(".")
# List files
print("Files in current directory:")
files = code_list_files(".", recursive=False)
for f in files:
print(f" {f}")
# Read a file
print("\nReading example file:")
try:
content = code_read_file("README.md", start_line=1, end_line=10)
print(content)
except Exception as e:
print(f" File not found: {e}")
# Write a file
print("\nWriting test file:")
code_write_file("test_output.txt", "Hello from PraisonAI Code Tools!")
print(" Created test_output.txt")
# Apply a diff
print("\nApplying diff:")
diff = """
<<<<<<< SEARCH
Hello from PraisonAI Code Tools!
=======
Hello from PraisonAI Code Tools!
This line was added by a diff.
>>>>>>> REPLACE
"""
result = code_apply_diff("test_output.txt", diff)
print(f" {result}")
# Execute command
print("\nExecuting command:")
output = code_execute_command("cat test_output.txt")
print(f" {output}")
# Cleanup
import os
os.remove("test_output.txt")
print("\nCleanup complete.")
+36
View File
@@ -0,0 +1,36 @@
"""
Codex CLI Integration Example
Demonstrates how to use OpenAI Codex CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import CodexCLIIntegration
async def main():
# Create Codex CLI integration
codex = CodexCLIIntegration(
workspace=".",
full_auto=True,
sandbox="danger-full-access",
json_output=True
)
# Check availability
print(f"Codex CLI available: {codex.is_available}")
print(f"Full Auto: {codex.full_auto}")
print(f"Sandbox: {codex.sandbox}")
# Execute a coding task
result = await codex.execute("List files in the current directory")
print(f"Result: {result}")
# Stream JSON events
print("\nStreaming events:")
async for event in codex.stream("Explain the project"):
event_type = event.get("type", "")
if event_type == "item.completed":
print(f" {event}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,37 @@
"""
Cursor CLI Integration Example
Demonstrates how to use Cursor CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import CursorCLIIntegration
async def main():
# Create Cursor CLI integration
cursor = CursorCLIIntegration(
workspace=".",
output_format="json",
force=True,
model="gpt-5",
stream_partial=True
)
# Check availability
print(f"Cursor CLI available: {cursor.is_available}")
print(f"Force mode: {cursor.force}")
print(f"Model: {cursor.model}")
# Execute a coding task
result = await cursor.execute("List files in the current directory")
print(f"Result: {result}")
# Stream output
print("\nStreaming output:")
async for event in cursor.stream("Explain the project"):
content = event.get("content", "")
if content:
print(content, end="", flush=True)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,58 @@
"""
External Agents Integration Example
Demonstrates how to use all external CLI integrations as agent tools.
"""
import asyncio
from praisonai.integrations import (
ClaudeCodeIntegration,
GeminiCLIIntegration,
CodexCLIIntegration,
CursorCLIIntegration,
get_available_integrations
)
async def main():
# Check availability of all integrations
print("=== External CLI Integrations ===\n")
availability = get_available_integrations()
print("Availability:")
for name, avail in availability.items():
status = "✅" if avail else "❌"
print(f" {status} {name}")
# Create integrations
print("\n--- Claude Code ---")
claude = ClaudeCodeIntegration(workspace=".", skip_permissions=True)
print(f"CLI: {claude.cli_command}")
print(f"Available: {claude.is_available}")
tool = claude.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Gemini CLI ---")
gemini = GeminiCLIIntegration(workspace=".", model="gemini-2.5-flash")
print(f"CLI: {gemini.cli_command}")
print(f"Available: {gemini.is_available}")
tool = gemini.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Codex CLI ---")
codex = CodexCLIIntegration(workspace=".", full_auto=True)
print(f"CLI: {codex.cli_command}")
print(f"Available: {codex.is_available}")
tool = codex.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Cursor CLI ---")
cursor = CursorCLIIntegration(workspace=".", force=True)
print(f"CLI: {cursor.cli_command}")
print(f"Available: {cursor.is_available}")
tool = cursor.as_tool()
print(f"Tool: {tool.__name__}")
print("\n=== All integrations ready ===")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,33 @@
"""
Gemini CLI Integration Example
Demonstrates how to use Gemini CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import GeminiCLIIntegration
async def main():
# Create Gemini CLI integration
gemini = GeminiCLIIntegration(
workspace=".",
model="gemini-2.5-flash",
output_format="json",
include_directories=["../lib", "../docs"]
)
# Check availability
print(f"Gemini CLI available: {gemini.is_available}")
print(f"Model: {gemini.model}")
# Execute a coding task
result = await gemini.execute("Analyze the code structure")
print(f"Result: {result}")
# Execute with stats
result, stats = await gemini.execute_with_stats("Explain main.py")
print(f"Result: {result}")
print(f"Stats: {stats}")
if __name__ == "__main__":
asyncio.run(main())
+316 -10
View File
@@ -3,12 +3,45 @@ Router Handler for CLI.
Provides smart model selection based on task complexity.
Usage: praisonai "Complex task" --router
Supports custom model configuration from agents.yaml/workflow.yaml:
models:
gpt-4o-mini:
provider: openai
complexity: [simple, moderate]
cost_per_1k: 0.00075
capabilities: [text, function-calling]
"""
from typing import Any, Dict, Tuple
from typing import Any, Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import IntEnum
from .base import FlagHandler
class TaskComplexity(IntEnum):
"""Enum for task complexity levels."""
SIMPLE = 1
MODERATE = 2
COMPLEX = 3
VERY_COMPLEX = 4
@dataclass
class ModelProfile:
"""Profile for an LLM model with its characteristics."""
name: str
provider: str
complexity_range: Tuple[TaskComplexity, TaskComplexity]
cost_per_1k_tokens: float
capabilities: List[str]
context_window: int = 128000
supports_tools: bool = True
supports_streaming: bool = True
strengths: Optional[List[str]] = None
class RouterHandler(FlagHandler):
"""
Handler for --router flag.
@@ -20,13 +53,82 @@ class RouterHandler(FlagHandler):
praisonai "Complex analysis" --router
"""
# Model tiers for routing
MODEL_TIERS = {
'simple': ['gpt-4o-mini', 'claude-3-haiku', 'gemini-1.5-flash'],
'medium': ['gpt-4o', 'claude-3-sonnet', 'gemini-1.5-pro'],
'complex': ['gpt-4-turbo', 'claude-3-opus', 'o1-preview'],
# Default model tiers for routing (used when no custom models configured)
# Note: Use full model names for API compatibility
DEFAULT_MODEL_TIERS = {
'simple': ['gpt-4o-mini', 'anthropic/claude-3-haiku-20240307', 'gemini/gemini-1.5-flash'],
'medium': ['gpt-4o', 'anthropic/claude-3-5-sonnet-20241022', 'gemini/gemini-1.5-pro'],
'complex': ['gpt-4-turbo', 'anthropic/claude-3-opus-20240229', 'o1-preview'],
}
# Default model profiles
DEFAULT_MODELS: List[ModelProfile] = [
ModelProfile(
name="gpt-4o-mini",
provider="openai",
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
cost_per_1k_tokens=0.00075,
capabilities=["text", "function-calling"],
context_window=128000
),
ModelProfile(
name="gpt-4o",
provider="openai",
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
cost_per_1k_tokens=0.0075,
capabilities=["text", "vision", "function-calling"],
context_window=128000
),
ModelProfile(
name="anthropic/claude-3-haiku-20240307",
provider="anthropic",
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
cost_per_1k_tokens=0.0008,
capabilities=["text", "function-calling"],
context_window=200000
),
ModelProfile(
name="anthropic/claude-3-5-sonnet-20241022",
provider="anthropic",
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.VERY_COMPLEX),
cost_per_1k_tokens=0.009,
capabilities=["text", "vision", "function-calling"],
context_window=200000,
strengths=["reasoning", "code-generation", "analysis"]
),
ModelProfile(
name="gemini/gemini-1.5-flash",
provider="google",
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
cost_per_1k_tokens=0.000125,
capabilities=["text", "vision", "function-calling"],
context_window=1048576
),
ModelProfile(
name="gemini/gemini-1.5-pro",
provider="google",
complexity_range=(TaskComplexity.COMPLEX, TaskComplexity.VERY_COMPLEX),
cost_per_1k_tokens=0.00625,
capabilities=["text", "vision", "function-calling"],
context_window=2097152
),
]
def __init__(self, verbose: bool = False):
"""Initialize RouterHandler with model routing support."""
super().__init__(verbose=verbose)
self._model_router: Optional[List[ModelProfile]] = None
self._custom_models: Dict[str, ModelProfile] = {}
self._cost_threshold: Optional[float] = None
self._model_tiers = self.DEFAULT_MODEL_TIERS.copy()
@property
def model_router(self) -> List[ModelProfile]:
"""Get the list of available model profiles."""
if self._model_router is None:
return self.DEFAULT_MODELS.copy()
return self._model_router
@property
def feature_name(self) -> str:
return "router"
@@ -94,17 +196,29 @@ class RouterHandler(FlagHandler):
Selected model name
"""
complexity = self.analyze_complexity(prompt)
models = self.MODEL_TIERS.get(complexity, self.MODEL_TIERS['medium'])
# If custom models are loaded, use ModelProfile-based selection
if self._custom_models:
return self._select_from_profiles(prompt, complexity, preferred_provider)
# Otherwise use tier-based selection
models = self._model_tiers.get(complexity, self._model_tiers['medium'])
# Filter by provider if specified
if preferred_provider:
provider_prefixes = {
'openai': ['gpt-', 'o1-'],
'anthropic': ['claude-'],
'google': ['gemini-']
'anthropic': ['anthropic/', 'claude-'],
'google': ['gemini/', 'gemini-']
}
prefixes = provider_prefixes.get(preferred_provider.lower(), [])
filtered = [m for m in models if any(m.startswith(p) for p in prefixes)]
filtered = [m for m in models if any(p in m for p in prefixes)]
if filtered:
models = filtered
# Apply cost threshold if set
if self._cost_threshold:
filtered = [m for m in models if self.get_model_cost(m) <= self._cost_threshold]
if filtered:
models = filtered
@@ -115,6 +229,48 @@ class RouterHandler(FlagHandler):
return selected
def _select_from_profiles(
self,
prompt: str,
complexity: str,
preferred_provider: str = None
) -> str:
"""Select model from custom ModelProfile objects."""
# Map string complexity to TaskComplexity enum
complexity_map = {
'simple': TaskComplexity.SIMPLE,
'medium': TaskComplexity.MODERATE,
'complex': TaskComplexity.COMPLEX,
}
task_complexity = complexity_map.get(complexity, TaskComplexity.MODERATE)
# Get all models that support this complexity level
candidates = []
for name, profile in self._custom_models.items():
min_c, max_c = profile.complexity_range
if min_c.value <= task_complexity.value <= max_c.value:
candidates.append(profile)
if not candidates:
return 'gpt-4o-mini'
# Filter by provider if specified
if preferred_provider:
filtered = [m for m in candidates if m.provider == preferred_provider]
if filtered:
candidates = filtered
# Filter by cost threshold if set
if self._cost_threshold:
filtered = [m for m in candidates if m.cost_per_1k_tokens <= self._cost_threshold]
if filtered:
candidates = filtered
# Sort by cost (cheapest first)
candidates.sort(key=lambda m: m.cost_per_1k_tokens)
return candidates[0].name if candidates else 'gpt-4o-mini'
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply router configuration.
@@ -158,3 +314,153 @@ class RouterHandler(FlagHandler):
return 'gpt-4o-mini' # Default
return self.select_model(prompt, provider)
# =========================================================================
# Custom Model Configuration Methods
# =========================================================================
def yaml_to_model_profile(self, name: str, config: Dict[str, Any]) -> ModelProfile:
"""
Convert YAML model configuration to ModelProfile object.
Args:
name: Model name
config: Model configuration from YAML
Returns:
ModelProfile object
"""
# Parse complexity strings to TaskComplexity enum
complexity_list = config.get('complexity', ['moderate'])
complexity_map = {
'simple': TaskComplexity.SIMPLE,
'moderate': TaskComplexity.MODERATE,
'complex': TaskComplexity.COMPLEX,
'very_complex': TaskComplexity.VERY_COMPLEX,
}
complexities = [complexity_map.get(c.lower(), TaskComplexity.MODERATE) for c in complexity_list]
min_complexity = min(complexities, key=lambda x: x.value)
max_complexity = max(complexities, key=lambda x: x.value)
return ModelProfile(
name=name,
provider=config.get('provider', 'unknown'),
complexity_range=(min_complexity, max_complexity),
cost_per_1k_tokens=config.get('cost_per_1k', 0.001),
capabilities=config.get('capabilities', ['text']),
context_window=config.get('context_window', 128000),
supports_tools=config.get('supports_tools', True),
supports_streaming=config.get('supports_streaming', True),
strengths=config.get('strengths', None)
)
def load_models_from_config(
self,
models_config: Dict[str, Dict[str, Any]],
merge_with_defaults: bool = False
) -> None:
"""
Load custom models from YAML configuration.
Args:
models_config: Dictionary of model configurations from YAML
merge_with_defaults: If True, merge with default models
"""
# Start with defaults if merging
if merge_with_defaults:
for profile in self.DEFAULT_MODELS:
self._custom_models[profile.name] = profile
# Add custom models
for name, config in models_config.items():
profile = self.yaml_to_model_profile(name, config)
self._custom_models[name] = profile
# Rebuild model tiers from profiles
self._rebuild_model_tiers()
def _rebuild_model_tiers(self) -> None:
"""Rebuild MODEL_TIERS from custom model profiles."""
self._model_tiers = {
'simple': [],
'medium': [],
'complex': [],
}
for name, profile in self._custom_models.items():
min_c, max_c = profile.complexity_range
if min_c.value <= TaskComplexity.SIMPLE.value <= max_c.value:
self._model_tiers['simple'].append(name)
if min_c.value <= TaskComplexity.MODERATE.value <= max_c.value:
self._model_tiers['medium'].append(name)
if min_c.value <= TaskComplexity.COMPLEX.value <= max_c.value:
self._model_tiers['complex'].append(name)
# Sort by cost (cheapest first)
for tier in self._model_tiers:
self._model_tiers[tier].sort(
key=lambda n: self._custom_models[n].cost_per_1k_tokens
)
def get_available_models(self) -> List[str]:
"""
Get list of available model names.
Returns:
List of model names
"""
if self._custom_models:
return list(self._custom_models.keys())
return [p.name for p in self.DEFAULT_MODELS]
def get_model_cost(self, model_name: str) -> float:
"""
Get cost per 1k tokens for a model.
Args:
model_name: Name of the model
Returns:
Cost per 1k tokens
"""
# Check custom models first
if model_name in self._custom_models:
return self._custom_models[model_name].cost_per_1k_tokens
# Check default models
for profile in self.DEFAULT_MODELS:
if profile.name == model_name:
return profile.cost_per_1k_tokens
# Default cost for unknown models
return 0.01
def set_cost_threshold(self, threshold: float) -> None:
"""
Set maximum cost per 1k tokens for model selection.
Args:
threshold: Maximum cost per 1k tokens
"""
self._cost_threshold = threshold
def get_model_profile(self, model_name: str) -> Optional[ModelProfile]:
"""
Get ModelProfile for a model.
Args:
model_name: Name of the model
Returns:
ModelProfile or None if not found
"""
if model_name in self._custom_models:
return self._custom_models[model_name]
for profile in self.DEFAULT_MODELS:
if profile.name == model_name:
return profile
return None
@@ -199,6 +199,73 @@ steps:
repeat:
until: "approved"
max_iterations: 3
""",
"model-routing": """# Model Routing Workflow
name: Cost-Optimized Model Routing Workflow
description: Uses custom models with automatic routing based on task complexity
# Custom models configuration - define your own models with costs and capabilities
models:
cheap-fast:
provider: openai
complexity: [simple]
cost_per_1k: 0.0001
capabilities: [text]
context_window: 16000
balanced:
provider: openai
complexity: [moderate]
cost_per_1k: 0.001
capabilities: [text, function-calling]
context_window: 128000
premium:
provider: anthropic
complexity: [complex, very_complex]
cost_per_1k: 0.015
capabilities: [text, vision, function-calling]
context_window: 200000
strengths: [reasoning, analysis, code-generation]
workflow:
verbose: true
router: true # Enable model routing
routing_strategy: cost-optimized # Options: auto, cost-optimized, performance-optimized
agents:
classifier:
name: Classifier
role: Request Classifier
goal: Classify incoming requests by complexity
instructions: "Classify the request as 'simple', 'moderate', or 'complex'."
llm: cheap-fast # Always use cheap model for classification
researcher:
name: Researcher
role: Research Analyst
goal: Research topics thoroughly
instructions: "Research the topic and provide detailed findings."
llm_routing: auto # Auto-select based on task complexity
llm_models: [balanced, premium] # Models to choose from
writer:
name: Writer
role: Content Writer
goal: Write high-quality content
instructions: "Write clear, engaging content based on research."
llm: premium # Always use premium for quality writing
steps:
- agent: classifier
action: "Classify complexity of: {{input}}"
- name: routing
route:
simple: [researcher]
moderate: [researcher]
complex: [researcher, writer]
default: [researcher]
"""
}
@@ -224,7 +291,7 @@ Workflow Commands:
praisonai workflow create --template <name> - Create from template
praisonai workflow auto "topic" --pattern <pattern> - Auto-generate workflow
Templates: simple, routing, parallel, loop, evaluator-optimizer
Templates: simple, routing, parallel, loop, evaluator-optimizer, model-routing
Patterns: sequential, routing, parallel
Example:
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python
"""
Live test for model routing with real API calls.
Run this script directly to test with your API keys.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from praisonai.cli.features.router import RouterHandler
def test_router_basics():
"""Test basic router functionality."""
print("\n" + "="*60)
print("TEST 1: Router Basics")
print("="*60)
handler = RouterHandler(verbose=True)
# Test complexity analysis
print("\n📊 Complexity Analysis:")
prompts = [
("What is 2+2?", "simple"),
("Define AI", "simple"),
("Analyze and research the comprehensive impact of AI on healthcare", "complex"),
]
for prompt, expected in prompts:
result = handler.analyze_complexity(prompt)
status = "✅" if result == expected else "❌"
print(f" {status} '{prompt[:40]}...' -> {result} (expected: {expected})")
print("\n✅ Router basics test passed!")
def test_model_selection():
"""Test model selection with different providers."""
print("\n" + "="*60)
print("TEST 2: Model Selection by Provider")
print("="*60)
handler = RouterHandler(verbose=True)
providers = ['openai', 'anthropic', 'google']
for provider in providers:
model = handler.select_model("Simple question", preferred_provider=provider)
print(f" {provider}: {model}")
print("\n✅ Model selection test passed!")
def test_custom_models():
"""Test custom model configuration."""
print("\n" + "="*60)
print("TEST 3: Custom Models from YAML Config")
print("="*60)
handler = RouterHandler(verbose=True)
# Simulate YAML config
models_config = {
'cheap-model': {
'provider': 'openai',
'complexity': ['simple'],
'cost_per_1k': 0.0001,
'capabilities': ['text']
},
'premium-model': {
'provider': 'anthropic',
'complexity': ['complex', 'very_complex'],
'cost_per_1k': 0.015,
'capabilities': ['text', 'vision']
}
}
handler.load_models_from_config(models_config)
print(f"\n Available models: {handler.get_available_models()}")
simple_model = handler.select_model("What is 2+2?")
print(f" Simple task -> {simple_model}")
assert simple_model == 'cheap-model', f"Expected cheap-model, got {simple_model}"
complex_model = handler.select_model("Analyze and research comprehensive AI impact")
print(f" Complex task -> {complex_model}")
assert complex_model == 'premium-model', f"Expected premium-model, got {complex_model}"
print("\n✅ Custom models test passed!")
def test_real_openai():
"""Test real OpenAI API call."""
print("\n" + "="*60)
print("TEST 4: Real OpenAI API Call")
print("="*60)
api_key = os.getenv('OPENAI_API_KEY')
if not api_key or api_key == 'test-key':
print(" ⚠️ OPENAI_API_KEY not set, skipping")
return
print(f" API Key: {api_key[:20]}...")
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
model = handler.select_model("What is 2+2?", preferred_provider='openai')
print(f" Selected model: {model}")
agent = Agent(
name="MathAgent",
role="Calculator",
goal="Answer math questions",
llm=model,
verbose=False
)
result = agent.start("What is 2+2? Reply with just the number.")
print(f" Result: {result}")
if result and '4' in str(result):
print("\n✅ OpenAI API test passed!")
else:
print("\n❌ OpenAI API test failed!")
def test_real_anthropic():
"""Test real Anthropic API call."""
print("\n" + "="*60)
print("TEST 5: Real Anthropic API Call")
print("="*60)
api_key = os.getenv('ANTHROPIC_API_KEY')
if not api_key or api_key == 'test-key':
print(" ⚠️ ANTHROPIC_API_KEY not set, skipping")
return
print(f" API Key: {api_key[:20]}...")
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
model = handler.select_model("What is the capital of France?", preferred_provider='anthropic')
print(f" Selected model: {model}")
# Model now includes provider prefix (anthropic/...)
print(f" Using LLM: {model}")
agent = Agent(
name="GeoAgent",
role="Geography Expert",
goal="Answer geography questions",
llm=model,
verbose=False
)
result = agent.start("What is the capital of France? Reply with just the city name.")
print(f" Result: {result}")
if result and 'paris' in str(result).lower():
print("\n✅ Anthropic API test passed!")
else:
print("\n❌ Anthropic API test failed!")
def test_real_gemini():
"""Test real Gemini API call."""
print("\n" + "="*60)
print("TEST 6: Real Gemini API Call")
print("="*60)
api_key = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
if not api_key or api_key == 'test-key':
print(" ⚠️ GEMINI_API_KEY/GOOGLE_API_KEY not set, skipping")
return
print(f" API Key: {api_key[:20]}...")
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
model = handler.select_model("What is 3 times 7?", preferred_provider='google')
print(f" Selected model: {model}")
# Model should already have gemini/ prefix now
llm_model = model
print(f" Using LLM: {llm_model}")
agent = Agent(
name="MathAgent",
role="Calculator",
goal="Answer math questions",
llm=llm_model,
verbose=False
)
result = agent.start("What is 3 times 7? Reply with just the number.")
print(f" Result: {result}")
if result and '21' in str(result):
print("\n✅ Gemini API test passed!")
else:
print("\n❌ Gemini API test failed!")
if __name__ == '__main__':
print("\n" + "="*60)
print("MODEL ROUTING LIVE TESTS")
print("="*60)
# Check environment variables
print("\n🔑 Environment Variables:")
print(f" OPENAI_API_KEY: {'✅ Set' if os.getenv('OPENAI_API_KEY') else '❌ Not set'}")
print(f" ANTHROPIC_API_KEY: {'✅ Set' if os.getenv('ANTHROPIC_API_KEY') else '❌ Not set'}")
print(f" GEMINI_API_KEY: {'✅ Set' if os.getenv('GEMINI_API_KEY') else '❌ Not set'}")
# Run tests
test_router_basics()
test_model_selection()
test_custom_models()
test_real_openai()
test_real_anthropic()
test_real_gemini()
print("\n" + "="*60)
print("ALL TESTS COMPLETED")
print("="*60 + "\n")
@@ -0,0 +1,324 @@
"""
Real integration tests for Model Routing with actual API calls.
Tests the RouterHandler with real LLM providers.
Requires API keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY
"""
import pytest
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from praisonai.cli.features.router import RouterHandler
class TestRouterHandlerReal:
"""Real integration tests for RouterHandler."""
def test_router_handler_initialization(self):
"""Test RouterHandler initializes correctly."""
handler = RouterHandler(verbose=True)
assert handler is not None
assert handler.model_router is not None
assert len(handler.model_router) > 0
print(f"✅ RouterHandler initialized with {len(handler.model_router)} default models")
def test_complexity_analysis(self):
"""Test complexity analysis for different prompts."""
handler = RouterHandler()
# Simple prompts
simple_prompts = [
"What is 2+2?",
"Define photosynthesis",
"List the planets",
]
for prompt in simple_prompts:
complexity = handler.analyze_complexity(prompt)
print(f" '{prompt[:30]}...' -> {complexity}")
assert complexity == 'simple', f"Expected 'simple' for: {prompt}"
print("✅ Simple prompts correctly classified")
# Complex prompts
complex_prompts = [
"Analyze and research the comprehensive impact of AI on healthcare systems globally",
"Compare and evaluate different machine learning architectures for natural language processing",
]
for prompt in complex_prompts:
complexity = handler.analyze_complexity(prompt)
print(f" '{prompt[:40]}...' -> {complexity}")
assert complexity == 'complex', f"Expected 'complex' for: {prompt}"
print("✅ Complex prompts correctly classified")
def test_model_selection_by_complexity(self):
"""Test model selection based on complexity."""
handler = RouterHandler(verbose=True)
# Simple task - should select cheap model
simple_model = handler.select_model("What is the capital of France?")
print(f"Simple task model: {simple_model}")
assert simple_model in ['gpt-4o-mini', 'claude-3-haiku', 'gemini-1.5-flash']
# Complex task - should select powerful model
complex_model = handler.select_model(
"Analyze and research the comprehensive impact of quantum computing on cybersecurity"
)
print(f"Complex task model: {complex_model}")
assert complex_model in ['gpt-4-turbo', 'claude-3-opus', 'o1-preview']
print("✅ Model selection by complexity works correctly")
def test_model_selection_with_provider_preference(self):
"""Test model selection with provider preference."""
handler = RouterHandler(verbose=True)
# Prefer OpenAI
openai_model = handler.select_model(
"Write a simple greeting",
preferred_provider='openai'
)
print(f"OpenAI preferred: {openai_model}")
assert 'gpt' in openai_model.lower() or 'o1' in openai_model.lower()
# Prefer Anthropic
anthropic_model = handler.select_model(
"Write a simple greeting",
preferred_provider='anthropic'
)
print(f"Anthropic preferred: {anthropic_model}")
assert 'claude' in anthropic_model.lower()
# Prefer Google
google_model = handler.select_model(
"Write a simple greeting",
preferred_provider='google'
)
print(f"Google preferred: {google_model}")
assert 'gemini' in google_model.lower()
print("✅ Provider preference works correctly")
def test_custom_models_from_yaml(self):
"""Test loading custom models from YAML-like config."""
handler = RouterHandler(verbose=True)
# Define custom models (as would be in YAML)
custom_models = {
'my-cheap-model': {
'provider': 'openai',
'complexity': ['simple'],
'cost_per_1k': 0.0001,
'capabilities': ['text']
},
'my-balanced-model': {
'provider': 'openai',
'complexity': ['moderate'],
'cost_per_1k': 0.001,
'capabilities': ['text', 'function-calling']
},
'my-premium-model': {
'provider': 'anthropic',
'complexity': ['complex', 'very_complex'],
'cost_per_1k': 0.015,
'capabilities': ['text', 'vision', 'function-calling']
}
}
# Load custom models
handler.load_models_from_config(custom_models)
# Verify models are loaded
available = handler.get_available_models()
print(f"Available models: {available}")
assert 'my-cheap-model' in available
assert 'my-balanced-model' in available
assert 'my-premium-model' in available
# Test selection with custom models
simple_model = handler.select_model("What is 2+2?")
print(f"Simple task -> {simple_model}")
assert simple_model == 'my-cheap-model'
complex_model = handler.select_model(
"Analyze and research the comprehensive impact of AI"
)
print(f"Complex task -> {complex_model}")
assert complex_model == 'my-premium-model'
print("✅ Custom models from YAML config work correctly")
def test_cost_threshold(self):
"""Test cost threshold filtering."""
handler = RouterHandler(verbose=True)
# Load models with different costs
custom_models = {
'cheap': {
'provider': 'openai',
'complexity': ['simple', 'moderate', 'complex'],
'cost_per_1k': 0.0001,
'capabilities': ['text']
},
'expensive': {
'provider': 'anthropic',
'complexity': ['simple', 'moderate', 'complex'],
'cost_per_1k': 0.05,
'capabilities': ['text']
}
}
handler.load_models_from_config(custom_models)
# Set cost threshold
handler.set_cost_threshold(0.001)
# Should only select cheap model
model = handler.select_model("Any task here")
print(f"With cost threshold 0.001: {model}")
assert model == 'cheap'
# Verify cost
cost = handler.get_model_cost(model)
assert cost <= 0.001
print("✅ Cost threshold filtering works correctly")
def test_merge_with_defaults(self):
"""Test merging custom models with defaults."""
handler = RouterHandler(verbose=True)
custom_models = {
'my-custom-model': {
'provider': 'custom',
'complexity': ['moderate'],
'cost_per_1k': 0.002,
'capabilities': ['text']
}
}
handler.load_models_from_config(custom_models, merge_with_defaults=True)
available = handler.get_available_models()
print(f"Available models (merged): {available}")
# Should have both custom and default models
assert 'my-custom-model' in available
assert 'gpt-4o-mini' in available
print("✅ Merge with defaults works correctly")
class TestRealAPIIntegration:
"""Tests that make real API calls - requires API keys."""
@pytest.mark.skipif(
not os.getenv('OPENAI_API_KEY'),
reason="OPENAI_API_KEY not set"
)
def test_real_openai_call(self):
"""Test real OpenAI API call with routed model."""
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
# Select model for simple task
model = handler.select_model(
"What is 2+2?",
preferred_provider='openai'
)
print(f"Selected model: {model}")
# Create agent with selected model
agent = Agent(
name="TestAgent",
role="Calculator",
goal="Answer simple math questions",
llm=model,
verbose=True
)
# Run simple task
result = agent.start("What is 2+2? Answer with just the number.")
print(f"Result: {result}")
assert '4' in str(result)
print("✅ Real OpenAI API call successful")
@pytest.mark.skipif(
not os.getenv('ANTHROPIC_API_KEY'),
reason="ANTHROPIC_API_KEY not set"
)
def test_real_anthropic_call(self):
"""Test real Anthropic API call with routed model."""
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
# Select Anthropic model
model = handler.select_model(
"What is the capital of France?",
preferred_provider='anthropic'
)
print(f"Selected model: {model}")
# Create agent with selected model
agent = Agent(
name="TestAgent",
role="Geography Expert",
goal="Answer geography questions",
llm=model,
verbose=True
)
# Run task
result = agent.start("What is the capital of France? Answer with just the city name.")
print(f"Result: {result}")
assert 'paris' in str(result).lower()
print("✅ Real Anthropic API call successful")
@pytest.mark.skipif(
not os.getenv('GEMINI_API_KEY'),
reason="GEMINI_API_KEY not set"
)
def test_real_gemini_call(self):
"""Test real Gemini API call with routed model."""
from praisonaiagents import Agent
handler = RouterHandler(verbose=True)
# Select Google model
model = handler.select_model(
"What is 3 times 7?",
preferred_provider='google'
)
print(f"Selected model: {model}")
# Create agent with selected model
agent = Agent(
name="TestAgent",
role="Calculator",
goal="Answer math questions",
llm=f"gemini/{model}" if not model.startswith("gemini/") else model,
verbose=True
)
# Run task
result = agent.start("What is 3 times 7? Answer with just the number.")
print(f"Result: {result}")
assert '21' in str(result)
print("✅ Real Gemini API call successful")
if __name__ == '__main__':
# Run tests
pytest.main([__file__, '-v', '-s'])
@@ -0,0 +1,580 @@
"""
Unit tests for Model Routing features.
Tests cover:
1. Model config parsing from agents.yaml/workflow.yaml
2. Per-agent LLM field support
3. Unified CLI Router with ModelRouter
4. Custom model profiles
TDD approach - tests written before implementation.
"""
import pytest
import yaml
import os
import sys
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
# =============================================================================
# Feature 1: Model Config in YAML
# =============================================================================
class TestModelConfigParsing:
"""Tests for parsing 'models' section from agents.yaml/workflow.yaml."""
def test_parse_models_section_basic(self):
"""Test parsing basic models configuration from YAML."""
yaml_content = """
name: Test Workflow
description: Test workflow with custom models
models:
gpt-4o-mini:
provider: openai
complexity: [simple, moderate]
cost_per_1k: 0.00075
capabilities: [text, function-calling]
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
steps:
- agent: researcher
action: "Research: {{input}}"
"""
config = yaml.safe_load(yaml_content)
# Verify models section exists
assert 'models' in config
assert 'gpt-4o-mini' in config['models']
model_config = config['models']['gpt-4o-mini']
assert model_config['provider'] == 'openai'
assert model_config['complexity'] == ['simple', 'moderate']
assert model_config['cost_per_1k'] == 0.00075
assert 'text' in model_config['capabilities']
def test_parse_multiple_models(self):
"""Test parsing multiple model configurations."""
yaml_content = """
models:
gpt-4o-mini:
provider: openai
complexity: [simple, moderate]
cost_per_1k: 0.00075
capabilities: [text, function-calling]
claude-3-5-sonnet:
provider: anthropic
complexity: [moderate, complex, very_complex]
cost_per_1k: 0.009
capabilities: [text, vision, function-calling]
custom-openrouter-model:
provider: openrouter
complexity: [moderate, complex]
cost_per_1k: 0.001
capabilities: [text]
context_window: 32000
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
"""
config = yaml.safe_load(yaml_content)
assert len(config['models']) == 3
assert 'gpt-4o-mini' in config['models']
assert 'claude-3-5-sonnet' in config['models']
assert 'custom-openrouter-model' in config['models']
# Verify custom model has context_window
custom_model = config['models']['custom-openrouter-model']
assert custom_model.get('context_window') == 32000
def test_parse_model_with_all_fields(self):
"""Test parsing model with all optional fields."""
yaml_content = """
models:
full-model:
provider: openai
complexity: [simple, moderate, complex]
cost_per_1k: 0.005
capabilities: [text, vision, function-calling, streaming]
context_window: 128000
supports_tools: true
supports_streaming: true
strengths: [reasoning, code-generation, analysis]
"""
config = yaml.safe_load(yaml_content)
model = config['models']['full-model']
assert model['provider'] == 'openai'
assert model['context_window'] == 128000
assert model['supports_tools'] is True
assert model['supports_streaming'] is True
assert 'reasoning' in model['strengths']
def test_models_section_optional(self):
"""Test that models section is optional - backward compatibility."""
yaml_content = """
name: Simple Workflow
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
steps:
- agent: researcher
action: "Research: {{input}}"
"""
config = yaml.safe_load(yaml_content)
# models section should not exist
assert 'models' not in config or config.get('models') is None
# But agents should still work
assert 'agents' in config
class TestModelConfigToModelProfile:
"""Tests for converting YAML model config to ModelProfile objects."""
def test_convert_yaml_to_model_profile(self):
"""Test converting YAML model config to ModelProfile."""
from praisonai.cli.features.router import RouterHandler
yaml_model_config = {
'provider': 'openai',
'complexity': ['simple', 'moderate'],
'cost_per_1k': 0.00075,
'capabilities': ['text', 'function-calling'],
'context_window': 128000,
'supports_tools': True,
'strengths': ['speed', 'cost-effective']
}
handler = RouterHandler()
profile = handler.yaml_to_model_profile('gpt-4o-mini', yaml_model_config)
assert profile is not None
assert profile.name == 'gpt-4o-mini'
assert profile.provider == 'openai'
assert profile.cost_per_1k_tokens == 0.00075
assert profile.context_window == 128000
def test_convert_complexity_strings_to_enum(self):
"""Test converting complexity strings to TaskComplexity enum."""
from praisonai.cli.features.router import RouterHandler
yaml_model_config = {
'provider': 'anthropic',
'complexity': ['moderate', 'complex', 'very_complex'],
'cost_per_1k': 0.009,
'capabilities': ['text']
}
handler = RouterHandler()
profile = handler.yaml_to_model_profile('claude-3-5-sonnet', yaml_model_config)
# Complexity range should be (MODERATE, VERY_COMPLEX)
min_complexity, max_complexity = profile.complexity_range
assert min_complexity.value == 2 # MODERATE
assert max_complexity.value == 4 # VERY_COMPLEX
class TestRouterWithCustomModels:
"""Tests for RouterHandler using custom models from YAML."""
def test_router_loads_custom_models(self):
"""Test that router loads custom models from YAML config."""
from praisonai.cli.features.router import RouterHandler
models_config = {
'my-custom-model': {
'provider': 'openrouter',
'complexity': ['simple', 'moderate'],
'cost_per_1k': 0.001,
'capabilities': ['text', 'function-calling']
}
}
handler = RouterHandler()
handler.load_models_from_config(models_config)
# Custom model should be available
assert 'my-custom-model' in handler.get_available_models()
def test_router_selects_custom_model(self):
"""Test that router can select custom model based on complexity."""
from praisonai.cli.features.router import RouterHandler
models_config = {
'cheap-model': {
'provider': 'custom',
'complexity': ['simple'],
'cost_per_1k': 0.0001,
'capabilities': ['text']
},
'expensive-model': {
'provider': 'custom',
'complexity': ['complex', 'very_complex'],
'cost_per_1k': 0.05,
'capabilities': ['text', 'function-calling']
}
}
handler = RouterHandler()
handler.load_models_from_config(models_config)
# Simple task should select cheap model
simple_model = handler.select_model("What is 2+2?")
assert simple_model == 'cheap-model'
# Complex task should select expensive model
complex_model = handler.select_model(
"Analyze the comprehensive impact of quantum computing on cybersecurity"
)
assert complex_model == 'expensive-model'
def test_router_merges_with_default_models(self):
"""Test that custom models merge with default models."""
from praisonai.cli.features.router import RouterHandler
models_config = {
'my-custom-model': {
'provider': 'openrouter',
'complexity': ['moderate'],
'cost_per_1k': 0.002,
'capabilities': ['text']
}
}
handler = RouterHandler()
handler.load_models_from_config(models_config, merge_with_defaults=True)
available = handler.get_available_models()
# Should have both custom and default models
assert 'my-custom-model' in available
assert 'gpt-4o-mini' in available # Default model
# =============================================================================
# Feature 2: Per-Agent LLM Field
# =============================================================================
class TestPerAgentLLM:
"""Tests for per-agent LLM configuration in YAML."""
def test_parse_agent_with_llm_field(self):
"""Test parsing agent with explicit llm field."""
yaml_content = """
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
llm: gpt-4o
writer:
name: Writer
role: Content Writer
goal: Write content
llm: claude-3-5-sonnet-20241022
"""
config = yaml.safe_load(yaml_content)
assert config['agents']['researcher']['llm'] == 'gpt-4o'
assert config['agents']['writer']['llm'] == 'claude-3-5-sonnet-20241022'
def test_parse_agent_with_llm_routing(self):
"""Test parsing agent with llm_routing strategy."""
yaml_content = """
agents:
smart_agent:
name: Smart Agent
role: Adaptive Assistant
goal: Handle various tasks
llm_routing: auto
llm_models:
- gpt-4o-mini
- gpt-4o
- claude-3-5-sonnet-20241022
"""
config = yaml.safe_load(yaml_content)
agent = config['agents']['smart_agent']
assert agent['llm_routing'] == 'auto'
assert len(agent['llm_models']) == 3
def test_agent_llm_defaults_when_not_specified(self):
"""Test that agent uses default LLM when not specified."""
yaml_content = """
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
# No llm field - should use default
"""
config = yaml.safe_load(yaml_content)
# llm field should not exist
assert 'llm' not in config['agents']['researcher']
def test_agent_llm_with_provider_prefix(self):
"""Test parsing agent LLM with provider/model format."""
yaml_content = """
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
llm: openrouter/anthropic/claude-3-5-sonnet
"""
config = yaml.safe_load(yaml_content)
assert config['agents']['researcher']['llm'] == 'openrouter/anthropic/claude-3-5-sonnet'
class TestAgentCreationWithLLM:
"""Tests for creating Agent objects with LLM from YAML config."""
def test_create_agent_with_custom_llm(self):
"""Test that Agent config with custom LLM is properly parsed."""
agent_config = {
'name': 'Researcher',
'role': 'Research Analyst',
'goal': 'Research topics',
'llm': 'gpt-4o'
}
# This tests the parsing logic, not actual agent creation
llm_value = agent_config.get('llm', 'gpt-4o-mini')
assert llm_value == 'gpt-4o'
def test_agent_config_with_routing_strategy(self):
"""Test agent config with routing strategy is properly structured."""
agent_config = {
'name': 'Smart Agent',
'role': 'Adaptive Assistant',
'goal': 'Handle various tasks',
'llm_routing': 'cost-optimized',
'llm_models': ['gpt-4o-mini', 'gpt-4o', 'claude-3-5-sonnet-20241022']
}
# Verify structure
assert agent_config['llm_routing'] == 'cost-optimized'
assert isinstance(agent_config['llm_models'], list)
assert len(agent_config['llm_models']) == 3
# =============================================================================
# Feature 3: Unified CLI Router with ModelRouter
# =============================================================================
class TestUnifiedRouter:
"""Tests for unified RouterHandler that uses ModelRouter internally."""
def test_router_handler_uses_model_router(self):
"""Test that RouterHandler uses ModelRouter for selection."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
# Should have internal model_router
assert hasattr(handler, 'model_router') or hasattr(handler, '_model_router')
def test_router_complexity_analysis_matches(self):
"""Test that complexity analysis is consistent."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
# Simple prompts
assert handler.analyze_complexity("What is 2+2?") == 'simple'
assert handler.analyze_complexity("Define photosynthesis") == 'simple'
# Complex prompts
assert handler.analyze_complexity(
"Analyze and research the comprehensive impact of AI on healthcare"
) == 'complex'
def test_router_cost_threshold(self):
"""Test router respects cost threshold."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
handler.set_cost_threshold(0.001) # Max $0.001 per 1k tokens
# Should only select cheap models
model = handler.select_model("Complex analysis task")
# Model should be within cost threshold
model_cost = handler.get_model_cost(model)
assert model_cost <= 0.001
def test_router_provider_preference(self):
"""Test router respects provider preference."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
# Prefer Anthropic
model = handler.select_model(
"Write a detailed analysis",
preferred_provider='anthropic'
)
assert 'claude' in model.lower()
class TestRouterIntegrationWithWorkflow:
"""Tests for router integration with workflow execution."""
def test_workflow_uses_models_config(self):
"""Test that workflow uses models config for routing."""
yaml_content = """
name: Routing Workflow
models:
fast-model:
provider: openai
complexity: [simple]
cost_per_1k: 0.0001
capabilities: [text]
smart-model:
provider: anthropic
complexity: [complex, very_complex]
cost_per_1k: 0.01
capabilities: [text, function-calling]
workflow:
verbose: true
router: true # Enable model routing
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
llm_routing: auto # Use auto-routing with models config
"""
config = yaml.safe_load(yaml_content)
# Verify workflow has router enabled
assert config['workflow'].get('router') is True
# Verify agent has routing enabled
assert config['agents']['researcher']['llm_routing'] == 'auto'
# Verify models are defined
assert 'fast-model' in config['models']
assert 'smart-model' in config['models']
# =============================================================================
# Integration Tests
# =============================================================================
class TestEndToEndModelRouting:
"""End-to-end tests for model routing feature."""
def test_full_yaml_with_model_routing(self):
"""Test complete YAML with all model routing features."""
yaml_content = """
name: Full Model Routing Workflow
description: Demonstrates all model routing features
# Custom models configuration
models:
cheap-fast:
provider: openai
complexity: [simple]
cost_per_1k: 0.0001
capabilities: [text]
context_window: 16000
balanced:
provider: openai
complexity: [moderate]
cost_per_1k: 0.001
capabilities: [text, function-calling]
context_window: 128000
premium:
provider: anthropic
complexity: [complex, very_complex]
cost_per_1k: 0.015
capabilities: [text, vision, function-calling]
context_window: 200000
strengths: [reasoning, analysis, code-generation]
workflow:
verbose: true
router: true
routing_strategy: cost-optimized
agents:
classifier:
name: Classifier
role: Request Classifier
goal: Classify incoming requests
llm: cheap-fast # Always use cheap model for classification
researcher:
name: Researcher
role: Research Analyst
goal: Research topics thoroughly
llm_routing: auto # Auto-select based on task
llm_models: [balanced, premium]
writer:
name: Writer
role: Content Writer
goal: Write high-quality content
llm: premium # Always use premium for writing
steps:
- agent: classifier
action: "Classify: {{input}}"
- name: routing
route:
simple: [researcher]
complex: [researcher, writer]
default: [researcher]
"""
config = yaml.safe_load(yaml_content)
# Verify complete structure
assert 'models' in config
assert len(config['models']) == 3
assert 'workflow' in config
assert config['workflow']['router'] is True
assert config['workflow']['routing_strategy'] == 'cost-optimized'
assert 'agents' in config
assert config['agents']['classifier']['llm'] == 'cheap-fast'
assert config['agents']['researcher']['llm_routing'] == 'auto'
assert config['agents']['writer']['llm'] == 'premium'
assert 'steps' in config
assert len(config['steps']) == 2
if __name__ == '__main__':
pytest.main([__file__, '-v'])