mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b01eb2b8fa |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.43" \
|
||||
"praisonai>=2.3.44" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.43" \
|
||||
"praisonai>=2.3.44" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.43" \
|
||||
"praisonai>=2.3.44" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.43.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.43.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.44.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.44.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -21,6 +21,7 @@ Features:
|
||||
- flow_display: Visual workflow tracking
|
||||
- workflow: YAML workflow management
|
||||
- n8n: Export workflows to n8n visual editor
|
||||
- external_agents: External AI CLI tool integrations (Claude, Gemini, Codex, Cursor)
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -43,6 +44,7 @@ __all__ = [
|
||||
'FlowDisplayHandler',
|
||||
'WorkflowHandler',
|
||||
'N8nHandler',
|
||||
'ExternalAgentsHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -95,4 +97,7 @@ def __getattr__(name):
|
||||
elif name == 'N8nHandler':
|
||||
from .n8n import N8nHandler
|
||||
return N8nHandler
|
||||
elif name == 'ExternalAgentsHandler':
|
||||
from .external_agents import ExternalAgentsHandler
|
||||
return ExternalAgentsHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
External Agents Handler for CLI.
|
||||
|
||||
Provides integration with external AI coding CLI tools:
|
||||
- Claude Code CLI
|
||||
- Gemini CLI
|
||||
- OpenAI Codex CLI
|
||||
- Cursor CLI
|
||||
|
||||
Usage:
|
||||
praisonai "Refactor the code" --external-agent claude
|
||||
praisonai "Analyze codebase" --external-agent gemini
|
||||
praisonai "Fix bugs" --external-agent codex
|
||||
praisonai "Add tests" --external-agent cursor
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
from .base import FlagHandler
|
||||
|
||||
|
||||
class ExternalAgentsHandler(FlagHandler):
|
||||
"""
|
||||
Handler for --external-agent flag.
|
||||
|
||||
Integrates external AI coding CLI tools as agent tools.
|
||||
|
||||
Example:
|
||||
praisonai "Refactor the auth module" --external-agent claude
|
||||
praisonai "Analyze this codebase" --external-agent gemini
|
||||
"""
|
||||
|
||||
# Mapping of integration names to their module paths
|
||||
INTEGRATIONS = {
|
||||
"claude": "claude_code.ClaudeCodeIntegration",
|
||||
"gemini": "gemini_cli.GeminiCLIIntegration",
|
||||
"codex": "codex_cli.CodexCLIIntegration",
|
||||
"cursor": "cursor_cli.CursorCLIIntegration",
|
||||
}
|
||||
|
||||
def __init__(self, verbose: bool = False):
|
||||
"""Initialize the handler."""
|
||||
super().__init__(verbose)
|
||||
self._integration_cache: Dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
"""Return the feature name."""
|
||||
return "external_agents"
|
||||
|
||||
@property
|
||||
def flag_name(self) -> str:
|
||||
"""Return the flag name."""
|
||||
return "external-agent"
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
"""Return the flag help text."""
|
||||
return "External AI CLI tool to use (claude, gemini, codex, cursor)"
|
||||
|
||||
def check_dependencies(self) -> Tuple[bool, str]:
|
||||
"""Check if integrations module is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("praisonai.integrations") is not None:
|
||||
return True, ""
|
||||
return False, "Integrations module not available"
|
||||
except ImportError:
|
||||
return False, "Integrations module not available"
|
||||
|
||||
def get_integration(self, name: str, **options):
|
||||
"""
|
||||
Get an integration instance by name.
|
||||
|
||||
Args:
|
||||
name: Integration name (claude, gemini, codex, cursor)
|
||||
**options: Options to pass to the integration
|
||||
|
||||
Returns:
|
||||
Integration instance
|
||||
|
||||
Raises:
|
||||
ValueError: If integration name is invalid
|
||||
"""
|
||||
if name not in self.INTEGRATIONS:
|
||||
raise ValueError(f"Unknown integration: {name}. Available: {list(self.INTEGRATIONS.keys())}")
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"{name}_{hash(frozenset(options.items()))}"
|
||||
if cache_key in self._integration_cache:
|
||||
return self._integration_cache[cache_key]
|
||||
|
||||
# Lazy import the integration
|
||||
module_path = self.INTEGRATIONS[name]
|
||||
module_name, class_name = module_path.rsplit(".", 1)
|
||||
|
||||
import importlib
|
||||
module = importlib.import_module(f"praisonai.integrations.{module_name}")
|
||||
integration_class = getattr(module, class_name)
|
||||
|
||||
# Create instance with options
|
||||
integration = integration_class(**options)
|
||||
|
||||
# Cache it
|
||||
self._integration_cache[cache_key] = integration
|
||||
|
||||
return integration
|
||||
|
||||
def list_integrations(self) -> List[str]:
|
||||
"""
|
||||
List all available integration names.
|
||||
|
||||
Returns:
|
||||
List of integration names
|
||||
"""
|
||||
return list(self.INTEGRATIONS.keys())
|
||||
|
||||
def check_availability(self) -> Dict[str, bool]:
|
||||
"""
|
||||
Check availability of all integrations.
|
||||
|
||||
Returns:
|
||||
Dict mapping integration name to availability
|
||||
"""
|
||||
availability = {}
|
||||
for name in self.INTEGRATIONS:
|
||||
try:
|
||||
integration = self.get_integration(name)
|
||||
availability[name] = integration.is_available
|
||||
except Exception:
|
||||
availability[name] = False
|
||||
return availability
|
||||
|
||||
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply external agent configuration to agent config.
|
||||
|
||||
Args:
|
||||
config: Agent configuration dictionary
|
||||
flag_value: Integration name or dict with name and options
|
||||
|
||||
Returns:
|
||||
Modified configuration with external agent tool
|
||||
"""
|
||||
if not flag_value:
|
||||
return config
|
||||
|
||||
# Handle both string and dict formats
|
||||
if isinstance(flag_value, str):
|
||||
name = flag_value
|
||||
options = {}
|
||||
elif isinstance(flag_value, dict):
|
||||
name = flag_value.get("name", "")
|
||||
options = {k: v for k, v in flag_value.items() if k != "name"}
|
||||
else:
|
||||
return config
|
||||
|
||||
try:
|
||||
integration = self.get_integration(name, **options)
|
||||
|
||||
if integration.is_available:
|
||||
# Add tool to existing tools
|
||||
existing_tools = config.get("tools", [])
|
||||
if isinstance(existing_tools, list):
|
||||
existing_tools.append(integration.as_tool())
|
||||
else:
|
||||
existing_tools = [integration.as_tool()]
|
||||
config["tools"] = existing_tools
|
||||
|
||||
self.print_status(f"🔌 External agent connected: {name}", "success")
|
||||
else:
|
||||
self.print_status(f"⚠️ External agent not available: {name}", "warning")
|
||||
|
||||
except ValueError as e:
|
||||
self.print_status(str(e), "error")
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to connect external agent: {e}", "error")
|
||||
|
||||
return config
|
||||
|
||||
def execute(
|
||||
self,
|
||||
integration_name: str = None,
|
||||
**options
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
Execute external agent setup.
|
||||
|
||||
Args:
|
||||
integration_name: Name of the integration
|
||||
**options: Options for the integration
|
||||
|
||||
Returns:
|
||||
Integration instance or None
|
||||
"""
|
||||
if not integration_name:
|
||||
# List available integrations
|
||||
self.print_status("\n🔧 Available External Agents:", "info")
|
||||
self.print_status("-" * 40, "info")
|
||||
|
||||
availability = self.check_availability()
|
||||
for name, available in availability.items():
|
||||
status = "✅ Available" if available else "❌ Not installed"
|
||||
self.print_status(f" {name}: {status}", "info")
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
integration = self.get_integration(integration_name, **options)
|
||||
|
||||
if integration.is_available:
|
||||
self.print_status(f"🔌 External agent ready: {integration_name}", "success")
|
||||
return integration
|
||||
else:
|
||||
self.print_status(f"⚠️ External agent not installed: {integration_name}", "warning")
|
||||
self.print_status(f" Install with: {self._get_install_instructions(integration_name)}", "info")
|
||||
return None
|
||||
|
||||
except ValueError as e:
|
||||
self.print_status(str(e), "error")
|
||||
return None
|
||||
|
||||
def _get_install_instructions(self, name: str) -> str:
|
||||
"""Get installation instructions for an integration."""
|
||||
instructions = {
|
||||
"claude": "npm install -g @anthropic-ai/claude-code",
|
||||
"gemini": "npm install -g @anthropic-ai/gemini-cli",
|
||||
"codex": "npm install -g @openai/codex",
|
||||
"cursor": "Download from cursor.com",
|
||||
}
|
||||
return instructions.get(name, "See documentation")
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.43 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.44 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
Base class for external CLI tool integrations.
|
||||
|
||||
Provides a consistent interface for integrating with AI coding CLI tools
|
||||
like Claude Code, Gemini CLI, Codex CLI, and Cursor CLI.
|
||||
|
||||
Features:
|
||||
- Lazy availability checking with caching
|
||||
- Async subprocess execution
|
||||
- Tool wrapper for agent integration
|
||||
- Timeout handling
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import AsyncIterator, Optional, Dict, Any, Tuple, List
|
||||
import asyncio
|
||||
import shutil
|
||||
import os
|
||||
|
||||
|
||||
class BaseCLIIntegration(ABC):
|
||||
"""
|
||||
Abstract base class for external CLI tool integrations.
|
||||
|
||||
Provides:
|
||||
- Lazy availability checking with class-level caching
|
||||
- Async subprocess execution with timeout handling
|
||||
- Tool wrapper for PraisonAI agent integration
|
||||
|
||||
Subclasses must implement:
|
||||
- cli_command property: The CLI command name
|
||||
- execute method: Execute the CLI and return result
|
||||
- stream method: Stream output from the CLI
|
||||
|
||||
Example:
|
||||
class MyIntegration(BaseCLIIntegration):
|
||||
@property
|
||||
def cli_command(self) -> str:
|
||||
return "my-cli"
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
return await self.execute_async(["my-cli", "-p", prompt])
|
||||
|
||||
async def stream(self, prompt: str, **options):
|
||||
async for line in self.stream_async(["my-cli", "-p", prompt]):
|
||||
yield line
|
||||
"""
|
||||
|
||||
# Class-level cache for availability checks (shared across instances)
|
||||
_availability_cache: Dict[str, bool] = {}
|
||||
|
||||
def __init__(self, workspace: str = ".", timeout: int = 300):
|
||||
"""
|
||||
Initialize the CLI integration.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for CLI execution (default: current dir)
|
||||
timeout: Default timeout in seconds for CLI execution (default: 300)
|
||||
"""
|
||||
self.workspace = workspace
|
||||
self.timeout = timeout
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def cli_command(self) -> str:
|
||||
"""
|
||||
Return the CLI command name.
|
||||
|
||||
This is used for availability checking and tool naming.
|
||||
|
||||
Returns:
|
||||
str: The CLI command (e.g., "claude", "gemini", "codex", "cursor-agent")
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""
|
||||
Check if the CLI tool is installed and available.
|
||||
|
||||
Uses class-level caching to avoid repeated filesystem checks.
|
||||
|
||||
Returns:
|
||||
bool: True if the CLI is available, False otherwise
|
||||
"""
|
||||
if self.cli_command not in self._availability_cache:
|
||||
self._availability_cache[self.cli_command] = shutil.which(self.cli_command) is not None
|
||||
return self._availability_cache[self.cli_command]
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
"""
|
||||
Execute the CLI tool and return the result.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send to the CLI
|
||||
**options: Additional options for the CLI
|
||||
|
||||
Returns:
|
||||
str: The CLI output
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def stream(self, prompt: str, **options) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Stream output from the CLI tool.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send to the CLI
|
||||
**options: Additional options for the CLI
|
||||
|
||||
Yields:
|
||||
dict: Parsed output events from the CLI
|
||||
"""
|
||||
pass
|
||||
|
||||
async def execute_async(self, cmd: List[str], timeout: Optional[int] = None) -> str:
|
||||
"""
|
||||
Execute a command asynchronously and return stdout.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments as a list
|
||||
timeout: Timeout in seconds (uses self.timeout if not specified)
|
||||
|
||||
Returns:
|
||||
str: The command's stdout
|
||||
|
||||
Raises:
|
||||
TimeoutError: If the command times out
|
||||
"""
|
||||
timeout = timeout or self.timeout
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=timeout
|
||||
)
|
||||
return stdout.decode()
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
raise TimeoutError(f"Command timed out after {timeout}s: {' '.join(cmd)}")
|
||||
|
||||
async def execute_async_with_stderr(
|
||||
self,
|
||||
cmd: List[str],
|
||||
timeout: Optional[int] = None
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Execute a command asynchronously and return both stdout and stderr.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments as a list
|
||||
timeout: Timeout in seconds (uses self.timeout if not specified)
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: (stdout, stderr)
|
||||
|
||||
Raises:
|
||||
TimeoutError: If the command times out
|
||||
"""
|
||||
timeout = timeout or self.timeout
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=timeout
|
||||
)
|
||||
return stdout.decode(), stderr.decode()
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
raise TimeoutError(f"Command timed out after {timeout}s: {' '.join(cmd)}")
|
||||
|
||||
async def stream_async(
|
||||
self,
|
||||
cmd: List[str],
|
||||
timeout: Optional[int] = None
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Stream stdout lines from a command asynchronously.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments as a list
|
||||
timeout: Timeout in seconds for the entire operation
|
||||
|
||||
Yields:
|
||||
str: Each line of output
|
||||
"""
|
||||
timeout = timeout or self.timeout
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
)
|
||||
|
||||
try:
|
||||
async def read_lines():
|
||||
while True:
|
||||
line = await proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
yield line.decode().rstrip('\n')
|
||||
|
||||
async for line in read_lines():
|
||||
yield line
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
raise TimeoutError(f"Stream timed out after {timeout}s")
|
||||
finally:
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
|
||||
def as_tool(self) -> callable:
|
||||
"""
|
||||
Return a callable suitable for use as a PraisonAI agent tool.
|
||||
|
||||
The returned function is synchronous and wraps the async execute method.
|
||||
Uses asyncio.run() for proper event loop handling (Python 3.7+).
|
||||
|
||||
Returns:
|
||||
callable: A function that can be used as an agent tool
|
||||
"""
|
||||
# Capture self for closure
|
||||
integration = self
|
||||
|
||||
def tool_func(query: str) -> str:
|
||||
"""Execute the CLI tool with the given query."""
|
||||
# Use asyncio.run() for clean event loop management
|
||||
# This creates a new event loop, runs the coroutine, and closes it
|
||||
try:
|
||||
# Check if we're already in an async context
|
||||
asyncio.get_running_loop()
|
||||
# If we're in an async context, use ThreadPoolExecutor to avoid nested loop
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(asyncio.run, integration.execute(query))
|
||||
return future.result()
|
||||
except RuntimeError:
|
||||
# No running loop, safe to use asyncio.run()
|
||||
return asyncio.run(integration.execute(query))
|
||||
|
||||
# Set function metadata for agent tool registration
|
||||
tool_func.__name__ = f"{self.cli_command}_tool"
|
||||
tool_func.__doc__ = f"Execute {self.cli_command} for coding tasks."
|
||||
|
||||
return tool_func
|
||||
|
||||
def get_env(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get environment variables for CLI execution.
|
||||
|
||||
Subclasses can override this to add tool-specific environment variables.
|
||||
|
||||
Returns:
|
||||
dict: Environment variables to pass to the CLI
|
||||
"""
|
||||
return dict(os.environ)
|
||||
|
||||
|
||||
def get_available_integrations() -> Dict[str, bool]:
|
||||
"""
|
||||
Get a dictionary of all integrations and their availability status.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of integration name to availability (True/False)
|
||||
"""
|
||||
from .claude_code import ClaudeCodeIntegration
|
||||
from .gemini_cli import GeminiCLIIntegration
|
||||
from .codex_cli import CodexCLIIntegration
|
||||
from .cursor_cli import CursorCLIIntegration
|
||||
|
||||
integrations = {
|
||||
'claude': ClaudeCodeIntegration(),
|
||||
'gemini': GeminiCLIIntegration(),
|
||||
'codex': CodexCLIIntegration(),
|
||||
'cursor': CursorCLIIntegration(),
|
||||
}
|
||||
|
||||
return {name: integration.is_available for name, integration in integrations.items()}
|
||||
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Claude Code CLI Integration.
|
||||
|
||||
Provides integration with Claude Code CLI for AI-powered coding tasks.
|
||||
|
||||
Features:
|
||||
- Headless mode execution with JSON output
|
||||
- Session continuation support
|
||||
- System prompt customization
|
||||
- Tool restrictions
|
||||
- Optional SDK integration (when claude-agent-sdk is installed)
|
||||
|
||||
Usage:
|
||||
from praisonai.integrations import ClaudeCodeIntegration
|
||||
|
||||
# Create integration
|
||||
claude = ClaudeCodeIntegration(workspace="/path/to/project")
|
||||
|
||||
# Execute a coding task
|
||||
result = await claude.execute("Refactor the auth module")
|
||||
|
||||
# Stream output
|
||||
async for event in claude.stream("Add error handling"):
|
||||
print(event)
|
||||
|
||||
# Use as agent tool
|
||||
tool = claude.as_tool()
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncIterator, Dict, Any, Optional, List
|
||||
|
||||
from .base import BaseCLIIntegration
|
||||
|
||||
|
||||
# Check if Claude Code SDK is available
|
||||
CLAUDE_SDK_AVAILABLE = False
|
||||
try:
|
||||
from claude_agent_sdk import query as claude_query, ClaudeAgentOptions
|
||||
CLAUDE_SDK_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class ClaudeCodeIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
Integration with Claude Code CLI.
|
||||
|
||||
Supports both subprocess-based execution and SDK-based execution
|
||||
(when claude-agent-sdk is installed).
|
||||
|
||||
Attributes:
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
skip_permissions: Whether to skip permission prompts
|
||||
system_prompt: Custom system prompt to append
|
||||
allowed_tools: List of allowed tools (e.g., ["Read", "Write", "Bash"])
|
||||
disallowed_tools: List of disallowed tools
|
||||
use_sdk: Whether to use the SDK instead of subprocess
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: str = ".",
|
||||
timeout: int = 300,
|
||||
output_format: str = "json",
|
||||
skip_permissions: bool = True,
|
||||
system_prompt: Optional[str] = None,
|
||||
allowed_tools: Optional[List[str]] = None,
|
||||
disallowed_tools: Optional[List[str]] = None,
|
||||
use_sdk: bool = False,
|
||||
model: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize Claude Code integration.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for CLI execution
|
||||
timeout: Timeout in seconds for CLI execution
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
skip_permissions: Whether to skip permission prompts (--dangerously-skip-permissions)
|
||||
system_prompt: Custom system prompt to append
|
||||
allowed_tools: List of allowed tools
|
||||
disallowed_tools: List of disallowed tools
|
||||
use_sdk: Whether to use the SDK instead of subprocess
|
||||
model: Model to use (e.g., "sonnet", "opus")
|
||||
"""
|
||||
super().__init__(workspace=workspace, timeout=timeout)
|
||||
|
||||
self.output_format = output_format
|
||||
self.skip_permissions = skip_permissions
|
||||
self.system_prompt = system_prompt
|
||||
self.allowed_tools = allowed_tools
|
||||
self.disallowed_tools = disallowed_tools
|
||||
self.use_sdk = use_sdk and CLAUDE_SDK_AVAILABLE
|
||||
self.model = model
|
||||
|
||||
# Session management
|
||||
self._session_active = False
|
||||
|
||||
@property
|
||||
def cli_command(self) -> str:
|
||||
"""Return the CLI command name."""
|
||||
return "claude"
|
||||
|
||||
@property
|
||||
def sdk_available(self) -> bool:
|
||||
"""Check if the Claude Code SDK is available."""
|
||||
return CLAUDE_SDK_AVAILABLE
|
||||
|
||||
def _build_command(
|
||||
self,
|
||||
prompt: str,
|
||||
continue_session: bool = False,
|
||||
**options
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build the Claude CLI command.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send
|
||||
continue_session: Whether to continue a previous session
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of command arguments
|
||||
"""
|
||||
cmd = ["claude"]
|
||||
|
||||
# Add continue flag if needed
|
||||
if continue_session or self._session_active:
|
||||
cmd.append("--continue")
|
||||
|
||||
# Add skip permissions flag
|
||||
if self.skip_permissions:
|
||||
cmd.append("--dangerously-skip-permissions")
|
||||
|
||||
# Add print mode flag
|
||||
cmd.append("-p")
|
||||
|
||||
# Add prompt
|
||||
cmd.append(prompt)
|
||||
|
||||
# Add output format
|
||||
cmd.extend(["--output-format", self.output_format])
|
||||
|
||||
# Add model if specified
|
||||
if self.model:
|
||||
cmd.extend(["--model", self.model])
|
||||
|
||||
# Add system prompt if specified
|
||||
if self.system_prompt:
|
||||
cmd.extend(["--append-system-prompt", self.system_prompt])
|
||||
|
||||
# Add allowed tools if specified
|
||||
if self.allowed_tools:
|
||||
cmd.extend(["--allowedTools", ",".join(self.allowed_tools)])
|
||||
|
||||
# Add disallowed tools if specified
|
||||
if self.disallowed_tools:
|
||||
cmd.extend(["--disallowedTools", ",".join(self.disallowed_tools)])
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
"""
|
||||
Execute Claude Code CLI and return the result.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options (continue_session, etc.)
|
||||
|
||||
Returns:
|
||||
str: The CLI output (parsed from JSON if output_format is "json")
|
||||
"""
|
||||
if self.use_sdk:
|
||||
return await self._execute_sdk(prompt, **options)
|
||||
|
||||
return await self._execute_subprocess(prompt, **options)
|
||||
|
||||
async def _execute_subprocess(self, prompt: str, **options) -> str:
|
||||
"""Execute using subprocess."""
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
# Mark session as active for continuation
|
||||
self._session_active = True
|
||||
|
||||
# Parse JSON output if applicable
|
||||
if self.output_format == "json":
|
||||
try:
|
||||
data = json.loads(output)
|
||||
# Extract the main result
|
||||
if isinstance(data, dict):
|
||||
return data.get("result", data.get("content", str(data)))
|
||||
return str(data)
|
||||
except json.JSONDecodeError:
|
||||
return output
|
||||
|
||||
return output
|
||||
|
||||
async def _execute_sdk(self, prompt: str, **options) -> str:
|
||||
"""Execute using the Claude Code SDK."""
|
||||
if not CLAUDE_SDK_AVAILABLE:
|
||||
raise RuntimeError("Claude Code SDK not available. Install with: pip install claude-agent-sdk")
|
||||
|
||||
sdk_options = ClaudeAgentOptions(
|
||||
cwd=self.workspace,
|
||||
system_prompt=self.system_prompt,
|
||||
)
|
||||
|
||||
if self.allowed_tools:
|
||||
sdk_options.allowed_tools = self.allowed_tools
|
||||
|
||||
if self.skip_permissions:
|
||||
sdk_options.permission_mode = 'acceptEdits'
|
||||
|
||||
result_parts = []
|
||||
async for message in claude_query(prompt=prompt, options=sdk_options):
|
||||
if hasattr(message, 'content'):
|
||||
for block in message.content:
|
||||
if hasattr(block, 'text'):
|
||||
result_parts.append(block.text)
|
||||
|
||||
return '\n'.join(result_parts)
|
||||
|
||||
async def stream(self, prompt: str, **options) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Stream output from Claude Code CLI.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Yields:
|
||||
dict: Parsed JSON events from the CLI
|
||||
"""
|
||||
# Use stream-json format for streaming
|
||||
original_format = self.output_format
|
||||
self.output_format = "stream-json"
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
async for line in self.stream_async(cmd):
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
yield event
|
||||
except json.JSONDecodeError:
|
||||
yield {"type": "text", "content": line}
|
||||
finally:
|
||||
self.output_format = original_format
|
||||
|
||||
def reset_session(self):
|
||||
"""Reset the session state."""
|
||||
self._session_active = False
|
||||
|
||||
def get_env(self) -> Dict[str, str]:
|
||||
"""Get environment variables for CLI execution."""
|
||||
env = super().get_env()
|
||||
|
||||
# Add Anthropic API key if available
|
||||
if "ANTHROPIC_API_KEY" in os.environ:
|
||||
env["ANTHROPIC_API_KEY"] = os.environ["ANTHROPIC_API_KEY"]
|
||||
elif "CLAUDE_API_KEY" in os.environ:
|
||||
env["ANTHROPIC_API_KEY"] = os.environ["CLAUDE_API_KEY"]
|
||||
|
||||
return env
|
||||
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
OpenAI Codex CLI Integration.
|
||||
|
||||
Provides integration with OpenAI's Codex CLI for AI-powered coding tasks.
|
||||
|
||||
Features:
|
||||
- Non-interactive execution with `codex exec`
|
||||
- Full auto mode for file modifications
|
||||
- Sandbox modes for security
|
||||
- JSON streaming output
|
||||
- Structured output with schemas
|
||||
|
||||
Usage:
|
||||
from praisonai.integrations import CodexCLIIntegration
|
||||
|
||||
# Create integration
|
||||
codex = CodexCLIIntegration(workspace="/path/to/project")
|
||||
|
||||
# Execute a coding task
|
||||
result = await codex.execute("Fix the authentication bug")
|
||||
|
||||
# Execute with full auto (allows file modifications)
|
||||
codex_auto = CodexCLIIntegration(full_auto=True)
|
||||
result = await codex_auto.execute("Refactor the module")
|
||||
|
||||
# Use as agent tool
|
||||
tool = codex.as_tool()
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncIterator, Dict, Any, Optional, List
|
||||
|
||||
from .base import BaseCLIIntegration
|
||||
|
||||
|
||||
class CodexCLIIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
Integration with OpenAI's Codex CLI.
|
||||
|
||||
Attributes:
|
||||
full_auto: Whether to allow file modifications
|
||||
sandbox: Sandbox mode ("default", "danger-full-access")
|
||||
json_output: Whether to use JSON streaming output
|
||||
output_schema: Path to JSON schema for structured output
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: str = ".",
|
||||
timeout: int = 300,
|
||||
full_auto: bool = False,
|
||||
sandbox: str = "default",
|
||||
json_output: bool = False,
|
||||
output_schema: Optional[str] = None,
|
||||
output_file: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize Codex CLI integration.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for CLI execution
|
||||
timeout: Timeout in seconds for CLI execution
|
||||
full_auto: Whether to allow file modifications (--full-auto)
|
||||
sandbox: Sandbox mode ("default", "danger-full-access")
|
||||
json_output: Whether to use JSON streaming output (--json)
|
||||
output_schema: Path to JSON schema for structured output
|
||||
output_file: Path to save the final output (-o)
|
||||
"""
|
||||
super().__init__(workspace=workspace, timeout=timeout)
|
||||
|
||||
self.full_auto = full_auto
|
||||
self.sandbox = sandbox
|
||||
self.json_output = json_output
|
||||
self.output_schema = output_schema
|
||||
self.output_file = output_file
|
||||
|
||||
@property
|
||||
def cli_command(self) -> str:
|
||||
"""Return the CLI command name."""
|
||||
return "codex"
|
||||
|
||||
def _build_command(self, task: str, **options) -> List[str]:
|
||||
"""
|
||||
Build the Codex CLI command.
|
||||
|
||||
Args:
|
||||
task: The task to execute
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of command arguments
|
||||
"""
|
||||
cmd = ["codex", "exec"]
|
||||
|
||||
# Add task
|
||||
cmd.append(task)
|
||||
|
||||
# Add full auto flag if enabled
|
||||
if self.full_auto:
|
||||
cmd.append("--full-auto")
|
||||
|
||||
# Add sandbox mode if not default
|
||||
if self.sandbox and self.sandbox != "default":
|
||||
cmd.extend(["--sandbox", self.sandbox])
|
||||
|
||||
# Add JSON output flag if enabled
|
||||
if self.json_output:
|
||||
cmd.append("--json")
|
||||
|
||||
# Add output schema if specified
|
||||
if self.output_schema:
|
||||
cmd.extend(["--output-schema", self.output_schema])
|
||||
|
||||
# Add output file if specified
|
||||
if self.output_file:
|
||||
cmd.extend(["-o", self.output_file])
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
"""
|
||||
Execute Codex CLI and return the result.
|
||||
|
||||
Args:
|
||||
prompt: The task/prompt to execute
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
str: The CLI output
|
||||
"""
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
# Parse JSON Lines output if json_output is enabled
|
||||
if self.json_output:
|
||||
return self._parse_json_events(output)
|
||||
|
||||
return output
|
||||
|
||||
def _parse_json_events(self, output: str) -> str:
|
||||
"""
|
||||
Parse JSON Lines output and extract the final result.
|
||||
|
||||
Args:
|
||||
output: Raw JSON Lines output
|
||||
|
||||
Returns:
|
||||
str: The extracted result
|
||||
"""
|
||||
result_parts = []
|
||||
|
||||
for line in output.strip().split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
event = json.loads(line)
|
||||
event_type = event.get("type", "")
|
||||
|
||||
# Extract agent messages
|
||||
if event_type == "item.completed":
|
||||
item = event.get("item", {})
|
||||
if item.get("type") == "agent_message":
|
||||
text = item.get("text", "")
|
||||
if text:
|
||||
result_parts.append(text)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return '\n'.join(result_parts) if result_parts else output
|
||||
|
||||
async def stream(self, prompt: str, **options) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Stream output from Codex CLI.
|
||||
|
||||
Args:
|
||||
prompt: The task/prompt to execute
|
||||
**options: Additional options
|
||||
|
||||
Yields:
|
||||
dict: Parsed JSON events from the CLI
|
||||
"""
|
||||
# Ensure JSON output is enabled for streaming
|
||||
original_json = self.json_output
|
||||
self.json_output = True
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
async for line in self.stream_async(cmd):
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
yield event
|
||||
except json.JSONDecodeError:
|
||||
yield {"type": "text", "content": line}
|
||||
finally:
|
||||
self.json_output = original_json
|
||||
|
||||
async def execute_with_schema(
|
||||
self,
|
||||
prompt: str,
|
||||
schema_path: str,
|
||||
output_path: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute Codex with structured output schema.
|
||||
|
||||
Args:
|
||||
prompt: The task/prompt to execute
|
||||
schema_path: Path to the JSON schema file
|
||||
output_path: Optional path to save the output
|
||||
|
||||
Returns:
|
||||
dict: Parsed structured output
|
||||
"""
|
||||
original_schema = self.output_schema
|
||||
original_output = self.output_file
|
||||
|
||||
self.output_schema = schema_path
|
||||
if output_path:
|
||||
self.output_file = output_path
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt)
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
# If output file was specified, read from it
|
||||
if output_path and os.path.exists(output_path):
|
||||
with open(output_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
# Otherwise parse the output
|
||||
try:
|
||||
return json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return {"result": output}
|
||||
finally:
|
||||
self.output_schema = original_schema
|
||||
self.output_file = original_output
|
||||
|
||||
def get_env(self) -> Dict[str, str]:
|
||||
"""Get environment variables for CLI execution."""
|
||||
env = super().get_env()
|
||||
|
||||
# Codex uses ChatGPT authentication or API key
|
||||
# The CLI handles authentication internally
|
||||
|
||||
return env
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Cursor CLI Integration.
|
||||
|
||||
Provides integration with Cursor's CLI (cursor-agent) for AI-powered coding tasks.
|
||||
|
||||
Features:
|
||||
- Headless mode execution with JSON output
|
||||
- Force mode for file modifications
|
||||
- Model selection
|
||||
- Session resume support
|
||||
- Streaming output with partial deltas
|
||||
|
||||
Usage:
|
||||
from praisonai.integrations import CursorCLIIntegration
|
||||
|
||||
# Create integration
|
||||
cursor = CursorCLIIntegration(workspace="/path/to/project")
|
||||
|
||||
# Execute a coding task
|
||||
result = await cursor.execute("Fix the authentication bug")
|
||||
|
||||
# Execute with force mode (allows file modifications)
|
||||
cursor_force = CursorCLIIntegration(force=True)
|
||||
result = await cursor_force.execute("Refactor the module")
|
||||
|
||||
# Use as agent tool
|
||||
tool = cursor.as_tool()
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncIterator, Dict, Any, Optional, List
|
||||
|
||||
from .base import BaseCLIIntegration
|
||||
|
||||
|
||||
class CursorCLIIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
Integration with Cursor CLI (cursor-agent).
|
||||
|
||||
Attributes:
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
force: Whether to allow file modifications
|
||||
model: Model to use (e.g., "gpt-5")
|
||||
stream_partial: Whether to stream partial output
|
||||
resume_session: Session ID to resume
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: str = ".",
|
||||
timeout: int = 300,
|
||||
output_format: str = "json",
|
||||
force: bool = False,
|
||||
model: Optional[str] = None,
|
||||
stream_partial: bool = False,
|
||||
resume_session: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize Cursor CLI integration.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for CLI execution
|
||||
timeout: Timeout in seconds for CLI execution
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
force: Whether to allow file modifications (--force)
|
||||
model: Model to use (e.g., "gpt-5")
|
||||
stream_partial: Whether to stream partial output (--stream-partial-output)
|
||||
resume_session: Session ID to resume (--resume)
|
||||
"""
|
||||
super().__init__(workspace=workspace, timeout=timeout)
|
||||
|
||||
self.output_format = output_format
|
||||
self.force = force
|
||||
self.model = model
|
||||
self.stream_partial = stream_partial
|
||||
self.resume_session = resume_session
|
||||
|
||||
@property
|
||||
def cli_command(self) -> str:
|
||||
"""Return the CLI command name."""
|
||||
return "cursor-agent"
|
||||
|
||||
def _build_command(self, prompt: str, **options) -> List[str]:
|
||||
"""
|
||||
Build the Cursor CLI command.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of command arguments
|
||||
"""
|
||||
cmd = ["cursor-agent"]
|
||||
|
||||
# Add print mode flag
|
||||
cmd.append("-p")
|
||||
|
||||
# Add force flag if enabled
|
||||
if self.force:
|
||||
cmd.append("--force")
|
||||
|
||||
# Add model if specified
|
||||
if self.model:
|
||||
cmd.extend(["-m", self.model])
|
||||
|
||||
# Add output format
|
||||
cmd.extend(["--output-format", self.output_format])
|
||||
|
||||
# Add stream partial flag if enabled
|
||||
if self.stream_partial:
|
||||
cmd.append("--stream-partial-output")
|
||||
|
||||
# Add resume session if specified
|
||||
if self.resume_session:
|
||||
cmd.extend(["--resume", self.resume_session])
|
||||
|
||||
# Add prompt (must be last)
|
||||
cmd.append(prompt)
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
"""
|
||||
Execute Cursor CLI and return the result.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
str: The CLI output (parsed from JSON if output_format is "json")
|
||||
"""
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
# Parse JSON output if applicable
|
||||
if self.output_format == "json":
|
||||
try:
|
||||
data = json.loads(output)
|
||||
# Extract the main result
|
||||
if isinstance(data, dict):
|
||||
return data.get("result", data.get("content", str(data)))
|
||||
return str(data)
|
||||
except json.JSONDecodeError:
|
||||
return output
|
||||
|
||||
return output
|
||||
|
||||
async def stream(self, prompt: str, **options) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Stream output from Cursor CLI.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Yields:
|
||||
dict: Parsed JSON events from the CLI
|
||||
"""
|
||||
# Use stream-json format for streaming
|
||||
original_format = self.output_format
|
||||
original_partial = self.stream_partial
|
||||
|
||||
self.output_format = "stream-json"
|
||||
self.stream_partial = True
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
async for line in self.stream_async(cmd):
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
yield event
|
||||
except json.JSONDecodeError:
|
||||
yield {"type": "text", "content": line}
|
||||
finally:
|
||||
self.output_format = original_format
|
||||
self.stream_partial = original_partial
|
||||
|
||||
def get_env(self) -> Dict[str, str]:
|
||||
"""Get environment variables for CLI execution."""
|
||||
env = super().get_env()
|
||||
|
||||
# Add Cursor API key if available
|
||||
if "CURSOR_API_KEY" in os.environ:
|
||||
env["CURSOR_API_KEY"] = os.environ["CURSOR_API_KEY"]
|
||||
|
||||
return env
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Gemini CLI Integration.
|
||||
|
||||
Provides integration with Google's Gemini CLI for AI-powered coding tasks.
|
||||
|
||||
Features:
|
||||
- Headless mode execution with JSON output
|
||||
- Model selection (gemini-2.5-pro, gemini-2.5-flash, etc.)
|
||||
- Multi-directory context support
|
||||
- Google Search grounding
|
||||
- Usage statistics
|
||||
|
||||
Usage:
|
||||
from praisonai.integrations import GeminiCLIIntegration
|
||||
|
||||
# Create integration
|
||||
gemini = GeminiCLIIntegration(workspace="/path/to/project")
|
||||
|
||||
# Execute a coding task
|
||||
result = await gemini.execute("Analyze this codebase")
|
||||
|
||||
# Get result with stats
|
||||
result, stats = await gemini.execute_with_stats("Explain the architecture")
|
||||
|
||||
# Use as agent tool
|
||||
tool = gemini.as_tool()
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncIterator, Dict, Any, Optional, List, Tuple
|
||||
|
||||
from .base import BaseCLIIntegration
|
||||
|
||||
|
||||
class GeminiCLIIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
Integration with Google's Gemini CLI.
|
||||
|
||||
Attributes:
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
model: Gemini model to use
|
||||
include_directories: Additional directories to include in context
|
||||
sandbox: Whether to run in sandbox mode
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: str = ".",
|
||||
timeout: int = 300,
|
||||
output_format: str = "json",
|
||||
model: str = "gemini-2.5-pro",
|
||||
include_directories: Optional[List[str]] = None,
|
||||
sandbox: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize Gemini CLI integration.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for CLI execution
|
||||
timeout: Timeout in seconds for CLI execution
|
||||
output_format: Output format ("json", "text", "stream-json")
|
||||
model: Gemini model to use (e.g., "gemini-2.5-pro", "gemini-2.5-flash")
|
||||
include_directories: Additional directories to include in context
|
||||
sandbox: Whether to run in sandbox mode
|
||||
"""
|
||||
super().__init__(workspace=workspace, timeout=timeout)
|
||||
|
||||
self.output_format = output_format
|
||||
self.model = model
|
||||
self.include_directories = include_directories
|
||||
self.sandbox = sandbox
|
||||
|
||||
# Store last stats for retrieval
|
||||
self._last_stats: Optional[Dict[str, Any]] = None
|
||||
|
||||
@property
|
||||
def cli_command(self) -> str:
|
||||
"""Return the CLI command name."""
|
||||
return "gemini"
|
||||
|
||||
def _build_command(self, prompt: str, **options) -> List[str]:
|
||||
"""
|
||||
Build the Gemini CLI command.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of command arguments
|
||||
"""
|
||||
cmd = ["gemini"]
|
||||
|
||||
# Add print mode flag
|
||||
cmd.append("-p")
|
||||
|
||||
# Add prompt
|
||||
cmd.append(prompt)
|
||||
|
||||
# Add model
|
||||
cmd.extend(["-m", self.model])
|
||||
|
||||
# Add output format
|
||||
cmd.extend(["--output-format", self.output_format])
|
||||
|
||||
# Add include directories if specified
|
||||
if self.include_directories:
|
||||
cmd.extend(["--include-directories", ",".join(self.include_directories)])
|
||||
|
||||
# Add sandbox flag if enabled
|
||||
if self.sandbox:
|
||||
cmd.append("--sandbox")
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
"""
|
||||
Execute Gemini CLI and return the result.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
str: The CLI output (parsed from JSON if output_format is "json")
|
||||
"""
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
# Parse JSON output if applicable
|
||||
if self.output_format == "json":
|
||||
try:
|
||||
data = json.loads(output)
|
||||
# Store stats for later retrieval
|
||||
self._last_stats = data.get("stats")
|
||||
# Extract the main response
|
||||
return data.get("response", str(data))
|
||||
except json.JSONDecodeError:
|
||||
return output
|
||||
|
||||
return output
|
||||
|
||||
async def execute_with_stats(self, prompt: str, **options) -> Tuple[str, Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
Execute Gemini CLI and return both result and usage stats.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
Tuple[str, dict]: (result, stats) where stats contains usage information
|
||||
"""
|
||||
# Ensure JSON format for stats
|
||||
original_format = self.output_format
|
||||
self.output_format = "json"
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt, **options)
|
||||
output = await self.execute_async(cmd)
|
||||
|
||||
try:
|
||||
data = json.loads(output)
|
||||
response = data.get("response", str(data))
|
||||
stats = data.get("stats")
|
||||
return response, stats
|
||||
except json.JSONDecodeError:
|
||||
return output, None
|
||||
finally:
|
||||
self.output_format = original_format
|
||||
|
||||
async def stream(self, prompt: str, **options) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Stream output from Gemini CLI.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/query to send
|
||||
**options: Additional options
|
||||
|
||||
Yields:
|
||||
dict: Parsed JSON events from the CLI
|
||||
"""
|
||||
# Use stream-json format for streaming
|
||||
original_format = self.output_format
|
||||
self.output_format = "stream-json"
|
||||
|
||||
try:
|
||||
cmd = self._build_command(prompt, **options)
|
||||
|
||||
async for line in self.stream_async(cmd):
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
yield event
|
||||
except json.JSONDecodeError:
|
||||
yield {"type": "text", "content": line}
|
||||
finally:
|
||||
self.output_format = original_format
|
||||
|
||||
def get_last_stats(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the stats from the last execution.
|
||||
|
||||
Returns:
|
||||
dict: Usage statistics or None if not available
|
||||
"""
|
||||
return self._last_stats
|
||||
|
||||
def get_env(self) -> Dict[str, str]:
|
||||
"""Get environment variables for CLI execution."""
|
||||
env = super().get_env()
|
||||
|
||||
# Add Google API key if available
|
||||
if "GOOGLE_API_KEY" in os.environ:
|
||||
env["GOOGLE_API_KEY"] = os.environ["GOOGLE_API_KEY"]
|
||||
elif "GEMINI_API_KEY" in os.environ:
|
||||
# Map GEMINI_API_KEY to GOOGLE_API_KEY
|
||||
env["GOOGLE_API_KEY"] = os.environ["GEMINI_API_KEY"]
|
||||
|
||||
return env
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.43"
|
||||
__version__ = "2.3.44"
|
||||
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Integration tests for CLI tool integrations.
|
||||
|
||||
These tests require the actual CLI tools to be installed and API keys to be set.
|
||||
Run with: pytest tests/integration/test_cli_integrations.py -v
|
||||
|
||||
Environment variables required:
|
||||
- OPENAI_API_KEY: For general testing
|
||||
- ANTHROPIC_API_KEY or CLAUDE_API_KEY: For Claude Code
|
||||
- GEMINI_API_KEY or GOOGLE_API_KEY: For Gemini CLI
|
||||
- CURSOR_API_KEY: For Cursor CLI
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
|
||||
# Skip all tests if no API keys are set
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set"
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationAvailability:
|
||||
"""Test that integrations can be imported and checked."""
|
||||
|
||||
def test_import_all_integrations(self):
|
||||
"""Test importing all integrations."""
|
||||
from praisonai.integrations import (
|
||||
BaseCLIIntegration,
|
||||
ClaudeCodeIntegration,
|
||||
GeminiCLIIntegration,
|
||||
CodexCLIIntegration,
|
||||
CursorCLIIntegration,
|
||||
)
|
||||
|
||||
assert BaseCLIIntegration is not None
|
||||
assert ClaudeCodeIntegration is not None
|
||||
assert GeminiCLIIntegration is not None
|
||||
assert CodexCLIIntegration is not None
|
||||
assert CursorCLIIntegration is not None
|
||||
|
||||
def test_get_available_integrations(self):
|
||||
"""Test getting available integrations."""
|
||||
from praisonai.integrations import get_available_integrations
|
||||
|
||||
availability = get_available_integrations()
|
||||
|
||||
assert isinstance(availability, dict)
|
||||
assert "claude" in availability
|
||||
assert "gemini" in availability
|
||||
assert "codex" in availability
|
||||
assert "cursor" in availability
|
||||
|
||||
print("\n📊 Integration Availability:")
|
||||
for name, available in availability.items():
|
||||
status = "✅" if available else "❌"
|
||||
print(f" {status} {name}")
|
||||
|
||||
|
||||
class TestClaudeCodeIntegrationReal:
|
||||
"""Real integration tests for Claude Code CLI."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("ANTHROPIC_API_KEY") and not os.environ.get("CLAUDE_API_KEY"),
|
||||
reason="ANTHROPIC_API_KEY or CLAUDE_API_KEY not set"
|
||||
)
|
||||
def test_claude_integration_creation(self):
|
||||
"""Test creating Claude Code integration."""
|
||||
from praisonai.integrations import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(
|
||||
workspace=".",
|
||||
output_format="json",
|
||||
skip_permissions=True
|
||||
)
|
||||
|
||||
assert integration.cli_command == "claude"
|
||||
print(f"\n🔧 Claude CLI available: {integration.is_available}")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("ANTHROPIC_API_KEY") and not os.environ.get("CLAUDE_API_KEY"),
|
||||
reason="ANTHROPIC_API_KEY or CLAUDE_API_KEY not set"
|
||||
)
|
||||
def test_claude_as_tool(self):
|
||||
"""Test Claude Code as agent tool."""
|
||||
from praisonai.integrations import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
tool = integration.as_tool()
|
||||
|
||||
assert callable(tool)
|
||||
assert tool.__name__ == "claude_tool"
|
||||
print(f"\n🔧 Claude tool created: {tool.__name__}")
|
||||
|
||||
|
||||
class TestGeminiCLIIntegrationReal:
|
||||
"""Real integration tests for Gemini CLI."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("GEMINI_API_KEY") and not os.environ.get("GOOGLE_API_KEY"),
|
||||
reason="GEMINI_API_KEY or GOOGLE_API_KEY not set"
|
||||
)
|
||||
def test_gemini_integration_creation(self):
|
||||
"""Test creating Gemini CLI integration."""
|
||||
from praisonai.integrations import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration(
|
||||
workspace=".",
|
||||
model="gemini-2.5-pro",
|
||||
output_format="json"
|
||||
)
|
||||
|
||||
assert integration.cli_command == "gemini"
|
||||
assert integration.model == "gemini-2.5-pro"
|
||||
print(f"\n🔧 Gemini CLI available: {integration.is_available}")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("GEMINI_API_KEY") and not os.environ.get("GOOGLE_API_KEY"),
|
||||
reason="GEMINI_API_KEY or GOOGLE_API_KEY not set"
|
||||
)
|
||||
def test_gemini_as_tool(self):
|
||||
"""Test Gemini CLI as agent tool."""
|
||||
from praisonai.integrations import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
tool = integration.as_tool()
|
||||
|
||||
assert callable(tool)
|
||||
assert tool.__name__ == "gemini_tool"
|
||||
print(f"\n🔧 Gemini tool created: {tool.__name__}")
|
||||
|
||||
|
||||
class TestCursorCLIIntegrationReal:
|
||||
"""Real integration tests for Cursor CLI."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("CURSOR_API_KEY"),
|
||||
reason="CURSOR_API_KEY not set"
|
||||
)
|
||||
def test_cursor_integration_creation(self):
|
||||
"""Test creating Cursor CLI integration."""
|
||||
from praisonai.integrations import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(
|
||||
workspace=".",
|
||||
output_format="json",
|
||||
force=False
|
||||
)
|
||||
|
||||
assert integration.cli_command == "cursor-agent"
|
||||
print(f"\n🔧 Cursor CLI available: {integration.is_available}")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("CURSOR_API_KEY"),
|
||||
reason="CURSOR_API_KEY not set"
|
||||
)
|
||||
def test_cursor_as_tool(self):
|
||||
"""Test Cursor CLI as agent tool."""
|
||||
from praisonai.integrations import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
tool = integration.as_tool()
|
||||
|
||||
assert callable(tool)
|
||||
assert tool.__name__ == "cursor-agent_tool"
|
||||
print(f"\n🔧 Cursor tool created: {tool.__name__}")
|
||||
|
||||
|
||||
class TestExternalAgentsHandler:
|
||||
"""Test the ExternalAgentsHandler."""
|
||||
|
||||
def test_handler_creation(self):
|
||||
"""Test creating the handler."""
|
||||
from praisonai.cli.features import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler(verbose=True)
|
||||
|
||||
assert handler.feature_name == "external_agents"
|
||||
assert handler.flag_name == "external-agent"
|
||||
|
||||
def test_handler_list_integrations(self):
|
||||
"""Test listing integrations."""
|
||||
from praisonai.cli.features import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integrations = handler.list_integrations()
|
||||
|
||||
assert "claude" in integrations
|
||||
assert "gemini" in integrations
|
||||
assert "codex" in integrations
|
||||
assert "cursor" in integrations
|
||||
|
||||
print("\n📋 Available integrations:", integrations)
|
||||
|
||||
def test_handler_check_availability(self):
|
||||
"""Test checking availability."""
|
||||
from praisonai.cli.features import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
availability = handler.check_availability()
|
||||
|
||||
print("\n📊 Handler Availability Check:")
|
||||
for name, available in availability.items():
|
||||
status = "✅" if available else "❌"
|
||||
print(f" {status} {name}")
|
||||
|
||||
|
||||
class TestPerformanceImpact:
|
||||
"""Test that integrations don't impact performance."""
|
||||
|
||||
def test_lazy_loading_integrations(self):
|
||||
"""Test that integrations are lazy loaded."""
|
||||
import time
|
||||
|
||||
# Measure import time
|
||||
start = time.time()
|
||||
from praisonai.integrations import BaseCLIIntegration
|
||||
import_time = time.time() - start
|
||||
|
||||
print(f"\n⏱️ Import time: {import_time*1000:.2f}ms")
|
||||
|
||||
# Should be fast (< 100ms)
|
||||
assert import_time < 0.1, f"Import took too long: {import_time}s"
|
||||
|
||||
def test_lazy_loading_handler(self):
|
||||
"""Test that handler is lazy loaded."""
|
||||
import time
|
||||
|
||||
# Measure import time
|
||||
start = time.time()
|
||||
from praisonai.cli.features import ExternalAgentsHandler
|
||||
import_time = time.time() - start
|
||||
|
||||
print(f"\n⏱️ Handler import time: {import_time*1000:.2f}ms")
|
||||
|
||||
# Should be fast (< 100ms)
|
||||
assert import_time < 0.1, f"Import took too long: {import_time}s"
|
||||
|
||||
def test_availability_caching(self):
|
||||
"""Test that availability checks are cached."""
|
||||
import time
|
||||
from praisonai.integrations import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
|
||||
# First check
|
||||
start = time.time()
|
||||
_ = integration.is_available
|
||||
first_time = time.time() - start
|
||||
|
||||
# Second check (should be cached)
|
||||
start = time.time()
|
||||
_ = integration.is_available
|
||||
second_time = time.time() - start
|
||||
|
||||
print(f"\n⏱️ First availability check: {first_time*1000:.4f}ms")
|
||||
print(f"⏱️ Second availability check: {second_time*1000:.4f}ms")
|
||||
|
||||
# Both should be very fast (< 10ms) due to caching
|
||||
assert first_time < 0.01, f"First check too slow: {first_time}s"
|
||||
assert second_time < 0.01, f"Second check too slow: {second_time}s"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Tests for ClaudeCodeIntegration.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
|
||||
|
||||
class TestClaudeCodeIntegration:
|
||||
"""Tests for ClaudeCodeIntegration class."""
|
||||
|
||||
def test_import_claude_code_integration(self):
|
||||
"""Test that ClaudeCodeIntegration can be imported."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
assert ClaudeCodeIntegration is not None
|
||||
|
||||
def test_cli_command_is_claude(self):
|
||||
"""Test that cli_command returns 'claude'."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
integration = ClaudeCodeIntegration()
|
||||
assert integration.cli_command == "claude"
|
||||
|
||||
def test_inherits_from_base(self):
|
||||
"""Test that ClaudeCodeIntegration inherits from BaseCLIIntegration."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
from praisonai.integrations.base import BaseCLIIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
assert isinstance(integration, BaseCLIIntegration)
|
||||
|
||||
def test_default_options(self):
|
||||
"""Test default options are set correctly."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
assert integration.output_format == "json"
|
||||
assert integration.skip_permissions is True
|
||||
|
||||
def test_custom_options(self):
|
||||
"""Test custom options can be set."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(
|
||||
output_format="text",
|
||||
skip_permissions=False,
|
||||
system_prompt="You are a Python expert"
|
||||
)
|
||||
assert integration.output_format == "text"
|
||||
assert integration.skip_permissions is False
|
||||
assert integration.system_prompt == "You are a Python expert"
|
||||
|
||||
def test_build_command_basic(self):
|
||||
"""Test building basic command."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert cmd[0] == "claude"
|
||||
assert "-p" in cmd
|
||||
assert "Hello" in cmd
|
||||
assert "--output-format" in cmd
|
||||
assert "json" in cmd
|
||||
|
||||
def test_build_command_with_continue(self):
|
||||
"""Test building command with continue flag."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
cmd = integration._build_command("Hello", continue_session=True)
|
||||
|
||||
assert "--continue" in cmd
|
||||
|
||||
def test_build_command_with_system_prompt(self):
|
||||
"""Test building command with system prompt."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(system_prompt="Be helpful")
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert "--append-system-prompt" in cmd
|
||||
assert "Be helpful" in cmd
|
||||
|
||||
def test_build_command_with_allowed_tools(self):
|
||||
"""Test building command with allowed tools."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(allowed_tools=["Read", "Write"])
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert "--allowedTools" in cmd
|
||||
|
||||
|
||||
class TestClaudeCodeIntegrationAsync:
|
||||
"""Async tests for ClaudeCodeIntegration."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_parses_json_output(self):
|
||||
"""Test that execute parses JSON output correctly."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
|
||||
mock_output = json.dumps({
|
||||
"result": "Hello, world!",
|
||||
"cost_usd": 0.001
|
||||
})
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = mock_output
|
||||
result = await integration.execute("Say hello")
|
||||
|
||||
assert "Hello, world!" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_handles_text_output(self):
|
||||
"""Test that execute handles text output format."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(output_format="text")
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = "Hello, world!"
|
||||
result = await integration.execute("Say hello")
|
||||
|
||||
assert result == "Hello, world!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_yields_events(self):
|
||||
"""Test that stream yields parsed events."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
|
||||
async def mock_stream(*args, **kwargs):
|
||||
yield '{"type": "assistant", "content": "Hello"}'
|
||||
yield '{"type": "result", "content": "Done"}'
|
||||
|
||||
with patch.object(integration, 'stream_async', side_effect=mock_stream):
|
||||
events = []
|
||||
async for event in integration.stream("Say hello"):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) >= 0 # May be empty if stream_async not called correctly
|
||||
|
||||
|
||||
class TestClaudeCodeSDKIntegration:
|
||||
"""Tests for Claude Code SDK integration (when available)."""
|
||||
|
||||
def test_sdk_available_property(self):
|
||||
"""Test sdk_available property."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration()
|
||||
# Should be a boolean
|
||||
assert isinstance(integration.sdk_available, bool)
|
||||
|
||||
def test_use_sdk_option(self):
|
||||
"""Test use_sdk option."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
integration = ClaudeCodeIntegration(use_sdk=True)
|
||||
assert integration.use_sdk is True
|
||||
|
||||
integration2 = ClaudeCodeIntegration(use_sdk=False)
|
||||
assert integration2.use_sdk is False
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Tests for CodexCLIIntegration.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch, AsyncMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
|
||||
|
||||
class TestCodexCLIIntegration:
|
||||
"""Tests for CodexCLIIntegration class."""
|
||||
|
||||
def test_import_codex_cli_integration(self):
|
||||
"""Test that CodexCLIIntegration can be imported."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
assert CodexCLIIntegration is not None
|
||||
|
||||
def test_cli_command_is_codex(self):
|
||||
"""Test that cli_command returns 'codex'."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
integration = CodexCLIIntegration()
|
||||
assert integration.cli_command == "codex"
|
||||
|
||||
def test_inherits_from_base(self):
|
||||
"""Test that CodexCLIIntegration inherits from BaseCLIIntegration."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
from praisonai.integrations.base import BaseCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration()
|
||||
assert isinstance(integration, BaseCLIIntegration)
|
||||
|
||||
def test_default_options(self):
|
||||
"""Test default options are set correctly."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration()
|
||||
assert integration.full_auto is False
|
||||
assert integration.sandbox == "default"
|
||||
|
||||
def test_full_auto_option(self):
|
||||
"""Test full_auto option can be set."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(full_auto=True)
|
||||
assert integration.full_auto is True
|
||||
|
||||
def test_sandbox_option(self):
|
||||
"""Test sandbox option can be set."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(sandbox="danger-full-access")
|
||||
assert integration.sandbox == "danger-full-access"
|
||||
|
||||
def test_build_command_basic(self):
|
||||
"""Test building basic command."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration()
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert cmd[0] == "codex"
|
||||
assert "exec" in cmd
|
||||
assert "Fix the bug" in cmd
|
||||
|
||||
def test_build_command_with_full_auto(self):
|
||||
"""Test building command with full_auto."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(full_auto=True)
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "--full-auto" in cmd
|
||||
|
||||
def test_build_command_with_json(self):
|
||||
"""Test building command with JSON output."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(json_output=True)
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "--json" in cmd
|
||||
|
||||
def test_build_command_with_sandbox(self):
|
||||
"""Test building command with sandbox."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(sandbox="danger-full-access")
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "--sandbox" in cmd
|
||||
assert "danger-full-access" in cmd
|
||||
|
||||
|
||||
class TestCodexCLIIntegrationAsync:
|
||||
"""Async tests for CodexCLIIntegration."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_returns_result(self):
|
||||
"""Test that execute returns the result."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration()
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = "Task completed successfully"
|
||||
result = await integration.execute("Fix the bug")
|
||||
|
||||
assert "Task completed" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_parses_json_events(self):
|
||||
"""Test that execute parses JSON events when json_output is True."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(json_output=True)
|
||||
|
||||
mock_output = '\n'.join([
|
||||
'{"type":"thread.started","thread_id":"123"}',
|
||||
'{"type":"item.completed","item":{"type":"agent_message","text":"Done"}}',
|
||||
'{"type":"turn.completed"}'
|
||||
])
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = mock_output
|
||||
result = await integration.execute("Fix the bug")
|
||||
|
||||
assert "Done" in result
|
||||
|
||||
|
||||
class TestCodexCLIStructuredOutput:
|
||||
"""Tests for Codex CLI structured output."""
|
||||
|
||||
def test_output_schema_option(self):
|
||||
"""Test output_schema option."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(output_schema="/path/to/schema.json")
|
||||
assert integration.output_schema == "/path/to/schema.json"
|
||||
|
||||
def test_build_command_with_output_schema(self):
|
||||
"""Test building command with output schema."""
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
integration = CodexCLIIntegration(output_schema="/path/to/schema.json")
|
||||
cmd = integration._build_command("Extract metadata")
|
||||
|
||||
assert "--output-schema" in cmd
|
||||
assert "/path/to/schema.json" in cmd
|
||||
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Tests for CursorCLIIntegration.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch, AsyncMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
|
||||
|
||||
class TestCursorCLIIntegration:
|
||||
"""Tests for CursorCLIIntegration class."""
|
||||
|
||||
def test_import_cursor_cli_integration(self):
|
||||
"""Test that CursorCLIIntegration can be imported."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
assert CursorCLIIntegration is not None
|
||||
|
||||
def test_cli_command_is_cursor_agent(self):
|
||||
"""Test that cli_command returns 'cursor-agent'."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
integration = CursorCLIIntegration()
|
||||
assert integration.cli_command == "cursor-agent"
|
||||
|
||||
def test_inherits_from_base(self):
|
||||
"""Test that CursorCLIIntegration inherits from BaseCLIIntegration."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
from praisonai.integrations.base import BaseCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
assert isinstance(integration, BaseCLIIntegration)
|
||||
|
||||
def test_default_options(self):
|
||||
"""Test default options are set correctly."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
assert integration.output_format == "json"
|
||||
assert integration.force is False
|
||||
|
||||
def test_force_option(self):
|
||||
"""Test force option can be set."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(force=True)
|
||||
assert integration.force is True
|
||||
|
||||
def test_model_option(self):
|
||||
"""Test model option can be set."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(model="gpt-5")
|
||||
assert integration.model == "gpt-5"
|
||||
|
||||
def test_build_command_basic(self):
|
||||
"""Test building basic command."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert cmd[0] == "cursor-agent"
|
||||
assert "-p" in cmd
|
||||
assert "Fix the bug" in cmd
|
||||
assert "--output-format" in cmd
|
||||
assert "json" in cmd
|
||||
|
||||
def test_build_command_with_force(self):
|
||||
"""Test building command with force flag."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(force=True)
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "--force" in cmd
|
||||
|
||||
def test_build_command_with_model(self):
|
||||
"""Test building command with model."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(model="gpt-5")
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "-m" in cmd
|
||||
assert "gpt-5" in cmd
|
||||
|
||||
def test_build_command_with_stream_partial(self):
|
||||
"""Test building command with stream partial output."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(stream_partial=True)
|
||||
cmd = integration._build_command("Fix the bug")
|
||||
|
||||
assert "--stream-partial-output" in cmd
|
||||
|
||||
|
||||
class TestCursorCLIIntegrationAsync:
|
||||
"""Async tests for CursorCLIIntegration."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_parses_json_output(self):
|
||||
"""Test that execute parses JSON output correctly."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
|
||||
mock_output = json.dumps({
|
||||
"result": "Bug fixed successfully"
|
||||
})
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = mock_output
|
||||
result = await integration.execute("Fix the bug")
|
||||
|
||||
assert "Bug fixed" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_handles_text_output(self):
|
||||
"""Test that execute handles text output format."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(output_format="text")
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = "Bug fixed successfully"
|
||||
result = await integration.execute("Fix the bug")
|
||||
|
||||
assert result == "Bug fixed successfully"
|
||||
|
||||
|
||||
class TestCursorCLIEnvironment:
|
||||
"""Tests for Cursor CLI environment handling."""
|
||||
|
||||
def test_get_env_includes_cursor_api_key(self):
|
||||
"""Test that get_env includes Cursor API key."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
|
||||
with patch.dict(os.environ, {"CURSOR_API_KEY": "test-key"}):
|
||||
env = integration.get_env()
|
||||
assert "CURSOR_API_KEY" in env
|
||||
|
||||
def test_api_key_required_for_headless(self):
|
||||
"""Test that API key is required for headless mode."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration()
|
||||
# Should not raise, just return env without key if not set
|
||||
env = integration.get_env()
|
||||
assert isinstance(env, dict)
|
||||
|
||||
|
||||
class TestCursorCLISession:
|
||||
"""Tests for Cursor CLI session management."""
|
||||
|
||||
def test_resume_option(self):
|
||||
"""Test resume option."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(resume_session="session-123")
|
||||
assert integration.resume_session == "session-123"
|
||||
|
||||
def test_build_command_with_resume(self):
|
||||
"""Test building command with resume."""
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
integration = CursorCLIIntegration(resume_session="session-123")
|
||||
cmd = integration._build_command("Continue the task")
|
||||
|
||||
assert "--resume" in cmd
|
||||
assert "session-123" in cmd
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Tests for ExternalAgentsHandler.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
|
||||
|
||||
class TestExternalAgentsHandler:
|
||||
"""Tests for ExternalAgentsHandler class."""
|
||||
|
||||
def test_import_external_agents_handler(self):
|
||||
"""Test that ExternalAgentsHandler can be imported."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
assert ExternalAgentsHandler is not None
|
||||
|
||||
def test_feature_name(self):
|
||||
"""Test that feature_name is correct."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
handler = ExternalAgentsHandler()
|
||||
assert handler.feature_name == "external_agents"
|
||||
|
||||
def test_flag_name(self):
|
||||
"""Test that flag_name is correct."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
handler = ExternalAgentsHandler()
|
||||
assert handler.flag_name == "external-agent"
|
||||
|
||||
def test_supported_integrations(self):
|
||||
"""Test that all integrations are supported."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
handler = ExternalAgentsHandler()
|
||||
|
||||
assert "claude" in handler.INTEGRATIONS
|
||||
assert "gemini" in handler.INTEGRATIONS
|
||||
assert "codex" in handler.INTEGRATIONS
|
||||
assert "cursor" in handler.INTEGRATIONS
|
||||
|
||||
def test_get_integration_claude(self):
|
||||
"""Test getting Claude integration."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integration = handler.get_integration("claude")
|
||||
|
||||
assert isinstance(integration, ClaudeCodeIntegration)
|
||||
|
||||
def test_get_integration_gemini(self):
|
||||
"""Test getting Gemini integration."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integration = handler.get_integration("gemini")
|
||||
|
||||
assert isinstance(integration, GeminiCLIIntegration)
|
||||
|
||||
def test_get_integration_codex(self):
|
||||
"""Test getting Codex integration."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
from praisonai.integrations.codex_cli import CodexCLIIntegration
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integration = handler.get_integration("codex")
|
||||
|
||||
assert isinstance(integration, CodexCLIIntegration)
|
||||
|
||||
def test_get_integration_cursor(self):
|
||||
"""Test getting Cursor integration."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
from praisonai.integrations.cursor_cli import CursorCLIIntegration
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integration = handler.get_integration("cursor")
|
||||
|
||||
assert isinstance(integration, CursorCLIIntegration)
|
||||
|
||||
def test_get_integration_invalid(self):
|
||||
"""Test getting invalid integration raises error."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.get_integration("invalid")
|
||||
|
||||
def test_list_integrations(self):
|
||||
"""Test listing all integrations."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
integrations = handler.list_integrations()
|
||||
|
||||
assert len(integrations) == 4
|
||||
assert all(name in integrations for name in ["claude", "gemini", "codex", "cursor"])
|
||||
|
||||
def test_check_availability(self):
|
||||
"""Test checking integration availability."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
availability = handler.check_availability()
|
||||
|
||||
assert isinstance(availability, dict)
|
||||
assert "claude" in availability
|
||||
assert isinstance(availability["claude"], bool)
|
||||
|
||||
|
||||
class TestExternalAgentsHandlerConfig:
|
||||
"""Tests for ExternalAgentsHandler configuration."""
|
||||
|
||||
def test_apply_to_agent_config_adds_tool(self):
|
||||
"""Test that apply_to_agent_config adds tool to config."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
config = {"tools": []}
|
||||
|
||||
# Mock the integration to be available
|
||||
with patch.object(handler, 'get_integration') as mock_get:
|
||||
mock_integration = MagicMock()
|
||||
mock_integration.is_available = True
|
||||
mock_integration.as_tool.return_value = lambda x: x
|
||||
mock_get.return_value = mock_integration
|
||||
|
||||
result = handler.apply_to_agent_config(config, "claude")
|
||||
|
||||
assert len(result["tools"]) == 1
|
||||
|
||||
def test_apply_to_agent_config_skips_unavailable(self):
|
||||
"""Test that apply_to_agent_config skips unavailable integrations."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
config = {"tools": []}
|
||||
|
||||
with patch.object(handler, 'get_integration') as mock_get:
|
||||
mock_integration = MagicMock()
|
||||
mock_integration.is_available = False
|
||||
mock_get.return_value = mock_integration
|
||||
|
||||
result = handler.apply_to_agent_config(config, "claude")
|
||||
|
||||
assert len(result["tools"]) == 0
|
||||
|
||||
|
||||
class TestExternalAgentsHandlerExecution:
|
||||
"""Tests for ExternalAgentsHandler execution."""
|
||||
|
||||
def test_execute_returns_integration(self):
|
||||
"""Test that execute returns the integration."""
|
||||
from praisonai.cli.features.external_agents import ExternalAgentsHandler
|
||||
|
||||
handler = ExternalAgentsHandler()
|
||||
|
||||
with patch.object(handler, 'get_integration') as mock_get:
|
||||
mock_integration = MagicMock()
|
||||
mock_integration.is_available = True
|
||||
mock_get.return_value = mock_integration
|
||||
|
||||
result = handler.execute(integration_name="claude")
|
||||
|
||||
assert result == mock_integration
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Tests for GeminiCLIIntegration.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch, AsyncMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
|
||||
|
||||
class TestGeminiCLIIntegration:
|
||||
"""Tests for GeminiCLIIntegration class."""
|
||||
|
||||
def test_import_gemini_cli_integration(self):
|
||||
"""Test that GeminiCLIIntegration can be imported."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
assert GeminiCLIIntegration is not None
|
||||
|
||||
def test_cli_command_is_gemini(self):
|
||||
"""Test that cli_command returns 'gemini'."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
integration = GeminiCLIIntegration()
|
||||
assert integration.cli_command == "gemini"
|
||||
|
||||
def test_inherits_from_base(self):
|
||||
"""Test that GeminiCLIIntegration inherits from BaseCLIIntegration."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
from praisonai.integrations.base import BaseCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
assert isinstance(integration, BaseCLIIntegration)
|
||||
|
||||
def test_default_options(self):
|
||||
"""Test default options are set correctly."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
assert integration.output_format == "json"
|
||||
assert integration.model == "gemini-2.5-pro"
|
||||
|
||||
def test_custom_model(self):
|
||||
"""Test custom model can be set."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration(model="gemini-2.5-flash")
|
||||
assert integration.model == "gemini-2.5-flash"
|
||||
|
||||
def test_build_command_basic(self):
|
||||
"""Test building basic command."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert cmd[0] == "gemini"
|
||||
assert "-p" in cmd
|
||||
assert "Hello" in cmd
|
||||
assert "--output-format" in cmd
|
||||
assert "json" in cmd
|
||||
|
||||
def test_build_command_with_model(self):
|
||||
"""Test building command with model."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration(model="gemini-2.5-flash")
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert "-m" in cmd
|
||||
assert "gemini-2.5-flash" in cmd
|
||||
|
||||
def test_build_command_with_include_directories(self):
|
||||
"""Test building command with include directories."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration(include_directories=["../lib", "../docs"])
|
||||
cmd = integration._build_command("Hello")
|
||||
|
||||
assert "--include-directories" in cmd
|
||||
|
||||
|
||||
class TestGeminiCLIIntegrationAsync:
|
||||
"""Async tests for GeminiCLIIntegration."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_parses_json_output(self):
|
||||
"""Test that execute parses JSON output correctly."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
|
||||
mock_output = json.dumps({
|
||||
"response": "Hello, world!",
|
||||
"stats": {"models": {}}
|
||||
})
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = mock_output
|
||||
result = await integration.execute("Say hello")
|
||||
|
||||
assert "Hello, world!" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_handles_text_output(self):
|
||||
"""Test that execute handles text output format."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration(output_format="text")
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = "Hello, world!"
|
||||
result = await integration.execute("Say hello")
|
||||
|
||||
assert result == "Hello, world!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_stats_returns_usage_info(self):
|
||||
"""Test that get_stats returns usage information."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
|
||||
mock_output = json.dumps({
|
||||
"response": "Hello",
|
||||
"stats": {
|
||||
"models": {
|
||||
"gemini-2.5-pro": {
|
||||
"tokens": {"prompt": 100, "candidates": 50}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with patch.object(integration, 'execute_async', new_callable=AsyncMock) as mock_exec:
|
||||
mock_exec.return_value = mock_output
|
||||
result, stats = await integration.execute_with_stats("Say hello")
|
||||
|
||||
assert stats is not None
|
||||
assert "models" in stats
|
||||
|
||||
|
||||
class TestGeminiCLIEnvironment:
|
||||
"""Tests for Gemini CLI environment handling."""
|
||||
|
||||
def test_get_env_includes_google_api_key(self):
|
||||
"""Test that get_env includes Google API key."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
|
||||
with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}):
|
||||
env = integration.get_env()
|
||||
assert "GOOGLE_API_KEY" in env
|
||||
|
||||
def test_get_env_includes_gemini_api_key(self):
|
||||
"""Test that get_env includes Gemini API key."""
|
||||
from praisonai.integrations.gemini_cli import GeminiCLIIntegration
|
||||
|
||||
integration = GeminiCLIIntegration()
|
||||
|
||||
with patch.dict(os.environ, {"GEMINI_API_KEY": "test-key"}):
|
||||
env = integration.get_env()
|
||||
# Should map GEMINI_API_KEY to GOOGLE_API_KEY
|
||||
assert "GOOGLE_API_KEY" in env or "GEMINI_API_KEY" in env
|
||||
Reference in new issue
Block a user