mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af56a753bd | ||
|
|
15c2559a3c | ||
|
|
3215bdc0a6 | ||
|
|
0ff0d83744 | ||
|
|
8f956f91dd | ||
|
|
954f6ac33d | ||
|
|
264d11d8d9 | ||
|
|
88da2bdd7b | ||
|
|
b84e6c2460 | ||
|
|
0434b23ada | ||
|
|
a9951e0694 | ||
|
|
073062cd96 | ||
|
|
936b422876 | ||
|
|
99ab421ce2 | ||
|
|
c1f334905e |
No files matched your search
@@ -1058,6 +1058,20 @@ praisonai "Complex analysis task" --planning --planning-reasoning
|
||||
praisonai "Task" --planning --auto-approve-plan
|
||||
```
|
||||
|
||||
### Tool Approval CLI:
|
||||
```bash
|
||||
# Auto-approve ALL tool executions (use with caution!)
|
||||
praisonai "run ls command" --trust
|
||||
|
||||
# Auto-approve tools up to a risk level (prompt for higher)
|
||||
# Levels: low, medium, high, critical
|
||||
praisonai "write to file" --approve-level high # Prompts for critical tools only
|
||||
praisonai "task" --approve-level medium # Prompts for high and critical
|
||||
|
||||
# Default behavior (no flags): prompts for all dangerous tools
|
||||
praisonai "run shell command" # Will prompt for approval
|
||||
```
|
||||
|
||||
### Memory CLI:
|
||||
```bash
|
||||
# Enable memory for agent (persists across sessions)
|
||||
@@ -2399,6 +2413,7 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
|
||||
- 🤝 **Agent Handoffs** - Transfer context between specialised agents
|
||||
- 🛡️ **Guardrails** - Input/output validation and safety checks
|
||||
- ✅ **Human Approval** - Require human confirmation for critical actions
|
||||
- 🔐 **Tool Approval CLI** - `--trust` (auto-approve all) and `--approve-level` (risk-based approval)
|
||||
- 💬 **Sessions Management** - Isolated conversation contexts
|
||||
- 🔄 **Stateful Agents** - Maintain state across interactions
|
||||
|
||||
|
||||
@@ -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.52" \
|
||||
"praisonai>=2.3.63" \
|
||||
"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.52" \
|
||||
"praisonai>=2.3.63" \
|
||||
"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.52" \
|
||||
"praisonai>=2.3.63" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@ def _get_all_noisy_loggers() -> List[str]:
|
||||
|
||||
def _configure_loggers():
|
||||
"""Configure all loggers based on LOGLEVEL environment variable."""
|
||||
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
|
||||
loglevel = os.environ.get('LOGLEVEL', 'WARNING').upper()
|
||||
|
||||
# When DEBUG is set, allow some HTTP logging for API endpoints
|
||||
if loglevel == 'DEBUG':
|
||||
# When DEBUG or INFO is set, allow some HTTP logging for API endpoints
|
||||
if loglevel in ('DEBUG', 'INFO'):
|
||||
allowed_debug_loggers = {"httpx", "httpx._client", "openai._client"}
|
||||
|
||||
for logger_name in _get_all_noisy_loggers():
|
||||
@@ -109,10 +109,10 @@ def configure_root_logger():
|
||||
"""Configure the root logger with RichHandler."""
|
||||
from rich.logging import RichHandler
|
||||
|
||||
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
|
||||
loglevel = os.environ.get('LOGLEVEL', 'WARNING').upper()
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, loglevel, logging.INFO),
|
||||
level=getattr(logging, loglevel, logging.WARNING),
|
||||
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(rich_tracebacks=True)],
|
||||
|
||||
@@ -1538,13 +1538,13 @@ Your Goal: {self.goal}"""
|
||||
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
|
||||
|
||||
# Check if approval is required for this tool
|
||||
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, ApprovalDecision
|
||||
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, ApprovalDecision, get_approval_callback
|
||||
if is_approval_required(function_name):
|
||||
risk_level = get_risk_level(function_name)
|
||||
logging.info(f"Tool {function_name} requires approval (risk level: {risk_level})")
|
||||
logging.debug(f"Tool {function_name} requires approval (risk level: {risk_level})")
|
||||
|
||||
# Use global approval callback or default console callback
|
||||
callback = approval_callback or console_approval_callback
|
||||
callback = get_approval_callback() or console_approval_callback
|
||||
|
||||
try:
|
||||
decision = callback(function_name, arguments, risk_level)
|
||||
|
||||
@@ -43,6 +43,15 @@ def set_approval_callback(callback_fn: Callable):
|
||||
global approval_callback
|
||||
approval_callback = callback_fn
|
||||
|
||||
def get_approval_callback() -> Optional[Callable]:
|
||||
"""Get the current approval callback function.
|
||||
|
||||
Returns the custom callback if set, otherwise None.
|
||||
This should be used instead of directly accessing approval_callback
|
||||
to ensure the latest callback is always used.
|
||||
"""
|
||||
return approval_callback
|
||||
|
||||
def mark_approved(tool_name: str):
|
||||
"""Mark a tool as approved in the current context."""
|
||||
approved = _approved_context.get(set())
|
||||
@@ -90,7 +99,7 @@ def require_approval(risk_level: RiskLevel = "high"):
|
||||
except Exception as e:
|
||||
# Fallback to sync approval if async fails
|
||||
logging.warning(f"Async approval failed, using sync fallback: {e}")
|
||||
callback = approval_callback or console_approval_callback
|
||||
callback = get_approval_callback() or console_approval_callback
|
||||
decision = callback(tool_name, kwargs, risk_level)
|
||||
|
||||
if not decision.approved:
|
||||
@@ -198,7 +207,7 @@ async def request_approval(function_name: str, arguments: Dict[str, Any]) -> App
|
||||
risk_level = TOOL_RISK_LEVELS.get(function_name, "medium")
|
||||
|
||||
# Use custom callback if set, otherwise use console callback
|
||||
callback = approval_callback or console_approval_callback
|
||||
callback = get_approval_callback() or console_approval_callback
|
||||
|
||||
try:
|
||||
# Handle async callbacks
|
||||
|
||||
@@ -1054,7 +1054,7 @@ class LLM:
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""Enhanced get_response with all OpenAI-like features"""
|
||||
logging.info(f"Getting response from {self.model}")
|
||||
logging.debug(f"Getting response from {self.model}")
|
||||
# Log all self values when in debug mode
|
||||
self._log_llm_config(
|
||||
'LLM instance',
|
||||
@@ -2472,7 +2472,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
"""Async version of get_response with identical functionality."""
|
||||
try:
|
||||
import litellm
|
||||
logging.info(f"Getting async response from {self.model}")
|
||||
logging.debug(f"Getting async response from {self.model}")
|
||||
# Log all self values when in debug mode
|
||||
self._log_llm_config(
|
||||
'get_response_async',
|
||||
|
||||
@@ -19,6 +19,15 @@ from rich.console import Console
|
||||
from rich.live import Live
|
||||
import inspect
|
||||
|
||||
# Import display_tool_call for callback support (lazy import to avoid circular imports)
|
||||
_display_tool_call = None
|
||||
def _get_display_tool_call():
|
||||
global _display_tool_call
|
||||
if _display_tool_call is None:
|
||||
from ..main import display_tool_call
|
||||
_display_tool_call = display_tool_call
|
||||
return _display_tool_call
|
||||
|
||||
# Constants
|
||||
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
|
||||
|
||||
@@ -978,16 +987,19 @@ class OpenAIClient:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
# Always trigger callback for tool call tracking (even when verbose=False)
|
||||
display_tool_call_fn = _get_display_tool_call()
|
||||
display_tool_call_fn(f"Calling function: {function_name}", console=console if verbose else None)
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[bold]Calling function:[/bold] {function_name}")
|
||||
console.print(f"[dim]Arguments:[/dim] {arguments}")
|
||||
|
||||
# Execute the tool
|
||||
tool_result = execute_tool_fn(function_name, arguments)
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[dim]Result:[/dim] {results_str}")
|
||||
# Trigger callback with result
|
||||
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=console if verbose else None)
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
@@ -1150,8 +1162,11 @@ class OpenAIClient:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
# Always trigger callback for tool call tracking (even when verbose=False)
|
||||
display_tool_call_fn = _get_display_tool_call()
|
||||
display_tool_call_fn(f"Calling function: {function_name}", console=console if verbose else None)
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[bold]Calling function:[/bold] {function_name}")
|
||||
console.print(f"[dim]Arguments:[/dim] {arguments}")
|
||||
|
||||
# Execute the tool (async)
|
||||
@@ -1167,8 +1182,8 @@ class OpenAIClient:
|
||||
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[dim]Result:[/dim] {results_str}")
|
||||
# Trigger callback with result
|
||||
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=console if verbose else None)
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
@@ -1299,6 +1314,10 @@ class OpenAIClient:
|
||||
yield f"\n[Error parsing arguments for {function_name if 'function_name' in locals() else 'unknown function'}: {str(e)}]"
|
||||
continue
|
||||
|
||||
# Always trigger callback for tool call tracking (even when verbose=False)
|
||||
display_tool_call_fn = _get_display_tool_call()
|
||||
display_tool_call_fn(f"Calling function: {function_name}", console=None)
|
||||
|
||||
if verbose:
|
||||
yield f"\n[Calling function: {function_name}]"
|
||||
|
||||
@@ -1311,6 +1330,9 @@ class OpenAIClient:
|
||||
if verbose:
|
||||
yield f"\n[Function error: {str(e)}]"
|
||||
|
||||
# Trigger callback with result
|
||||
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=None)
|
||||
|
||||
if verbose:
|
||||
yield f"\n[Function result: {results_str}]"
|
||||
|
||||
|
||||
@@ -231,15 +231,15 @@ def display_tool_call(message: str, console=None):
|
||||
if not message or not message.strip():
|
||||
logging.debug("Empty message in display_tool_call, returning early")
|
||||
return
|
||||
if console is None:
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
|
||||
|
||||
# Execute synchronous callbacks
|
||||
# Execute synchronous callbacks (always, even when console is None)
|
||||
execute_sync_callback('tool_call', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
# Only print panel if console is provided (verbose mode)
|
||||
if console is not None:
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
|
||||
def display_error(message: str, console=None):
|
||||
if not message or not message.strip():
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.17"
|
||||
version = "0.1.21"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.17"
|
||||
version = "0.1.21"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -1058,6 +1058,20 @@ praisonai "Complex analysis task" --planning --planning-reasoning
|
||||
praisonai "Task" --planning --auto-approve-plan
|
||||
```
|
||||
|
||||
### Tool Approval CLI:
|
||||
```bash
|
||||
# Auto-approve ALL tool executions (use with caution!)
|
||||
praisonai "run ls command" --trust
|
||||
|
||||
# Auto-approve tools up to a risk level (prompt for higher)
|
||||
# Levels: low, medium, high, critical
|
||||
praisonai "write to file" --approve-level high # Prompts for critical tools only
|
||||
praisonai "task" --approve-level medium # Prompts for high and critical
|
||||
|
||||
# Default behavior (no flags): prompts for all dangerous tools
|
||||
praisonai "run shell command" # Will prompt for approval
|
||||
```
|
||||
|
||||
### Memory CLI:
|
||||
```bash
|
||||
# Enable memory for agent (persists across sessions)
|
||||
@@ -2399,6 +2413,7 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
|
||||
- 🤝 **Agent Handoffs** - Transfer context between specialised agents
|
||||
- 🛡️ **Guardrails** - Input/output validation and safety checks
|
||||
- ✅ **Human Approval** - Require human confirmation for critical actions
|
||||
- 🔐 **Tool Approval CLI** - `--trust` (auto-approve all) and `--approve-level` (risk-based approval)
|
||||
- 💬 **Sessions Management** - Isolated conversation contexts
|
||||
- 🔄 **Stateful Agents** - Maintain state across interactions
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# PraisonAI CLI - Benchmark Results
|
||||
|
||||
**Generated:** 2025-12-19 16:04:40
|
||||
|
||||
## Import Times
|
||||
|
||||
| Module | Time (ms) | Target | Status |
|
||||
|--------|-----------|--------|--------|
|
||||
| praisonai | 4.11 | <10 | ✅ |
|
||||
| message_queue | 6.87 | <50 | ✅ |
|
||||
| at_mentions | 0.58 | <50 | ✅ |
|
||||
| profiler | 0.90 | <10 | ✅ |
|
||||
|
||||
## Instantiation Times
|
||||
|
||||
| Class | Time (μs) | Target | Status |
|
||||
|-------|-----------|--------|--------|
|
||||
| MessageQueue | 0.26 | <100 | ✅ |
|
||||
| StateManager | 0.30 | <100 | ✅ |
|
||||
| FileSearchService | 8.42 | <100 | ✅ |
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
| Operation | Time (ms) |
|
||||
|-----------|----------|
|
||||
| PraisonAI lazy load | 3886.47 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai
|
||||
python benchmarks/simple_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI CLI - Performance Benchmark
|
||||
|
||||
Measures import times and module loading performance.
|
||||
|
||||
Usage:
|
||||
python benchmarks/simple_benchmark.py
|
||||
python benchmarks/simple_benchmark.py --no-save
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
|
||||
|
||||
def measure_import(module_name: str) -> float:
|
||||
"""Measure import time in milliseconds."""
|
||||
# Clear from cache if present
|
||||
modules_to_remove = [k for k in sys.modules.keys() if k.startswith(module_name)]
|
||||
for mod in modules_to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
start = time.perf_counter()
|
||||
__import__(module_name)
|
||||
return (time.perf_counter() - start) * 1000
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS) -> float:
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
create_fn()
|
||||
times.append((time.perf_counter() - start) * 1_000_000)
|
||||
return sum(times) / len(times)
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
"""Run the benchmark."""
|
||||
results = {}
|
||||
|
||||
print('=' * 60)
|
||||
print('PraisonAI CLI - Performance Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nIterations: {ITERATIONS}')
|
||||
print('Metrics: Import time (ms), Instantiation time (μs)\n')
|
||||
|
||||
# 1. Package import time
|
||||
print("1. Testing package imports...")
|
||||
|
||||
# Clear cache
|
||||
modules_to_remove = [k for k in sys.modules.keys() if 'praisonai' in k]
|
||||
for mod in modules_to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
start = time.perf_counter()
|
||||
import praisonai
|
||||
results['praisonai_import_ms'] = (time.perf_counter() - start) * 1000
|
||||
print(f" import praisonai: {results['praisonai_import_ms']:.2f}ms")
|
||||
|
||||
# 2. CLI features import
|
||||
print("\n2. Testing CLI features imports...")
|
||||
|
||||
start = time.perf_counter()
|
||||
from praisonai.cli.features.message_queue import MessageQueue
|
||||
results['message_queue_import_ms'] = (time.perf_counter() - start) * 1000
|
||||
print(f" message_queue: {results['message_queue_import_ms']:.2f}ms")
|
||||
|
||||
start = time.perf_counter()
|
||||
from praisonai.cli.features.at_mentions import CombinedCompleter
|
||||
results['at_mentions_import_ms'] = (time.perf_counter() - start) * 1000
|
||||
print(f" at_mentions: {results['at_mentions_import_ms']:.2f}ms")
|
||||
|
||||
# 3. Profiler import (should be fast, standalone)
|
||||
print("\n3. Testing profiler import...")
|
||||
start = time.perf_counter()
|
||||
from praisonai.profiler import Profiler
|
||||
results['profiler_import_ms'] = (time.perf_counter() - start) * 1000
|
||||
print(f" profiler: {results['profiler_import_ms']:.2f}ms")
|
||||
|
||||
# 4. Instantiation benchmarks
|
||||
print(f"\n4. Testing instantiation ({ITERATIONS} iterations)...")
|
||||
|
||||
from praisonai.cli.features.message_queue import MessageQueue, StateManager
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
results['MessageQueue_us'] = measure_instantiation(lambda: MessageQueue())
|
||||
print(f" MessageQueue: {results['MessageQueue_us']:.2f}μs")
|
||||
|
||||
results['StateManager_us'] = measure_instantiation(lambda: StateManager())
|
||||
print(f" StateManager: {results['StateManager_us']:.2f}μs")
|
||||
|
||||
results['FileSearchService_us'] = measure_instantiation(
|
||||
lambda: FileSearchService(root_dir=".")
|
||||
)
|
||||
print(f" FileSearchService: {results['FileSearchService_us']:.2f}μs")
|
||||
|
||||
# 5. PraisonAI class (lazy load)
|
||||
print("\n5. Testing PraisonAI class access (lazy load)...")
|
||||
start = time.perf_counter()
|
||||
PraisonAI = praisonai.PraisonAI
|
||||
results['PraisonAI_lazy_ms'] = (time.perf_counter() - start) * 1000
|
||||
print(f" PraisonAI lazy load: {results['PraisonAI_lazy_ms']:.2f}ms")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def print_summary(results: dict):
|
||||
"""Print benchmark summary."""
|
||||
print('\n' + '=' * 60)
|
||||
print('SUMMARY')
|
||||
print('=' * 60)
|
||||
|
||||
print('\nImport Times (target: <50ms for features, <10ms for package):')
|
||||
print(f" {'Module':<25} {'Time':>10} {'Status':>10}")
|
||||
print(f" {'-'*25} {'-'*10} {'-'*10}")
|
||||
|
||||
import_checks = [
|
||||
('praisonai', results['praisonai_import_ms'], 10),
|
||||
('message_queue', results['message_queue_import_ms'], 50),
|
||||
('at_mentions', results['at_mentions_import_ms'], 50),
|
||||
('profiler', results['profiler_import_ms'], 10),
|
||||
]
|
||||
|
||||
all_pass = True
|
||||
for name, time_ms, threshold in import_checks:
|
||||
status = '✅ PASS' if time_ms < threshold else '❌ SLOW'
|
||||
if time_ms >= threshold:
|
||||
all_pass = False
|
||||
print(f" {name:<25} {time_ms:>8.2f}ms {status:>10}")
|
||||
|
||||
print('\nInstantiation Times (target: <100μs):')
|
||||
print(f" {'Class':<25} {'Time':>10} {'Status':>10}")
|
||||
print(f" {'-'*25} {'-'*10} {'-'*10}")
|
||||
|
||||
inst_checks = [
|
||||
('MessageQueue', results['MessageQueue_us'], 100),
|
||||
('StateManager', results['StateManager_us'], 100),
|
||||
('FileSearchService', results['FileSearchService_us'], 100),
|
||||
]
|
||||
|
||||
for name, time_us, threshold in inst_checks:
|
||||
status = '✅ PASS' if time_us < threshold else '❌ SLOW'
|
||||
if time_us >= threshold:
|
||||
all_pass = False
|
||||
print(f" {name:<25} {time_us:>8.2f}μs {status:>10}")
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
if all_pass:
|
||||
print('✅ All benchmarks PASSED')
|
||||
else:
|
||||
print('❌ Some benchmarks FAILED')
|
||||
print('=' * 60)
|
||||
|
||||
return all_pass
|
||||
|
||||
|
||||
def save_results(results: dict):
|
||||
"""Save benchmark results to markdown file."""
|
||||
filepath = os.path.join(os.path.dirname(__file__), 'BENCHMARK_RESULTS.md')
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI CLI - Benchmark Results\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n\n')
|
||||
|
||||
f.write('## Import Times\n\n')
|
||||
f.write('| Module | Time (ms) | Target | Status |\n')
|
||||
f.write('|--------|-----------|--------|--------|\n')
|
||||
|
||||
import_data = [
|
||||
('praisonai', results['praisonai_import_ms'], 10),
|
||||
('message_queue', results['message_queue_import_ms'], 50),
|
||||
('at_mentions', results['at_mentions_import_ms'], 50),
|
||||
('profiler', results['profiler_import_ms'], 10),
|
||||
]
|
||||
|
||||
for name, time_ms, threshold in import_data:
|
||||
status = '✅' if time_ms < threshold else '❌'
|
||||
f.write(f'| {name} | {time_ms:.2f} | <{threshold} | {status} |\n')
|
||||
|
||||
f.write('\n## Instantiation Times\n\n')
|
||||
f.write('| Class | Time (μs) | Target | Status |\n')
|
||||
f.write('|-------|-----------|--------|--------|\n')
|
||||
|
||||
inst_data = [
|
||||
('MessageQueue', results['MessageQueue_us'], 100),
|
||||
('StateManager', results['StateManager_us'], 100),
|
||||
('FileSearchService', results['FileSearchService_us'], 100),
|
||||
]
|
||||
|
||||
for name, time_us, threshold in inst_data:
|
||||
status = '✅' if time_us < threshold else '❌'
|
||||
f.write(f'| {name} | {time_us:.2f} | <{threshold} | {status} |\n')
|
||||
|
||||
f.write('\n## Lazy Loading\n\n')
|
||||
f.write(f'| Operation | Time (ms) |\n')
|
||||
f.write(f'|-----------|----------|\n')
|
||||
f.write(f'| PraisonAI lazy load | {results["PraisonAI_lazy_ms"]:.2f} |\n')
|
||||
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('cd praisonai\n')
|
||||
f.write('python benchmarks/simple_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='PraisonAI CLI Performance Benchmark')
|
||||
parser.add_argument('--no-save', action='store_true', help='Do not save results to file')
|
||||
args = parser.parse_args()
|
||||
|
||||
results = run_benchmark()
|
||||
all_pass = print_summary(results)
|
||||
|
||||
if not args.no_save:
|
||||
save_results(results)
|
||||
|
||||
return 0 if all_pass else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -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.52.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.52.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.63.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.63.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -6,30 +6,29 @@ logging.getLogger('crewai.cli.config').setLevel(logging.ERROR)
|
||||
import os
|
||||
os.environ["OTEL_SDK_DISABLED"] = "true"
|
||||
os.environ["EC_TELEMETRY"] = "false"
|
||||
from .cli import PraisonAI
|
||||
|
||||
# Version is lightweight, import directly
|
||||
from .version import __version__
|
||||
|
||||
# Re-export all classes from praisonaiagents to enable:
|
||||
# from praisonai import Agent, Task, PraisonAIAgents
|
||||
try:
|
||||
import praisonaiagents
|
||||
# Import all symbols from praisonaiagents using * import
|
||||
from praisonaiagents import *
|
||||
except ImportError:
|
||||
# If praisonaiagents is not available, these imports will fail gracefully
|
||||
pass
|
||||
|
||||
# Define __all__ to include both PraisonAI core classes and praisonaiagents exports
|
||||
# Define __all__ for lazy loading
|
||||
__all__ = [
|
||||
# Core PraisonAI classes
|
||||
'PraisonAI',
|
||||
'__version__',
|
||||
]
|
||||
|
||||
# Dynamically extend __all__ with praisonaiagents exports
|
||||
try:
|
||||
import praisonaiagents
|
||||
__all__.extend(praisonaiagents.__all__)
|
||||
except (ImportError, AttributeError):
|
||||
# If praisonaiagents is not available or doesn't have __all__, fail gracefully
|
||||
pass
|
||||
# Lazy loading for heavy imports
|
||||
def __getattr__(name):
|
||||
"""Lazy load heavy modules to improve import time."""
|
||||
if name == 'PraisonAI':
|
||||
from .cli import PraisonAI
|
||||
return PraisonAI
|
||||
|
||||
# Try praisonaiagents exports
|
||||
try:
|
||||
import praisonaiagents
|
||||
if hasattr(praisonaiagents, name):
|
||||
return getattr(praisonaiagents, name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -8,12 +8,19 @@ Structure:
|
||||
- features/: Feature handlers for CLI flags and commands
|
||||
"""
|
||||
|
||||
from .main import PraisonAI
|
||||
|
||||
__all__ = ["PraisonAI"]
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load PraisonAI to avoid slow imports."""
|
||||
if name == "PraisonAI":
|
||||
from .main import PraisonAI
|
||||
return PraisonAI
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point function."""
|
||||
from .main import PraisonAI
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
@@ -26,6 +26,7 @@ Features:
|
||||
- autonomy_mode: Autonomy levels (suggest, auto-edit, full-auto)
|
||||
- cost_tracker: Real-time cost and token tracking
|
||||
- message_queue: Message queuing while agent is processing
|
||||
- at_mentions: @ mention autocomplete for files and directories
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -58,6 +59,10 @@ __all__ = [
|
||||
'GitIntegrationHandler',
|
||||
'SandboxExecutorHandler',
|
||||
'MessageQueueHandler',
|
||||
# @ mention autocomplete
|
||||
'AtMentionCompleter',
|
||||
'CombinedCompleter',
|
||||
'FileSearchService',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -138,4 +143,14 @@ def __getattr__(name):
|
||||
elif name == 'MessageQueueHandler':
|
||||
from .message_queue import MessageQueueHandler
|
||||
return MessageQueueHandler
|
||||
# @ mention autocomplete
|
||||
elif name == 'AtMentionCompleter':
|
||||
from .at_mentions import AtMentionCompleter
|
||||
return AtMentionCompleter
|
||||
elif name == 'CombinedCompleter':
|
||||
from .at_mentions import CombinedCompleter
|
||||
return CombinedCompleter
|
||||
elif name == 'FileSearchService':
|
||||
from .at_mentions import FileSearchService
|
||||
return FileSearchService
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
@ Mention Autocomplete for PraisonAI CLI.
|
||||
|
||||
Provides file/directory autocomplete when user types @.
|
||||
Uses only stdlib imports for zero performance impact.
|
||||
|
||||
Features:
|
||||
- Detect @ trigger in input
|
||||
- Fuzzy search files/directories
|
||||
- Cache results for performance
|
||||
- Integrate with prompt_toolkit
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import fnmatch
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Classes
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class AtMentionContext:
|
||||
"""Context for an @ mention in user input."""
|
||||
is_active: bool
|
||||
query: str
|
||||
start_pos: int
|
||||
mention_type: str = "file" # file, directory, web, etc.
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileSuggestion:
|
||||
"""A file or directory suggestion."""
|
||||
path: str
|
||||
file_type: str # "file" or "directory"
|
||||
score: int = 100
|
||||
display: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.display:
|
||||
self.display = self.path
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# @ Mention Detection
|
||||
# ============================================================================
|
||||
|
||||
def detect_at_mention(text: str, cursor_pos: int) -> Optional[AtMentionContext]:
|
||||
"""
|
||||
Detect if cursor is within an @ mention context.
|
||||
|
||||
Scans backward from cursor to find @ symbol.
|
||||
Returns None if not in @ context.
|
||||
|
||||
Args:
|
||||
text: Full input text
|
||||
cursor_pos: Current cursor position
|
||||
|
||||
Returns:
|
||||
AtMentionContext if in @ mention, None otherwise
|
||||
"""
|
||||
if cursor_pos <= 0 or cursor_pos > len(text):
|
||||
return None
|
||||
|
||||
# Scan backward from cursor to find @
|
||||
at_pos = -1
|
||||
for i in range(cursor_pos - 1, -1, -1):
|
||||
char = text[i]
|
||||
|
||||
# Found @
|
||||
if char == '@':
|
||||
at_pos = i
|
||||
break
|
||||
|
||||
# Space/newline breaks @ context
|
||||
if char in ' \t\n\r':
|
||||
return None
|
||||
|
||||
if at_pos == -1:
|
||||
return None
|
||||
|
||||
# Extract query between @ and cursor
|
||||
query = text[at_pos + 1:cursor_pos]
|
||||
|
||||
return AtMentionContext(
|
||||
is_active=True,
|
||||
query=query,
|
||||
start_pos=at_pos
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File Search Service
|
||||
# ============================================================================
|
||||
|
||||
class FileSearchService:
|
||||
"""
|
||||
Service for searching files in a directory.
|
||||
|
||||
Features:
|
||||
- Walk directory tree
|
||||
- Fuzzy match file paths
|
||||
- Cache results with TTL
|
||||
- Respect .gitignore (basic)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_dir: str,
|
||||
cache_ttl: int = 30,
|
||||
max_depth: int = 5,
|
||||
ignore_patterns: Optional[List[str]] = None
|
||||
):
|
||||
self._root = os.path.abspath(root_dir)
|
||||
self._cache_ttl = cache_ttl
|
||||
self._max_depth = max_depth
|
||||
self._ignore_patterns = ignore_patterns or [
|
||||
'.git', '__pycache__', 'node_modules', '.venv', 'venv',
|
||||
'*.pyc', '*.pyo', '.DS_Store', '*.egg-info'
|
||||
]
|
||||
|
||||
# Cache: query -> (results, timestamp)
|
||||
self._cache: Dict[str, Tuple[List[FileSuggestion], float]] = {}
|
||||
|
||||
# File list cache
|
||||
self._file_list: Optional[List[str]] = None
|
||||
self._file_list_time: float = 0
|
||||
|
||||
def _should_ignore(self, name: str) -> bool:
|
||||
"""Check if file/dir should be ignored."""
|
||||
for pattern in self._ignore_patterns:
|
||||
if fnmatch.fnmatch(name, pattern):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_file_list(self) -> List[str]:
|
||||
"""Get list of all files (cached)."""
|
||||
now = time.time()
|
||||
|
||||
# Return cached if fresh
|
||||
if self._file_list is not None and (now - self._file_list_time) < self._cache_ttl:
|
||||
return self._file_list
|
||||
|
||||
files = []
|
||||
|
||||
try:
|
||||
for root, dirs, filenames in os.walk(self._root):
|
||||
# Calculate depth
|
||||
rel_root = os.path.relpath(root, self._root)
|
||||
depth = 0 if rel_root == '.' else rel_root.count(os.sep) + 1
|
||||
|
||||
if depth > self._max_depth:
|
||||
dirs.clear() # Don't descend further
|
||||
continue
|
||||
|
||||
# Filter ignored directories
|
||||
dirs[:] = [d for d in dirs if not self._should_ignore(d)]
|
||||
|
||||
# Add directories
|
||||
for d in dirs:
|
||||
rel_path = os.path.relpath(os.path.join(root, d), self._root)
|
||||
files.append(rel_path + os.sep)
|
||||
|
||||
# Add files
|
||||
for f in filenames:
|
||||
if not self._should_ignore(f):
|
||||
rel_path = os.path.relpath(os.path.join(root, f), self._root)
|
||||
files.append(rel_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._file_list = files
|
||||
self._file_list_time = now
|
||||
return files
|
||||
|
||||
def _fuzzy_match(self, text: str, query: str) -> int:
|
||||
"""
|
||||
Simple fuzzy match scoring.
|
||||
|
||||
Returns score 0-100, higher is better.
|
||||
"""
|
||||
if not query:
|
||||
return 50 # Default score for empty query
|
||||
|
||||
text_lower = text.lower()
|
||||
query_lower = query.lower()
|
||||
|
||||
# Exact match
|
||||
if text_lower == query_lower:
|
||||
return 100
|
||||
|
||||
# Starts with
|
||||
if text_lower.startswith(query_lower):
|
||||
return 90
|
||||
|
||||
# Contains
|
||||
if query_lower in text_lower:
|
||||
return 80
|
||||
|
||||
# Fuzzy: all query chars appear in order
|
||||
text_idx = 0
|
||||
query_idx = 0
|
||||
matches = 0
|
||||
|
||||
while text_idx < len(text_lower) and query_idx < len(query_lower):
|
||||
if text_lower[text_idx] == query_lower[query_idx]:
|
||||
matches += 1
|
||||
query_idx += 1
|
||||
text_idx += 1
|
||||
|
||||
if query_idx == len(query_lower):
|
||||
# All query chars found
|
||||
return 50 + int(30 * matches / len(text))
|
||||
|
||||
return 0
|
||||
|
||||
def search(self, query: str, max_results: int = 20) -> List[FileSuggestion]:
|
||||
"""
|
||||
Search for files matching query.
|
||||
|
||||
Args:
|
||||
query: Search query (fuzzy matched)
|
||||
max_results: Maximum results to return
|
||||
|
||||
Returns:
|
||||
List of FileSuggestion sorted by score
|
||||
"""
|
||||
# Check cache
|
||||
cache_key = f"{query}:{max_results}"
|
||||
now = time.time()
|
||||
|
||||
if cache_key in self._cache:
|
||||
results, cached_time = self._cache[cache_key]
|
||||
if (now - cached_time) < self._cache_ttl:
|
||||
return results
|
||||
|
||||
# Get all files
|
||||
all_files = self._get_file_list()
|
||||
|
||||
# Score and filter
|
||||
scored: List[Tuple[int, str]] = []
|
||||
for path in all_files:
|
||||
score = self._fuzzy_match(path, query)
|
||||
if score > 0:
|
||||
scored.append((score, path))
|
||||
|
||||
# Sort by score descending
|
||||
scored.sort(key=lambda x: (-x[0], x[1]))
|
||||
|
||||
# Build results
|
||||
results = []
|
||||
for score, path in scored[:max_results]:
|
||||
file_type = "directory" if path.endswith(os.sep) else "file"
|
||||
results.append(FileSuggestion(
|
||||
path=path,
|
||||
file_type=file_type,
|
||||
score=score
|
||||
))
|
||||
|
||||
# Cache results
|
||||
self._cache[cache_key] = (results, now)
|
||||
|
||||
return results
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear all caches."""
|
||||
self._cache.clear()
|
||||
self._file_list = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Completers for prompt_toolkit
|
||||
# ============================================================================
|
||||
|
||||
# Lazy import to avoid performance impact
|
||||
_Completer = None
|
||||
_Completion = None
|
||||
|
||||
def _get_prompt_toolkit():
|
||||
"""Lazy import prompt_toolkit."""
|
||||
global _Completer, _Completion
|
||||
if _Completer is None:
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
_Completer = Completer
|
||||
_Completion = Completion
|
||||
return _Completer, _Completion
|
||||
|
||||
|
||||
class AtMentionCompleter:
|
||||
"""
|
||||
Completer for @ mentions.
|
||||
|
||||
Shows file suggestions when user types @.
|
||||
Implements prompt_toolkit Completer interface.
|
||||
"""
|
||||
|
||||
def __init__(self, root_dir: str):
|
||||
self._file_service = FileSearchService(root_dir)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""Get completions for @ mentions."""
|
||||
_, Completion = _get_prompt_toolkit()
|
||||
|
||||
text = document.text_before_cursor
|
||||
cursor_pos = len(text)
|
||||
|
||||
# Detect @ context
|
||||
context = detect_at_mention(text, cursor_pos)
|
||||
if context is None or not context.is_active:
|
||||
return
|
||||
|
||||
# Search files
|
||||
results = self._file_service.search(context.query, max_results=15)
|
||||
|
||||
# Yield completions
|
||||
for suggestion in results:
|
||||
# Icon based on type
|
||||
icon = "📁 " if suggestion.file_type == "directory" else "📄 "
|
||||
display = f"{icon}{suggestion.path}"
|
||||
|
||||
# Calculate replacement position
|
||||
# Replace from @ to cursor
|
||||
start_position = -(cursor_pos - context.start_pos)
|
||||
|
||||
yield Completion(
|
||||
text=f"@{suggestion.path}",
|
||||
start_position=start_position,
|
||||
display=display
|
||||
)
|
||||
|
||||
|
||||
def create_combined_completer(commands: List[str], root_dir: str):
|
||||
"""
|
||||
Factory function to create a CombinedCompleter.
|
||||
|
||||
Returns a proper Completer subclass that handles both / and @.
|
||||
"""
|
||||
Completer, Completion = _get_prompt_toolkit()
|
||||
file_service = FileSearchService(root_dir)
|
||||
|
||||
class CombinedCompleter(Completer):
|
||||
"""
|
||||
Combined completer for both / commands and @ mentions.
|
||||
|
||||
Properly inherits from prompt_toolkit Completer.
|
||||
"""
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""Get completions for / or @."""
|
||||
text = document.text_before_cursor.lstrip()
|
||||
|
||||
# Check for slash command
|
||||
if text.startswith('/'):
|
||||
cmd_text = text[1:].lower()
|
||||
for cmd in commands:
|
||||
if cmd.lower().startswith(cmd_text):
|
||||
yield Completion(
|
||||
f'/{cmd}',
|
||||
start_position=-len(text),
|
||||
display=f'/{cmd}'
|
||||
)
|
||||
return
|
||||
|
||||
# Check for @ mention
|
||||
full_text = document.text_before_cursor
|
||||
cursor_pos = len(full_text)
|
||||
|
||||
context = detect_at_mention(full_text, cursor_pos)
|
||||
if context is None or not context.is_active:
|
||||
return
|
||||
|
||||
# Search files
|
||||
results = file_service.search(context.query, max_results=15)
|
||||
|
||||
# Yield completions
|
||||
for suggestion in results:
|
||||
icon = "📁 " if suggestion.file_type == "directory" else "📄 "
|
||||
display = f"{icon}{suggestion.path}"
|
||||
start_position = -(cursor_pos - context.start_pos)
|
||||
|
||||
yield Completion(
|
||||
text=f"@{suggestion.path}",
|
||||
start_position=start_position,
|
||||
display=display
|
||||
)
|
||||
|
||||
return CombinedCompleter()
|
||||
|
||||
|
||||
# Backward compatible alias
|
||||
class CombinedCompleter:
|
||||
"""
|
||||
Wrapper class for backward compatibility.
|
||||
|
||||
Use create_combined_completer() for proper Completer subclass.
|
||||
"""
|
||||
|
||||
def __new__(cls, commands: List[str], root_dir: str):
|
||||
return create_combined_completer(commands, root_dir)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
'AtMentionContext',
|
||||
'FileSuggestion',
|
||||
'detect_at_mention',
|
||||
'FileSearchService',
|
||||
'AtMentionCompleter',
|
||||
'CombinedCompleter',
|
||||
]
|
||||
@@ -59,49 +59,27 @@ def _get_chainlit_run():
|
||||
from chainlit.cli import chainlit_run
|
||||
return chainlit_run
|
||||
|
||||
try:
|
||||
import gradio as gr
|
||||
GRADIO_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
# Use find_spec for fast availability checks (no actual import)
|
||||
import importlib.util
|
||||
GRADIO_AVAILABLE = importlib.util.find_spec("gradio") is not None
|
||||
CALL_MODULE_AVAILABLE = importlib.util.find_spec("praisonai.api.call") is not None
|
||||
CREWAI_AVAILABLE = importlib.util.find_spec("crewai") is not None
|
||||
AUTOGEN_AVAILABLE = importlib.util.find_spec("autogen") is not None
|
||||
PRAISONAI_AVAILABLE = importlib.util.find_spec("praisonaiagents") is not None
|
||||
TRAIN_AVAILABLE = importlib.util.find_spec("unsloth") is not None
|
||||
|
||||
try:
|
||||
import praisonai.api.call as call_module
|
||||
CALL_MODULE_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import crewai
|
||||
CREWAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import autogen
|
||||
AUTOGEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent as PraisonAgent, Task as PraisonTask, PraisonAIAgents
|
||||
PRAISONAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from unsloth import FastLanguageModel
|
||||
TRAIN_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'), format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'WARNING'), format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.getLogger('alembic').setLevel(logging.ERROR)
|
||||
logging.getLogger('gradio').setLevel(logging.ERROR)
|
||||
logging.getLogger('gradio').setLevel(os.environ.get('GRADIO_LOGLEVEL', 'WARNING'))
|
||||
logging.getLogger('rust_logger').setLevel(logging.WARNING)
|
||||
logging.getLogger('duckduckgo').setLevel(logging.ERROR)
|
||||
logging.getLogger('_client').setLevel(logging.ERROR)
|
||||
# Suppress praisonaiagents INFO logs unless LOGLEVEL is explicitly set to debug/info
|
||||
if os.environ.get('LOGLEVEL', '').upper() not in ('DEBUG', 'INFO'):
|
||||
logging.getLogger('praisonaiagents').setLevel(logging.WARNING)
|
||||
logging.getLogger('praisonaiagents.llm').setLevel(logging.WARNING)
|
||||
logging.getLogger('praisonaiagents.llm.llm').setLevel(logging.WARNING)
|
||||
|
||||
def stream_subprocess(command, env=None):
|
||||
"""
|
||||
@@ -720,9 +698,18 @@ class PraisonAI:
|
||||
# Autonomy Mode - control AI action approval
|
||||
parser.add_argument("--autonomy", type=str, choices=["suggest", "auto_edit", "full_auto"], help="Set autonomy mode for AI actions")
|
||||
|
||||
# Tool Approval - control tool execution approval
|
||||
parser.add_argument("--trust", action="store_true", help="Auto-approve all tool executions (skip approval prompts)")
|
||||
parser.add_argument("--approve-level", type=str, choices=["low", "medium", "high", "critical"],
|
||||
help="Auto-approve tools up to this risk level (e.g., --approve-level high approves low/medium/high but prompts for critical)")
|
||||
|
||||
# Sandbox Execution - secure command execution
|
||||
parser.add_argument("--sandbox", type=str, choices=["off", "basic", "strict"], help="Enable sandboxed command execution")
|
||||
|
||||
# External Agent - use external AI CLI tools
|
||||
parser.add_argument("--external-agent", type=str, choices=["claude", "gemini", "codex", "cursor"],
|
||||
help="Use external AI CLI tool (claude, gemini, codex, cursor)")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
args, unknown_args = parser.parse_known_args([])
|
||||
@@ -1532,6 +1519,7 @@ class PraisonAI:
|
||||
args: Parsed command line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from praisonaiagents.memory import WorkflowManager
|
||||
from rich import print
|
||||
from rich.table import Table
|
||||
@@ -2773,6 +2761,7 @@ Provide ONLY the commit message, no explanations."""
|
||||
Returns:
|
||||
Final output from the workflow
|
||||
"""
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from rich import print
|
||||
|
||||
workflow_str = self.args.workflow
|
||||
@@ -2919,6 +2908,8 @@ Provide ONLY the commit message, no explanations."""
|
||||
prompt = f"{mention_context}# Task:\n{prompt}"
|
||||
|
||||
if PRAISONAI_AVAILABLE:
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
agent_config = {
|
||||
"name": "DirectAgent",
|
||||
"role": "Assistant",
|
||||
@@ -2926,10 +2917,18 @@ Provide ONLY the commit message, no explanations."""
|
||||
"backstory": "You are a helpful AI assistant"
|
||||
}
|
||||
|
||||
# Suppress verbose output in interactive mode (no Agent Info, Task, Response panels)
|
||||
if getattr(self, '_interactive_mode', False):
|
||||
# Set verbose=False by default, only enable with --verbose flag
|
||||
# This shows minimal "Generating..." status instead of full Agent Info panels
|
||||
if hasattr(self, 'args') and getattr(self.args, 'verbose', False):
|
||||
agent_config["verbose"] = True
|
||||
else:
|
||||
agent_config["verbose"] = False
|
||||
|
||||
# Load default tools (same as interactive mode)
|
||||
default_tools = self._load_interactive_tools()
|
||||
if default_tools:
|
||||
agent_config["tools"] = default_tools
|
||||
|
||||
# Add llm if specified
|
||||
if hasattr(self, 'args') and self.args.llm:
|
||||
# Check if max_tokens is specified - pass as dict config
|
||||
@@ -2937,7 +2936,7 @@ Provide ONLY the commit message, no explanations."""
|
||||
if max_tokens:
|
||||
# Pass llm as dict with model and max_tokens
|
||||
agent_config["llm"] = {"model": self.args.llm, "max_tokens": max_tokens}
|
||||
print(f"[bold cyan]Max tokens set to: {max_tokens}[/bold cyan]")
|
||||
logging.debug(f"Max tokens set to: {max_tokens}")
|
||||
else:
|
||||
agent_config["llm"] = self.args.llm
|
||||
|
||||
@@ -3010,6 +3009,35 @@ Provide ONLY the commit message, no explanations."""
|
||||
|
||||
# ===== NEW CLI FEATURES INTEGRATION =====
|
||||
|
||||
# Tool Approval - Auto-approve tools based on --trust or --approve-level
|
||||
if getattr(self.args, 'trust', False) or getattr(self.args, 'approve_level', None):
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
if getattr(self.args, 'trust', False):
|
||||
# Auto-approve all tools
|
||||
def auto_approve_all(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved via --trust flag")
|
||||
set_approval_callback(auto_approve_all)
|
||||
print("[bold yellow]⚠️ Trust mode enabled - all tool executions will be auto-approved[/bold yellow]")
|
||||
elif getattr(self.args, 'approve_level', None):
|
||||
# Auto-approve up to specified risk level
|
||||
max_level = self.args.approve_level
|
||||
risk_order = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
||||
max_level_value = risk_order.get(max_level, 3)
|
||||
|
||||
def level_based_approve(function_name, arguments, risk_level):
|
||||
tool_level_value = risk_order.get(risk_level, 4)
|
||||
if tool_level_value <= max_level_value:
|
||||
return ApprovalDecision(approved=True, reason=f"Auto-approved (level {risk_level} <= {max_level})")
|
||||
else:
|
||||
# Signal to pause Live display before showing approval prompt
|
||||
# This is handled by the approval_pending flag in status_info
|
||||
from praisonaiagents.approval import console_approval_callback
|
||||
return console_approval_callback(function_name, arguments, risk_level)
|
||||
|
||||
set_approval_callback(level_based_approve)
|
||||
print(f"[bold cyan]Auto-approve enabled for tools up to '{max_level}' risk level[/bold cyan]")
|
||||
|
||||
# Router - Smart model selection (must be before agent creation)
|
||||
if getattr(self.args, 'router', False):
|
||||
from .features.router import RouterHandler
|
||||
@@ -3050,6 +3078,43 @@ Provide ONLY the commit message, no explanations."""
|
||||
existing_tools = list(mcp_tools)
|
||||
agent_config['tools'] = existing_tools
|
||||
|
||||
# External Agent - Use external AI CLI tools directly
|
||||
if getattr(self.args, 'external_agent', None):
|
||||
from rich.console import Console
|
||||
ext_console = Console()
|
||||
external_agent_name = self.args.external_agent
|
||||
try:
|
||||
from .features.external_agents import ExternalAgentsHandler
|
||||
handler = ExternalAgentsHandler(verbose=getattr(self.args, 'verbose', False))
|
||||
|
||||
# Get workspace from current directory
|
||||
import os
|
||||
workspace = os.getcwd()
|
||||
|
||||
integration = handler.get_integration(external_agent_name, workspace=workspace)
|
||||
|
||||
if integration.is_available:
|
||||
ext_console.print(f"[bold cyan]🔌 Using external agent: {external_agent_name}[/bold cyan]")
|
||||
|
||||
# Run the external agent directly instead of PraisonAI agent
|
||||
import asyncio
|
||||
try:
|
||||
result = asyncio.run(integration.execute(prompt))
|
||||
ext_console.print(f"\n[bold green]Result from {external_agent_name}:[/bold green]")
|
||||
ext_console.print(result)
|
||||
# Return empty string to avoid duplicate printing by caller
|
||||
return ""
|
||||
except Exception as e:
|
||||
ext_console.print(f"[red]Error executing {external_agent_name}: {e}[/red]")
|
||||
return None
|
||||
else:
|
||||
ext_console.print(f"[yellow]⚠️ External agent '{external_agent_name}' is not installed[/yellow]")
|
||||
ext_console.print(f"[dim]Install with: {handler._get_install_instructions(external_agent_name)}[/dim]")
|
||||
return None
|
||||
except Exception as e:
|
||||
ext_console.print(f"[red]Error setting up external agent: {e}[/red]")
|
||||
return None
|
||||
|
||||
# Fast Context - Codebase search
|
||||
if getattr(self.args, 'fast_context', None):
|
||||
from .features.fast_context import FastContextHandler
|
||||
@@ -3112,7 +3177,13 @@ Provide ONLY the commit message, no explanations."""
|
||||
flow.display_workflow_start("Direct Prompt", ["DirectAgent"])
|
||||
|
||||
agent = PraisonAgent(**agent_config)
|
||||
result = agent.start(prompt)
|
||||
|
||||
# Run with minimal status display when verbose=False
|
||||
is_verbose = agent_config.get("verbose", False)
|
||||
if not is_verbose:
|
||||
result = self._run_with_status_display(agent, prompt)
|
||||
else:
|
||||
result = agent.start(prompt)
|
||||
|
||||
# ===== POST-PROCESSING WITH NEW FEATURES =====
|
||||
|
||||
@@ -3236,6 +3307,207 @@ Now, {final_instruction.lower()}:"""
|
||||
print("pip install praisonaiagents # For PraisonAIAgents\n")
|
||||
sys.exit(1)
|
||||
|
||||
def _run_with_status_display(self, agent, prompt):
|
||||
"""
|
||||
Run agent with minimal status display (spinner + tool/handoff updates).
|
||||
|
||||
Shows:
|
||||
- "Generating..." with spinner while processing
|
||||
- Real-time tool call notifications via registered callback
|
||||
- Agent handoff notifications
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.text import Text
|
||||
|
||||
# Import callback registration from praisonaiagents
|
||||
from praisonaiagents import register_display_callback, sync_display_callbacks
|
||||
|
||||
console = Console()
|
||||
|
||||
# Get tool names for display
|
||||
tool_names = []
|
||||
if hasattr(agent, 'tools') and agent.tools:
|
||||
for tool in agent.tools:
|
||||
if hasattr(tool, '__name__'):
|
||||
tool_names.append(tool.__name__)
|
||||
elif hasattr(tool, 'name'):
|
||||
tool_names.append(tool.name)
|
||||
|
||||
status_info = {
|
||||
'status': 'Generating...',
|
||||
'tool_calls': [],
|
||||
'handoffs': [],
|
||||
'done': False,
|
||||
'result': None,
|
||||
'error': None,
|
||||
'start_time': time.time(),
|
||||
'available_tools': tool_names,
|
||||
'approval_pending': False, # Flag to pause Live display during approval
|
||||
'live_instance': None # Reference to Live instance for stopping
|
||||
}
|
||||
|
||||
def tool_call_callback(message):
|
||||
"""Callback triggered when a tool is called."""
|
||||
# Extract tool name from message
|
||||
if "Calling function:" in message:
|
||||
# Format: "Calling function: function_name"
|
||||
parts = message.split("Calling function:")
|
||||
if len(parts) > 1:
|
||||
tool_name = parts[1].strip()
|
||||
if tool_name and tool_name not in status_info['tool_calls']:
|
||||
status_info['tool_calls'].append(tool_name)
|
||||
status_info['status'] = f"Using {tool_name}..."
|
||||
elif "Function " in message and " returned:" in message:
|
||||
# Format: "Function function_name returned: ..."
|
||||
# Tool execution completed, update status
|
||||
status_info['status'] = "Processing result..."
|
||||
|
||||
# Register callback for tool calls
|
||||
register_display_callback('tool_call', tool_call_callback)
|
||||
|
||||
def build_status_display():
|
||||
"""Build the status display text."""
|
||||
elapsed = time.time() - status_info['start_time']
|
||||
|
||||
# Main status with spinner
|
||||
text = Text()
|
||||
text.append("⏳ ", style="cyan")
|
||||
text.append(f"{status_info['status']} ", style="bold")
|
||||
text.append(f"({elapsed:.1f}s)", style="dim")
|
||||
|
||||
# Show available tools on first line
|
||||
if status_info['available_tools'] and not status_info['tool_calls']:
|
||||
tools_str = ', '.join(status_info['available_tools'][:3])
|
||||
if len(status_info['available_tools']) > 3:
|
||||
tools_str += f" +{len(status_info['available_tools']) - 3} more"
|
||||
text.append(f"\n 🔧 Tools: {tools_str}", style="dim")
|
||||
|
||||
# Show recent tool calls
|
||||
if status_info['tool_calls']:
|
||||
text.append("\n")
|
||||
for tool in status_info['tool_calls'][-3:]: # Show last 3
|
||||
text.append(f" ⚙ {tool}", style="dim yellow")
|
||||
text.append("\n")
|
||||
|
||||
# Show handoffs
|
||||
if status_info['handoffs']:
|
||||
for handoff in status_info['handoffs'][-2:]: # Show last 2
|
||||
text.append(f" → {handoff}", style="dim cyan")
|
||||
text.append("\n")
|
||||
|
||||
return text
|
||||
|
||||
def run_agent():
|
||||
"""Run the agent in background thread."""
|
||||
try:
|
||||
status_info['result'] = agent.start(prompt)
|
||||
except Exception as e:
|
||||
status_info['error'] = e
|
||||
finally:
|
||||
status_info['done'] = True
|
||||
|
||||
# Set up approval callback that stops Live display before prompting
|
||||
# Only if not using --trust (which auto-approves everything)
|
||||
if not getattr(self.args, 'trust', False):
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
from rich.prompt import Confirm
|
||||
from rich.panel import Panel
|
||||
|
||||
def cli_approval_with_live_pause(function_name, arguments, risk_level):
|
||||
"""Approval callback that stops Live display before prompting."""
|
||||
# Signal to stop Live display
|
||||
status_info['approval_pending'] = True
|
||||
|
||||
# Wait a moment for Live to stop
|
||||
time.sleep(0.2)
|
||||
|
||||
# Now show the approval prompt
|
||||
risk_colors = {"critical": "bold red", "high": "red", "medium": "yellow", "low": "blue"}
|
||||
risk_color = risk_colors.get(risk_level, "white")
|
||||
|
||||
tool_info = f"[bold]Function:[/] {function_name}\n"
|
||||
tool_info += f"[bold]Risk Level:[/] [{risk_color}]{risk_level.upper()}[/{risk_color}]\n"
|
||||
tool_info += "[bold]Arguments:[/]\n"
|
||||
for key, value in arguments.items():
|
||||
str_value = str(value)[:100] + "..." if len(str(value)) > 100 else str(value)
|
||||
tool_info += f" {key}: {str_value}\n"
|
||||
|
||||
console.print(Panel(tool_info.strip(), title="🔒 Tool Approval Required", border_style=risk_color))
|
||||
|
||||
try:
|
||||
approved = Confirm.ask(f"[{risk_color}]Execute this {risk_level} risk tool?[/{risk_color}]", default=False)
|
||||
status_info['approval_pending'] = False
|
||||
|
||||
if approved:
|
||||
console.print("[green]✅ Approved[/green]")
|
||||
return ApprovalDecision(approved=True, reason="User approved")
|
||||
else:
|
||||
console.print("[red]❌ Denied[/red]")
|
||||
return ApprovalDecision(approved=False, reason="User denied")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
status_info['approval_pending'] = False
|
||||
console.print("\n[red]❌ Cancelled[/red]")
|
||||
return ApprovalDecision(approved=False, reason="User cancelled")
|
||||
|
||||
# Only set if not already set by --approve-level
|
||||
if not getattr(self.args, 'approve_level', None):
|
||||
set_approval_callback(cli_approval_with_live_pause)
|
||||
|
||||
# Start agent in background thread
|
||||
thread = threading.Thread(target=run_agent, daemon=True)
|
||||
thread.start()
|
||||
|
||||
# Show live status while processing
|
||||
try:
|
||||
with Live(build_status_display(), console=console, refresh_per_second=4, transient=True) as live:
|
||||
status_info['live_instance'] = live
|
||||
while not status_info['done']:
|
||||
# Check if approval is pending - stop Live to show prompt
|
||||
if status_info['approval_pending']:
|
||||
break
|
||||
live.update(build_status_display())
|
||||
time.sleep(0.1)
|
||||
|
||||
# If approval was pending, wait for it to complete then restart Live
|
||||
while status_info['approval_pending']:
|
||||
time.sleep(0.1)
|
||||
|
||||
# Continue with Live display if not done
|
||||
if not status_info['done']:
|
||||
with Live(build_status_display(), console=console, refresh_per_second=4, transient=True) as live:
|
||||
while not status_info['done']:
|
||||
if status_info['approval_pending']:
|
||||
break
|
||||
live.update(build_status_display())
|
||||
time.sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[dim]Interrupted[/dim]")
|
||||
# Unregister callback
|
||||
if 'tool_call' in sync_display_callbacks:
|
||||
del sync_display_callbacks['tool_call']
|
||||
return None
|
||||
|
||||
# Wait for thread to complete
|
||||
thread.join(timeout=1.0)
|
||||
|
||||
# Unregister callback to avoid memory leaks
|
||||
if 'tool_call' in sync_display_callbacks:
|
||||
del sync_display_callbacks['tool_call']
|
||||
|
||||
# Handle result
|
||||
if status_info['error']:
|
||||
console.print(f"[red]Error: {status_info['error']}[/red]")
|
||||
return None
|
||||
|
||||
# Show tool calls that were made (if any)
|
||||
if status_info['tool_calls']:
|
||||
console.print(f"[dim]Tools used: {', '.join(status_info['tool_calls'])}[/dim]")
|
||||
|
||||
return status_info['result']
|
||||
|
||||
def _handle_serve_command(self, args, unknown_args):
|
||||
"""
|
||||
Handle the serve command - start API server for agents.
|
||||
@@ -3845,22 +4117,23 @@ Now, {final_instruction.lower()}:"""
|
||||
prompt_session = None
|
||||
try:
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.completion import WordCompleter
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.history import InMemoryHistory
|
||||
from praisonai.cli.features.at_mentions import CombinedCompleter
|
||||
|
||||
# Create completer for slash commands
|
||||
# Create combined completer for / commands and @ mentions
|
||||
commands = ['help', 'exit', 'quit', 'clear', 'tools', 'profile', 'model', 'stats', 'compact', 'undo', 'queue', 'q']
|
||||
slash_completer = WordCompleter(
|
||||
[f'/{cmd}' for cmd in commands],
|
||||
ignore_case=True
|
||||
combined_completer = CombinedCompleter(
|
||||
commands=commands,
|
||||
root_dir=os.getcwd()
|
||||
)
|
||||
|
||||
prompt_session = PromptSession(
|
||||
message="❯ ",
|
||||
completer=slash_completer,
|
||||
completer=combined_completer,
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
history=InMemoryHistory()
|
||||
history=InMemoryHistory(),
|
||||
complete_while_typing=True
|
||||
)
|
||||
except ImportError:
|
||||
pass # Fall back to simple input
|
||||
|
||||
@@ -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.52 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.63 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ class BaseCLIIntegration(ABC):
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
cwd=self.workspace,
|
||||
env=self.get_env()
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -173,7 +174,8 @@ class BaseCLIIntegration(ABC):
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
cwd=self.workspace,
|
||||
env=self.get_env()
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -208,7 +210,8 @@ class BaseCLIIntegration(ABC):
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.workspace
|
||||
cwd=self.workspace,
|
||||
env=self.get_env()
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -127,23 +127,16 @@ class ClaudeCodeIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
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
|
||||
# Add print mode flag for non-interactive output
|
||||
cmd.append("-p")
|
||||
|
||||
# Add prompt
|
||||
cmd.append(prompt)
|
||||
|
||||
# Add output format
|
||||
cmd.extend(["--output-format", self.output_format])
|
||||
|
||||
# Add continue flag if needed
|
||||
if continue_session or self._session_active:
|
||||
cmd.append("--continue")
|
||||
|
||||
# Add model if specified
|
||||
if self.model:
|
||||
cmd.extend(["--model", self.model])
|
||||
@@ -160,6 +153,13 @@ class ClaudeCodeIntegration(BaseCLIIntegration):
|
||||
if self.disallowed_tools:
|
||||
cmd.extend(["--disallowedTools", ",".join(self.disallowed_tools)])
|
||||
|
||||
# Add verbose if needed
|
||||
if options.get('verbose'):
|
||||
cmd.append("--verbose")
|
||||
|
||||
# Add prompt last
|
||||
cmd.append(prompt)
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
|
||||
@@ -93,6 +93,9 @@ class CodexCLIIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
cmd = ["codex", "exec"]
|
||||
|
||||
# Add working directory
|
||||
cmd.extend(["-C", self.workspace])
|
||||
|
||||
# Add task
|
||||
cmd.append(task)
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ class CursorCLIIntegration(BaseCLIIntegration):
|
||||
# Add print mode flag
|
||||
cmd.append("-p")
|
||||
|
||||
# Add workspace
|
||||
cmd.extend(["--workspace", self.workspace])
|
||||
|
||||
# Add force flag if enabled
|
||||
if self.force:
|
||||
cmd.append("--force")
|
||||
|
||||
@@ -92,12 +92,6 @@ class GeminiCLIIntegration(BaseCLIIntegration):
|
||||
"""
|
||||
cmd = ["gemini"]
|
||||
|
||||
# Add print mode flag
|
||||
cmd.append("-p")
|
||||
|
||||
# Add prompt
|
||||
cmd.append(prompt)
|
||||
|
||||
# Add model
|
||||
cmd.extend(["-m", self.model])
|
||||
|
||||
@@ -112,6 +106,12 @@ class GeminiCLIIntegration(BaseCLIIntegration):
|
||||
if self.sandbox:
|
||||
cmd.append("--sandbox")
|
||||
|
||||
# Add YOLO mode for non-interactive execution
|
||||
cmd.append("--yolo")
|
||||
|
||||
# Add prompt as positional argument (must be last)
|
||||
cmd.append(prompt)
|
||||
|
||||
return cmd
|
||||
|
||||
async def execute(self, prompt: str, **options) -> str:
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
"""
|
||||
PraisonAI Profiler Module
|
||||
|
||||
Standardized profiling for performance monitoring across praisonai and praisonai-agents.
|
||||
|
||||
Features:
|
||||
- Import timing
|
||||
- Function execution timing
|
||||
- Flow tracking
|
||||
- File/module usage tracking
|
||||
- Memory usage (optional)
|
||||
|
||||
Usage:
|
||||
from praisonai.profiler import Profiler, profile, profile_imports
|
||||
|
||||
# Profile a function
|
||||
@profile
|
||||
def my_function():
|
||||
pass
|
||||
|
||||
# Profile a block
|
||||
with Profiler.block("my_operation"):
|
||||
do_something()
|
||||
|
||||
# Profile imports
|
||||
with profile_imports():
|
||||
import heavy_module
|
||||
|
||||
# Get report
|
||||
Profiler.report()
|
||||
"""
|
||||
|
||||
import time
|
||||
import functools
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Classes
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class TimingRecord:
|
||||
"""Record of a single timing measurement."""
|
||||
name: str
|
||||
duration_ms: float
|
||||
category: str = "function"
|
||||
file: str = ""
|
||||
line: int = 0
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportRecord:
|
||||
"""Record of a module import."""
|
||||
module: str
|
||||
duration_ms: float
|
||||
parent: str = ""
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowRecord:
|
||||
"""Record of execution flow."""
|
||||
step: int
|
||||
name: str
|
||||
file: str
|
||||
line: int
|
||||
duration_ms: float
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profiler Class
|
||||
# ============================================================================
|
||||
|
||||
class Profiler:
|
||||
"""
|
||||
Centralized profiler for performance monitoring.
|
||||
|
||||
Thread-safe singleton pattern for global access.
|
||||
"""
|
||||
|
||||
_instance: Optional['Profiler'] = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
# Class-level storage
|
||||
_timings: List[TimingRecord] = []
|
||||
_imports: List[ImportRecord] = []
|
||||
_flow: List[FlowRecord] = []
|
||||
_enabled: bool = False
|
||||
_flow_step: int = 0
|
||||
_files_accessed: Dict[str, int] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def enable(cls) -> None:
|
||||
"""Enable profiling."""
|
||||
cls._enabled = True
|
||||
|
||||
@classmethod
|
||||
def disable(cls) -> None:
|
||||
"""Disable profiling."""
|
||||
cls._enabled = False
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
"""Check if profiling is enabled."""
|
||||
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
"""Clear all profiling data."""
|
||||
with cls._lock:
|
||||
cls._timings.clear()
|
||||
cls._imports.clear()
|
||||
cls._flow.clear()
|
||||
cls._flow_step = 0
|
||||
cls._files_accessed.clear()
|
||||
|
||||
@classmethod
|
||||
def record_timing(cls, name: str, duration_ms: float, category: str = "function",
|
||||
file: str = "", line: int = 0) -> None:
|
||||
"""Record a timing measurement."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
|
||||
with cls._lock:
|
||||
cls._timings.append(TimingRecord(
|
||||
name=name,
|
||||
duration_ms=duration_ms,
|
||||
category=category,
|
||||
file=file,
|
||||
line=line
|
||||
))
|
||||
|
||||
# Track file access
|
||||
if file:
|
||||
cls._files_accessed[file] = cls._files_accessed.get(file, 0) + 1
|
||||
|
||||
@classmethod
|
||||
def record_import(cls, module: str, duration_ms: float, parent: str = "") -> None:
|
||||
"""Record an import timing."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
|
||||
with cls._lock:
|
||||
cls._imports.append(ImportRecord(
|
||||
module=module,
|
||||
duration_ms=duration_ms,
|
||||
parent=parent
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def record_flow(cls, name: str, duration_ms: float, file: str = "", line: int = 0) -> None:
|
||||
"""Record a flow step."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
|
||||
with cls._lock:
|
||||
cls._flow_step += 1
|
||||
cls._flow.append(FlowRecord(
|
||||
step=cls._flow_step,
|
||||
name=name,
|
||||
file=file,
|
||||
line=line,
|
||||
duration_ms=duration_ms
|
||||
))
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def block(cls, name: str, category: str = "block"):
|
||||
"""Context manager for profiling a block of code."""
|
||||
start = time.time()
|
||||
frame = sys._getframe(2) if hasattr(sys, '_getframe') else None
|
||||
file = frame.f_code.co_filename if frame else ""
|
||||
line = frame.f_lineno if frame else 0
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
cls.record_timing(name, duration_ms, category, file, line)
|
||||
cls.record_flow(name, duration_ms, file, line)
|
||||
|
||||
@classmethod
|
||||
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
|
||||
"""Get timing records, optionally filtered by category."""
|
||||
with cls._lock:
|
||||
if category:
|
||||
return [t for t in cls._timings if t.category == category]
|
||||
return cls._timings.copy()
|
||||
|
||||
@classmethod
|
||||
def get_imports(cls, min_duration_ms: float = 0) -> List[ImportRecord]:
|
||||
"""Get import records, optionally filtered by minimum duration."""
|
||||
with cls._lock:
|
||||
if min_duration_ms > 0:
|
||||
return [i for i in cls._imports if i.duration_ms >= min_duration_ms]
|
||||
return cls._imports.copy()
|
||||
|
||||
@classmethod
|
||||
def get_flow(cls) -> List[FlowRecord]:
|
||||
"""Get flow records."""
|
||||
with cls._lock:
|
||||
return cls._flow.copy()
|
||||
|
||||
@classmethod
|
||||
def get_files_accessed(cls) -> Dict[str, int]:
|
||||
"""Get files accessed with counts."""
|
||||
with cls._lock:
|
||||
return cls._files_accessed.copy()
|
||||
|
||||
@classmethod
|
||||
def get_summary(cls) -> Dict[str, Any]:
|
||||
"""Get profiling summary."""
|
||||
with cls._lock:
|
||||
total_time = sum(t.duration_ms for t in cls._timings)
|
||||
import_time = sum(i.duration_ms for i in cls._imports)
|
||||
|
||||
# Group by category
|
||||
by_category: Dict[str, float] = {}
|
||||
for t in cls._timings:
|
||||
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
|
||||
|
||||
# Top slowest
|
||||
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:10]
|
||||
slowest_imports = sorted(cls._imports, key=lambda x: x.duration_ms, reverse=True)[:10]
|
||||
|
||||
return {
|
||||
'total_time_ms': total_time,
|
||||
'import_time_ms': import_time,
|
||||
'timing_count': len(cls._timings),
|
||||
'import_count': len(cls._imports),
|
||||
'flow_steps': len(cls._flow),
|
||||
'files_accessed': len(cls._files_accessed),
|
||||
'by_category': by_category,
|
||||
'slowest_operations': [(s.name, s.duration_ms) for s in slowest],
|
||||
'slowest_imports': [(s.module, s.duration_ms) for s in slowest_imports],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def report(cls, output: str = "console") -> str:
|
||||
"""Generate and output profiling report."""
|
||||
summary = cls.get_summary()
|
||||
|
||||
lines = [
|
||||
"=" * 60,
|
||||
"PraisonAI Profiling Report",
|
||||
"=" * 60,
|
||||
"",
|
||||
f"Total Time: {summary['total_time_ms']:.2f}ms",
|
||||
f"Import Time: {summary['import_time_ms']:.2f}ms",
|
||||
f"Timing Records: {summary['timing_count']}",
|
||||
f"Import Records: {summary['import_count']}",
|
||||
f"Flow Steps: {summary['flow_steps']}",
|
||||
f"Files Accessed: {summary['files_accessed']}",
|
||||
"",
|
||||
"By Category:",
|
||||
]
|
||||
|
||||
for cat, time_ms in summary['by_category'].items():
|
||||
lines.append(f" {cat}: {time_ms:.2f}ms")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"Slowest Operations:",
|
||||
])
|
||||
for name, time_ms in summary['slowest_operations']:
|
||||
lines.append(f" {name}: {time_ms:.2f}ms")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"Slowest Imports:",
|
||||
])
|
||||
for module, time_ms in summary['slowest_imports']:
|
||||
lines.append(f" {module}: {time_ms:.2f}ms")
|
||||
|
||||
lines.append("=" * 60)
|
||||
|
||||
report_text = "\n".join(lines)
|
||||
|
||||
if output == "console":
|
||||
print(report_text)
|
||||
|
||||
return report_text
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Decorators
|
||||
# ============================================================================
|
||||
|
||||
def profile(func: Optional[Callable] = None, *, category: str = "function"):
|
||||
"""
|
||||
Decorator to profile a function.
|
||||
|
||||
Usage:
|
||||
@profile
|
||||
def my_function():
|
||||
pass
|
||||
|
||||
@profile(category="api")
|
||||
def api_call():
|
||||
pass
|
||||
"""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
|
||||
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
|
||||
Profiler.record_flow(fn.__name__, duration_ms, file, line)
|
||||
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
|
||||
"""
|
||||
Decorator to profile an async function.
|
||||
"""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return await fn(*args, **kwargs)
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
|
||||
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
|
||||
Profiler.record_flow(fn.__name__, duration_ms, file, line)
|
||||
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Import Profiling
|
||||
# ============================================================================
|
||||
|
||||
class ImportProfiler:
|
||||
"""
|
||||
Context manager to profile imports.
|
||||
|
||||
Usage:
|
||||
with profile_imports() as profiler:
|
||||
import heavy_module
|
||||
|
||||
print(profiler.get_imports())
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._original_import = None
|
||||
self._imports: List[ImportRecord] = []
|
||||
|
||||
def __enter__(self):
|
||||
import builtins
|
||||
self._original_import = builtins.__import__
|
||||
|
||||
def profiled_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
start = time.time()
|
||||
try:
|
||||
return self._original_import(name, globals, locals, fromlist, level)
|
||||
finally:
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
if duration_ms > 1: # Only record imports > 1ms
|
||||
record = ImportRecord(module=name, duration_ms=duration_ms)
|
||||
self._imports.append(record)
|
||||
Profiler.record_import(name, duration_ms)
|
||||
|
||||
builtins.__import__ = profiled_import
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
import builtins
|
||||
builtins.__import__ = self._original_import
|
||||
return False
|
||||
|
||||
def get_imports(self, min_duration_ms: float = 0) -> List[ImportRecord]:
|
||||
"""Get recorded imports."""
|
||||
if min_duration_ms > 0:
|
||||
return [i for i in self._imports if i.duration_ms >= min_duration_ms]
|
||||
return self._imports.copy()
|
||||
|
||||
def get_slowest(self, n: int = 10) -> List[ImportRecord]:
|
||||
"""Get N slowest imports."""
|
||||
return sorted(self._imports, key=lambda x: x.duration_ms, reverse=True)[:n]
|
||||
|
||||
|
||||
def profile_imports():
|
||||
"""Create an import profiler context manager."""
|
||||
return ImportProfiler()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Quick Profiling Functions
|
||||
# ============================================================================
|
||||
|
||||
def time_import(module_name: str) -> float:
|
||||
"""
|
||||
Time how long it takes to import a module.
|
||||
|
||||
Returns duration in milliseconds.
|
||||
"""
|
||||
start = time.time()
|
||||
__import__(module_name)
|
||||
return (time.time() - start) * 1000
|
||||
|
||||
|
||||
def check_module_available(module_name: str) -> bool:
|
||||
"""
|
||||
Check if a module is available without importing it.
|
||||
|
||||
Uses importlib.util.find_spec which is fast.
|
||||
"""
|
||||
import importlib.util
|
||||
return importlib.util.find_spec(module_name) is not None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
'Profiler',
|
||||
'profile',
|
||||
'profile_async',
|
||||
'profile_imports',
|
||||
'ImportProfiler',
|
||||
'time_import',
|
||||
'check_module_available',
|
||||
'TimingRecord',
|
||||
'ImportRecord',
|
||||
'FlowRecord',
|
||||
]
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.52"
|
||||
__version__ = "2.3.63"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.17",
|
||||
"praisonaiagents>=0.1.21",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -83,13 +83,19 @@ def setup_test_environment(request):
|
||||
# Real tests (marked with @pytest.mark.real) should use actual environment variables
|
||||
is_real_test = False
|
||||
|
||||
# Check if this test is marked as a real test
|
||||
# Check if this test is marked as a real test or integration test
|
||||
if hasattr(request, 'node') and hasattr(request.node, 'iter_markers'):
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name == 'real':
|
||||
if marker.name in ('real', 'integration'):
|
||||
is_real_test = True
|
||||
break
|
||||
|
||||
# Also check if the test file is in the integration directory
|
||||
if hasattr(request, 'fspath') and request.fspath:
|
||||
test_path = str(request.fspath)
|
||||
if '/integration/' in test_path or '\\integration\\' in test_path:
|
||||
is_real_test = True
|
||||
|
||||
# Store original values to restore later
|
||||
original_values = {}
|
||||
|
||||
|
||||
@@ -14,9 +14,17 @@ import yaml
|
||||
# Add the source path for imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Skip if no API key
|
||||
if not os.environ.get('OPENAI_API_KEY'):
|
||||
pytest.skip("OPENAI_API_KEY not set", allow_module_level=True)
|
||||
# Skip if no API key or using test/invalid key
|
||||
# Valid OpenAI keys start with 'sk-' and are typically 51+ characters
|
||||
api_key = os.environ.get('OPENAI_API_KEY', '')
|
||||
is_test_key = (
|
||||
not api_key or
|
||||
'test' in api_key.lower() or
|
||||
not api_key.startswith('sk-') or
|
||||
len(api_key) < 40
|
||||
)
|
||||
if is_test_key:
|
||||
pytest.skip("OPENAI_API_KEY not set or using test/invalid key", allow_module_level=True)
|
||||
|
||||
try:
|
||||
from praisonai.auto import AutoGenerator, WorkflowAutoGenerator
|
||||
|
||||
@@ -19,10 +19,15 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
|
||||
# Skip all tests if no API keys are set
|
||||
# Skip all tests if no API keys are set or using test/invalid key
|
||||
# Valid OpenAI keys start with 'sk-' and are typically 51+ characters
|
||||
_api_key = os.environ.get("OPENAI_API_KEY", "")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set"
|
||||
not _api_key or
|
||||
'test' in _api_key.lower() or
|
||||
not _api_key.startswith('sk-') or
|
||||
len(_api_key) < 40,
|
||||
reason="OPENAI_API_KEY not set or using test/invalid key"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -220,8 +220,11 @@ 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"
|
||||
not os.getenv('OPENAI_API_KEY') or
|
||||
'test' in os.getenv('OPENAI_API_KEY', '').lower() or
|
||||
not os.getenv('OPENAI_API_KEY', '').startswith('sk-') or
|
||||
len(os.getenv('OPENAI_API_KEY', '')) < 40,
|
||||
reason="OPENAI_API_KEY not set or using test/invalid key"
|
||||
)
|
||||
def test_real_openai_call(self):
|
||||
"""Test real OpenAI API call with routed model."""
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Tests for the CLI approval system.
|
||||
|
||||
Tests:
|
||||
1. --trust flag auto-approves all tools
|
||||
2. --approve-level flag with different risk levels
|
||||
3. Approval callback integration
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
|
||||
class TestApprovalFlags:
|
||||
"""Test CLI approval flags."""
|
||||
|
||||
def test_trust_flag_exists(self):
|
||||
"""Test that --trust flag is defined in argparse."""
|
||||
import argparse
|
||||
|
||||
# Check that the args object has the trust attribute after parsing
|
||||
# Parse with --help to avoid actually running anything
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--trust", action="store_true")
|
||||
parser.add_argument("--approve-level", type=str, choices=["low", "medium", "high", "critical"])
|
||||
|
||||
# Test that the arguments can be parsed
|
||||
args = parser.parse_args(['--trust'])
|
||||
assert args.trust is True
|
||||
|
||||
args = parser.parse_args([])
|
||||
assert args.trust is False
|
||||
|
||||
def test_approve_level_flag_exists(self):
|
||||
"""Test that --approve-level flag is defined in argparse."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--approve-level", type=str, choices=["low", "medium", "high", "critical"])
|
||||
|
||||
# Test that the arguments can be parsed
|
||||
args = parser.parse_args(['--approve-level', 'high'])
|
||||
assert args.approve_level == 'high'
|
||||
|
||||
args = parser.parse_args(['--approve-level', 'critical'])
|
||||
assert args.approve_level == 'critical'
|
||||
|
||||
def test_trust_flag_in_praisonai_parser(self):
|
||||
"""Test that --trust flag is in PraisonAI's actual parser."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
# Create instance - parse_args is called in __init__
|
||||
praison = PraisonAI()
|
||||
|
||||
# Call parse_args to get the args
|
||||
args = praison.parse_args()
|
||||
|
||||
# The args should have trust attribute after parse_args
|
||||
assert hasattr(args, 'trust'), "PraisonAI.args should have 'trust' attribute"
|
||||
# Cleanup
|
||||
del praison
|
||||
|
||||
def test_approve_level_flag_in_praisonai_parser(self):
|
||||
"""Test that --approve-level flag is in PraisonAI's actual parser."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
# Create instance - parse_args is called in __init__
|
||||
praison = PraisonAI()
|
||||
|
||||
# Call parse_args to get the args
|
||||
args = praison.parse_args()
|
||||
|
||||
# The args should have approve_level attribute after parse_args
|
||||
assert hasattr(args, 'approve_level'), "PraisonAI.args should have 'approve_level' attribute"
|
||||
|
||||
|
||||
class TestApprovalCallback:
|
||||
"""Test approval callback system."""
|
||||
|
||||
def test_set_approval_callback(self):
|
||||
"""Test that we can set a custom approval callback."""
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
callback_called = []
|
||||
|
||||
def custom_callback(function_name, arguments, risk_level):
|
||||
callback_called.append((function_name, arguments, risk_level))
|
||||
return ApprovalDecision(approved=True, reason="Test approved")
|
||||
|
||||
set_approval_callback(custom_callback)
|
||||
|
||||
# Verify callback was set
|
||||
from praisonaiagents.approval import approval_callback
|
||||
assert approval_callback == custom_callback
|
||||
|
||||
# Reset callback
|
||||
set_approval_callback(None)
|
||||
|
||||
def test_auto_approve_callback(self):
|
||||
"""Test auto-approve callback returns approved decision."""
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved")
|
||||
|
||||
decision = auto_approve_callback("execute_command", {"command": "ls"}, "critical")
|
||||
assert decision.approved is True
|
||||
assert decision.reason == "Auto-approved"
|
||||
|
||||
def test_level_based_callback(self):
|
||||
"""Test level-based approval callback."""
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
|
||||
def level_based_callback(function_name, arguments, risk_level, max_level="high"):
|
||||
"""Approve based on risk level threshold."""
|
||||
levels = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
||||
tool_level = levels.get(risk_level, 4)
|
||||
max_allowed = levels.get(max_level, 3)
|
||||
|
||||
if tool_level <= max_allowed:
|
||||
return ApprovalDecision(approved=True, reason=f"Auto-approved (level {risk_level} <= {max_level})")
|
||||
else:
|
||||
return ApprovalDecision(approved=False, reason=f"Denied (level {risk_level} > {max_level})")
|
||||
|
||||
# Test with max_level="high"
|
||||
# Should approve low, medium, high but deny critical
|
||||
assert level_based_callback("test", {}, "low", "high").approved is True
|
||||
assert level_based_callback("test", {}, "medium", "high").approved is True
|
||||
assert level_based_callback("test", {}, "high", "high").approved is True
|
||||
assert level_based_callback("test", {}, "critical", "high").approved is False
|
||||
|
||||
# Test with max_level="critical"
|
||||
# Should approve everything
|
||||
assert level_based_callback("test", {}, "critical", "critical").approved is True
|
||||
|
||||
|
||||
class TestApprovalRequirements:
|
||||
"""Test approval requirement configuration."""
|
||||
|
||||
def test_default_dangerous_tools(self):
|
||||
"""Test that default dangerous tools are configured."""
|
||||
from praisonaiagents.approval import APPROVAL_REQUIRED_TOOLS, TOOL_RISK_LEVELS
|
||||
|
||||
# Check critical tools
|
||||
assert "execute_command" in APPROVAL_REQUIRED_TOOLS
|
||||
assert TOOL_RISK_LEVELS.get("execute_command") == "critical"
|
||||
|
||||
# Check high risk tools
|
||||
assert "write_file" in APPROVAL_REQUIRED_TOOLS
|
||||
assert TOOL_RISK_LEVELS.get("write_file") == "high"
|
||||
|
||||
def test_remove_approval_requirement(self):
|
||||
"""Test removing approval requirement."""
|
||||
from praisonaiagents.approval import (
|
||||
add_approval_requirement,
|
||||
remove_approval_requirement,
|
||||
is_approval_required,
|
||||
APPROVAL_REQUIRED_TOOLS
|
||||
)
|
||||
|
||||
# Add a test tool
|
||||
add_approval_requirement("test_tool", "medium")
|
||||
assert is_approval_required("test_tool") is True
|
||||
|
||||
# Remove it
|
||||
remove_approval_requirement("test_tool")
|
||||
assert is_approval_required("test_tool") is False
|
||||
|
||||
def test_is_approval_required(self):
|
||||
"""Test checking if approval is required."""
|
||||
from praisonaiagents.approval import is_approval_required
|
||||
|
||||
# execute_command should require approval
|
||||
assert is_approval_required("execute_command") is True
|
||||
|
||||
# read_file should not require approval
|
||||
assert is_approval_required("read_file") is False
|
||||
|
||||
# list_files should not require approval
|
||||
assert is_approval_required("list_files") is False
|
||||
|
||||
|
||||
class TestApprovalDecision:
|
||||
"""Test ApprovalDecision class."""
|
||||
|
||||
def test_approval_decision_approved(self):
|
||||
"""Test approved decision."""
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
|
||||
decision = ApprovalDecision(approved=True, reason="User approved")
|
||||
assert decision.approved is True
|
||||
assert decision.reason == "User approved"
|
||||
assert decision.modified_args == {}
|
||||
|
||||
def test_approval_decision_denied(self):
|
||||
"""Test denied decision."""
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
|
||||
decision = ApprovalDecision(approved=False, reason="User denied")
|
||||
assert decision.approved is False
|
||||
assert decision.reason == "User denied"
|
||||
|
||||
def test_approval_decision_with_modified_args(self):
|
||||
"""Test decision with modified arguments."""
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
|
||||
decision = ApprovalDecision(
|
||||
approved=True,
|
||||
reason="Approved with modifications",
|
||||
modified_args={"command": "ls -la"}
|
||||
)
|
||||
assert decision.approved is True
|
||||
assert decision.modified_args == {"command": "ls -la"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
Unit tests for @ mention autocomplete feature.
|
||||
|
||||
Tests for:
|
||||
1. AtMentionContext - Context detection
|
||||
2. FileSearchService - File search with fuzzy matching
|
||||
3. AtMentionCompleter - Autocomplete integration
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 1. AtMentionContext Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAtMentionContext:
|
||||
"""Test @ mention context detection."""
|
||||
|
||||
def test_detect_at_start(self):
|
||||
"""Should detect @ at start of input."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("@", cursor_pos=1)
|
||||
assert result is not None
|
||||
assert result.is_active
|
||||
assert result.query == ""
|
||||
assert result.start_pos == 0
|
||||
|
||||
def test_detect_at_with_query(self):
|
||||
"""Should detect @ with partial query."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("@main", cursor_pos=5)
|
||||
assert result is not None
|
||||
assert result.is_active
|
||||
assert result.query == "main"
|
||||
assert result.start_pos == 0
|
||||
|
||||
def test_detect_at_mid_text(self):
|
||||
"""Should detect @ in middle of text."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("read @src/", cursor_pos=10)
|
||||
assert result is not None
|
||||
assert result.is_active
|
||||
assert result.query == "src/"
|
||||
assert result.start_pos == 5
|
||||
|
||||
def test_no_at_symbol(self):
|
||||
"""Should return None when no @ present."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("hello world", cursor_pos=11)
|
||||
assert result is None
|
||||
|
||||
def test_at_after_space_breaks_context(self):
|
||||
"""Should not detect @ if space after query."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("@file.txt ", cursor_pos=10)
|
||||
assert result is None # Space breaks the @ context
|
||||
|
||||
def test_cursor_before_at(self):
|
||||
"""Should not detect if cursor is before @."""
|
||||
from praisonai.cli.features.at_mentions import detect_at_mention
|
||||
|
||||
result = detect_at_mention("hello @file", cursor_pos=3)
|
||||
assert result is None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 2. FileSuggestion Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestFileSuggestion:
|
||||
"""Test FileSuggestion dataclass."""
|
||||
|
||||
def test_create_file_suggestion(self):
|
||||
"""Should create file suggestion."""
|
||||
from praisonai.cli.features.at_mentions import FileSuggestion
|
||||
|
||||
suggestion = FileSuggestion(
|
||||
path="src/main.py",
|
||||
file_type="file",
|
||||
score=100
|
||||
)
|
||||
assert suggestion.path == "src/main.py"
|
||||
assert suggestion.file_type == "file"
|
||||
assert suggestion.score == 100
|
||||
|
||||
def test_directory_suggestion(self):
|
||||
"""Should create directory suggestion."""
|
||||
from praisonai.cli.features.at_mentions import FileSuggestion
|
||||
|
||||
suggestion = FileSuggestion(
|
||||
path="src/",
|
||||
file_type="directory",
|
||||
score=90
|
||||
)
|
||||
assert suggestion.file_type == "directory"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 3. FileSearchService Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestFileSearchService:
|
||||
"""Test FileSearchService for file discovery."""
|
||||
|
||||
def test_service_creation(self):
|
||||
"""Should create service."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
service = FileSearchService(root_dir=".")
|
||||
assert service is not None
|
||||
|
||||
def test_search_files(self):
|
||||
"""Should search files in directory."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Create test files
|
||||
open(os.path.join(tmpdir, "main.py"), "w").close()
|
||||
open(os.path.join(tmpdir, "test.py"), "w").close()
|
||||
os.makedirs(os.path.join(tmpdir, "src"))
|
||||
open(os.path.join(tmpdir, "src", "app.py"), "w").close()
|
||||
|
||||
service = FileSearchService(root_dir=tmpdir)
|
||||
results = service.search("main")
|
||||
|
||||
assert len(results) >= 1
|
||||
assert any("main.py" in r.path for r in results)
|
||||
|
||||
def test_fuzzy_match(self):
|
||||
"""Should fuzzy match file names."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
open(os.path.join(tmpdir, "my_long_filename.py"), "w").close()
|
||||
|
||||
service = FileSearchService(root_dir=tmpdir)
|
||||
results = service.search("mlf") # Fuzzy match
|
||||
|
||||
assert len(results) >= 1
|
||||
|
||||
def test_empty_query_returns_all(self):
|
||||
"""Should return files when query is empty."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
open(os.path.join(tmpdir, "a.py"), "w").close()
|
||||
open(os.path.join(tmpdir, "b.py"), "w").close()
|
||||
|
||||
service = FileSearchService(root_dir=tmpdir)
|
||||
results = service.search("")
|
||||
|
||||
assert len(results) >= 2
|
||||
|
||||
def test_respects_max_results(self):
|
||||
"""Should limit results."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for i in range(20):
|
||||
open(os.path.join(tmpdir, f"file{i}.py"), "w").close()
|
||||
|
||||
service = FileSearchService(root_dir=tmpdir)
|
||||
results = service.search("", max_results=5)
|
||||
|
||||
assert len(results) <= 5
|
||||
|
||||
def test_cache_works(self):
|
||||
"""Should cache results."""
|
||||
from praisonai.cli.features.at_mentions import FileSearchService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
open(os.path.join(tmpdir, "test.py"), "w").close()
|
||||
|
||||
service = FileSearchService(root_dir=tmpdir, cache_ttl=10)
|
||||
|
||||
# First search
|
||||
start = time.time()
|
||||
results1 = service.search("test")
|
||||
first_time = time.time() - start
|
||||
|
||||
# Second search (cached)
|
||||
start = time.time()
|
||||
results2 = service.search("test")
|
||||
second_time = time.time() - start
|
||||
|
||||
assert len(results1) == len(results2)
|
||||
# Cache should be faster (or at least not slower)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 4. AtMentionCompleter Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAtMentionCompleter:
|
||||
"""Test AtMentionCompleter for prompt_toolkit integration."""
|
||||
|
||||
def test_completer_creation(self):
|
||||
"""Should create completer."""
|
||||
from praisonai.cli.features.at_mentions import AtMentionCompleter
|
||||
|
||||
completer = AtMentionCompleter(root_dir=".")
|
||||
assert completer is not None
|
||||
|
||||
def test_no_completions_without_at(self):
|
||||
"""Should not complete without @."""
|
||||
from praisonai.cli.features.at_mentions import AtMentionCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
completer = AtMentionCompleter(root_dir=".")
|
||||
doc = Document("hello world")
|
||||
completions = list(completer.get_completions(doc, None))
|
||||
|
||||
assert len(completions) == 0
|
||||
|
||||
def test_completions_with_at(self):
|
||||
"""Should complete with @."""
|
||||
from praisonai.cli.features.at_mentions import AtMentionCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
open(os.path.join(tmpdir, "main.py"), "w").close()
|
||||
|
||||
completer = AtMentionCompleter(root_dir=tmpdir)
|
||||
doc = Document("@main")
|
||||
completions = list(completer.get_completions(doc, None))
|
||||
|
||||
assert len(completions) >= 1
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 5. CombinedCompleter Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestCombinedCompleter:
|
||||
"""Test CombinedCompleter that handles both / and @."""
|
||||
|
||||
def test_slash_commands(self):
|
||||
"""Should complete slash commands."""
|
||||
from praisonai.cli.features.at_mentions import CombinedCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
completer = CombinedCompleter(
|
||||
commands=["help", "exit", "queue"],
|
||||
root_dir="."
|
||||
)
|
||||
doc = Document("/he")
|
||||
completions = list(completer.get_completions(doc, None))
|
||||
|
||||
assert len(completions) >= 1
|
||||
assert any("/help" in c.text for c in completions)
|
||||
|
||||
def test_at_mentions(self):
|
||||
"""Should complete @ mentions."""
|
||||
from praisonai.cli.features.at_mentions import CombinedCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
open(os.path.join(tmpdir, "test.py"), "w").close()
|
||||
|
||||
completer = CombinedCompleter(
|
||||
commands=["help"],
|
||||
root_dir=tmpdir
|
||||
)
|
||||
doc = Document("@test")
|
||||
completions = list(completer.get_completions(doc, None))
|
||||
|
||||
assert len(completions) >= 1
|
||||
|
||||
def test_no_completions_for_regular_text(self):
|
||||
"""Should not complete regular text."""
|
||||
from praisonai.cli.features.at_mentions import CombinedCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
completer = CombinedCompleter(
|
||||
commands=["help"],
|
||||
root_dir="."
|
||||
)
|
||||
doc = Document("hello world")
|
||||
completions = list(completer.get_completions(doc, None))
|
||||
|
||||
assert len(completions) == 0
|
||||
@@ -163,11 +163,16 @@ class TestClaudeCodeSDKIntegration:
|
||||
assert isinstance(integration.sdk_available, bool)
|
||||
|
||||
def test_use_sdk_option(self):
|
||||
"""Test use_sdk option."""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration
|
||||
"""Test use_sdk option.
|
||||
|
||||
Note: use_sdk is only True if both use_sdk=True AND SDK is available.
|
||||
Since SDK is not installed in test environment, use_sdk will be False.
|
||||
"""
|
||||
from praisonai.integrations.claude_code import ClaudeCodeIntegration, CLAUDE_SDK_AVAILABLE
|
||||
|
||||
integration = ClaudeCodeIntegration(use_sdk=True)
|
||||
assert integration.use_sdk is True
|
||||
# use_sdk is True only if SDK is available
|
||||
assert integration.use_sdk == (True and CLAUDE_SDK_AVAILABLE)
|
||||
|
||||
integration2 = ClaudeCodeIntegration(use_sdk=False)
|
||||
assert integration2.use_sdk is False
|
||||
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Unit tests for PraisonAI Profiler module.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class TestProfiler:
|
||||
"""Test Profiler class."""
|
||||
|
||||
def test_profiler_singleton(self):
|
||||
"""Should return same instance."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
p1 = Profiler()
|
||||
p2 = Profiler()
|
||||
assert p1 is p2
|
||||
|
||||
def test_enable_disable(self):
|
||||
"""Should enable and disable profiling."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
Profiler.enable()
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_record_timing(self):
|
||||
"""Should record timing."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 100.5, "test")
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) >= 1
|
||||
assert timings[-1].name == "test_op"
|
||||
assert timings[-1].duration_ms == 100.5
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
def test_record_import(self):
|
||||
"""Should record import timing."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_import("test_module", 50.0)
|
||||
|
||||
imports = Profiler.get_imports()
|
||||
assert len(imports) >= 1
|
||||
assert imports[-1].module == "test_module"
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
def test_block_context_manager(self):
|
||||
"""Should profile a block of code."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.block("test_block"):
|
||||
time.sleep(0.01)
|
||||
|
||||
timings = Profiler.get_timings(category="block")
|
||||
assert len(timings) >= 1
|
||||
assert timings[-1].name == "test_block"
|
||||
assert timings[-1].duration_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
def test_get_summary(self):
|
||||
"""Should return summary."""
|
||||
from praisonai.profiler import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("op1", 100, "function")
|
||||
Profiler.record_timing("op2", 200, "function")
|
||||
Profiler.record_import("mod1", 50)
|
||||
|
||||
summary = Profiler.get_summary()
|
||||
|
||||
assert summary['total_time_ms'] == 300
|
||||
assert summary['import_time_ms'] == 50
|
||||
assert summary['timing_count'] == 2
|
||||
assert summary['import_count'] == 1
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
|
||||
class TestProfileDecorator:
|
||||
"""Test @profile decorator."""
|
||||
|
||||
def test_profile_function(self):
|
||||
"""Should profile a function."""
|
||||
from praisonai.profiler import Profiler, profile
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile
|
||||
def test_func():
|
||||
time.sleep(0.01)
|
||||
return "result"
|
||||
|
||||
result = test_func()
|
||||
|
||||
assert result == "result"
|
||||
|
||||
timings = Profiler.get_timings(category="function")
|
||||
assert len(timings) >= 1
|
||||
assert timings[-1].name == "test_func"
|
||||
assert timings[-1].duration_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
def test_profile_with_category(self):
|
||||
"""Should profile with custom category."""
|
||||
from praisonai.profiler import Profiler, profile
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile(category="api")
|
||||
def api_call():
|
||||
return "response"
|
||||
|
||||
api_call()
|
||||
|
||||
timings = Profiler.get_timings(category="api")
|
||||
assert len(timings) >= 1
|
||||
assert timings[-1].name == "api_call"
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
def test_profile_disabled(self):
|
||||
"""Should not record when disabled."""
|
||||
from praisonai.profiler import Profiler, profile
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile
|
||||
def test_func():
|
||||
return "result"
|
||||
|
||||
test_func()
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
|
||||
class TestImportProfiler:
|
||||
"""Test ImportProfiler."""
|
||||
|
||||
def test_profile_imports(self):
|
||||
"""Should profile imports."""
|
||||
from praisonai.profiler import profile_imports
|
||||
|
||||
with profile_imports() as profiler:
|
||||
import json
|
||||
import re
|
||||
|
||||
imports = profiler.get_imports()
|
||||
# May or may not have imports depending on cache
|
||||
assert isinstance(imports, list)
|
||||
|
||||
def test_get_slowest(self):
|
||||
"""Should get slowest imports."""
|
||||
from praisonai.profiler import profile_imports
|
||||
|
||||
with profile_imports() as profiler:
|
||||
import collections
|
||||
|
||||
slowest = profiler.get_slowest(5)
|
||||
assert isinstance(slowest, list)
|
||||
assert len(slowest) <= 5
|
||||
|
||||
|
||||
class TestUtilityFunctions:
|
||||
"""Test utility functions."""
|
||||
|
||||
def test_check_module_available(self):
|
||||
"""Should check module availability."""
|
||||
from praisonai.profiler import check_module_available
|
||||
|
||||
assert check_module_available("os") is True
|
||||
assert check_module_available("nonexistent_module_xyz") is False
|
||||
|
||||
def test_time_import(self):
|
||||
"""Should time import."""
|
||||
from praisonai.profiler import time_import
|
||||
|
||||
duration = time_import("json")
|
||||
assert isinstance(duration, float)
|
||||
assert duration >= 0
|
||||
|
||||
|
||||
class TestDataClasses:
|
||||
"""Test data classes."""
|
||||
|
||||
def test_timing_record(self):
|
||||
"""Should create TimingRecord."""
|
||||
from praisonai.profiler import TimingRecord
|
||||
|
||||
record = TimingRecord(
|
||||
name="test",
|
||||
duration_ms=100.0,
|
||||
category="function",
|
||||
file="test.py",
|
||||
line=10
|
||||
)
|
||||
|
||||
assert record.name == "test"
|
||||
assert record.duration_ms == 100.0
|
||||
assert record.category == "function"
|
||||
|
||||
def test_import_record(self):
|
||||
"""Should create ImportRecord."""
|
||||
from praisonai.profiler import ImportRecord
|
||||
|
||||
record = ImportRecord(
|
||||
module="test_module",
|
||||
duration_ms=50.0,
|
||||
parent="parent_module"
|
||||
)
|
||||
|
||||
assert record.module == "test_module"
|
||||
assert record.duration_ms == 50.0
|
||||
|
||||
def test_flow_record(self):
|
||||
"""Should create FlowRecord."""
|
||||
from praisonai.profiler import FlowRecord
|
||||
|
||||
record = FlowRecord(
|
||||
step=1,
|
||||
name="step1",
|
||||
file="test.py",
|
||||
line=20,
|
||||
duration_ms=30.0
|
||||
)
|
||||
|
||||
assert record.step == 1
|
||||
assert record.name == "step1"
|
||||
@@ -161,20 +161,26 @@ class TestHandleDirectPromptWithRewrite:
|
||||
llm=None
|
||||
)
|
||||
|
||||
# Mock the agent and its start method
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.start.return_value = "result"
|
||||
|
||||
with patch.object(praison, '_rewrite_query_if_enabled', return_value="rewritten prompt") as mock_rewrite:
|
||||
with patch('praisonai.cli.main.PRAISONAI_AVAILABLE', True):
|
||||
with patch('praisonai.cli.main.PraisonAgent') as mock_agent_class:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.start.return_value = "result"
|
||||
mock_agent_class.return_value = mock_agent
|
||||
with patch.object(praison, '_run_with_status_display', return_value="result") as mock_run:
|
||||
with patch('praisonai.cli.main.PRAISONAI_AVAILABLE', True):
|
||||
# Mock praisonaiagents and all its submodules
|
||||
mock_praisonaiagents = MagicMock()
|
||||
mock_praisonaiagents.Agent = MagicMock(return_value=mock_agent)
|
||||
mock_praisonaiagents.approval = MagicMock()
|
||||
|
||||
praison.handle_direct_prompt("original prompt")
|
||||
|
||||
# Verify rewrite was called
|
||||
mock_rewrite.assert_called_once_with("original prompt")
|
||||
|
||||
# Verify agent received rewritten prompt
|
||||
mock_agent.start.assert_called_once_with("rewritten prompt")
|
||||
with patch.dict('sys.modules', {
|
||||
'praisonaiagents': mock_praisonaiagents,
|
||||
'praisonaiagents.approval': mock_praisonaiagents.approval
|
||||
}):
|
||||
praison.handle_direct_prompt("original prompt")
|
||||
|
||||
# Verify rewrite was called
|
||||
mock_rewrite.assert_called_once_with("original prompt")
|
||||
|
||||
|
||||
class TestToolLoading:
|
||||
|
||||
Generated
+4
-4
@@ -4197,7 +4197,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.17" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.21" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4252,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.17"
|
||||
version = "0.1.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/ed/fd26940dd2ee4506f2d6e178841b36d09f90f6a817ea052e0c1a9473f132/praisonaiagents-0.1.17.tar.gz", hash = "sha256:fa3bb9e77c6dbd16ed1fdb933d3669fac2208b1e58a2a5d5986fd630fef975c1", size = 647705, upload-time = "2025-12-18T21:52:30.244Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/9a/c5b74fdfafc3557107f5c5dc35978d818537f35047f76e2a50142a2401bd/praisonaiagents-0.1.21.tar.gz", hash = "sha256:09b9448f4aa8c88d2ff3cab6514b5746ff62cbe07db97541a467ac1eac38777f", size = 658730, upload-time = "2025-12-20T11:57:22.359Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/40/261bbe4e0597442bd8d3aef9450c315063735970bb8f32f95cc4f0267b7b/praisonaiagents-0.1.17-py3-none-any.whl", hash = "sha256:61b98ede790e14fd7a21f4fc51729ba0a4b32ab1165f3ad9ee966757fa581c0f", size = 583244, upload-time = "2025-12-18T21:52:28.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/0a/c408a92c1721bc7a15e1c6dffc099381411fc87c241fba09d60c91a74d47/praisonaiagents-0.1.21-py3-none-any.whl", hash = "sha256:9d77c484013a1ea7c52ad6a9a0750c0c336133f6e913da6b9bd3da73f2bb5b11", size = 589189, upload-time = "2025-12-20T11:57:20.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user