mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 11:41:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5d4db4262 | ||
|
|
87d691c616 | ||
|
|
8ea15dee0f | ||
|
|
4f56842e08 | ||
|
|
a24836aaee | ||
|
|
be9ce27615 | ||
|
|
0159a1e082 | ||
|
|
71a5076c07 | ||
|
|
9815a9f8a8 | ||
|
|
2ae44a05b0 | ||
|
|
3386d25022 | ||
|
|
6dca2ace8a | ||
|
|
63548739e8 | ||
|
|
3e046537f4 | ||
|
|
4a34677567 | ||
|
|
19e91cb669 | ||
|
|
5a6125021a | ||
|
|
da26887f24 | ||
|
|
1b2f7139f9 | ||
|
|
aafbc86180 | ||
|
|
3d4ca1b7c0 | ||
|
|
1180aa38bd | ||
|
|
65f65e5c7b | ||
|
|
f81a12d5e8 | ||
|
|
cf6f5fbefe | ||
|
|
908d7710db | ||
|
|
7d5b41e31f | ||
|
|
e46a0bc9f3 | ||
|
|
911483c92c | ||
|
|
4ffb9d6e3f | ||
|
|
b686e5b00f | ||
|
|
d8d84ee3e3 |
No files matched your search
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Auto-generated API server for PraisonAI agents.
|
||||
"""
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from praisonai import PraisonAI
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# CORS configuration
|
||||
CORS(app)
|
||||
|
||||
# Authentication
|
||||
AUTH_ENABLED = False
|
||||
AUTH_TOKEN = None
|
||||
|
||||
def check_auth():
|
||||
"""Check authentication if enabled."""
|
||||
if not AUTH_ENABLED:
|
||||
return True
|
||||
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
return token == AUTH_TOKEN
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({"status": "ok", "service": "praisonai-api"})
|
||||
|
||||
@app.route('/chat', methods=['POST'])
|
||||
def chat():
|
||||
"""Chat endpoint for agent interaction."""
|
||||
if not check_auth():
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'message' not in data:
|
||||
return jsonify({"error": "Message required"}), 400
|
||||
|
||||
try:
|
||||
praisonai = PraisonAI(agent_file="agents.yaml")
|
||||
result = praisonai.run()
|
||||
|
||||
return jsonify({
|
||||
"response": result,
|
||||
"status": "success"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"error": str(e),
|
||||
"status": "error"
|
||||
}), 500
|
||||
|
||||
@app.route('/agents', methods=['GET'])
|
||||
def list_agents():
|
||||
"""List available agents."""
|
||||
if not check_auth():
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
return jsonify({
|
||||
"agents": ["default"],
|
||||
"agent_file": "agents.yaml"
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=8080,
|
||||
debug=False
|
||||
)
|
||||
@@ -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.8.9" \
|
||||
"praisonai>=3.1.2" \
|
||||
"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.8.9" \
|
||||
"praisonai>=3.1.2" \
|
||||
"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.8.9" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Profiling CLI
|
||||
|
||||
PraisonAI provides unified profiling capabilities for performance analysis and debugging, with **full visibility from ENTER to first response**.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Basic profiling
|
||||
praisonai "What is 2+2?" --profile
|
||||
|
||||
# Deep profiling with call graph, decision trace, and module breakdown
|
||||
praisonai "What is 2+2?" --profile --profile-deep
|
||||
|
||||
# JSON output format
|
||||
praisonai "What is 2+2?" --profile --profile-format json
|
||||
```
|
||||
|
||||
## Time to First Response (TTFR)
|
||||
|
||||
**TTFR** is the key user-perceived latency metric. It measures the time from when you press ENTER until the first visible output appears.
|
||||
|
||||
### What contributes to TTFR?
|
||||
|
||||
1. **CLI Entry** - Time to reach the CLI entry point
|
||||
2. **CLI Parse** - Argparse/Typer routing time
|
||||
3. **Routing** - Command routing to handler
|
||||
4. **Imports** - Loading required modules
|
||||
5. **Agent Init** - Initializing the agent
|
||||
6. **Network Request** - Sending request to LLM API
|
||||
7. **First Token** - Receiving first token (streaming)
|
||||
8. **First Output** - Rendering first visible output
|
||||
|
||||
### How to reduce TTFR
|
||||
|
||||
1. **Warm runs** - Imports are cached after first run
|
||||
2. **Streaming mode** - See output as tokens arrive
|
||||
3. **Lighter models** - Faster response times
|
||||
4. **Local models** - Eliminate network latency
|
||||
|
||||
## CLI Flags
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--profile` | Enable profiling with timing breakdown | Disabled |
|
||||
| `--profile-deep` | Enable deep profiling with call graph (higher overhead) | Disabled |
|
||||
| `--profile-format` | Output format: `text` or `json` | `text` |
|
||||
|
||||
## Profile Command
|
||||
|
||||
For more detailed profiling options, use the `profile` subcommand:
|
||||
|
||||
```bash
|
||||
# Profile a query
|
||||
praisonai profile query "What is 2+2?"
|
||||
|
||||
# With deep call tracing
|
||||
praisonai profile query "Hello" --deep
|
||||
|
||||
# Limit functions shown
|
||||
praisonai profile query "Test" --limit 20
|
||||
|
||||
# Save artifacts to directory
|
||||
praisonai profile query "Test" --save ./profile_results
|
||||
|
||||
# JSON output
|
||||
praisonai profile query "Test" --format json
|
||||
```
|
||||
|
||||
### Profile Query Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--model, -m` | Model to use | Default model |
|
||||
| `--stream/--no-stream` | Use streaming mode | No stream |
|
||||
| `--deep` | Enable deep call tracing | Disabled |
|
||||
| `--limit, -n` | Top N functions to show | 30 |
|
||||
| `--sort, -s` | Sort by: `cumulative` or `tottime` | `cumulative` |
|
||||
| `--show-callers` | Show caller functions | Disabled |
|
||||
| `--show-callees` | Show callee functions | Disabled |
|
||||
| `--save` | Save artifacts to path | None |
|
||||
| `--format, -f` | Output format: `text` or `json` | `text` |
|
||||
|
||||
## Other Profile Commands
|
||||
|
||||
```bash
|
||||
# Profile module import times
|
||||
praisonai profile imports
|
||||
|
||||
# Profile CLI startup time
|
||||
praisonai profile startup
|
||||
|
||||
# Run comprehensive profiling suite
|
||||
praisonai profile suite
|
||||
|
||||
# Create performance snapshots for comparison
|
||||
praisonai profile snapshot --baseline
|
||||
praisonai profile snapshot current --compare
|
||||
|
||||
# Configure performance optimizations
|
||||
praisonai profile optimize --show
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Text Report
|
||||
|
||||
```
|
||||
======================================================================
|
||||
PraisonAI Profile Report
|
||||
======================================================================
|
||||
|
||||
Run ID: abc12345
|
||||
Timestamp: 2024-01-02T12:00:00.000000Z
|
||||
Method: cli_direct
|
||||
Version: 3.0.1
|
||||
|
||||
## Execution Timeline
|
||||
---------------------------------------------
|
||||
Imports : 500.00 ms
|
||||
Agent Init : 0.50 ms
|
||||
Execution : 2000.00 ms
|
||||
───────────────────────────────────────────
|
||||
⏱ Time to First Response : 2500.50 ms
|
||||
TOTAL : 2500.50 ms
|
||||
|
||||
## Timing Breakdown
|
||||
----------------------------------------
|
||||
Imports: 500.00 ms
|
||||
Agent Init: 0.50 ms
|
||||
Execution: 2000.00 ms
|
||||
─────────────────────────────────────
|
||||
TOTAL: 2500.50 ms
|
||||
|
||||
## Decision Trace (deep profile only)
|
||||
----------------------------------------
|
||||
Agent Config: default
|
||||
Model: gpt-4
|
||||
Streaming: disabled
|
||||
Profile Layer: 2
|
||||
|
||||
## Top Functions by Cumulative Time
|
||||
----------------------------------------------------------------------
|
||||
Function Calls Cumul (ms)
|
||||
----------------------------------------------------------------------
|
||||
request 1 1800.00
|
||||
send 1 1750.00
|
||||
...
|
||||
```
|
||||
|
||||
### JSON Report
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"run_id": "abc12345",
|
||||
"timestamp": "2024-01-02T12:00:00.000000Z",
|
||||
"invocation": {
|
||||
"method": "cli_direct",
|
||||
"flags": {"layer": 1, "stream": false, "model": null},
|
||||
"praisonai_version": "3.0.1",
|
||||
"python_version": "3.12.0"
|
||||
},
|
||||
"timing": {
|
||||
"total_ms": 2500.5,
|
||||
"imports_ms": 500.0,
|
||||
"agent_init_ms": 0.5,
|
||||
"execution_ms": 2000.0
|
||||
},
|
||||
"functions": [...],
|
||||
"response_preview": "four"
|
||||
}
|
||||
```
|
||||
|
||||
## Profiling Layers
|
||||
|
||||
| Layer | Overhead | Features |
|
||||
|-------|----------|----------|
|
||||
| 0 | < 1ms | Wall-clock phases only |
|
||||
| 1 | ~5% | + cProfile function stats |
|
||||
| 2 | ~15% | + Call graph (callers/callees) |
|
||||
|
||||
- `--profile` uses Layer 1
|
||||
- `--profile --profile-deep` uses Layer 2
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use Layer 0 for production monitoring** - minimal overhead
|
||||
2. **Use Layer 1 for development profiling** - good balance
|
||||
3. **Use Layer 2 only for deep debugging** - higher overhead
|
||||
|
||||
## Observational Equivalence
|
||||
|
||||
The following commands produce equivalent profile reports:
|
||||
|
||||
```bash
|
||||
# CLI direct
|
||||
praisonai "prompt" --profile
|
||||
|
||||
# Profile command
|
||||
praisonai profile query "prompt"
|
||||
```
|
||||
|
||||
Both use the same unified profiling architecture and produce identical schema output.
|
||||
@@ -0,0 +1,257 @@
|
||||
# Profiling Module
|
||||
|
||||
PraisonAI provides a unified profiling module for programmatic performance analysis.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from praisonai.cli.execution import (
|
||||
ExecutionRequest,
|
||||
Profiler,
|
||||
ProfilerConfig,
|
||||
)
|
||||
|
||||
# Create execution request
|
||||
request = ExecutionRequest(prompt="What is 2+2?")
|
||||
|
||||
# Configure profiler
|
||||
config = ProfilerConfig(layer=1)
|
||||
|
||||
# Run with profiling
|
||||
profiler = Profiler(config)
|
||||
result, report = profiler.profile_sync(request)
|
||||
|
||||
# Access results
|
||||
print(f"Output: {result.output}")
|
||||
print(f"Total time: {report.timing.total_ms}ms")
|
||||
```
|
||||
|
||||
## ExecutionRequest
|
||||
|
||||
Immutable request descriptor for execution.
|
||||
|
||||
```python
|
||||
from praisonai.cli.execution import ExecutionRequest
|
||||
|
||||
# Basic request
|
||||
request = ExecutionRequest(prompt="Hello")
|
||||
|
||||
# With model
|
||||
request = ExecutionRequest(
|
||||
prompt="Hello",
|
||||
model="gpt-4",
|
||||
agent_name="MyAgent",
|
||||
)
|
||||
|
||||
# With streaming
|
||||
request = ExecutionRequest(
|
||||
prompt="Hello",
|
||||
stream=True,
|
||||
)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | str | Required | The prompt to execute |
|
||||
| `agent_name` | str | "Assistant" | Name of the agent |
|
||||
| `agent_instructions` | str | None | Custom instructions |
|
||||
| `model` | str | None | Model to use |
|
||||
| `stream` | bool | False | Enable streaming |
|
||||
| `tools` | tuple | () | Tools to provide |
|
||||
|
||||
## ProfilerConfig
|
||||
|
||||
Configuration for profiling behavior.
|
||||
|
||||
```python
|
||||
from praisonai.cli.execution import ProfilerConfig
|
||||
|
||||
# Layer 0: Wall-clock only (< 1ms overhead)
|
||||
config = ProfilerConfig(layer=0)
|
||||
|
||||
# Layer 1: + cProfile stats (~5% overhead)
|
||||
config = ProfilerConfig(layer=1)
|
||||
|
||||
# Layer 2: + Call graph (~15% overhead)
|
||||
config = ProfilerConfig(
|
||||
layer=2,
|
||||
show_callers=True,
|
||||
show_callees=True,
|
||||
)
|
||||
|
||||
# From CLI flags
|
||||
config = ProfilerConfig.from_flags(
|
||||
profile=True,
|
||||
deep=True,
|
||||
network=False,
|
||||
)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `layer` | int | 1 | Profiling layer (0, 1, or 2) |
|
||||
| `limit` | int | 30 | Max functions to show |
|
||||
| `sort_by` | str | "cumulative" | Sort by: cumulative, time, calls |
|
||||
| `show_callers` | bool | False | Show caller functions |
|
||||
| `show_callees` | bool | False | Show callee functions |
|
||||
| `track_network` | bool | False | Track network requests |
|
||||
| `output_format` | str | "text" | Output format: text, json |
|
||||
| `save_path` | str | None | Path to save artifacts |
|
||||
|
||||
## Profiler
|
||||
|
||||
The main profiler class that wraps execution.
|
||||
|
||||
```python
|
||||
from praisonai.cli.execution import Profiler, ProfilerConfig, ExecutionRequest
|
||||
|
||||
config = ProfilerConfig(layer=1)
|
||||
profiler = Profiler(config)
|
||||
|
||||
request = ExecutionRequest(prompt="Hello")
|
||||
result, report = profiler.profile_sync(request, invocation_method="api")
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
#### `profile_sync(request, invocation_method="profile_command")`
|
||||
|
||||
Profile synchronous execution.
|
||||
|
||||
**Parameters:**
|
||||
- `request`: ExecutionRequest - The execution request
|
||||
- `invocation_method`: str - How this was invoked (for report)
|
||||
|
||||
**Returns:** Tuple of (ExecutionResult, ProfileReport)
|
||||
|
||||
## ProfileReport
|
||||
|
||||
The canonical profile report with timing and statistics.
|
||||
|
||||
```python
|
||||
# Access timing breakdown
|
||||
print(f"Total: {report.timing.total_ms}ms")
|
||||
print(f"Imports: {report.timing.imports_ms}ms")
|
||||
print(f"Agent Init: {report.timing.agent_init_ms}ms")
|
||||
print(f"Execution: {report.timing.execution_ms}ms")
|
||||
|
||||
# Access function stats (layer 1+)
|
||||
if report.functions:
|
||||
for func in report.functions[:5]:
|
||||
print(f"{func.name}: {func.cumulative_time_ms}ms")
|
||||
|
||||
# Access call graph (layer 2)
|
||||
if report.call_graph:
|
||||
print(f"Edges: {len(report.call_graph.edges)}")
|
||||
|
||||
# Export formats
|
||||
json_str = report.to_json()
|
||||
text_str = report.to_text()
|
||||
dict_data = report.to_dict()
|
||||
```
|
||||
|
||||
### Schema Version
|
||||
|
||||
The report uses schema version `1.0`. Schema is frozen after release - new fields are always optional with defaults.
|
||||
|
||||
### Mandatory Fields
|
||||
|
||||
- `schema_version`: str
|
||||
- `run_id`: str
|
||||
- `timestamp`: str (ISO 8601)
|
||||
- `invocation`: InvocationInfo
|
||||
- `timing`: TimingBreakdown
|
||||
- `response_preview`: str
|
||||
|
||||
### Optional Fields
|
||||
|
||||
- `functions`: List[FunctionStat] (layer 1+)
|
||||
- `call_graph`: CallGraph (layer 2)
|
||||
- `network`: List[RequestTiming] (if track_network)
|
||||
|
||||
## ExecutionResult
|
||||
|
||||
Result of execution with output and metadata.
|
||||
|
||||
```python
|
||||
# Access output
|
||||
print(result.output)
|
||||
print(result.success)
|
||||
print(result.run_id)
|
||||
|
||||
# Check for errors
|
||||
if result.error:
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
# Duration
|
||||
print(f"Duration: {result.duration_ms}ms")
|
||||
```
|
||||
|
||||
## Data Bounds
|
||||
|
||||
Profile data is strictly bounded to prevent memory issues:
|
||||
|
||||
| Data Type | Maximum |
|
||||
|-----------|---------|
|
||||
| Function stats | 1000 |
|
||||
| Call graph edges | 5000 |
|
||||
| Network requests | 100 |
|
||||
|
||||
## Example: Full Profiling Workflow
|
||||
|
||||
```python
|
||||
from praisonai.cli.execution import (
|
||||
ExecutionRequest,
|
||||
Profiler,
|
||||
ProfilerConfig,
|
||||
)
|
||||
|
||||
# Configure deep profiling
|
||||
config = ProfilerConfig(
|
||||
layer=2,
|
||||
limit=50,
|
||||
show_callers=True,
|
||||
show_callees=True,
|
||||
)
|
||||
|
||||
# Create request
|
||||
request = ExecutionRequest(
|
||||
prompt="Explain quantum computing in simple terms",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
# Profile execution
|
||||
profiler = Profiler(config)
|
||||
result, report = profiler.profile_sync(request)
|
||||
|
||||
# Print text report
|
||||
print(report.to_text())
|
||||
|
||||
# Save JSON report
|
||||
with open("profile.json", "w") as f:
|
||||
f.write(report.to_json())
|
||||
|
||||
# Analyze timing
|
||||
print(f"\nTiming Analysis:")
|
||||
print(f" Import overhead: {report.timing.imports_ms / report.timing.total_ms * 100:.1f}%")
|
||||
print(f" Execution time: {report.timing.execution_ms / report.timing.total_ms * 100:.1f}%")
|
||||
|
||||
# Top 5 hotspots
|
||||
if report.functions:
|
||||
print(f"\nTop 5 Hotspots:")
|
||||
for func in report.functions[:5]:
|
||||
print(f" {func.name}: {func.cumulative_time_ms:.2f}ms ({func.calls} calls)")
|
||||
```
|
||||
|
||||
## Invariants
|
||||
|
||||
The profiling module guarantees:
|
||||
|
||||
1. **Output Equivalence**: Profiled execution produces identical output to non-profiled execution
|
||||
2. **Schema Stability**: Schema version 1.0 is frozen; new fields are always optional
|
||||
3. **Bounded Data**: All profile data respects maximum bounds
|
||||
4. **Zero Overhead When Disabled**: No profiling code runs unless explicitly enabled
|
||||
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Agent-Centric Tools Example
|
||||
|
||||
This example demonstrates how to use LSP/ACP-powered tools that make
|
||||
the Agent the central orchestrator for file operations.
|
||||
|
||||
How to run:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python examples/agent_tools/agent_centric_example.py
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
# Add paths for development
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai-agents'))
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("AGENT-CENTRIC TOOLS EXAMPLE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Create temporary workspace
|
||||
workspace = tempfile.mkdtemp(prefix="praisonai_example_")
|
||||
print(f"Workspace: {workspace}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Import after path setup
|
||||
from praisonai.cli.features import (
|
||||
create_agent_centric_tools,
|
||||
InteractiveRuntime,
|
||||
RuntimeConfig
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# 1. Create runtime with ACP enabled
|
||||
print("1. Creating InteractiveRuntime with ACP...")
|
||||
config = RuntimeConfig(
|
||||
workspace=workspace,
|
||||
lsp_enabled=False, # Skip LSP for this example
|
||||
acp_enabled=True,
|
||||
approval_mode="auto" # Auto-approve for demo
|
||||
)
|
||||
runtime = InteractiveRuntime(config)
|
||||
status = await runtime.start()
|
||||
|
||||
print(f" ACP ready: {runtime.acp_ready}")
|
||||
print(f" Read-only: {runtime.read_only}")
|
||||
print()
|
||||
|
||||
# 2. Create agent-centric tools
|
||||
print("2. Creating agent-centric tools...")
|
||||
tools = create_agent_centric_tools(runtime)
|
||||
print(f" Tools created: {len(tools)}")
|
||||
for tool in tools:
|
||||
print(f" - {tool.__name__}")
|
||||
print()
|
||||
|
||||
# 3. Create Agent with ACP-powered tools
|
||||
print("3. Creating Agent with ACP-powered tools...")
|
||||
agent = Agent(
|
||||
name="FileAgent",
|
||||
instructions="""You help create and manage files.
|
||||
Use acp_create_file to create files.
|
||||
Use read_file to read files.
|
||||
Use list_files to list directory contents.""",
|
||||
tools=tools,
|
||||
verbose=True
|
||||
)
|
||||
print()
|
||||
|
||||
# 4. Ask agent to create a file
|
||||
print("4. Asking agent to create a Python file...")
|
||||
print("-" * 40)
|
||||
result = agent.start(
|
||||
"Create a Python file called calculator.py with add and subtract functions"
|
||||
)
|
||||
print("-" * 40)
|
||||
print(f"Agent response: {result[:200]}..." if len(str(result)) > 200 else f"Agent response: {result}")
|
||||
print()
|
||||
|
||||
# 5. Verify file was created
|
||||
print("5. Verifying file creation...")
|
||||
calc_path = os.path.join(workspace, "calculator.py")
|
||||
if os.path.exists(calc_path):
|
||||
print(f" ✓ File created at {calc_path}")
|
||||
with open(calc_path) as f:
|
||||
content = f.read()
|
||||
print(f" Content preview:")
|
||||
for line in content.split('\n')[:10]:
|
||||
print(f" {line}")
|
||||
else:
|
||||
# Check what files exist
|
||||
files = os.listdir(workspace)
|
||||
print(f" Files in workspace: {files}")
|
||||
for f in files:
|
||||
fp = os.path.join(workspace, f)
|
||||
if os.path.isfile(fp):
|
||||
print(f" ✓ Found: {f}")
|
||||
print()
|
||||
|
||||
# 6. Ask agent to read the file
|
||||
print("6. Asking agent to read the file...")
|
||||
print("-" * 40)
|
||||
result2 = agent.start("Read the calculator.py file and tell me what functions it has")
|
||||
print("-" * 40)
|
||||
print(f"Agent response: {result2[:300]}..." if len(str(result2)) > 300 else f"Agent response: {result2}")
|
||||
print()
|
||||
|
||||
# Cleanup
|
||||
await runtime.stop()
|
||||
|
||||
finally:
|
||||
# Clean up workspace
|
||||
shutil.rmtree(workspace, ignore_errors=True)
|
||||
print(f"Cleaned up workspace: {workspace}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("EXAMPLE COMPLETE")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Benchmark Example
|
||||
|
||||
This example demonstrates how to use the benchmark system programmatically
|
||||
to compare performance across different execution paths.
|
||||
|
||||
Usage:
|
||||
python benchmark_example.py
|
||||
|
||||
Requirements:
|
||||
- OPENAI_API_KEY environment variable set
|
||||
- praisonai package installed
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure the package is importable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
|
||||
from praisonai.cli.features.benchmark import BenchmarkHandler, BenchmarkReport
|
||||
|
||||
|
||||
def run_quick_comparison():
|
||||
"""Run a quick comparison of key execution paths."""
|
||||
print("=" * 60)
|
||||
print("Quick Benchmark Comparison")
|
||||
print("=" * 60)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
|
||||
# Run benchmark with 2 iterations on key paths
|
||||
report = handler.run_full_benchmark(
|
||||
prompt="What is 2+2?",
|
||||
iterations=2,
|
||||
paths=["openai_sdk", "praisonai_agent"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Print comparison table
|
||||
print("\n" + handler.create_comparison_table(report))
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def run_full_benchmark():
|
||||
"""Run full benchmark suite across all paths."""
|
||||
print("=" * 60)
|
||||
print("Full Benchmark Suite")
|
||||
print("=" * 60)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
|
||||
# Run full benchmark
|
||||
report = handler.run_full_benchmark(
|
||||
prompt="Hi",
|
||||
iterations=3,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Print full report
|
||||
handler.print_report(report)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def run_agent_benchmark():
|
||||
"""Benchmark PraisonAI Agent specifically."""
|
||||
print("=" * 60)
|
||||
print("Agent Benchmark")
|
||||
print("=" * 60)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
|
||||
# Benchmark agent vs SDK
|
||||
report = handler.run_full_benchmark(
|
||||
prompt="Explain Python in one sentence",
|
||||
iterations=3,
|
||||
paths=["openai_sdk", "praisonai_agent"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Show timeline diagrams
|
||||
for name, result in report.results.items():
|
||||
print(f"\n### {name}")
|
||||
print(handler.create_timeline_diagram(result))
|
||||
|
||||
# Show variance analysis
|
||||
print(handler.create_variance_table(report))
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def save_benchmark_results(report: BenchmarkReport, output_path: str):
|
||||
"""Save benchmark results to JSON file."""
|
||||
import json
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(report.to_dict(), f, indent=2)
|
||||
|
||||
print(f"\nResults saved to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="PraisonAI Benchmark Example")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["quick", "full", "agent"],
|
||||
default="quick",
|
||||
help="Benchmark mode: quick (2 paths), full (all paths), agent (agent focus)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Output JSON file path"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
# Run selected benchmark
|
||||
if args.mode == "quick":
|
||||
report = run_quick_comparison()
|
||||
elif args.mode == "full":
|
||||
report = run_full_benchmark()
|
||||
elif args.mode == "agent":
|
||||
report = run_agent_benchmark()
|
||||
|
||||
# Save results if requested
|
||||
if args.output:
|
||||
save_benchmark_results(report, args.output)
|
||||
|
||||
print("\n✅ Benchmark complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Debug Interactive Mode Example
|
||||
|
||||
This example demonstrates how to use the debug CLI commands to test
|
||||
the Interactive Coding Assistant without entering TUI mode.
|
||||
|
||||
Features demonstrated:
|
||||
- LSP symbol listing
|
||||
- LSP definition lookup
|
||||
- ACP action planning
|
||||
- ACP action execution
|
||||
- Trace capture and JSON output
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to path for development
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src" / "praisonai"))
|
||||
|
||||
from praisonai.cli.features.interactive_runtime import create_runtime
|
||||
from praisonai.cli.features.code_intelligence import CodeIntelligenceRouter
|
||||
from praisonai.cli.features.action_orchestrator import ActionOrchestrator
|
||||
|
||||
|
||||
async def demo_lsp_queries(workspace: str):
|
||||
"""Demonstrate LSP-based code queries."""
|
||||
print("\n" + "=" * 60)
|
||||
print("LSP CODE QUERIES DEMO")
|
||||
print("=" * 60)
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=workspace,
|
||||
lsp=True,
|
||||
acp=False,
|
||||
trace=True
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
router = CodeIntelligenceRouter(runtime)
|
||||
|
||||
# Query 1: List symbols
|
||||
print("\n[Query 1] List all symbols in app.py")
|
||||
result = await router.handle_query("list all functions in app.py", "app.py")
|
||||
print(f" Success: {result.success}")
|
||||
print(f" LSP Used: {result.lsp_used}")
|
||||
print(f" Fallback Used: {result.fallback_used}")
|
||||
if result.data:
|
||||
print(f" Symbols found: {len(result.data)}")
|
||||
for symbol in result.data[:5]:
|
||||
print(f" - {symbol['name']} ({symbol['kind']}) at line {symbol['line']}")
|
||||
|
||||
# Query 2: Find definition
|
||||
print("\n[Query 2] Find definition of Calculator")
|
||||
result = await router.handle_query("where is Calculator defined", "app.py")
|
||||
print(f" Success: {result.success}")
|
||||
if result.citations:
|
||||
print(f" Found at: {result.citations[0]}")
|
||||
|
||||
# Get trace
|
||||
trace = runtime.get_trace()
|
||||
print(f"\n[Trace] {len(trace.entries)} entries captured")
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
|
||||
async def demo_acp_actions(workspace: str):
|
||||
"""Demonstrate ACP-based code modifications."""
|
||||
print("\n" + "=" * 60)
|
||||
print("ACP CODE MODIFICATIONS DEMO")
|
||||
print("=" * 60)
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=workspace,
|
||||
lsp=False,
|
||||
acp=True,
|
||||
approval="auto",
|
||||
trace=True
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
print(f"\n ACP Ready: {runtime.acp_ready}")
|
||||
print(f" Read Only: {runtime.read_only}")
|
||||
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
|
||||
# Action 1: Create a plan
|
||||
print("\n[Action 1] Create plan for new file")
|
||||
result = await orchestrator.create_plan("create a new file called demo_output.py")
|
||||
print(f" Success: {result.success}")
|
||||
if result.plan:
|
||||
print(f" Plan ID: {result.plan.id}")
|
||||
print(f" Steps: {len(result.plan.steps)}")
|
||||
for step in result.plan.steps:
|
||||
print(f" - {step.action_type.value}: {step.target}")
|
||||
|
||||
# Action 2: Execute with auto-approve
|
||||
print("\n[Action 2] Execute file creation")
|
||||
exec_result = await orchestrator.execute(
|
||||
"create a new file called demo_output.py with a hello function",
|
||||
auto_approve=True
|
||||
)
|
||||
print(f" Success: {exec_result.success}")
|
||||
print(f" Applied Steps: {exec_result.applied_steps}")
|
||||
|
||||
# Verify file was created
|
||||
output_file = Path(workspace) / "demo_output.py"
|
||||
if output_file.exists():
|
||||
print(f" File created: {output_file}")
|
||||
print(f" Content preview: {output_file.read_text()[:100]}...")
|
||||
|
||||
# Get trace
|
||||
trace = runtime.get_trace()
|
||||
print(f"\n[Trace] {len(trace.entries)} entries captured")
|
||||
for entry in trace.entries:
|
||||
print(f" [{entry.category}] {entry.action}")
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
|
||||
async def demo_full_interactive_turn(workspace: str):
|
||||
"""Demonstrate full interactive turn simulation."""
|
||||
print("\n" + "=" * 60)
|
||||
print("FULL INTERACTIVE TURN SIMULATION")
|
||||
print("=" * 60)
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=workspace,
|
||||
lsp=True,
|
||||
acp=True,
|
||||
approval="auto",
|
||||
trace=True
|
||||
)
|
||||
|
||||
try:
|
||||
status = await runtime.start()
|
||||
print("\n Runtime Status:")
|
||||
print(f" LSP: {'ready' if status['lsp']['ready'] else 'fallback'}")
|
||||
print(f" ACP: {'ready' if status['acp']['ready'] else 'unavailable'}")
|
||||
|
||||
router = CodeIntelligenceRouter(runtime)
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
|
||||
# Simulate user prompt: code query
|
||||
prompt1 = "What functions are in app.py?"
|
||||
print(f"\n[Turn 1] User: {prompt1}")
|
||||
|
||||
# Classify and route
|
||||
result = await router.handle_query(prompt1, "app.py")
|
||||
print(f" Intent: {result.intent.value}")
|
||||
print(f" Response: Found {len(result.data) if result.data else 0} symbols")
|
||||
|
||||
# Simulate user prompt: code modification
|
||||
prompt2 = "Create a new test file"
|
||||
print(f"\n[Turn 2] User: {prompt2}")
|
||||
|
||||
exec_result = await orchestrator.execute(
|
||||
"create a new file called test_demo.py",
|
||||
auto_approve=True
|
||||
)
|
||||
print(" Action: file_create")
|
||||
print(f" Result: {'success' if exec_result.success else 'failed'}")
|
||||
|
||||
# Export trace as JSON
|
||||
trace = runtime.get_trace()
|
||||
trace_json = trace.to_dict()
|
||||
print("[Trace Export]")
|
||||
print(json.dumps(trace_json, indent=2, default=str)[:500] + "...")
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the debug demo."""
|
||||
# Create a temporary workspace
|
||||
workspace = Path(__file__).parent / "_demo_workspace"
|
||||
workspace.mkdir(exist_ok=True)
|
||||
|
||||
# Create a sample app.py
|
||||
app_file = workspace / "app.py"
|
||||
app_file.write_text('''"""Sample application."""
|
||||
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
class Calculator:
|
||||
"""Simple calculator."""
|
||||
|
||||
def __init__(self):
|
||||
self.value = 0
|
||||
|
||||
def add(self, x):
|
||||
self.value += x
|
||||
return self.value
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(greet("World"))
|
||||
''')
|
||||
|
||||
print("=" * 60)
|
||||
print("PRAISONAI DEBUG MODE DEMO")
|
||||
print("=" * 60)
|
||||
print(f"Workspace: {workspace}")
|
||||
|
||||
# Run demos
|
||||
asyncio.run(demo_lsp_queries(str(workspace)))
|
||||
asyncio.run(demo_acp_actions(str(workspace)))
|
||||
asyncio.run(demo_full_interactive_turn(str(workspace)))
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("DEMO COMPLETE")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Interactive Default Tools Example
|
||||
|
||||
This example demonstrates how PraisonAI's interactive modes (TUI and prompt)
|
||||
include ACP and LSP tools by default.
|
||||
|
||||
Tool Groups:
|
||||
- ACP: acp_create_file, acp_edit_file, acp_delete_file, acp_execute_command
|
||||
- LSP: lsp_list_symbols, lsp_find_definition, lsp_find_references, lsp_get_diagnostics
|
||||
- Basic: read_file, write_file, list_files, execute_command, internet_search
|
||||
|
||||
Usage:
|
||||
python interactive_tools_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add paths for development
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai-agents'))
|
||||
|
||||
|
||||
def example_get_default_tools():
|
||||
"""Example: Get all default interactive tools."""
|
||||
from praisonai.cli.features.interactive_tools import (
|
||||
get_interactive_tools,
|
||||
ToolConfig,
|
||||
TOOL_GROUPS,
|
||||
)
|
||||
|
||||
print("=== Default Interactive Tools ===\n")
|
||||
|
||||
# Show tool groups
|
||||
print("Tool Groups:")
|
||||
for group, tools in TOOL_GROUPS.items():
|
||||
if group != "interactive":
|
||||
print(f" {group}: {tools}")
|
||||
print()
|
||||
|
||||
# Get all tools
|
||||
config = ToolConfig(workspace=os.getcwd())
|
||||
tools = get_interactive_tools(config=config)
|
||||
|
||||
print(f"Total tools loaded: {len(tools)}")
|
||||
print(f"Tool names: {[t.__name__ for t in tools]}")
|
||||
print()
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def example_disable_acp():
|
||||
"""Example: Disable ACP tools."""
|
||||
from praisonai.cli.features.interactive_tools import (
|
||||
get_interactive_tools,
|
||||
ToolConfig,
|
||||
)
|
||||
|
||||
print("=== Disable ACP Tools (--no-acp) ===\n")
|
||||
|
||||
config = ToolConfig(workspace=os.getcwd(), enable_acp=False)
|
||||
tools = get_interactive_tools(config=config, disable=["acp"])
|
||||
|
||||
acp_tools = [t for t in tools if t.__name__.startswith("acp_")]
|
||||
print(f"ACP tools after --no-acp: {len(acp_tools)}")
|
||||
print(f"Remaining tools: {[t.__name__ for t in tools]}")
|
||||
print()
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def example_disable_lsp():
|
||||
"""Example: Disable LSP tools."""
|
||||
from praisonai.cli.features.interactive_tools import (
|
||||
get_interactive_tools,
|
||||
ToolConfig,
|
||||
)
|
||||
|
||||
print("=== Disable LSP Tools (--no-lsp) ===\n")
|
||||
|
||||
config = ToolConfig(workspace=os.getcwd(), enable_lsp=False)
|
||||
tools = get_interactive_tools(config=config, disable=["lsp"])
|
||||
|
||||
lsp_tools = [t for t in tools if t.__name__.startswith("lsp_")]
|
||||
print(f"LSP tools after --no-lsp: {len(lsp_tools)}")
|
||||
print(f"Remaining tools: {[t.__name__ for t in tools]}")
|
||||
print()
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def example_agent_with_default_tools():
|
||||
"""Example: Create an Agent with default interactive tools."""
|
||||
from praisonai.cli.features.interactive_tools import get_interactive_tools, ToolConfig
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("=== Agent with Default Interactive Tools ===\n")
|
||||
|
||||
# Get default tools
|
||||
config = ToolConfig(workspace=os.getcwd())
|
||||
tools = get_interactive_tools(config=config)
|
||||
|
||||
# Create agent with tools
|
||||
agent = Agent(
|
||||
name="InteractiveAgent",
|
||||
instructions="You are a helpful coding assistant with file and code analysis capabilities.",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print(f"Agent created with {len(tools)} tools")
|
||||
print(f"Tools: {[t.__name__ for t in tools]}")
|
||||
print()
|
||||
|
||||
# Simple test - list files
|
||||
print("Testing agent with list_files tool...")
|
||||
result = agent.start("List the Python files in the current directory using list_files tool.")
|
||||
print(f"Result: {result[:200]}..." if len(str(result)) > 200 else f"Result: {result}")
|
||||
print()
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("PraisonAI Interactive Default Tools Example")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Example 1: Get default tools
|
||||
example_get_default_tools()
|
||||
|
||||
# Example 2: Disable ACP
|
||||
example_disable_acp()
|
||||
|
||||
# Example 3: Disable LSP
|
||||
example_disable_lsp()
|
||||
|
||||
# Example 4: Agent with default tools (requires API key)
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
example_agent_with_default_tools()
|
||||
else:
|
||||
print("=== Agent Example Skipped (no OPENAI_API_KEY) ===")
|
||||
|
||||
print("=" * 60)
|
||||
print("Examples Complete")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
LSP Code Intelligence Example
|
||||
|
||||
This example demonstrates how to use LSP-powered tools through an Agent
|
||||
for semantic code analysis (listing symbols, finding definitions, etc.).
|
||||
|
||||
How to run:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python examples/lsp/lsp_code_analysis_example.py
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
pip install python-lsp-server # Optional, for full LSP support
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add paths for development
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai-agents'))
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("LSP CODE INTELLIGENCE EXAMPLE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Import after path setup
|
||||
from praisonai.cli.features import (
|
||||
create_agent_centric_tools,
|
||||
InteractiveRuntime,
|
||||
RuntimeConfig
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Use the praisonaiagents source as workspace for analysis
|
||||
workspace = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'../../src/praisonai-agents/praisonaiagents'
|
||||
)
|
||||
workspace = os.path.abspath(workspace)
|
||||
|
||||
print(f"Workspace: {workspace}")
|
||||
print()
|
||||
|
||||
# 1. Create runtime with LSP enabled
|
||||
print("1. Creating InteractiveRuntime with LSP...")
|
||||
config = RuntimeConfig(
|
||||
workspace=workspace,
|
||||
lsp_enabled=True, # Enable LSP for code intelligence
|
||||
acp_enabled=False, # Disable ACP for this example
|
||||
approval_mode="auto"
|
||||
)
|
||||
runtime = InteractiveRuntime(config)
|
||||
status = await runtime.start()
|
||||
|
||||
print(f" LSP enabled: {status['lsp']['enabled']}")
|
||||
print(f" LSP ready: {runtime.lsp_ready}")
|
||||
print(f" LSP status: {status['lsp']['status']}")
|
||||
if not runtime.lsp_ready:
|
||||
print(" Note: LSP not available, will use regex fallback")
|
||||
print()
|
||||
|
||||
# 2. Create agent-centric tools
|
||||
print("2. Creating agent-centric tools...")
|
||||
tools = create_agent_centric_tools(runtime)
|
||||
|
||||
# Filter to show only LSP tools
|
||||
lsp_tools = [t for t in tools if t.__name__.startswith('lsp_')]
|
||||
print(f" LSP tools available:")
|
||||
for tool in lsp_tools:
|
||||
print(f" - {tool.__name__}")
|
||||
print()
|
||||
|
||||
# 3. Create Agent with LSP-powered tools
|
||||
print("3. Creating Agent with LSP tools...")
|
||||
agent = Agent(
|
||||
name="CodeAnalyzer",
|
||||
instructions="""You are a code analysis assistant.
|
||||
Use lsp_list_symbols to list functions and classes in files.
|
||||
Use lsp_find_definition to find where symbols are defined.
|
||||
Use lsp_find_references to find where symbols are used.
|
||||
Provide clear, organized summaries of code structure.""",
|
||||
tools=tools,
|
||||
verbose=True
|
||||
)
|
||||
print()
|
||||
|
||||
# 4. Test 1: List symbols in a file
|
||||
print("4. TEST 1: List symbols in agent/agent.py")
|
||||
print("-" * 50)
|
||||
result1 = agent.start("List all the main classes and public methods in agent/agent.py. Give me a brief summary.")
|
||||
print("-" * 50)
|
||||
print(f"Summary: {str(result1)[:300]}...")
|
||||
print()
|
||||
|
||||
# 5. Test 2: Find definition of a symbol
|
||||
print("5. TEST 2: Find where 'Agent' class is defined")
|
||||
print("-" * 50)
|
||||
result2 = agent.start("Where is the Agent class defined? Use lsp_find_definition to find it.")
|
||||
print("-" * 50)
|
||||
print(f"Result: {str(result2)[:300]}...")
|
||||
print()
|
||||
|
||||
# 6. Test 3: Analyze main.py
|
||||
print("6. TEST 3: Analyze main.py structure")
|
||||
print("-" * 50)
|
||||
result3 = agent.start("List all the functions in main.py and briefly describe what the file does.")
|
||||
print("-" * 50)
|
||||
print(f"Analysis: {str(result3)[:300]}...")
|
||||
print()
|
||||
|
||||
# Cleanup
|
||||
await runtime.stop()
|
||||
|
||||
print("=" * 60)
|
||||
print("EXAMPLE COMPLETE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Key takeaways:")
|
||||
print("- LSP tools work through the Agent (agentic LSP)")
|
||||
print("- Falls back to regex if LSP server not available")
|
||||
print("- Returns structured data with citations")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
PraisonAI Output UX Examples
|
||||
|
||||
This example demonstrates the various output modes and configurations
|
||||
available in PraisonAI agents.
|
||||
|
||||
Features demonstrated:
|
||||
- verbose=True: Full output with agent info, task, and response panels
|
||||
- verbose=False: Clean output with just the result
|
||||
- Tool call display with consolidated output
|
||||
- Multi-agent output with clear attribution
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '/Users/praison/praisonai-package/src/praisonai-agents')
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get the current weather for a city.
|
||||
|
||||
Args:
|
||||
city: The name of the city to get weather for
|
||||
|
||||
Returns:
|
||||
Weather information string
|
||||
"""
|
||||
return f"The weather in {city} is sunny and 72°F"
|
||||
|
||||
|
||||
def example_verbose_true():
|
||||
"""Example with verbose=True (default) - shows full output panels."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 1: verbose=True (Full Output)")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be concise.",
|
||||
verbose=True # Default - shows Agent Info, Task, and Response panels
|
||||
)
|
||||
result = agent.start("What is the capital of France? Answer in one sentence.")
|
||||
print(f"\nReturned value: {result}")
|
||||
|
||||
|
||||
def example_verbose_false():
|
||||
"""Example with verbose=False - clean output, just the result."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 2: verbose=False (Clean Output)")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be concise.",
|
||||
verbose=False # No panels, just returns the result
|
||||
)
|
||||
result = agent.start("What is 2+2? Answer in one word.")
|
||||
print(f"Result: {result}")
|
||||
|
||||
|
||||
def example_with_tools():
|
||||
"""Example showing tool call output."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 3: Agent with Tools")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Weather Bot",
|
||||
instructions="You help users check the weather. Use the get_weather tool.",
|
||||
tools=[get_weather],
|
||||
verbose=True
|
||||
)
|
||||
result = agent.start("What's the weather in Tokyo?")
|
||||
print(f"\nReturned value: {result}")
|
||||
|
||||
|
||||
def example_multi_agent():
|
||||
"""Example showing multi-agent output with clear attribution."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 4: Multi-Agent Output")
|
||||
print("=" * 60)
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Analyst",
|
||||
instructions="You research topics and provide factual information.",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
instructions="You write clear summaries based on research.",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Research what AI is in one sentence",
|
||||
expected_output="A brief factual description",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
task2 = Task(
|
||||
description="Write a one-sentence summary about AI",
|
||||
expected_output="A clear summary",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print(f"\nFinal result: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("PRAISONAI OUTPUT UX EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
# Run examples
|
||||
example_verbose_true()
|
||||
example_verbose_false()
|
||||
example_with_tools()
|
||||
example_multi_agent()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL EXAMPLES COMPLETE")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,83 @@
|
||||
# PraisonAI Profiling Examples
|
||||
|
||||
This directory contains examples for profiling PraisonAI agent performance.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install praisonai
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Basic Profiling (`basic_profiling.py`)
|
||||
|
||||
Demonstrates programmatic profiling of a single query:
|
||||
|
||||
```bash
|
||||
python basic_profiling.py
|
||||
```
|
||||
|
||||
### 2. Suite Profiling (`suite_profiling.py`)
|
||||
|
||||
Runs a comprehensive profiling suite with multiple scenarios:
|
||||
|
||||
```bash
|
||||
python suite_profiling.py
|
||||
```
|
||||
|
||||
### 3. Optimization Demo (`optimization_example.py`)
|
||||
|
||||
Demonstrates Tier 0/1/2 performance optimizations:
|
||||
|
||||
```bash
|
||||
python optimization_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
You can also profile directly from the command line:
|
||||
|
||||
```bash
|
||||
# Profile a query
|
||||
praisonai profile query "What is 2+2?"
|
||||
|
||||
# Profile with file grouping
|
||||
praisonai profile query "Hello" --show-files --limit 20
|
||||
|
||||
# Profile imports
|
||||
praisonai profile imports
|
||||
|
||||
# Profile startup time
|
||||
praisonai profile startup
|
||||
|
||||
# Run comprehensive suite
|
||||
praisonai profile suite --quick
|
||||
|
||||
# Create performance baseline
|
||||
praisonai profile snapshot --baseline
|
||||
|
||||
# Compare against baseline
|
||||
praisonai profile snapshot current --compare
|
||||
|
||||
# Show optimization status
|
||||
praisonai profile optimize --show
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Text output**: Human-readable timing breakdown
|
||||
- **JSON output**: Machine-readable for CI/CD (`--format json`)
|
||||
- **Artifacts**: Binary cProfile data and reports (`--save ./results`)
|
||||
- **Snapshots**: Performance baselines for regression detection
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
Enable opt-in optimizations via environment variables:
|
||||
|
||||
```bash
|
||||
export PRAISONAI_LITE_MODE=1
|
||||
export PRAISONAI_SKIP_TYPE_VALIDATION=1
|
||||
export PRAISONAI_MINIMAL_IMPORTS=1
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Basic Profiling Example for PraisonAI
|
||||
|
||||
This example demonstrates how to use the unified profiler programmatically
|
||||
to measure agent performance.
|
||||
|
||||
Run with:
|
||||
python basic_profiling.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
from praisonai.cli.execution import (
|
||||
ExecutionRequest,
|
||||
Profiler,
|
||||
ProfilerConfig,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run a profiled query and display results."""
|
||||
|
||||
# Configure profiler (Layer 1 = basic profiling with function stats)
|
||||
config = ProfilerConfig(
|
||||
layer=1, # 0=minimal, 1=basic, 2=deep
|
||||
limit=15, # Show top 15 functions
|
||||
show_callers=False, # Set True for caller info (layer 2)
|
||||
show_callees=False, # Set True for callee info (layer 2)
|
||||
)
|
||||
|
||||
# Create execution request
|
||||
request = ExecutionRequest(
|
||||
prompt="What is 2+2?",
|
||||
agent_name="ProfiledAgent",
|
||||
)
|
||||
|
||||
# Run profiled query
|
||||
print("🔬 Running profiled query...")
|
||||
profiler = Profiler(config)
|
||||
result, report = profiler.profile_sync(request)
|
||||
|
||||
# Print formatted report
|
||||
print("\n" + report.to_text())
|
||||
|
||||
# Access timing data programmatically
|
||||
print("\n📊 Timing Summary:")
|
||||
print(f" Imports: {report.timing.imports_ms:>10.2f} ms")
|
||||
print(f" Agent Init: {report.timing.agent_init_ms:>10.2f} ms")
|
||||
print(f" Execution: {report.timing.execution_ms:>10.2f} ms")
|
||||
print(f" Total: {report.timing.total_ms:>10.2f} ms")
|
||||
|
||||
# Show top 5 functions
|
||||
if report.functions:
|
||||
print("\n🔥 Top 5 Functions by Cumulative Time:")
|
||||
for i, func in enumerate(report.functions[:5], 1):
|
||||
print(f" {i}. {func.name}: {func.cumulative_time_ms:.2f}ms")
|
||||
|
||||
# Show response
|
||||
print(f"\n💬 Response: {result.output}")
|
||||
|
||||
# Export to JSON
|
||||
print("\n📄 JSON Export available via: report.to_json()")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Performance Optimization Example for PraisonAI
|
||||
|
||||
This example demonstrates how to use the Tier 0/1/2 optimization features
|
||||
programmatically.
|
||||
|
||||
Run with:
|
||||
python optimization_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
|
||||
def demo_provider_cache():
|
||||
"""Demonstrate provider caching (Tier 0)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 0: Provider Cache Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import (
|
||||
get_provider_cache,
|
||||
resolve_model_provider,
|
||||
)
|
||||
|
||||
cache = get_provider_cache()
|
||||
cache.invalidate() # Start fresh
|
||||
|
||||
# Resolve some models
|
||||
models = ['gpt-4o', 'claude-3-opus', 'gemini-pro', 'gpt-4o-mini']
|
||||
|
||||
print("\nResolving models:")
|
||||
for model in models:
|
||||
provider, name = resolve_model_provider(model)
|
||||
print(f" {model} -> {provider}/{name}")
|
||||
|
||||
# Check cache stats
|
||||
stats = cache.get_stats()
|
||||
print("\nCache Stats:")
|
||||
print(f" Hits: {stats['hits']}")
|
||||
print(f" Misses: {stats['misses']}")
|
||||
print(f" Size: {stats['size']}")
|
||||
|
||||
|
||||
def demo_lazy_importer():
|
||||
"""Demonstrate lazy imports (Tier 0)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 0: Lazy Importer Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import LazyImporter
|
||||
import time
|
||||
|
||||
LazyImporter.clear()
|
||||
|
||||
# Time lazy import
|
||||
start = time.perf_counter()
|
||||
json_module = LazyImporter.get('json')
|
||||
first_import = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Second import (cached)
|
||||
start = time.perf_counter()
|
||||
json_module2 = LazyImporter.get('json')
|
||||
cached_import = (time.perf_counter() - start) * 1000
|
||||
|
||||
print(f"\nFirst import: {first_import:.3f}ms")
|
||||
print(f"Cached import: {cached_import:.3f}ms")
|
||||
print(f"Same object: {json_module is json_module2}")
|
||||
|
||||
|
||||
def demo_client_pool():
|
||||
"""Demonstrate client pooling (Tier 1)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 1: Client Pool Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import ClientPool
|
||||
|
||||
pool = ClientPool(max_size=3)
|
||||
|
||||
# Create some mock clients
|
||||
def create_client(name):
|
||||
return {'name': name, 'created': True}
|
||||
|
||||
# Get or create clients
|
||||
client1 = pool.get_or_create('openai', lambda: create_client('openai'))
|
||||
_ = pool.get_or_create('anthropic', lambda: create_client('anthropic'))
|
||||
|
||||
# Reuse existing client
|
||||
client1_again = pool.get_or_create('openai', lambda: create_client('openai_new'))
|
||||
|
||||
print(f"\nPool size: {pool.size()}")
|
||||
print(f"Client reused: {client1 is client1_again}")
|
||||
print(f"Client1 name: {client1['name']}")
|
||||
|
||||
|
||||
def demo_prewarm():
|
||||
"""Demonstrate pre-warming (Tier 1)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 1: Pre-warming Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import PrewarmManager
|
||||
import time
|
||||
|
||||
PrewarmManager.reset()
|
||||
|
||||
print(f"\nPre-warming enabled: {PrewarmManager.is_enabled()}")
|
||||
|
||||
# Enable and prewarm
|
||||
PrewarmManager.enable()
|
||||
print(f"After enable: {PrewarmManager.is_enabled()}")
|
||||
|
||||
# Prewarm OpenAI (runs in background)
|
||||
PrewarmManager.prewarm_provider('openai')
|
||||
print("Pre-warming OpenAI in background...")
|
||||
|
||||
# Wait a bit for background thread
|
||||
time.sleep(0.5)
|
||||
|
||||
print(f"OpenAI prewarmed: {PrewarmManager.is_prewarmed('openai')}")
|
||||
|
||||
PrewarmManager.reset()
|
||||
|
||||
|
||||
def demo_lite_mode():
|
||||
"""Demonstrate lite mode (Tier 2)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 2: Lite Mode Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import (
|
||||
LiteModeConfig,
|
||||
is_lite_mode,
|
||||
)
|
||||
|
||||
# Default config
|
||||
config = LiteModeConfig()
|
||||
print(f"\nDefault lite mode: {config.enabled}")
|
||||
|
||||
# Config from env
|
||||
print("\nTo enable lite mode, set environment variables:")
|
||||
print(" export PRAISONAI_LITE_MODE=1")
|
||||
print(" export PRAISONAI_SKIP_TYPE_VALIDATION=1")
|
||||
|
||||
print(f"\nCurrent is_lite_mode(): {is_lite_mode()}")
|
||||
|
||||
|
||||
def demo_perf_snapshot():
|
||||
"""Demonstrate performance snapshots (Tier 2)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Tier 2: Performance Snapshot Demo")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.cli.features.profiler import (
|
||||
PerfSnapshot,
|
||||
PerfComparison,
|
||||
PerfSnapshotManager,
|
||||
)
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Create manager with temp directory
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
manager = PerfSnapshotManager(snapshot_dir=Path(tmpdir))
|
||||
|
||||
# Create baseline
|
||||
baseline = PerfSnapshot(
|
||||
timestamp='2025-01-01T00:00:00Z',
|
||||
name='baseline',
|
||||
startup_cold_ms=100.0,
|
||||
startup_warm_ms=80.0,
|
||||
import_time_ms=500.0,
|
||||
query_time_ms=2000.0,
|
||||
first_token_ms=300.0,
|
||||
)
|
||||
manager.save_baseline(baseline)
|
||||
print("\nBaseline saved")
|
||||
|
||||
# Create current snapshot (simulated improvement)
|
||||
current = PerfSnapshot(
|
||||
timestamp='2025-01-02T00:00:00Z',
|
||||
name='current',
|
||||
startup_cold_ms=95.0, # 5% faster
|
||||
startup_warm_ms=78.0,
|
||||
import_time_ms=480.0, # 4% faster
|
||||
query_time_ms=1900.0, # 5% faster
|
||||
first_token_ms=280.0,
|
||||
)
|
||||
|
||||
# Compare
|
||||
comparison = PerfComparison(baseline=baseline, current=current)
|
||||
|
||||
print(f"\nStartup improvement: {-comparison.startup_cold_diff_pct:.1f}%")
|
||||
print(f"Import improvement: {-comparison.import_time_diff_pct:.1f}%")
|
||||
print(f"Query improvement: {-comparison.query_time_diff_pct:.1f}%")
|
||||
print(f"Is regression: {comparison.is_regression()}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all demos."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Performance Optimization Demo")
|
||||
print("=" * 60)
|
||||
|
||||
demo_provider_cache()
|
||||
demo_lazy_importer()
|
||||
demo_client_pool()
|
||||
demo_prewarm()
|
||||
demo_lite_mode()
|
||||
demo_perf_snapshot()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All demos complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"timestamp": "2025-12-31T18:45:38.187539Z",
|
||||
"metadata": {
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-15.7.4-arm64-arm-64bit",
|
||||
"praisonai_version": "2.9.2",
|
||||
"timestamp": "2025-12-31T18:45:38.226558Z"
|
||||
},
|
||||
"startup": {
|
||||
"cold_ms": 80.9305000002496,
|
||||
"warm_ms": 55.266541996388696
|
||||
},
|
||||
"import_analysis": [
|
||||
{
|
||||
"module": "praisonaiagents",
|
||||
"self_us": 2419,
|
||||
"cumulative_us": 1645390,
|
||||
"self_ms": 2.419,
|
||||
"cumulative_ms": 1645.39
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.workflows",
|
||||
"self_us": 670,
|
||||
"cumulative_us": 1375919,
|
||||
"self_ms": 0.67,
|
||||
"cumulative_ms": 1375.919
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.workflows.yaml_parser",
|
||||
"self_us": 775,
|
||||
"cumulative_us": 1369435,
|
||||
"self_ms": 0.775,
|
||||
"cumulative_ms": 1369.435
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.agent.agent",
|
||||
"self_us": 19,
|
||||
"cumulative_us": 1349174,
|
||||
"self_ms": 0.019,
|
||||
"cumulative_ms": 1349.174
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.agent",
|
||||
"self_us": 537,
|
||||
"cumulative_us": 1349155,
|
||||
"self_ms": 0.537,
|
||||
"cumulative_ms": 1349.155
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.agent.agent",
|
||||
"self_us": 1453,
|
||||
"cumulative_us": 1285299,
|
||||
"self_ms": 1.453,
|
||||
"cumulative_ms": 1285.299
|
||||
},
|
||||
{
|
||||
"module": "praisonaiagents.llm.openai_client",
|
||||
"self_us": 7887,
|
||||
"cumulative_us": 1123578,
|
||||
"self_ms": 7.887,
|
||||
"cumulative_ms": 1123.578
|
||||
},
|
||||
{
|
||||
"module": "openai",
|
||||
"self_us": 1083,
|
||||
"cumulative_us": 1115692,
|
||||
"self_ms": 1.083,
|
||||
"cumulative_ms": 1115.692
|
||||
},
|
||||
{
|
||||
"module": "openai.types",
|
||||
"self_us": 2606,
|
||||
"cumulative_us": 957990,
|
||||
"self_ms": 2.606,
|
||||
"cumulative_ms": 957.99
|
||||
},
|
||||
{
|
||||
"module": "openai.types.batch",
|
||||
"self_us": 1787,
|
||||
"cumulative_us": 485667,
|
||||
"self_ms": 1.787,
|
||||
"cumulative_ms": 485.667
|
||||
},
|
||||
{
|
||||
"module": "openai._models",
|
||||
"self_us": 13423,
|
||||
"cumulative_us": 450798,
|
||||
"self_ms": 13.423,
|
||||
"cumulative_ms": 450.798
|
||||
},
|
||||
{
|
||||
"module": "openai.types.eval_create_params",
|
||||
"self_us": 644,
|
||||
"cumulative_us": 393275,
|
||||
"self_ms": 0.644,
|
||||
"cumulative_ms": 393.275
|
||||
},
|
||||
{
|
||||
"module": "openai.types.graders.grader_inputs_param",
|
||||
"self_us": 17,
|
||||
"cumulative_us": 359515,
|
||||
"self_ms": 0.017,
|
||||
"cumulative_ms": 359.515
|
||||
},
|
||||
{
|
||||
"module": "openai.types.graders",
|
||||
"self_us": 392,
|
||||
"cumulative_us": 359498,
|
||||
"self_ms": 0.392,
|
||||
"cumulative_ms": 359.498
|
||||
},
|
||||
{
|
||||
"module": "openai.types.graders.multi_grader",
|
||||
"self_us": 3847,
|
||||
"cumulative_us": 351251,
|
||||
"self_ms": 3.847,
|
||||
"cumulative_ms": 351.251
|
||||
},
|
||||
{
|
||||
"module": "openai.types.graders.label_model_grader",
|
||||
"self_us": 5450,
|
||||
"cumulative_us": 337291,
|
||||
"self_ms": 5.45,
|
||||
"cumulative_ms": 337.291
|
||||
},
|
||||
{
|
||||
"module": "openai.types.graders.grader_inputs",
|
||||
"self_us": 545,
|
||||
"cumulative_us": 331841,
|
||||
"self_ms": 0.545,
|
||||
"cumulative_ms": 331.841
|
||||
},
|
||||
{
|
||||
"module": "openai.types.responses.response_input_text",
|
||||
"self_us": 21,
|
||||
"cumulative_us": 331297,
|
||||
"self_ms": 0.021,
|
||||
"cumulative_ms": 331.297
|
||||
},
|
||||
{
|
||||
"module": "openai.types.responses",
|
||||
"self_us": 1301,
|
||||
"cumulative_us": 331277,
|
||||
"self_ms": 1.301,
|
||||
"cumulative_ms": 331.277
|
||||
},
|
||||
{
|
||||
"module": "rich.logging",
|
||||
"self_us": 1066,
|
||||
"cumulative_us": 238256,
|
||||
"self_ms": 1.066,
|
||||
"cumulative_ms": 238.256
|
||||
}
|
||||
],
|
||||
"scenarios": [
|
||||
{
|
||||
"name": "simple_query",
|
||||
"config": {
|
||||
"prompt": "What is 2+2?",
|
||||
"model": "default",
|
||||
"stream": false,
|
||||
"iterations": 2
|
||||
},
|
||||
"import_time_stats": {
|
||||
"mean": 0.008833496394800022,
|
||||
"min": 0.005916997906751931,
|
||||
"max": 0.011749994882848114,
|
||||
"stdev": 0.0041245517164382375,
|
||||
"median": 0.008833496394800022
|
||||
},
|
||||
"total_time_stats": {
|
||||
"mean": 1727.4090415012324,
|
||||
"min": 1367.2264170018025,
|
||||
"max": 2087.5916660006624,
|
||||
"stdev": 509.3751524982296,
|
||||
"median": 1727.4090415012324
|
||||
},
|
||||
"first_token_stats": null,
|
||||
"raw_import_times_ms": [
|
||||
0.011749994882848114,
|
||||
0.005916997906751931
|
||||
],
|
||||
"raw_total_times_ms": [
|
||||
1367.2264170018025,
|
||||
2087.5916660006624
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "simple_stream",
|
||||
"config": {
|
||||
"prompt": "What is 2+2?",
|
||||
"model": "default",
|
||||
"stream": true,
|
||||
"iterations": 2
|
||||
},
|
||||
"import_time_stats": {
|
||||
"mean": 0.008792001608526334,
|
||||
"min": 0.006375004886649549,
|
||||
"max": 0.01120899833040312,
|
||||
"stdev": 0.0034181495442894613,
|
||||
"median": 0.008792001608526334
|
||||
},
|
||||
"total_time_stats": {
|
||||
"mean": 2321.0982290001994,
|
||||
"min": 1697.1089580038097,
|
||||
"max": 2945.087499996589,
|
||||
"stdev": 882.4540898183949,
|
||||
"median": 2321.0982290001994
|
||||
},
|
||||
"first_token_stats": {
|
||||
"mean": 2312.8511880022415,
|
||||
"min": 1681.2440420035273,
|
||||
"max": 2944.458334000956,
|
||||
"stdev": 893.2273919631452,
|
||||
"median": 2312.8511880022415
|
||||
},
|
||||
"raw_import_times_ms": [
|
||||
0.01120899833040312,
|
||||
0.006375004886649549
|
||||
],
|
||||
"raw_total_times_ms": [
|
||||
2945.087499996589,
|
||||
1697.1089580038097
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Profile Suite Example for PraisonAI
|
||||
|
||||
This example demonstrates how to run a comprehensive profiling suite
|
||||
programmatically with multiple scenarios.
|
||||
|
||||
Run with:
|
||||
python suite_profiling.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
from praisonai.cli.features.profiler import (
|
||||
ScenarioConfig,
|
||||
run_profile_suite,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run a profiling suite with custom scenarios."""
|
||||
|
||||
# Define custom scenarios
|
||||
scenarios = [
|
||||
ScenarioConfig(
|
||||
name="simple_query",
|
||||
prompt="What is 2+2?",
|
||||
stream=False,
|
||||
iterations=2,
|
||||
warmup=1,
|
||||
),
|
||||
ScenarioConfig(
|
||||
name="simple_stream",
|
||||
prompt="What is 2+2?",
|
||||
stream=True,
|
||||
iterations=2,
|
||||
warmup=1,
|
||||
),
|
||||
]
|
||||
|
||||
# Run suite with custom output directory
|
||||
print("🔬 Running Profile Suite...")
|
||||
result = run_profile_suite(
|
||||
output_dir="./profile_results",
|
||||
scenarios=scenarios,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 60)
|
||||
print("Profile Suite Results")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n📊 Startup Times:")
|
||||
print(f" Cold Start: {result.startup_cold_ms:.2f}ms")
|
||||
print(f" Warm Start: {result.startup_warm_ms:.2f}ms")
|
||||
|
||||
if result.import_analysis:
|
||||
print("\n📦 Top Import:")
|
||||
print(f" {result.import_analysis[0]['module']}: {result.import_analysis[0]['cumulative_ms']:.2f}ms")
|
||||
|
||||
print("\n🎯 Scenario Results:")
|
||||
for scenario in result.scenarios:
|
||||
stats = scenario.get_stats(scenario.total_times)
|
||||
print(f" {scenario.name}:")
|
||||
print(f" Mean: {stats['mean']:.2f}ms")
|
||||
print(f" Min: {stats['min']:.2f}ms")
|
||||
print(f" Max: {stats['max']:.2f}ms")
|
||||
|
||||
print("\n✅ Results saved to: ./profile_results/")
|
||||
print(" - suite_results.json (machine-readable)")
|
||||
print(" - suite_report.txt (human-readable)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Session Persistence Example
|
||||
|
||||
Demonstrates automatic session persistence with zero configuration.
|
||||
When you provide a session_id to an Agent, conversation history is
|
||||
automatically saved to disk and restored on subsequent runs.
|
||||
|
||||
Usage:
|
||||
python session_persistence_example.py
|
||||
|
||||
Requirements:
|
||||
- praisonaiagents package installed
|
||||
- OPENAI_API_KEY environment variable set
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def example_basic_persistence():
|
||||
"""Basic session persistence example."""
|
||||
print("=" * 60)
|
||||
print("EXAMPLE 1: Basic Session Persistence")
|
||||
print("=" * 60)
|
||||
|
||||
session_id = "demo-session-basic"
|
||||
|
||||
# Create agent with session_id
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# First message
|
||||
print("\n[User]: My favorite color is blue and my lucky number is 7.")
|
||||
response1 = agent.chat("My favorite color is blue and my lucky number is 7.")
|
||||
print(f"[Assistant]: {response1}")
|
||||
|
||||
# Second message - agent remembers
|
||||
print("\n[User]: What is my favorite color?")
|
||||
response2 = agent.chat("What is my favorite color?")
|
||||
print(f"[Assistant]: {response2}")
|
||||
|
||||
print(f"\n✅ Session saved to: ~/.praison/sessions/{session_id}.json")
|
||||
print(f" Chat history length: {len(agent.chat_history)} messages")
|
||||
|
||||
|
||||
def example_session_restoration():
|
||||
"""Demonstrates session restoration across Agent instances."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 2: Session Restoration")
|
||||
print("=" * 60)
|
||||
|
||||
session_id = "demo-session-restore"
|
||||
|
||||
# First agent instance
|
||||
print("\n--- First Agent Instance ---")
|
||||
agent1 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
print("[User]: Remember that my pet's name is Max.")
|
||||
response1 = agent1.chat("Remember that my pet's name is Max.")
|
||||
print(f"[Assistant]: {response1}")
|
||||
|
||||
# Simulate new process by creating new agent instance
|
||||
print("\n--- New Agent Instance (simulating restart) ---")
|
||||
agent2 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
print("[User]: What is my pet's name?")
|
||||
response2 = agent2.chat("What is my pet's name?")
|
||||
print(f"[Assistant]: {response2}")
|
||||
|
||||
if "max" in response2.lower():
|
||||
print("\n✅ Session restored successfully! Agent remembered 'Max'.")
|
||||
else:
|
||||
print("\n⚠️ Agent may not have remembered correctly.")
|
||||
|
||||
|
||||
def example_in_memory_only():
|
||||
"""Demonstrates in-memory memory without persistence."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 3: In-Memory Only (No session_id)")
|
||||
print("=" * 60)
|
||||
|
||||
# Agent without session_id - in-memory only
|
||||
agent = Agent(
|
||||
name="TempBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
print("\n[User]: My name is Alice.")
|
||||
response1 = agent.chat("My name is Alice.")
|
||||
print(f"[Assistant]: {response1}")
|
||||
|
||||
print("\n[User]: What is my name?")
|
||||
response2 = agent.chat("What is my name?")
|
||||
print(f"[Assistant]: {response2}")
|
||||
|
||||
print(f"\n✅ In-memory chat history: {len(agent.chat_history)} messages")
|
||||
print(" (This will be lost when the Agent instance is garbage collected)")
|
||||
|
||||
|
||||
def example_direct_store_access():
|
||||
"""Demonstrates direct access to the session store."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 4: Direct Session Store Access")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents.session import get_default_session_store
|
||||
|
||||
store = get_default_session_store()
|
||||
session_id = "demo-direct-access"
|
||||
|
||||
# Add messages directly
|
||||
store.add_user_message(session_id, "Hello from direct access!")
|
||||
store.add_assistant_message(session_id, "Hi! I received your message.")
|
||||
|
||||
# Get history
|
||||
history = store.get_chat_history(session_id)
|
||||
print(f"\n✅ Added {len(history)} messages directly to session store")
|
||||
|
||||
for msg in history:
|
||||
print(f" [{msg['role']}]: {msg['content']}")
|
||||
|
||||
# List all sessions
|
||||
sessions = store.list_sessions()
|
||||
print(f"\n📋 Total sessions in store: {len(sessions)}")
|
||||
|
||||
# Clean up demo session
|
||||
store.delete_session(session_id)
|
||||
print(f" Deleted demo session: {session_id}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all examples."""
|
||||
print("\n" + "=" * 60)
|
||||
print("SESSION PERSISTENCE EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
# Run examples
|
||||
example_basic_persistence()
|
||||
example_session_restoration()
|
||||
example_in_memory_only()
|
||||
example_direct_store_access()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL EXAMPLES COMPLETE")
|
||||
print("=" * 60)
|
||||
print("\nSession files are stored in: ~/.praison/sessions/")
|
||||
print("You can inspect them with: cat ~/.praison/sessions/<session_id>.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Basic TUI Usage Example for PraisonAI.
|
||||
|
||||
This example demonstrates how to launch the interactive TUI.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""Launch the TUI."""
|
||||
try:
|
||||
from praisonai.cli.features.tui import run_tui
|
||||
|
||||
# Launch with default settings
|
||||
run_tui()
|
||||
|
||||
except ImportError:
|
||||
print("Textual is required for TUI. Install with:")
|
||||
print(" pip install praisonai[tui]")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Headless TUI Simulation Example for PraisonAI.
|
||||
|
||||
This example demonstrates how to run TUI simulations without
|
||||
launching the interactive UI - useful for testing and CI.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from praisonai.cli.features.tui import (
|
||||
TuiOrchestrator,
|
||||
SimulationRunner,
|
||||
MockProvider,
|
||||
)
|
||||
from praisonai.cli.features.tui.orchestrator import OutputMode
|
||||
from praisonai.cli.features.queue import QueueConfig
|
||||
|
||||
|
||||
async def run_simulation():
|
||||
"""Run a headless simulation."""
|
||||
|
||||
# Create orchestrator with mock provider (no real API calls)
|
||||
config = QueueConfig(enable_persistence=False)
|
||||
orchestrator = TuiOrchestrator(
|
||||
queue_config=config,
|
||||
output_mode=OutputMode.PRETTY,
|
||||
debug=True,
|
||||
)
|
||||
|
||||
# Create simulation runner
|
||||
runner = SimulationRunner(orchestrator, assert_mode=True)
|
||||
|
||||
# Define simulation script
|
||||
script = {
|
||||
"session_id": "sim-example",
|
||||
"model": "gpt-4o-mini",
|
||||
"steps": [
|
||||
# Navigate to different screens
|
||||
{"action": "navigate", "args": {"screen": "main"}},
|
||||
{"action": "focus", "args": {"widget": "composer"}},
|
||||
|
||||
# Change model
|
||||
{
|
||||
"action": "model",
|
||||
"args": {"model": "gpt-4"},
|
||||
"expected": {"model": "gpt-4"}
|
||||
},
|
||||
|
||||
# Navigate to queue screen
|
||||
{"action": "navigate", "args": {"screen": "queue"}},
|
||||
|
||||
# Take a snapshot
|
||||
{"action": "snapshot"},
|
||||
|
||||
# Navigate back
|
||||
{"action": "navigate", "args": {"screen": "main"}},
|
||||
|
||||
# Small delay
|
||||
{"action": "sleep", "args": {"seconds": 0.5}},
|
||||
]
|
||||
}
|
||||
|
||||
# Run the simulation
|
||||
print("Running headless TUI simulation...")
|
||||
print("=" * 50)
|
||||
|
||||
success = await runner.run_script(script)
|
||||
|
||||
# Print results
|
||||
print("=" * 50)
|
||||
summary = runner.get_summary()
|
||||
|
||||
print(f"\nSimulation {'PASSED' if success else 'FAILED'}")
|
||||
print(f" Assertions passed: {summary['assertions_passed']}")
|
||||
print(f" Assertions failed: {summary['assertions_failed']}")
|
||||
|
||||
if summary['errors']:
|
||||
print("\nErrors:")
|
||||
for error in summary['errors']:
|
||||
print(f" - {error}")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
async def run_with_events():
|
||||
"""Run simulation and capture all events."""
|
||||
|
||||
config = QueueConfig(enable_persistence=False)
|
||||
orchestrator = TuiOrchestrator(
|
||||
queue_config=config,
|
||||
output_mode=OutputMode.SILENT,
|
||||
)
|
||||
|
||||
# Capture events
|
||||
events = []
|
||||
orchestrator.add_event_callback(lambda e: events.append(e))
|
||||
|
||||
await orchestrator.start(session_id="event-capture")
|
||||
|
||||
# Perform some actions
|
||||
orchestrator.set_model("gpt-4")
|
||||
orchestrator.navigate_screen("queue")
|
||||
orchestrator.set_focus("queue-panel")
|
||||
orchestrator.navigate_screen("main")
|
||||
|
||||
await orchestrator.stop()
|
||||
|
||||
# Print captured events
|
||||
print("\nCaptured Events:")
|
||||
print("-" * 40)
|
||||
for event in events:
|
||||
print(f" {event.event_type.value}")
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
print("PraisonAI TUI Headless Simulation Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Run simulation
|
||||
success = asyncio.run(run_simulation())
|
||||
|
||||
# Run event capture
|
||||
print("\n" + "=" * 50)
|
||||
events = asyncio.run(run_with_events())
|
||||
|
||||
print(f"\nTotal events captured: {len(events)}")
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Queue Management Example for PraisonAI.
|
||||
|
||||
This example demonstrates how to use the queue system programmatically.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from praisonai.cli.features.queue import (
|
||||
QueueManager,
|
||||
QueueConfig,
|
||||
QueuedRun,
|
||||
RunState,
|
||||
RunPriority,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Demonstrate queue management."""
|
||||
|
||||
print("PraisonAI Queue Management Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Create queue configuration
|
||||
config = QueueConfig(
|
||||
max_concurrent_global=2,
|
||||
max_concurrent_per_agent=1,
|
||||
max_queue_size=10,
|
||||
enable_persistence=False, # Use in-memory for demo
|
||||
)
|
||||
|
||||
# Track outputs
|
||||
outputs = {}
|
||||
|
||||
async def on_output(run_id: str, chunk: str):
|
||||
if run_id not in outputs:
|
||||
outputs[run_id] = ""
|
||||
outputs[run_id] += chunk
|
||||
print(f" [{run_id[:8]}] {chunk}", end="", flush=True)
|
||||
|
||||
async def on_complete(run_id: str, run: QueuedRun):
|
||||
print(f"\n [{run_id[:8]}] Completed: {run.state.value}")
|
||||
|
||||
async def on_error(run_id: str, error: Exception):
|
||||
print(f"\n [{run_id[:8]}] Error: {error}")
|
||||
|
||||
# Create queue manager
|
||||
manager = QueueManager(
|
||||
config=config,
|
||||
on_output=on_output,
|
||||
on_complete=on_complete,
|
||||
on_error=on_error,
|
||||
)
|
||||
|
||||
# Start the manager
|
||||
await manager.start(recover=False)
|
||||
print("Queue manager started")
|
||||
|
||||
# Submit some runs with different priorities
|
||||
print("\nSubmitting runs...")
|
||||
|
||||
run1 = await manager.submit(
|
||||
input_content="Low priority task",
|
||||
agent_name="Agent1",
|
||||
priority=RunPriority.LOW,
|
||||
)
|
||||
print(f" Submitted run1: {run1} (LOW priority)")
|
||||
|
||||
run2 = await manager.submit(
|
||||
input_content="High priority task",
|
||||
agent_name="Agent2",
|
||||
priority=RunPriority.HIGH,
|
||||
)
|
||||
print(f" Submitted run2: {run2} (HIGH priority)")
|
||||
|
||||
run3 = await manager.submit(
|
||||
input_content="Normal priority task",
|
||||
agent_name="Agent1",
|
||||
priority=RunPriority.NORMAL,
|
||||
)
|
||||
print(f" Submitted run3: {run3} (NORMAL priority)")
|
||||
|
||||
# Check queue status
|
||||
print(f"\nQueue status:")
|
||||
print(f" Queued: {manager.queued_count}")
|
||||
print(f" Running: {manager.running_count}")
|
||||
|
||||
# List runs
|
||||
runs = manager.list_runs()
|
||||
print(f"\nAll runs ({len(runs)}):")
|
||||
for run in runs:
|
||||
print(f" {run.run_id[:8]}: {run.agent_name} - {run.state.value} ({run.priority.name})")
|
||||
|
||||
# Wait a bit for processing
|
||||
print("\nWaiting for processing...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Cancel a run
|
||||
if runs:
|
||||
cancel_id = runs[-1].run_id
|
||||
print(f"\nCancelling run: {cancel_id[:8]}")
|
||||
await manager.cancel(cancel_id)
|
||||
|
||||
# Get stats
|
||||
stats = manager.get_stats()
|
||||
print(f"\nQueue statistics:")
|
||||
print(f" Total runs: {stats.total_runs}")
|
||||
print(f" Queued: {stats.queued_count}")
|
||||
print(f" Running: {stats.running_count}")
|
||||
print(f" Succeeded: {stats.succeeded_count}")
|
||||
print(f" Failed: {stats.failed_count}")
|
||||
print(f" Cancelled: {stats.cancelled_count}")
|
||||
|
||||
# Stop the manager
|
||||
await manager.stop()
|
||||
print("\nQueue manager stopped")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Example complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -687,6 +687,11 @@ Your Goal: {self.goal}
|
||||
self._db = db
|
||||
self._session_id = session_id
|
||||
self._db_initialized = False
|
||||
|
||||
# Session store for JSON-based persistence (lazy - no imports until used)
|
||||
# Used when session_id is provided but no DB adapter
|
||||
self._session_store = None
|
||||
self._session_store_initialized = False
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
@@ -2094,6 +2099,49 @@ Your Goal: {self.goal}"""
|
||||
|
||||
self._db_initialized = True
|
||||
self._current_run_id = None # Track current run
|
||||
|
||||
def _init_session_store(self):
|
||||
"""
|
||||
Initialize session store for JSON-based persistence (lazy, first chat only).
|
||||
|
||||
This is used when session_id is provided but no DB adapter.
|
||||
Enables automatic session persistence with zero configuration.
|
||||
"""
|
||||
if self._session_store_initialized:
|
||||
return
|
||||
|
||||
# Only initialize if session_id is provided and no DB adapter
|
||||
if self._session_id is None or self._db is not None:
|
||||
self._session_store_initialized = True
|
||||
return
|
||||
|
||||
try:
|
||||
from ..session import get_default_session_store
|
||||
self._session_store = get_default_session_store()
|
||||
|
||||
# Restore chat history from previous session
|
||||
history = self._session_store.get_chat_history(self._session_id)
|
||||
if history:
|
||||
# Only restore if chat_history is empty (avoid duplicates)
|
||||
if not self.chat_history:
|
||||
for msg in history:
|
||||
self.chat_history.append({
|
||||
"role": msg["role"],
|
||||
"content": msg["content"]
|
||||
})
|
||||
logging.info(f"Restored session {self._session_id} with {len(history)} messages from JSON store")
|
||||
|
||||
# Set agent info
|
||||
self._session_store.set_agent_info(
|
||||
self._session_id,
|
||||
agent_name=self.name,
|
||||
user_id=self.user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to initialize session store: {e}")
|
||||
self._session_store = None
|
||||
|
||||
self._session_store_initialized = True
|
||||
|
||||
def _start_run(self, input_content: str):
|
||||
"""Start a new run (turn) for persistence tracking."""
|
||||
@@ -2135,17 +2183,27 @@ Your Goal: {self.goal}"""
|
||||
self._current_run_id = None
|
||||
|
||||
def _persist_message(self, role: str, content: str):
|
||||
"""Persist a message to the DB if adapter is provided."""
|
||||
if self._db is None:
|
||||
"""Persist a message to the DB or session store."""
|
||||
# Try DB adapter first
|
||||
if self._db is not None:
|
||||
try:
|
||||
if role == "user":
|
||||
self._db.on_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._db.on_agent_message(self._session_id, content)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to DB: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
if role == "user":
|
||||
self._db.on_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._db.on_agent_message(self._session_id, content)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message: {e}")
|
||||
# Fall back to session store (JSON-based)
|
||||
if self._session_store is not None and self._session_id is not None:
|
||||
try:
|
||||
if role == "user":
|
||||
self._session_store.add_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._session_store.add_assistant_message(self._session_id, content)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to session store: {e}")
|
||||
|
||||
@property
|
||||
def session_id(self) -> Optional[str]:
|
||||
@@ -2160,6 +2218,10 @@ Your Goal: {self.goal}"""
|
||||
# Initialize DB session on first chat (lazy)
|
||||
self._init_db_session()
|
||||
|
||||
# Initialize session store for JSON-based persistence (lazy)
|
||||
# This enables automatic session persistence when session_id is provided
|
||||
self._init_session_store()
|
||||
|
||||
# Start a new run for this chat turn
|
||||
prompt_str = prompt if isinstance(prompt, str) else str(prompt)
|
||||
self._start_run(prompt_str)
|
||||
|
||||
@@ -58,6 +58,12 @@ class LLM:
|
||||
# Class-level flag for one-time logging configuration
|
||||
_logging_configured = False
|
||||
|
||||
# Class-level cache for LiteLLM module (avoids repeated import overhead)
|
||||
_litellm_module = None
|
||||
|
||||
# Class-level flag for LiteLLM global settings (set once)
|
||||
_litellm_configured = False
|
||||
|
||||
# Default window sizes for different models (75% of actual to be safe)
|
||||
MODEL_WINDOWS = {
|
||||
# OpenAI
|
||||
@@ -272,14 +278,16 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
if not LLM._logging_configured:
|
||||
LLM._configure_logging()
|
||||
|
||||
# Import litellm after logging is configured
|
||||
try:
|
||||
import litellm
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"LiteLLM is required but not installed. "
|
||||
"Please install with: pip install 'praisonaiagents[llm]'"
|
||||
)
|
||||
# Use cached LiteLLM module or import once (avoids repeated import overhead)
|
||||
if LLM._litellm_module is None:
|
||||
try:
|
||||
import litellm
|
||||
LLM._litellm_module = litellm
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"LiteLLM is required but not installed. "
|
||||
"Please install with: pip install 'praisonaiagents[llm]'"
|
||||
)
|
||||
|
||||
self.model = model
|
||||
self.timeout = timeout
|
||||
@@ -341,11 +349,12 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
self._formatted_tools_cache = {}
|
||||
self._max_cache_size = 100
|
||||
|
||||
# Enable error dropping for cleaner output
|
||||
import litellm
|
||||
litellm.drop_params = True
|
||||
# Enable parameter modification for providers like Anthropic
|
||||
litellm.modify_params = True
|
||||
# Configure LiteLLM global settings only once (avoids repeated setup cost)
|
||||
if not LLM._litellm_configured:
|
||||
litellm = LLM._litellm_module
|
||||
litellm.drop_params = True
|
||||
litellm.modify_params = True
|
||||
LLM._litellm_configured = True
|
||||
self._setup_event_tracking(events)
|
||||
|
||||
# Log all initialization parameters when in debug mode or verbose >= 10
|
||||
|
||||
@@ -150,7 +150,10 @@ def _clean_display_content(content: str, max_length: int = 20000) -> str:
|
||||
return content.strip()
|
||||
|
||||
def display_interaction(message, response, markdown=True, generation_time=None, console=None, agent_name=None, agent_role=None, agent_tools=None, task_name=None, task_description=None, task_id=None):
|
||||
"""Synchronous version of display_interaction."""
|
||||
"""Synchronous version of display_interaction.
|
||||
|
||||
Displays the task/message and response in clean panels with response time in title.
|
||||
"""
|
||||
if console is None:
|
||||
console = Console()
|
||||
|
||||
@@ -179,16 +182,18 @@ def display_interaction(message, response, markdown=True, generation_time=None,
|
||||
task_description=task_description,
|
||||
task_id=task_id
|
||||
)
|
||||
# Rest of the display logic...
|
||||
|
||||
# Build response title with time (Agno-style: "Response (1.2s)")
|
||||
response_title = "Response"
|
||||
if generation_time:
|
||||
console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
|
||||
response_title = f"Response ({generation_time:.1f}s)"
|
||||
|
||||
if markdown:
|
||||
console.print(Panel.fit(Markdown(message), title="Task", border_style="cyan"))
|
||||
console.print(Panel.fit(Markdown(response), title="Response", border_style="cyan"))
|
||||
console.print(Panel.fit(Markdown(response), title=response_title, border_style="cyan"))
|
||||
else:
|
||||
console.print(Panel.fit(Text(message, style="bold green"), title="Task", border_style="cyan"))
|
||||
console.print(Panel.fit(Text(response, style="bold blue"), title="Response", border_style="cyan"))
|
||||
console.print(Panel.fit(Text(response, style="bold blue"), title=response_title, border_style="cyan"))
|
||||
|
||||
def display_self_reflection(message: str, console=None):
|
||||
if not message or not message.strip():
|
||||
@@ -226,7 +231,16 @@ def display_instruction(message: str, console=None, agent_name: str = None, agen
|
||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
console.print(Panel.fit(Text(message, style="bold blue"), title="Instruction", border_style="cyan"))
|
||||
|
||||
def display_tool_call(message: str, console=None):
|
||||
def display_tool_call(message: str, console=None, tool_name: str = None, tool_input: dict = None, tool_output: str = None):
|
||||
"""Display tool call information in a consolidated panel.
|
||||
|
||||
Args:
|
||||
message: The tool call message (legacy format)
|
||||
console: Rich console for output
|
||||
tool_name: Name of the tool being called
|
||||
tool_input: Input arguments to the tool
|
||||
tool_output: Output from the tool (if available)
|
||||
"""
|
||||
logging.debug(f"display_tool_call called with message: {repr(message)}")
|
||||
if not message or not message.strip():
|
||||
logging.debug("Empty message in display_tool_call, returning early")
|
||||
@@ -235,11 +249,21 @@ def display_tool_call(message: str, console=None):
|
||||
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
|
||||
|
||||
# Execute synchronous callbacks (always, even when console is None)
|
||||
execute_sync_callback('tool_call', message=message)
|
||||
execute_sync_callback('tool_call', message=message, tool_name=tool_name, tool_input=tool_input, tool_output=tool_output)
|
||||
|
||||
# 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"))
|
||||
# Build consolidated tool call display (Agno-style)
|
||||
if tool_name and tool_input is not None:
|
||||
# Format: tool_name(arg1=val1, arg2=val2)
|
||||
args_str = ", ".join(f"{k}={v}" for k, v in tool_input.items()) if tool_input else ""
|
||||
tool_display = f"• {tool_name}({args_str})"
|
||||
if tool_output:
|
||||
tool_display += f"\n → {tool_output}"
|
||||
console.print(Panel.fit(Text(tool_display, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
else:
|
||||
# Legacy format
|
||||
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():
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Session Persistence for PraisonAI Agents
|
||||
|
||||
This module provides automatic session persistence with zero configuration.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# With session persistence (auto-enabled)
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session-123"
|
||||
)
|
||||
agent.start("Hello, my name is Alice")
|
||||
|
||||
# Later, in a new process - history is restored automatically
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session-123"
|
||||
)
|
||||
agent.start("What is my name?") # Agent remembers: "Alice"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When you provide a `session_id` to an Agent:
|
||||
|
||||
1. **Automatic Persistence**: Conversation history is automatically saved to disk
|
||||
2. **Automatic Restoration**: When a new Agent is created with the same `session_id`, history is restored
|
||||
3. **Zero Configuration**: No database setup required - uses JSON files by default
|
||||
|
||||
### Default Storage Location
|
||||
|
||||
Sessions are stored in: `~/.praison/sessions/{session_id}.json`
|
||||
|
||||
### Session File Format
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "my-session-123",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello", "timestamp": 1704153600.0},
|
||||
{"role": "assistant", "content": "Hi there!", "timestamp": 1704153601.5}
|
||||
],
|
||||
"created_at": "2026-01-02T04:00:00+00:00",
|
||||
"updated_at": "2026-01-02T04:01:00+00:00",
|
||||
"agent_name": "Assistant"
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior Matrix
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| `session_id` provided, no DB | JSON persistence (auto) |
|
||||
| `session_id` provided, with DB | DB adapter used |
|
||||
| No `session_id`, same Agent instance | In-memory only |
|
||||
| No `session_id`, new Agent instance | No history |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Direct Session Store Access
|
||||
|
||||
```python
|
||||
from praisonaiagents.session import get_default_session_store
|
||||
|
||||
store = get_default_session_store()
|
||||
|
||||
# Add messages
|
||||
store.add_user_message("session-123", "Hello")
|
||||
store.add_assistant_message("session-123", "Hi there!")
|
||||
|
||||
# Get history
|
||||
history = store.get_chat_history("session-123")
|
||||
# [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there!"}]
|
||||
|
||||
# List all sessions
|
||||
sessions = store.list_sessions()
|
||||
|
||||
# Delete a session
|
||||
store.delete_session("session-123")
|
||||
```
|
||||
|
||||
### Custom Session Directory
|
||||
|
||||
```python
|
||||
from praisonaiagents.session import DefaultSessionStore
|
||||
|
||||
store = DefaultSessionStore(
|
||||
session_dir="/custom/path/sessions",
|
||||
max_messages=200, # Default: 100
|
||||
lock_timeout=10.0, # Default: 5.0 seconds
|
||||
)
|
||||
```
|
||||
|
||||
### Using with DB Adapter
|
||||
|
||||
When a DB adapter is provided, it takes precedence over JSON persistence:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.db import PostgresAdapter
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session",
|
||||
db=PostgresAdapter(connection_string="...")
|
||||
)
|
||||
```
|
||||
|
||||
## Multi-Process Safety
|
||||
|
||||
The session store uses file locking to ensure safe concurrent access:
|
||||
|
||||
- **Unix**: Uses `fcntl.flock()` for file locking
|
||||
- **Windows**: Uses `msvcrt.locking()` for file locking
|
||||
- **Atomic Writes**: Uses temp file + rename to prevent corruption
|
||||
|
||||
Multiple processes can safely read/write to the same session.
|
||||
|
||||
## Context Caching
|
||||
|
||||
For cost optimization, use `prompt_caching=True` with Anthropic models:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session",
|
||||
prompt_caching=True, # Enables Anthropic prompt caching
|
||||
)
|
||||
```
|
||||
|
||||
This caches the system prompt, reducing token costs for repeated conversations.
|
||||
|
||||
## API Reference
|
||||
|
||||
### DefaultSessionStore
|
||||
|
||||
```python
|
||||
class DefaultSessionStore:
|
||||
def __init__(
|
||||
self,
|
||||
session_dir: Optional[str] = None, # Default: ~/.praison/sessions/
|
||||
max_messages: int = 100,
|
||||
lock_timeout: float = 5.0,
|
||||
): ...
|
||||
|
||||
def add_message(self, session_id: str, role: str, content: str) -> bool: ...
|
||||
def add_user_message(self, session_id: str, content: str) -> bool: ...
|
||||
def add_assistant_message(self, session_id: str, content: str) -> bool: ...
|
||||
def get_chat_history(self, session_id: str, max_messages: int = None) -> List[Dict]: ...
|
||||
def get_session(self, session_id: str) -> SessionData: ...
|
||||
def clear_session(self, session_id: str) -> bool: ...
|
||||
def delete_session(self, session_id: str) -> bool: ...
|
||||
def list_sessions(self, limit: int = 50) -> List[Dict]: ...
|
||||
def session_exists(self, session_id: str) -> bool: ...
|
||||
```
|
||||
|
||||
### SessionData
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SessionData:
|
||||
session_id: str
|
||||
messages: List[SessionMessage]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
agent_name: Optional[str]
|
||||
user_id: Optional[str]
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
def get_chat_history(self, max_messages: int = None) -> List[Dict[str, str]]: ...
|
||||
```
|
||||
|
||||
### SessionMessage
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SessionMessage:
|
||||
role: str # "user", "assistant", "system"
|
||||
content: str
|
||||
timestamp: float
|
||||
metadata: Dict[str, Any]
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Session persistence module for PraisonAI Agents.
|
||||
|
||||
Provides automatic session persistence with zero configuration.
|
||||
When a session_id is provided to an Agent, conversation history
|
||||
is automatically persisted to disk and restored on subsequent runs.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# With session persistence (auto-enabled)
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session-123"
|
||||
)
|
||||
agent.start("Hello")
|
||||
|
||||
# Later, new process - history is restored
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
session_id="my-session-123"
|
||||
)
|
||||
agent.start("What did I say before?") # Remembers!
|
||||
|
||||
Default storage: ~/.praison/sessions/{session_id}.json
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .store import DefaultSessionStore, SessionMessage, SessionData
|
||||
|
||||
# Lazy loading for zero import overhead
|
||||
_module_cache = {}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load session components."""
|
||||
if name in _module_cache:
|
||||
return _module_cache[name]
|
||||
|
||||
if name == "DefaultSessionStore":
|
||||
from .store import DefaultSessionStore
|
||||
_module_cache[name] = DefaultSessionStore
|
||||
return DefaultSessionStore
|
||||
|
||||
if name == "SessionMessage":
|
||||
from .store import SessionMessage
|
||||
_module_cache[name] = SessionMessage
|
||||
return SessionMessage
|
||||
|
||||
if name == "SessionData":
|
||||
from .store import SessionData
|
||||
_module_cache[name] = SessionData
|
||||
return SessionData
|
||||
|
||||
if name == "get_default_session_store":
|
||||
from .store import get_default_session_store
|
||||
_module_cache[name] = get_default_session_store
|
||||
return get_default_session_store
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DefaultSessionStore",
|
||||
"SessionMessage",
|
||||
"SessionData",
|
||||
"get_default_session_store",
|
||||
]
|
||||
@@ -0,0 +1,521 @@
|
||||
"""
|
||||
Default Session Store for PraisonAI Agents.
|
||||
|
||||
JSON-based session persistence with file locking and atomic writes.
|
||||
Zero dependencies beyond stdlib.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default session directory
|
||||
DEFAULT_SESSION_DIR = os.path.expanduser("~/.praison/sessions")
|
||||
|
||||
# Default limits
|
||||
DEFAULT_MAX_MESSAGES = 100
|
||||
DEFAULT_LOCK_TIMEOUT = 5.0 # seconds
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionMessage:
|
||||
"""A single message in a session."""
|
||||
role: str # "user", "assistant", "system"
|
||||
content: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content,
|
||||
"timestamp": self.timestamp,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SessionMessage":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
role=data.get("role", "user"),
|
||||
content=data.get("content", ""),
|
||||
timestamp=data.get("timestamp", time.time()),
|
||||
metadata=data.get("metadata", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionData:
|
||||
"""Complete session data structure."""
|
||||
session_id: str
|
||||
messages: List[SessionMessage] = field(default_factory=list)
|
||||
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
agent_name: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"messages": [m.to_dict() for m in self.messages],
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"agent_name": self.agent_name,
|
||||
"user_id": self.user_id,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SessionData":
|
||||
"""Create from dictionary."""
|
||||
messages = [
|
||||
SessionMessage.from_dict(m)
|
||||
for m in data.get("messages", [])
|
||||
]
|
||||
return cls(
|
||||
session_id=data.get("session_id", ""),
|
||||
messages=messages,
|
||||
created_at=data.get("created_at", datetime.now(timezone.utc).isoformat()),
|
||||
updated_at=data.get("updated_at", datetime.now(timezone.utc).isoformat()),
|
||||
agent_name=data.get("agent_name"),
|
||||
user_id=data.get("user_id"),
|
||||
metadata=data.get("metadata", {}),
|
||||
)
|
||||
|
||||
def get_chat_history(self, max_messages: Optional[int] = None) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get chat history in LLM-compatible format.
|
||||
|
||||
Returns list of {"role": "user/assistant", "content": "..."} dicts.
|
||||
"""
|
||||
messages = self.messages
|
||||
if max_messages and len(messages) > max_messages:
|
||||
messages = messages[-max_messages:]
|
||||
return [{"role": m.role, "content": m.content} for m in messages]
|
||||
|
||||
|
||||
class FileLock:
|
||||
"""
|
||||
Cross-platform file locking.
|
||||
|
||||
Uses fcntl on Unix and msvcrt on Windows.
|
||||
"""
|
||||
|
||||
def __init__(self, filepath: str, timeout: float = DEFAULT_LOCK_TIMEOUT):
|
||||
self.filepath = filepath
|
||||
self.timeout = timeout
|
||||
self._lock_file = None
|
||||
self._lock_path = filepath + ".lock"
|
||||
|
||||
def __enter__(self):
|
||||
self.acquire()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.release()
|
||||
return False
|
||||
|
||||
def acquire(self) -> bool:
|
||||
"""Acquire the file lock."""
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self._lock_path) or ".", exist_ok=True)
|
||||
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
self._lock_file = open(self._lock_path, "w")
|
||||
if sys.platform == "win32":
|
||||
# Windows locking
|
||||
import msvcrt
|
||||
msvcrt.locking(self._lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
else:
|
||||
# Unix locking
|
||||
fcntl.flock(self._lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return True
|
||||
except (IOError, OSError, BlockingIOError):
|
||||
if self._lock_file:
|
||||
self._lock_file.close()
|
||||
self._lock_file = None
|
||||
|
||||
if time.time() - start_time > self.timeout:
|
||||
logger.warning(f"Failed to acquire lock for {self.filepath} after {self.timeout}s")
|
||||
return False
|
||||
|
||||
time.sleep(0.05) # Wait 50ms before retry
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the file lock."""
|
||||
if self._lock_file:
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
msvcrt.locking(self._lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
fcntl.flock(self._lock_file.fileno(), fcntl.LOCK_UN)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
finally:
|
||||
self._lock_file.close()
|
||||
self._lock_file = None
|
||||
# Clean up lock file
|
||||
try:
|
||||
os.remove(self._lock_path)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
class DefaultSessionStore:
|
||||
"""
|
||||
JSON-based session persistence with file locking.
|
||||
|
||||
Features:
|
||||
- Zero configuration required
|
||||
- Automatic file locking for multi-process safety
|
||||
- Atomic writes to prevent corruption
|
||||
- Configurable message limits
|
||||
- Thread-safe operations
|
||||
|
||||
Usage:
|
||||
store = DefaultSessionStore()
|
||||
|
||||
# Add messages
|
||||
store.add_message("session-123", "user", "Hello")
|
||||
store.add_message("session-123", "assistant", "Hi there!")
|
||||
|
||||
# Get history
|
||||
history = store.get_chat_history("session-123")
|
||||
# [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there!"}]
|
||||
|
||||
# Restore in new process
|
||||
store2 = DefaultSessionStore()
|
||||
history = store2.get_chat_history("session-123") # Same history!
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session_dir: Optional[str] = None,
|
||||
max_messages: int = DEFAULT_MAX_MESSAGES,
|
||||
lock_timeout: float = DEFAULT_LOCK_TIMEOUT,
|
||||
):
|
||||
"""
|
||||
Initialize session store.
|
||||
|
||||
Args:
|
||||
session_dir: Directory for session files. Defaults to ~/.praison/sessions/
|
||||
max_messages: Maximum messages to keep per session.
|
||||
lock_timeout: Timeout for file lock acquisition.
|
||||
"""
|
||||
self.session_dir = session_dir or DEFAULT_SESSION_DIR
|
||||
self.max_messages = max_messages
|
||||
self.lock_timeout = lock_timeout
|
||||
self._lock = threading.RLock()
|
||||
self._cache: Dict[str, SessionData] = {}
|
||||
|
||||
# Ensure session directory exists
|
||||
os.makedirs(self.session_dir, exist_ok=True)
|
||||
|
||||
def _get_session_path(self, session_id: str) -> str:
|
||||
"""Get the file path for a session."""
|
||||
# Sanitize session_id for filesystem
|
||||
safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in session_id)
|
||||
return os.path.join(self.session_dir, f"{safe_id}.json")
|
||||
|
||||
def _load_session(self, session_id: str) -> SessionData:
|
||||
"""Load session from disk with file locking."""
|
||||
filepath = self._get_session_path(session_id)
|
||||
|
||||
# Check cache first
|
||||
with self._lock:
|
||||
if session_id in self._cache:
|
||||
return self._cache[session_id]
|
||||
|
||||
# Load from disk
|
||||
if not os.path.exists(filepath):
|
||||
session = SessionData(session_id=session_id)
|
||||
with self._lock:
|
||||
self._cache[session_id] = session
|
||||
return session
|
||||
|
||||
with FileLock(filepath, self.lock_timeout):
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
session = SessionData.from_dict(data)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.warning(f"Failed to load session {session_id}: {e}")
|
||||
session = SessionData(session_id=session_id)
|
||||
|
||||
with self._lock:
|
||||
self._cache[session_id] = session
|
||||
|
||||
return session
|
||||
|
||||
def _save_session(self, session: SessionData) -> bool:
|
||||
"""Save session to disk with atomic write."""
|
||||
filepath = self._get_session_path(session.session_id)
|
||||
session.updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Trim messages if over limit
|
||||
if len(session.messages) > self.max_messages:
|
||||
session.messages = session.messages[-self.max_messages:]
|
||||
|
||||
with FileLock(filepath, self.lock_timeout):
|
||||
try:
|
||||
# Atomic write: write to temp file, then rename
|
||||
dir_path = os.path.dirname(filepath)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=dir_path,
|
||||
delete=False,
|
||||
suffix=".tmp"
|
||||
) as f:
|
||||
json.dump(session.to_dict(), f, indent=2, ensure_ascii=False)
|
||||
temp_path = f.name
|
||||
|
||||
# Atomic rename
|
||||
os.replace(temp_path, filepath)
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
logger.error(f"Failed to save session {session.session_id}: {e}")
|
||||
# Clean up temp file if it exists
|
||||
try:
|
||||
if 'temp_path' in locals():
|
||||
os.remove(temp_path)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
session_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Add a message to a session.
|
||||
|
||||
Args:
|
||||
session_id: The session ID.
|
||||
role: Message role ("user", "assistant", "system").
|
||||
content: Message content.
|
||||
metadata: Optional metadata.
|
||||
|
||||
Returns:
|
||||
True if saved successfully.
|
||||
"""
|
||||
filepath = self._get_session_path(session_id)
|
||||
|
||||
message = SessionMessage(
|
||||
role=role,
|
||||
content=content,
|
||||
timestamp=time.time(),
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
# Use file lock for atomic read-modify-write
|
||||
with FileLock(filepath, self.lock_timeout):
|
||||
# Always reload from disk inside lock to avoid race conditions
|
||||
if os.path.exists(filepath):
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
session = SessionData.from_dict(data)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
session = SessionData(session_id=session_id)
|
||||
else:
|
||||
session = SessionData(session_id=session_id)
|
||||
|
||||
# Add message
|
||||
session.messages.append(message)
|
||||
session.updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Trim messages if over limit
|
||||
if len(session.messages) > self.max_messages:
|
||||
session.messages = session.messages[-self.max_messages:]
|
||||
|
||||
# Write atomically
|
||||
try:
|
||||
dir_path = os.path.dirname(filepath) or "."
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=dir_path,
|
||||
delete=False,
|
||||
suffix=".tmp"
|
||||
) as f:
|
||||
json.dump(session.to_dict(), f, indent=2, ensure_ascii=False)
|
||||
temp_path = f.name
|
||||
|
||||
os.replace(temp_path, filepath)
|
||||
|
||||
# Update cache
|
||||
with self._lock:
|
||||
self._cache[session_id] = session
|
||||
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
logger.error(f"Failed to save session {session_id}: {e}")
|
||||
try:
|
||||
if 'temp_path' in locals():
|
||||
os.remove(temp_path)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def add_user_message(
|
||||
self,
|
||||
session_id: str,
|
||||
content: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""Add a user message to a session."""
|
||||
return self.add_message(session_id, "user", content, metadata)
|
||||
|
||||
def add_assistant_message(
|
||||
self,
|
||||
session_id: str,
|
||||
content: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""Add an assistant message to a session."""
|
||||
return self.add_message(session_id, "assistant", content, metadata)
|
||||
|
||||
def get_chat_history(
|
||||
self,
|
||||
session_id: str,
|
||||
max_messages: Optional[int] = None,
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get chat history for a session in LLM-compatible format.
|
||||
|
||||
Args:
|
||||
session_id: The session ID.
|
||||
max_messages: Maximum messages to return (defaults to store limit).
|
||||
|
||||
Returns:
|
||||
List of {"role": "user/assistant", "content": "..."} dicts.
|
||||
"""
|
||||
session = self._load_session(session_id)
|
||||
limit = max_messages or self.max_messages
|
||||
return session.get_chat_history(limit)
|
||||
|
||||
def get_session(self, session_id: str) -> SessionData:
|
||||
"""Get full session data."""
|
||||
return self._load_session(session_id)
|
||||
|
||||
def set_agent_info(
|
||||
self,
|
||||
session_id: str,
|
||||
agent_name: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""Set agent info for a session."""
|
||||
session = self._load_session(session_id)
|
||||
|
||||
with self._lock:
|
||||
if agent_name:
|
||||
session.agent_name = agent_name
|
||||
if user_id:
|
||||
session.user_id = user_id
|
||||
self._cache[session_id] = session
|
||||
|
||||
return self._save_session(session)
|
||||
|
||||
def clear_session(self, session_id: str) -> bool:
|
||||
"""Clear all messages from a session."""
|
||||
session = self._load_session(session_id)
|
||||
|
||||
with self._lock:
|
||||
session.messages.clear()
|
||||
self._cache[session_id] = session
|
||||
|
||||
return self._save_session(session)
|
||||
|
||||
def delete_session(self, session_id: str) -> bool:
|
||||
"""Delete a session completely."""
|
||||
filepath = self._get_session_path(session_id)
|
||||
|
||||
with self._lock:
|
||||
if session_id in self._cache:
|
||||
del self._cache[session_id]
|
||||
|
||||
try:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
logger.error(f"Failed to delete session {session_id}: {e}")
|
||||
return False
|
||||
|
||||
def list_sessions(self, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""List all sessions with metadata."""
|
||||
sessions = []
|
||||
|
||||
try:
|
||||
for filename in os.listdir(self.session_dir):
|
||||
if filename.endswith(".json"):
|
||||
filepath = os.path.join(self.session_dir, filename)
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
sessions.append({
|
||||
"session_id": data.get("session_id", filename[:-5]),
|
||||
"agent_name": data.get("agent_name"),
|
||||
"created_at": data.get("created_at"),
|
||||
"updated_at": data.get("updated_at"),
|
||||
"message_count": len(data.get("messages", [])),
|
||||
})
|
||||
except (json.JSONDecodeError, IOError):
|
||||
continue
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
# Sort by updated_at descending
|
||||
sessions.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
|
||||
return sessions[:limit]
|
||||
|
||||
def session_exists(self, session_id: str) -> bool:
|
||||
"""Check if a session exists."""
|
||||
filepath = self._get_session_path(session_id)
|
||||
return os.path.exists(filepath)
|
||||
|
||||
def invalidate_cache(self, session_id: Optional[str] = None) -> None:
|
||||
"""Invalidate cache for a session or all sessions."""
|
||||
with self._lock:
|
||||
if session_id:
|
||||
self._cache.pop(session_id, None)
|
||||
else:
|
||||
self._cache.clear()
|
||||
|
||||
|
||||
# Global session store instance (lazy initialized)
|
||||
_default_store: Optional[DefaultSessionStore] = None
|
||||
_store_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_default_session_store() -> DefaultSessionStore:
|
||||
"""Get the global default session store instance."""
|
||||
global _default_store
|
||||
|
||||
if _default_store is None:
|
||||
with _store_lock:
|
||||
if _default_store is None:
|
||||
_default_store = DefaultSessionStore()
|
||||
|
||||
return _default_store
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.2"
|
||||
version = "0.6.2"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Session module tests
|
||||
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
TDD Tests for Session Store.
|
||||
|
||||
Tests for DefaultSessionStore with JSON-based persistence.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import pytest
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from praisonaiagents.session.store import (
|
||||
DefaultSessionStore,
|
||||
SessionMessage,
|
||||
SessionData,
|
||||
FileLock,
|
||||
DEFAULT_MAX_MESSAGES,
|
||||
)
|
||||
|
||||
|
||||
class TestSessionMessage:
|
||||
"""Tests for SessionMessage dataclass."""
|
||||
|
||||
def test_create_message(self):
|
||||
"""Test creating a session message."""
|
||||
msg = SessionMessage(role="user", content="Hello")
|
||||
assert msg.role == "user"
|
||||
assert msg.content == "Hello"
|
||||
assert msg.timestamp > 0
|
||||
assert msg.metadata == {}
|
||||
|
||||
def test_message_to_dict(self):
|
||||
"""Test converting message to dict."""
|
||||
msg = SessionMessage(role="assistant", content="Hi there!")
|
||||
d = msg.to_dict()
|
||||
assert d["role"] == "assistant"
|
||||
assert d["content"] == "Hi there!"
|
||||
assert "timestamp" in d
|
||||
assert "metadata" in d
|
||||
|
||||
def test_message_from_dict(self):
|
||||
"""Test creating message from dict."""
|
||||
d = {"role": "user", "content": "Test", "timestamp": 123.0, "metadata": {"key": "value"}}
|
||||
msg = SessionMessage.from_dict(d)
|
||||
assert msg.role == "user"
|
||||
assert msg.content == "Test"
|
||||
assert msg.timestamp == 123.0
|
||||
assert msg.metadata == {"key": "value"}
|
||||
|
||||
|
||||
class TestSessionData:
|
||||
"""Tests for SessionData dataclass."""
|
||||
|
||||
def test_create_session(self):
|
||||
"""Test creating session data."""
|
||||
session = SessionData(session_id="test-123")
|
||||
assert session.session_id == "test-123"
|
||||
assert session.messages == []
|
||||
assert session.created_at is not None
|
||||
assert session.updated_at is not None
|
||||
|
||||
def test_session_to_dict(self):
|
||||
"""Test converting session to dict."""
|
||||
session = SessionData(session_id="test-123", agent_name="Assistant")
|
||||
session.messages.append(SessionMessage(role="user", content="Hello"))
|
||||
d = session.to_dict()
|
||||
assert d["session_id"] == "test-123"
|
||||
assert d["agent_name"] == "Assistant"
|
||||
assert len(d["messages"]) == 1
|
||||
|
||||
def test_session_from_dict(self):
|
||||
"""Test creating session from dict."""
|
||||
d = {
|
||||
"session_id": "test-456",
|
||||
"messages": [{"role": "user", "content": "Hi"}],
|
||||
"agent_name": "Bot",
|
||||
}
|
||||
session = SessionData.from_dict(d)
|
||||
assert session.session_id == "test-456"
|
||||
assert session.agent_name == "Bot"
|
||||
assert len(session.messages) == 1
|
||||
|
||||
def test_get_chat_history(self):
|
||||
"""Test getting chat history in LLM format."""
|
||||
session = SessionData(session_id="test")
|
||||
session.messages.append(SessionMessage(role="user", content="Hello"))
|
||||
session.messages.append(SessionMessage(role="assistant", content="Hi!"))
|
||||
|
||||
history = session.get_chat_history()
|
||||
assert len(history) == 2
|
||||
assert history[0] == {"role": "user", "content": "Hello"}
|
||||
assert history[1] == {"role": "assistant", "content": "Hi!"}
|
||||
|
||||
def test_get_chat_history_with_limit(self):
|
||||
"""Test getting limited chat history."""
|
||||
session = SessionData(session_id="test")
|
||||
for i in range(10):
|
||||
session.messages.append(SessionMessage(role="user", content=f"Message {i}"))
|
||||
|
||||
history = session.get_chat_history(max_messages=3)
|
||||
assert len(history) == 3
|
||||
assert history[0]["content"] == "Message 7"
|
||||
assert history[2]["content"] == "Message 9"
|
||||
|
||||
|
||||
class TestFileLock:
|
||||
"""Tests for FileLock."""
|
||||
|
||||
def test_acquire_release(self):
|
||||
"""Test basic lock acquire and release."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
filepath = os.path.join(tmpdir, "test.json")
|
||||
lock = FileLock(filepath, timeout=1.0)
|
||||
|
||||
assert lock.acquire()
|
||||
lock.release()
|
||||
|
||||
def test_context_manager(self):
|
||||
"""Test lock as context manager."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
filepath = os.path.join(tmpdir, "test.json")
|
||||
|
||||
with FileLock(filepath, timeout=1.0):
|
||||
# Lock is held
|
||||
pass
|
||||
# Lock is released
|
||||
|
||||
def test_concurrent_locks(self):
|
||||
"""Test that concurrent locks are serialized."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
filepath = os.path.join(tmpdir, "test.json")
|
||||
results = []
|
||||
|
||||
def worker(worker_id):
|
||||
with FileLock(filepath, timeout=5.0):
|
||||
results.append(f"start-{worker_id}")
|
||||
time.sleep(0.1)
|
||||
results.append(f"end-{worker_id}")
|
||||
|
||||
threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Check that starts and ends are paired (no interleaving)
|
||||
for i in range(3):
|
||||
start_idx = results.index(f"start-{i}")
|
||||
end_idx = results.index(f"end-{i}")
|
||||
# No other start should be between this start and end
|
||||
for j in range(3):
|
||||
if i != j:
|
||||
other_start = results.index(f"start-{j}")
|
||||
if start_idx < other_start < end_idx:
|
||||
pytest.fail(f"Lock interleaving detected: {results}")
|
||||
|
||||
|
||||
class TestDefaultSessionStore:
|
||||
"""Tests for DefaultSessionStore."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_store(self):
|
||||
"""Create a temporary session store."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = DefaultSessionStore(session_dir=tmpdir)
|
||||
yield store
|
||||
|
||||
def test_add_message(self, temp_store):
|
||||
"""Test adding a message."""
|
||||
result = temp_store.add_message("session-1", "user", "Hello")
|
||||
assert result is True
|
||||
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert len(history) == 1
|
||||
assert history[0]["role"] == "user"
|
||||
assert history[0]["content"] == "Hello"
|
||||
|
||||
def test_add_user_message(self, temp_store):
|
||||
"""Test adding a user message."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert history[0]["role"] == "user"
|
||||
|
||||
def test_add_assistant_message(self, temp_store):
|
||||
"""Test adding an assistant message."""
|
||||
temp_store.add_assistant_message("session-1", "Hi there!")
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert history[0]["role"] == "assistant"
|
||||
|
||||
def test_persistence_across_instances(self, temp_store):
|
||||
"""Test that sessions persist across store instances."""
|
||||
session_dir = temp_store.session_dir
|
||||
|
||||
# Add messages with first store
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
temp_store.add_assistant_message("session-1", "Hi!")
|
||||
|
||||
# Create new store instance
|
||||
store2 = DefaultSessionStore(session_dir=session_dir)
|
||||
|
||||
# Verify messages are restored
|
||||
history = store2.get_chat_history("session-1")
|
||||
assert len(history) == 2
|
||||
assert history[0]["content"] == "Hello"
|
||||
assert history[1]["content"] == "Hi!"
|
||||
|
||||
def test_session_file_created(self, temp_store):
|
||||
"""Test that session file is created on disk."""
|
||||
temp_store.add_user_message("test-session", "Hello")
|
||||
|
||||
filepath = os.path.join(temp_store.session_dir, "test-session.json")
|
||||
assert os.path.exists(filepath)
|
||||
|
||||
with open(filepath, "r") as f:
|
||||
data = json.load(f)
|
||||
assert data["session_id"] == "test-session"
|
||||
assert len(data["messages"]) == 1
|
||||
|
||||
def test_max_messages_limit(self):
|
||||
"""Test that messages are trimmed to max limit."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = DefaultSessionStore(session_dir=tmpdir, max_messages=5)
|
||||
|
||||
for i in range(10):
|
||||
store.add_user_message("session-1", f"Message {i}")
|
||||
|
||||
history = store.get_chat_history("session-1")
|
||||
assert len(history) == 5
|
||||
assert history[0]["content"] == "Message 5"
|
||||
assert history[4]["content"] == "Message 9"
|
||||
|
||||
def test_clear_session(self, temp_store):
|
||||
"""Test clearing a session."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
temp_store.add_assistant_message("session-1", "Hi!")
|
||||
|
||||
temp_store.clear_session("session-1")
|
||||
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert len(history) == 0
|
||||
|
||||
def test_delete_session(self, temp_store):
|
||||
"""Test deleting a session."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
filepath = os.path.join(temp_store.session_dir, "session-1.json")
|
||||
assert os.path.exists(filepath)
|
||||
|
||||
temp_store.delete_session("session-1")
|
||||
assert not os.path.exists(filepath)
|
||||
|
||||
def test_list_sessions(self, temp_store):
|
||||
"""Test listing sessions."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
temp_store.add_user_message("session-2", "Hi")
|
||||
temp_store.add_user_message("session-3", "Hey")
|
||||
|
||||
sessions = temp_store.list_sessions()
|
||||
assert len(sessions) == 3
|
||||
session_ids = [s["session_id"] for s in sessions]
|
||||
assert "session-1" in session_ids
|
||||
assert "session-2" in session_ids
|
||||
assert "session-3" in session_ids
|
||||
|
||||
def test_session_exists(self, temp_store):
|
||||
"""Test checking if session exists."""
|
||||
assert not temp_store.session_exists("session-1")
|
||||
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
assert temp_store.session_exists("session-1")
|
||||
|
||||
def test_set_agent_info(self, temp_store):
|
||||
"""Test setting agent info."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
temp_store.set_agent_info("session-1", agent_name="TestBot", user_id="user-123")
|
||||
|
||||
session = temp_store.get_session("session-1")
|
||||
assert session.agent_name == "TestBot"
|
||||
assert session.user_id == "user-123"
|
||||
|
||||
def test_concurrent_writes(self, temp_store):
|
||||
"""Test concurrent writes to same session."""
|
||||
def writer(msg_id):
|
||||
temp_store.add_user_message("session-1", f"Message {msg_id}")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
futures = [executor.submit(writer, i) for i in range(10)]
|
||||
for f in futures:
|
||||
f.result()
|
||||
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert len(history) == 10
|
||||
|
||||
def test_sanitize_session_id(self, temp_store):
|
||||
"""Test that session IDs are sanitized for filesystem."""
|
||||
temp_store.add_user_message("session/with/slashes", "Hello")
|
||||
temp_store.add_user_message("session:with:colons", "Hi")
|
||||
|
||||
# Should not raise errors
|
||||
history1 = temp_store.get_chat_history("session/with/slashes")
|
||||
history2 = temp_store.get_chat_history("session:with:colons")
|
||||
assert len(history1) == 1
|
||||
assert len(history2) == 1
|
||||
|
||||
def test_empty_session_returns_empty_history(self, temp_store):
|
||||
"""Test that non-existent session returns empty history."""
|
||||
history = temp_store.get_chat_history("nonexistent")
|
||||
assert history == []
|
||||
|
||||
def test_invalidate_cache(self, temp_store):
|
||||
"""Test cache invalidation."""
|
||||
temp_store.add_user_message("session-1", "Hello")
|
||||
|
||||
# Modify file directly
|
||||
filepath = os.path.join(temp_store.session_dir, "session-1.json")
|
||||
with open(filepath, "r") as f:
|
||||
data = json.load(f)
|
||||
data["messages"].append({"role": "user", "content": "Direct edit", "timestamp": time.time(), "metadata": {}})
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
# Without invalidation, cache returns old data
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert len(history) == 1 # Cached
|
||||
|
||||
# After invalidation, new data is loaded
|
||||
temp_store.invalidate_cache("session-1")
|
||||
history = temp_store.get_chat_history("session-1")
|
||||
assert len(history) == 2
|
||||
|
||||
|
||||
class TestAgentSessionIntegration:
|
||||
"""Tests for Agent integration with session store."""
|
||||
|
||||
def test_agent_session_persistence(self):
|
||||
"""Test that Agent persists session with session_id."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Patch the default session directory
|
||||
import praisonaiagents.session.store as store_module
|
||||
original_dir = store_module.DEFAULT_SESSION_DIR
|
||||
store_module.DEFAULT_SESSION_DIR = tmpdir
|
||||
|
||||
# Reset global store
|
||||
store_module._default_store = None
|
||||
|
||||
try:
|
||||
# Create agent with session_id
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="You are a test agent.",
|
||||
session_id="test-session-123",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Verify session store is initialized after first chat
|
||||
# (We can't actually call chat without API key, so we test init)
|
||||
agent._init_session_store()
|
||||
|
||||
assert agent._session_store is not None
|
||||
assert agent._session_store_initialized is True
|
||||
finally:
|
||||
store_module.DEFAULT_SESSION_DIR = original_dir
|
||||
store_module._default_store = None
|
||||
|
||||
def test_agent_no_session_id_no_persistence(self):
|
||||
"""Test that Agent without session_id doesn't use session store."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="You are a test agent.",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
agent._init_session_store()
|
||||
|
||||
assert agent._session_store is None
|
||||
assert agent._session_store_initialized is True
|
||||
|
||||
def test_agent_with_db_no_session_store(self):
|
||||
"""Test that Agent with DB adapter doesn't use session store."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Mock DB adapter
|
||||
class MockDbAdapter:
|
||||
def on_agent_start(self, **kwargs):
|
||||
return []
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="You are a test agent.",
|
||||
session_id="test-session",
|
||||
db=MockDbAdapter(),
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
agent._init_session_store()
|
||||
|
||||
# Session store should not be used when DB is provided
|
||||
assert agent._session_store is None
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.2"
|
||||
version = "0.6.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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.8.9.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.9.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# __main__.py
|
||||
"""
|
||||
PraisonAI CLI Entry Point.
|
||||
|
||||
from .cli import PraisonAI
|
||||
Supports both new Typer-based CLI and legacy argparse CLI.
|
||||
"""
|
||||
|
||||
def main():
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
"""Main entry point with legacy support."""
|
||||
from .cli.legacy import main_with_legacy_support
|
||||
main_with_legacy_support()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
Safe Edit Pipeline for ACP.
|
||||
|
||||
Implements a propose → diff → approve → apply workflow for file edits.
|
||||
Ensures no silent file writes - all changes require explicit approval.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditStatus(str, Enum):
|
||||
"""Status of an edit proposal."""
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
APPLIED = "applied"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditProposal:
|
||||
"""
|
||||
A proposed file edit with diff preview.
|
||||
|
||||
Represents a change that has been proposed but not yet applied.
|
||||
"""
|
||||
proposal_id: str
|
||||
file_path: Path
|
||||
original_content: str
|
||||
proposed_content: str
|
||||
description: str
|
||||
diff_lines: List[str] = field(default_factory=list)
|
||||
status: EditStatus = EditStatus.PENDING
|
||||
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
approved_at: Optional[str] = None
|
||||
applied_at: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Generate diff on creation."""
|
||||
if not self.diff_lines:
|
||||
self.diff_lines = self._generate_diff()
|
||||
|
||||
def _generate_diff(self) -> List[str]:
|
||||
"""Generate unified diff between original and proposed content."""
|
||||
original_lines = self.original_content.splitlines(keepends=True)
|
||||
proposed_lines = self.proposed_content.splitlines(keepends=True)
|
||||
|
||||
diff = difflib.unified_diff(
|
||||
original_lines,
|
||||
proposed_lines,
|
||||
fromfile=f"a/{self.file_path.name}",
|
||||
tofile=f"b/{self.file_path.name}",
|
||||
lineterm=""
|
||||
)
|
||||
return list(diff)
|
||||
|
||||
@property
|
||||
def diff_text(self) -> str:
|
||||
"""Get diff as a single string."""
|
||||
return "\n".join(self.diff_lines)
|
||||
|
||||
@property
|
||||
def is_new_file(self) -> bool:
|
||||
"""Check if this is a new file creation."""
|
||||
return not self.original_content
|
||||
|
||||
@property
|
||||
def is_deletion(self) -> bool:
|
||||
"""Check if this is a file deletion."""
|
||||
return not self.proposed_content and bool(self.original_content)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"proposal_id": self.proposal_id,
|
||||
"file_path": str(self.file_path),
|
||||
"description": self.description,
|
||||
"status": self.status.value,
|
||||
"is_new_file": self.is_new_file,
|
||||
"is_deletion": self.is_deletion,
|
||||
"diff": self.diff_text,
|
||||
"created_at": self.created_at,
|
||||
"approved_at": self.approved_at,
|
||||
"applied_at": self.applied_at,
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
|
||||
class SafeEditPipeline:
|
||||
"""
|
||||
Safe file editing with diff preview and approval workflow.
|
||||
|
||||
Workflow:
|
||||
1. propose_edit() - Create an edit proposal with diff
|
||||
2. show_diff() - Display the diff to user
|
||||
3. approve() / reject() - User decision
|
||||
4. apply_edit() - Apply approved edit
|
||||
|
||||
ACP remains read-only by default. Writes only happen after explicit approval.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: Optional[Path] = None,
|
||||
auto_approve: bool = False,
|
||||
approval_callback: Optional[callable] = None,
|
||||
):
|
||||
"""
|
||||
Initialize safe edit pipeline.
|
||||
|
||||
Args:
|
||||
workspace: Root workspace directory for path validation
|
||||
auto_approve: If True, auto-approve all edits (dangerous, for testing only)
|
||||
approval_callback: Optional callback for approval (async or sync)
|
||||
"""
|
||||
self.workspace = Path(workspace) if workspace else Path.cwd()
|
||||
self.auto_approve = auto_approve
|
||||
self.approval_callback = approval_callback
|
||||
self._proposals: Dict[str, EditProposal] = {}
|
||||
|
||||
def _validate_path(self, file_path: Path) -> Path:
|
||||
"""
|
||||
Validate and resolve file path.
|
||||
|
||||
Ensures path is within workspace and resolves to absolute path.
|
||||
"""
|
||||
if not file_path.is_absolute():
|
||||
file_path = self.workspace / file_path
|
||||
|
||||
resolved = file_path.resolve()
|
||||
|
||||
# Security check: ensure path is within workspace
|
||||
try:
|
||||
resolved.relative_to(self.workspace.resolve())
|
||||
except ValueError:
|
||||
raise ValueError(f"Path {file_path} is outside workspace {self.workspace}")
|
||||
|
||||
return resolved
|
||||
|
||||
def propose_edit(
|
||||
self,
|
||||
file_path: Path,
|
||||
new_content: str,
|
||||
description: str,
|
||||
) -> EditProposal:
|
||||
"""
|
||||
Create an edit proposal with diff preview.
|
||||
|
||||
Args:
|
||||
file_path: Path to file to edit
|
||||
new_content: Proposed new content
|
||||
description: Human-readable description of the change
|
||||
|
||||
Returns:
|
||||
EditProposal with diff
|
||||
"""
|
||||
resolved_path = self._validate_path(Path(file_path))
|
||||
|
||||
# Read original content if file exists
|
||||
original_content = ""
|
||||
if resolved_path.exists():
|
||||
try:
|
||||
original_content = resolved_path.read_text()
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read {resolved_path}: {e}")
|
||||
|
||||
# Create proposal
|
||||
proposal = EditProposal(
|
||||
proposal_id=str(uuid.uuid4())[:8],
|
||||
file_path=resolved_path,
|
||||
original_content=original_content,
|
||||
proposed_content=new_content,
|
||||
description=description,
|
||||
)
|
||||
|
||||
self._proposals[proposal.proposal_id] = proposal
|
||||
logger.info(f"Created edit proposal {proposal.proposal_id} for {file_path}")
|
||||
|
||||
return proposal
|
||||
|
||||
def propose_create(
|
||||
self,
|
||||
file_path: Path,
|
||||
content: str,
|
||||
description: str,
|
||||
) -> EditProposal:
|
||||
"""
|
||||
Create a proposal for a new file.
|
||||
|
||||
Args:
|
||||
file_path: Path for new file
|
||||
content: File content
|
||||
description: Description of the new file
|
||||
|
||||
Returns:
|
||||
EditProposal for file creation
|
||||
"""
|
||||
return self.propose_edit(file_path, content, description)
|
||||
|
||||
def propose_delete(
|
||||
self,
|
||||
file_path: Path,
|
||||
description: str,
|
||||
) -> EditProposal:
|
||||
"""
|
||||
Create a proposal to delete a file.
|
||||
|
||||
Args:
|
||||
file_path: Path to file to delete
|
||||
description: Reason for deletion
|
||||
|
||||
Returns:
|
||||
EditProposal for file deletion
|
||||
"""
|
||||
return self.propose_edit(file_path, "", description)
|
||||
|
||||
def get_proposal(self, proposal_id: str) -> Optional[EditProposal]:
|
||||
"""Get a proposal by ID."""
|
||||
return self._proposals.get(proposal_id)
|
||||
|
||||
def list_proposals(self, status: Optional[EditStatus] = None) -> List[EditProposal]:
|
||||
"""
|
||||
List all proposals, optionally filtered by status.
|
||||
|
||||
Args:
|
||||
status: Optional status filter
|
||||
|
||||
Returns:
|
||||
List of proposals
|
||||
"""
|
||||
proposals = list(self._proposals.values())
|
||||
if status:
|
||||
proposals = [p for p in proposals if p.status == status]
|
||||
return proposals
|
||||
|
||||
def approve(self, proposal_id: str) -> bool:
|
||||
"""
|
||||
Approve an edit proposal.
|
||||
|
||||
Args:
|
||||
proposal_id: ID of proposal to approve
|
||||
|
||||
Returns:
|
||||
True if approved, False if not found or already processed
|
||||
"""
|
||||
proposal = self._proposals.get(proposal_id)
|
||||
if not proposal:
|
||||
logger.warning(f"Proposal {proposal_id} not found")
|
||||
return False
|
||||
|
||||
if proposal.status != EditStatus.PENDING:
|
||||
logger.warning(f"Proposal {proposal_id} is not pending (status: {proposal.status})")
|
||||
return False
|
||||
|
||||
proposal.status = EditStatus.APPROVED
|
||||
proposal.approved_at = datetime.now().isoformat()
|
||||
logger.info(f"Approved proposal {proposal_id}")
|
||||
return True
|
||||
|
||||
def reject(self, proposal_id: str, reason: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Reject an edit proposal.
|
||||
|
||||
Args:
|
||||
proposal_id: ID of proposal to reject
|
||||
reason: Optional rejection reason
|
||||
|
||||
Returns:
|
||||
True if rejected, False if not found or already processed
|
||||
"""
|
||||
proposal = self._proposals.get(proposal_id)
|
||||
if not proposal:
|
||||
logger.warning(f"Proposal {proposal_id} not found")
|
||||
return False
|
||||
|
||||
if proposal.status != EditStatus.PENDING:
|
||||
logger.warning(f"Proposal {proposal_id} is not pending (status: {proposal.status})")
|
||||
return False
|
||||
|
||||
proposal.status = EditStatus.REJECTED
|
||||
proposal.error = reason
|
||||
logger.info(f"Rejected proposal {proposal_id}: {reason}")
|
||||
return True
|
||||
|
||||
def apply_edit(self, proposal_id: str) -> bool:
|
||||
"""
|
||||
Apply an approved edit.
|
||||
|
||||
Args:
|
||||
proposal_id: ID of approved proposal to apply
|
||||
|
||||
Returns:
|
||||
True if applied successfully, False otherwise
|
||||
"""
|
||||
proposal = self._proposals.get(proposal_id)
|
||||
if not proposal:
|
||||
logger.error(f"Proposal {proposal_id} not found")
|
||||
return False
|
||||
|
||||
if proposal.status != EditStatus.APPROVED:
|
||||
logger.error(f"Proposal {proposal_id} is not approved (status: {proposal.status})")
|
||||
return False
|
||||
|
||||
try:
|
||||
if proposal.is_deletion:
|
||||
# Delete file
|
||||
if proposal.file_path.exists():
|
||||
proposal.file_path.unlink()
|
||||
logger.info(f"Deleted {proposal.file_path}")
|
||||
else:
|
||||
# Create parent directories if needed
|
||||
proposal.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write new content
|
||||
proposal.file_path.write_text(proposal.proposed_content)
|
||||
logger.info(f"Applied edit to {proposal.file_path}")
|
||||
|
||||
proposal.status = EditStatus.APPLIED
|
||||
proposal.applied_at = datetime.now().isoformat()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply proposal {proposal_id}: {e}")
|
||||
proposal.status = EditStatus.FAILED
|
||||
proposal.error = str(e)
|
||||
return False
|
||||
|
||||
def propose_and_apply(
|
||||
self,
|
||||
file_path: Path,
|
||||
new_content: str,
|
||||
description: str,
|
||||
) -> EditProposal:
|
||||
"""
|
||||
Convenience method: propose, auto-approve if enabled, and apply.
|
||||
|
||||
Only applies if auto_approve is True or approval_callback returns True.
|
||||
|
||||
Args:
|
||||
file_path: Path to file
|
||||
new_content: New content
|
||||
description: Description
|
||||
|
||||
Returns:
|
||||
EditProposal (may be pending, approved, or applied)
|
||||
"""
|
||||
proposal = self.propose_edit(file_path, new_content, description)
|
||||
|
||||
if self.auto_approve:
|
||||
self.approve(proposal.proposal_id)
|
||||
self.apply_edit(proposal.proposal_id)
|
||||
elif self.approval_callback:
|
||||
try:
|
||||
if self.approval_callback(proposal):
|
||||
self.approve(proposal.proposal_id)
|
||||
self.apply_edit(proposal.proposal_id)
|
||||
else:
|
||||
self.reject(proposal.proposal_id, "Rejected by callback")
|
||||
except Exception as e:
|
||||
logger.error(f"Approval callback failed: {e}")
|
||||
proposal.error = str(e)
|
||||
|
||||
return proposal
|
||||
|
||||
def format_diff_for_display(self, proposal: EditProposal) -> str:
|
||||
"""
|
||||
Format diff for terminal display with colors.
|
||||
|
||||
Args:
|
||||
proposal: Edit proposal
|
||||
|
||||
Returns:
|
||||
Formatted diff string with ANSI colors
|
||||
"""
|
||||
lines = []
|
||||
lines.append(f"📝 Edit Proposal: {proposal.proposal_id}")
|
||||
lines.append(f" File: {proposal.file_path}")
|
||||
lines.append(f" Description: {proposal.description}")
|
||||
lines.append(f" Status: {proposal.status.value}")
|
||||
lines.append("")
|
||||
|
||||
if proposal.is_new_file:
|
||||
lines.append(" [NEW FILE]")
|
||||
elif proposal.is_deletion:
|
||||
lines.append(" [DELETE FILE]")
|
||||
|
||||
lines.append(" --- Diff ---")
|
||||
for line in proposal.diff_lines:
|
||||
if line.startswith("+") and not line.startswith("+++"):
|
||||
lines.append(f" \033[32m{line}\033[0m") # Green for additions
|
||||
elif line.startswith("-") and not line.startswith("---"):
|
||||
lines.append(f" \033[31m{line}\033[0m") # Red for deletions
|
||||
elif line.startswith("@@"):
|
||||
lines.append(f" \033[36m{line}\033[0m") # Cyan for line numbers
|
||||
else:
|
||||
lines.append(f" {line}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def clear_proposals(self, status: Optional[EditStatus] = None) -> int:
|
||||
"""
|
||||
Clear proposals, optionally filtered by status.
|
||||
|
||||
Args:
|
||||
status: Optional status filter (None = clear all)
|
||||
|
||||
Returns:
|
||||
Number of proposals cleared
|
||||
"""
|
||||
if status is None:
|
||||
count = len(self._proposals)
|
||||
self._proposals.clear()
|
||||
else:
|
||||
to_remove = [pid for pid, p in self._proposals.items() if p.status == status]
|
||||
for pid in to_remove:
|
||||
del self._proposals[pid]
|
||||
count = len(to_remove)
|
||||
|
||||
logger.info(f"Cleared {count} proposals")
|
||||
return count
|
||||
|
||||
|
||||
# Global pipeline instance for ACP
|
||||
_safe_edit_pipeline: Optional[SafeEditPipeline] = None
|
||||
|
||||
|
||||
def get_safe_edit_pipeline(workspace: Optional[Path] = None) -> SafeEditPipeline:
|
||||
"""Get or create the global safe edit pipeline."""
|
||||
global _safe_edit_pipeline
|
||||
if _safe_edit_pipeline is None:
|
||||
_safe_edit_pipeline = SafeEditPipeline(workspace=workspace)
|
||||
return _safe_edit_pipeline
|
||||
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
PraisonAI CLI Typer Application.
|
||||
|
||||
Main Typer app that registers all command groups and handles global options.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from .output.console import OutputController, OutputMode, set_output_controller
|
||||
from .state.identifiers import create_context
|
||||
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
"""Output format options."""
|
||||
text = "text"
|
||||
json = "json"
|
||||
stream_json = "stream-json"
|
||||
|
||||
|
||||
# Create main Typer app
|
||||
app = typer.Typer(
|
||||
name="praisonai",
|
||||
help="PraisonAI - AI Agents Framework CLI",
|
||||
add_completion=False, # We handle completion manually
|
||||
no_args_is_help=False, # Allow running without args for legacy compatibility
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
|
||||
# Global state for options
|
||||
class GlobalState:
|
||||
"""Global state for CLI options."""
|
||||
output_format: OutputFormat = OutputFormat.text
|
||||
no_color: bool = False
|
||||
quiet: bool = False
|
||||
verbose: bool = False
|
||||
screen_reader: bool = False
|
||||
output_controller: Optional[OutputController] = None
|
||||
|
||||
|
||||
state = GlobalState()
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
"""Show version and exit."""
|
||||
if value:
|
||||
from praisonai.version import __version__
|
||||
typer.echo(f"PraisonAI version {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main_callback(
|
||||
ctx: typer.Context,
|
||||
version: bool = typer.Option(
|
||||
False,
|
||||
"--version",
|
||||
"-V",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
help="Show version and exit",
|
||||
),
|
||||
output_format: OutputFormat = typer.Option(
|
||||
OutputFormat.text,
|
||||
"--output-format",
|
||||
"-o",
|
||||
help="Output format",
|
||||
envvar="PRAISONAI_OUTPUT_FORMAT",
|
||||
),
|
||||
json_output: bool = typer.Option(
|
||||
False,
|
||||
"--json",
|
||||
help="Output in JSON format (alias for --output-format json)",
|
||||
),
|
||||
no_color: bool = typer.Option(
|
||||
False,
|
||||
"--no-color",
|
||||
help="Disable colored output",
|
||||
envvar="NO_COLOR",
|
||||
),
|
||||
quiet: bool = typer.Option(
|
||||
False,
|
||||
"--quiet",
|
||||
"-q",
|
||||
help="Minimal output",
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose",
|
||||
"-v",
|
||||
help="Verbose output with debug details",
|
||||
),
|
||||
screen_reader: bool = typer.Option(
|
||||
False,
|
||||
"--screen-reader",
|
||||
help="Screen reader friendly output (no spinners/panels)",
|
||||
),
|
||||
):
|
||||
"""
|
||||
PraisonAI - AI Agents Framework CLI.
|
||||
|
||||
Run agents, manage configuration, and more.
|
||||
"""
|
||||
# Store global options
|
||||
state.output_format = output_format
|
||||
state.no_color = no_color
|
||||
state.quiet = quiet
|
||||
state.verbose = verbose
|
||||
state.screen_reader = screen_reader
|
||||
|
||||
# Handle --json alias
|
||||
if json_output:
|
||||
state.output_format = OutputFormat.json
|
||||
|
||||
# Determine output mode
|
||||
if state.quiet:
|
||||
mode = OutputMode.QUIET
|
||||
elif state.verbose:
|
||||
mode = OutputMode.VERBOSE
|
||||
elif state.screen_reader:
|
||||
mode = OutputMode.SCREEN_READER
|
||||
elif state.output_format == OutputFormat.json:
|
||||
mode = OutputMode.JSON
|
||||
elif state.output_format == OutputFormat.stream_json:
|
||||
mode = OutputMode.STREAM_JSON
|
||||
else:
|
||||
mode = OutputMode.TEXT
|
||||
|
||||
# Create run context
|
||||
context = create_context()
|
||||
|
||||
# Create and set output controller
|
||||
state.output_controller = OutputController(
|
||||
mode=mode,
|
||||
no_color=state.no_color,
|
||||
run_id=context.run_id,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
set_output_controller(state.output_controller)
|
||||
|
||||
# If no command provided, show help or handle legacy behavior
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Check if there are remaining args (legacy behavior)
|
||||
# This is handled by the legacy adapter in main.py
|
||||
pass
|
||||
|
||||
|
||||
def get_output_controller() -> OutputController:
|
||||
"""Get the current output controller."""
|
||||
if state.output_controller is None:
|
||||
state.output_controller = OutputController()
|
||||
return state.output_controller
|
||||
|
||||
|
||||
# Import and register command groups
|
||||
def register_commands():
|
||||
"""Register all command groups."""
|
||||
# Import command modules - Core commands
|
||||
from .commands.config import app as config_app
|
||||
from .commands.traces import app as traces_app
|
||||
from .commands.environment import app as env_app
|
||||
from .commands.session import app as session_app
|
||||
from .commands.completion import app as completion_app
|
||||
from .commands.version import app as version_app
|
||||
from .commands.debug import app as debug_app
|
||||
from .commands.lsp import app as lsp_app
|
||||
from .commands.diag import app as diag_app
|
||||
from .commands.doctor import app as doctor_app
|
||||
from .commands.acp import app as acp_app
|
||||
from .commands.mcp import app as mcp_app
|
||||
from .commands.serve import app as serve_app
|
||||
from .commands.schedule import app as schedule_app
|
||||
from .commands.run import app as run_app
|
||||
from .commands.profile import app as profile_app
|
||||
from .commands.benchmark import app as benchmark_app
|
||||
|
||||
# Import new command modules - Previously legacy-only commands
|
||||
from .commands.chat import app as chat_app
|
||||
from .commands.code import app as code_app
|
||||
from .commands.call import app as call_app
|
||||
from .commands.realtime import app as realtime_app
|
||||
from .commands.train import app as train_app
|
||||
from .commands.ui import app as ui_app
|
||||
from .commands.context import app as context_app
|
||||
from .commands.research import app as research_app
|
||||
from .commands.memory import app as memory_app
|
||||
from .commands.workflow import app as workflow_app
|
||||
from .commands.tools import app as tools_app
|
||||
from .commands.knowledge import app as knowledge_app
|
||||
from .commands.deploy import app as deploy_app
|
||||
from .commands.agents import app as agents_app
|
||||
from .commands.skills import app as skills_app
|
||||
from .commands.eval import app as eval_app
|
||||
from .commands.templates import app as templates_app
|
||||
from .commands.recipe import app as recipe_app
|
||||
from .commands.todo import app as todo_app
|
||||
from .commands.docs import app as docs_app
|
||||
from .commands.commit import app as commit_app
|
||||
from .commands.hooks import app as hooks_app
|
||||
from .commands.rules import app as rules_app
|
||||
from .commands.registry import app as registry_app
|
||||
from .commands.package import app as package_app
|
||||
from .commands.endpoints import app as endpoints_app
|
||||
|
||||
# Import TUI and queue commands
|
||||
from .features.tui.debug import create_debug_app as create_tui_debug_app
|
||||
from .features.tui.cli import create_queue_app
|
||||
|
||||
# Register sub-apps - Core commands
|
||||
app.add_typer(config_app, name="config", help="Configuration management")
|
||||
app.add_typer(traces_app, name="traces", help="Trace collection management")
|
||||
app.add_typer(env_app, name="env", help="Environment and diagnostics")
|
||||
app.add_typer(session_app, name="session", help="Session management")
|
||||
app.add_typer(completion_app, name="completion", help="Shell completion scripts")
|
||||
app.add_typer(version_app, name="version", help="Version information")
|
||||
app.add_typer(debug_app, name="debug", help="Debug and test interactive flows")
|
||||
app.add_typer(lsp_app, name="lsp", help="LSP service lifecycle")
|
||||
app.add_typer(diag_app, name="diag", help="Diagnostics export")
|
||||
app.add_typer(doctor_app, name="doctor", help="Health checks and diagnostics")
|
||||
app.add_typer(acp_app, name="acp", help="Agent Client Protocol server")
|
||||
app.add_typer(mcp_app, name="mcp", help="MCP server management")
|
||||
app.add_typer(serve_app, name="serve", help="API server management")
|
||||
app.add_typer(schedule_app, name="schedule", help="Scheduler management")
|
||||
app.add_typer(run_app, name="run", help="Run agents")
|
||||
app.add_typer(profile_app, name="profile", help="Performance profiling and diagnostics")
|
||||
app.add_typer(benchmark_app, name="benchmark", help="Comprehensive performance benchmarking")
|
||||
|
||||
# Register sub-apps - Previously legacy-only commands (now visible in --help)
|
||||
app.add_typer(chat_app, name="chat", help="Interactive chat mode")
|
||||
app.add_typer(code_app, name="code", help="Code assistant mode")
|
||||
app.add_typer(call_app, name="call", help="Voice/call interaction mode")
|
||||
app.add_typer(realtime_app, name="realtime", help="Realtime interaction mode")
|
||||
app.add_typer(train_app, name="train", help="Model training and fine-tuning")
|
||||
app.add_typer(ui_app, name="ui", help="Web UI management")
|
||||
app.add_typer(context_app, name="context", help="Context management")
|
||||
app.add_typer(research_app, name="research", help="Research and analysis")
|
||||
app.add_typer(memory_app, name="memory", help="Memory management")
|
||||
app.add_typer(workflow_app, name="workflow", help="Workflow management")
|
||||
app.add_typer(tools_app, name="tools", help="Tool management")
|
||||
app.add_typer(knowledge_app, name="knowledge", help="Knowledge base management")
|
||||
app.add_typer(deploy_app, name="deploy", help="Deployment management")
|
||||
app.add_typer(agents_app, name="agents", help="Agent management")
|
||||
app.add_typer(skills_app, name="skills", help="Skill management")
|
||||
app.add_typer(eval_app, name="eval", help="Evaluation and testing")
|
||||
app.add_typer(templates_app, name="templates", help="Template management")
|
||||
app.add_typer(recipe_app, name="recipe", help="Recipe management")
|
||||
app.add_typer(todo_app, name="todo", help="Todo/task management")
|
||||
app.add_typer(docs_app, name="docs", help="Documentation management")
|
||||
app.add_typer(commit_app, name="commit", help="AI-assisted git commits")
|
||||
app.add_typer(hooks_app, name="hooks", help="Hook management")
|
||||
app.add_typer(rules_app, name="rules", help="Rules management")
|
||||
app.add_typer(registry_app, name="registry", help="Registry management")
|
||||
app.add_typer(package_app, name="package", help="Package management")
|
||||
app.add_typer(endpoints_app, name="endpoints", help="API endpoint management")
|
||||
|
||||
# Register TUI and queue commands
|
||||
tui_app = create_tui_debug_app()
|
||||
queue_app = create_queue_app()
|
||||
if tui_app:
|
||||
app.add_typer(tui_app, name="tui", help="Interactive TUI and simulation")
|
||||
if queue_app:
|
||||
app.add_typer(queue_app, name="queue", help="Queue management")
|
||||
|
||||
|
||||
# Register commands on import
|
||||
register_commands()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
PraisonAI CLI Commands Module.
|
||||
|
||||
This module contains all Typer command groups and subcommands.
|
||||
Each file represents a command group (e.g., config, traces, session).
|
||||
"""
|
||||
|
||||
# Lazy imports to avoid loading all commands at startup
|
||||
__all__ = [
|
||||
'run_app',
|
||||
'config_app',
|
||||
'traces_app',
|
||||
'env_app',
|
||||
'session_app',
|
||||
'schedule_app',
|
||||
'serve_app',
|
||||
'completion_app',
|
||||
'version_app',
|
||||
'debug_app',
|
||||
'lsp_app',
|
||||
'diag_app',
|
||||
'doctor_app',
|
||||
'acp_app',
|
||||
'mcp_app',
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load command apps."""
|
||||
if name == 'run_app':
|
||||
from .run import app as run_app
|
||||
return run_app
|
||||
elif name == 'config_app':
|
||||
from .config import app as config_app
|
||||
return config_app
|
||||
elif name == 'traces_app':
|
||||
from .traces import app as traces_app
|
||||
return traces_app
|
||||
elif name == 'env_app':
|
||||
from .environment import app as env_app
|
||||
return env_app
|
||||
elif name == 'session_app':
|
||||
from .session import app as session_app
|
||||
return session_app
|
||||
elif name == 'schedule_app':
|
||||
from .schedule import app as schedule_app
|
||||
return schedule_app
|
||||
elif name == 'serve_app':
|
||||
from .serve import app as serve_app
|
||||
return serve_app
|
||||
elif name == 'completion_app':
|
||||
from .completion import app as completion_app
|
||||
return completion_app
|
||||
elif name == 'version_app':
|
||||
from .version import app as version_app
|
||||
return version_app
|
||||
elif name == 'debug_app':
|
||||
from .debug import app as debug_app
|
||||
return debug_app
|
||||
elif name == 'lsp_app':
|
||||
from .lsp import app as lsp_app
|
||||
return lsp_app
|
||||
elif name == 'diag_app':
|
||||
from .diag import app as diag_app
|
||||
return diag_app
|
||||
elif name == 'doctor_app':
|
||||
from .doctor import app as doctor_app
|
||||
return doctor_app
|
||||
elif name == 'acp_app':
|
||||
from .acp import app as acp_app
|
||||
return acp_app
|
||||
elif name == 'mcp_app':
|
||||
from .mcp import app as mcp_app
|
||||
return mcp_app
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
ACP command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing ACP functionality from features/acp.py.
|
||||
Provides Agent Client Protocol server for IDE integration.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Agent Client Protocol server")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def acp_main(
|
||||
ctx: typer.Context,
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
agent: str = typer.Option("default", "--agent", "-a", help="Agent name or configuration file"),
|
||||
agents_file: Optional[str] = typer.Option(None, "--agents", help="Multi-agent configuration YAML file"),
|
||||
router: bool = typer.Option(False, "--router", help="Enable router agent for task delegation"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
resume: Optional[str] = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
|
||||
last: bool = typer.Option(False, "--last", help="Resume the last session"),
|
||||
approve: str = typer.Option("manual", "--approve", help="Approval mode: manual, auto, scoped"),
|
||||
read_only: bool = typer.Option(True, "--read-only/--allow-write", help="Read-only mode"),
|
||||
allow_shell: bool = typer.Option(False, "--allow-shell", help="Allow shell command execution"),
|
||||
allow_network: bool = typer.Option(False, "--allow-network", help="Allow network requests"),
|
||||
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
||||
profile: Optional[str] = typer.Option(None, "--profile", help="Use named profile from config"),
|
||||
):
|
||||
"""Start ACP server for IDE integration."""
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"--workspace", workspace,
|
||||
"--agent", agent,
|
||||
"--approve", approve,
|
||||
]
|
||||
|
||||
if agents_file:
|
||||
args.extend(["--agents", agents_file])
|
||||
if router:
|
||||
args.append("--router")
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if resume:
|
||||
args.extend(["--resume", resume])
|
||||
if last:
|
||||
args.append("--last")
|
||||
if not read_only:
|
||||
args.append("--allow-write")
|
||||
if allow_shell:
|
||||
args.append("--allow-shell")
|
||||
if allow_network:
|
||||
args.append("--allow-network")
|
||||
if debug:
|
||||
args.append("--debug")
|
||||
if profile:
|
||||
args.extend(["--profile", profile])
|
||||
|
||||
try:
|
||||
from ..features.acp import run_acp_command
|
||||
exit_code = run_acp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"ACP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Agents command group for PraisonAI CLI.
|
||||
|
||||
Provides agent management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Agent management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def agents_list(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed info"),
|
||||
):
|
||||
"""List available agents."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['agents', 'list']
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("create")
|
||||
def agents_create(
|
||||
name: str = typer.Argument(..., help="Agent name"),
|
||||
template: str = typer.Option(None, "--template", "-t", help="Template to use"),
|
||||
):
|
||||
"""Create a new agent."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['agents', 'create', name]
|
||||
if template:
|
||||
argv.extend(['--template', template])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("info")
|
||||
def agents_info(
|
||||
name: str = typer.Argument(..., help="Agent name"),
|
||||
):
|
||||
"""Show agent information."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['agents', 'info', name]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Benchmark command group for PraisonAI CLI.
|
||||
|
||||
Provides comprehensive performance benchmarking across all execution paths.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Comprehensive performance benchmarking")
|
||||
|
||||
|
||||
@app.command("profile")
|
||||
def benchmark_profile(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations per path"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save results to file"),
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deep cProfile profiling (per-function timing, call graphs)"),
|
||||
limit: int = typer.Option(30, "--limit", "-l", help="Top N functions to show in deep profile"),
|
||||
):
|
||||
"""
|
||||
Run full benchmark suite across all execution paths.
|
||||
|
||||
Benchmarks:
|
||||
- OpenAI SDK (baseline)
|
||||
- PraisonAI Agent
|
||||
- PraisonAI CLI
|
||||
- PraisonAI CLI with profiling
|
||||
- PraisonAI Workflow (single agent)
|
||||
- PraisonAI Workflow (multi-agent)
|
||||
- PraisonAI via LiteLLM
|
||||
- LiteLLM standalone
|
||||
|
||||
With --deep flag, includes:
|
||||
- Per-function timing (cProfile stats)
|
||||
- Self time vs cumulative time
|
||||
- Call counts per function
|
||||
- Caller/callee relationships
|
||||
- Module breakdown by category
|
||||
- Call graph data
|
||||
|
||||
Examples:
|
||||
praisonai benchmark profile "What is 2+2?"
|
||||
praisonai benchmark profile "Hi" --iterations 5
|
||||
praisonai benchmark profile "Hi" --deep --limit 50
|
||||
praisonai benchmark profile "Hi" --deep --format json --output results.json
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if deep:
|
||||
typer.echo("⚠️ Deep profiling enabled - this adds overhead to measurements", err=True)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
verbose=True,
|
||||
deep=deep,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
output = json.dumps(report.to_dict(), indent=2)
|
||||
if output_file:
|
||||
with open(output_file, "w") as f:
|
||||
f.write(output)
|
||||
typer.echo(f"Results saved to {output_file}")
|
||||
else:
|
||||
typer.echo(output)
|
||||
else:
|
||||
handler.print_report(report, deep=deep, limit=limit)
|
||||
if output_file:
|
||||
import json
|
||||
with open(output_file, "w") as f:
|
||||
f.write(json.dumps(report.to_dict(), indent=2))
|
||||
typer.echo(f"\nJSON results saved to {output_file}")
|
||||
|
||||
|
||||
@app.command("compare")
|
||||
def benchmark_compare(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(2, "--iterations", "-n", help="Number of iterations"),
|
||||
):
|
||||
"""
|
||||
Quick comparison of key execution paths.
|
||||
|
||||
Compares OpenAI SDK, PraisonAI Agent, PraisonAI CLI, and LiteLLM.
|
||||
|
||||
Examples:
|
||||
praisonai benchmark compare "Hi"
|
||||
praisonai benchmark compare "What is 2+2?" --iterations 3
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
paths=["openai_sdk", "praisonai_agent", "praisonai_cli", "litellm_standalone"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
typer.echo("\n" + handler.create_comparison_table(report))
|
||||
|
||||
|
||||
@app.command("sdk")
|
||||
def benchmark_sdk(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Benchmark OpenAI SDK only (baseline).
|
||||
|
||||
Examples:
|
||||
praisonai benchmark sdk "Hi"
|
||||
praisonai benchmark sdk "What is 2+2?" --iterations 5
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(prompt=prompt, iterations=iterations, paths=["openai_sdk"], verbose=True)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(report.to_dict(), indent=2))
|
||||
else:
|
||||
handler.print_report(report)
|
||||
|
||||
|
||||
@app.command("agent")
|
||||
def benchmark_agent(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Benchmark PraisonAI Agent vs SDK baseline.
|
||||
|
||||
Examples:
|
||||
praisonai benchmark agent "Hi"
|
||||
praisonai benchmark agent "What is 2+2?" --iterations 5
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
paths=["openai_sdk", "praisonai_agent"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(report.to_dict(), indent=2))
|
||||
else:
|
||||
handler.print_report(report)
|
||||
|
||||
|
||||
@app.command("cli")
|
||||
def benchmark_cli(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Benchmark PraisonAI CLI vs SDK baseline.
|
||||
|
||||
Examples:
|
||||
praisonai benchmark cli "Hi"
|
||||
praisonai benchmark cli "What is 2+2?" --iterations 5
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
paths=["openai_sdk", "praisonai_cli", "praisonai_cli_profile"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(report.to_dict(), indent=2))
|
||||
else:
|
||||
handler.print_report(report)
|
||||
|
||||
|
||||
@app.command("workflow")
|
||||
def benchmark_workflow(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Benchmark PraisonAI Workflow (single and multi-agent) vs SDK baseline.
|
||||
|
||||
Examples:
|
||||
praisonai benchmark workflow "Hi"
|
||||
praisonai benchmark workflow "What is 2+2?" --iterations 5
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
paths=["openai_sdk", "praisonai_workflow_single", "praisonai_workflow_multi"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(report.to_dict(), indent=2))
|
||||
else:
|
||||
handler.print_report(report)
|
||||
|
||||
|
||||
@app.command("litellm")
|
||||
def benchmark_litellm(
|
||||
prompt: str = typer.Argument("Hi", help="Prompt to benchmark"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Benchmark LiteLLM paths vs SDK baseline.
|
||||
|
||||
Examples:
|
||||
praisonai benchmark litellm "Hi"
|
||||
praisonai benchmark litellm "What is 2+2?" --iterations 5
|
||||
"""
|
||||
try:
|
||||
from ..features.benchmark import BenchmarkHandler
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Benchmark module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
handler = BenchmarkHandler()
|
||||
report = handler.run_full_benchmark(
|
||||
prompt=prompt,
|
||||
iterations=iterations,
|
||||
paths=["openai_sdk", "praisonai_litellm", "litellm_standalone"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(report.to_dict(), indent=2))
|
||||
else:
|
||||
handler.print_report(report)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def benchmark_callback(ctx: typer.Context):
|
||||
"""Show benchmark help if no subcommand."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
typer.echo(ctx.get_help())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Call command group for PraisonAI CLI.
|
||||
|
||||
Provides voice/call interaction commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Voice/call interaction mode")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def call_main(
|
||||
ctx: typer.Context,
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
):
|
||||
"""
|
||||
Start voice/call interaction mode.
|
||||
|
||||
Examples:
|
||||
praisonai call
|
||||
praisonai call --model gpt-4o
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['call']
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Chat command group for PraisonAI CLI.
|
||||
|
||||
Provides interactive chat commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Interactive chat mode")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def chat_main(
|
||||
ctx: typer.Context,
|
||||
prompt: Optional[str] = typer.Argument(None, help="Initial prompt for chat"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
|
||||
user_id: Optional[str] = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID to resume"),
|
||||
):
|
||||
"""
|
||||
Start interactive chat mode.
|
||||
|
||||
Examples:
|
||||
praisonai chat
|
||||
praisonai chat "Hello, how are you?"
|
||||
praisonai chat --model gpt-4o --memory
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
# Build args for legacy handler
|
||||
argv = ['chat']
|
||||
if prompt:
|
||||
argv.append(prompt)
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
if memory:
|
||||
argv.append('--memory')
|
||||
if tools:
|
||||
argv.extend(['--tools', tools])
|
||||
if user_id:
|
||||
argv.extend(['--user-id', user_id])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Code command group for PraisonAI CLI.
|
||||
|
||||
Provides code assistant commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Code assistant mode")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def code_main(
|
||||
ctx: typer.Context,
|
||||
prompt: Optional[str] = typer.Argument(None, help="Code task or question"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
|
||||
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
|
||||
):
|
||||
"""
|
||||
Start code assistant mode.
|
||||
|
||||
Examples:
|
||||
praisonai code
|
||||
praisonai code "Refactor this function"
|
||||
praisonai code --model gpt-4o --workspace ./src
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['code']
|
||||
if prompt:
|
||||
argv.append(prompt)
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
if tools:
|
||||
argv.extend(['--tools', tools])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Commit command group for PraisonAI CLI.
|
||||
|
||||
Provides git commit commands with AI assistance.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="AI-assisted git commits")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def commit_main(
|
||||
ctx: typer.Context,
|
||||
message: str = typer.Option(None, "--message", "-m", help="Commit message (auto-generated if not provided)"),
|
||||
all_files: bool = typer.Option(False, "--all", "-a", help="Stage all changes"),
|
||||
push: bool = typer.Option(False, "--push", "-p", help="Push after commit"),
|
||||
):
|
||||
"""
|
||||
Create AI-assisted git commits.
|
||||
|
||||
Examples:
|
||||
praisonai commit
|
||||
praisonai commit -m "Fix bug"
|
||||
praisonai commit --all --push
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['commit']
|
||||
if message:
|
||||
argv.extend(['--message', message])
|
||||
if all_files:
|
||||
argv.append('--all')
|
||||
if push:
|
||||
argv.append('--push')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Completion command group for PraisonAI CLI.
|
||||
|
||||
Provides shell completion script generation:
|
||||
- completion bash: Generate bash completions
|
||||
- completion zsh: Generate zsh completions
|
||||
- completion fish: Generate fish completions
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Shell completion scripts")
|
||||
|
||||
|
||||
BASH_COMPLETION = '''
|
||||
# PraisonAI bash completion
|
||||
_praisonai_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Top-level commands
|
||||
opts="run config traces env session schedule serve completion version debug lsp diag doctor acp mcp chat code call realtime train ui context research memory rules workflow hooks knowledge tools todo docs commit skills profile eval templates recipe endpoints agents thinking compaction output deploy registry package install uninstall"
|
||||
|
||||
# Subcommands for specific commands
|
||||
case "${prev}" in
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "list get set reset path" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
traces)
|
||||
COMPREPLY=( $(compgen -W "enable disable status list" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "view check doctor" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
session)
|
||||
COMPREPLY=( $(compgen -W "list resume delete export show" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
schedule)
|
||||
COMPREPLY=( $(compgen -W "start stop list logs restart delete describe save stop-all stats" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
serve)
|
||||
COMPREPLY=( $(compgen -W "start stop status" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=( $(compgen -W "bash zsh fish" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
debug)
|
||||
COMPREPLY=( $(compgen -W "interactive lsp acp trace" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
lsp)
|
||||
COMPREPLY=( $(compgen -W "start stop status logs" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
diag)
|
||||
COMPREPLY=( $(compgen -W "export" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
doctor)
|
||||
COMPREPLY=( $(compgen -W "env config tools db mcp obs skills memory permissions network performance ci selftest" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
mcp)
|
||||
COMPREPLY=( $(compgen -W "list add remove test" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
# Global options
|
||||
if [[ ${cur} == -* ]] ; then
|
||||
COMPREPLY=( $(compgen -W "--help --version --output-format --json --no-color --quiet --verbose --screen-reader" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _praisonai_completion praisonai
|
||||
'''
|
||||
|
||||
ZSH_COMPLETION = '''
|
||||
#compdef praisonai
|
||||
|
||||
_praisonai() {
|
||||
local -a commands
|
||||
local -a subcommands
|
||||
|
||||
commands=(
|
||||
'run:Run agents'
|
||||
'config:Configuration management'
|
||||
'traces:Trace collection management'
|
||||
'env:Environment and diagnostics'
|
||||
'session:Session management'
|
||||
'schedule:Scheduler management'
|
||||
'serve:API server management'
|
||||
'completion:Shell completion scripts'
|
||||
'version:Version information'
|
||||
'debug:Debug and test interactive flows'
|
||||
'lsp:LSP service lifecycle'
|
||||
'diag:Diagnostics export'
|
||||
'doctor:Health checks and diagnostics'
|
||||
'acp:Agent Client Protocol server'
|
||||
'mcp:MCP server management'
|
||||
'chat:Start chat UI'
|
||||
'code:Start code UI'
|
||||
'call:Start call server'
|
||||
'realtime:Start realtime interface'
|
||||
'train:Training commands'
|
||||
'ui:Start UI'
|
||||
'context:Context engineering'
|
||||
'research:Deep research'
|
||||
'memory:Memory management'
|
||||
'rules:Rules management'
|
||||
'workflow:Workflow management'
|
||||
'hooks:Hooks management'
|
||||
'knowledge:Knowledge management'
|
||||
'tools:Tools management'
|
||||
'todo:Todo management'
|
||||
'docs:Documentation'
|
||||
'commit:Git commit'
|
||||
'skills:Skills management'
|
||||
'profile:Profile management'
|
||||
'eval:Evaluation'
|
||||
'templates:Templates'
|
||||
'recipe:Recipe management'
|
||||
'endpoints:Endpoints'
|
||||
'agents:Agents management'
|
||||
'deploy:Deployment'
|
||||
'registry:Registry'
|
||||
'package:Package management'
|
||||
'install:Install packages'
|
||||
'uninstall:Uninstall packages'
|
||||
)
|
||||
|
||||
_arguments -C \\
|
||||
'--help[Show help]' \\
|
||||
'--version[Show version]' \\
|
||||
'--output-format[Output format]:format:(text json stream-json)' \\
|
||||
'--json[JSON output]' \\
|
||||
'--no-color[Disable colors]' \\
|
||||
'--quiet[Minimal output]' \\
|
||||
'--verbose[Verbose output]' \\
|
||||
'--screen-reader[Screen reader mode]' \\
|
||||
'1: :->command' \\
|
||||
'*::arg:->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
_describe 'command' commands
|
||||
;;
|
||||
args)
|
||||
case $words[1] in
|
||||
config)
|
||||
subcommands=('list:List config' 'get:Get value' 'set:Set value' 'reset:Reset config' 'path:Show path')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
traces)
|
||||
subcommands=('enable:Enable traces' 'disable:Disable traces' 'status:Show status' 'list:List traces')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
env)
|
||||
subcommands=('view:View env vars' 'check:Check API keys' 'doctor:Run diagnostics')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
session)
|
||||
subcommands=('list:List sessions' 'resume:Resume session' 'delete:Delete session' 'export:Export session' 'show:Show session')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
schedule)
|
||||
subcommands=('start:Start scheduler' 'stop:Stop scheduler' 'list:List jobs' 'logs:View logs' 'restart:Restart' 'delete:Delete job' 'describe:Describe job' 'save:Save config' 'stop-all:Stop all' 'stats:Show stats')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
serve)
|
||||
subcommands=('start:Start server' 'stop:Stop server' 'status:Show status')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
completion)
|
||||
subcommands=('bash:Bash completion' 'zsh:Zsh completion' 'fish:Fish completion')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
debug)
|
||||
subcommands=('interactive:Interactive debug' 'lsp:LSP debug' 'acp:ACP debug' 'trace:Trace debug')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
lsp)
|
||||
subcommands=('start:Start LSP' 'stop:Stop LSP' 'status:LSP status' 'logs:LSP logs')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
diag)
|
||||
subcommands=('export:Export diagnostics')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
doctor)
|
||||
subcommands=('env:Check env' 'config:Check config' 'tools:Check tools' 'db:Check DB' 'mcp:Check MCP' 'obs:Check observability' 'skills:Check skills' 'memory:Check memory' 'permissions:Check permissions' 'network:Check network' 'performance:Check performance' 'ci:CI checks' 'selftest:Self test')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
mcp)
|
||||
subcommands=('list:List servers' 'add:Add server' 'remove:Remove server' 'test:Test server')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_praisonai "$@"
|
||||
'''
|
||||
|
||||
FISH_COMPLETION = '''
|
||||
# PraisonAI fish completion
|
||||
|
||||
# Top-level commands
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "run" -d "Run agents"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "config" -d "Configuration management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "traces" -d "Trace collection management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "env" -d "Environment and diagnostics"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "session" -d "Session management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "schedule" -d "Scheduler management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "serve" -d "API server management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "completion" -d "Shell completion scripts"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "version" -d "Version information"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "debug" -d "Debug and test interactive flows"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "lsp" -d "LSP service lifecycle"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "diag" -d "Diagnostics export"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "doctor" -d "Health checks and diagnostics"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "acp" -d "Agent Client Protocol server"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "mcp" -d "MCP server management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "chat" -d "Start chat UI"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "code" -d "Start code UI"
|
||||
|
||||
# Global options
|
||||
complete -c praisonai -l help -d "Show help"
|
||||
complete -c praisonai -l version -d "Show version"
|
||||
complete -c praisonai -l output-format -x -a "text json stream-json" -d "Output format"
|
||||
complete -c praisonai -l json -d "JSON output"
|
||||
complete -c praisonai -l no-color -d "Disable colors"
|
||||
complete -c praisonai -l quiet -s q -d "Minimal output"
|
||||
complete -c praisonai -l verbose -s v -d "Verbose output"
|
||||
complete -c praisonai -l screen-reader -d "Screen reader mode"
|
||||
|
||||
# config subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "list" -d "List config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "get" -d "Get value"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "set" -d "Set value"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "reset" -d "Reset config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "path" -d "Show path"
|
||||
|
||||
# traces subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "enable" -d "Enable traces"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "disable" -d "Disable traces"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "status" -d "Show status"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "list" -d "List traces"
|
||||
|
||||
# env subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "view" -d "View env vars"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "check" -d "Check API keys"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "doctor" -d "Run diagnostics"
|
||||
|
||||
# session subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "list" -d "List sessions"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "resume" -d "Resume session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "delete" -d "Delete session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "export" -d "Export session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "show" -d "Show session"
|
||||
|
||||
# completion subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "bash" -d "Bash completion"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "zsh" -d "Zsh completion"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "fish" -d "Fish completion"
|
||||
|
||||
# debug subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "interactive" -d "Interactive debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "lsp" -d "LSP debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "acp" -d "ACP debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "trace" -d "Trace debug"
|
||||
|
||||
# doctor subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "env" -d "Check env"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "config" -d "Check config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "tools" -d "Check tools"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "db" -d "Check DB"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "mcp" -d "Check MCP"
|
||||
'''
|
||||
|
||||
|
||||
@app.command("bash")
|
||||
def completion_bash():
|
||||
"""Generate bash completion script."""
|
||||
print(BASH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.command("zsh")
|
||||
def completion_zsh():
|
||||
"""Generate zsh completion script."""
|
||||
print(ZSH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.command("fish")
|
||||
def completion_fish():
|
||||
"""Generate fish completion script."""
|
||||
print(FISH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def completion_callback(ctx: typer.Context):
|
||||
"""Show completion installation instructions."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"Generate shell completion scripts.\n\n"
|
||||
"Usage:\n"
|
||||
" # Bash\n"
|
||||
" praisonai completion bash >> ~/.bashrc\n\n"
|
||||
" # Zsh\n"
|
||||
" praisonai completion zsh > ~/.zsh/completions/_praisonai\n\n"
|
||||
" # Fish\n"
|
||||
" praisonai completion fish > ~/.config/fish/completions/praisonai.fish",
|
||||
title="Shell Completions"
|
||||
)
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Config command group for PraisonAI CLI.
|
||||
|
||||
Provides configuration management commands:
|
||||
- config list: Show all configuration
|
||||
- config get: Get a specific value
|
||||
- config set: Set a configuration value
|
||||
- config reset: Reset to defaults
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..configuration.loader import get_config_loader
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Configuration management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def config_list(
|
||||
scope: str = typer.Option(
|
||||
"all",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope to list: all, user, project",
|
||||
),
|
||||
):
|
||||
"""List all configuration values."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.list_all()
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(config)
|
||||
return
|
||||
|
||||
output.print_panel("Configuration", title="PraisonAI Config")
|
||||
|
||||
def print_dict(d, prefix=""):
|
||||
for key, value in d.items():
|
||||
full_key = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict):
|
||||
print_dict(value, full_key)
|
||||
else:
|
||||
output.print(f" {full_key} = {value}")
|
||||
|
||||
print_dict(config)
|
||||
|
||||
|
||||
@app.command("get")
|
||||
def config_get(
|
||||
key: str = typer.Argument(..., help="Configuration key (dotted notation, e.g., output.format)"),
|
||||
):
|
||||
"""Get a configuration value."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
value = loader.get(key)
|
||||
|
||||
if value is None:
|
||||
output.print_error(f"Key not found: {key}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"key": key, "value": value})
|
||||
else:
|
||||
output.print(f"{key} = {value}")
|
||||
|
||||
|
||||
@app.command("set")
|
||||
def config_set(
|
||||
key: str = typer.Argument(..., help="Configuration key (dotted notation)"),
|
||||
value: str = typer.Argument(..., help="Value to set"),
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope: user or project",
|
||||
),
|
||||
):
|
||||
"""Set a configuration value."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
loader.set(key, parsed_value, scope=scope)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"key": key, "value": parsed_value, "scope": scope})
|
||||
else:
|
||||
output.print_success(f"Set {key} = {parsed_value} ({scope})")
|
||||
|
||||
|
||||
@app.command("reset")
|
||||
def config_reset(
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope to reset: user or project",
|
||||
),
|
||||
confirm: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation",
|
||||
),
|
||||
):
|
||||
"""Reset configuration to defaults."""
|
||||
output = get_output_controller()
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Reset {scope} configuration to defaults?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
loader = get_config_loader()
|
||||
loader.reset(scope=scope)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"reset": True, "scope": scope})
|
||||
else:
|
||||
output.print_success(f"Reset {scope} configuration to defaults")
|
||||
|
||||
|
||||
@app.command("path")
|
||||
def config_path(
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope: user or project",
|
||||
),
|
||||
):
|
||||
"""Show configuration file path."""
|
||||
output = get_output_controller()
|
||||
|
||||
from ..configuration.paths import get_user_config_path, get_project_config_path
|
||||
|
||||
if scope == "project":
|
||||
path = get_project_config_path()
|
||||
else:
|
||||
path = get_user_config_path()
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"path": str(path), "exists": path.exists()})
|
||||
else:
|
||||
exists = "✓" if path.exists() else "✗"
|
||||
output.print(f"{path} [{exists}]")
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Context command group for PraisonAI CLI.
|
||||
|
||||
Provides context management commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Context management")
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def context_show(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
):
|
||||
"""Show current context."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['context', 'show']
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("clear")
|
||||
def context_clear():
|
||||
"""Clear current context."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['context', 'clear']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def context_add(
|
||||
content: str = typer.Argument(..., help="Content to add to context"),
|
||||
):
|
||||
"""Add content to context."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['context', 'add', content]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Debug command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing debug functionality from features/debug.py.
|
||||
Provides debug commands for testing interactive flows.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Debug and test interactive flows")
|
||||
|
||||
|
||||
@app.command("interactive")
|
||||
def debug_interactive(
|
||||
prompt: str = typer.Option(..., "--prompt", "-p", help="Prompt to execute"),
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
lsp: bool = typer.Option(True, "--lsp/--no-lsp", help="Enable/disable LSP"),
|
||||
acp: bool = typer.Option(True, "--acp/--no-acp", help="Enable/disable ACP"),
|
||||
approval: str = typer.Option("auto", "--approval", help="Approval mode: manual, auto, scoped"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON trace"),
|
||||
trace_file: Optional[str] = typer.Option(None, "--trace-file", help="Save trace to file"),
|
||||
timeout: float = typer.Option(60.0, "--timeout", help="Timeout in seconds"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
):
|
||||
"""Run single interactive turn non-interactively."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"interactive",
|
||||
"--prompt", prompt,
|
||||
"--workspace", workspace,
|
||||
"--approval", approval,
|
||||
"--timeout", str(timeout),
|
||||
]
|
||||
|
||||
if not lsp:
|
||||
args.append("--no-lsp")
|
||||
if not acp:
|
||||
args.append("--no-acp")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
if trace_file:
|
||||
args.extend(["--trace-file", trace_file])
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
|
||||
# Delegate to existing handler
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("lsp")
|
||||
def debug_lsp(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
language: Optional[str] = typer.Option(None, "--language", "-l", help="Language to probe"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Direct LSP probes."""
|
||||
args = ["lsp", "--workspace", workspace]
|
||||
if language:
|
||||
args.extend(["--language", language])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("acp")
|
||||
def debug_acp(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Direct ACP probes."""
|
||||
args = ["acp", "--workspace", workspace]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("trace")
|
||||
def debug_trace(
|
||||
action: str = typer.Argument("list", help="Action: record, replay, diff, list"),
|
||||
trace_id: Optional[str] = typer.Option(None, "--trace-id", "-t", help="Trace ID"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Trace record/replay/diff."""
|
||||
args = ["trace", action]
|
||||
if trace_id:
|
||||
args.extend(["--trace-id", trace_id])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def debug_callback(ctx: typer.Context):
|
||||
"""Show debug help."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"Debug commands for testing interactive coding assistant flows.\n\n"
|
||||
"Commands:\n"
|
||||
" interactive Run single interactive turn\n"
|
||||
" lsp Direct LSP probes\n"
|
||||
" acp Direct ACP probes\n"
|
||||
" trace Trace record/replay/diff",
|
||||
title="Debug Commands"
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Deploy command group for PraisonAI CLI.
|
||||
|
||||
Provides deployment commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Deployment management")
|
||||
|
||||
|
||||
@app.command("docker")
|
||||
def deploy_docker(
|
||||
file: str = typer.Argument("agents.yaml", help="Agent file to deploy"),
|
||||
tag: str = typer.Option(None, "--tag", "-t", help="Docker image tag"),
|
||||
):
|
||||
"""Deploy as Docker container."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['deploy', 'docker', file]
|
||||
if tag:
|
||||
argv.extend(['--tag', tag])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("aws")
|
||||
def deploy_aws(
|
||||
file: str = typer.Argument("agents.yaml", help="Agent file to deploy"),
|
||||
region: str = typer.Option(None, "--region", "-r", help="AWS region"),
|
||||
):
|
||||
"""Deploy to AWS."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['deploy', 'aws', file]
|
||||
if region:
|
||||
argv.extend(['--region', region])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("gcp")
|
||||
def deploy_gcp(
|
||||
file: str = typer.Argument("agents.yaml", help="Agent file to deploy"),
|
||||
project: str = typer.Option(None, "--project", "-p", help="GCP project"),
|
||||
):
|
||||
"""Deploy to Google Cloud."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['deploy', 'gcp', file]
|
||||
if project:
|
||||
argv.extend(['--project', project])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("azure")
|
||||
def deploy_azure(
|
||||
file: str = typer.Argument("agents.yaml", help="Agent file to deploy"),
|
||||
resource_group: str = typer.Option(None, "--resource-group", "-g", help="Azure resource group"),
|
||||
):
|
||||
"""Deploy to Azure."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['deploy', 'azure', file]
|
||||
if resource_group:
|
||||
argv.extend(['--resource-group', resource_group])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Diag command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing diagnostics functionality from features/diag.py.
|
||||
Provides diagnostic export for bug reports.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Diagnostics export")
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def diag_export(
|
||||
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output path"),
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
include_logs: bool = typer.Option(True, "--include-logs/--no-logs", help="Include log files"),
|
||||
include_config: bool = typer.Option(True, "--include-config/--no-config", help="Include configuration"),
|
||||
include_trace: bool = typer.Option(True, "--include-trace/--no-trace", help="Include recent traces"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Export diagnostic bundle for bug reports."""
|
||||
args = ["export", "--workspace", workspace]
|
||||
|
||||
if output_path:
|
||||
args.extend(["--output", output_path])
|
||||
if not include_logs:
|
||||
args.append("--no-logs")
|
||||
if not include_config:
|
||||
args.append("--no-config")
|
||||
if not include_trace:
|
||||
args.append("--no-trace")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.diag import run_diag_command
|
||||
exit_code = run_diag_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Diag module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def diag_callback(ctx: typer.Context):
|
||||
"""Show diag help or run export."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Default to export
|
||||
diag_export()
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Docs command group for PraisonAI CLI.
|
||||
|
||||
Provides documentation commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Documentation management")
|
||||
|
||||
|
||||
@app.command("generate")
|
||||
def docs_generate(
|
||||
source: str = typer.Argument(".", help="Source directory"),
|
||||
output: str = typer.Option("docs", "--output", "-o", help="Output directory"),
|
||||
):
|
||||
"""Generate documentation."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['docs', 'generate', source, '--output', output]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("serve")
|
||||
def docs_serve(
|
||||
port: int = typer.Option(8000, "--port", "-p", help="Port to serve on"),
|
||||
):
|
||||
"""Serve documentation locally."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['docs', 'serve', '--port', str(port)]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Doctor command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing doctor functionality from features/doctor/.
|
||||
Provides health checks and diagnostics.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Health checks and diagnostics")
|
||||
|
||||
|
||||
def _run_doctor(args: list) -> int:
|
||||
"""Run doctor command with args."""
|
||||
try:
|
||||
from ..features.doctor.handler import DoctorHandler
|
||||
handler = DoctorHandler()
|
||||
# DoctorHandler uses execute(action, action_args) pattern
|
||||
action = args[0] if args else None
|
||||
action_args = args[1:] if len(args) > 1 else []
|
||||
return handler.execute(action, action_args)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Doctor module not available: {e}")
|
||||
return 4
|
||||
except Exception as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Doctor error: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
@app.command("env")
|
||||
def doctor_env(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
timeout: float = typer.Option(10.0, "--timeout", help="Per-check timeout"),
|
||||
):
|
||||
"""Check environment variables and API keys."""
|
||||
args = ["env"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
args.extend(["--timeout", str(timeout)])
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("config")
|
||||
def doctor_config(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check configuration files."""
|
||||
args = ["config"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("tools")
|
||||
def doctor_tools(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check tools availability."""
|
||||
args = ["tools"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("db")
|
||||
def doctor_db(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check database connections."""
|
||||
args = ["db"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("mcp")
|
||||
def doctor_mcp(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check MCP servers."""
|
||||
args = ["mcp"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("network")
|
||||
def doctor_network(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check network connectivity."""
|
||||
args = ["network"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("performance")
|
||||
def doctor_performance(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check performance metrics."""
|
||||
args = ["performance"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("selftest")
|
||||
def doctor_selftest(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Run self-test."""
|
||||
args = ["selftest"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def doctor_callback(
|
||||
ctx: typer.Context,
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
strict: bool = typer.Option(False, "--strict", help="Treat warnings as failures"),
|
||||
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
||||
):
|
||||
"""Run all fast health checks."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
args = []
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
if strict:
|
||||
args.append("--strict")
|
||||
if quiet:
|
||||
args.append("--quiet")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Endpoints command group for PraisonAI CLI.
|
||||
|
||||
Provides API endpoint management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="API endpoint management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def endpoints_list():
|
||||
"""List available endpoints."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['endpoints', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("test")
|
||||
def endpoints_test(
|
||||
endpoint: str = typer.Argument(..., help="Endpoint to test"),
|
||||
):
|
||||
"""Test an endpoint."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['endpoints', 'test', endpoint]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Environment command group for PraisonAI CLI.
|
||||
|
||||
Provides environment management and diagnostics:
|
||||
- env view: Show environment variables
|
||||
- env check: Validate API keys
|
||||
- env doctor: Run diagnostics (alias for doctor command)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Environment and diagnostics")
|
||||
|
||||
|
||||
# Known API key patterns
|
||||
API_KEY_PATTERNS = {
|
||||
"OPENAI_API_KEY": {"prefix": "sk-", "min_length": 20},
|
||||
"ANTHROPIC_API_KEY": {"prefix": "sk-ant-", "min_length": 20},
|
||||
"GOOGLE_API_KEY": {"prefix": "AI", "min_length": 20},
|
||||
"GEMINI_API_KEY": {"prefix": "AI", "min_length": 20},
|
||||
"TAVILY_API_KEY": {"prefix": "tvly-", "min_length": 20},
|
||||
"GROQ_API_KEY": {"prefix": "gsk_", "min_length": 20},
|
||||
}
|
||||
|
||||
|
||||
def _redact_key(value: str, show_chars: int = 4) -> str:
|
||||
"""Redact an API key for display."""
|
||||
if len(value) <= show_chars * 2:
|
||||
return "*" * len(value)
|
||||
return value[:show_chars] + "*" * (len(value) - show_chars * 2) + value[-show_chars:]
|
||||
|
||||
|
||||
def _validate_key(name: str, value: str) -> tuple[bool, str]:
|
||||
"""Validate an API key format."""
|
||||
if name not in API_KEY_PATTERNS:
|
||||
return True, "Unknown key type"
|
||||
|
||||
pattern = API_KEY_PATTERNS[name]
|
||||
|
||||
if len(value) < pattern["min_length"]:
|
||||
return False, f"Too short (expected >= {pattern['min_length']} chars)"
|
||||
|
||||
if pattern.get("prefix") and not value.startswith(pattern["prefix"]):
|
||||
return False, f"Invalid prefix (expected {pattern['prefix']}...)"
|
||||
|
||||
return True, "Valid format"
|
||||
|
||||
|
||||
@app.command("view")
|
||||
def env_view(
|
||||
show_values: bool = typer.Option(
|
||||
False,
|
||||
"--show-values",
|
||||
help="Show actual values (redacted by default)",
|
||||
),
|
||||
filter_prefix: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--filter",
|
||||
"-f",
|
||||
help="Filter by prefix (e.g., OPENAI, PRAISONAI)",
|
||||
),
|
||||
):
|
||||
"""Show relevant environment variables."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Relevant prefixes
|
||||
prefixes = ["OPENAI", "ANTHROPIC", "GOOGLE", "GEMINI", "TAVILY", "GROQ",
|
||||
"PRAISONAI", "MODEL", "LLM", "OLLAMA", "AZURE"]
|
||||
|
||||
if filter_prefix:
|
||||
prefixes = [filter_prefix.upper()]
|
||||
|
||||
env_vars = {}
|
||||
for key, value in os.environ.items():
|
||||
for prefix in prefixes:
|
||||
if key.upper().startswith(prefix):
|
||||
if show_values:
|
||||
if "KEY" in key.upper() or "SECRET" in key.upper() or "TOKEN" in key.upper():
|
||||
env_vars[key] = _redact_key(value)
|
||||
else:
|
||||
env_vars[key] = value
|
||||
else:
|
||||
env_vars[key] = "***" if value else "(empty)"
|
||||
break
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"environment": env_vars})
|
||||
return
|
||||
|
||||
if not env_vars:
|
||||
output.print_info("No relevant environment variables found")
|
||||
return
|
||||
|
||||
headers = ["Variable", "Value"]
|
||||
rows = [[k, v] for k, v in sorted(env_vars.items())]
|
||||
output.print_table(headers, rows, title="Environment Variables")
|
||||
|
||||
|
||||
@app.command("check")
|
||||
def env_check():
|
||||
"""Validate API keys and configuration."""
|
||||
output = get_output_controller()
|
||||
|
||||
results = []
|
||||
all_valid = True
|
||||
|
||||
for key_name, pattern in API_KEY_PATTERNS.items():
|
||||
value = os.environ.get(key_name)
|
||||
|
||||
if value:
|
||||
valid, message = _validate_key(key_name, value)
|
||||
results.append({
|
||||
"key": key_name,
|
||||
"present": True,
|
||||
"valid": valid,
|
||||
"message": message,
|
||||
})
|
||||
if not valid:
|
||||
all_valid = False
|
||||
else:
|
||||
results.append({
|
||||
"key": key_name,
|
||||
"present": False,
|
||||
"valid": False,
|
||||
"message": "Not set",
|
||||
})
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"checks": results, "all_valid": all_valid})
|
||||
return
|
||||
|
||||
headers = ["Key", "Status", "Message"]
|
||||
rows = []
|
||||
for r in results:
|
||||
if r["present"]:
|
||||
status = "✅" if r["valid"] else "⚠️"
|
||||
else:
|
||||
status = "❌"
|
||||
rows.append([r["key"], status, r["message"]])
|
||||
|
||||
output.print_table(headers, rows, title="API Key Validation")
|
||||
|
||||
if not all_valid:
|
||||
output.print_warning("Some API keys are missing or invalid")
|
||||
raise typer.Exit(3) # Exit code 3 = missing config/env
|
||||
|
||||
|
||||
@app.command("doctor")
|
||||
def env_doctor(
|
||||
deep: bool = typer.Option(
|
||||
False,
|
||||
"--deep",
|
||||
help="Run deep checks (network, DB connections)",
|
||||
),
|
||||
):
|
||||
"""Run environment diagnostics (alias for 'praisonai doctor env')."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Delegate to doctor command
|
||||
try:
|
||||
from ..features.doctor.handler import DoctorHandler
|
||||
handler = DoctorHandler()
|
||||
|
||||
# Run env checks
|
||||
if output.is_json_mode:
|
||||
# Run and get JSON output
|
||||
result = handler.handle(["env", "--json"] if not deep else ["env", "--json", "--deep"])
|
||||
else:
|
||||
result = handler.handle(["env"] if not deep else ["env", "--deep"])
|
||||
|
||||
raise typer.Exit(result)
|
||||
except ImportError:
|
||||
# Fallback to basic env check
|
||||
env_check()
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Eval command group for PraisonAI CLI.
|
||||
|
||||
Provides evaluation commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Evaluation and testing")
|
||||
|
||||
|
||||
@app.command("accuracy")
|
||||
def eval_accuracy(
|
||||
agent: str = typer.Argument(..., help="Agent file or name"),
|
||||
input_text: str = typer.Option(..., "--input", "-i", help="Input text"),
|
||||
expected: str = typer.Option(..., "--expected", "-e", help="Expected output"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"),
|
||||
):
|
||||
"""Run accuracy evaluation."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['eval', 'accuracy', agent, '--input', input_text, '--expected', expected]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("performance")
|
||||
def eval_performance(
|
||||
agent: str = typer.Argument(..., help="Agent file or name"),
|
||||
iterations: int = typer.Option(50, "--iterations", "-n", help="Number of iterations"),
|
||||
):
|
||||
"""Run performance evaluation."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['eval', 'performance', agent, '--iterations', str(iterations)]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Hooks command group for PraisonAI CLI.
|
||||
|
||||
Provides hook management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Hook management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def hooks_list():
|
||||
"""List available hooks."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['hooks', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def hooks_add(
|
||||
name: str = typer.Argument(..., help="Hook name"),
|
||||
event: str = typer.Option(..., "--event", "-e", help="Event to hook into"),
|
||||
):
|
||||
"""Add a hook."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['hooks', 'add', name, '--event', event]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("remove")
|
||||
def hooks_remove(
|
||||
name: str = typer.Argument(..., help="Hook name to remove"),
|
||||
):
|
||||
"""Remove a hook."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['hooks', 'remove', name]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Knowledge command group for PraisonAI CLI.
|
||||
|
||||
Provides knowledge base management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Knowledge base management")
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def knowledge_add(
|
||||
source: str = typer.Argument(..., help="Source file or URL"),
|
||||
name: str = typer.Option(None, "--name", "-n", help="Knowledge base name"),
|
||||
):
|
||||
"""Add knowledge from a source."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['knowledge', 'add', source]
|
||||
if name:
|
||||
argv.extend(['--name', name])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def knowledge_list():
|
||||
"""List knowledge bases."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['knowledge', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("search")
|
||||
def knowledge_search(
|
||||
query: str = typer.Argument(..., help="Search query"),
|
||||
):
|
||||
"""Search knowledge base."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['knowledge', 'search', query]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
LSP command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing LSP functionality from features/lsp_cli.py.
|
||||
Provides LSP service lifecycle management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="LSP service lifecycle management")
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def lsp_start(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
language: Optional[str] = typer.Option(None, "--language", "-l", help="Language to start"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Start LSP server(s)."""
|
||||
args = ["start", "--workspace", workspace]
|
||||
if language:
|
||||
args.extend(["--language", language])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def lsp_stop(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Stop LSP server(s)."""
|
||||
args = ["stop"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def lsp_status(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show LSP status."""
|
||||
args = ["status", "--workspace", workspace]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("logs")
|
||||
def lsp_logs(
|
||||
tail: int = typer.Option(50, "--tail", "-n", help="Number of lines to show"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show recent LSP logs."""
|
||||
args = ["logs", "--tail", str(tail)]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def lsp_callback(ctx: typer.Context):
|
||||
"""Show LSP help."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"LSP service lifecycle management.\n\n"
|
||||
"Commands:\n"
|
||||
" start Start LSP server(s)\n"
|
||||
" stop Stop LSP server(s)\n"
|
||||
" status Show LSP status\n"
|
||||
" logs Show recent logs",
|
||||
title="LSP Commands"
|
||||
)
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
MCP command group for PraisonAI CLI.
|
||||
|
||||
Provides MCP (Model Context Protocol) server management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..configuration.loader import get_config_loader
|
||||
|
||||
app = typer.Typer(help="MCP server management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def mcp_list(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List configured MCP servers."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
servers = config.mcp.servers
|
||||
|
||||
if output.is_json_mode or json_output:
|
||||
output.print_json({
|
||||
"servers": {
|
||||
name: {
|
||||
"command": s.command,
|
||||
"args": s.args,
|
||||
"enabled": s.enabled,
|
||||
}
|
||||
for name, s in servers.items()
|
||||
}
|
||||
})
|
||||
return
|
||||
|
||||
if not servers:
|
||||
output.print_info("No MCP servers configured")
|
||||
output.print("\nAdd a server with: praisonai mcp add <name> <command>")
|
||||
return
|
||||
|
||||
headers = ["Name", "Command", "Enabled"]
|
||||
rows = []
|
||||
for name, server in servers.items():
|
||||
cmd = f"{server.command} {' '.join(server.args)}"
|
||||
if len(cmd) > 50:
|
||||
cmd = cmd[:47] + "..."
|
||||
rows.append([name, cmd, "✓" if server.enabled else "✗"])
|
||||
|
||||
output.print_table(headers, rows, title="MCP Servers")
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def mcp_add(
|
||||
name: str = typer.Argument(..., help="Server name"),
|
||||
command: str = typer.Argument(..., help="Command to run"),
|
||||
args: Optional[str] = typer.Option(None, "--args", "-a", help="Command arguments (space-separated)"),
|
||||
env: Optional[str] = typer.Option(None, "--env", "-e", help="Environment variables (KEY=VALUE,...)"),
|
||||
):
|
||||
"""Add an MCP server."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
# Parse args
|
||||
arg_list = args.split() if args else []
|
||||
|
||||
# Parse env
|
||||
env_dict = {}
|
||||
if env:
|
||||
for pair in env.split(","):
|
||||
if "=" in pair:
|
||||
key, value = pair.split("=", 1)
|
||||
env_dict[key.strip()] = value.strip()
|
||||
|
||||
# Build server config
|
||||
server_config = {
|
||||
"command": command,
|
||||
"args": arg_list,
|
||||
"env": env_dict,
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
# Save to config
|
||||
loader.set(f"mcp.servers.{name}", server_config)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"added": name, "config": server_config})
|
||||
else:
|
||||
output.print_success(f"Added MCP server: {name}")
|
||||
|
||||
|
||||
@app.command("remove")
|
||||
def mcp_remove(
|
||||
name: str = typer.Argument(..., help="Server name to remove"),
|
||||
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
||||
):
|
||||
"""Remove an MCP server."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
if name not in config.mcp.servers:
|
||||
output.print_error(f"Server not found: {name}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Remove MCP server '{name}'?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Remove from config by setting to None (will be cleaned up)
|
||||
# For now, we need to reload and modify the raw config
|
||||
from ..configuration.paths import get_user_config_path
|
||||
from ..configuration.loader import _load_toml, _save_toml
|
||||
|
||||
config_path = get_user_config_path()
|
||||
if config_path.exists():
|
||||
raw_config = _load_toml(config_path)
|
||||
if "mcp" in raw_config and "servers" in raw_config["mcp"]:
|
||||
if name in raw_config["mcp"]["servers"]:
|
||||
del raw_config["mcp"]["servers"][name]
|
||||
_save_toml(config_path, raw_config)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"removed": name})
|
||||
else:
|
||||
output.print_success(f"Removed MCP server: {name}")
|
||||
|
||||
|
||||
@app.command("test")
|
||||
def mcp_test(
|
||||
name: str = typer.Argument(..., help="Server name to test"),
|
||||
timeout: float = typer.Option(10.0, "--timeout", "-t", help="Timeout in seconds"),
|
||||
):
|
||||
"""Test an MCP server connection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
if name not in config.mcp.servers:
|
||||
output.print_error(f"Server not found: {name}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
server = config.mcp.servers[name]
|
||||
|
||||
output.print_info(f"Testing MCP server: {name}")
|
||||
output.print(f" Command: {server.command} {' '.join(server.args)}")
|
||||
|
||||
# Try to start the server and check if it responds
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
try:
|
||||
cmd = [server.command] + server.args
|
||||
env = dict(**server.env) if server.env else None
|
||||
|
||||
# Start process
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env,
|
||||
)
|
||||
|
||||
# Wait briefly and check if it's running
|
||||
time.sleep(0.5)
|
||||
|
||||
if proc.poll() is None:
|
||||
# Process is running
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "ok", "message": "Server started successfully"})
|
||||
else:
|
||||
output.print_success(f"Server '{name}' started successfully")
|
||||
else:
|
||||
# Process exited
|
||||
stderr = proc.stderr.read().decode() if proc.stderr else ""
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": stderr or "Server exited immediately"})
|
||||
else:
|
||||
output.print_error(f"Server exited immediately: {stderr}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
except FileNotFoundError:
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": f"Command not found: {server.command}"})
|
||||
else:
|
||||
output.print_error(f"Command not found: {server.command}")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": str(e)})
|
||||
else:
|
||||
output.print_error(f"Test failed: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def mcp_callback(ctx: typer.Context):
|
||||
"""Show MCP help or list servers."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
mcp_list(json_output=False)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Memory command group for PraisonAI CLI.
|
||||
|
||||
Provides memory management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Memory management")
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def memory_show(
|
||||
user_id: str = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
limit: int = typer.Option(10, "--limit", "-l", help="Number of memories to show"),
|
||||
):
|
||||
"""Show stored memories."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['memory', 'show']
|
||||
if user_id:
|
||||
argv.extend(['--user-id', user_id])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def memory_add(
|
||||
content: str = typer.Argument(..., help="Memory content to add"),
|
||||
user_id: str = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
):
|
||||
"""Add a memory."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['memory', 'add', content]
|
||||
if user_id:
|
||||
argv.extend(['--user-id', user_id])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("search")
|
||||
def memory_search(
|
||||
query: str = typer.Argument(..., help="Search query"),
|
||||
user_id: str = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
):
|
||||
"""Search memories."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['memory', 'search', query]
|
||||
if user_id:
|
||||
argv.extend(['--user-id', user_id])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("clear")
|
||||
def memory_clear(
|
||||
user_id: str = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Force clear without confirmation"),
|
||||
):
|
||||
"""Clear all memories."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['memory', 'clear']
|
||||
if user_id:
|
||||
argv.extend(['--user-id', user_id])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Package command group for PraisonAI CLI.
|
||||
|
||||
Provides package management commands (pip-like).
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Package management")
|
||||
|
||||
|
||||
@app.command("install")
|
||||
def package_install(
|
||||
package: str = typer.Argument(..., help="Package to install"),
|
||||
upgrade: bool = typer.Option(False, "--upgrade", "-U", help="Upgrade if already installed"),
|
||||
):
|
||||
"""Install a package."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['package', 'install', package]
|
||||
if upgrade:
|
||||
argv.append('--upgrade')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("uninstall")
|
||||
def package_uninstall(
|
||||
package: str = typer.Argument(..., help="Package to uninstall"),
|
||||
):
|
||||
"""Uninstall a package."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['package', 'uninstall', package]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def package_list():
|
||||
"""List installed packages."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['package', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
Profile command group for PraisonAI CLI.
|
||||
|
||||
Provides detailed cProfile-based profiling for query execution.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Performance profiling and diagnostics")
|
||||
|
||||
|
||||
@app.command("query")
|
||||
def profile_query(
|
||||
prompt: str = typer.Argument(..., help="Prompt to profile"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use"),
|
||||
stream: bool = typer.Option(False, "--stream/--no-stream", help="Use streaming mode"),
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deep call tracing (higher overhead)"),
|
||||
limit: int = typer.Option(30, "--limit", "-n", help="Top N functions to show"),
|
||||
sort: str = typer.Option("cumulative", "--sort", "-s", help="Sort by: cumulative or tottime"),
|
||||
show_callers: bool = typer.Option(False, "--show-callers", help="Show caller functions"),
|
||||
show_callees: bool = typer.Option(False, "--show-callees", help="Show callee functions"),
|
||||
save: Optional[str] = typer.Option(None, "--save", help="Save artifacts to path (creates .prof, .txt, .json)"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Profile a query execution with detailed timing breakdown.
|
||||
|
||||
Uses the unified profiling architecture for consistent results across
|
||||
CLI direct invocation (--profile) and this command.
|
||||
|
||||
Shows per-function timing, call graphs, and latency metrics.
|
||||
|
||||
Examples:
|
||||
praisonai profile query "What is 2+2?"
|
||||
praisonai profile query "Hello" --limit 20
|
||||
praisonai profile query "Test" --deep --show-callers --show-callees
|
||||
praisonai profile query "Test" --save ./profile_results
|
||||
"""
|
||||
# Use unified profiler for consistent results
|
||||
try:
|
||||
from ..execution import ExecutionRequest, Profiler, ProfilerConfig as UnifiedProfilerConfig
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Unified profiler not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Warn about deep tracing overhead
|
||||
if deep:
|
||||
typer.echo("⚠️ Deep call tracing enabled - this adds significant overhead", err=True)
|
||||
|
||||
# Create unified execution request
|
||||
request = ExecutionRequest(
|
||||
prompt=prompt,
|
||||
agent_name="ProfiledAgent",
|
||||
model=model,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
# Create unified profiler config
|
||||
config = UnifiedProfilerConfig(
|
||||
layer=2 if deep else 1,
|
||||
limit=limit,
|
||||
sort_by=sort,
|
||||
show_callers=show_callers,
|
||||
show_callees=show_callees,
|
||||
output_format=output_format,
|
||||
save_path=save,
|
||||
)
|
||||
|
||||
# Run profiler
|
||||
typer.echo("🔬 Starting profiled execution...", err=True)
|
||||
|
||||
try:
|
||||
profiler = Profiler(config)
|
||||
result, report = profiler.profile_sync(request, invocation_method="profile_command")
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
typer.echo("Install praisonaiagents: pip install praisonaiagents", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
typer.echo(f"Error during profiling: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Output results
|
||||
if output_format == "json":
|
||||
typer.echo(report.to_json())
|
||||
else:
|
||||
typer.echo(report.to_text())
|
||||
|
||||
# Save artifacts if requested
|
||||
if save:
|
||||
try:
|
||||
import os
|
||||
os.makedirs(save, exist_ok=True)
|
||||
|
||||
# Save JSON report
|
||||
json_path = os.path.join(save, "profile_report.json")
|
||||
with open(json_path, 'w') as f:
|
||||
f.write(report.to_json())
|
||||
|
||||
# Save text report
|
||||
txt_path = os.path.join(save, "profile_report.txt")
|
||||
with open(txt_path, 'w') as f:
|
||||
f.write(report.to_text())
|
||||
|
||||
typer.echo("\n✅ Artifacts saved:", err=True)
|
||||
typer.echo(f" {json_path}", err=True)
|
||||
typer.echo(f" {txt_path}", err=True)
|
||||
except Exception as e:
|
||||
typer.echo(f"Warning: Failed to save artifacts: {e}", err=True)
|
||||
|
||||
|
||||
@app.command("imports")
|
||||
def profile_imports():
|
||||
"""
|
||||
Profile module import times.
|
||||
|
||||
Shows which modules take longest to import, useful for optimizing startup time.
|
||||
"""
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
typer.echo("🔬 Profiling import times...", err=True)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-X", "importtime", "-c", "import praisonaiagents"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
# Parse import times
|
||||
import_times = []
|
||||
for line in result.stderr.split('\n'):
|
||||
if 'import time:' in line:
|
||||
match = re.search(r'import time:\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(.+)', line)
|
||||
if match:
|
||||
self_time = int(match.group(1))
|
||||
cumulative = int(match.group(2))
|
||||
module = match.group(3).strip()
|
||||
import_times.append((module, self_time, cumulative))
|
||||
|
||||
# Sort by cumulative time
|
||||
import_times.sort(key=lambda x: x[2], reverse=True)
|
||||
|
||||
# Display
|
||||
typer.echo("\n" + "=" * 70)
|
||||
typer.echo("Import Time Analysis")
|
||||
typer.echo("=" * 70)
|
||||
typer.echo(f"{'Module':<45} {'Self (μs)':>10} {'Cumul (μs)':>12}")
|
||||
typer.echo("-" * 70)
|
||||
|
||||
for module, self_time, cumulative in import_times[:30]:
|
||||
module_name = module
|
||||
if len(module_name) > 43:
|
||||
module_name = "..." + module_name[-40:]
|
||||
typer.echo(f"{module_name:<45} {self_time:>10} {cumulative:>12}")
|
||||
|
||||
typer.echo("-" * 70)
|
||||
|
||||
if import_times:
|
||||
total_time = max(t[2] for t in import_times)
|
||||
typer.echo(f"Total import time: {total_time / 1000:.2f} ms")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
typer.echo("Error: Import profiling timed out", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("startup")
|
||||
def profile_startup():
|
||||
"""
|
||||
Profile CLI startup time.
|
||||
|
||||
Measures time from CLI invocation to ready state.
|
||||
"""
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
typer.echo("🔬 Profiling startup time...", err=True)
|
||||
|
||||
# Measure cold start (new process)
|
||||
start = time.perf_counter()
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", "import praisonai; import praisonai.cli"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
cold_start = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Measure warm start (imports cached)
|
||||
start = time.perf_counter()
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", "import praisonai; import praisonai.cli"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
warm_start = (time.perf_counter() - start) * 1000
|
||||
|
||||
typer.echo("\n" + "=" * 50)
|
||||
typer.echo("Startup Time Analysis")
|
||||
typer.echo("=" * 50)
|
||||
typer.echo(f"Cold Start: {cold_start:>10.2f} ms")
|
||||
typer.echo(f"Warm Start: {warm_start:>10.2f} ms")
|
||||
typer.echo("=" * 50)
|
||||
|
||||
if cold_start > 1000:
|
||||
typer.echo("\n⚠️ Cold start > 1s - consider lazy imports", err=True)
|
||||
|
||||
|
||||
@app.command("suite")
|
||||
def profile_suite(
|
||||
output_dir: Optional[str] = typer.Option("/tmp/praisonai_profile_suite", "--output", "-o", help="Output directory for results"),
|
||||
iterations: int = typer.Option(3, "--iterations", "-n", help="Iterations per scenario"),
|
||||
quick: bool = typer.Option(False, "--quick", help="Quick mode (fewer iterations)"),
|
||||
):
|
||||
"""
|
||||
Run a comprehensive profiling suite.
|
||||
|
||||
Runs multiple scenarios (streaming/non-streaming, simple/complex prompts)
|
||||
and produces aggregated statistics and reports.
|
||||
|
||||
Examples:
|
||||
praisonai profile suite
|
||||
praisonai profile suite --output ./my_profile_results
|
||||
praisonai profile suite --quick
|
||||
"""
|
||||
try:
|
||||
from ..features.profiler import run_profile_suite, ScenarioConfig
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Profiler module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Define scenarios
|
||||
iters = 1 if quick else iterations
|
||||
scenarios = [
|
||||
ScenarioConfig(name="simple_non_stream", prompt="hi", stream=False, iterations=iters, warmup=0 if quick else 1),
|
||||
ScenarioConfig(name="simple_stream", prompt="hi", stream=True, iterations=iters, warmup=0 if quick else 1),
|
||||
]
|
||||
|
||||
if not quick:
|
||||
scenarios.extend([
|
||||
ScenarioConfig(name="medium_non_stream", prompt="Explain Python in 2 sentences", stream=False, iterations=max(1, iters-1)),
|
||||
ScenarioConfig(name="medium_stream", prompt="Explain Python in 2 sentences", stream=True, iterations=max(1, iters-1)),
|
||||
])
|
||||
|
||||
typer.echo("🔬 Running Profile Suite...", err=True)
|
||||
typer.echo(f" Output: {output_dir}", err=True)
|
||||
typer.echo(f" Scenarios: {len(scenarios)}", err=True)
|
||||
typer.echo(f" Iterations: {iterations}", err=True)
|
||||
|
||||
try:
|
||||
result = run_profile_suite(
|
||||
output_dir=output_dir,
|
||||
scenarios=scenarios,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Print summary
|
||||
typer.echo("\n" + "=" * 60)
|
||||
typer.echo("Profile Suite Summary")
|
||||
typer.echo("=" * 60)
|
||||
typer.echo(f"Startup Cold: {result.startup_cold_ms:.2f}ms")
|
||||
typer.echo(f"Startup Warm: {result.startup_warm_ms:.2f}ms")
|
||||
|
||||
if result.import_analysis:
|
||||
typer.echo(f"\nTop Import: {result.import_analysis[0]['module']}")
|
||||
typer.echo(f" Time: {result.import_analysis[0]['cumulative_ms']:.2f}ms")
|
||||
|
||||
typer.echo("\nScenario Results:")
|
||||
for scenario in result.scenarios:
|
||||
stats = scenario.get_stats(scenario.total_times)
|
||||
typer.echo(f" {scenario.name}: {stats['mean']:.2f}ms (±{stats['stdev']:.2f}ms)")
|
||||
|
||||
typer.echo(f"\n✅ Full results saved to: {output_dir}")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error running suite: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("snapshot")
|
||||
def profile_snapshot(
|
||||
name: str = typer.Argument("current", help="Name for the snapshot"),
|
||||
baseline: bool = typer.Option(False, "--baseline", "-b", help="Save as baseline for comparison"),
|
||||
compare: bool = typer.Option(False, "--compare", "-c", help="Compare against baseline"),
|
||||
output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
||||
):
|
||||
"""
|
||||
Create or compare performance snapshots.
|
||||
|
||||
Snapshots record timing data for baseline comparison.
|
||||
Use --baseline to save a reference point, then --compare to check for regressions.
|
||||
|
||||
Examples:
|
||||
praisonai profile snapshot --baseline
|
||||
praisonai profile snapshot current --compare
|
||||
praisonai profile snapshot v2.0 --format json
|
||||
"""
|
||||
try:
|
||||
from ..features.profiler import (
|
||||
run_profile_suite,
|
||||
ScenarioConfig,
|
||||
PerfSnapshotManager,
|
||||
create_snapshot_from_suite,
|
||||
format_comparison_report,
|
||||
)
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Profiler module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
manager = PerfSnapshotManager()
|
||||
|
||||
if compare:
|
||||
# Load baseline and compare
|
||||
baseline_snap = manager.load_baseline()
|
||||
if not baseline_snap:
|
||||
typer.echo("❌ No baseline found. Create one with: praisonai profile snapshot --baseline", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
typer.echo("🔬 Running current profile for comparison...", err=True)
|
||||
|
||||
# Run quick suite
|
||||
scenarios = [
|
||||
ScenarioConfig(name="simple", prompt="hi", stream=False, iterations=2, warmup=1),
|
||||
]
|
||||
result = run_profile_suite(
|
||||
output_dir="/tmp/praisonai_snapshot_compare",
|
||||
scenarios=scenarios,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
current_snap = create_snapshot_from_suite(result, name)
|
||||
comparison = manager.compare(current_snap)
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(comparison.to_dict(), indent=2))
|
||||
else:
|
||||
typer.echo(format_comparison_report(comparison))
|
||||
|
||||
if comparison.is_regression():
|
||||
raise typer.Exit(1) # Exit with error on regression
|
||||
else:
|
||||
# Create new snapshot
|
||||
typer.echo(f"🔬 Creating performance snapshot '{name}'...", err=True)
|
||||
|
||||
scenarios = [
|
||||
ScenarioConfig(name="simple", prompt="hi", stream=False, iterations=3, warmup=1),
|
||||
ScenarioConfig(name="stream", prompt="hi", stream=True, iterations=2, warmup=1),
|
||||
]
|
||||
result = run_profile_suite(
|
||||
output_dir="/tmp/praisonai_snapshot",
|
||||
scenarios=scenarios,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
snapshot = create_snapshot_from_suite(result, name)
|
||||
|
||||
if baseline:
|
||||
path = manager.save_baseline(snapshot)
|
||||
typer.echo(f"\n✅ Baseline saved to: {path}")
|
||||
else:
|
||||
path = manager.save(snapshot)
|
||||
typer.echo(f"\n✅ Snapshot saved to: {path}")
|
||||
|
||||
if output_format == "json":
|
||||
import json
|
||||
typer.echo(json.dumps(snapshot.to_dict(), indent=2))
|
||||
else:
|
||||
typer.echo("\nSnapshot Summary:")
|
||||
typer.echo(f" Startup Cold: {snapshot.startup_cold_ms:.2f}ms")
|
||||
typer.echo(f" Import Time: {snapshot.import_time_ms:.2f}ms")
|
||||
typer.echo(f" Query Time: {snapshot.query_time_ms:.2f}ms")
|
||||
|
||||
|
||||
@app.command("optimize")
|
||||
def profile_optimize(
|
||||
prewarm: bool = typer.Option(False, "--prewarm", help="Enable provider pre-warming"),
|
||||
lite: bool = typer.Option(False, "--lite", help="Enable lite mode (skip type validation)"),
|
||||
show_config: bool = typer.Option(False, "--show", help="Show current optimization config"),
|
||||
):
|
||||
"""
|
||||
Configure performance optimizations.
|
||||
|
||||
All optimizations are opt-in and safe by default.
|
||||
|
||||
Examples:
|
||||
praisonai profile optimize --show
|
||||
praisonai profile optimize --prewarm
|
||||
praisonai profile optimize --lite
|
||||
"""
|
||||
try:
|
||||
from ..features.profiler import (
|
||||
PrewarmManager,
|
||||
get_lite_mode_config,
|
||||
get_provider_cache,
|
||||
)
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Profiler module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if show_config:
|
||||
typer.echo("=" * 50)
|
||||
typer.echo("Performance Optimization Status")
|
||||
typer.echo("=" * 50)
|
||||
|
||||
lite_config = get_lite_mode_config()
|
||||
typer.echo(f"\nLite Mode: {'Enabled' if lite_config.enabled else 'Disabled'}")
|
||||
if lite_config.enabled:
|
||||
typer.echo(f" Skip Type Validation: {lite_config.skip_type_validation}")
|
||||
typer.echo(f" Skip Model Validation: {lite_config.skip_model_validation}")
|
||||
typer.echo(f" Minimal Imports: {lite_config.minimal_imports}")
|
||||
|
||||
typer.echo(f"\nPre-warming: {'Enabled' if PrewarmManager.is_enabled() else 'Disabled'}")
|
||||
|
||||
cache = get_provider_cache()
|
||||
stats = cache.get_stats()
|
||||
typer.echo("\nProvider Cache:")
|
||||
typer.echo(f" Hits: {stats['hits']}")
|
||||
typer.echo(f" Misses: {stats['misses']}")
|
||||
typer.echo(f" Size: {stats['size']}")
|
||||
|
||||
typer.echo("\n" + "=" * 50)
|
||||
typer.echo("\nTo enable optimizations, set environment variables:")
|
||||
typer.echo(" PRAISONAI_LITE_MODE=1")
|
||||
typer.echo(" PRAISONAI_SKIP_TYPE_VALIDATION=1")
|
||||
typer.echo(" PRAISONAI_MINIMAL_IMPORTS=1")
|
||||
return
|
||||
|
||||
if prewarm:
|
||||
typer.echo("🔥 Enabling provider pre-warming...")
|
||||
PrewarmManager.enable()
|
||||
|
||||
import os
|
||||
if os.environ.get('OPENAI_API_KEY'):
|
||||
typer.echo(" Pre-warming OpenAI...")
|
||||
PrewarmManager.prewarm_provider('openai')
|
||||
if os.environ.get('ANTHROPIC_API_KEY'):
|
||||
typer.echo(" Pre-warming Anthropic...")
|
||||
PrewarmManager.prewarm_provider('anthropic')
|
||||
|
||||
typer.echo("✅ Pre-warming initiated (runs in background)")
|
||||
|
||||
if lite:
|
||||
typer.echo("⚡ Lite mode configuration:")
|
||||
typer.echo(" Set these environment variables to enable:")
|
||||
typer.echo(" export PRAISONAI_LITE_MODE=1")
|
||||
typer.echo(" export PRAISONAI_SKIP_TYPE_VALIDATION=1")
|
||||
typer.echo(" export PRAISONAI_MINIMAL_IMPORTS=1")
|
||||
|
||||
if not prewarm and not lite and not show_config:
|
||||
typer.echo("Use --show to see current config, or --prewarm/--lite to enable optimizations")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def profile_callback(ctx: typer.Context):
|
||||
"""Show profile help if no subcommand."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
typer.echo(ctx.get_help())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Realtime command group for PraisonAI CLI.
|
||||
|
||||
Provides realtime interaction commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Realtime interaction mode")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def realtime_main(
|
||||
ctx: typer.Context,
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
):
|
||||
"""
|
||||
Start realtime interaction mode.
|
||||
|
||||
Examples:
|
||||
praisonai realtime
|
||||
praisonai realtime --model gpt-4o
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['realtime']
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Recipe command group for PraisonAI CLI.
|
||||
|
||||
Provides recipe management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Recipe management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def recipe_list():
|
||||
"""List available recipes."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['recipe', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("run")
|
||||
def recipe_run(
|
||||
name: str = typer.Argument(..., help="Recipe name"),
|
||||
model: str = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
):
|
||||
"""Run a recipe."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['recipe', 'run', name]
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("install")
|
||||
def recipe_install(
|
||||
source: str = typer.Argument(..., help="Recipe source (path or URL)"),
|
||||
):
|
||||
"""Install a recipe."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['recipe', 'install', source]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Registry command group for PraisonAI CLI.
|
||||
|
||||
Provides registry management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Registry management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def registry_list():
|
||||
"""List registry entries."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['registry', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("serve")
|
||||
def registry_serve(
|
||||
port: int = typer.Option(8080, "--port", "-p", help="Port to serve on"),
|
||||
):
|
||||
"""Start registry server."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['registry', 'serve', '--port', str(port)]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Research command group for PraisonAI CLI.
|
||||
|
||||
Provides research and analysis commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Research and analysis")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def research_main(
|
||||
ctx: typer.Context,
|
||||
query: str = typer.Argument(None, help="Research query"),
|
||||
model: str = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
output: str = typer.Option(None, "--output", "-o", help="Output file path"),
|
||||
):
|
||||
"""
|
||||
Run research and analysis tasks.
|
||||
|
||||
Examples:
|
||||
praisonai research "What are the latest AI trends?"
|
||||
praisonai research --model gpt-4o "Analyze market data"
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['research']
|
||||
if query:
|
||||
argv.append(query)
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
if output:
|
||||
argv.extend(['--output', output])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Rules command group for PraisonAI CLI.
|
||||
|
||||
Provides rules management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Rules management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def rules_list():
|
||||
"""List active rules."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['rules', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def rules_add(
|
||||
rule: str = typer.Argument(..., help="Rule to add"),
|
||||
):
|
||||
"""Add a rule."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['rules', 'add', rule]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("clear")
|
||||
def rules_clear():
|
||||
"""Clear all rules."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['rules', 'clear']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Run command group for PraisonAI CLI.
|
||||
|
||||
Provides agent execution commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..state.identifiers import get_current_context
|
||||
|
||||
app = typer.Typer(help="Run agents")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def run_main(
|
||||
ctx: typer.Context,
|
||||
target: Optional[str] = typer.Argument(None, help="Agent file or prompt"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Framework: praisonai, crewai, autogen"),
|
||||
interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
stream: bool = typer.Option(True, "--stream/--no-stream", help="Stream output"),
|
||||
trace: bool = typer.Option(False, "--trace", help="Enable tracing"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
|
||||
max_tokens: int = typer.Option(16000, "--max-tokens", help="Maximum output tokens"),
|
||||
):
|
||||
"""
|
||||
Run agents from a file or prompt.
|
||||
|
||||
Examples:
|
||||
praisonai run agents.yaml
|
||||
praisonai run "What is the weather?"
|
||||
praisonai run agents.yaml --interactive
|
||||
"""
|
||||
output = get_output_controller()
|
||||
context = get_current_context()
|
||||
|
||||
if not target:
|
||||
output.print_panel(
|
||||
"Run agents from a file or prompt.\n\n"
|
||||
"Usage:\n"
|
||||
" praisonai run agents.yaml\n"
|
||||
" praisonai run \"What is the weather?\"\n"
|
||||
" praisonai run agents.yaml --interactive\n\n"
|
||||
"Options:\n"
|
||||
" --model, -m LLM model to use\n"
|
||||
" --framework, -f Framework (praisonai, crewai, autogen)\n"
|
||||
" --interactive, -i Interactive mode\n"
|
||||
" --verbose, -v Verbose output\n"
|
||||
" --trace Enable tracing\n"
|
||||
" --memory Enable memory",
|
||||
title="Run Command"
|
||||
)
|
||||
return
|
||||
|
||||
# Emit start event
|
||||
output.emit_start(
|
||||
message=f"Starting run: {target[:50]}..." if len(target) > 50 else f"Starting run: {target}",
|
||||
data={
|
||||
"target": target,
|
||||
"model": model,
|
||||
"framework": framework,
|
||||
}
|
||||
)
|
||||
|
||||
# Check if target is a file or prompt
|
||||
import os
|
||||
is_file = os.path.exists(target) and (target.endswith('.yaml') or target.endswith('.yml'))
|
||||
|
||||
if is_file:
|
||||
# Run from file
|
||||
_run_from_file(
|
||||
target,
|
||||
model=model,
|
||||
framework=framework,
|
||||
interactive=interactive,
|
||||
verbose=verbose,
|
||||
stream=stream,
|
||||
trace=trace,
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
else:
|
||||
# Run as prompt
|
||||
_run_prompt(
|
||||
target,
|
||||
model=model,
|
||||
verbose=verbose,
|
||||
stream=stream,
|
||||
trace=trace,
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
||||
|
||||
def _run_from_file(
|
||||
file_path: str,
|
||||
model: Optional[str] = None,
|
||||
framework: Optional[str] = None,
|
||||
interactive: bool = False,
|
||||
verbose: bool = False,
|
||||
stream: bool = True,
|
||||
trace: bool = False,
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
):
|
||||
"""Run agents from a YAML file."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
# Use existing PraisonAI class
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
praison = PraisonAI(
|
||||
agent_file=file_path,
|
||||
framework=framework or "praisonai",
|
||||
)
|
||||
|
||||
# Set model if provided
|
||||
if model:
|
||||
praison.config_list[0]['model'] = model
|
||||
|
||||
# Run
|
||||
result = praison.run()
|
||||
|
||||
output.emit_result(
|
||||
message="Run completed",
|
||||
data={"result": str(result) if result else None}
|
||||
)
|
||||
|
||||
if result:
|
||||
if not output.is_json_mode:
|
||||
output.print_success("Run completed")
|
||||
|
||||
except Exception as e:
|
||||
output.emit_error(message=str(e))
|
||||
output.print_error(str(e))
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _run_prompt(
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
stream: bool = True,
|
||||
trace: bool = False,
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
):
|
||||
"""Run a direct prompt."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
# Use existing handle_direct_prompt
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
|
||||
if model:
|
||||
praison.config_list[0]['model'] = model
|
||||
|
||||
# Create args-like object for handle_direct_prompt
|
||||
class Args:
|
||||
pass
|
||||
|
||||
args = Args()
|
||||
args.llm = model
|
||||
args.verbose = verbose
|
||||
args.memory = memory
|
||||
args.tools = tools
|
||||
args.max_tokens = max_tokens
|
||||
args.web_search = False
|
||||
args.web_fetch = False
|
||||
args.prompt_caching = False
|
||||
args.planning = False
|
||||
args.planning_tools = None
|
||||
args.planning_reasoning = False
|
||||
args.auto_approve_plan = False
|
||||
args.final_agent = None
|
||||
args.user_id = None
|
||||
args.auto_save = None
|
||||
args.history = None
|
||||
args.include_rules = None
|
||||
args.workflow = None
|
||||
args.workflow_var = None
|
||||
args.claude_memory = False
|
||||
args.guardrail = None
|
||||
args.metrics = False
|
||||
args.image = None
|
||||
args.image_generate = False
|
||||
args.telemetry = False
|
||||
args.mcp = None
|
||||
args.mcp_env = None
|
||||
args.fast_context = None
|
||||
args.handoff = None
|
||||
args.auto_memory = False
|
||||
args.todo = False
|
||||
args.router = False
|
||||
args.router_provider = None
|
||||
args.query_rewrite = False
|
||||
args.rewrite_tools = None
|
||||
args.expand_prompt = False
|
||||
args.expand_tools = None
|
||||
args.no_tools = False
|
||||
|
||||
praison.args = args
|
||||
result = praison.handle_direct_prompt(prompt)
|
||||
|
||||
output.emit_result(
|
||||
message="Prompt completed",
|
||||
data={"result": str(result) if result else None}
|
||||
)
|
||||
|
||||
if result and not output.is_json_mode:
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
output.emit_error(message=str(e))
|
||||
output.print_error(str(e))
|
||||
raise typer.Exit(1)
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Schedule command group for PraisonAI CLI.
|
||||
|
||||
Provides scheduler management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Scheduler management")
|
||||
|
||||
|
||||
def _run_schedule(args: list) -> int:
|
||||
"""Run schedule command with args."""
|
||||
try:
|
||||
from ..features.agent_scheduler import AgentSchedulerHandler
|
||||
|
||||
# Parse subcommand
|
||||
if args and args[0] in ['start', 'list', 'stop', 'logs', 'restart', 'delete', 'describe', 'save', 'stop-all', 'stats']:
|
||||
subcommand = args[0]
|
||||
remaining = args[1:] if len(args) > 1 else []
|
||||
|
||||
# Create minimal args namespace
|
||||
class Args:
|
||||
pass
|
||||
|
||||
fake_args = Args()
|
||||
return AgentSchedulerHandler.handle_daemon_command(subcommand, fake_args, remaining)
|
||||
|
||||
return 0
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Scheduler module not available: {e}")
|
||||
return 4
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def schedule_start(
|
||||
agents_file: str = typer.Argument("agents.yaml", help="Agents YAML file"),
|
||||
interval: Optional[str] = typer.Option(None, "--interval", "-i", help="Schedule interval (e.g., 'hourly', '*/30m')"),
|
||||
daemon: bool = typer.Option(True, "--daemon/--no-daemon", help="Run as daemon"),
|
||||
name: Optional[str] = typer.Option(None, "--name", "-n", help="Job name"),
|
||||
):
|
||||
"""Start scheduled agent execution."""
|
||||
args = ["start", agents_file]
|
||||
if interval:
|
||||
args.extend(["--interval", interval])
|
||||
if not daemon:
|
||||
args.append("--no-daemon")
|
||||
if name:
|
||||
args.extend(["--name", name])
|
||||
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def schedule_stop(
|
||||
job_id: Optional[str] = typer.Argument(None, help="Job ID to stop (or 'all')"),
|
||||
):
|
||||
"""Stop scheduled job(s)."""
|
||||
if job_id == "all":
|
||||
raise typer.Exit(_run_schedule(["stop-all"]))
|
||||
elif job_id:
|
||||
raise typer.Exit(_run_schedule(["stop", job_id]))
|
||||
else:
|
||||
raise typer.Exit(_run_schedule(["stop"]))
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def schedule_list(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List scheduled jobs."""
|
||||
args = ["list"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("logs")
|
||||
def schedule_logs(
|
||||
job_id: Optional[str] = typer.Argument(None, help="Job ID"),
|
||||
tail: int = typer.Option(50, "--tail", "-n", help="Number of lines"),
|
||||
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
|
||||
):
|
||||
"""View scheduler logs."""
|
||||
args = ["logs"]
|
||||
if job_id:
|
||||
args.append(job_id)
|
||||
args.extend(["--tail", str(tail)])
|
||||
if follow:
|
||||
args.append("--follow")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("restart")
|
||||
def schedule_restart(
|
||||
job_id: str = typer.Argument(..., help="Job ID to restart"),
|
||||
):
|
||||
"""Restart a scheduled job."""
|
||||
raise typer.Exit(_run_schedule(["restart", job_id]))
|
||||
|
||||
|
||||
@app.command("delete")
|
||||
def schedule_delete(
|
||||
job_id: str = typer.Argument(..., help="Job ID to delete"),
|
||||
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
||||
):
|
||||
"""Delete a scheduled job."""
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Delete job {job_id}?")
|
||||
if not confirmed:
|
||||
output = get_output_controller()
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
raise typer.Exit(_run_schedule(["delete", job_id]))
|
||||
|
||||
|
||||
@app.command("describe")
|
||||
def schedule_describe(
|
||||
job_id: str = typer.Argument(..., help="Job ID"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show job details."""
|
||||
args = ["describe", job_id]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("stats")
|
||||
def schedule_stats(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show scheduler statistics."""
|
||||
args = ["stats"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def schedule_callback(ctx: typer.Context):
|
||||
"""Show schedule help or list jobs."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
schedule_list(json_output=False)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Serve command group for PraisonAI CLI.
|
||||
|
||||
Provides API server management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="API server management")
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def serve_start(
|
||||
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
||||
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
||||
agents_file: Optional[str] = typer.Option(None, "--agents", "-a", help="Agents YAML file"),
|
||||
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
|
||||
workers: int = typer.Option(1, "--workers", "-w", help="Number of workers"),
|
||||
):
|
||||
"""Start API server."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"start",
|
||||
"--host", host,
|
||||
"--port", str(port),
|
||||
"--workers", str(workers),
|
||||
]
|
||||
|
||||
if agents_file:
|
||||
args.extend(["--agents", agents_file])
|
||||
if reload:
|
||||
args.append("--reload")
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def serve_stop():
|
||||
"""Stop API server."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(["stop"])
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def serve_status(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show server status."""
|
||||
output = get_output_controller()
|
||||
|
||||
args = ["status"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def serve_callback(ctx: typer.Context):
|
||||
"""Show serve help or start server."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"API server management.\n\n"
|
||||
"Commands:\n"
|
||||
" start Start API server\n"
|
||||
" stop Stop API server\n"
|
||||
" status Show server status\n\n"
|
||||
"Quick start:\n"
|
||||
" praisonai serve start --agents agents.yaml",
|
||||
title="Serve Commands"
|
||||
)
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Session command group for PraisonAI CLI.
|
||||
|
||||
Provides session management:
|
||||
- session list: List all sessions
|
||||
- session resume: Resume a session
|
||||
- session delete: Delete a session
|
||||
- session export: Export a session
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..state.sessions import get_session_manager
|
||||
|
||||
app = typer.Typer(help="Session management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def session_list(
|
||||
limit: int = typer.Option(
|
||||
20,
|
||||
"--limit",
|
||||
"-n",
|
||||
help="Maximum number of sessions to show",
|
||||
),
|
||||
):
|
||||
"""List all sessions."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
sessions = manager.list(limit=limit)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"sessions": [s.to_dict() for s in sessions]
|
||||
})
|
||||
return
|
||||
|
||||
if not sessions:
|
||||
output.print_info("No sessions found")
|
||||
return
|
||||
|
||||
headers = ["ID", "Name", "Status", "Events", "Updated"]
|
||||
rows = []
|
||||
for s in sessions:
|
||||
rows.append([
|
||||
s.session_id[:20] + "..." if len(s.session_id) > 20 else s.session_id,
|
||||
s.name or "-",
|
||||
s.status,
|
||||
str(s.event_count),
|
||||
s.updated_at.strftime("%Y-%m-%d %H:%M"),
|
||||
])
|
||||
|
||||
output.print_table(headers, rows, title="Sessions")
|
||||
|
||||
|
||||
@app.command("resume")
|
||||
def session_resume(
|
||||
session_id: str = typer.Argument(..., help="Session ID to resume"),
|
||||
):
|
||||
"""Resume a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(
|
||||
f"Session not found: {session_id}",
|
||||
remediation="Use 'praisonai session list' to see available sessions"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Load session events
|
||||
events = manager.get_events(session_id)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"session": session.to_dict(),
|
||||
"events": events,
|
||||
"message": "Session loaded. True resume not supported - showing transcript.",
|
||||
})
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"Session: {session.name or session.session_id}\n"
|
||||
f"Run ID: {session.run_id}\n"
|
||||
f"Trace ID: {session.trace_id}\n"
|
||||
f"Events: {session.event_count}\n"
|
||||
f"Status: {session.status}",
|
||||
title="Session Loaded"
|
||||
)
|
||||
|
||||
output.print_warning(
|
||||
"True session resume is not yet supported. "
|
||||
"Showing session transcript instead."
|
||||
)
|
||||
|
||||
# Show recent events
|
||||
if events:
|
||||
output.print("\n--- Recent Events ---\n")
|
||||
for event in events[-10:]:
|
||||
event_type = event.get("event", "unknown")
|
||||
message = event.get("message", "")
|
||||
output.print(f"[{event_type}] {message}")
|
||||
|
||||
|
||||
@app.command("delete")
|
||||
def session_delete(
|
||||
session_id: str = typer.Argument(..., help="Session ID to delete"),
|
||||
confirm: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation",
|
||||
),
|
||||
):
|
||||
"""Delete a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Delete session {session_id}?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
deleted = manager.delete(session_id)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"deleted": deleted, "session_id": session_id})
|
||||
else:
|
||||
if deleted:
|
||||
output.print_success(f"Deleted session: {session_id}")
|
||||
else:
|
||||
output.print_error(f"Failed to delete session: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def session_export(
|
||||
session_id: str = typer.Argument(..., help="Session ID to export"),
|
||||
format: str = typer.Option(
|
||||
"md",
|
||||
"--format",
|
||||
"-f",
|
||||
help="Export format: md or json",
|
||||
),
|
||||
output_file: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
help="Output file path",
|
||||
),
|
||||
):
|
||||
"""Export a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
content = manager.export(session_id, format=format)
|
||||
|
||||
if content is None:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output_file:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
output.print_success(f"Exported to: {output_file}")
|
||||
else:
|
||||
print(content)
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def session_show(
|
||||
session_id: str = typer.Argument(..., help="Session ID to show"),
|
||||
):
|
||||
"""Show session details."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(session.to_dict())
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"Session ID: {session.session_id}\n"
|
||||
f"Name: {session.name or '-'}\n"
|
||||
f"Run ID: {session.run_id}\n"
|
||||
f"Trace ID: {session.trace_id}\n"
|
||||
f"Created: {session.created_at.isoformat()}\n"
|
||||
f"Updated: {session.updated_at.isoformat()}\n"
|
||||
f"Status: {session.status}\n"
|
||||
f"Events: {session.event_count}\n"
|
||||
f"Workspace: {session.workspace or '-'}",
|
||||
title="Session Details"
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Skills command group for PraisonAI CLI.
|
||||
|
||||
Provides skill management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Skill management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def skills_list(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed info"),
|
||||
):
|
||||
"""List available skills."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['skills', 'list']
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("validate")
|
||||
def skills_validate(
|
||||
path: str = typer.Argument(..., help="Skill directory path"),
|
||||
):
|
||||
"""Validate a skill."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['skills', 'validate', path]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("create")
|
||||
def skills_create(
|
||||
name: str = typer.Argument(..., help="Skill name"),
|
||||
):
|
||||
"""Create a new skill."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['skills', 'create', name]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("install")
|
||||
def skills_install(
|
||||
source: str = typer.Argument(..., help="Skill source (path or URL)"),
|
||||
):
|
||||
"""Install a skill."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['skills', 'install', source]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Templates command group for PraisonAI CLI.
|
||||
|
||||
Provides template management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Template management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def templates_list():
|
||||
"""List available templates."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['templates', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("create")
|
||||
def templates_create(
|
||||
name: str = typer.Argument(..., help="Template name"),
|
||||
source: str = typer.Option(None, "--source", "-s", help="Source file to create template from"),
|
||||
):
|
||||
"""Create a new template."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['templates', 'create', name]
|
||||
if source:
|
||||
argv.extend(['--source', source])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Todo command group for PraisonAI CLI.
|
||||
|
||||
Provides todo/task management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Todo/task management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def todo_list():
|
||||
"""List todos."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['todo', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def todo_add(
|
||||
task: str = typer.Argument(..., help="Task description"),
|
||||
priority: str = typer.Option("medium", "--priority", "-p", help="Priority (low, medium, high)"),
|
||||
):
|
||||
"""Add a todo."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['todo', 'add', task]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("done")
|
||||
def todo_done(
|
||||
task_id: str = typer.Argument(..., help="Task ID to mark as done"),
|
||||
):
|
||||
"""Mark a todo as done."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['todo', 'done', task_id]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Tools command group for PraisonAI CLI.
|
||||
|
||||
Provides tool management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Tool management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def tools_list(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed info"),
|
||||
):
|
||||
"""List available tools."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['tools', 'list']
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("info")
|
||||
def tools_info(
|
||||
name: str = typer.Argument(..., help="Tool name"),
|
||||
):
|
||||
"""Show tool information."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['tools', 'info', name]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("test")
|
||||
def tools_test(
|
||||
name: str = typer.Argument(..., help="Tool name to test"),
|
||||
):
|
||||
"""Test a tool."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['tools', 'test', name]
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Traces command group for PraisonAI CLI.
|
||||
|
||||
Provides trace collection management:
|
||||
- traces enable: Enable trace collection
|
||||
- traces disable: Disable trace collection
|
||||
- traces status: Show current status
|
||||
- traces list: List recent traces
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..configuration.loader import get_config_loader
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Trace collection management")
|
||||
|
||||
|
||||
@app.command("enable")
|
||||
def traces_enable(
|
||||
endpoint: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--endpoint",
|
||||
"-e",
|
||||
help="Trace endpoint URL",
|
||||
),
|
||||
sample_rate: float = typer.Option(
|
||||
1.0,
|
||||
"--sample-rate",
|
||||
"-r",
|
||||
help="Sample rate (0.0 to 1.0)",
|
||||
),
|
||||
):
|
||||
"""Enable trace collection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
loader.set("traces.enabled", True)
|
||||
if endpoint:
|
||||
loader.set("traces.endpoint", endpoint)
|
||||
if sample_rate != 1.0:
|
||||
loader.set("traces.sample_rate", sample_rate)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"traces_enabled": True,
|
||||
"endpoint": endpoint,
|
||||
"sample_rate": sample_rate,
|
||||
})
|
||||
else:
|
||||
output.print_success("Trace collection enabled")
|
||||
if endpoint:
|
||||
output.print(f" Endpoint: {endpoint}")
|
||||
output.print(f" Sample rate: {sample_rate}")
|
||||
|
||||
|
||||
@app.command("disable")
|
||||
def traces_disable():
|
||||
"""Disable trace collection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
loader.set("traces.enabled", False)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces_enabled": False})
|
||||
else:
|
||||
output.print_success("Trace collection disabled")
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def traces_status():
|
||||
"""Show trace collection status."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
enabled = loader.get("traces.enabled", False)
|
||||
endpoint = loader.get("traces.endpoint")
|
||||
sample_rate = loader.get("traces.sample_rate", 1.0)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"enabled": enabled,
|
||||
"endpoint": endpoint,
|
||||
"sample_rate": sample_rate,
|
||||
})
|
||||
else:
|
||||
status = "✅ Enabled" if enabled else "❌ Disabled"
|
||||
output.print_panel(
|
||||
f"Status: {status}\n"
|
||||
f"Endpoint: {endpoint or 'Not configured'}\n"
|
||||
f"Sample Rate: {sample_rate}",
|
||||
title="Trace Collection Status"
|
||||
)
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def traces_list(
|
||||
limit: int = typer.Option(
|
||||
20,
|
||||
"--limit",
|
||||
"-n",
|
||||
help="Maximum number of traces to show",
|
||||
),
|
||||
):
|
||||
"""List recent traces."""
|
||||
output = get_output_controller()
|
||||
|
||||
from ..configuration.paths import get_traces_dir
|
||||
|
||||
traces_dir = get_traces_dir()
|
||||
|
||||
if not traces_dir.exists():
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces": []})
|
||||
else:
|
||||
output.print_info("No traces found")
|
||||
return
|
||||
|
||||
traces = []
|
||||
for trace_file in sorted(traces_dir.iterdir(), reverse=True)[:limit]:
|
||||
if trace_file.is_file() and trace_file.suffix == ".json":
|
||||
traces.append({
|
||||
"name": trace_file.stem,
|
||||
"size": trace_file.stat().st_size,
|
||||
"modified": trace_file.stat().st_mtime,
|
||||
})
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces": traces})
|
||||
else:
|
||||
if not traces:
|
||||
output.print_info("No traces found")
|
||||
return
|
||||
|
||||
headers = ["Name", "Size", "Modified"]
|
||||
rows = []
|
||||
for t in traces:
|
||||
from datetime import datetime
|
||||
modified = datetime.fromtimestamp(t["modified"]).strftime("%Y-%m-%d %H:%M")
|
||||
rows.append([t["name"], f"{t['size']} bytes", modified])
|
||||
|
||||
output.print_table(headers, rows, title="Recent Traces")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Train command group for PraisonAI CLI.
|
||||
|
||||
Provides model training commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Model training and fine-tuning")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def train_main(
|
||||
ctx: typer.Context,
|
||||
dataset: Optional[str] = typer.Argument(None, help="Training dataset path"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="Base model to fine-tune"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
):
|
||||
"""
|
||||
Train or fine-tune models.
|
||||
|
||||
Examples:
|
||||
praisonai train dataset.json
|
||||
praisonai train --model gpt-4o dataset.json
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['train']
|
||||
if dataset:
|
||||
argv.append(dataset)
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
UI command group for PraisonAI CLI.
|
||||
|
||||
Provides web UI commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Web UI management")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def ui_main(
|
||||
ctx: typer.Context,
|
||||
port: int = typer.Option(8080, "--port", "-p", help="Port to run UI on"),
|
||||
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
|
||||
public: bool = typer.Option(False, "--public", help="Make UI publicly accessible"),
|
||||
):
|
||||
"""
|
||||
Start the web UI.
|
||||
|
||||
Examples:
|
||||
praisonai ui
|
||||
praisonai ui --port 3000
|
||||
praisonai ui --public
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['ui']
|
||||
argv.extend(['--port', str(port)])
|
||||
argv.extend(['--host', host])
|
||||
if public:
|
||||
argv.append('--public')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Version command group for PraisonAI CLI.
|
||||
|
||||
Provides version information:
|
||||
- version show: Show version
|
||||
- version check: Check for updates
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Version information")
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def version_show():
|
||||
"""Show version information."""
|
||||
output = get_output_controller()
|
||||
|
||||
from praisonai.version import __version__
|
||||
|
||||
# Try to get additional version info
|
||||
versions = {
|
||||
"praisonai": __version__,
|
||||
}
|
||||
|
||||
# Check praisonaiagents version
|
||||
try:
|
||||
import praisonaiagents
|
||||
versions["praisonaiagents"] = getattr(praisonaiagents, "__version__", "unknown")
|
||||
except ImportError:
|
||||
versions["praisonaiagents"] = "not installed"
|
||||
|
||||
# Check Python version
|
||||
import sys
|
||||
versions["python"] = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(versions)
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"PraisonAI: {versions['praisonai']}\n"
|
||||
f"PraisonAI Agents: {versions['praisonaiagents']}\n"
|
||||
f"Python: {versions['python']}",
|
||||
title="Version Information"
|
||||
)
|
||||
|
||||
|
||||
@app.command("check")
|
||||
def version_check():
|
||||
"""Check for updates."""
|
||||
output = get_output_controller()
|
||||
|
||||
from praisonai.version import __version__
|
||||
|
||||
output.print_info("Checking for updates...")
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
url = "https://pypi.org/pypi/praisonai/json"
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
latest = data["info"]["version"]
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"current": __version__,
|
||||
"latest": latest,
|
||||
"update_available": latest != __version__,
|
||||
})
|
||||
return
|
||||
|
||||
if latest != __version__:
|
||||
output.print_warning(
|
||||
f"Update available: {__version__} → {latest}\n"
|
||||
f"Run: pip install --upgrade praisonai"
|
||||
)
|
||||
else:
|
||||
output.print_success(f"You are using the latest version ({__version__})")
|
||||
|
||||
except Exception as e:
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"current": __version__,
|
||||
"latest": None,
|
||||
"error": str(e),
|
||||
})
|
||||
else:
|
||||
output.print_error(f"Failed to check for updates: {e}")
|
||||
output.print(f"Current version: {__version__}")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def version_callback(ctx: typer.Context):
|
||||
"""Show version (default behavior)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
version_show()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Workflow command group for PraisonAI CLI.
|
||||
|
||||
Provides workflow management commands.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Workflow management")
|
||||
|
||||
|
||||
@app.command("run")
|
||||
def workflow_run(
|
||||
file: str = typer.Argument(..., help="Workflow file path"),
|
||||
model: str = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
):
|
||||
"""Run a workflow."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['workflow', 'run', file]
|
||||
if model:
|
||||
argv.extend(['--model', model])
|
||||
if verbose:
|
||||
argv.append('--verbose')
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def workflow_list():
|
||||
"""List available workflows."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['workflow', 'list']
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("create")
|
||||
def workflow_create(
|
||||
name: str = typer.Argument(..., help="Workflow name"),
|
||||
template: str = typer.Option(None, "--template", "-t", help="Template to use"),
|
||||
):
|
||||
"""Create a new workflow."""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import sys
|
||||
|
||||
argv = ['workflow', 'create', name]
|
||||
if template:
|
||||
argv.extend(['--template', template])
|
||||
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
praison.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
PraisonAI CLI Configuration Module.
|
||||
|
||||
Provides configuration management with TOML support and precedence handling.
|
||||
"""
|
||||
|
||||
from .loader import ConfigLoader, get_config
|
||||
from .schema import ConfigSchema
|
||||
from .paths import get_config_paths, get_user_config_path, get_project_config_path
|
||||
|
||||
__all__ = [
|
||||
'ConfigLoader',
|
||||
'ConfigSchema',
|
||||
'get_config',
|
||||
'get_config_paths',
|
||||
'get_user_config_path',
|
||||
'get_project_config_path',
|
||||
]
|
||||
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Configuration loader for PraisonAI CLI.
|
||||
|
||||
Handles loading, merging, and saving configuration with precedence.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .paths import (
|
||||
get_config_paths,
|
||||
get_user_config_path,
|
||||
get_project_config_path,
|
||||
get_env_prefix,
|
||||
ensure_config_dirs,
|
||||
)
|
||||
from .schema import ConfigSchema, DEFAULT_CONFIG
|
||||
|
||||
|
||||
# Thread-safe config cache
|
||||
_config_lock = threading.Lock()
|
||||
_config_cache: Optional[ConfigSchema] = None
|
||||
|
||||
|
||||
def _load_toml(path: Path) -> Dict[str, Any]:
|
||||
"""Load a TOML file."""
|
||||
try:
|
||||
# Try tomllib (Python 3.11+) first
|
||||
import tomllib
|
||||
with open(path, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Fall back to tomli
|
||||
import tomli
|
||||
with open(path, "rb") as f:
|
||||
return tomli.load(f)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Last resort: basic TOML parsing
|
||||
return _basic_toml_parse(path)
|
||||
|
||||
|
||||
def _basic_toml_parse(path: Path) -> Dict[str, Any]:
|
||||
"""Basic TOML parser for simple configs."""
|
||||
result: Dict[str, Any] = {}
|
||||
current_section: Optional[str] = None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
current_section = line[1:-1]
|
||||
if current_section not in result:
|
||||
result[current_section] = {}
|
||||
elif "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
elif value.startswith('"') and value.endswith('"'):
|
||||
parsed_value = value[1:-1]
|
||||
elif value.startswith("'") and value.endswith("'"):
|
||||
parsed_value = value[1:-1]
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
if current_section:
|
||||
result[current_section][key] = parsed_value
|
||||
else:
|
||||
result[key] = parsed_value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _save_toml(path: Path, data: Dict[str, Any]) -> None:
|
||||
"""Save data to a TOML file."""
|
||||
try:
|
||||
import tomli_w
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Fall back to basic TOML writing
|
||||
_basic_toml_write(path, data)
|
||||
|
||||
|
||||
def _basic_toml_write(path: Path, data: Dict[str, Any]) -> None:
|
||||
"""Basic TOML writer."""
|
||||
lines = []
|
||||
|
||||
# Write top-level keys first
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, dict):
|
||||
lines.append(f"{key} = {_format_toml_value(value)}")
|
||||
|
||||
# Write sections
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f"\n[{key}]")
|
||||
for sub_key, sub_value in value.items():
|
||||
if not isinstance(sub_value, dict):
|
||||
lines.append(f"{sub_key} = {_format_toml_value(sub_value)}")
|
||||
else:
|
||||
# Nested section
|
||||
lines.append(f"\n[{key}.{sub_key}]")
|
||||
for nested_key, nested_value in sub_value.items():
|
||||
lines.append(f"{nested_key} = {_format_toml_value(nested_value)}")
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def _format_toml_value(value: Any) -> str:
|
||||
"""Format a value for TOML."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
elif isinstance(value, str):
|
||||
return f'"{value}"'
|
||||
elif isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
elif isinstance(value, list):
|
||||
items = ", ".join(_format_toml_value(v) for v in value)
|
||||
return f"[{items}]"
|
||||
else:
|
||||
return f'"{value}"'
|
||||
|
||||
|
||||
def _merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Deep merge two dictionaries."""
|
||||
result = base.copy()
|
||||
for key, value in override.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = _merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _get_env_overrides() -> Dict[str, Any]:
|
||||
"""Get configuration overrides from environment variables."""
|
||||
prefix = get_env_prefix()
|
||||
overrides: Dict[str, Any] = {}
|
||||
|
||||
for key, value in os.environ.items():
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
# Convert PRAISONAI_OUTPUT_FORMAT to output.format
|
||||
config_key = key[len(prefix):].lower()
|
||||
parts = config_key.split("_")
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
# Build nested dict
|
||||
if len(parts) >= 2:
|
||||
section = parts[0]
|
||||
sub_key = "_".join(parts[1:])
|
||||
if section not in overrides:
|
||||
overrides[section] = {}
|
||||
overrides[section][sub_key] = parsed_value
|
||||
else:
|
||||
overrides[config_key] = parsed_value
|
||||
|
||||
return overrides
|
||||
|
||||
|
||||
def _get_dotted_value(data: Dict[str, Any], key: str) -> Any:
|
||||
"""Get a value using dotted key notation."""
|
||||
parts = key.split(".")
|
||||
current = data
|
||||
for part in parts:
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
def _set_dotted_value(data: Dict[str, Any], key: str, value: Any) -> None:
|
||||
"""Set a value using dotted key notation."""
|
||||
parts = key.split(".")
|
||||
current = data
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""
|
||||
Configuration loader with precedence handling.
|
||||
|
||||
Precedence (highest to lowest):
|
||||
1. CLI flags (not handled here)
|
||||
2. Environment variables (PRAISONAI_*)
|
||||
3. Project config (.praison/config.toml)
|
||||
4. User config (~/.praison/config.toml)
|
||||
5. Defaults
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self._config: Optional[ConfigSchema] = None
|
||||
self._raw_config: Dict[str, Any] = {}
|
||||
|
||||
def load(self, force_reload: bool = False) -> ConfigSchema:
|
||||
"""Load configuration with precedence."""
|
||||
if self._config is not None and not force_reload:
|
||||
return self._config
|
||||
|
||||
# Start with defaults
|
||||
merged = DEFAULT_CONFIG.to_dict()
|
||||
|
||||
# Load config files in reverse precedence order
|
||||
config_paths = get_config_paths(self.project_root)
|
||||
for path in reversed(config_paths):
|
||||
try:
|
||||
file_config = _load_toml(path)
|
||||
merged = _merge_dicts(merged, file_config)
|
||||
except Exception:
|
||||
pass # Skip invalid files
|
||||
|
||||
# Apply environment overrides
|
||||
env_overrides = _get_env_overrides()
|
||||
merged = _merge_dicts(merged, env_overrides)
|
||||
|
||||
self._raw_config = merged
|
||||
self._config = ConfigSchema.from_dict(merged)
|
||||
return self._config
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a configuration value using dotted key notation."""
|
||||
self.load()
|
||||
value = _get_dotted_value(self._raw_config, key)
|
||||
return value if value is not None else default
|
||||
|
||||
def set(self, key: str, value: Any, scope: str = "user") -> None:
|
||||
"""
|
||||
Set a configuration value.
|
||||
|
||||
Args:
|
||||
key: Dotted key notation (e.g., "output.format")
|
||||
value: Value to set
|
||||
scope: "user" or "project"
|
||||
"""
|
||||
if scope == "project":
|
||||
config_path = get_project_config_path(self.project_root)
|
||||
else:
|
||||
config_path = get_user_config_path()
|
||||
|
||||
# Load existing config or create empty
|
||||
if config_path.exists():
|
||||
try:
|
||||
existing = _load_toml(config_path)
|
||||
except Exception:
|
||||
existing = {}
|
||||
else:
|
||||
existing = {}
|
||||
|
||||
# Set the value
|
||||
_set_dotted_value(existing, key, value)
|
||||
|
||||
# Ensure directory exists
|
||||
ensure_config_dirs()
|
||||
if scope == "project":
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save
|
||||
_save_toml(config_path, existing)
|
||||
|
||||
# Reload
|
||||
self.load(force_reload=True)
|
||||
|
||||
def reset(self, scope: str = "user") -> None:
|
||||
"""Reset configuration to defaults."""
|
||||
if scope == "project":
|
||||
config_path = get_project_config_path(self.project_root)
|
||||
else:
|
||||
config_path = get_user_config_path()
|
||||
|
||||
if config_path.exists():
|
||||
config_path.unlink()
|
||||
|
||||
self.load(force_reload=True)
|
||||
|
||||
def list_all(self) -> Dict[str, Any]:
|
||||
"""List all configuration values."""
|
||||
self.load()
|
||||
return self._raw_config.copy()
|
||||
|
||||
|
||||
# Global config loader
|
||||
_global_loader: Optional[ConfigLoader] = None
|
||||
|
||||
|
||||
def get_config(project_root: Optional[Path] = None) -> ConfigSchema:
|
||||
"""Get the global configuration."""
|
||||
global _global_loader, _config_cache
|
||||
|
||||
with _config_lock:
|
||||
if _global_loader is None or project_root is not None:
|
||||
_global_loader = ConfigLoader(project_root)
|
||||
|
||||
if _config_cache is None:
|
||||
_config_cache = _global_loader.load()
|
||||
|
||||
return _config_cache
|
||||
|
||||
|
||||
def get_config_loader(project_root: Optional[Path] = None) -> ConfigLoader:
|
||||
"""Get the global config loader."""
|
||||
global _global_loader
|
||||
|
||||
with _config_lock:
|
||||
if _global_loader is None or project_root is not None:
|
||||
_global_loader = ConfigLoader(project_root)
|
||||
return _global_loader
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Configuration paths for PraisonAI CLI.
|
||||
|
||||
Defines standard locations for configuration files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def get_user_config_dir() -> Path:
|
||||
"""Get user configuration directory (~/.praison/)."""
|
||||
return Path.home() / ".praison"
|
||||
|
||||
|
||||
def get_user_config_path() -> Path:
|
||||
"""Get user configuration file path (~/.praison/config.toml)."""
|
||||
return get_user_config_dir() / "config.toml"
|
||||
|
||||
|
||||
def get_project_config_dir(project_root: Optional[Path] = None) -> Path:
|
||||
"""Get project configuration directory (.praison/)."""
|
||||
root = project_root or Path.cwd()
|
||||
return root / ".praison"
|
||||
|
||||
|
||||
def get_project_config_path(project_root: Optional[Path] = None) -> Path:
|
||||
"""Get project configuration file path (.praison/config.toml)."""
|
||||
return get_project_config_dir(project_root) / "config.toml"
|
||||
|
||||
|
||||
def get_sessions_dir() -> Path:
|
||||
"""Get sessions directory (~/.praison/sessions/)."""
|
||||
return get_user_config_dir() / "sessions"
|
||||
|
||||
|
||||
def get_traces_dir() -> Path:
|
||||
"""Get traces directory (~/.praison/traces/)."""
|
||||
return get_user_config_dir() / "traces"
|
||||
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
"""Get logs directory (~/.praison/logs/)."""
|
||||
return get_user_config_dir() / "logs"
|
||||
|
||||
|
||||
def get_cache_dir() -> Path:
|
||||
"""Get cache directory (~/.praison/cache/)."""
|
||||
return get_user_config_dir() / "cache"
|
||||
|
||||
|
||||
def get_config_paths(project_root: Optional[Path] = None) -> List[Path]:
|
||||
"""
|
||||
Get all configuration file paths in precedence order (highest first).
|
||||
|
||||
Precedence:
|
||||
1. Project config: .praison/config.toml
|
||||
2. User config: ~/.praison/config.toml
|
||||
|
||||
Returns:
|
||||
List of paths in precedence order
|
||||
"""
|
||||
paths = []
|
||||
|
||||
# Project config (highest precedence)
|
||||
project_config = get_project_config_path(project_root)
|
||||
if project_config.exists():
|
||||
paths.append(project_config)
|
||||
|
||||
# User config
|
||||
user_config = get_user_config_path()
|
||||
if user_config.exists():
|
||||
paths.append(user_config)
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def ensure_config_dirs() -> None:
|
||||
"""Ensure all configuration directories exist."""
|
||||
dirs = [
|
||||
get_user_config_dir(),
|
||||
get_sessions_dir(),
|
||||
get_traces_dir(),
|
||||
get_logs_dir(),
|
||||
get_cache_dir(),
|
||||
]
|
||||
for d in dirs:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_env_prefix() -> str:
|
||||
"""Get environment variable prefix."""
|
||||
return "PRAISONAI_"
|
||||
|
||||
|
||||
def env_to_config_key(env_var: str) -> Optional[str]:
|
||||
"""
|
||||
Convert environment variable name to config key.
|
||||
|
||||
Example: PRAISONAI_OUTPUT_FORMAT -> output.format
|
||||
"""
|
||||
prefix = get_env_prefix()
|
||||
if not env_var.startswith(prefix):
|
||||
return None
|
||||
|
||||
key = env_var[len(prefix):].lower()
|
||||
# Convert underscores to dots for nested keys
|
||||
# Single underscore = dot, double underscore = single underscore
|
||||
parts = key.split("__")
|
||||
result_parts = []
|
||||
for part in parts:
|
||||
result_parts.append(part.replace("_", "."))
|
||||
return "_".join(result_parts)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Configuration schema for PraisonAI CLI.
|
||||
|
||||
Defines the structure and defaults for configuration.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
"""Output configuration."""
|
||||
format: str = "text" # text, json, stream-json
|
||||
color: bool = True
|
||||
verbose: bool = False
|
||||
quiet: bool = False
|
||||
screen_reader: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TracesConfig:
|
||||
"""Traces configuration."""
|
||||
enabled: bool = False
|
||||
endpoint: Optional[str] = None
|
||||
sample_rate: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
"""MCP server configuration."""
|
||||
command: str = ""
|
||||
args: List[str] = field(default_factory=list)
|
||||
env: Dict[str, str] = field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPConfig:
|
||||
"""MCP configuration."""
|
||||
servers: Dict[str, MCPServerConfig] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelConfig:
|
||||
"""Model configuration."""
|
||||
default: str = "gpt-4o-mini"
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 16000
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
"""Session configuration."""
|
||||
auto_save: bool = False
|
||||
history_limit: int = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigSchema:
|
||||
"""
|
||||
Complete configuration schema.
|
||||
|
||||
Represents all configurable options for PraisonAI CLI.
|
||||
"""
|
||||
output: OutputConfig = field(default_factory=OutputConfig)
|
||||
traces: TracesConfig = field(default_factory=TracesConfig)
|
||||
mcp: MCPConfig = field(default_factory=MCPConfig)
|
||||
model: ModelConfig = field(default_factory=ModelConfig)
|
||||
session: SessionConfig = field(default_factory=SessionConfig)
|
||||
|
||||
# Additional settings stored as dict for flexibility
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"output": {
|
||||
"format": self.output.format,
|
||||
"color": self.output.color,
|
||||
"verbose": self.output.verbose,
|
||||
"quiet": self.output.quiet,
|
||||
"screen_reader": self.output.screen_reader,
|
||||
},
|
||||
"traces": {
|
||||
"enabled": self.traces.enabled,
|
||||
"endpoint": self.traces.endpoint,
|
||||
"sample_rate": self.traces.sample_rate,
|
||||
},
|
||||
"mcp": {
|
||||
"servers": {
|
||||
name: {
|
||||
"command": server.command,
|
||||
"args": server.args,
|
||||
"env": server.env,
|
||||
"enabled": server.enabled,
|
||||
}
|
||||
for name, server in self.mcp.servers.items()
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"default": self.model.default,
|
||||
"temperature": self.model.temperature,
|
||||
"max_tokens": self.model.max_tokens,
|
||||
},
|
||||
"session": {
|
||||
"auto_save": self.session.auto_save,
|
||||
"history_limit": self.session.history_limit,
|
||||
},
|
||||
**self.extra,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ConfigSchema":
|
||||
"""Create from dictionary."""
|
||||
output_data = data.get("output", {})
|
||||
traces_data = data.get("traces", {})
|
||||
mcp_data = data.get("mcp", {})
|
||||
model_data = data.get("model", {})
|
||||
session_data = data.get("session", {})
|
||||
|
||||
# Extract known keys
|
||||
known_keys = {"output", "traces", "mcp", "model", "session"}
|
||||
extra = {k: v for k, v in data.items() if k not in known_keys}
|
||||
|
||||
# Parse MCP servers
|
||||
mcp_servers = {}
|
||||
for name, server_data in mcp_data.get("servers", {}).items():
|
||||
mcp_servers[name] = MCPServerConfig(
|
||||
command=server_data.get("command", ""),
|
||||
args=server_data.get("args", []),
|
||||
env=server_data.get("env", {}),
|
||||
enabled=server_data.get("enabled", True),
|
||||
)
|
||||
|
||||
return cls(
|
||||
output=OutputConfig(
|
||||
format=output_data.get("format", "text"),
|
||||
color=output_data.get("color", True),
|
||||
verbose=output_data.get("verbose", False),
|
||||
quiet=output_data.get("quiet", False),
|
||||
screen_reader=output_data.get("screen_reader", False),
|
||||
),
|
||||
traces=TracesConfig(
|
||||
enabled=traces_data.get("enabled", False),
|
||||
endpoint=traces_data.get("endpoint"),
|
||||
sample_rate=traces_data.get("sample_rate", 1.0),
|
||||
),
|
||||
mcp=MCPConfig(servers=mcp_servers),
|
||||
model=ModelConfig(
|
||||
default=model_data.get("default", "gpt-4o-mini"),
|
||||
temperature=model_data.get("temperature", 0.7),
|
||||
max_tokens=model_data.get("max_tokens", 16000),
|
||||
),
|
||||
session=SessionConfig(
|
||||
auto_save=session_data.get("auto_save", False),
|
||||
history_limit=session_data.get("history_limit", 10),
|
||||
),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
|
||||
# Default configuration
|
||||
DEFAULT_CONFIG = ConfigSchema()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
PraisonAI Unified Execution Module.
|
||||
|
||||
Provides a single, unified execution primitive for all CLI modes:
|
||||
- CLI direct prompt
|
||||
- Profile command
|
||||
- TUI workers
|
||||
|
||||
Design Principles:
|
||||
1. Profiling observes, never alters execution
|
||||
2. Zero overhead when profiling disabled
|
||||
3. Sync and async are first-class citizens
|
||||
4. Fail-safe, not fail-fast
|
||||
5. Bounded resource usage
|
||||
6. Schema stability with versioning
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .request import ExecutionRequest
|
||||
from .result import ExecutionResult
|
||||
from .core import execute_sync, execute_async
|
||||
from .profiler import Profiler, ProfilerConfig, ProfileReport
|
||||
|
||||
# Lazy loading to maintain zero-overhead when not used
|
||||
_lazy_cache = {}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load execution components."""
|
||||
global _lazy_cache
|
||||
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "ExecutionRequest":
|
||||
from .request import ExecutionRequest
|
||||
_lazy_cache[name] = ExecutionRequest
|
||||
return ExecutionRequest
|
||||
elif name == "ExecutionResult":
|
||||
from .result import ExecutionResult
|
||||
_lazy_cache[name] = ExecutionResult
|
||||
return ExecutionResult
|
||||
elif name == "execute_sync":
|
||||
from .core import execute_sync
|
||||
_lazy_cache[name] = execute_sync
|
||||
return execute_sync
|
||||
elif name == "execute_async":
|
||||
from .core import execute_async
|
||||
_lazy_cache[name] = execute_async
|
||||
return execute_async
|
||||
elif name == "Profiler":
|
||||
from .profiler import Profiler
|
||||
_lazy_cache[name] = Profiler
|
||||
return Profiler
|
||||
elif name == "ProfilerConfig":
|
||||
from .profiler import ProfilerConfig
|
||||
_lazy_cache[name] = ProfilerConfig
|
||||
return ProfilerConfig
|
||||
elif name == "ProfileReport":
|
||||
from .profiler import ProfileReport
|
||||
_lazy_cache[name] = ProfileReport
|
||||
return ProfileReport
|
||||
elif name == "TimingBreakdown":
|
||||
from .profiler import TimingBreakdown
|
||||
_lazy_cache[name] = TimingBreakdown
|
||||
return TimingBreakdown
|
||||
elif name == "InvocationInfo":
|
||||
from .profiler import InvocationInfo
|
||||
_lazy_cache[name] = InvocationInfo
|
||||
return InvocationInfo
|
||||
elif name == "DecisionTrace":
|
||||
from .profiler import DecisionTrace
|
||||
_lazy_cache[name] = DecisionTrace
|
||||
return DecisionTrace
|
||||
elif name == "ModuleBreakdown":
|
||||
from .profiler import ModuleBreakdown
|
||||
_lazy_cache[name] = ModuleBreakdown
|
||||
return ModuleBreakdown
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Core execution
|
||||
"ExecutionRequest",
|
||||
"ExecutionResult",
|
||||
"execute_sync",
|
||||
"execute_async",
|
||||
# Profiling
|
||||
"Profiler",
|
||||
"ProfilerConfig",
|
||||
"ProfileReport",
|
||||
"TimingBreakdown",
|
||||
"InvocationInfo",
|
||||
"DecisionTrace",
|
||||
"ModuleBreakdown",
|
||||
]
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Core Execution Module.
|
||||
|
||||
Provides the unified execution primitives:
|
||||
- _execute_core(): Single implementation shared by all paths
|
||||
- execute_sync(): Synchronous wrapper for CLI
|
||||
- execute_async(): Asynchronous wrapper for TUI workers
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional, AsyncGenerator
|
||||
|
||||
from .request import ExecutionRequest
|
||||
from .result import ExecutionResult
|
||||
|
||||
|
||||
def _generate_run_id() -> str:
|
||||
"""Generate a unique run ID."""
|
||||
return str(uuid.uuid4())[:8]
|
||||
|
||||
|
||||
def _execute_core(
|
||||
request: ExecutionRequest,
|
||||
timing: Optional[dict] = None,
|
||||
) -> ExecutionResult:
|
||||
"""
|
||||
THE SINGLE EXECUTION IMPLEMENTATION.
|
||||
|
||||
All execution modes call this function.
|
||||
Profiler wraps this function, never injects into it.
|
||||
|
||||
Args:
|
||||
request: The execution request
|
||||
timing: Optional dict to record timing phases (for profiling)
|
||||
|
||||
Returns:
|
||||
ExecutionResult with output and metadata
|
||||
"""
|
||||
run_id = _generate_run_id()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["core_start"] = start_time
|
||||
|
||||
try:
|
||||
# Phase 1: Import (measured inline)
|
||||
import_start = time.perf_counter()
|
||||
from praisonaiagents import Agent
|
||||
import_end = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["imports_ms"] = (import_end - import_start) * 1000
|
||||
|
||||
# Phase 2: Agent Construction
|
||||
agent_start = time.perf_counter()
|
||||
|
||||
agent_config = {
|
||||
"name": request.agent_name,
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
if request.agent_instructions:
|
||||
agent_config["instructions"] = request.agent_instructions
|
||||
|
||||
if request.model:
|
||||
agent_config["llm"] = request.model
|
||||
|
||||
if request.tools:
|
||||
agent_config["tools"] = list(request.tools)
|
||||
|
||||
agent = Agent(**agent_config)
|
||||
agent_end = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["agent_init_ms"] = (agent_end - agent_start) * 1000
|
||||
|
||||
# Phase 3: Execution
|
||||
exec_start = time.perf_counter()
|
||||
|
||||
if request.stream:
|
||||
# For streaming, collect all chunks
|
||||
chunks = []
|
||||
if hasattr(agent, '_start_stream'):
|
||||
for chunk in agent._start_stream(request.prompt):
|
||||
chunks.append(chunk)
|
||||
else:
|
||||
# Fallback to non-streaming
|
||||
result = agent.start(request.prompt)
|
||||
chunks = [result] if result else []
|
||||
output = "".join(chunks)
|
||||
else:
|
||||
output = agent.start(request.prompt)
|
||||
|
||||
exec_end = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["execution_ms"] = (exec_end - exec_start) * 1000
|
||||
|
||||
end_time = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["total_ms"] = (end_time - start_time) * 1000
|
||||
|
||||
return ExecutionResult(
|
||||
output=output or "",
|
||||
run_id=run_id,
|
||||
success=True,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
metadata={
|
||||
"model": request.model,
|
||||
"stream": request.stream,
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
end_time = time.perf_counter()
|
||||
|
||||
if timing is not None:
|
||||
timing["total_ms"] = (end_time - start_time) * 1000
|
||||
timing["error"] = str(e)
|
||||
|
||||
return ExecutionResult.from_error(
|
||||
error=str(e),
|
||||
run_id=run_id,
|
||||
)
|
||||
|
||||
|
||||
def execute_sync(request: ExecutionRequest) -> ExecutionResult:
|
||||
"""
|
||||
Synchronous execution wrapper.
|
||||
|
||||
Used by:
|
||||
- CLI direct prompt
|
||||
- Profile command
|
||||
|
||||
Args:
|
||||
request: The execution request
|
||||
|
||||
Returns:
|
||||
ExecutionResult with output and metadata
|
||||
"""
|
||||
return _execute_core(request)
|
||||
|
||||
|
||||
async def execute_async(request: ExecutionRequest) -> ExecutionResult:
|
||||
"""
|
||||
Asynchronous execution wrapper.
|
||||
|
||||
Used by:
|
||||
- TUI workers
|
||||
- Queue system
|
||||
|
||||
Args:
|
||||
request: The execution request
|
||||
|
||||
Returns:
|
||||
ExecutionResult with output and metadata
|
||||
"""
|
||||
# Run the synchronous core in a thread pool to avoid blocking
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, _execute_core, request, None)
|
||||
|
||||
|
||||
async def execute_async_stream(
|
||||
request: ExecutionRequest,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Asynchronous streaming execution.
|
||||
|
||||
Yields chunks as they arrive.
|
||||
|
||||
Args:
|
||||
request: The execution request (stream=True recommended)
|
||||
|
||||
Yields:
|
||||
String chunks of the response
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent_config = {
|
||||
"name": request.agent_name,
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
if request.agent_instructions:
|
||||
agent_config["instructions"] = request.agent_instructions
|
||||
|
||||
if request.model:
|
||||
agent_config["llm"] = request.model
|
||||
|
||||
if request.tools:
|
||||
agent_config["tools"] = list(request.tools)
|
||||
|
||||
agent = Agent(**agent_config)
|
||||
|
||||
if hasattr(agent, '_start_stream'):
|
||||
for chunk in agent._start_stream(request.prompt):
|
||||
yield chunk
|
||||
await asyncio.sleep(0) # Yield control
|
||||
else:
|
||||
result = agent.start(request.prompt)
|
||||
if result:
|
||||
yield result
|
||||
|
||||
except Exception as e:
|
||||
yield f"Error: {e}"
|
||||
@@ -0,0 +1,898 @@
|
||||
"""
|
||||
Profiler Module.
|
||||
|
||||
Provides external profiling that wraps execution.
|
||||
Execution code has ZERO awareness of profiling.
|
||||
|
||||
Design Principles:
|
||||
1. Profiler wraps execution, does not inject into it
|
||||
2. Layered profiling with explicit overhead
|
||||
3. Bounded resource usage
|
||||
4. Versioned, stable output schema
|
||||
"""
|
||||
|
||||
import cProfile
|
||||
import pstats
|
||||
import time
|
||||
import platform
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from .request import ExecutionRequest
|
||||
from .result import ExecutionResult
|
||||
from .core import _execute_core
|
||||
|
||||
|
||||
# Schema version - MUST be incremented for breaking changes
|
||||
SCHEMA_VERSION = "1.0"
|
||||
|
||||
# Bounds for profile data
|
||||
MAX_FUNCTION_STATS = 1000
|
||||
MAX_CALL_GRAPH_EDGES = 5000
|
||||
MAX_NETWORK_REQUESTS = 100
|
||||
MAX_IMPORT_MODULES = 200
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfilerConfig:
|
||||
"""
|
||||
Profiler configuration with explicit layer control.
|
||||
|
||||
Layers:
|
||||
- 0: Wall-clock phases only (<1ms overhead)
|
||||
- 1: + cProfile function stats (~5% overhead)
|
||||
- 2: + Call graph callers/callees (~15% overhead)
|
||||
|
||||
Network timing is orthogonal (opt-in via track_network).
|
||||
"""
|
||||
|
||||
# Layer control (0, 1, or 2)
|
||||
layer: int = 1
|
||||
|
||||
# Layer 1+ options
|
||||
limit: int = 30 # Max functions to show
|
||||
sort_by: str = "cumulative" # cumulative, time, calls
|
||||
|
||||
# Layer 2 options
|
||||
show_callers: bool = False
|
||||
show_callees: bool = False
|
||||
|
||||
# Network timing (orthogonal to layers)
|
||||
track_network: bool = False
|
||||
|
||||
# Output options
|
||||
output_format: str = "text" # text, json
|
||||
save_path: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_flags(
|
||||
cls,
|
||||
profile: bool = False,
|
||||
deep: bool = False,
|
||||
network: bool = False,
|
||||
) -> 'ProfilerConfig':
|
||||
"""Create config from CLI flags."""
|
||||
if deep:
|
||||
return cls(
|
||||
layer=2,
|
||||
show_callers=True,
|
||||
show_callees=True,
|
||||
track_network=network,
|
||||
)
|
||||
elif profile:
|
||||
return cls(layer=1, track_network=network)
|
||||
else:
|
||||
return cls(layer=0, track_network=network)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimingBreakdown:
|
||||
"""
|
||||
Phase timing breakdown with full timeline visibility.
|
||||
|
||||
All fields in milliseconds.
|
||||
Timeline order: cli_entry -> cli_parse -> routing -> imports -> agent_init ->
|
||||
network_start -> first_token -> first_output -> execution_end
|
||||
"""
|
||||
|
||||
# MANDATORY fields (always present)
|
||||
total_ms: float = 0.0
|
||||
imports_ms: float = 0.0
|
||||
agent_init_ms: float = 0.0
|
||||
execution_ms: float = 0.0
|
||||
|
||||
# TIMELINE PHASES (new - for full visibility)
|
||||
cli_entry_ms: float = 0.0 # Time from ENTER to CLI entry point
|
||||
cli_parse_ms: float = 0.0 # Argparse/Typer routing time
|
||||
routing_ms: float = 0.0 # Command routing time
|
||||
network_start_ms: float = 0.0 # When network request started (relative to start)
|
||||
first_token_ms: float = 0.0 # Time to first token (streaming)
|
||||
first_output_ms: float = 0.0 # Time to First Response (TTFR) - first visible output
|
||||
|
||||
# OPTIONAL fields (may be 0)
|
||||
tool_time_ms: float = 0.0 # If tools were called
|
||||
|
||||
@property
|
||||
def time_to_first_response_ms(self) -> float:
|
||||
"""Time to First Response (TTFR) - the key user-perceived latency metric."""
|
||||
if self.first_output_ms > 0:
|
||||
return self.first_output_ms
|
||||
# Fallback: use execution end if first_output not captured
|
||||
return self.total_ms
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"total_ms": self.total_ms,
|
||||
"imports_ms": self.imports_ms,
|
||||
"agent_init_ms": self.agent_init_ms,
|
||||
"execution_ms": self.execution_ms,
|
||||
"cli_entry_ms": self.cli_entry_ms,
|
||||
"cli_parse_ms": self.cli_parse_ms,
|
||||
"routing_ms": self.routing_ms,
|
||||
"network_start_ms": self.network_start_ms,
|
||||
"first_token_ms": self.first_token_ms,
|
||||
"first_output_ms": self.first_output_ms,
|
||||
"time_to_first_response_ms": self.time_to_first_response_ms,
|
||||
"tool_time_ms": self.tool_time_ms,
|
||||
}
|
||||
|
||||
def to_timeline(self) -> List[Tuple[str, float, float]]:
|
||||
"""
|
||||
Get ordered timeline of phases.
|
||||
|
||||
Returns list of (phase_name, start_ms, duration_ms) tuples.
|
||||
"""
|
||||
timeline = []
|
||||
cursor = 0.0
|
||||
|
||||
# CLI Entry (if captured)
|
||||
if self.cli_entry_ms > 0:
|
||||
timeline.append(("CLI Entry", 0.0, self.cli_entry_ms))
|
||||
cursor = self.cli_entry_ms
|
||||
|
||||
# CLI Parse
|
||||
if self.cli_parse_ms > 0:
|
||||
timeline.append(("CLI Parse", cursor, self.cli_parse_ms))
|
||||
cursor += self.cli_parse_ms
|
||||
|
||||
# Routing
|
||||
if self.routing_ms > 0:
|
||||
timeline.append(("Routing", cursor, self.routing_ms))
|
||||
cursor += self.routing_ms
|
||||
|
||||
# Imports
|
||||
if self.imports_ms > 0:
|
||||
timeline.append(("Imports", cursor, self.imports_ms))
|
||||
cursor += self.imports_ms
|
||||
|
||||
# Agent Init
|
||||
if self.agent_init_ms > 0:
|
||||
timeline.append(("Agent Init", cursor, self.agent_init_ms))
|
||||
cursor += self.agent_init_ms
|
||||
|
||||
# Network to First Token (if streaming)
|
||||
if self.network_start_ms > 0 and self.first_token_ms > 0:
|
||||
network_duration = self.first_token_ms - self.network_start_ms
|
||||
if network_duration > 0:
|
||||
timeline.append(("Network → First Token", self.network_start_ms, network_duration))
|
||||
|
||||
# Execution
|
||||
if self.execution_ms > 0:
|
||||
timeline.append(("Execution", cursor, self.execution_ms))
|
||||
|
||||
return timeline
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionStat:
|
||||
"""Statistics for a single function."""
|
||||
|
||||
name: str
|
||||
file: str
|
||||
line: int
|
||||
calls: int
|
||||
total_time_ms: float
|
||||
cumulative_time_ms: float
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"file": self.file,
|
||||
"line": self.line,
|
||||
"calls": self.calls,
|
||||
"total_time_ms": self.total_time_ms,
|
||||
"cumulative_time_ms": self.cumulative_time_ms,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallGraph:
|
||||
"""Call graph data (callers and callees)."""
|
||||
|
||||
callers: Dict[str, List[str]] = field(default_factory=dict)
|
||||
callees: Dict[str, List[str]] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def edges(self) -> List[Tuple[str, str]]:
|
||||
"""Get all edges as (caller, callee) tuples."""
|
||||
edges = []
|
||||
for callee, callers in self.callers.items():
|
||||
for caller in callers:
|
||||
edges.append((caller, callee))
|
||||
return edges
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"callers": self.callers,
|
||||
"callees": self.callees,
|
||||
"edge_count": len(self.edges),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestTiming:
|
||||
"""Single HTTP request timing."""
|
||||
|
||||
url: str
|
||||
method: str
|
||||
start_time: float
|
||||
first_byte_time: float
|
||||
end_time: float
|
||||
status_code: int
|
||||
|
||||
@property
|
||||
def ttfb_ms(self) -> float:
|
||||
"""Time to first byte in milliseconds."""
|
||||
return (self.first_byte_time - self.start_time) * 1000
|
||||
|
||||
@property
|
||||
def total_ms(self) -> float:
|
||||
"""Total request time in milliseconds."""
|
||||
return (self.end_time - self.start_time) * 1000
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"url": self.url,
|
||||
"method": self.method,
|
||||
"ttfb_ms": self.ttfb_ms,
|
||||
"total_ms": self.total_ms,
|
||||
"status_code": self.status_code,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class InvocationInfo:
|
||||
"""How the profile was invoked."""
|
||||
|
||||
method: str # "cli_direct", "profile_command", "tui"
|
||||
flags: Dict[str, Any] = field(default_factory=dict)
|
||||
praisonai_version: str = ""
|
||||
python_version: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Set version info if not provided."""
|
||||
if not self.python_version:
|
||||
self.python_version = platform.python_version()
|
||||
if not self.praisonai_version:
|
||||
try:
|
||||
from praisonai.version import __version__
|
||||
self.praisonai_version = __version__
|
||||
except ImportError:
|
||||
self.praisonai_version = "unknown"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"method": self.method,
|
||||
"flags": self.flags,
|
||||
"praisonai_version": self.praisonai_version,
|
||||
"python_version": self.python_version,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecisionTrace:
|
||||
"""
|
||||
Decision trace for deep profile visibility.
|
||||
|
||||
Explains what decisions were made during execution.
|
||||
"""
|
||||
|
||||
agent_config: str = "" # Agent configuration chosen
|
||||
model_selected: str = "" # Model used
|
||||
streaming_mode: bool = False # Streaming vs non-streaming
|
||||
profile_layer: int = 0 # Profile layer (0/1/2)
|
||||
tools_enabled: List[str] = field(default_factory=list)
|
||||
tools_disabled: List[str] = field(default_factory=list)
|
||||
fallbacks_used: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"agent_config": self.agent_config,
|
||||
"model_selected": self.model_selected,
|
||||
"streaming_mode": self.streaming_mode,
|
||||
"profile_layer": self.profile_layer,
|
||||
"tools_enabled": self.tools_enabled,
|
||||
"tools_disabled": self.tools_disabled,
|
||||
"fallbacks_used": self.fallbacks_used,
|
||||
}
|
||||
|
||||
def to_text(self) -> str:
|
||||
"""Format as human-readable text."""
|
||||
lines = []
|
||||
lines.append(f" Agent Config: {self.agent_config or 'default'}")
|
||||
lines.append(f" Model: {self.model_selected or 'default'}")
|
||||
lines.append(f" Streaming: {'enabled' if self.streaming_mode else 'disabled'}")
|
||||
lines.append(f" Profile Layer: {self.profile_layer}")
|
||||
if self.tools_enabled:
|
||||
lines.append(f" Tools Enabled: {', '.join(self.tools_enabled)}")
|
||||
if self.tools_disabled:
|
||||
lines.append(f" Tools Disabled: {', '.join(self.tools_disabled)}")
|
||||
if self.fallbacks_used:
|
||||
lines.append(f" Fallbacks: {', '.join(self.fallbacks_used)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleBreakdown:
|
||||
"""
|
||||
Module/file breakdown for deep profile visibility.
|
||||
|
||||
Groups functions by module category.
|
||||
"""
|
||||
|
||||
cli_modules: List[str] = field(default_factory=list)
|
||||
execution_modules: List[str] = field(default_factory=list)
|
||||
agent_modules: List[str] = field(default_factory=list)
|
||||
tool_modules: List[str] = field(default_factory=list)
|
||||
network_modules: List[str] = field(default_factory=list)
|
||||
third_party_modules: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, List[str]]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"cli": self.cli_modules,
|
||||
"execution": self.execution_modules,
|
||||
"agent": self.agent_modules,
|
||||
"tools": self.tool_modules,
|
||||
"network": self.network_modules,
|
||||
"third_party": self.third_party_modules,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileReport:
|
||||
"""
|
||||
Canonical profile report.
|
||||
|
||||
VERSION 1.0 - Schema is frozen after release.
|
||||
New fields MUST be optional with defaults.
|
||||
"""
|
||||
|
||||
# Schema version (MANDATORY, NEVER CHANGES MEANING)
|
||||
schema_version: str = SCHEMA_VERSION
|
||||
|
||||
# Identity (MANDATORY)
|
||||
run_id: str = ""
|
||||
timestamp: str = "" # ISO 8601
|
||||
|
||||
# Invocation (MANDATORY)
|
||||
invocation: InvocationInfo = field(default_factory=lambda: InvocationInfo(method="unknown"))
|
||||
|
||||
# Timing (MANDATORY)
|
||||
timing: TimingBreakdown = field(default_factory=TimingBreakdown)
|
||||
|
||||
# Function stats (OPTIONAL, present if layer >= 1)
|
||||
functions: Optional[List[FunctionStat]] = None
|
||||
|
||||
# Call graph (OPTIONAL, present if layer >= 2)
|
||||
call_graph: Optional[CallGraph] = None
|
||||
|
||||
# Network (OPTIONAL, present if track_network)
|
||||
network: Optional[List[RequestTiming]] = None
|
||||
|
||||
# Response (MANDATORY but truncated)
|
||||
response_preview: str = "" # Max 500 chars
|
||||
|
||||
# Decision trace (OPTIONAL, present if layer >= 2)
|
||||
decision_trace: Optional[DecisionTrace] = None
|
||||
|
||||
# Module breakdown (OPTIONAL, present if layer >= 2)
|
||||
module_breakdown: Optional[ModuleBreakdown] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Set timestamp if not provided."""
|
||||
if not self.timestamp:
|
||||
self.timestamp = datetime.utcnow().isoformat() + "Z"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
result = {
|
||||
"schema_version": self.schema_version,
|
||||
"run_id": self.run_id,
|
||||
"timestamp": self.timestamp,
|
||||
"invocation": self.invocation.to_dict(),
|
||||
"timing": self.timing.to_dict(),
|
||||
"timeline": self.timing.to_timeline(),
|
||||
"timeline_diagram": self.to_timeline_diagram(),
|
||||
"response_preview": self.response_preview,
|
||||
}
|
||||
|
||||
if self.functions is not None:
|
||||
result["functions"] = [f.to_dict() for f in self.functions]
|
||||
|
||||
if self.call_graph is not None:
|
||||
result["call_graph"] = self.call_graph.to_dict()
|
||||
|
||||
if self.network is not None:
|
||||
result["network"] = [r.to_dict() for r in self.network]
|
||||
|
||||
if self.decision_trace is not None:
|
||||
result["decision_trace"] = self.decision_trace.to_dict()
|
||||
|
||||
if self.module_breakdown is not None:
|
||||
result["module_breakdown"] = self.module_breakdown.to_dict()
|
||||
|
||||
return result
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string."""
|
||||
import json
|
||||
return json.dumps(self.to_dict(), indent=2, default=str)
|
||||
|
||||
def to_timeline_diagram(self) -> str:
|
||||
"""
|
||||
Generate ASCII timeline diagram showing execution phases.
|
||||
|
||||
Format:
|
||||
ENTER ─────────────────────────────────────────────────────────► RESPONSE
|
||||
│ imports │ init │ network │
|
||||
│ XXXms │ XXms │ XXXms │
|
||||
└─────────┴──────┴───────────────────────┘
|
||||
TOTAL: XXXXms
|
||||
"""
|
||||
lines = []
|
||||
total = self.timing.total_ms
|
||||
if total <= 0:
|
||||
return "No timing data available"
|
||||
|
||||
# Calculate phase widths (proportional to time, min 8 chars)
|
||||
phases = []
|
||||
if self.timing.imports_ms > 0:
|
||||
phases.append(("imports", self.timing.imports_ms))
|
||||
if self.timing.agent_init_ms > 0:
|
||||
phases.append(("init", self.timing.agent_init_ms))
|
||||
if self.timing.execution_ms > 0:
|
||||
phases.append(("network", self.timing.execution_ms))
|
||||
|
||||
if not phases:
|
||||
phases = [("total", total)]
|
||||
|
||||
# Scale to fit in ~60 chars
|
||||
scale = 50.0 / total if total > 0 else 1
|
||||
|
||||
# Build the diagram
|
||||
lines.append("")
|
||||
lines.append("## Timeline Diagram")
|
||||
lines.append("")
|
||||
|
||||
# Top line with arrow
|
||||
top_line = "ENTER "
|
||||
for name, ms in phases:
|
||||
width = max(8, int(ms * scale))
|
||||
top_line += "─" * width
|
||||
top_line += "► RESPONSE"
|
||||
lines.append(top_line)
|
||||
|
||||
# Phase names line
|
||||
name_line = " "
|
||||
for name, ms in phases:
|
||||
width = max(8, int(ms * scale))
|
||||
name_line += "│" + name.center(width - 1)
|
||||
name_line += "│"
|
||||
lines.append(name_line)
|
||||
|
||||
# Phase times line
|
||||
time_line = " "
|
||||
for name, ms in phases:
|
||||
width = max(8, int(ms * scale))
|
||||
time_str = f"{ms:.0f}ms"
|
||||
time_line += "│" + time_str.center(width - 1)
|
||||
time_line += "│"
|
||||
lines.append(time_line)
|
||||
|
||||
# Bottom line
|
||||
bottom_line = " "
|
||||
for i, (name, ms) in enumerate(phases):
|
||||
width = max(8, int(ms * scale))
|
||||
if i == 0:
|
||||
bottom_line += "└" + "─" * (width - 1)
|
||||
else:
|
||||
bottom_line += "┴" + "─" * (width - 1)
|
||||
bottom_line += "┘"
|
||||
lines.append(bottom_line)
|
||||
|
||||
# Total line
|
||||
total_line = " " * (len(bottom_line) - 15) + f"TOTAL: {total:.0f}ms"
|
||||
lines.append(total_line)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_text(self) -> str:
|
||||
"""Format as human-readable text."""
|
||||
lines = []
|
||||
lines.append("=" * 70)
|
||||
lines.append("PraisonAI Profile Report")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# Identity
|
||||
lines.append(f"Run ID: {self.run_id}")
|
||||
lines.append(f"Timestamp: {self.timestamp}")
|
||||
lines.append(f"Method: {self.invocation.method}")
|
||||
lines.append(f"Version: {self.invocation.praisonai_version}")
|
||||
lines.append("")
|
||||
|
||||
# TIMELINE DIAGRAM (visual representation)
|
||||
lines.append(self.to_timeline_diagram())
|
||||
|
||||
# TIMELINE SECTION (shows ENTER → First Response)
|
||||
lines.append("## Execution Timeline")
|
||||
lines.append("-" * 45)
|
||||
timeline = self.timing.to_timeline()
|
||||
if timeline:
|
||||
for phase_name, start_ms, duration_ms in timeline:
|
||||
lines.append(f" {phase_name:<25} : {duration_ms:>8.2f} ms")
|
||||
else:
|
||||
# Fallback to basic phases
|
||||
if self.timing.imports_ms > 0:
|
||||
lines.append(f" {'Imports':<25} : {self.timing.imports_ms:>8.2f} ms")
|
||||
if self.timing.agent_init_ms > 0:
|
||||
lines.append(f" {'Agent Init':<25} : {self.timing.agent_init_ms:>8.2f} ms")
|
||||
if self.timing.execution_ms > 0:
|
||||
lines.append(f" {'Execution':<25} : {self.timing.execution_ms:>8.2f} ms")
|
||||
lines.append(" " + "─" * 43)
|
||||
|
||||
# TIME TO FIRST RESPONSE (TTFR) - the key metric
|
||||
ttfr = self.timing.time_to_first_response_ms
|
||||
lines.append(f" {'⏱ Time to First Response':<25} : {ttfr:>8.2f} ms")
|
||||
lines.append(f" {'TOTAL':<25} : {self.timing.total_ms:>8.2f} ms")
|
||||
lines.append("")
|
||||
|
||||
# Timing breakdown (legacy format for compatibility)
|
||||
lines.append("## Timing Breakdown")
|
||||
lines.append("-" * 40)
|
||||
if self.timing.cli_entry_ms > 0:
|
||||
lines.append(f" CLI Entry: {self.timing.cli_entry_ms:>10.2f} ms")
|
||||
if self.timing.cli_parse_ms > 0:
|
||||
lines.append(f" CLI Parse: {self.timing.cli_parse_ms:>10.2f} ms")
|
||||
if self.timing.routing_ms > 0:
|
||||
lines.append(f" Routing: {self.timing.routing_ms:>10.2f} ms")
|
||||
lines.append(f" Imports: {self.timing.imports_ms:>10.2f} ms")
|
||||
lines.append(f" Agent Init: {self.timing.agent_init_ms:>10.2f} ms")
|
||||
lines.append(f" Execution: {self.timing.execution_ms:>10.2f} ms")
|
||||
if self.timing.network_start_ms > 0:
|
||||
lines.append(f" Network Start: {self.timing.network_start_ms:>10.2f} ms")
|
||||
if self.timing.first_token_ms > 0:
|
||||
lines.append(f" First Token: {self.timing.first_token_ms:>10.2f} ms")
|
||||
if self.timing.first_output_ms > 0:
|
||||
lines.append(f" First Output: {self.timing.first_output_ms:>10.2f} ms")
|
||||
lines.append(" ─────────────────────────────────────")
|
||||
lines.append(f" TOTAL: {self.timing.total_ms:>10.2f} ms")
|
||||
lines.append("")
|
||||
|
||||
# Decision trace (deep profile only)
|
||||
if self.decision_trace:
|
||||
lines.append("## Decision Trace")
|
||||
lines.append("-" * 40)
|
||||
lines.append(self.decision_trace.to_text())
|
||||
lines.append("")
|
||||
|
||||
# Function stats
|
||||
if self.functions:
|
||||
lines.append("## Top Functions by Cumulative Time")
|
||||
lines.append("-" * 70)
|
||||
lines.append(f"{'Function':<40} {'Calls':>8} {'Cumul (ms)':>12}")
|
||||
lines.append("-" * 70)
|
||||
for func in self.functions[:20]:
|
||||
name = func.name[:38] if len(func.name) > 38 else func.name
|
||||
lines.append(f"{name:<40} {func.calls:>8} {func.cumulative_time_ms:>12.2f}")
|
||||
lines.append("")
|
||||
|
||||
# Module breakdown (deep profile only)
|
||||
if self.module_breakdown:
|
||||
lines.append("## Module Breakdown")
|
||||
lines.append("-" * 40)
|
||||
mb = self.module_breakdown
|
||||
if mb.cli_modules:
|
||||
lines.append(f" CLI: {len(mb.cli_modules)} modules")
|
||||
if mb.execution_modules:
|
||||
lines.append(f" Execution: {len(mb.execution_modules)} modules")
|
||||
if mb.agent_modules:
|
||||
lines.append(f" Agent: {len(mb.agent_modules)} modules")
|
||||
if mb.tool_modules:
|
||||
lines.append(f" Tools: {len(mb.tool_modules)} modules")
|
||||
if mb.network_modules:
|
||||
lines.append(f" Network: {len(mb.network_modules)} modules")
|
||||
if mb.third_party_modules:
|
||||
lines.append(f" Third-party:{len(mb.third_party_modules)} modules")
|
||||
lines.append("")
|
||||
|
||||
# Call graph summary
|
||||
if self.call_graph:
|
||||
lines.append(f"## Call Graph: {len(self.call_graph.edges)} edges")
|
||||
lines.append("")
|
||||
|
||||
# Network timing
|
||||
if self.network:
|
||||
lines.append("## Network Requests")
|
||||
lines.append("-" * 70)
|
||||
for req in self.network[:10]:
|
||||
lines.append(f" {req.method} {req.url[:50]} - {req.total_ms:.2f}ms (TTFB: {req.ttfb_ms:.2f}ms)")
|
||||
lines.append("")
|
||||
|
||||
# Response preview
|
||||
if self.response_preview:
|
||||
lines.append("## Response Preview")
|
||||
lines.append("-" * 40)
|
||||
preview = self.response_preview[:200]
|
||||
if len(self.response_preview) > 200:
|
||||
preview += "..."
|
||||
lines.append(preview)
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class Profiler:
|
||||
"""
|
||||
External profiler that wraps execution.
|
||||
|
||||
Execution code has ZERO awareness of profiling.
|
||||
Profiler uses cProfile.runctx() or similar.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ProfilerConfig):
|
||||
self.config = config
|
||||
self._cprofile: Optional[cProfile.Profile] = None
|
||||
self._timing: Dict[str, float] = {}
|
||||
self._network_requests: List[RequestTiming] = []
|
||||
|
||||
def profile_sync(
|
||||
self,
|
||||
request: ExecutionRequest,
|
||||
invocation_method: str = "profile_command",
|
||||
) -> Tuple[ExecutionResult, ProfileReport]:
|
||||
"""
|
||||
Profile synchronous execution.
|
||||
|
||||
Args:
|
||||
request: The execution request
|
||||
invocation_method: How this was invoked (for report)
|
||||
|
||||
Returns:
|
||||
Tuple of (ExecutionResult, ProfileReport)
|
||||
"""
|
||||
timing_dict: Dict[str, float] = {}
|
||||
|
||||
# Layer 0: Wall-clock timing (always)
|
||||
start = time.perf_counter()
|
||||
|
||||
# Layer 1+: cProfile
|
||||
if self.config.layer >= 1:
|
||||
self._cprofile = cProfile.Profile()
|
||||
self._cprofile.enable()
|
||||
|
||||
try:
|
||||
result = _execute_core(request, timing_dict)
|
||||
finally:
|
||||
if self._cprofile:
|
||||
self._cprofile.disable()
|
||||
|
||||
total_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Build report
|
||||
report = self._build_report(
|
||||
request=request,
|
||||
result=result,
|
||||
timing_dict=timing_dict,
|
||||
total_ms=total_ms,
|
||||
invocation_method=invocation_method,
|
||||
)
|
||||
|
||||
return result, report
|
||||
|
||||
def _build_report(
|
||||
self,
|
||||
request: ExecutionRequest,
|
||||
result: ExecutionResult,
|
||||
timing_dict: Dict[str, float],
|
||||
total_ms: float,
|
||||
invocation_method: str,
|
||||
) -> ProfileReport:
|
||||
"""Build the profile report from collected data."""
|
||||
|
||||
# Calculate first_output_ms (TTFR) - time from start to when output is available
|
||||
# This is imports + agent_init + execution (when output becomes available)
|
||||
imports_ms = timing_dict.get("imports_ms", 0.0)
|
||||
agent_init_ms = timing_dict.get("agent_init_ms", 0.0)
|
||||
execution_ms = timing_dict.get("execution_ms", 0.0)
|
||||
first_output_ms = imports_ms + agent_init_ms + execution_ms
|
||||
|
||||
# Timing breakdown with full timeline
|
||||
timing = TimingBreakdown(
|
||||
total_ms=total_ms,
|
||||
imports_ms=imports_ms,
|
||||
agent_init_ms=agent_init_ms,
|
||||
execution_ms=execution_ms,
|
||||
cli_entry_ms=timing_dict.get("cli_entry_ms", 0.0),
|
||||
cli_parse_ms=timing_dict.get("cli_parse_ms", 0.0),
|
||||
routing_ms=timing_dict.get("routing_ms", 0.0),
|
||||
network_start_ms=timing_dict.get("network_start_ms", 0.0),
|
||||
first_token_ms=timing_dict.get("first_token_ms", 0.0),
|
||||
first_output_ms=first_output_ms,
|
||||
)
|
||||
|
||||
# Invocation info
|
||||
invocation = InvocationInfo(
|
||||
method=invocation_method,
|
||||
flags={
|
||||
"layer": self.config.layer,
|
||||
"stream": request.stream,
|
||||
"model": request.model,
|
||||
},
|
||||
)
|
||||
|
||||
# Function stats (layer 1+)
|
||||
functions = None
|
||||
if self.config.layer >= 1 and self._cprofile:
|
||||
functions = self._extract_function_stats()
|
||||
|
||||
# Call graph (layer 2)
|
||||
call_graph = None
|
||||
if self.config.layer >= 2 and self._cprofile:
|
||||
call_graph = self._extract_call_graph()
|
||||
|
||||
# Decision trace (layer 2 - deep profile)
|
||||
decision_trace = None
|
||||
if self.config.layer >= 2:
|
||||
decision_trace = DecisionTrace(
|
||||
agent_config=request.agent_name or "default",
|
||||
model_selected=request.model or "default",
|
||||
streaming_mode=request.stream,
|
||||
profile_layer=self.config.layer,
|
||||
tools_enabled=list(request.tools) if request.tools else [],
|
||||
tools_disabled=[],
|
||||
fallbacks_used=[],
|
||||
)
|
||||
|
||||
# Module breakdown (layer 2 - deep profile)
|
||||
module_breakdown = None
|
||||
if self.config.layer >= 2 and functions:
|
||||
module_breakdown = self._extract_module_breakdown(functions)
|
||||
|
||||
# Response preview (truncated)
|
||||
response_preview = result.output[:500] if result.output else ""
|
||||
|
||||
return ProfileReport(
|
||||
run_id=result.run_id,
|
||||
invocation=invocation,
|
||||
timing=timing,
|
||||
functions=functions,
|
||||
call_graph=call_graph,
|
||||
network=self._network_requests if self._network_requests else None,
|
||||
response_preview=response_preview,
|
||||
decision_trace=decision_trace,
|
||||
module_breakdown=module_breakdown,
|
||||
)
|
||||
|
||||
def _extract_module_breakdown(self, functions: List[FunctionStat]) -> ModuleBreakdown:
|
||||
"""Extract module breakdown from function stats."""
|
||||
cli_modules = set()
|
||||
execution_modules = set()
|
||||
agent_modules = set()
|
||||
tool_modules = set()
|
||||
network_modules = set()
|
||||
third_party_modules = set()
|
||||
|
||||
for func in functions:
|
||||
file_path = func.file.lower()
|
||||
|
||||
if "praisonai/cli" in file_path:
|
||||
cli_modules.add(func.file)
|
||||
elif "praisonai" in file_path and "execution" in file_path:
|
||||
execution_modules.add(func.file)
|
||||
elif "praisonaiagents" in file_path and "agent" in file_path:
|
||||
agent_modules.add(func.file)
|
||||
elif "praisonaiagents" in file_path and "tool" in file_path:
|
||||
tool_modules.add(func.file)
|
||||
elif any(x in file_path for x in ["httpx", "httpcore", "urllib", "requests", "aiohttp"]):
|
||||
network_modules.add(func.file)
|
||||
elif "site-packages" in file_path or not file_path.startswith("/"):
|
||||
third_party_modules.add(func.file)
|
||||
|
||||
return ModuleBreakdown(
|
||||
cli_modules=list(cli_modules)[:20],
|
||||
execution_modules=list(execution_modules)[:20],
|
||||
agent_modules=list(agent_modules)[:20],
|
||||
tool_modules=list(tool_modules)[:20],
|
||||
network_modules=list(network_modules)[:20],
|
||||
third_party_modules=list(third_party_modules)[:20],
|
||||
)
|
||||
|
||||
def _extract_function_stats(self) -> List[FunctionStat]:
|
||||
"""Extract function statistics from cProfile."""
|
||||
if not self._cprofile:
|
||||
return []
|
||||
|
||||
stats = pstats.Stats(self._cprofile)
|
||||
|
||||
# Sort by cumulative time
|
||||
stats.sort_stats(self.config.sort_by)
|
||||
|
||||
# Extract stats
|
||||
function_stats = []
|
||||
for (filename, line, name), (cc, nc, tt, ct, callers) in stats.stats.items():
|
||||
if len(function_stats) >= MAX_FUNCTION_STATS:
|
||||
break
|
||||
|
||||
function_stats.append(FunctionStat(
|
||||
name=name,
|
||||
file=filename,
|
||||
line=line,
|
||||
calls=nc,
|
||||
total_time_ms=tt * 1000,
|
||||
cumulative_time_ms=ct * 1000,
|
||||
))
|
||||
|
||||
# Sort and limit
|
||||
function_stats.sort(key=lambda x: x.cumulative_time_ms, reverse=True)
|
||||
return function_stats[:self.config.limit]
|
||||
|
||||
def _extract_call_graph(self) -> CallGraph:
|
||||
"""Extract call graph from cProfile."""
|
||||
if not self._cprofile:
|
||||
return CallGraph()
|
||||
|
||||
stats = pstats.Stats(self._cprofile)
|
||||
|
||||
callers: Dict[str, List[str]] = {}
|
||||
callees: Dict[str, List[str]] = {}
|
||||
edge_count = 0
|
||||
|
||||
for (filename, line, name), (cc, nc, tt, ct, caller_dict) in stats.stats.items():
|
||||
if edge_count >= MAX_CALL_GRAPH_EDGES:
|
||||
break
|
||||
|
||||
callee_key = f"{name}:{filename}:{line}"
|
||||
|
||||
for (caller_file, caller_line, caller_name), _ in caller_dict.items():
|
||||
if edge_count >= MAX_CALL_GRAPH_EDGES:
|
||||
break
|
||||
|
||||
caller_key = f"{caller_name}:{caller_file}:{caller_line}"
|
||||
|
||||
# Add to callers
|
||||
if callee_key not in callers:
|
||||
callers[callee_key] = []
|
||||
if caller_key not in callers[callee_key]:
|
||||
callers[callee_key].append(caller_key)
|
||||
|
||||
# Add to callees
|
||||
if caller_key not in callees:
|
||||
callees[caller_key] = []
|
||||
if callee_key not in callees[caller_key]:
|
||||
callees[caller_key].append(callee_key)
|
||||
|
||||
edge_count += 1
|
||||
|
||||
return CallGraph(callers=callers, callees=callees)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
ExecutionRequest - Immutable request for execution.
|
||||
|
||||
This is the minimal, immutable request that all execution paths use.
|
||||
CLI/TUI translate their flags into this structure.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional, Tuple, Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutionRequest:
|
||||
"""
|
||||
Minimal, immutable request for execution.
|
||||
|
||||
NOT a kitchen-sink of all CLI flags.
|
||||
CLI/TUI translate their flags into this.
|
||||
|
||||
Attributes:
|
||||
prompt: The user prompt to execute
|
||||
agent_name: Name of the agent (default: "Assistant")
|
||||
agent_instructions: Custom instructions for the agent
|
||||
model: LLM model to use (None = use default)
|
||||
tools: Tuple of tool callables (immutable)
|
||||
stream: Whether to stream the response
|
||||
|
||||
Note: This is frozen (immutable) to:
|
||||
- Prevent accidental mutation during profiling
|
||||
- Enable safe sharing across threads/coroutines
|
||||
"""
|
||||
|
||||
# Required
|
||||
prompt: str
|
||||
|
||||
# Agent identity (optional, has defaults)
|
||||
agent_name: str = "Assistant"
|
||||
agent_instructions: Optional[str] = None
|
||||
|
||||
# Model (optional, has defaults)
|
||||
model: Optional[str] = None # None = use default
|
||||
|
||||
# Tools (optional) - using tuple for immutability
|
||||
tools: Tuple[Callable[..., Any], ...] = ()
|
||||
|
||||
# Execution mode
|
||||
stream: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate request after initialization."""
|
||||
if not self.prompt:
|
||||
raise ValueError("prompt cannot be empty")
|
||||
|
||||
def with_model(self, model: str) -> 'ExecutionRequest':
|
||||
"""Create a new request with a different model."""
|
||||
return ExecutionRequest(
|
||||
prompt=self.prompt,
|
||||
agent_name=self.agent_name,
|
||||
agent_instructions=self.agent_instructions,
|
||||
model=model,
|
||||
tools=self.tools,
|
||||
stream=self.stream,
|
||||
)
|
||||
|
||||
def with_tools(self, tools: Tuple[Callable[..., Any], ...]) -> 'ExecutionRequest':
|
||||
"""Create a new request with different tools."""
|
||||
return ExecutionRequest(
|
||||
prompt=self.prompt,
|
||||
agent_name=self.agent_name,
|
||||
agent_instructions=self.agent_instructions,
|
||||
model=self.model,
|
||||
tools=tools,
|
||||
stream=self.stream,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"prompt": self.prompt,
|
||||
"agent_name": self.agent_name,
|
||||
"agent_instructions": self.agent_instructions,
|
||||
"model": self.model,
|
||||
"tools_count": len(self.tools),
|
||||
"stream": self.stream,
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
ExecutionResult - Result from execution.
|
||||
|
||||
Contains the output and metadata from an execution.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""
|
||||
Result from executing a request.
|
||||
|
||||
Attributes:
|
||||
output: The text output from the agent
|
||||
run_id: Unique identifier for this execution
|
||||
success: Whether execution completed successfully
|
||||
error: Error message if execution failed
|
||||
metadata: Additional metadata about the execution
|
||||
tool_calls: List of tools that were called
|
||||
start_time: When execution started (perf_counter)
|
||||
end_time: When execution ended (perf_counter)
|
||||
"""
|
||||
|
||||
# Primary output
|
||||
output: str
|
||||
|
||||
# Identity
|
||||
run_id: str = ""
|
||||
|
||||
# Status
|
||||
success: bool = True
|
||||
error: Optional[str] = None
|
||||
|
||||
# Metadata
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Tool tracking
|
||||
tool_calls: List[str] = field(default_factory=list)
|
||||
|
||||
# Timing (raw perf_counter values)
|
||||
start_time: float = 0.0
|
||||
end_time: float = 0.0
|
||||
|
||||
@property
|
||||
def duration_ms(self) -> float:
|
||||
"""Duration in milliseconds."""
|
||||
if self.start_time and self.end_time:
|
||||
return (self.end_time - self.start_time) * 1000
|
||||
return 0.0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"output": self.output,
|
||||
"run_id": self.run_id,
|
||||
"success": self.success,
|
||||
"error": self.error,
|
||||
"metadata": self.metadata,
|
||||
"tool_calls": self.tool_calls,
|
||||
"duration_ms": self.duration_ms,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_error(cls, error: str, run_id: str = "") -> 'ExecutionResult':
|
||||
"""Create a result from an error."""
|
||||
return cls(
|
||||
output="",
|
||||
run_id=run_id,
|
||||
success=False,
|
||||
error=error,
|
||||
)
|
||||
@@ -94,8 +94,21 @@ __all__ = [
|
||||
'CapabilitiesHandler',
|
||||
# Performance benchmarking
|
||||
'PerformanceHandler',
|
||||
# Comprehensive benchmarking
|
||||
'BenchmarkHandler',
|
||||
# Lite agent (BYO-LLM)
|
||||
'LiteHandler',
|
||||
# Agent-centric tools (LSP/ACP powered)
|
||||
'create_agent_centric_tools',
|
||||
'InteractiveRuntime',
|
||||
'RuntimeConfig',
|
||||
'CodeIntelligenceRouter',
|
||||
'ActionOrchestrator',
|
||||
# Interactive tools provider (canonical source)
|
||||
'get_interactive_tools',
|
||||
'resolve_tool_groups',
|
||||
'ToolConfig',
|
||||
'TOOL_GROUPS',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -222,7 +235,39 @@ def __getattr__(name):
|
||||
elif name == 'PerformanceHandler':
|
||||
from .performance import PerformanceHandler
|
||||
return PerformanceHandler
|
||||
elif name == 'BenchmarkHandler':
|
||||
from .benchmark import BenchmarkHandler
|
||||
return BenchmarkHandler
|
||||
elif name == 'LiteHandler':
|
||||
from .lite import LiteHandler
|
||||
return LiteHandler
|
||||
# Agent-centric tools (LSP/ACP powered)
|
||||
elif name == 'create_agent_centric_tools':
|
||||
from .agent_tools import create_agent_centric_tools
|
||||
return create_agent_centric_tools
|
||||
elif name == 'InteractiveRuntime':
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
return InteractiveRuntime
|
||||
elif name == 'RuntimeConfig':
|
||||
from .interactive_runtime import RuntimeConfig
|
||||
return RuntimeConfig
|
||||
elif name == 'CodeIntelligenceRouter':
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
return CodeIntelligenceRouter
|
||||
elif name == 'ActionOrchestrator':
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
return ActionOrchestrator
|
||||
# Interactive tools provider (canonical source)
|
||||
elif name == 'get_interactive_tools':
|
||||
from .interactive_tools import get_interactive_tools
|
||||
return get_interactive_tools
|
||||
elif name == 'resolve_tool_groups':
|
||||
from .interactive_tools import resolve_tool_groups
|
||||
return resolve_tool_groups
|
||||
elif name == 'ToolConfig':
|
||||
from .interactive_tools import ToolConfig
|
||||
return ToolConfig
|
||||
elif name == 'TOOL_GROUPS':
|
||||
from .interactive_tools import TOOL_GROUPS
|
||||
return TOOL_GROUPS
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
Action Orchestrator for PraisonAI.
|
||||
|
||||
Orchestrates code modifications through ACP with plan → approval → apply → verify steps.
|
||||
Enforces read-only mode when ACP is unavailable.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
"""Types of actions that can be orchestrated."""
|
||||
FILE_CREATE = "file_create"
|
||||
FILE_EDIT = "file_edit"
|
||||
FILE_DELETE = "file_delete"
|
||||
FILE_RENAME = "file_rename"
|
||||
SHELL_COMMAND = "shell_command"
|
||||
REFACTOR = "refactor"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class ActionStatus(Enum):
|
||||
"""Status of an action."""
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
APPLIED = "applied"
|
||||
FAILED = "failed"
|
||||
VERIFIED = "verified"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionStep:
|
||||
"""A single step in an action plan."""
|
||||
id: str
|
||||
action_type: ActionType
|
||||
description: str
|
||||
target: str # File path or command
|
||||
params: Dict[str, Any] = field(default_factory=dict)
|
||||
status: ActionStatus = ActionStatus.PENDING
|
||||
result: Optional[Any] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionPlan:
|
||||
"""A plan of actions to execute."""
|
||||
id: str
|
||||
prompt: str
|
||||
steps: List[ActionStep] = field(default_factory=list)
|
||||
created_at: float = field(default_factory=time.time)
|
||||
status: ActionStatus = ActionStatus.PENDING
|
||||
approval_mode: str = "manual"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"prompt": self.prompt,
|
||||
"steps": [
|
||||
{
|
||||
"id": s.id,
|
||||
"action_type": s.action_type.value,
|
||||
"description": s.description,
|
||||
"target": s.target,
|
||||
"params": s.params,
|
||||
"status": s.status.value,
|
||||
"result": s.result,
|
||||
"error": s.error
|
||||
}
|
||||
for s in self.steps
|
||||
],
|
||||
"created_at": self.created_at,
|
||||
"status": self.status.value,
|
||||
"approval_mode": self.approval_mode
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionResult:
|
||||
"""Result of action orchestration."""
|
||||
success: bool
|
||||
plan: Optional[ActionPlan] = None
|
||||
applied_steps: int = 0
|
||||
failed_steps: int = 0
|
||||
error: Optional[str] = None
|
||||
read_only_blocked: bool = False
|
||||
diff_summary: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
"plan": self.plan.to_dict() if self.plan else None,
|
||||
"applied_steps": self.applied_steps,
|
||||
"failed_steps": self.failed_steps,
|
||||
"error": self.error,
|
||||
"read_only_blocked": self.read_only_blocked,
|
||||
"diff_summary": self.diff_summary
|
||||
}
|
||||
|
||||
|
||||
class ActionOrchestrator:
|
||||
"""
|
||||
Orchestrates code modifications through ACP.
|
||||
|
||||
All edits must go through:
|
||||
1. Plan creation
|
||||
2. Approval (manual, auto, or scoped)
|
||||
3. Application
|
||||
4. Verification
|
||||
|
||||
If ACP is unavailable, runtime is read-only and edits are blocked.
|
||||
"""
|
||||
|
||||
def __init__(self, runtime: "InteractiveRuntime"):
|
||||
"""Initialize with runtime reference."""
|
||||
self.runtime = runtime
|
||||
self._plan_counter = 0
|
||||
|
||||
def _generate_plan_id(self) -> str:
|
||||
"""Generate a unique plan ID."""
|
||||
self._plan_counter += 1
|
||||
return f"plan_{int(time.time())}_{self._plan_counter}"
|
||||
|
||||
def _generate_step_id(self, plan_id: str, index: int) -> str:
|
||||
"""Generate a unique step ID."""
|
||||
return f"{plan_id}_step_{index}"
|
||||
|
||||
async def create_plan(self, prompt: str) -> ActionResult:
|
||||
"""
|
||||
Create an action plan for a modification request.
|
||||
|
||||
Args:
|
||||
prompt: The user's modification request
|
||||
|
||||
Returns:
|
||||
ActionResult with the plan
|
||||
"""
|
||||
if self.runtime.read_only:
|
||||
return ActionResult(
|
||||
success=False,
|
||||
error="Runtime is in read-only mode. ACP is required for modifications.",
|
||||
read_only_blocked=True
|
||||
)
|
||||
|
||||
plan_id = self._generate_plan_id()
|
||||
|
||||
# Always use fallback plan creation (ACP session tracks but doesn't create plans)
|
||||
# In production, this would integrate with an LLM for intelligent planning
|
||||
return await self._create_fallback_plan(prompt, plan_id)
|
||||
|
||||
async def _create_fallback_plan(self, prompt: str, plan_id: str) -> ActionResult:
|
||||
"""Create a fallback plan when ACP is not available for planning."""
|
||||
# This is a simplified fallback - in production, this would use LLM
|
||||
steps = []
|
||||
|
||||
# Analyze prompt for common patterns
|
||||
prompt_lower = prompt.lower()
|
||||
|
||||
if "create" in prompt_lower or "new file" in prompt_lower:
|
||||
# Extract file path if mentioned
|
||||
import re
|
||||
file_match = re.search(r'(?:create|new)\s+(?:a\s+)?(?:file\s+)?(?:called\s+)?([^\s]+\.\w+)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else "new_file.py"
|
||||
|
||||
# Generate default content based on file type
|
||||
if target.endswith('.py'):
|
||||
content = f'"""\n{target} - Auto-generated file\n"""\n\ndef main():\n pass\n\n\nif __name__ == "__main__":\n main()\n'
|
||||
else:
|
||||
content = f"// {target} - Auto-generated file\n"
|
||||
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.FILE_CREATE,
|
||||
description=f"Create file: {target}",
|
||||
target=target,
|
||||
params={"content": content},
|
||||
status=ActionStatus.APPROVED # Auto-approve for fallback
|
||||
))
|
||||
|
||||
elif "edit" in prompt_lower or "modify" in prompt_lower or "change" in prompt_lower:
|
||||
file_match = re.search(r'(?:edit|modify|change)\s+([^\s]+\.\w+)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else ""
|
||||
|
||||
if target:
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.FILE_EDIT,
|
||||
description=f"Edit file: {target}",
|
||||
target=target,
|
||||
params={"changes": prompt}
|
||||
))
|
||||
|
||||
elif "delete" in prompt_lower or "remove" in prompt_lower:
|
||||
file_match = re.search(r'(?:delete|remove)\s+([^\s]+\.\w+)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else ""
|
||||
|
||||
if target:
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.FILE_DELETE,
|
||||
description=f"Delete file: {target}",
|
||||
target=target
|
||||
))
|
||||
|
||||
elif "rename" in prompt_lower:
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.REFACTOR,
|
||||
description="Rename operation",
|
||||
target="",
|
||||
params={"prompt": prompt}
|
||||
))
|
||||
|
||||
elif "run" in prompt_lower or "execute" in prompt_lower:
|
||||
cmd_match = re.search(r'(?:run|execute)\s+[`"\']?([^`"\']+)[`"\']?', prompt_lower)
|
||||
command = cmd_match.group(1) if cmd_match else ""
|
||||
|
||||
if command:
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.SHELL_COMMAND,
|
||||
description=f"Execute: {command}",
|
||||
target=command
|
||||
))
|
||||
|
||||
if not steps:
|
||||
return ActionResult(
|
||||
success=False,
|
||||
error="Could not determine action from prompt. Please be more specific."
|
||||
)
|
||||
|
||||
plan = ActionPlan(
|
||||
id=plan_id,
|
||||
prompt=prompt,
|
||||
steps=steps,
|
||||
approval_mode=self.runtime.config.approval_mode
|
||||
)
|
||||
|
||||
return ActionResult(
|
||||
success=True,
|
||||
plan=plan
|
||||
)
|
||||
|
||||
def _classify_action_type(self, step_data: Dict[str, Any]) -> ActionType:
|
||||
"""Classify the action type from step data."""
|
||||
action = step_data.get("action", "").lower()
|
||||
|
||||
if "create" in action:
|
||||
return ActionType.FILE_CREATE
|
||||
elif "edit" in action or "modify" in action:
|
||||
return ActionType.FILE_EDIT
|
||||
elif "delete" in action or "remove" in action:
|
||||
return ActionType.FILE_DELETE
|
||||
elif "rename" in action:
|
||||
return ActionType.FILE_RENAME
|
||||
elif "shell" in action or "command" in action or "run" in action:
|
||||
return ActionType.SHELL_COMMAND
|
||||
elif "refactor" in action:
|
||||
return ActionType.REFACTOR
|
||||
|
||||
return ActionType.UNKNOWN
|
||||
|
||||
async def approve_plan(self, plan: ActionPlan, auto: bool = False) -> bool:
|
||||
"""
|
||||
Request approval for a plan.
|
||||
|
||||
Args:
|
||||
plan: The plan to approve
|
||||
auto: Whether to auto-approve
|
||||
|
||||
Returns:
|
||||
True if approved, False if rejected
|
||||
"""
|
||||
if self.runtime.read_only:
|
||||
logger.warning("Cannot approve plan in read-only mode")
|
||||
return False
|
||||
|
||||
approval_mode = self.runtime.config.approval_mode
|
||||
|
||||
if approval_mode == "auto" or auto:
|
||||
plan.status = ActionStatus.APPROVED
|
||||
for step in plan.steps:
|
||||
step.status = ActionStatus.APPROVED
|
||||
return True
|
||||
|
||||
elif approval_mode == "scoped":
|
||||
# Auto-approve safe actions, require manual for dangerous ones
|
||||
dangerous_types = {ActionType.FILE_DELETE, ActionType.SHELL_COMMAND}
|
||||
|
||||
for step in plan.steps:
|
||||
if step.action_type in dangerous_types:
|
||||
step.status = ActionStatus.PENDING # Needs manual approval
|
||||
else:
|
||||
step.status = ActionStatus.APPROVED
|
||||
|
||||
# Plan is approved if all steps are approved
|
||||
if all(s.status == ActionStatus.APPROVED for s in plan.steps):
|
||||
plan.status = ActionStatus.APPROVED
|
||||
return True
|
||||
|
||||
return False # Some steps need manual approval
|
||||
|
||||
else: # manual
|
||||
# In manual mode, we return False to indicate approval is needed
|
||||
return False
|
||||
|
||||
async def apply_plan(self, plan: ActionPlan, force: bool = False) -> ActionResult:
|
||||
"""
|
||||
Apply an approved plan.
|
||||
|
||||
Args:
|
||||
plan: The plan to apply
|
||||
force: Force apply even if not fully approved
|
||||
|
||||
Returns:
|
||||
ActionResult with application status
|
||||
"""
|
||||
if self.runtime.read_only:
|
||||
return ActionResult(
|
||||
success=False,
|
||||
plan=plan,
|
||||
error="Runtime is in read-only mode",
|
||||
read_only_blocked=True
|
||||
)
|
||||
|
||||
if plan.status != ActionStatus.APPROVED and not force:
|
||||
return ActionResult(
|
||||
success=False,
|
||||
plan=plan,
|
||||
error="Plan is not approved"
|
||||
)
|
||||
|
||||
applied = 0
|
||||
failed = 0
|
||||
diff_parts = []
|
||||
|
||||
# Track in ACP session if available (but always do manual application)
|
||||
if self.runtime.acp_ready:
|
||||
try:
|
||||
auto_approve = self.runtime.config.approval_mode == "auto"
|
||||
await self.runtime.acp_apply_plan(
|
||||
plan.to_dict(),
|
||||
auto_approve=auto_approve
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"ACP tracking failed: {e}")
|
||||
|
||||
# Manual application (always execute)
|
||||
for step in plan.steps:
|
||||
if step.status not in [ActionStatus.APPROVED, ActionStatus.PENDING]:
|
||||
continue
|
||||
|
||||
try:
|
||||
result = await self._apply_step(step)
|
||||
if result:
|
||||
step.status = ActionStatus.APPLIED
|
||||
step.result = result
|
||||
applied += 1
|
||||
if isinstance(result, dict) and "diff" in result:
|
||||
diff_parts.append(result["diff"])
|
||||
else:
|
||||
step.status = ActionStatus.FAILED
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
step.status = ActionStatus.FAILED
|
||||
step.error = str(e)
|
||||
failed += 1
|
||||
|
||||
plan.status = ActionStatus.APPLIED if failed == 0 else ActionStatus.FAILED
|
||||
|
||||
return ActionResult(
|
||||
success=failed == 0,
|
||||
plan=plan,
|
||||
applied_steps=applied,
|
||||
failed_steps=failed,
|
||||
diff_summary="\n".join(diff_parts) if diff_parts else None
|
||||
)
|
||||
|
||||
async def _apply_step(self, step: ActionStep) -> Optional[Dict[str, Any]]:
|
||||
"""Apply a single step."""
|
||||
workspace = Path(self.runtime.config.workspace)
|
||||
|
||||
if step.action_type == ActionType.FILE_CREATE:
|
||||
target = workspace / step.target
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = step.params.get("content", "")
|
||||
target.write_text(content)
|
||||
return {"created": str(target), "diff": f"+++ {target}\n+ (new file)"}
|
||||
|
||||
elif step.action_type == ActionType.FILE_EDIT:
|
||||
target = workspace / step.target
|
||||
if not target.exists():
|
||||
return None
|
||||
|
||||
old_content = target.read_text()
|
||||
new_content = step.params.get("new_content", old_content)
|
||||
target.write_text(new_content)
|
||||
|
||||
return {
|
||||
"edited": str(target),
|
||||
"diff": f"--- {target}\n+++ {target}\n(content changed)"
|
||||
}
|
||||
|
||||
elif step.action_type == ActionType.FILE_DELETE:
|
||||
target = workspace / step.target
|
||||
if target.exists():
|
||||
target.unlink()
|
||||
return {"deleted": str(target), "diff": f"--- {target}\n- (deleted)"}
|
||||
return None
|
||||
|
||||
elif step.action_type == ActionType.FILE_RENAME:
|
||||
source = workspace / step.params.get("source", "")
|
||||
dest = workspace / step.params.get("dest", "")
|
||||
if source.exists():
|
||||
source.rename(dest)
|
||||
return {"renamed": f"{source} -> {dest}"}
|
||||
return None
|
||||
|
||||
elif step.action_type == ActionType.SHELL_COMMAND:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
step.target,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(workspace),
|
||||
timeout=30
|
||||
)
|
||||
return {
|
||||
"command": step.target,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
"returncode": result.returncode
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
async def verify_plan(self, plan: ActionPlan) -> ActionResult:
|
||||
"""
|
||||
Verify that a plan was applied correctly.
|
||||
|
||||
Args:
|
||||
plan: The plan to verify
|
||||
|
||||
Returns:
|
||||
ActionResult with verification status
|
||||
"""
|
||||
if plan.status != ActionStatus.APPLIED:
|
||||
return ActionResult(
|
||||
success=False,
|
||||
plan=plan,
|
||||
error="Plan was not applied"
|
||||
)
|
||||
|
||||
workspace = Path(self.runtime.config.workspace)
|
||||
verified = 0
|
||||
failed = 0
|
||||
|
||||
for step in plan.steps:
|
||||
if step.status != ActionStatus.APPLIED:
|
||||
continue
|
||||
|
||||
try:
|
||||
if step.action_type == ActionType.FILE_CREATE:
|
||||
target = workspace / step.target
|
||||
if target.exists():
|
||||
step.status = ActionStatus.VERIFIED
|
||||
verified += 1
|
||||
else:
|
||||
step.status = ActionStatus.FAILED
|
||||
step.error = "File was not created"
|
||||
failed += 1
|
||||
|
||||
elif step.action_type == ActionType.FILE_DELETE:
|
||||
target = workspace / step.target
|
||||
if not target.exists():
|
||||
step.status = ActionStatus.VERIFIED
|
||||
verified += 1
|
||||
else:
|
||||
step.status = ActionStatus.FAILED
|
||||
step.error = "File was not deleted"
|
||||
failed += 1
|
||||
|
||||
else:
|
||||
# For other types, assume verified if applied
|
||||
step.status = ActionStatus.VERIFIED
|
||||
verified += 1
|
||||
|
||||
except Exception as e:
|
||||
step.status = ActionStatus.FAILED
|
||||
step.error = str(e)
|
||||
failed += 1
|
||||
|
||||
plan.status = ActionStatus.VERIFIED if failed == 0 else ActionStatus.FAILED
|
||||
|
||||
return ActionResult(
|
||||
success=failed == 0,
|
||||
plan=plan,
|
||||
applied_steps=verified,
|
||||
failed_steps=failed
|
||||
)
|
||||
|
||||
async def execute(self, prompt: str, auto_approve: bool = False) -> ActionResult:
|
||||
"""
|
||||
Execute a full plan → approve → apply → verify cycle.
|
||||
|
||||
Args:
|
||||
prompt: The modification request
|
||||
auto_approve: Whether to auto-approve
|
||||
|
||||
Returns:
|
||||
ActionResult with final status
|
||||
"""
|
||||
# Create plan
|
||||
result = await self.create_plan(prompt)
|
||||
if not result.success:
|
||||
return result
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Approve
|
||||
approved = await self.approve_plan(plan, auto=auto_approve)
|
||||
if not approved and self.runtime.config.approval_mode == "manual":
|
||||
return ActionResult(
|
||||
success=False,
|
||||
plan=plan,
|
||||
error="Plan requires manual approval"
|
||||
)
|
||||
|
||||
# Apply
|
||||
result = await self.apply_plan(plan)
|
||||
if not result.success:
|
||||
return result
|
||||
|
||||
# Verify
|
||||
return await self.verify_plan(plan)
|
||||
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
Agent-Centric Tools for PraisonAI Interactive Mode.
|
||||
|
||||
These tools route file operations and code intelligence through LSP/ACP,
|
||||
making the Agent the central orchestrator for all actions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Callable, Dict, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_agent_centric_tools(
|
||||
runtime: "InteractiveRuntime",
|
||||
router: "CodeIntelligenceRouter" = None,
|
||||
orchestrator: "ActionOrchestrator" = None
|
||||
) -> List[Callable]:
|
||||
"""
|
||||
Create tools that route through LSP/ACP for agent-centric architecture.
|
||||
|
||||
Args:
|
||||
runtime: The InteractiveRuntime instance
|
||||
router: Optional CodeIntelligenceRouter (created if not provided)
|
||||
orchestrator: Optional ActionOrchestrator (created if not provided)
|
||||
|
||||
Returns:
|
||||
List of tool functions for the Agent
|
||||
"""
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
if router is None:
|
||||
router = CodeIntelligenceRouter(runtime)
|
||||
if orchestrator is None:
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
|
||||
# Helper to run async functions synchronously
|
||||
def run_async(coro):
|
||||
"""Run async coroutine synchronously."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# Create a new loop in a thread
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(asyncio.run, coro)
|
||||
return future.result(timeout=60)
|
||||
else:
|
||||
return loop.run_until_complete(coro)
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
|
||||
# =========================================================================
|
||||
# ACP-Powered File Tools
|
||||
# =========================================================================
|
||||
|
||||
def acp_create_file(filepath: str, content: str) -> str:
|
||||
"""
|
||||
Create a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
This tool routes file creation through the ActionOrchestrator,
|
||||
ensuring proper tracking, approval, and verification.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to create (relative to workspace)
|
||||
content: Content to write to the file
|
||||
|
||||
Returns:
|
||||
JSON string with result including plan status and verification
|
||||
"""
|
||||
async def _create():
|
||||
# Build a detailed prompt for the orchestrator
|
||||
prompt = f"create file {filepath}"
|
||||
|
||||
# Create plan
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"read_only": result.read_only_blocked
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Update the plan step with actual content
|
||||
if plan.steps:
|
||||
plan.steps[0].params["content"] = content
|
||||
|
||||
# Approve based on runtime config
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved and runtime.config.approval_mode == "manual":
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Manual approval required",
|
||||
"plan": plan.to_dict(),
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
# Apply the plan
|
||||
result = await orchestrator.apply_plan(plan, force=auto_approve)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"plan": plan.to_dict()
|
||||
})
|
||||
|
||||
# Verify
|
||||
result = await orchestrator.verify_plan(plan)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_created": filepath,
|
||||
"plan_id": plan.id,
|
||||
"status": plan.status.value,
|
||||
"verified": result.success
|
||||
})
|
||||
|
||||
return run_async(_create())
|
||||
|
||||
def acp_edit_file(filepath: str, new_content: str) -> str:
|
||||
"""
|
||||
Edit a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to edit (relative to workspace)
|
||||
new_content: New content for the file
|
||||
|
||||
Returns:
|
||||
JSON string with result including plan status
|
||||
"""
|
||||
async def _edit():
|
||||
prompt = f"edit file {filepath}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"read_only": result.read_only_blocked
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Update step with new content
|
||||
if plan.steps:
|
||||
plan.steps[0].params["new_content"] = new_content
|
||||
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved and runtime.config.approval_mode == "manual":
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Manual approval required",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan, force=auto_approve)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_edited": filepath,
|
||||
"plan_id": plan.id,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
return run_async(_edit())
|
||||
|
||||
def acp_delete_file(filepath: str) -> str:
|
||||
"""
|
||||
Delete a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to delete
|
||||
|
||||
Returns:
|
||||
JSON string with result
|
||||
"""
|
||||
async def _delete():
|
||||
prompt = f"delete file {filepath}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Delete requires explicit approval even in auto mode
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Delete requires approval",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan)
|
||||
result = await orchestrator.verify_plan(plan)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_deleted": filepath,
|
||||
"verified": result.success
|
||||
})
|
||||
|
||||
return run_async(_delete())
|
||||
|
||||
def acp_execute_command(command: str, cwd: str = None) -> str:
|
||||
"""
|
||||
Execute a shell command through ACP with tracking.
|
||||
|
||||
Args:
|
||||
command: The command to execute
|
||||
cwd: Working directory (optional)
|
||||
|
||||
Returns:
|
||||
JSON string with command output
|
||||
"""
|
||||
async def _execute():
|
||||
prompt = f"run command: {command}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Commands require approval
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Command requires approval",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan)
|
||||
|
||||
# Extract command result
|
||||
if plan.steps and plan.steps[0].result:
|
||||
cmd_result = plan.steps[0].result
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"command": command,
|
||||
"stdout": cmd_result.get("stdout", ""),
|
||||
"stderr": cmd_result.get("stderr", ""),
|
||||
"returncode": cmd_result.get("returncode", -1)
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
return run_async(_execute())
|
||||
|
||||
# =========================================================================
|
||||
# LSP-Powered Code Intelligence Tools
|
||||
# =========================================================================
|
||||
|
||||
def lsp_list_symbols(file_path: str) -> str:
|
||||
"""
|
||||
List all symbols (functions, classes, methods) in a file using LSP.
|
||||
|
||||
Falls back to regex-based extraction if LSP is unavailable.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to analyze
|
||||
|
||||
Returns:
|
||||
JSON string with list of symbols and their locations
|
||||
"""
|
||||
async def _list():
|
||||
result = await router.handle_query(
|
||||
f"list all functions and classes in {file_path}",
|
||||
file_path=file_path
|
||||
)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_list())
|
||||
|
||||
def lsp_find_definition(symbol: str, file_path: str = None) -> str:
|
||||
"""
|
||||
Find where a symbol is defined using LSP.
|
||||
|
||||
Args:
|
||||
symbol: The symbol name to find
|
||||
file_path: Optional file path for context
|
||||
|
||||
Returns:
|
||||
JSON string with definition location(s)
|
||||
"""
|
||||
async def _find():
|
||||
query = f"where is {symbol} defined"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_find())
|
||||
|
||||
def lsp_find_references(symbol: str, file_path: str = None) -> str:
|
||||
"""
|
||||
Find all references to a symbol using LSP.
|
||||
|
||||
Args:
|
||||
symbol: The symbol name to find references for
|
||||
file_path: Optional file path for context
|
||||
|
||||
Returns:
|
||||
JSON string with reference locations
|
||||
"""
|
||||
async def _find():
|
||||
query = f"find all references to {symbol}"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_find())
|
||||
|
||||
def lsp_get_diagnostics(file_path: str = None) -> str:
|
||||
"""
|
||||
Get diagnostics (errors, warnings) for a file using LSP.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file (optional, gets all if not specified)
|
||||
|
||||
Returns:
|
||||
JSON string with diagnostic information
|
||||
"""
|
||||
async def _get():
|
||||
query = "show all errors and warnings"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_get())
|
||||
|
||||
# =========================================================================
|
||||
# Basic File Tools (for read-only operations)
|
||||
# =========================================================================
|
||||
|
||||
def read_file(filepath: str) -> str:
|
||||
"""
|
||||
Read content from a file.
|
||||
|
||||
This is a read-only operation that doesn't require ACP.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to read
|
||||
|
||||
Returns:
|
||||
File content or error message
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
path = Path(filepath)
|
||||
if not path.is_absolute():
|
||||
path = Path(runtime.config.workspace) / filepath
|
||||
|
||||
if not path.exists():
|
||||
return json.dumps({"error": f"File not found: {filepath}"})
|
||||
|
||||
content = path.read_text()
|
||||
|
||||
# Track in trace if enabled
|
||||
if runtime._trace:
|
||||
runtime._trace.add_entry(
|
||||
category="file",
|
||||
action="read",
|
||||
params={"file": str(path)},
|
||||
result={"size": len(content)}
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def list_files(directory: str = ".", pattern: str = "*") -> str:
|
||||
"""
|
||||
List files in a directory.
|
||||
|
||||
Args:
|
||||
directory: Directory to list (relative to workspace)
|
||||
pattern: Glob pattern to filter files
|
||||
|
||||
Returns:
|
||||
JSON string with list of files
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
path = Path(directory)
|
||||
if not path.is_absolute():
|
||||
path = Path(runtime.config.workspace) / directory
|
||||
|
||||
if not path.exists():
|
||||
return json.dumps({"error": f"Directory not found: {directory}"})
|
||||
|
||||
files = []
|
||||
for f in path.glob(pattern):
|
||||
files.append({
|
||||
"name": f.name,
|
||||
"path": str(f.relative_to(runtime.config.workspace)),
|
||||
"is_dir": f.is_dir(),
|
||||
"size": f.stat().st_size if f.is_file() else None
|
||||
})
|
||||
|
||||
return json.dumps({"files": files, "count": len(files)})
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
# Return all tools
|
||||
return [
|
||||
# ACP-powered file tools
|
||||
acp_create_file,
|
||||
acp_edit_file,
|
||||
acp_delete_file,
|
||||
acp_execute_command,
|
||||
# LSP-powered code intelligence
|
||||
lsp_list_symbols,
|
||||
lsp_find_definition,
|
||||
lsp_find_references,
|
||||
lsp_get_diagnostics,
|
||||
# Basic read-only tools
|
||||
read_file,
|
||||
list_files,
|
||||
]
|
||||
|
||||
|
||||
def get_tool_descriptions() -> Dict[str, str]:
|
||||
"""Get descriptions of all agent-centric tools."""
|
||||
return {
|
||||
"acp_create_file": "Create a file through ACP with plan/approve/apply/verify",
|
||||
"acp_edit_file": "Edit a file through ACP with tracking",
|
||||
"acp_delete_file": "Delete a file through ACP (requires approval)",
|
||||
"acp_execute_command": "Execute a shell command through ACP",
|
||||
"lsp_list_symbols": "List symbols in a file using LSP",
|
||||
"lsp_find_definition": "Find where a symbol is defined",
|
||||
"lsp_find_references": "Find all references to a symbol",
|
||||
"lsp_get_diagnostics": "Get errors and warnings for a file",
|
||||
"read_file": "Read content from a file (read-only)",
|
||||
"list_files": "List files in a directory",
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,652 @@
|
||||
"""
|
||||
Code Intelligence Router for PraisonAI.
|
||||
|
||||
Routes code-related queries to LSP when available, with fallback to grep/file search.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CodeIntent(Enum):
|
||||
"""Classification of code-related intents."""
|
||||
LIST_SYMBOLS = "list_symbols"
|
||||
GO_TO_DEFINITION = "go_to_definition"
|
||||
FIND_REFERENCES = "find_references"
|
||||
GET_DIAGNOSTICS = "get_diagnostics"
|
||||
EXPLAIN_CODE = "explain_code"
|
||||
SEARCH_CODE = "search_code"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeQueryResult:
|
||||
"""Result of a code intelligence query."""
|
||||
intent: CodeIntent
|
||||
success: bool
|
||||
lsp_used: bool
|
||||
data: Any = None
|
||||
citations: List[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
fallback_used: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.citations is None:
|
||||
self.citations = []
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"intent": self.intent.value,
|
||||
"success": self.success,
|
||||
"lsp_used": self.lsp_used,
|
||||
"data": self.data,
|
||||
"citations": self.citations,
|
||||
"error": self.error,
|
||||
"fallback_used": self.fallback_used
|
||||
}
|
||||
|
||||
|
||||
class CodeIntelligenceRouter:
|
||||
"""
|
||||
Routes code intelligence queries to LSP or fallback mechanisms.
|
||||
|
||||
LSP-first approach:
|
||||
- If LSP is available, use it for semantic queries
|
||||
- If LSP fails or unavailable, fall back to grep/file search
|
||||
- Always provide citations with file:line references
|
||||
"""
|
||||
|
||||
# Patterns for intent classification
|
||||
INTENT_PATTERNS = {
|
||||
CodeIntent.LIST_SYMBOLS: [
|
||||
r"list\s+(all\s+)?(functions?|classes?|methods?|symbols?)",
|
||||
r"what\s+(functions?|classes?|methods?)\s+(are|exist)",
|
||||
r"show\s+(me\s+)?(all\s+)?(functions?|classes?|symbols?)",
|
||||
r"find\s+(all\s+)?(functions?|classes?|methods?)",
|
||||
],
|
||||
CodeIntent.GO_TO_DEFINITION: [
|
||||
r"(go\s+to|find|show|where\s+is)\s+(the\s+)?definition",
|
||||
r"where\s+is\s+(\w+)\s+defined",
|
||||
r"definition\s+of\s+(\w+)",
|
||||
r"(\w+)\s+is\s+defined\s+where",
|
||||
],
|
||||
CodeIntent.FIND_REFERENCES: [
|
||||
r"(find|show|list)\s+(all\s+)?references",
|
||||
r"where\s+is\s+(\w+)\s+used",
|
||||
r"usages?\s+of\s+(\w+)",
|
||||
r"who\s+(calls?|uses?)\s+(\w+)",
|
||||
],
|
||||
CodeIntent.GET_DIAGNOSTICS: [
|
||||
r"(show|list|get)\s+(all\s+)?(errors?|warnings?|diagnostics?|problems?)",
|
||||
r"what('s|\s+is)\s+wrong",
|
||||
r"any\s+(errors?|issues?|problems?)",
|
||||
r"check\s+(for\s+)?(errors?|issues?)",
|
||||
],
|
||||
CodeIntent.EXPLAIN_CODE: [
|
||||
r"explain\s+(this|the)\s+code",
|
||||
r"what\s+does\s+(this|the)\s+code\s+do",
|
||||
r"how\s+does\s+(\w+)\s+work",
|
||||
],
|
||||
CodeIntent.SEARCH_CODE: [
|
||||
r"search\s+(for\s+)?",
|
||||
r"find\s+(the\s+)?string",
|
||||
r"grep\s+",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, runtime: "InteractiveRuntime"):
|
||||
"""Initialize with runtime reference."""
|
||||
self.runtime = runtime
|
||||
|
||||
def classify_intent(self, query: str) -> CodeIntent:
|
||||
"""Classify the intent of a code query."""
|
||||
query_lower = query.lower()
|
||||
|
||||
for intent, patterns in self.INTENT_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, query_lower):
|
||||
return intent
|
||||
|
||||
return CodeIntent.UNKNOWN
|
||||
|
||||
async def handle_query(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""
|
||||
Handle a code intelligence query.
|
||||
|
||||
Args:
|
||||
query: The user's query
|
||||
file_path: Optional file path context
|
||||
|
||||
Returns:
|
||||
CodeQueryResult with data and citations
|
||||
"""
|
||||
intent = self.classify_intent(query)
|
||||
|
||||
if intent == CodeIntent.LIST_SYMBOLS:
|
||||
return await self._handle_list_symbols(query, file_path)
|
||||
elif intent == CodeIntent.GO_TO_DEFINITION:
|
||||
return await self._handle_go_to_definition(query, file_path)
|
||||
elif intent == CodeIntent.FIND_REFERENCES:
|
||||
return await self._handle_find_references(query, file_path)
|
||||
elif intent == CodeIntent.GET_DIAGNOSTICS:
|
||||
return await self._handle_get_diagnostics(query, file_path)
|
||||
elif intent == CodeIntent.SEARCH_CODE:
|
||||
return await self._handle_search_code(query, file_path)
|
||||
else:
|
||||
return CodeQueryResult(
|
||||
intent=intent,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="Could not determine code query intent"
|
||||
)
|
||||
|
||||
async def _handle_list_symbols(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""Handle list symbols query."""
|
||||
if not file_path:
|
||||
# Try to find a file from the query
|
||||
file_path = self._extract_file_from_query(query)
|
||||
|
||||
if not file_path:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.LIST_SYMBOLS,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="No file specified. Please specify a file path."
|
||||
)
|
||||
|
||||
# Try LSP first
|
||||
if self.runtime.lsp_ready:
|
||||
try:
|
||||
symbols = await self.runtime.lsp_get_symbols(file_path)
|
||||
if symbols:
|
||||
citations = [
|
||||
{"file": file_path, "type": "symbols", "count": len(symbols)}
|
||||
]
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.LIST_SYMBOLS,
|
||||
success=True,
|
||||
lsp_used=True,
|
||||
data=symbols,
|
||||
citations=citations
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"LSP symbols failed: {e}")
|
||||
|
||||
# Fallback to regex-based symbol extraction
|
||||
return await self._fallback_list_symbols(file_path)
|
||||
|
||||
async def _fallback_list_symbols(self, file_path: str) -> CodeQueryResult:
|
||||
"""Fallback symbol listing using regex."""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
path = Path(self.runtime.config.workspace) / file_path
|
||||
|
||||
if not path.exists():
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.LIST_SYMBOLS,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=f"File not found: {file_path}"
|
||||
)
|
||||
|
||||
content = path.read_text()
|
||||
symbols = []
|
||||
|
||||
# Python patterns
|
||||
if path.suffix == ".py":
|
||||
# Functions
|
||||
for match in re.finditer(r'^(async\s+)?def\s+(\w+)\s*\(', content, re.MULTILINE):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
symbols.append({
|
||||
"name": match.group(2),
|
||||
"kind": "function",
|
||||
"line": line_num
|
||||
})
|
||||
# Classes
|
||||
for match in re.finditer(r'^class\s+(\w+)\s*[:\(]', content, re.MULTILINE):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
symbols.append({
|
||||
"name": match.group(1),
|
||||
"kind": "class",
|
||||
"line": line_num
|
||||
})
|
||||
|
||||
# JavaScript/TypeScript patterns
|
||||
elif path.suffix in [".js", ".ts", ".jsx", ".tsx"]:
|
||||
# Functions
|
||||
for match in re.finditer(r'(?:async\s+)?function\s+(\w+)\s*\(', content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
symbols.append({
|
||||
"name": match.group(1),
|
||||
"kind": "function",
|
||||
"line": line_num
|
||||
})
|
||||
# Arrow functions assigned to const/let/var
|
||||
for match in re.finditer(r'(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(', content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
symbols.append({
|
||||
"name": match.group(1),
|
||||
"kind": "function",
|
||||
"line": line_num
|
||||
})
|
||||
# Classes
|
||||
for match in re.finditer(r'class\s+(\w+)\s*(?:extends|implements|{)', content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
symbols.append({
|
||||
"name": match.group(1),
|
||||
"kind": "class",
|
||||
"line": line_num
|
||||
})
|
||||
|
||||
citations = [
|
||||
{"file": str(path), "type": "symbols", "count": len(symbols)}
|
||||
]
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.LIST_SYMBOLS,
|
||||
success=True,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
data=symbols,
|
||||
citations=citations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.LIST_SYMBOLS,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _handle_go_to_definition(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""Handle go to definition query."""
|
||||
# Extract symbol name from query
|
||||
symbol = self._extract_symbol_from_query(query)
|
||||
|
||||
if not symbol:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GO_TO_DEFINITION,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="Could not identify symbol to find definition for"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
file_path = self._extract_file_from_query(query)
|
||||
|
||||
# Try LSP first
|
||||
if self.runtime.lsp_ready and file_path:
|
||||
try:
|
||||
# Need to find the symbol position first
|
||||
line, col = await self._find_symbol_position(file_path, symbol)
|
||||
if line is not None:
|
||||
definitions = await self.runtime.lsp_get_definition(file_path, line, col)
|
||||
if definitions:
|
||||
citations = [
|
||||
{"file": d.get("uri", "").replace("file://", ""),
|
||||
"line": d.get("range", {}).get("start", {}).get("line", 0) + 1,
|
||||
"type": "definition"}
|
||||
for d in definitions
|
||||
]
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GO_TO_DEFINITION,
|
||||
success=True,
|
||||
lsp_used=True,
|
||||
data={"symbol": symbol, "definitions": definitions},
|
||||
citations=citations
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"LSP definition failed: {e}")
|
||||
|
||||
# Fallback to grep
|
||||
return await self._fallback_find_definition(symbol)
|
||||
|
||||
async def _fallback_find_definition(self, symbol: str) -> CodeQueryResult:
|
||||
"""Fallback definition finding using grep."""
|
||||
try:
|
||||
import subprocess
|
||||
workspace = self.runtime.config.workspace
|
||||
|
||||
# Search for definition patterns
|
||||
patterns = [
|
||||
f"def {symbol}\\s*\\(", # Python function
|
||||
f"class {symbol}\\s*[:\\(]", # Python class
|
||||
f"function {symbol}\\s*\\(", # JS function
|
||||
f"const {symbol}\\s*=", # JS const
|
||||
f"let {symbol}\\s*=", # JS let
|
||||
]
|
||||
|
||||
results = []
|
||||
for pattern in patterns:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "-rn", "-E", pattern, workspace],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.stdout:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
parts = line.split(':', 2)
|
||||
if len(parts) >= 2:
|
||||
results.append({
|
||||
"file": parts[0],
|
||||
"line": int(parts[1]),
|
||||
"content": parts[2] if len(parts) > 2 else ""
|
||||
})
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
if results:
|
||||
citations = [
|
||||
{"file": r["file"], "line": r["line"], "type": "definition"}
|
||||
for r in results
|
||||
]
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GO_TO_DEFINITION,
|
||||
success=True,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
data={"symbol": symbol, "definitions": results},
|
||||
citations=citations
|
||||
)
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GO_TO_DEFINITION,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=f"Definition for '{symbol}' not found"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GO_TO_DEFINITION,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _handle_find_references(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""Handle find references query."""
|
||||
symbol = self._extract_symbol_from_query(query)
|
||||
|
||||
if not symbol:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.FIND_REFERENCES,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="Could not identify symbol to find references for"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
file_path = self._extract_file_from_query(query)
|
||||
|
||||
# Try LSP first
|
||||
if self.runtime.lsp_ready and file_path:
|
||||
try:
|
||||
line, col = await self._find_symbol_position(file_path, symbol)
|
||||
if line is not None:
|
||||
references = await self.runtime.lsp_get_references(file_path, line, col)
|
||||
if references:
|
||||
citations = [
|
||||
{"file": r.get("uri", "").replace("file://", ""),
|
||||
"line": r.get("range", {}).get("start", {}).get("line", 0) + 1,
|
||||
"type": "reference"}
|
||||
for r in references
|
||||
]
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.FIND_REFERENCES,
|
||||
success=True,
|
||||
lsp_used=True,
|
||||
data={"symbol": symbol, "references": references},
|
||||
citations=citations
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"LSP references failed: {e}")
|
||||
|
||||
# Fallback to grep
|
||||
return await self._fallback_find_references(symbol)
|
||||
|
||||
async def _fallback_find_references(self, symbol: str) -> CodeQueryResult:
|
||||
"""Fallback reference finding using grep."""
|
||||
try:
|
||||
import subprocess
|
||||
workspace = self.runtime.config.workspace
|
||||
|
||||
result = subprocess.run(
|
||||
["grep", "-rn", "-w", symbol, workspace],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
results = []
|
||||
if result.stdout:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
parts = line.split(':', 2)
|
||||
if len(parts) >= 2:
|
||||
results.append({
|
||||
"file": parts[0],
|
||||
"line": int(parts[1]),
|
||||
"content": parts[2] if len(parts) > 2 else ""
|
||||
})
|
||||
|
||||
if results:
|
||||
citations = [
|
||||
{"file": r["file"], "line": r["line"], "type": "reference"}
|
||||
for r in results
|
||||
]
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.FIND_REFERENCES,
|
||||
success=True,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
data={"symbol": symbol, "references": results},
|
||||
citations=citations
|
||||
)
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.FIND_REFERENCES,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=f"No references found for '{symbol}'"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.FIND_REFERENCES,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
fallback_used=True,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _handle_get_diagnostics(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""Handle get diagnostics query."""
|
||||
if not file_path:
|
||||
file_path = self._extract_file_from_query(query)
|
||||
|
||||
# Try LSP
|
||||
if self.runtime.lsp_ready:
|
||||
try:
|
||||
diagnostics = await self.runtime.lsp_get_diagnostics(file_path)
|
||||
citations = []
|
||||
if diagnostics:
|
||||
for d in diagnostics:
|
||||
if isinstance(d, dict):
|
||||
citations.append({
|
||||
"file": file_path or "workspace",
|
||||
"line": d.get("range", {}).get("start", {}).get("line", 0) + 1,
|
||||
"type": "diagnostic",
|
||||
"severity": d.get("severity", "unknown")
|
||||
})
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GET_DIAGNOSTICS,
|
||||
success=True,
|
||||
lsp_used=True,
|
||||
data=diagnostics,
|
||||
citations=citations
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"LSP diagnostics failed: {e}")
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.GET_DIAGNOSTICS,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="LSP not available for diagnostics"
|
||||
)
|
||||
|
||||
async def _handle_search_code(self, query: str, file_path: str = None) -> CodeQueryResult:
|
||||
"""Handle code search query."""
|
||||
# Extract search term
|
||||
search_term = self._extract_search_term(query)
|
||||
|
||||
if not search_term:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.SEARCH_CODE,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error="Could not identify search term"
|
||||
)
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
workspace = self.runtime.config.workspace
|
||||
|
||||
cmd = ["grep", "-rn", search_term]
|
||||
if file_path:
|
||||
cmd.append(file_path)
|
||||
else:
|
||||
cmd.append(workspace)
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
results = []
|
||||
if result.stdout:
|
||||
for line in result.stdout.strip().split('\n')[:50]: # Limit results
|
||||
if line:
|
||||
parts = line.split(':', 2)
|
||||
if len(parts) >= 2:
|
||||
results.append({
|
||||
"file": parts[0],
|
||||
"line": int(parts[1]),
|
||||
"content": parts[2] if len(parts) > 2 else ""
|
||||
})
|
||||
|
||||
citations = [
|
||||
{"file": r["file"], "line": r["line"], "type": "search_result"}
|
||||
for r in results
|
||||
]
|
||||
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.SEARCH_CODE,
|
||||
success=True,
|
||||
lsp_used=False,
|
||||
data={"search_term": search_term, "results": results},
|
||||
citations=citations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CodeQueryResult(
|
||||
intent=CodeIntent.SEARCH_CODE,
|
||||
success=False,
|
||||
lsp_used=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def _extract_file_from_query(self, query: str) -> Optional[str]:
|
||||
"""Extract file path from query."""
|
||||
# Look for file patterns
|
||||
patterns = [
|
||||
r'(?:in|from|file)\s+["\']?([^\s"\']+\.\w+)["\']?',
|
||||
r'([^\s]+\.(?:py|js|ts|tsx|jsx|go|rs|java|cpp|c|h))',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
def _extract_symbol_from_query(self, query: str) -> Optional[str]:
|
||||
"""Extract symbol name from query."""
|
||||
# Common patterns for symbol extraction
|
||||
patterns = [
|
||||
r'(?:definition\s+of|where\s+is|find|show)\s+["\']?(\w+)["\']?',
|
||||
r'["\'](\w+)["\']',
|
||||
r'`(\w+)`',
|
||||
r'(\w+)\s+(?:is\s+)?(?:defined|used)',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
symbol = match.group(1)
|
||||
# Filter out common words
|
||||
if symbol.lower() not in ['the', 'a', 'an', 'is', 'are', 'in', 'of', 'to', 'for']:
|
||||
return symbol
|
||||
|
||||
return None
|
||||
|
||||
def _extract_search_term(self, query: str) -> Optional[str]:
|
||||
"""Extract search term from query."""
|
||||
patterns = [
|
||||
r'search\s+(?:for\s+)?["\']([^"\']+)["\']',
|
||||
r'find\s+["\']([^"\']+)["\']',
|
||||
r'grep\s+["\']?([^\s"\']+)["\']?',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
async def _find_symbol_position(self, file_path: str, symbol: str) -> tuple:
|
||||
"""Find the position of a symbol in a file."""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
path = Path(self.runtime.config.workspace) / file_path
|
||||
|
||||
if not path.exists():
|
||||
return None, None
|
||||
|
||||
content = path.read_text()
|
||||
lines = content.split('\n')
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
col = line.find(symbol)
|
||||
if col >= 0:
|
||||
return i, col
|
||||
|
||||
return None, None
|
||||
except Exception:
|
||||
return None, None
|
||||
@@ -0,0 +1,810 @@
|
||||
"""
|
||||
Debug CLI handler for PraisonAI.
|
||||
|
||||
Provides non-interactive debug commands for testing interactive flows:
|
||||
- debug interactive: Run single interactive turn
|
||||
- debug lsp: Direct LSP probes
|
||||
- debug acp: Direct ACP probes
|
||||
- debug trace: Trace record/replay/diff
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebugHandler:
|
||||
"""Handler for debug CLI commands."""
|
||||
|
||||
@staticmethod
|
||||
def setup_subparser(subparsers) -> None:
|
||||
"""Set up the debug subcommand parser."""
|
||||
debug_parser = subparsers.add_parser(
|
||||
"debug",
|
||||
help="Debug and test interactive flows",
|
||||
description="Debug commands for testing interactive coding assistant flows"
|
||||
)
|
||||
|
||||
debug_subparsers = debug_parser.add_subparsers(dest="debug_command", help="Debug subcommands")
|
||||
|
||||
# debug interactive
|
||||
interactive_parser = debug_subparsers.add_parser(
|
||||
"interactive",
|
||||
help="Run single interactive turn non-interactively",
|
||||
description="Execute one interactive turn with full LSP/ACP pipeline"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"-p", "--prompt",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Prompt to execute"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--lsp",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Enable LSP (default: enabled)"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--no-lsp",
|
||||
action="store_true",
|
||||
help="Disable LSP"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--acp",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Enable ACP (default: enabled)"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--no-acp",
|
||||
action="store_true",
|
||||
help="Disable ACP"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--approval",
|
||||
type=str,
|
||||
choices=["manual", "auto", "scoped"],
|
||||
default="auto",
|
||||
help="Approval mode (default: auto for debug)"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--workspace", "-w",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Workspace root directory"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output JSON trace"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--trace-file",
|
||||
type=str,
|
||||
help="Save trace to file"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=60.0,
|
||||
help="Timeout in seconds"
|
||||
)
|
||||
interactive_parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
help="LLM model to use"
|
||||
)
|
||||
interactive_parser.set_defaults(func=DebugHandler.handle_interactive)
|
||||
|
||||
# debug lsp
|
||||
lsp_parser = debug_subparsers.add_parser(
|
||||
"lsp",
|
||||
help="Direct LSP probes",
|
||||
description="Probe LSP server directly"
|
||||
)
|
||||
lsp_subparsers = lsp_parser.add_subparsers(dest="lsp_command", help="LSP subcommands")
|
||||
|
||||
# debug lsp status
|
||||
lsp_status = lsp_subparsers.add_parser("status", help="Show LSP status")
|
||||
lsp_status.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_status.add_argument("--json", action="store_true")
|
||||
lsp_status.set_defaults(func=DebugHandler.handle_lsp_status)
|
||||
|
||||
# debug lsp symbols
|
||||
lsp_symbols = lsp_subparsers.add_parser("symbols", help="List symbols in file")
|
||||
lsp_symbols.add_argument("file", type=str, help="File path")
|
||||
lsp_symbols.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_symbols.add_argument("--json", action="store_true")
|
||||
lsp_symbols.set_defaults(func=DebugHandler.handle_lsp_symbols)
|
||||
|
||||
# debug lsp definition
|
||||
lsp_def = lsp_subparsers.add_parser("definition", help="Get definition location")
|
||||
lsp_def.add_argument("location", type=str, help="file:line:col")
|
||||
lsp_def.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_def.add_argument("--json", action="store_true")
|
||||
lsp_def.set_defaults(func=DebugHandler.handle_lsp_definition)
|
||||
|
||||
# debug lsp references
|
||||
lsp_refs = lsp_subparsers.add_parser("references", help="Get references")
|
||||
lsp_refs.add_argument("location", type=str, help="file:line:col")
|
||||
lsp_refs.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_refs.add_argument("--json", action="store_true")
|
||||
lsp_refs.set_defaults(func=DebugHandler.handle_lsp_references)
|
||||
|
||||
# debug lsp diagnostics
|
||||
lsp_diag = lsp_subparsers.add_parser("diagnostics", help="Get diagnostics")
|
||||
lsp_diag.add_argument("file", type=str, nargs="?", help="File path (optional)")
|
||||
lsp_diag.add_argument("--all", action="store_true", help="Get all diagnostics")
|
||||
lsp_diag.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_diag.add_argument("--json", action="store_true")
|
||||
lsp_diag.set_defaults(func=DebugHandler.handle_lsp_diagnostics)
|
||||
|
||||
# debug acp
|
||||
acp_parser = debug_subparsers.add_parser(
|
||||
"acp",
|
||||
help="Direct ACP probes",
|
||||
description="Probe ACP server directly"
|
||||
)
|
||||
acp_subparsers = acp_parser.add_subparsers(dest="acp_command", help="ACP subcommands")
|
||||
|
||||
# debug acp status
|
||||
acp_status = acp_subparsers.add_parser("status", help="Show ACP status")
|
||||
acp_status.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_status.add_argument("--json", action="store_true")
|
||||
acp_status.set_defaults(func=DebugHandler.handle_acp_status)
|
||||
|
||||
# debug acp plan
|
||||
acp_plan = acp_subparsers.add_parser("plan", help="Create action plan")
|
||||
acp_plan.add_argument("-p", "--prompt", type=str, required=True)
|
||||
acp_plan.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_plan.add_argument("--json", action="store_true")
|
||||
acp_plan.set_defaults(func=DebugHandler.handle_acp_plan)
|
||||
|
||||
# debug acp apply
|
||||
acp_apply = acp_subparsers.add_parser("apply", help="Apply action plan")
|
||||
acp_apply.add_argument("-p", "--prompt", type=str, required=True)
|
||||
acp_apply.add_argument("--approval", type=str, choices=["manual", "auto", "scoped"], default="auto")
|
||||
acp_apply.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_apply.add_argument("--json", action="store_true")
|
||||
acp_apply.set_defaults(func=DebugHandler.handle_acp_apply)
|
||||
|
||||
# debug trace
|
||||
trace_parser = debug_subparsers.add_parser(
|
||||
"trace",
|
||||
help="Trace record/replay/diff",
|
||||
description="Manage trace recordings"
|
||||
)
|
||||
trace_subparsers = trace_parser.add_subparsers(dest="trace_command", help="Trace subcommands")
|
||||
|
||||
# debug trace record
|
||||
trace_record = trace_subparsers.add_parser("record", help="Record session trace")
|
||||
trace_record.add_argument("-o", "--output", type=str, required=True, help="Output file")
|
||||
trace_record.set_defaults(func=DebugHandler.handle_trace_record)
|
||||
|
||||
# debug trace replay
|
||||
trace_replay = trace_subparsers.add_parser("replay", help="Replay recorded trace")
|
||||
trace_replay.add_argument("file", type=str, help="Trace file to replay")
|
||||
trace_replay.add_argument("--json", action="store_true")
|
||||
trace_replay.set_defaults(func=DebugHandler.handle_trace_replay)
|
||||
|
||||
# debug trace diff
|
||||
trace_diff = trace_subparsers.add_parser("diff", help="Compare two traces")
|
||||
trace_diff.add_argument("file1", type=str, help="First trace file")
|
||||
trace_diff.add_argument("file2", type=str, help="Second trace file")
|
||||
trace_diff.add_argument("--json", action="store_true")
|
||||
trace_diff.set_defaults(func=DebugHandler.handle_trace_diff)
|
||||
|
||||
@staticmethod
|
||||
def handle_interactive(args: argparse.Namespace) -> int:
|
||||
"""Handle debug interactive command."""
|
||||
return asyncio.run(DebugHandler._run_interactive(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_interactive(args: argparse.Namespace) -> int:
|
||||
"""Run interactive turn asynchronously."""
|
||||
from .interactive_runtime import create_runtime
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Create runtime
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=args.lsp and not args.no_lsp,
|
||||
acp=args.acp and not args.no_acp,
|
||||
approval=args.approval,
|
||||
trace=True,
|
||||
trace_file=args.trace_file,
|
||||
json_output=args.json,
|
||||
timeout=args.timeout,
|
||||
model=getattr(args, 'model', None)
|
||||
)
|
||||
|
||||
result = {
|
||||
"prompt": args.prompt,
|
||||
"workspace": args.workspace,
|
||||
"start_time": start_time
|
||||
}
|
||||
|
||||
try:
|
||||
# Start runtime
|
||||
status = await runtime.start()
|
||||
result["runtime_status"] = status
|
||||
|
||||
if not status.get("started"):
|
||||
result["error"] = "Failed to start runtime"
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 1
|
||||
|
||||
# Classify intent
|
||||
code_router = CodeIntelligenceRouter(runtime)
|
||||
action_orchestrator = ActionOrchestrator(runtime)
|
||||
|
||||
prompt = args.prompt.lower()
|
||||
|
||||
# Determine if this is a code query or action
|
||||
is_code_query = any(kw in prompt for kw in [
|
||||
"list", "show", "find", "where", "definition", "reference",
|
||||
"symbol", "function", "class", "diagnostic", "error"
|
||||
])
|
||||
|
||||
is_action = any(kw in prompt for kw in [
|
||||
"create", "edit", "modify", "delete", "rename", "add", "remove",
|
||||
"change", "update", "refactor"
|
||||
])
|
||||
|
||||
if is_code_query:
|
||||
# Route to code intelligence
|
||||
query_result = await code_router.handle_query(args.prompt)
|
||||
result["type"] = "code_query"
|
||||
result["query_result"] = query_result.to_dict()
|
||||
result["lsp_used"] = query_result.lsp_used
|
||||
result["citations"] = query_result.citations
|
||||
|
||||
elif is_action:
|
||||
# Route to action orchestrator
|
||||
action_result = await action_orchestrator.execute(
|
||||
args.prompt,
|
||||
auto_approve=(args.approval == "auto")
|
||||
)
|
||||
result["type"] = "action"
|
||||
result["action_result"] = action_result.to_dict()
|
||||
result["read_only_blocked"] = action_result.read_only_blocked
|
||||
|
||||
else:
|
||||
# General query - just return status
|
||||
result["type"] = "general"
|
||||
result["message"] = "Query processed"
|
||||
|
||||
# Get trace
|
||||
trace = runtime.get_trace()
|
||||
if trace:
|
||||
result["trace"] = trace.to_dict()
|
||||
|
||||
result["success"] = True
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
result["success"] = False
|
||||
logger.exception("Debug interactive failed")
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
result["end_time"] = time.time()
|
||||
result["duration_ms"] = (result["end_time"] - start_time) * 1000
|
||||
|
||||
# Save trace if requested
|
||||
if args.trace_file and "trace" in result:
|
||||
with open(args.trace_file, "w") as f:
|
||||
json.dump(result["trace"], f, indent=2, default=str)
|
||||
if not args.json:
|
||||
print(f"Trace saved to: {args.trace_file}")
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0 if result.get("success") else 1
|
||||
|
||||
@staticmethod
|
||||
def handle_lsp_status(args: argparse.Namespace) -> int:
|
||||
"""Handle debug lsp status command."""
|
||||
return asyncio.run(DebugHandler._run_lsp_status(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_lsp_status(args: argparse.Namespace) -> int:
|
||||
"""Run LSP status check."""
|
||||
from .interactive_runtime import create_runtime
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
result = {
|
||||
"lsp_enabled": True,
|
||||
"lsp_ready": runtime.lsp_ready,
|
||||
"lsp_status": runtime._lsp_state.status.value,
|
||||
"lsp_error": runtime._lsp_state.error,
|
||||
"workspace": args.workspace
|
||||
}
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0 if runtime.lsp_ready else 1
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_lsp_symbols(args: argparse.Namespace) -> int:
|
||||
"""Handle debug lsp symbols command."""
|
||||
return asyncio.run(DebugHandler._run_lsp_symbols(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_lsp_symbols(args: argparse.Namespace) -> int:
|
||||
"""Run LSP symbols query."""
|
||||
from .interactive_runtime import create_runtime
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
if not runtime.lsp_ready:
|
||||
# Use fallback
|
||||
router = CodeIntelligenceRouter(runtime)
|
||||
result = await router._fallback_list_symbols(args.file)
|
||||
else:
|
||||
symbols = await runtime.lsp_get_symbols(args.file)
|
||||
result = {
|
||||
"file": args.file,
|
||||
"symbols": symbols,
|
||||
"count": len(symbols) if symbols else 0,
|
||||
"lsp_used": True
|
||||
}
|
||||
|
||||
if hasattr(result, 'to_dict'):
|
||||
result = result.to_dict()
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_lsp_definition(args: argparse.Namespace) -> int:
|
||||
"""Handle debug lsp definition command."""
|
||||
return asyncio.run(DebugHandler._run_lsp_definition(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_lsp_definition(args: argparse.Namespace) -> int:
|
||||
"""Run LSP definition query."""
|
||||
from .interactive_runtime import create_runtime
|
||||
|
||||
# Parse location
|
||||
parts = args.location.split(":")
|
||||
if len(parts) != 3:
|
||||
print("Error: Location must be in format file:line:col", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
file_path, line, col = parts[0], int(parts[1]), int(parts[2])
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
definitions = await runtime.lsp_get_definition(file_path, line, col)
|
||||
result = {
|
||||
"file": file_path,
|
||||
"line": line,
|
||||
"col": col,
|
||||
"definitions": definitions,
|
||||
"count": len(definitions) if definitions else 0,
|
||||
"lsp_used": runtime.lsp_ready
|
||||
}
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_lsp_references(args: argparse.Namespace) -> int:
|
||||
"""Handle debug lsp references command."""
|
||||
return asyncio.run(DebugHandler._run_lsp_references(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_lsp_references(args: argparse.Namespace) -> int:
|
||||
"""Run LSP references query."""
|
||||
from .interactive_runtime import create_runtime
|
||||
|
||||
# Parse location
|
||||
parts = args.location.split(":")
|
||||
if len(parts) != 3:
|
||||
print("Error: Location must be in format file:line:col", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
file_path, line, col = parts[0], int(parts[1]), int(parts[2])
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
references = await runtime.lsp_get_references(file_path, line, col)
|
||||
result = {
|
||||
"file": file_path,
|
||||
"line": line,
|
||||
"col": col,
|
||||
"references": references,
|
||||
"count": len(references) if references else 0,
|
||||
"lsp_used": runtime.lsp_ready
|
||||
}
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_lsp_diagnostics(args: argparse.Namespace) -> int:
|
||||
"""Handle debug lsp diagnostics command."""
|
||||
return asyncio.run(DebugHandler._run_lsp_diagnostics(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_lsp_diagnostics(args: argparse.Namespace) -> int:
|
||||
"""Run LSP diagnostics query."""
|
||||
from .interactive_runtime import create_runtime
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
file_path = args.file if not args.all else None
|
||||
diagnostics = await runtime.lsp_get_diagnostics(file_path)
|
||||
|
||||
result = {
|
||||
"file": file_path or "all",
|
||||
"diagnostics": diagnostics,
|
||||
"count": len(diagnostics) if diagnostics else 0,
|
||||
"lsp_used": runtime.lsp_ready
|
||||
}
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_acp_status(args: argparse.Namespace) -> int:
|
||||
"""Handle debug acp status command."""
|
||||
return asyncio.run(DebugHandler._run_acp_status(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_acp_status(args: argparse.Namespace) -> int:
|
||||
"""Run ACP status check."""
|
||||
from .interactive_runtime import create_runtime
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=False,
|
||||
acp=True
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
result = {
|
||||
"acp_enabled": True,
|
||||
"acp_ready": runtime.acp_ready,
|
||||
"acp_status": runtime._acp_state.status.value,
|
||||
"acp_error": runtime._acp_state.error,
|
||||
"read_only": runtime.read_only,
|
||||
"workspace": args.workspace
|
||||
}
|
||||
|
||||
DebugHandler._output_result(result, args.json)
|
||||
return 0 if runtime.acp_ready else 1
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_acp_plan(args: argparse.Namespace) -> int:
|
||||
"""Handle debug acp plan command."""
|
||||
return asyncio.run(DebugHandler._run_acp_plan(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_acp_plan(args: argparse.Namespace) -> int:
|
||||
"""Run ACP plan creation."""
|
||||
from .interactive_runtime import create_runtime
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=False,
|
||||
acp=True
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
result = await orchestrator.create_plan(args.prompt)
|
||||
|
||||
DebugHandler._output_result(result.to_dict(), args.json)
|
||||
return 0 if result.success else 1
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_acp_apply(args: argparse.Namespace) -> int:
|
||||
"""Handle debug acp apply command."""
|
||||
return asyncio.run(DebugHandler._run_acp_apply(args))
|
||||
|
||||
@staticmethod
|
||||
async def _run_acp_apply(args: argparse.Namespace) -> int:
|
||||
"""Run ACP plan and apply."""
|
||||
from .interactive_runtime import create_runtime
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
runtime = create_runtime(
|
||||
workspace=args.workspace,
|
||||
lsp=False,
|
||||
acp=True,
|
||||
approval=args.approval
|
||||
)
|
||||
|
||||
try:
|
||||
await runtime.start()
|
||||
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
result = await orchestrator.execute(
|
||||
args.prompt,
|
||||
auto_approve=(args.approval == "auto")
|
||||
)
|
||||
|
||||
DebugHandler._output_result(result.to_dict(), args.json)
|
||||
return 0 if result.success else 1
|
||||
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
@staticmethod
|
||||
def handle_trace_record(args: argparse.Namespace) -> int:
|
||||
"""Handle debug trace record command."""
|
||||
print(f"Trace recording will be saved to: {args.output}")
|
||||
print("Start an interactive session with --trace-file to record.")
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def handle_trace_replay(args: argparse.Namespace) -> int:
|
||||
"""Handle debug trace replay command."""
|
||||
try:
|
||||
with open(args.file) as f:
|
||||
trace = json.load(f)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(trace, indent=2))
|
||||
else:
|
||||
print(f"Trace from: {args.file}")
|
||||
print(f"Start time: {trace.get('start_time')}")
|
||||
print(f"End time: {trace.get('end_time')}")
|
||||
print(f"Entries: {len(trace.get('entries', []))}")
|
||||
print("\nEntries:")
|
||||
for entry in trace.get("entries", []):
|
||||
print(f" [{entry.get('category')}] {entry.get('action')}")
|
||||
if entry.get('duration_ms'):
|
||||
print(f" Duration: {entry.get('duration_ms'):.2f}ms")
|
||||
if entry.get('error'):
|
||||
print(f" Error: {entry.get('error')}")
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def handle_trace_diff(args: argparse.Namespace) -> int:
|
||||
"""Handle debug trace diff command."""
|
||||
try:
|
||||
with open(args.file1) as f:
|
||||
trace1 = json.load(f)
|
||||
with open(args.file2) as f:
|
||||
trace2 = json.load(f)
|
||||
|
||||
diff = {
|
||||
"file1": args.file1,
|
||||
"file2": args.file2,
|
||||
"entries_diff": {
|
||||
"file1_count": len(trace1.get("entries", [])),
|
||||
"file2_count": len(trace2.get("entries", []))
|
||||
},
|
||||
"duration_diff": {
|
||||
"file1": trace1.get("end_time", 0) - trace1.get("start_time", 0),
|
||||
"file2": trace2.get("end_time", 0) - trace2.get("start_time", 0)
|
||||
}
|
||||
}
|
||||
|
||||
DebugHandler._output_result(diff, args.json)
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def _output_result(result: Dict[str, Any], as_json: bool) -> None:
|
||||
"""Output result in appropriate format."""
|
||||
if as_json:
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
else:
|
||||
for key, value in result.items():
|
||||
if isinstance(value, dict):
|
||||
print(f"{key}:")
|
||||
for k, v in value.items():
|
||||
print(f" {k}: {v}")
|
||||
elif isinstance(value, list):
|
||||
print(f"{key}: ({len(value)} items)")
|
||||
for item in value[:5]: # Show first 5
|
||||
print(f" - {item}")
|
||||
if len(value) > 5:
|
||||
print(f" ... and {len(value) - 5} more")
|
||||
else:
|
||||
print(f"{key}: {value}")
|
||||
|
||||
|
||||
def setup_debug_subparser(subparsers) -> None:
|
||||
"""Set up the debug subcommand parser."""
|
||||
DebugHandler.setup_subparser(subparsers)
|
||||
|
||||
|
||||
def run_debug_command(args: List[str]) -> int:
|
||||
"""Run debug command from CLI."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="praisonai debug",
|
||||
description="Debug and test interactive coding assistant flows"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="debug_command", help="Debug subcommands")
|
||||
|
||||
# debug interactive
|
||||
interactive_parser = subparsers.add_parser(
|
||||
"interactive",
|
||||
help="Run single interactive turn non-interactively",
|
||||
description="Execute one interactive turn with full LSP/ACP pipeline"
|
||||
)
|
||||
interactive_parser.add_argument("-p", "--prompt", type=str, required=True, help="Prompt to execute")
|
||||
interactive_parser.add_argument("--lsp", action="store_true", default=True, help="Enable LSP (default: enabled)")
|
||||
interactive_parser.add_argument("--no-lsp", action="store_true", help="Disable LSP")
|
||||
interactive_parser.add_argument("--acp", action="store_true", default=True, help="Enable ACP (default: enabled)")
|
||||
interactive_parser.add_argument("--no-acp", action="store_true", help="Disable ACP")
|
||||
interactive_parser.add_argument("--approval", type=str, choices=["manual", "auto", "scoped"], default="auto", help="Approval mode")
|
||||
interactive_parser.add_argument("--workspace", "-w", type=str, default=".", help="Workspace root directory")
|
||||
interactive_parser.add_argument("--json", action="store_true", help="Output JSON trace")
|
||||
interactive_parser.add_argument("--trace-file", type=str, help="Save trace to file")
|
||||
interactive_parser.add_argument("--timeout", type=float, default=60.0, help="Timeout in seconds")
|
||||
interactive_parser.add_argument("--model", type=str, help="LLM model to use")
|
||||
interactive_parser.set_defaults(func=DebugHandler.handle_interactive)
|
||||
|
||||
# debug lsp
|
||||
lsp_parser = subparsers.add_parser("lsp", help="Direct LSP probes")
|
||||
lsp_subparsers = lsp_parser.add_subparsers(dest="lsp_command", help="LSP subcommands")
|
||||
|
||||
# debug lsp status
|
||||
lsp_status = lsp_subparsers.add_parser("status", help="Show LSP status")
|
||||
lsp_status.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_status.add_argument("--json", action="store_true")
|
||||
lsp_status.set_defaults(func=DebugHandler.handle_lsp_status)
|
||||
|
||||
# debug lsp symbols
|
||||
lsp_symbols = lsp_subparsers.add_parser("symbols", help="List symbols in file")
|
||||
lsp_symbols.add_argument("file", type=str, help="File path")
|
||||
lsp_symbols.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_symbols.add_argument("--json", action="store_true")
|
||||
lsp_symbols.set_defaults(func=DebugHandler.handle_lsp_symbols)
|
||||
|
||||
# debug lsp definition
|
||||
lsp_def = lsp_subparsers.add_parser("definition", help="Get definition location")
|
||||
lsp_def.add_argument("location", type=str, help="file:line:col")
|
||||
lsp_def.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_def.add_argument("--json", action="store_true")
|
||||
lsp_def.set_defaults(func=DebugHandler.handle_lsp_definition)
|
||||
|
||||
# debug lsp references
|
||||
lsp_refs = lsp_subparsers.add_parser("references", help="Get references")
|
||||
lsp_refs.add_argument("location", type=str, help="file:line:col")
|
||||
lsp_refs.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_refs.add_argument("--json", action="store_true")
|
||||
lsp_refs.set_defaults(func=DebugHandler.handle_lsp_references)
|
||||
|
||||
# debug lsp diagnostics
|
||||
lsp_diag = lsp_subparsers.add_parser("diagnostics", help="Get diagnostics")
|
||||
lsp_diag.add_argument("file", type=str, nargs="?", help="File path (optional)")
|
||||
lsp_diag.add_argument("--all", action="store_true", help="Get all diagnostics")
|
||||
lsp_diag.add_argument("--workspace", "-w", type=str, default=".")
|
||||
lsp_diag.add_argument("--json", action="store_true")
|
||||
lsp_diag.set_defaults(func=DebugHandler.handle_lsp_diagnostics)
|
||||
|
||||
# debug acp
|
||||
acp_parser = subparsers.add_parser("acp", help="Direct ACP probes")
|
||||
acp_subparsers = acp_parser.add_subparsers(dest="acp_command", help="ACP subcommands")
|
||||
|
||||
# debug acp status
|
||||
acp_status = acp_subparsers.add_parser("status", help="Show ACP status")
|
||||
acp_status.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_status.add_argument("--json", action="store_true")
|
||||
acp_status.set_defaults(func=DebugHandler.handle_acp_status)
|
||||
|
||||
# debug acp plan
|
||||
acp_plan = acp_subparsers.add_parser("plan", help="Create action plan")
|
||||
acp_plan.add_argument("-p", "--prompt", type=str, required=True)
|
||||
acp_plan.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_plan.add_argument("--json", action="store_true")
|
||||
acp_plan.set_defaults(func=DebugHandler.handle_acp_plan)
|
||||
|
||||
# debug acp apply
|
||||
acp_apply = acp_subparsers.add_parser("apply", help="Apply action plan")
|
||||
acp_apply.add_argument("-p", "--prompt", type=str, required=True)
|
||||
acp_apply.add_argument("--approval", type=str, choices=["manual", "auto", "scoped"], default="auto")
|
||||
acp_apply.add_argument("--workspace", "-w", type=str, default=".")
|
||||
acp_apply.add_argument("--json", action="store_true")
|
||||
acp_apply.set_defaults(func=DebugHandler.handle_acp_apply)
|
||||
|
||||
# debug trace
|
||||
trace_parser = subparsers.add_parser("trace", help="Trace record/replay/diff")
|
||||
trace_subparsers = trace_parser.add_subparsers(dest="trace_command", help="Trace subcommands")
|
||||
|
||||
# debug trace record
|
||||
trace_record = trace_subparsers.add_parser("record", help="Record session trace")
|
||||
trace_record.add_argument("-o", "--output", type=str, required=True, help="Output file")
|
||||
trace_record.set_defaults(func=DebugHandler.handle_trace_record)
|
||||
|
||||
# debug trace replay
|
||||
trace_replay = trace_subparsers.add_parser("replay", help="Replay recorded trace")
|
||||
trace_replay.add_argument("file", type=str, help="Trace file to replay")
|
||||
trace_replay.add_argument("--json", action="store_true")
|
||||
trace_replay.set_defaults(func=DebugHandler.handle_trace_replay)
|
||||
|
||||
# debug trace diff
|
||||
trace_diff = trace_subparsers.add_parser("diff", help="Compare two traces")
|
||||
trace_diff.add_argument("file1", type=str, help="First trace file")
|
||||
trace_diff.add_argument("file2", type=str, help="Second trace file")
|
||||
trace_diff.add_argument("--json", action="store_true")
|
||||
trace_diff.set_defaults(func=DebugHandler.handle_trace_diff)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
if hasattr(parsed, 'func'):
|
||||
return parsed.func(parsed)
|
||||
else:
|
||||
parser.print_help()
|
||||
return 0
|
||||
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
Diagnostics export CLI handler for PraisonAI.
|
||||
|
||||
Provides diagnostic export functionality:
|
||||
- diag export: Bundle logs, traces, config for bug reports
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DiagHandler:
|
||||
"""Handler for diag CLI commands."""
|
||||
|
||||
@staticmethod
|
||||
def setup_subparser(subparsers) -> None:
|
||||
"""Set up the diag subcommand parser."""
|
||||
diag_parser = subparsers.add_parser(
|
||||
"diag",
|
||||
help="Diagnostics export",
|
||||
description="Export diagnostic information for bug reports"
|
||||
)
|
||||
|
||||
diag_subparsers = diag_parser.add_subparsers(dest="diag_command", help="Diag subcommands")
|
||||
|
||||
# diag export
|
||||
export_parser = diag_subparsers.add_parser("export", help="Export diagnostic bundle")
|
||||
export_parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=str,
|
||||
help="Output path (default: praisonai-diag-{timestamp}.zip)"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--include-logs",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Include log files (default: true)"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--include-config",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Include configuration (default: true)"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--include-trace",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Include recent traces (default: true)"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--workspace", "-w",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Workspace root directory"
|
||||
)
|
||||
export_parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
export_parser.set_defaults(func=DiagHandler.handle_export)
|
||||
|
||||
@staticmethod
|
||||
def handle_export(args: argparse.Namespace) -> int:
|
||||
"""Handle diag export command."""
|
||||
timestamp = int(time.time())
|
||||
output_path = args.output or f"praisonai-diag-{timestamp}.zip"
|
||||
|
||||
collected_files = []
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmppath = Path(tmpdir)
|
||||
|
||||
# Collect system info
|
||||
system_info = DiagHandler._collect_system_info()
|
||||
system_file = tmppath / "system_info.json"
|
||||
with open(system_file, "w") as f:
|
||||
json.dump(system_info, f, indent=2, default=str)
|
||||
collected_files.append("system_info.json")
|
||||
|
||||
# Collect config
|
||||
if args.include_config:
|
||||
config_files = DiagHandler._collect_config(args.workspace, tmppath)
|
||||
collected_files.extend(config_files)
|
||||
|
||||
# Collect logs
|
||||
if args.include_logs:
|
||||
log_files = DiagHandler._collect_logs(tmppath)
|
||||
collected_files.extend(log_files)
|
||||
|
||||
# Collect traces
|
||||
if args.include_trace:
|
||||
trace_files = DiagHandler._collect_traces(args.workspace, tmppath)
|
||||
collected_files.extend(trace_files)
|
||||
|
||||
# Create zip
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for file in tmppath.rglob("*"):
|
||||
if file.is_file():
|
||||
arcname = file.relative_to(tmppath)
|
||||
zf.write(file, arcname)
|
||||
|
||||
result = {
|
||||
"action": "export",
|
||||
"output": output_path,
|
||||
"files_collected": collected_files,
|
||||
"count": len(collected_files)
|
||||
}
|
||||
|
||||
DiagHandler._output_result(result, args.json)
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _collect_system_info() -> dict:
|
||||
"""Collect system information."""
|
||||
import platform
|
||||
import sys
|
||||
|
||||
info = {
|
||||
"timestamp": time.time(),
|
||||
"platform": {
|
||||
"system": platform.system(),
|
||||
"release": platform.release(),
|
||||
"version": platform.version(),
|
||||
"machine": platform.machine(),
|
||||
"python_version": sys.version
|
||||
},
|
||||
"praisonai": {}
|
||||
}
|
||||
|
||||
# Get PraisonAI version
|
||||
try:
|
||||
from praisonai.version import __version__
|
||||
info["praisonai"]["version"] = __version__
|
||||
except ImportError:
|
||||
info["praisonai"]["version"] = "unknown"
|
||||
|
||||
# Get praisonaiagents version
|
||||
try:
|
||||
import praisonaiagents
|
||||
info["praisonai"]["agents_version"] = getattr(praisonaiagents, '__version__', 'unknown')
|
||||
except ImportError:
|
||||
info["praisonai"]["agents_version"] = "not installed"
|
||||
|
||||
# Check for optional dependencies
|
||||
info["dependencies"] = {}
|
||||
for dep in ["acp", "litellm", "openai", "anthropic", "google.generativeai"]:
|
||||
try:
|
||||
__import__(dep.split(".")[0])
|
||||
info["dependencies"][dep] = "installed"
|
||||
except ImportError:
|
||||
info["dependencies"][dep] = "not installed"
|
||||
|
||||
# Environment (redacted)
|
||||
env_keys = [
|
||||
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY",
|
||||
"PRAISONAI_APPROVAL_MODE", "LOGLEVEL"
|
||||
]
|
||||
info["environment"] = {}
|
||||
for key in env_keys:
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
if "KEY" in key or "SECRET" in key:
|
||||
info["environment"][key] = f"{value[:4]}...{value[-4:]}" if len(value) > 8 else "***"
|
||||
else:
|
||||
info["environment"][key] = value
|
||||
else:
|
||||
info["environment"][key] = None
|
||||
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def _collect_config(workspace: str, tmppath: Path) -> List[str]:
|
||||
"""Collect configuration files."""
|
||||
collected = []
|
||||
workspace_path = Path(workspace)
|
||||
config_dir = tmppath / "config"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Look for config files
|
||||
config_patterns = [
|
||||
"agents.yaml",
|
||||
"workflow.yaml",
|
||||
".praisonai.yaml",
|
||||
".env.example", # Not .env to avoid secrets
|
||||
"pyproject.toml",
|
||||
]
|
||||
|
||||
for pattern in config_patterns:
|
||||
for file in workspace_path.glob(pattern):
|
||||
if file.is_file():
|
||||
dest = config_dir / file.name
|
||||
shutil.copy(file, dest)
|
||||
collected.append(f"config/{file.name}")
|
||||
|
||||
return collected
|
||||
|
||||
@staticmethod
|
||||
def _collect_logs(tmppath: Path) -> List[str]:
|
||||
"""Collect log files."""
|
||||
collected = []
|
||||
logs_dir = tmppath / "logs"
|
||||
logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Look for log files in common locations
|
||||
log_locations = [
|
||||
Path.home() / ".praison" / "logs",
|
||||
Path.home() / ".praisonai" / "logs",
|
||||
Path("/var/log/praisonai"),
|
||||
]
|
||||
|
||||
for log_dir in log_locations:
|
||||
if log_dir.exists():
|
||||
for log_file in log_dir.glob("*.log"):
|
||||
if log_file.is_file():
|
||||
dest = logs_dir / log_file.name
|
||||
# Only copy last 1000 lines
|
||||
try:
|
||||
with open(log_file) as f:
|
||||
lines = f.readlines()[-1000:]
|
||||
with open(dest, "w") as f:
|
||||
f.writelines(lines)
|
||||
collected.append(f"logs/{log_file.name}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return collected
|
||||
|
||||
@staticmethod
|
||||
def _collect_traces(workspace: str, tmppath: Path) -> List[str]:
|
||||
"""Collect trace files."""
|
||||
collected = []
|
||||
traces_dir = tmppath / "traces"
|
||||
traces_dir.mkdir(exist_ok=True)
|
||||
|
||||
workspace_path = Path(workspace)
|
||||
|
||||
# Look for trace files
|
||||
trace_patterns = ["*_trace.json", "praisonai_trace*.json", "*.trace"]
|
||||
|
||||
for pattern in trace_patterns:
|
||||
for trace_file in workspace_path.glob(pattern):
|
||||
if trace_file.is_file():
|
||||
dest = traces_dir / trace_file.name
|
||||
shutil.copy(trace_file, dest)
|
||||
collected.append(f"traces/{trace_file.name}")
|
||||
|
||||
return collected
|
||||
|
||||
@staticmethod
|
||||
def _output_result(result: dict, as_json: bool) -> None:
|
||||
"""Output result in appropriate format."""
|
||||
if as_json:
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
else:
|
||||
print(f"Diagnostic bundle created: {result['output']}")
|
||||
print(f"Files collected: {result['count']}")
|
||||
for f in result['files_collected']:
|
||||
print(f" - {f}")
|
||||
|
||||
|
||||
def run_diag_command(args: List[str]) -> int:
|
||||
"""Run diag command from CLI."""
|
||||
parser = argparse.ArgumentParser(prog="praisonai diag")
|
||||
subparsers = parser.add_subparsers(dest="diag_command", help="Diag subcommands")
|
||||
|
||||
# diag export
|
||||
export_parser = subparsers.add_parser("export", help="Export diagnostic bundle")
|
||||
export_parser.add_argument("-o", "--output", type=str)
|
||||
export_parser.add_argument("--include-logs", action="store_true", default=True)
|
||||
export_parser.add_argument("--include-config", action="store_true", default=True)
|
||||
export_parser.add_argument("--include-trace", action="store_true", default=True)
|
||||
export_parser.add_argument("--workspace", "-w", type=str, default=".")
|
||||
export_parser.add_argument("--json", action="store_true")
|
||||
export_parser.set_defaults(func=DiagHandler.handle_export)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
if hasattr(parsed, 'func'):
|
||||
return parsed.func(parsed)
|
||||
else:
|
||||
parser.print_help()
|
||||
return 0
|
||||
@@ -20,3 +20,5 @@ def register_all_checks():
|
||||
from . import performance_checks
|
||||
from . import selftest_checks
|
||||
from . import serve_checks
|
||||
from . import lsp_checks
|
||||
from . import acp_checks
|
||||
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
ACP health checks for PraisonAI Doctor.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..models import CheckCategory, CheckResult, CheckStatus
|
||||
from ..registry import register_check
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="acp_module",
|
||||
title="ACP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if ACP module is installed"
|
||||
)
|
||||
def check_acp_module(config=None) -> CheckResult:
|
||||
"""Check if ACP module is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.find_spec("praisonai.acp")
|
||||
|
||||
if spec is not None:
|
||||
return CheckResult(
|
||||
id="acp_module",
|
||||
title="ACP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="ACP module is available",
|
||||
metadata={"module": "praisonai.acp"}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="acp_module",
|
||||
title="ACP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message="ACP module not found",
|
||||
remediation="ACP module should be part of praisonai package"
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="acp_module",
|
||||
title="ACP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Error checking ACP module: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="acp_sdk",
|
||||
title="ACP SDK Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if agent-client-protocol SDK is installed"
|
||||
)
|
||||
def check_acp_sdk(config=None) -> CheckResult:
|
||||
"""Check if ACP SDK is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.find_spec("acp")
|
||||
|
||||
if spec is not None:
|
||||
return CheckResult(
|
||||
id="acp_sdk",
|
||||
title="ACP SDK Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="ACP SDK (agent-client-protocol) is installed",
|
||||
metadata={"package": "agent-client-protocol"}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="acp_sdk",
|
||||
title="ACP SDK Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="ACP SDK not installed (optional)",
|
||||
remediation="Install with: pip install agent-client-protocol"
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="acp_sdk",
|
||||
title="ACP SDK Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Error checking ACP SDK: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="acp_server",
|
||||
title="ACP Server Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if ACP server can be imported"
|
||||
)
|
||||
def check_acp_server(config=None) -> CheckResult:
|
||||
"""Check if ACP server can be imported."""
|
||||
try:
|
||||
from praisonai.acp import ACPServer # noqa: F401
|
||||
|
||||
return CheckResult(
|
||||
id="acp_server",
|
||||
title="ACP Server Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="ACP server can be imported",
|
||||
metadata={"class": "ACPServer"}
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="acp_server",
|
||||
title="ACP Server Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"ACP server import failed: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="acp_server",
|
||||
title="ACP Server Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Error importing ACP server: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="acp_config",
|
||||
title="ACP Configuration",
|
||||
category=CheckCategory.CONFIG,
|
||||
description="Check ACP configuration"
|
||||
)
|
||||
def check_acp_config(config=None) -> CheckResult:
|
||||
"""Check ACP configuration."""
|
||||
acp_conf = {
|
||||
"approval_mode": os.environ.get("PRAISONAI_APPROVAL_MODE", "manual"),
|
||||
"allow_write": os.environ.get("PRAISONAI_ALLOW_WRITE", "false"),
|
||||
"allow_shell": os.environ.get("PRAISONAI_ALLOW_SHELL", "false"),
|
||||
}
|
||||
|
||||
return CheckResult(
|
||||
id="acp_config",
|
||||
title="ACP Configuration",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"ACP configured with approval_mode={acp_conf['approval_mode']}",
|
||||
metadata=acp_conf
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="acp_runtime",
|
||||
title="ACP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if ACP runtime can start",
|
||||
requires_deep=True
|
||||
)
|
||||
def check_acp_runtime(config=None) -> CheckResult:
|
||||
"""Check if ACP runtime can start."""
|
||||
try:
|
||||
from praisonai.cli.features.interactive_runtime import create_runtime
|
||||
|
||||
async def _check():
|
||||
runtime = create_runtime(
|
||||
workspace=".",
|
||||
lsp=False,
|
||||
acp=True
|
||||
)
|
||||
try:
|
||||
await runtime.start()
|
||||
ready = runtime.acp_ready
|
||||
error = runtime._acp_state.error
|
||||
return ready, error
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
ready, error = asyncio.run(_check())
|
||||
|
||||
if ready:
|
||||
return CheckResult(
|
||||
id="acp_runtime",
|
||||
title="ACP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="ACP runtime started successfully",
|
||||
metadata={"ready": True}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="acp_runtime",
|
||||
title="ACP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"ACP runtime failed to start: {error}",
|
||||
metadata={"ready": False, "error": str(error)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="acp_runtime",
|
||||
title="ACP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"ACP runtime check failed: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
def register_acp_checks():
|
||||
"""Register all ACP checks."""
|
||||
pass
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
LSP health checks for PraisonAI Doctor.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from ..models import CheckCategory, CheckResult, CheckStatus
|
||||
from ..registry import register_check
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="lsp_module",
|
||||
title="LSP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if LSP module is installed"
|
||||
)
|
||||
def check_lsp_module(config=None) -> CheckResult:
|
||||
"""Check if LSP module is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.find_spec("praisonaiagents.lsp")
|
||||
|
||||
if spec is not None:
|
||||
return CheckResult(
|
||||
id="lsp_module",
|
||||
title="LSP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="LSP module is available",
|
||||
metadata={"module": "praisonaiagents.lsp"}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="lsp_module",
|
||||
title="LSP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message="LSP module not found",
|
||||
remediation="Install praisonaiagents with LSP support"
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="lsp_module",
|
||||
title="LSP Module Available",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Error checking LSP module: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="lsp_client",
|
||||
title="LSP Client Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if LSP client can be imported"
|
||||
)
|
||||
def check_lsp_client(config=None) -> CheckResult:
|
||||
"""Check if LSP client can be imported."""
|
||||
try:
|
||||
from praisonaiagents.lsp import LSPClient # noqa: F401
|
||||
|
||||
return CheckResult(
|
||||
id="lsp_client",
|
||||
title="LSP Client Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="LSP client can be imported",
|
||||
metadata={"class": "LSPClient"}
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="lsp_client",
|
||||
title="LSP Client Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"LSP client import failed: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="lsp_client",
|
||||
title="LSP Client Import",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Error importing LSP client: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="lsp_server_python",
|
||||
title="Python LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if Python language server is available"
|
||||
)
|
||||
def check_lsp_server_python(config=None) -> CheckResult:
|
||||
"""Check if Python language server is available."""
|
||||
servers = [
|
||||
("pyright-langserver", "pyright"),
|
||||
("pylsp", "python-lsp-server"),
|
||||
("pyls", "python-language-server"),
|
||||
]
|
||||
|
||||
found = []
|
||||
for cmd, package in servers:
|
||||
if shutil.which(cmd):
|
||||
found.append({"command": cmd, "package": package})
|
||||
|
||||
if found:
|
||||
return CheckResult(
|
||||
id="lsp_server_python",
|
||||
title="Python LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Python LSP server available: {found[0]['command']}",
|
||||
metadata={"servers": found}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="lsp_server_python",
|
||||
title="Python LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message="No Python LSP server found",
|
||||
remediation="Install with: npm install -g pyright OR pip install python-lsp-server"
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="lsp_server_typescript",
|
||||
title="TypeScript LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if TypeScript language server is available"
|
||||
)
|
||||
def check_lsp_server_typescript(config=None) -> CheckResult:
|
||||
"""Check if TypeScript language server is available."""
|
||||
if shutil.which("typescript-language-server"):
|
||||
return CheckResult(
|
||||
id="lsp_server_typescript",
|
||||
title="TypeScript LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="TypeScript LSP server available",
|
||||
metadata={"command": "typescript-language-server"}
|
||||
)
|
||||
elif shutil.which("tsserver"):
|
||||
return CheckResult(
|
||||
id="lsp_server_typescript",
|
||||
title="TypeScript LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="TypeScript server available (tsserver)",
|
||||
metadata={"command": "tsserver"}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="lsp_server_typescript",
|
||||
title="TypeScript LSP Server",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="TypeScript LSP server not found (optional)",
|
||||
remediation="Install with: npm install -g typescript-language-server typescript"
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="lsp_runtime",
|
||||
title="LSP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
description="Check if LSP runtime can start",
|
||||
requires_deep=True
|
||||
)
|
||||
def check_lsp_runtime(config=None) -> CheckResult:
|
||||
"""Check if LSP runtime can start."""
|
||||
try:
|
||||
from praisonai.cli.features.interactive_runtime import create_runtime
|
||||
|
||||
async def _check():
|
||||
runtime = create_runtime(
|
||||
workspace=".",
|
||||
lsp=True,
|
||||
acp=False
|
||||
)
|
||||
try:
|
||||
await runtime.start()
|
||||
ready = runtime.lsp_ready
|
||||
error = runtime._lsp_state.error
|
||||
return ready, error
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
ready, error = asyncio.run(_check())
|
||||
|
||||
if ready:
|
||||
return CheckResult(
|
||||
id="lsp_runtime",
|
||||
title="LSP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="LSP runtime started successfully",
|
||||
metadata={"ready": True}
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="lsp_runtime",
|
||||
title="LSP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"LSP runtime failed to start: {error}",
|
||||
metadata={"ready": False, "error": str(error)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="lsp_runtime",
|
||||
title="LSP Runtime Check",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"LSP runtime check failed: {e}",
|
||||
metadata={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
def register_lsp_checks():
|
||||
"""Register all LSP checks."""
|
||||
pass
|
||||
@@ -0,0 +1,585 @@
|
||||
"""
|
||||
Interactive Runtime for PraisonAI.
|
||||
|
||||
Provides the core runtime that powers both TUI interactive mode and debug non-interactive mode.
|
||||
Manages LSP and ACP subsystem lifecycle with graceful degradation.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubsystemStatus(Enum):
|
||||
"""Status of a subsystem."""
|
||||
NOT_STARTED = "not_started"
|
||||
STARTING = "starting"
|
||||
READY = "ready"
|
||||
FAILED = "failed"
|
||||
STOPPED = "stopped"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubsystemState:
|
||||
"""State of a subsystem."""
|
||||
status: SubsystemStatus = SubsystemStatus.NOT_STARTED
|
||||
error: Optional[str] = None
|
||||
start_time: Optional[float] = None
|
||||
ready_time: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeConfig:
|
||||
"""Configuration for InteractiveRuntime."""
|
||||
workspace: str = "."
|
||||
lsp_enabled: bool = True
|
||||
acp_enabled: bool = True
|
||||
approval_mode: str = "manual" # manual, auto, scoped
|
||||
trace_enabled: bool = False
|
||||
trace_file: Optional[str] = None
|
||||
json_output: bool = False
|
||||
timeout: float = 60.0
|
||||
model: Optional[str] = None
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TraceEntry:
|
||||
"""A single trace entry."""
|
||||
timestamp: float
|
||||
category: str # lsp, acp, tool, llm, file
|
||||
action: str
|
||||
params: Dict[str, Any] = field(default_factory=dict)
|
||||
result: Optional[Any] = None
|
||||
duration_ms: Optional[float] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeTrace:
|
||||
"""Trace of a runtime session."""
|
||||
version: str = "1.0"
|
||||
start_time: float = field(default_factory=time.time)
|
||||
end_time: Optional[float] = None
|
||||
config: Optional[RuntimeConfig] = None
|
||||
entries: List[TraceEntry] = field(default_factory=list)
|
||||
|
||||
def add_entry(self, category: str, action: str, params: Dict[str, Any] = None,
|
||||
result: Any = None, duration_ms: float = None, error: str = None):
|
||||
"""Add a trace entry."""
|
||||
self.entries.append(TraceEntry(
|
||||
timestamp=time.time(),
|
||||
category=category,
|
||||
action=action,
|
||||
params=params or {},
|
||||
result=result,
|
||||
duration_ms=duration_ms,
|
||||
error=error
|
||||
))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert trace to dictionary."""
|
||||
return {
|
||||
"version": self.version,
|
||||
"start_time": self.start_time,
|
||||
"end_time": self.end_time,
|
||||
"config": {
|
||||
"workspace": self.config.workspace if self.config else None,
|
||||
"lsp_enabled": self.config.lsp_enabled if self.config else None,
|
||||
"acp_enabled": self.config.acp_enabled if self.config else None,
|
||||
"approval_mode": self.config.approval_mode if self.config else None,
|
||||
} if self.config else None,
|
||||
"entries": [
|
||||
{
|
||||
"timestamp": e.timestamp,
|
||||
"category": e.category,
|
||||
"action": e.action,
|
||||
"params": e.params,
|
||||
"result": e.result,
|
||||
"duration_ms": e.duration_ms,
|
||||
"error": e.error
|
||||
}
|
||||
for e in self.entries
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class InteractiveRuntime:
|
||||
"""
|
||||
Core runtime for interactive coding assistant.
|
||||
|
||||
Manages:
|
||||
- LSP subsystem for code intelligence
|
||||
- ACP subsystem for action orchestration
|
||||
- Trace collection for debugging
|
||||
- Graceful degradation when subsystems fail
|
||||
"""
|
||||
|
||||
def __init__(self, config: RuntimeConfig = None):
|
||||
"""Initialize the runtime."""
|
||||
self.config = config or RuntimeConfig()
|
||||
self._lsp_state = SubsystemState()
|
||||
self._acp_state = SubsystemState()
|
||||
self._lsp_client = None
|
||||
self._acp_session = None
|
||||
self._trace = RuntimeTrace(config=self.config) if self.config.trace_enabled else None
|
||||
self._started = False
|
||||
self._read_only = False # Enforced when ACP fails
|
||||
|
||||
@property
|
||||
def lsp_ready(self) -> bool:
|
||||
"""Check if LSP is ready."""
|
||||
return self._lsp_state.status == SubsystemStatus.READY
|
||||
|
||||
@property
|
||||
def acp_ready(self) -> bool:
|
||||
"""Check if ACP is ready."""
|
||||
return self._acp_state.status == SubsystemStatus.READY
|
||||
|
||||
@property
|
||||
def read_only(self) -> bool:
|
||||
"""Check if runtime is in read-only mode."""
|
||||
return self._read_only or not self.acp_ready
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get runtime status."""
|
||||
return {
|
||||
"started": self._started,
|
||||
"workspace": self.config.workspace,
|
||||
"lsp": {
|
||||
"enabled": self.config.lsp_enabled,
|
||||
"status": self._lsp_state.status.value,
|
||||
"ready": self.lsp_ready,
|
||||
"error": self._lsp_state.error
|
||||
},
|
||||
"acp": {
|
||||
"enabled": self.config.acp_enabled,
|
||||
"status": self._acp_state.status.value,
|
||||
"ready": self.acp_ready,
|
||||
"error": self._acp_state.error
|
||||
},
|
||||
"read_only": self.read_only,
|
||||
"approval_mode": self.config.approval_mode
|
||||
}
|
||||
|
||||
async def start(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Start the runtime and all enabled subsystems.
|
||||
|
||||
Returns status dict with subsystem states.
|
||||
"""
|
||||
if self._started:
|
||||
return self.get_status()
|
||||
|
||||
workspace = Path(self.config.workspace).resolve()
|
||||
if not workspace.exists():
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.config.workspace = str(workspace)
|
||||
|
||||
# Start subsystems in parallel
|
||||
tasks = []
|
||||
if self.config.lsp_enabled:
|
||||
tasks.append(self._start_lsp())
|
||||
if self.config.acp_enabled:
|
||||
tasks.append(self._start_acp())
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Enforce read-only if ACP failed
|
||||
if self.config.acp_enabled and not self.acp_ready:
|
||||
self._read_only = True
|
||||
logger.warning("ACP unavailable - runtime is READ-ONLY")
|
||||
|
||||
self._started = True
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="runtime",
|
||||
action="start",
|
||||
params={"workspace": self.config.workspace},
|
||||
result=self.get_status()
|
||||
)
|
||||
|
||||
return self.get_status()
|
||||
|
||||
async def _start_lsp(self):
|
||||
"""Start LSP subsystem."""
|
||||
self._lsp_state.status = SubsystemStatus.STARTING
|
||||
self._lsp_state.start_time = time.time()
|
||||
|
||||
try:
|
||||
# Lazy import LSP client
|
||||
from praisonaiagents.lsp import LSPClient
|
||||
|
||||
# Detect language from workspace
|
||||
language = self._detect_workspace_language()
|
||||
|
||||
self._lsp_client = LSPClient(
|
||||
language=language,
|
||||
root_uri=f"file://{self.config.workspace}"
|
||||
)
|
||||
await self._lsp_client.start()
|
||||
|
||||
# Wait for initialization
|
||||
if self._lsp_client.is_running:
|
||||
self._lsp_state.status = SubsystemStatus.READY
|
||||
self._lsp_state.ready_time = time.time()
|
||||
logger.info(f"LSP ready for {language}")
|
||||
else:
|
||||
raise RuntimeError("LSP client failed to start")
|
||||
|
||||
except ImportError as e:
|
||||
self._lsp_state.status = SubsystemStatus.FAILED
|
||||
self._lsp_state.error = f"LSP module not available: {e}"
|
||||
logger.warning(self._lsp_state.error)
|
||||
except Exception as e:
|
||||
self._lsp_state.status = SubsystemStatus.FAILED
|
||||
self._lsp_state.error = str(e)
|
||||
logger.warning(f"LSP failed to start: {e}")
|
||||
|
||||
async def _start_acp(self):
|
||||
"""Start ACP subsystem (in-process session)."""
|
||||
self._acp_state.status = SubsystemStatus.STARTING
|
||||
self._acp_state.start_time = time.time()
|
||||
|
||||
try:
|
||||
# Lazy import ACP
|
||||
from praisonai.acp.session import ACPSession
|
||||
from pathlib import Path
|
||||
|
||||
# Create a session for the workspace
|
||||
self._acp_session = ACPSession.create(
|
||||
workspace=Path(self.config.workspace)
|
||||
)
|
||||
self._acp_session.mode = self.config.approval_mode
|
||||
|
||||
self._acp_state.status = SubsystemStatus.READY
|
||||
self._acp_state.ready_time = time.time()
|
||||
logger.info("ACP session ready")
|
||||
|
||||
except ImportError as e:
|
||||
self._acp_state.status = SubsystemStatus.FAILED
|
||||
self._acp_state.error = f"ACP module not available: {e}"
|
||||
logger.warning(self._acp_state.error)
|
||||
except Exception as e:
|
||||
self._acp_state.status = SubsystemStatus.FAILED
|
||||
self._acp_state.error = str(e)
|
||||
logger.warning(f"ACP failed to start: {e}")
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the runtime and all subsystems."""
|
||||
if self._lsp_client:
|
||||
try:
|
||||
await self._lsp_client.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error stopping LSP: {e}")
|
||||
self._lsp_state.status = SubsystemStatus.STOPPED
|
||||
|
||||
if self._acp_session:
|
||||
# ACPSession is a dataclass, no close method needed
|
||||
self._acp_session = None
|
||||
self._acp_state.status = SubsystemStatus.STOPPED
|
||||
|
||||
if self._trace:
|
||||
self._trace.end_time = time.time()
|
||||
self._trace.add_entry(
|
||||
category="runtime",
|
||||
action="stop",
|
||||
result=self.get_status()
|
||||
)
|
||||
|
||||
self._started = False
|
||||
|
||||
def _detect_workspace_language(self) -> str:
|
||||
"""Detect primary language in workspace."""
|
||||
workspace = Path(self.config.workspace)
|
||||
|
||||
# Count files by extension
|
||||
extensions = {}
|
||||
for ext in [".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java"]:
|
||||
count = len(list(workspace.rglob(f"*{ext}")))
|
||||
if count > 0:
|
||||
extensions[ext] = count
|
||||
|
||||
if not extensions:
|
||||
return "python" # Default
|
||||
|
||||
# Map extensions to languages
|
||||
ext_to_lang = {
|
||||
".py": "python",
|
||||
".js": "javascript",
|
||||
".ts": "typescript",
|
||||
".tsx": "typescript",
|
||||
".jsx": "javascript",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
".java": "java"
|
||||
}
|
||||
|
||||
# Return language with most files
|
||||
top_ext = max(extensions, key=extensions.get)
|
||||
return ext_to_lang.get(top_ext, "python")
|
||||
|
||||
# LSP Operations
|
||||
async def lsp_get_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
||||
"""Get symbols in a file via LSP."""
|
||||
if not self.lsp_ready:
|
||||
return []
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
# Use workspace symbols or document symbols
|
||||
result = await self._lsp_client.get_document_symbols(file_path)
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_symbols",
|
||||
params={"file": file_path},
|
||||
result={"count": len(result) if result else 0},
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return result or []
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_symbols",
|
||||
params={"file": file_path},
|
||||
error=str(e)
|
||||
)
|
||||
return []
|
||||
|
||||
async def lsp_get_definition(self, file_path: str, line: int, col: int) -> List[Dict[str, Any]]:
|
||||
"""Get definition location via LSP."""
|
||||
if not self.lsp_ready:
|
||||
return []
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
result = await self._lsp_client.get_definition(file_path, line, col)
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_definition",
|
||||
params={"file": file_path, "line": line, "col": col},
|
||||
result={"count": len(result) if result else 0},
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return [loc.__dict__ if hasattr(loc, '__dict__') else loc for loc in (result or [])]
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_definition",
|
||||
params={"file": file_path, "line": line, "col": col},
|
||||
error=str(e)
|
||||
)
|
||||
return []
|
||||
|
||||
async def lsp_get_references(self, file_path: str, line: int, col: int) -> List[Dict[str, Any]]:
|
||||
"""Get references via LSP."""
|
||||
if not self.lsp_ready:
|
||||
return []
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
result = await self._lsp_client.get_references(file_path, line, col)
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_references",
|
||||
params={"file": file_path, "line": line, "col": col},
|
||||
result={"count": len(result) if result else 0},
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return [loc.__dict__ if hasattr(loc, '__dict__') else loc for loc in (result or [])]
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_references",
|
||||
params={"file": file_path, "line": line, "col": col},
|
||||
error=str(e)
|
||||
)
|
||||
return []
|
||||
|
||||
async def lsp_get_diagnostics(self, file_path: str = None) -> List[Dict[str, Any]]:
|
||||
"""Get diagnostics via LSP."""
|
||||
if not self.lsp_ready:
|
||||
return []
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
if file_path:
|
||||
result = await self._lsp_client.get_diagnostics(file_path)
|
||||
else:
|
||||
result = self._lsp_client._diagnostics # All cached diagnostics
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_diagnostics",
|
||||
params={"file": file_path},
|
||||
result={"count": len(result) if result else 0},
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return [d.__dict__ if hasattr(d, '__dict__') else d for d in (result or [])]
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="lsp",
|
||||
action="get_diagnostics",
|
||||
params={"file": file_path},
|
||||
error=str(e)
|
||||
)
|
||||
return []
|
||||
|
||||
# ACP Operations
|
||||
async def acp_create_plan(self, prompt: str) -> Dict[str, Any]:
|
||||
"""Create an action plan via ACP session tracking."""
|
||||
if not self.acp_ready:
|
||||
return {"error": "ACP not available", "read_only": True}
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
# ACP session tracks the plan but doesn't create it
|
||||
# Return empty steps - ActionOrchestrator handles actual planning
|
||||
result = {"steps": [], "prompt": prompt}
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="acp",
|
||||
action="create_plan",
|
||||
params={"prompt": prompt[:100]},
|
||||
result=result,
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="acp",
|
||||
action="create_plan",
|
||||
params={"prompt": prompt[:100]},
|
||||
error=str(e)
|
||||
)
|
||||
return {"error": str(e)}
|
||||
|
||||
async def acp_apply_plan(self, plan: Dict[str, Any], auto_approve: bool = False) -> Dict[str, Any]:
|
||||
"""Apply an action plan via ACP session tracking."""
|
||||
if self.read_only:
|
||||
return {"error": "Runtime is read-only", "applied": False}
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
# Track the action in session
|
||||
if self._acp_session:
|
||||
self._acp_session.add_tool_call(
|
||||
tool_call_id=f"plan_{int(time.time())}",
|
||||
title="apply_plan",
|
||||
status="applied" if auto_approve else "pending"
|
||||
)
|
||||
|
||||
result = {"applied": True, "auto_approve": auto_approve}
|
||||
duration = (time.time() - start) * 1000
|
||||
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="acp",
|
||||
action="apply_plan",
|
||||
params={"plan_id": plan.get("id"), "auto_approve": auto_approve},
|
||||
result=result,
|
||||
duration_ms=duration
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if self._trace:
|
||||
self._trace.add_entry(
|
||||
category="acp",
|
||||
action="apply_plan",
|
||||
params={"plan_id": plan.get("id")},
|
||||
error=str(e)
|
||||
)
|
||||
return {"error": str(e), "applied": False}
|
||||
|
||||
def get_trace(self) -> Optional[RuntimeTrace]:
|
||||
"""Get the runtime trace."""
|
||||
return self._trace
|
||||
|
||||
def save_trace(self, path: str = None):
|
||||
"""Save trace to file."""
|
||||
if not self._trace:
|
||||
return
|
||||
|
||||
import json
|
||||
path = path or self.config.trace_file or "praisonai_trace.json"
|
||||
with open(path, "w") as f:
|
||||
json.dump(self._trace.to_dict(), f, indent=2, default=str)
|
||||
logger.info(f"Trace saved to {path}")
|
||||
|
||||
|
||||
def create_runtime(
|
||||
workspace: str = ".",
|
||||
lsp: bool = True,
|
||||
acp: bool = True,
|
||||
approval: str = "manual",
|
||||
trace: bool = False,
|
||||
trace_file: str = None,
|
||||
json_output: bool = False,
|
||||
timeout: float = 60.0,
|
||||
model: str = None,
|
||||
verbose: bool = False
|
||||
) -> InteractiveRuntime:
|
||||
"""
|
||||
Factory function to create an InteractiveRuntime.
|
||||
|
||||
Args:
|
||||
workspace: Workspace root directory
|
||||
lsp: Enable LSP subsystem
|
||||
acp: Enable ACP subsystem
|
||||
approval: Approval mode (manual, auto, scoped)
|
||||
trace: Enable trace collection
|
||||
trace_file: Path to save trace
|
||||
json_output: Enable JSON output mode
|
||||
timeout: Operation timeout
|
||||
model: LLM model to use
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Configured InteractiveRuntime instance
|
||||
"""
|
||||
config = RuntimeConfig(
|
||||
workspace=workspace,
|
||||
lsp_enabled=lsp,
|
||||
acp_enabled=acp,
|
||||
approval_mode=approval,
|
||||
trace_enabled=trace,
|
||||
trace_file=trace_file,
|
||||
json_output=json_output,
|
||||
timeout=timeout,
|
||||
model=model,
|
||||
verbose=verbose
|
||||
)
|
||||
return InteractiveRuntime(config)
|
||||
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
Interactive Default Tools Provider for PraisonAI.
|
||||
|
||||
This module provides the canonical source of truth for default tools
|
||||
in interactive modes (TUI and `praison "prompt"`).
|
||||
|
||||
Tool Groups:
|
||||
- `acp`: ACP-powered file operations (create, edit, delete, execute)
|
||||
- `lsp`: LSP-powered code intelligence (symbols, definitions, references)
|
||||
- `basic`: Basic file tools (read, list, search)
|
||||
- `interactive`: Union of all groups (default)
|
||||
|
||||
Usage:
|
||||
from praisonai.cli.features.interactive_tools import (
|
||||
get_interactive_tools,
|
||||
resolve_tool_groups,
|
||||
TOOL_GROUPS,
|
||||
)
|
||||
|
||||
# Get all interactive tools (default)
|
||||
tools = get_interactive_tools()
|
||||
|
||||
# Get specific groups
|
||||
tools = get_interactive_tools(groups=["acp", "basic"])
|
||||
|
||||
# Disable specific groups
|
||||
tools = get_interactive_tools(disable=["lsp"])
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Tool group definitions
|
||||
TOOL_GROUPS = {
|
||||
"acp": [
|
||||
"acp_create_file",
|
||||
"acp_edit_file",
|
||||
"acp_delete_file",
|
||||
"acp_execute_command",
|
||||
],
|
||||
"lsp": [
|
||||
"lsp_list_symbols",
|
||||
"lsp_find_definition",
|
||||
"lsp_find_references",
|
||||
"lsp_get_diagnostics",
|
||||
],
|
||||
"basic": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"list_files",
|
||||
"execute_command",
|
||||
"internet_search",
|
||||
],
|
||||
}
|
||||
|
||||
# Default interactive group includes all
|
||||
TOOL_GROUPS["interactive"] = (
|
||||
TOOL_GROUPS["acp"] +
|
||||
TOOL_GROUPS["lsp"] +
|
||||
TOOL_GROUPS["basic"]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolConfig:
|
||||
"""Configuration for tool loading."""
|
||||
workspace: str = field(default_factory=os.getcwd)
|
||||
enable_acp: bool = True
|
||||
enable_lsp: bool = True
|
||||
enable_basic: bool = True
|
||||
approval_mode: str = "auto" # auto, manual, scoped
|
||||
lsp_enabled: bool = True
|
||||
acp_enabled: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "ToolConfig":
|
||||
"""Create config from environment variables."""
|
||||
config = cls()
|
||||
|
||||
# Check env vars for disabling groups
|
||||
disable_str = os.environ.get("PRAISON_TOOLS_DISABLE", "")
|
||||
if disable_str:
|
||||
disabled = [g.strip().lower() for g in disable_str.split(",")]
|
||||
if "acp" in disabled:
|
||||
config.enable_acp = False
|
||||
if "lsp" in disabled:
|
||||
config.enable_lsp = False
|
||||
if "basic" in disabled:
|
||||
config.enable_basic = False
|
||||
|
||||
# Check for workspace
|
||||
workspace = os.environ.get("PRAISON_WORKSPACE", "")
|
||||
if workspace:
|
||||
config.workspace = workspace
|
||||
|
||||
# Check approval mode
|
||||
approval = os.environ.get("PRAISON_APPROVAL_MODE", "")
|
||||
if approval in ("auto", "manual", "scoped"):
|
||||
config.approval_mode = approval
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def resolve_tool_groups(
|
||||
groups: Optional[List[str]] = None,
|
||||
disable: Optional[List[str]] = None,
|
||||
config: Optional[ToolConfig] = None,
|
||||
) -> Set[str]:
|
||||
"""
|
||||
Resolve which tool names should be included based on groups and config.
|
||||
|
||||
Args:
|
||||
groups: Specific groups to include (None = all enabled)
|
||||
disable: Groups to explicitly disable
|
||||
config: Tool configuration
|
||||
|
||||
Returns:
|
||||
Set of tool names to include
|
||||
"""
|
||||
if config is None:
|
||||
config = ToolConfig.from_env()
|
||||
|
||||
disable = disable or []
|
||||
|
||||
# Start with requested groups or all enabled
|
||||
if groups:
|
||||
tool_names = set()
|
||||
for group in groups:
|
||||
if group in TOOL_GROUPS:
|
||||
tool_names.update(TOOL_GROUPS[group])
|
||||
else:
|
||||
tool_names = set()
|
||||
if config.enable_acp:
|
||||
tool_names.update(TOOL_GROUPS["acp"])
|
||||
if config.enable_lsp:
|
||||
tool_names.update(TOOL_GROUPS["lsp"])
|
||||
if config.enable_basic:
|
||||
tool_names.update(TOOL_GROUPS["basic"])
|
||||
|
||||
# Remove disabled groups
|
||||
for group in disable:
|
||||
if group in TOOL_GROUPS:
|
||||
tool_names -= set(TOOL_GROUPS[group])
|
||||
|
||||
return tool_names
|
||||
|
||||
|
||||
def _load_basic_tools() -> Dict[str, Callable]:
|
||||
"""Lazy load basic tools from praisonaiagents."""
|
||||
tools = {}
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import read_file
|
||||
tools["read_file"] = read_file
|
||||
except ImportError:
|
||||
logger.debug("read_file not available")
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import write_file
|
||||
tools["write_file"] = write_file
|
||||
except ImportError:
|
||||
logger.debug("write_file not available")
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import list_files
|
||||
tools["list_files"] = list_files
|
||||
except ImportError:
|
||||
logger.debug("list_files not available")
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import execute_command
|
||||
tools["execute_command"] = execute_command
|
||||
except ImportError:
|
||||
logger.debug("execute_command not available")
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import internet_search
|
||||
tools["internet_search"] = internet_search
|
||||
except ImportError:
|
||||
logger.debug("internet_search not available")
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def _load_acp_tools(config: ToolConfig) -> Dict[str, Callable]:
|
||||
"""Lazy load ACP tools with runtime initialization."""
|
||||
tools = {}
|
||||
|
||||
try:
|
||||
from .interactive_runtime import InteractiveRuntime, RuntimeConfig
|
||||
from .agent_tools import create_agent_centric_tools
|
||||
|
||||
# Create runtime config
|
||||
runtime_config = RuntimeConfig(
|
||||
workspace=config.workspace,
|
||||
lsp_enabled=config.lsp_enabled,
|
||||
acp_enabled=config.acp_enabled,
|
||||
approval_mode=config.approval_mode,
|
||||
)
|
||||
|
||||
# Create runtime (lazy - doesn't start until needed)
|
||||
runtime = InteractiveRuntime(runtime_config)
|
||||
|
||||
# Get all agent-centric tools
|
||||
all_tools = create_agent_centric_tools(runtime)
|
||||
|
||||
# Map by name
|
||||
for tool in all_tools:
|
||||
name = tool.__name__
|
||||
if name.startswith("acp_"):
|
||||
tools[name] = tool
|
||||
|
||||
logger.debug(f"Loaded {len(tools)} ACP tools")
|
||||
|
||||
except ImportError as e:
|
||||
logger.debug(f"ACP tools not available: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading ACP tools: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def _load_lsp_tools(config: ToolConfig) -> Dict[str, Callable]:
|
||||
"""Lazy load LSP tools with runtime initialization."""
|
||||
tools = {}
|
||||
|
||||
try:
|
||||
from .interactive_runtime import InteractiveRuntime, RuntimeConfig
|
||||
from .agent_tools import create_agent_centric_tools
|
||||
|
||||
# Create runtime config
|
||||
runtime_config = RuntimeConfig(
|
||||
workspace=config.workspace,
|
||||
lsp_enabled=config.lsp_enabled,
|
||||
acp_enabled=config.acp_enabled,
|
||||
approval_mode=config.approval_mode,
|
||||
)
|
||||
|
||||
# Create runtime
|
||||
runtime = InteractiveRuntime(runtime_config)
|
||||
|
||||
# Get all agent-centric tools
|
||||
all_tools = create_agent_centric_tools(runtime)
|
||||
|
||||
# Map by name
|
||||
for tool in all_tools:
|
||||
name = tool.__name__
|
||||
if name.startswith("lsp_"):
|
||||
tools[name] = tool
|
||||
|
||||
logger.debug(f"Loaded {len(tools)} LSP tools")
|
||||
|
||||
except ImportError as e:
|
||||
logger.debug(f"LSP tools not available: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading LSP tools: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def get_interactive_tools(
|
||||
groups: Optional[List[str]] = None,
|
||||
disable: Optional[List[str]] = None,
|
||||
config: Optional[ToolConfig] = None,
|
||||
workspace: Optional[str] = None,
|
||||
) -> List[Callable]:
|
||||
"""
|
||||
Get the default interactive tools for TUI and prompt modes.
|
||||
|
||||
This is the canonical source of truth for interactive tool defaults.
|
||||
Both `praisonai tui launch` and `praison "prompt"` should use this.
|
||||
|
||||
Args:
|
||||
groups: Specific tool groups to include (default: all enabled)
|
||||
disable: Tool groups to disable (e.g., ["acp", "lsp"])
|
||||
config: Tool configuration (created from env if not provided)
|
||||
workspace: Override workspace path
|
||||
|
||||
Returns:
|
||||
List of tool callables
|
||||
|
||||
Example:
|
||||
# Default (all tools)
|
||||
tools = get_interactive_tools()
|
||||
|
||||
# Disable ACP tools
|
||||
tools = get_interactive_tools(disable=["acp"])
|
||||
|
||||
# Only basic tools
|
||||
tools = get_interactive_tools(groups=["basic"])
|
||||
"""
|
||||
if config is None:
|
||||
config = ToolConfig.from_env()
|
||||
|
||||
if workspace:
|
||||
config.workspace = workspace
|
||||
|
||||
# Resolve which tools to include
|
||||
tool_names = resolve_tool_groups(groups, disable, config)
|
||||
|
||||
# Load tools by group
|
||||
all_tools: Dict[str, Callable] = {}
|
||||
|
||||
# Load basic tools (always available, no runtime needed)
|
||||
if config.enable_basic and not (disable and "basic" in disable):
|
||||
basic_tools = _load_basic_tools()
|
||||
all_tools.update(basic_tools)
|
||||
|
||||
# Load ACP tools (requires runtime)
|
||||
if config.enable_acp and not (disable and "acp" in disable):
|
||||
acp_tools = _load_acp_tools(config)
|
||||
all_tools.update(acp_tools)
|
||||
|
||||
# Load LSP tools (requires runtime)
|
||||
if config.enable_lsp and not (disable and "lsp" in disable):
|
||||
lsp_tools = _load_lsp_tools(config)
|
||||
all_tools.update(lsp_tools)
|
||||
|
||||
# Filter to requested tools
|
||||
result = []
|
||||
for name in sorted(tool_names): # Deterministic ordering
|
||||
if name in all_tools:
|
||||
result.append(all_tools[name])
|
||||
|
||||
logger.debug(f"Loaded {len(result)} interactive tools: {[t.__name__ for t in result]}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def merge_tool_overrides(
|
||||
default_tools: List[Callable],
|
||||
add_tools: Optional[List[Callable]] = None,
|
||||
remove_names: Optional[List[str]] = None,
|
||||
) -> List[Callable]:
|
||||
"""
|
||||
Merge tool overrides with default tools.
|
||||
|
||||
Args:
|
||||
default_tools: Base list of tools
|
||||
add_tools: Tools to add
|
||||
remove_names: Tool names to remove
|
||||
|
||||
Returns:
|
||||
Merged tool list
|
||||
"""
|
||||
# Build name -> tool map
|
||||
tool_map = {t.__name__: t for t in default_tools}
|
||||
|
||||
# Remove specified tools
|
||||
if remove_names:
|
||||
for name in remove_names:
|
||||
tool_map.pop(name, None)
|
||||
|
||||
# Add new tools
|
||||
if add_tools:
|
||||
for tool in add_tools:
|
||||
tool_map[tool.__name__] = tool
|
||||
|
||||
return list(tool_map.values())
|
||||
|
||||
|
||||
def get_tool_group_names() -> Dict[str, List[str]]:
|
||||
"""Get available tool groups and their tool names."""
|
||||
return TOOL_GROUPS.copy()
|
||||
|
||||
|
||||
def print_tool_summary(tools: List[Callable]) -> None:
|
||||
"""Print a summary of loaded tools (for debugging)."""
|
||||
acp_count = sum(1 for t in tools if t.__name__.startswith("acp_"))
|
||||
lsp_count = sum(1 for t in tools if t.__name__.startswith("lsp_"))
|
||||
basic_count = len(tools) - acp_count - lsp_count
|
||||
|
||||
print(f"Interactive tools loaded: {len(tools)} total")
|
||||
print(f" - ACP tools: {acp_count}")
|
||||
print(f" - LSP tools: {lsp_count}")
|
||||
print(f" - Basic tools: {basic_count}")
|
||||
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user