mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 03:31:28 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a71aac6b8 | ||
|
|
287f42dfee | ||
|
|
57b637948f | ||
|
|
757d402cae |
No files matched your search
@@ -1930,11 +1930,11 @@ agent = Agent(
|
||||
auto_save="my-project"
|
||||
)
|
||||
|
||||
# Load history from last 5 sessions
|
||||
# Load history from past sessions via context management
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
memory=True,
|
||||
history_in_context=5
|
||||
context=True, # Enable context management for history
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
"""
|
||||
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>=3.2.1" \
|
||||
"praisonai>=3.4.0" \
|
||||
"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>=3.2.1" \
|
||||
"praisonai>=3.4.0" \
|
||||
"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>=3.2.1" \
|
||||
"praisonai>=3.4.0" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,257 +0,0 @@
|
||||
# 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
|
||||
@@ -1,53 +0,0 @@
|
||||
# Templates CLI
|
||||
|
||||
```bash
|
||||
praisonai templates list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates search video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates info ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --offline
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --strict-tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-source praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --no-template-tools-py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates init my-project --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache clear
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache list
|
||||
```
|
||||
@@ -1,104 +0,0 @@
|
||||
# Templates Module
|
||||
|
||||
## Loading Templates
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateLoader
|
||||
|
||||
loader = TemplateLoader()
|
||||
|
||||
# Load from local path
|
||||
template = loader.load("./my-template")
|
||||
|
||||
# Load from URI
|
||||
template = loader.load("ai-video-editor")
|
||||
|
||||
# Load offline
|
||||
template = loader.load("ai-video-editor", offline=True)
|
||||
|
||||
# Check requirements
|
||||
missing = loader.check_requirements(template)
|
||||
```
|
||||
|
||||
## Template Discovery
|
||||
|
||||
```python
|
||||
from praisonai.templates.discovery import TemplateDiscovery
|
||||
|
||||
discovery = TemplateDiscovery(
|
||||
include_package=True,
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Find template
|
||||
result = discovery.find_template("ai-video-editor")
|
||||
|
||||
# List all templates
|
||||
templates = discovery.list_templates()
|
||||
```
|
||||
|
||||
## Template Config
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateConfig
|
||||
|
||||
# Access template properties
|
||||
template.name
|
||||
template.description
|
||||
template.version
|
||||
template.author
|
||||
template.requires
|
||||
template.workflow_file
|
||||
template.agents_file
|
||||
template.path
|
||||
```
|
||||
|
||||
## Tool Override Integration
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Build registry with template sources
|
||||
tools_sources = template.requires.get("tools_sources", [])
|
||||
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=tools_sources,
|
||||
template_dir=str(template.path),
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names
|
||||
tools = resolve_tools(["shell_tool"], registry=registry)
|
||||
```
|
||||
|
||||
## TEMPLATE.yaml Schema
|
||||
|
||||
```yaml
|
||||
name: my-template
|
||||
version: "1.0.0"
|
||||
description: Template description
|
||||
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
packages: [praisonai-tools]
|
||||
env: [OPENAI_API_KEY]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
|
||||
workflow: workflow.yaml
|
||||
agents: agents.yaml
|
||||
|
||||
config:
|
||||
input:
|
||||
type: string
|
||||
required: true
|
||||
output:
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
preset: podcast
|
||||
```
|
||||
@@ -1,65 +0,0 @@
|
||||
# Tools CLI
|
||||
|
||||
```bash
|
||||
praisonai tools list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info internet_search
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "web"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "file"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor --json
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve internet_search --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --include praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --entrypoints
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
@@ -1,90 +0,0 @@
|
||||
# Tools Module
|
||||
|
||||
## Tool Registry
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Create registry with defaults
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
# Create registry with overrides
|
||||
registry = create_tool_registry_with_overrides(
|
||||
override_files=["my_tools.py"],
|
||||
override_dirs=["./tools"],
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Create registry with tools_sources
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=["praisonai_tools.video"],
|
||||
template_dir="./my-template",
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names to callables
|
||||
tools = resolve_tools(
|
||||
["shell_tool", "internet_search"],
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Override Loader
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import ToolOverrideLoader
|
||||
|
||||
loader = ToolOverrideLoader()
|
||||
|
||||
# Load from file
|
||||
tools = loader.load_from_file("my_tools.py")
|
||||
|
||||
# Load from directory
|
||||
tools = loader.load_from_directory("./tools")
|
||||
|
||||
# Load from module
|
||||
tools = loader.load_from_module("praisonai_tools.video")
|
||||
|
||||
# Get default tool directories
|
||||
dirs = loader.get_default_tool_dirs()
|
||||
```
|
||||
|
||||
## Resolution Order
|
||||
|
||||
1. CLI `--tools` files (highest priority)
|
||||
2. CLI `--tools-dir` directories
|
||||
3. Template `tools_sources` (from TEMPLATE.yaml)
|
||||
4. Template-local `tools.py`
|
||||
5. Default dirs (`~/.praison/tools`, `~/.config/praison/tools`)
|
||||
6. Package discovery (`praisonai_tools`)
|
||||
7. Built-in tools (lowest priority)
|
||||
|
||||
## Creating Custom Tools
|
||||
|
||||
```python
|
||||
from praisonai_tools import tool
|
||||
|
||||
@tool
|
||||
def my_custom_tool(query: str) -> str:
|
||||
"""My custom tool description.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
"""
|
||||
return f"Result for: {query}"
|
||||
```
|
||||
|
||||
## Template tools_sources
|
||||
|
||||
```yaml
|
||||
# TEMPLATE.yaml
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
- ./tools_dir/
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Agent-Centric API Example
|
||||
|
||||
Demonstrates the new consolidated feature parameters for PraisonAI Agents.
|
||||
These params provide a cleaner, more intuitive API with progressive disclosure.
|
||||
|
||||
New Consolidated Params:
|
||||
- memory: Union[bool, MemoryConfig, MemoryManager] - Memory and session management
|
||||
- knowledge: Union[bool, List[str], KnowledgeConfig, KnowledgeBase] - RAG and retrieval
|
||||
- planning: Union[bool, PlanningConfig] - Planning mode
|
||||
- reflection: Union[bool, ReflectionConfig] - Self-reflection
|
||||
- guardrails: Union[bool, Callable, GuardrailConfig] - Safety validation
|
||||
- web: Union[bool, WebConfig] - Web search and fetch
|
||||
- context: Union[bool, ManagerConfig, ContextManager] - Context management
|
||||
"""
|
||||
|
||||
from praisonaiagents import (
|
||||
Agent,
|
||||
# Config classes for progressive disclosure
|
||||
MemoryConfig,
|
||||
KnowledgeConfig,
|
||||
PlanningConfig,
|
||||
ReflectionConfig,
|
||||
GuardrailConfig,
|
||||
WebConfig,
|
||||
)
|
||||
|
||||
|
||||
def example_simple_enable():
|
||||
"""Example 1: Simple boolean enable (easiest)"""
|
||||
print("\n=== Example 1: Simple Boolean Enable ===")
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
memory=True, # Enable memory with defaults
|
||||
reflection=True, # Enable self-reflection
|
||||
web=True, # Enable web search + fetch
|
||||
)
|
||||
print(f"Agent created with memory={agent._memory_instance is not None}")
|
||||
print(f" self_reflect={agent.self_reflect}")
|
||||
print(f" web_search={agent.web_search}")
|
||||
|
||||
|
||||
def example_with_config():
|
||||
"""Example 2: Using config objects (more control)"""
|
||||
print("\n=== Example 2: Using Config Objects ===")
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant",
|
||||
memory=MemoryConfig(
|
||||
backend="file",
|
||||
user_id="researcher_001",
|
||||
auto_memory=True,
|
||||
),
|
||||
reflection=ReflectionConfig(
|
||||
min_iterations=1,
|
||||
max_iterations=5,
|
||||
llm="gpt-4o-mini",
|
||||
),
|
||||
web=WebConfig(
|
||||
search=True,
|
||||
fetch=False, # Only search, no full page fetch
|
||||
max_results=3,
|
||||
),
|
||||
)
|
||||
print(f"Agent created with user_id={agent.user_id}")
|
||||
print(f" {agent.max_reflect}")
|
||||
print(f" web_search={agent.web_search}, web_fetch={agent.web_fetch}")
|
||||
|
||||
|
||||
def example_guardrails():
|
||||
"""Example 3: Guardrails with callable or config"""
|
||||
print("\n=== Example 3: Guardrails ===")
|
||||
|
||||
# Option A: Direct callable
|
||||
def my_validator(output):
|
||||
"""Simple validator that checks response length."""
|
||||
is_valid = len(output.raw) < 1000
|
||||
return (is_valid, output if is_valid else "Response too long")
|
||||
|
||||
agent_a = Agent(
|
||||
instructions="Be concise",
|
||||
guardrails=my_validator, # Direct callable
|
||||
)
|
||||
print(f"Agent A: guardrail is callable = {callable(agent_a.guardrail)}")
|
||||
|
||||
# Option B: Using GuardrailConfig
|
||||
agent_b = Agent(
|
||||
instructions="Be helpful and safe",
|
||||
guardrails=GuardrailConfig(
|
||||
llm_validator="Ensure the response is helpful, accurate, and safe",
|
||||
max_retries=3,
|
||||
),
|
||||
)
|
||||
print(f"Agent B: guardrail = {agent_b.guardrail}")
|
||||
print(f" max_retries = {agent_b.max_guardrail_retries}")
|
||||
|
||||
|
||||
def example_planning():
|
||||
"""Example 4: Planning mode"""
|
||||
print("\n=== Example 4: Planning Mode ===")
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a project planner",
|
||||
planning=PlanningConfig(
|
||||
reasoning=True,
|
||||
read_only=True, # Plan mode - only read operations
|
||||
),
|
||||
)
|
||||
print(f"Agent created with planning={agent.planning}")
|
||||
print(f" plan_mode={agent.plan_mode}")
|
||||
print(f" planning_reasoning={agent.planning_reasoning}")
|
||||
|
||||
|
||||
def example_backward_compatible():
|
||||
"""Example 5: Using new consolidated params"""
|
||||
print("\n=== Example 5: Consolidated Params ===")
|
||||
|
||||
# New consolidated params
|
||||
agent = Agent(
|
||||
instructions="Test agent",
|
||||
reflection=True,
|
||||
web=WebConfig(search=True, fetch=False),
|
||||
)
|
||||
print(f"Consolidated params work: self_reflect={agent.self_reflect}")
|
||||
print(f" web_search={agent.web_search}, web_fetch={agent.web_fetch}")
|
||||
|
||||
|
||||
def example_real_chat():
|
||||
"""Example 6: Real chat with new API"""
|
||||
print("\n=== Example 6: Real Chat ===")
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful math tutor. Be concise.",
|
||||
reflection=False,
|
||||
web=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
response = agent.start("What is the square root of 144?")
|
||||
print(f"Response: {response}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("PraisonAI Agent-Centric API Examples")
|
||||
print("=" * 60)
|
||||
|
||||
example_simple_enable()
|
||||
example_with_config()
|
||||
example_guardrails()
|
||||
example_planning()
|
||||
example_backward_compatible()
|
||||
example_real_chat()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
@@ -2,10 +2,10 @@
|
||||
"""
|
||||
Basic Background Tasks Example for PraisonAI Agents.
|
||||
|
||||
This example demonstrates how to use the background task system to:
|
||||
1. Run tasks asynchronously
|
||||
2. Track task progress
|
||||
3. Cancel running tasks
|
||||
This example demonstrates how to use the background task system with Agent:
|
||||
1. Create an Agent with background task support
|
||||
2. Run agent tasks asynchronously
|
||||
3. Track task progress
|
||||
4. Handle task completion
|
||||
|
||||
Usage:
|
||||
@@ -13,74 +13,77 @@ Usage:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.background import BackgroundRunner, BackgroundConfig, TaskStatus
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("Background Tasks Demo")
|
||||
print("Agent-Centric Background Tasks Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a background runner with config
|
||||
config = BackgroundConfig(max_concurrent_tasks=3)
|
||||
runner = BackgroundRunner(config=config)
|
||||
runner = BackgroundRunner(config=BackgroundConfig(max_concurrent_tasks=3))
|
||||
|
||||
# Create an Agent with background task support
|
||||
agent = Agent(
|
||||
name="AsyncAssistant",
|
||||
instructions="You are a helpful research assistant.",
|
||||
background=runner
|
||||
)
|
||||
|
||||
print("\n--- Agent with Background Support Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Background runner: {agent.background is not None}")
|
||||
|
||||
# Define some example tasks
|
||||
def slow_task(duration: float, name: str) -> str:
|
||||
"""A slow task that simulates work."""
|
||||
print(f" 🔄 Task '{name}' starting (will take {duration}s)")
|
||||
time.sleep(duration)
|
||||
return f"Task '{name}' completed after {duration}s"
|
||||
async def research_task(topic: str) -> str:
|
||||
"""Simulate a research task."""
|
||||
print(f" 🔄 Researching '{topic}'...")
|
||||
await asyncio.sleep(1.5)
|
||||
return f"Research on '{topic}' completed with 5 key findings"
|
||||
|
||||
async def async_task(duration: float, name: str) -> str:
|
||||
"""An async task that simulates work."""
|
||||
print(f" 🔄 Async task '{name}' starting (will take {duration}s)")
|
||||
await asyncio.sleep(duration)
|
||||
return f"Async task '{name}' completed after {duration}s"
|
||||
async def analysis_task(data: str) -> str:
|
||||
"""Simulate an analysis task."""
|
||||
print(f" 🔄 Analyzing data...")
|
||||
await asyncio.sleep(1.0)
|
||||
return f"Analysis complete: {data}"
|
||||
|
||||
# Submit tasks
|
||||
print("\n--- Submitting Tasks ---")
|
||||
# Submit tasks via agent's background runner
|
||||
print("\n--- Submitting Background Tasks ---")
|
||||
|
||||
task1 = await runner.submit(slow_task, args=(1.0, "quick-job"), name="task1")
|
||||
print(f"✅ Submitted task 1: {task1.id[:8]}")
|
||||
task1 = await agent.background.submit(
|
||||
research_task,
|
||||
args=("AI trends 2025",),
|
||||
name="research_ai"
|
||||
)
|
||||
print(f"✅ Submitted research task: {task1.id[:8]}")
|
||||
|
||||
task2 = await runner.submit(slow_task, args=(2.0, "medium-job"), name="task2")
|
||||
print(f"✅ Submitted task 2: {task2.id[:8]}")
|
||||
|
||||
task3 = await runner.submit(async_task, args=(1.5, "async-job"), name="task3")
|
||||
print(f"✅ Submitted async task 3: {task3.id[:8]}")
|
||||
task2 = await agent.background.submit(
|
||||
analysis_task,
|
||||
args=("market data",),
|
||||
name="analyze_market"
|
||||
)
|
||||
print(f"✅ Submitted analysis task: {task2.id[:8]}")
|
||||
|
||||
# List all tasks
|
||||
print("\n--- Task Status ---")
|
||||
for task in runner.tasks:
|
||||
for task in agent.background.tasks:
|
||||
print(f" [{task.id[:8]}] {task.status.value}")
|
||||
|
||||
# Wait for task 1 to complete
|
||||
print("\n--- Waiting for Task 1 ---")
|
||||
await task1.wait(timeout=5.0)
|
||||
print(f"✅ Task 1 result: {task1.result}")
|
||||
|
||||
# Check status of all tasks
|
||||
print("\n--- Updated Task Status ---")
|
||||
for task in [task1, task2, task3]:
|
||||
status = "✅" if task.status == TaskStatus.COMPLETED else "🔄"
|
||||
print(f" {status} [{task.id[:8]}] {task.status.value}")
|
||||
|
||||
# Wait for all remaining tasks
|
||||
print("\n--- Waiting for All Tasks ---")
|
||||
# Wait for tasks to complete
|
||||
print("\n--- Waiting for Tasks ---")
|
||||
await task1.wait(timeout=10.0)
|
||||
await task2.wait(timeout=10.0)
|
||||
await task3.wait(timeout=10.0)
|
||||
|
||||
# Final status
|
||||
print("\n--- Final Results ---")
|
||||
for task in [task1, task2, task3]:
|
||||
if task.result:
|
||||
print(f" ✅ {task.result}")
|
||||
# Final results
|
||||
print("\n--- Results ---")
|
||||
for task in [task1, task2]:
|
||||
status = "✅" if task.status == TaskStatus.COMPLETED else "❌"
|
||||
print(f" {status} {task.result}")
|
||||
|
||||
# Cleanup
|
||||
runner.clear_completed()
|
||||
agent.background.clear_completed()
|
||||
print("\n✅ Cleared completed tasks")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"""
|
||||
Basic Checkpoints Example for PraisonAI Agents.
|
||||
|
||||
This example demonstrates how to use the shadow git checkpointing system to:
|
||||
1. Save checkpoints before making changes
|
||||
2. List available checkpoints
|
||||
3. View diffs between checkpoints
|
||||
This example demonstrates how to use the shadow git checkpointing system with Agent:
|
||||
1. Create an Agent with checkpoint support
|
||||
2. Save checkpoints before making changes
|
||||
3. List available checkpoints
|
||||
4. Restore to a previous checkpoint
|
||||
|
||||
Usage:
|
||||
@@ -16,6 +16,7 @@ import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.checkpoints import CheckpointService
|
||||
|
||||
|
||||
@@ -23,7 +24,7 @@ async def main():
|
||||
# Create a temporary workspace for demonstration
|
||||
with tempfile.TemporaryDirectory() as workspace:
|
||||
print("=" * 60)
|
||||
print("Shadow Git Checkpointing Demo")
|
||||
print("Agent-Centric Checkpointing Demo")
|
||||
print("=" * 60)
|
||||
print(f"\nWorkspace: {workspace}")
|
||||
|
||||
@@ -32,24 +33,35 @@ async def main():
|
||||
test_file.write_text("# Initial content\nprint('Hello, World!')\n")
|
||||
print(f"\n✅ Created initial file: {test_file.name}")
|
||||
|
||||
# Initialize the checkpoint service
|
||||
service = CheckpointService(workspace_dir=workspace)
|
||||
# Create checkpoint service
|
||||
checkpoints = CheckpointService(workspace_dir=workspace)
|
||||
|
||||
# Create an Agent with checkpoint support
|
||||
agent = Agent(
|
||||
name="RefactorBot",
|
||||
instructions="You are a code refactoring assistant.",
|
||||
checkpoints=checkpoints
|
||||
)
|
||||
|
||||
print("\n--- Agent with Checkpoint Support Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Checkpoints enabled: {agent.checkpoints is not None}")
|
||||
|
||||
try:
|
||||
# Initialize the shadow git repository
|
||||
print("\n--- Initializing Checkpoint Service ---")
|
||||
await service.initialize()
|
||||
await agent.checkpoints.initialize()
|
||||
print("✅ Shadow git repository initialized")
|
||||
|
||||
# Save first checkpoint
|
||||
print("\n--- Saving Checkpoint 1: Initial State ---")
|
||||
result1 = await service.save("Initial state")
|
||||
result1 = await agent.checkpoints.save("Initial state")
|
||||
print(f"✅ Checkpoint saved: {result1.checkpoint.id[:8]}")
|
||||
print(f" Message: {result1.checkpoint.message}")
|
||||
|
||||
# Make some changes
|
||||
print("\n--- Making Changes ---")
|
||||
test_file.write_text("# Modified content\nprint('Hello, PraisonAI!')\nprint('Checkpoints are awesome!')\n")
|
||||
# Make some changes (simulating agent modifications)
|
||||
print("\n--- Simulating Agent Changes ---")
|
||||
test_file.write_text("# Refactored content\nprint('Hello, PraisonAI!')\nprint('Checkpoints are awesome!')\n")
|
||||
print("✅ Modified example.py")
|
||||
|
||||
# Create a new file
|
||||
@@ -58,38 +70,32 @@ async def main():
|
||||
print("✅ Created utils.py")
|
||||
|
||||
# Save second checkpoint
|
||||
print("\n--- Saving Checkpoint 2: After Changes ---")
|
||||
result2 = await service.save("Added features")
|
||||
print("\n--- Saving Checkpoint 2: After Refactoring ---")
|
||||
result2 = await agent.checkpoints.save("Added features")
|
||||
print(f"✅ Checkpoint saved: {result2.checkpoint.id[:8]}")
|
||||
|
||||
# List all checkpoints
|
||||
print("\n--- Listing All Checkpoints ---")
|
||||
checkpoints = await service.list_checkpoints()
|
||||
for i, cp in enumerate(checkpoints, 1):
|
||||
print(f" {i}. [{cp.id[:8]}] {cp.message} ({cp.timestamp})")
|
||||
checkpoints_list = await agent.checkpoints.list_checkpoints()
|
||||
for i, cp in enumerate(checkpoints_list, 1):
|
||||
print(f" {i}. [{cp.id[:8]}] {cp.message}")
|
||||
|
||||
# Show diff
|
||||
print("\n--- Current Diff (uncommitted changes) ---")
|
||||
diff = await service.diff()
|
||||
print("\n--- Current Diff ---")
|
||||
diff = await agent.checkpoints.diff()
|
||||
if diff.files:
|
||||
for f in diff.files:
|
||||
print(f" {f.status}: {f.path}")
|
||||
else:
|
||||
print(" No uncommitted changes")
|
||||
|
||||
# Make more changes to show diff
|
||||
test_file.write_text("# Even more changes\nprint('This will be reverted')\n")
|
||||
print("\n✅ Made more changes to example.py")
|
||||
|
||||
diff = await service.diff()
|
||||
print("\n--- New Diff ---")
|
||||
if diff.files:
|
||||
for f in diff.files:
|
||||
print(f" {f.status}: {f.path}")
|
||||
# Make more changes
|
||||
test_file.write_text("# Bad changes\nprint('This will be reverted')\n")
|
||||
print("\n✅ Made problematic changes to example.py")
|
||||
|
||||
# Restore to checkpoint 2
|
||||
print(f"\n--- Restoring to Checkpoint: {result2.checkpoint.id[:8]} ---")
|
||||
await service.restore(result2.checkpoint.id)
|
||||
await agent.checkpoints.restore(result2.checkpoint.id)
|
||||
print("✅ Restored successfully!")
|
||||
|
||||
# Verify restoration
|
||||
|
||||
@@ -2,140 +2,114 @@
|
||||
"""
|
||||
Basic Context Compaction Example for PraisonAI Agents.
|
||||
|
||||
This example demonstrates how to use context compaction to:
|
||||
1. Manage conversation context length
|
||||
2. Use different compaction strategies
|
||||
3. Track compaction results
|
||||
4. Preserve important messages
|
||||
This example demonstrates how to use context compaction with Agent:
|
||||
1. Create an Agent with context= parameter for compaction
|
||||
2. Use different compaction strategies via ManagerConfig
|
||||
3. Monitor compaction results
|
||||
|
||||
Usage:
|
||||
python basic_compaction.py
|
||||
"""
|
||||
|
||||
from praisonaiagents.compaction import (
|
||||
ContextCompactor, CompactionConfig, CompactionStrategy, CompactionResult
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Context Compaction Demo")
|
||||
print("Agent-Centric Context Compaction Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# ==========================================================================
|
||||
# Sample Messages
|
||||
# Available Strategies
|
||||
# ==========================================================================
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful AI assistant."},
|
||||
{"role": "user", "content": "Hello! Can you help me with Python?"},
|
||||
{"role": "assistant", "content": "Of course! I'd be happy to help you with Python. What would you like to know?"},
|
||||
{"role": "user", "content": "How do I read a file in Python?"},
|
||||
{"role": "assistant", "content": "To read a file in Python, you can use the built-in open() function. Here's an example:\n\nwith open('filename.txt', 'r') as f:\n content = f.read()\n\nThis opens the file, reads its content, and automatically closes it."},
|
||||
{"role": "user", "content": "What about writing to a file?"},
|
||||
{"role": "assistant", "content": "To write to a file, use the 'w' mode:\n\nwith open('filename.txt', 'w') as f:\n f.write('Hello, World!')\n\nUse 'a' mode to append instead of overwriting."},
|
||||
{"role": "user", "content": "Can you explain list comprehensions?"},
|
||||
{"role": "assistant", "content": "List comprehensions are a concise way to create lists. Instead of:\n\nresult = []\nfor x in range(10):\n result.append(x * 2)\n\nYou can write:\n\nresult = [x * 2 for x in range(10)]"},
|
||||
print("\n--- Available Compaction Strategies ---")
|
||||
|
||||
strategies = [
|
||||
("truncate", "Remove oldest messages first"),
|
||||
("sliding_window", "Keep most recent messages within limit"),
|
||||
("summarize", "Replace old messages with summary"),
|
||||
("smart", "Intelligently select messages to keep"),
|
||||
]
|
||||
|
||||
print(f"\n--- Original Messages ---")
|
||||
print(f" Message count: {len(messages)}")
|
||||
for name, desc in strategies:
|
||||
print(f" {name}: {desc}")
|
||||
|
||||
# ==========================================================================
|
||||
# Basic Compaction
|
||||
# Agent with Context Compaction via context= param
|
||||
# ==========================================================================
|
||||
print("\n--- Basic Compaction (Truncate Strategy) ---")
|
||||
print("\n--- Creating Agent with Context Compaction ---")
|
||||
|
||||
compactor = ContextCompactor(
|
||||
max_tokens=50, # Low limit to trigger compaction
|
||||
strategy=CompactionStrategy.TRUNCATE,
|
||||
preserve_system=True,
|
||||
preserve_recent=2
|
||||
# Use context= parameter with ManagerConfig for compaction
|
||||
agent = Agent(
|
||||
name="LongChat",
|
||||
instructions="You are a helpful assistant for extended conversations.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
compact_threshold=0.8,
|
||||
strategy="sliding_window",
|
||||
)
|
||||
)
|
||||
|
||||
# Check stats before
|
||||
stats = compactor.get_stats(messages)
|
||||
print(f" Total tokens: {stats['total_tokens']}")
|
||||
print(f" Max tokens: {stats['max_tokens']}")
|
||||
print(f" Needs compaction: {stats['needs_compaction']}")
|
||||
|
||||
# Compact
|
||||
compacted, result = compactor.compact(messages)
|
||||
|
||||
print(f"\n After compaction:")
|
||||
print(f" Messages: {len(messages)} -> {len(compacted)}")
|
||||
print(f" Tokens: {result.original_tokens} -> {result.compacted_tokens}")
|
||||
print(f" Saved: {result.tokens_saved} tokens")
|
||||
print(f" Compression: {result.compression_ratio:.1%}")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Context manager: {agent.context_manager is not None}")
|
||||
if agent.context_manager:
|
||||
print(f"Auto-compact: {agent.context_manager.config.auto_compact}")
|
||||
print(f"Threshold: {agent.context_manager.config.compact_threshold}")
|
||||
print(f"Strategy: {agent.context_manager.config.strategy}")
|
||||
|
||||
# ==========================================================================
|
||||
# Sliding Window Strategy
|
||||
# Different Strategy Examples via context= param
|
||||
# ==========================================================================
|
||||
print("\n--- Sliding Window Strategy ---")
|
||||
print("\n--- Strategy Examples ---")
|
||||
|
||||
compactor = ContextCompactor(
|
||||
max_tokens=50,
|
||||
strategy=CompactionStrategy.SLIDING,
|
||||
preserve_system=True,
|
||||
preserve_recent=3
|
||||
# Sliding window strategy
|
||||
sliding_agent = Agent(
|
||||
name="SlidingAgent",
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
strategy="sliding_window",
|
||||
)
|
||||
)
|
||||
print(f" Sliding: strategy={sliding_agent.context_manager.config.strategy}")
|
||||
|
||||
compacted, result = compactor.compact(messages)
|
||||
|
||||
print(f" Messages kept: {result.messages_kept}")
|
||||
print(f" Messages removed: {result.messages_removed}")
|
||||
print(f" Strategy: {result.strategy_used.value}")
|
||||
|
||||
# ==========================================================================
|
||||
# Summarize Strategy
|
||||
# ==========================================================================
|
||||
print("\n--- Summarize Strategy ---")
|
||||
|
||||
compactor = ContextCompactor(
|
||||
max_tokens=50,
|
||||
strategy=CompactionStrategy.SUMMARIZE,
|
||||
preserve_system=True,
|
||||
preserve_recent=2
|
||||
# Summarize strategy
|
||||
summarize_agent = Agent(
|
||||
name="SummarizeAgent",
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
strategy="summarize",
|
||||
)
|
||||
)
|
||||
print(f" Summarize: strategy={summarize_agent.context_manager.config.strategy}")
|
||||
|
||||
compacted, result = compactor.compact(messages)
|
||||
|
||||
print(f" Messages kept: {result.messages_kept}")
|
||||
print(f" Strategy: {result.strategy_used.value}")
|
||||
|
||||
# Show what messages remain
|
||||
print(f"\n Remaining message roles:")
|
||||
for msg in compacted:
|
||||
role = msg.get("role", "unknown")
|
||||
content_preview = msg.get("content", "")[:50]
|
||||
print(f" [{role}] {content_preview}...")
|
||||
|
||||
# ==========================================================================
|
||||
# No Compaction Needed
|
||||
# ==========================================================================
|
||||
print("\n--- No Compaction Needed ---")
|
||||
|
||||
compactor = ContextCompactor(
|
||||
max_tokens=10000, # High limit
|
||||
strategy=CompactionStrategy.TRUNCATE
|
||||
# Smart strategy
|
||||
smart_agent = Agent(
|
||||
name="SmartAgent",
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
strategy="smart",
|
||||
)
|
||||
)
|
||||
|
||||
compacted, result = compactor.compact(messages)
|
||||
|
||||
print(f" Was compacted: {result.was_compacted}")
|
||||
print(f" Messages unchanged: {len(compacted) == len(messages)}")
|
||||
print(f" Smart: strategy={smart_agent.context_manager.config.strategy}")
|
||||
|
||||
# ==========================================================================
|
||||
# Result Serialization
|
||||
# Context Manager Stats Demo
|
||||
# ==========================================================================
|
||||
print("\n--- Result Serialization ---")
|
||||
print("\n--- Context Manager Stats Demo ---")
|
||||
|
||||
compactor = ContextCompactor(max_tokens=50)
|
||||
_, result = compactor.compact(messages)
|
||||
|
||||
data = result.to_dict()
|
||||
print(f" Original tokens: {data['original_tokens']}")
|
||||
print(f" Compacted tokens: {data['compacted_tokens']}")
|
||||
print(f" Compression ratio: {data['compression_ratio']:.2f}")
|
||||
# Access context manager stats
|
||||
if agent.context_manager:
|
||||
stats = agent.context_manager.get_stats()
|
||||
print(f" Model: {stats.get('model', 'N/A')}")
|
||||
print(f" Model limit: {stats.get('model_limit', 'N/A')}")
|
||||
print(f" Output reserve: {stats.get('output_reserve', 'N/A')}")
|
||||
print(f" Usable budget: {stats.get('usable_budget', 'N/A')}")
|
||||
else:
|
||||
print(" Context manager not initialized")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demo Complete!")
|
||||
|
||||
@@ -1,87 +1,72 @@
|
||||
"""
|
||||
Fast Context with Agent - Basic Example
|
||||
Fast Context - Basic Example
|
||||
|
||||
This is the simplest way to use Fast Context with an Agent.
|
||||
Just set fast_context=True and the agent will automatically
|
||||
have access to rapid code search capabilities.
|
||||
FastContext provides rapid code search capabilities.
|
||||
Use it directly from the context module for code search.
|
||||
|
||||
Fast Context provides:
|
||||
FastContext provides:
|
||||
- 10-20x faster code search than traditional methods
|
||||
- Parallel execution (8 concurrent searches)
|
||||
- Automatic caching for repeated queries
|
||||
- Seamless integration with Agent workflows
|
||||
|
||||
Note: FastContext is now accessed via the context module directly,
|
||||
not as Agent parameters. For context management with agents,
|
||||
use Agent(context=True) or Agent(context=ManagerConfig(...)).
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Agent with Fast Context - Basic Example")
|
||||
print("Agent with Context Management - Basic Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Create an agent with Fast Context enabled
|
||||
# This is the simplest configuration
|
||||
# Create an agent with context management enabled
|
||||
# This is the recommended way to use context features
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
instructions="You are a helpful code assistant.",
|
||||
fast_context=True, # Enable Fast Context
|
||||
fast_context_path=".", # Search in current directory
|
||||
context=True, # Enable context management with defaults
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print("\n✓ Agent created with Fast Context enabled")
|
||||
print(f" - fast_context_enabled: {agent.fast_context_enabled}")
|
||||
print(f" - fast_context_path: {agent.fast_context_path}")
|
||||
print(f" - fast_context_model: {agent.fast_context_model}")
|
||||
print("\n✓ Agent created with context management enabled")
|
||||
print(f" - context_manager: {agent.context_manager is not None}")
|
||||
if agent.context_manager:
|
||||
print(f" - auto_compact: {agent.context_manager.config.auto_compact}")
|
||||
print(f" - strategy: {agent.context_manager.config.strategy}")
|
||||
|
||||
# Use delegate_to_fast_context to search code
|
||||
# For FastContext code search, use the module directly
|
||||
print("\n" + "-" * 60)
|
||||
print("Searching for 'class Agent' in the codebase...")
|
||||
print("Using FastContext for code search...")
|
||||
print("-" * 60)
|
||||
|
||||
context = agent.delegate_to_fast_context("class Agent")
|
||||
|
||||
if context:
|
||||
print(f"\n✓ Found relevant code!")
|
||||
print(f" Context length: {len(context)} characters")
|
||||
print("\nPreview of found context:")
|
||||
print("-" * 40)
|
||||
# Show first 500 characters
|
||||
preview = context[:500]
|
||||
print(preview)
|
||||
if len(context) > 500:
|
||||
print("...")
|
||||
else:
|
||||
print("\n✗ No relevant code found")
|
||||
|
||||
# Another search example
|
||||
print("\n" + "-" * 60)
|
||||
print("Searching for 'def chat' in the codebase...")
|
||||
print("-" * 60)
|
||||
|
||||
context = agent.delegate_to_fast_context("def chat")
|
||||
|
||||
if context:
|
||||
print(f"\n✓ Found relevant code!")
|
||||
print(f" Context length: {len(context)} characters")
|
||||
else:
|
||||
print("\n✗ No relevant code found")
|
||||
|
||||
# Access FastContext directly for more control
|
||||
print("\n" + "-" * 60)
|
||||
print("Direct FastContext access for advanced usage...")
|
||||
print("-" * 60)
|
||||
|
||||
fc = agent.fast_context
|
||||
if fc:
|
||||
result = fc.search("import")
|
||||
print(f"\n✓ Direct search for 'import':")
|
||||
try:
|
||||
from praisonaiagents.context.fast import FastContext
|
||||
|
||||
fc = FastContext(
|
||||
workspace_path=".",
|
||||
model="gpt-4o-mini",
|
||||
max_turns=4,
|
||||
max_parallel=8,
|
||||
)
|
||||
|
||||
result = fc.search("class Agent")
|
||||
print(f"\n✓ Search for 'class Agent':")
|
||||
print(f" Files found: {result.total_files}")
|
||||
print(f" Search time: {result.search_time_ms}ms")
|
||||
print(f" From cache: {result.from_cache}")
|
||||
|
||||
if result.total_files > 0:
|
||||
context = fc.get_context_for_agent("class Agent")
|
||||
print(f" Context length: {len(context)} characters")
|
||||
except ImportError:
|
||||
print("\n⚠ FastContext not available (optional feature)")
|
||||
except Exception as e:
|
||||
print(f"\n⚠ FastContext search failed: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Fast Context makes code search instant for your agents!")
|
||||
print("Context management is now unified via context= param!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
|
||||
@@ -1,103 +1,98 @@
|
||||
"""
|
||||
Example 4: Agent Integration with Fast Context
|
||||
Example 4: Agent Integration with Context Management
|
||||
|
||||
This example shows how to use Fast Context integrated with the Agent class
|
||||
for automatic code search delegation.
|
||||
This example shows how to use context management with the Agent class.
|
||||
Context management is now unified via the context= parameter.
|
||||
|
||||
Benefits:
|
||||
- Seamless integration with existing Agent workflows
|
||||
- Automatic context injection for code-related queries
|
||||
- Configurable via Agent parameters or environment variables
|
||||
- Automatic token tracking and optimization
|
||||
- Configurable via context= parameter with ManagerConfig
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
WORKSPACE = "/Users/praison/praisonai-package/src/praisonai-agents"
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("Agent Integration with Fast Context")
|
||||
print("Agent Integration with Context Management")
|
||||
print("=" * 70)
|
||||
|
||||
# Create an agent with Fast Context enabled
|
||||
print("\n1. Creating Agent with Fast Context")
|
||||
# Create an agent with context management enabled
|
||||
print("\n1. Creating Agent with Context Management")
|
||||
print("-" * 40)
|
||||
|
||||
config = ManagerConfig(
|
||||
auto_compact=True,
|
||||
compact_threshold=0.8,
|
||||
strategy="smart",
|
||||
monitor_enabled=True,
|
||||
monitor_path="./context_debug.txt",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
instructions="You are a helpful code assistant that can search and analyze code.",
|
||||
fast_context=True,
|
||||
fast_context_path=WORKSPACE,
|
||||
fast_context_model="gpt-4o-mini",
|
||||
fast_context_max_turns=4,
|
||||
fast_context_parallelism=8,
|
||||
fast_context_timeout=30.0,
|
||||
instructions="You are a helpful code assistant.",
|
||||
context=config,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print(f" Agent name: {agent.name}")
|
||||
print(f" Fast Context enabled: {agent.fast_context_enabled}")
|
||||
print(f" Fast Context path: {agent.fast_context_path}")
|
||||
print(f" Fast Context model: {agent.fast_context_model}")
|
||||
print(f" Max turns: {agent.fast_context_max_turns}")
|
||||
print(f" Parallelism: {agent.fast_context_parallelism}")
|
||||
print(f" Context manager: {agent.context_manager is not None}")
|
||||
if agent.context_manager:
|
||||
print(f" Auto-compact: {agent.context_manager.config.auto_compact}")
|
||||
print(f" Threshold: {agent.context_manager.config.compact_threshold}")
|
||||
print(f" Strategy: {agent.context_manager.config.strategy}")
|
||||
|
||||
# Use delegate_to_fast_context for code search
|
||||
print("\n2. Delegating Code Search to Fast Context")
|
||||
# For FastContext code search, use the module directly
|
||||
print("\n2. Using FastContext for Code Search")
|
||||
print("-" * 40)
|
||||
|
||||
queries = [
|
||||
"find the Agent class definition",
|
||||
"search for authentication handlers",
|
||||
"locate memory management code",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n Query: '{query}'")
|
||||
context = agent.delegate_to_fast_context(query)
|
||||
try:
|
||||
from praisonaiagents.context.fast import FastContext
|
||||
|
||||
if context:
|
||||
# Show preview of context
|
||||
lines = context.split('\n')
|
||||
preview_lines = lines[:5]
|
||||
print(f" Found context ({len(context)} chars):")
|
||||
for line in preview_lines:
|
||||
print(f" {line[:60]}{'...' if len(line) > 60 else ''}")
|
||||
if len(lines) > 5:
|
||||
print(f" ... and {len(lines) - 5} more lines")
|
||||
else:
|
||||
print(" No relevant code found")
|
||||
|
||||
# Access the FastContext instance directly
|
||||
print("\n3. Direct FastContext Access")
|
||||
print("-" * 40)
|
||||
|
||||
fc = agent.fast_context
|
||||
if fc:
|
||||
result = fc.search("def chat")
|
||||
print(f" Direct search for 'def chat':")
|
||||
print(f" Files found: {result.total_files}")
|
||||
print(f" Search time: {result.search_time_ms}ms")
|
||||
fc = FastContext(
|
||||
workspace_path=WORKSPACE,
|
||||
model="gpt-4o-mini",
|
||||
max_turns=4,
|
||||
max_parallel=8,
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
for f in result.files[:3]:
|
||||
print(f" - {f.path}")
|
||||
queries = [
|
||||
"find the Agent class definition",
|
||||
"search for chat method",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n Query: '{query}'")
|
||||
result = fc.search(query)
|
||||
print(f" Files found: {result.total_files}")
|
||||
print(f" Search time: {result.search_time_ms}ms")
|
||||
|
||||
except ImportError:
|
||||
print(" FastContext not available (optional feature)")
|
||||
except Exception as e:
|
||||
print(f" FastContext error: {e}")
|
||||
|
||||
# Show configuration options
|
||||
print("\n4. Configuration Options")
|
||||
print("\n3. Configuration Options")
|
||||
print("-" * 40)
|
||||
|
||||
print(" Agent parameters:")
|
||||
print(" - fast_context=True # Enable Fast Context")
|
||||
print(" - fast_context_path='.' # Workspace path")
|
||||
print(" - fast_context_model='gpt-4o-mini' # Search model")
|
||||
print(" - fast_context_max_turns=4 # Max search turns")
|
||||
print(" - fast_context_parallelism=8 # Parallel calls")
|
||||
print(" - fast_context_timeout=30.0 # Timeout (seconds)")
|
||||
print(" Agent context= parameter:")
|
||||
print(" - context=True # Enable with defaults")
|
||||
print(" - context=ManagerConfig(...) # Custom configuration")
|
||||
print(" - context=False # Disabled (default)")
|
||||
|
||||
print("\n Environment variables:")
|
||||
print(" - FAST_CONTEXT_MODEL")
|
||||
print(" - FAST_CONTEXT_MAX_TURNS")
|
||||
print("\n ManagerConfig options:")
|
||||
print(" - auto_compact=True # Auto-optimize context")
|
||||
print(" - compact_threshold=0.8 # Trigger at 80%")
|
||||
print(" - strategy='smart' # Optimization strategy")
|
||||
print(" - monitor_enabled=True # Write snapshots")
|
||||
print(" - monitor_path='./context.txt' # Snapshot path")
|
||||
print(" - FAST_CONTEXT_PARALLELISM")
|
||||
print(" - FAST_CONTEXT_TIMEOUT")
|
||||
print(" - FAST_CONTEXT_CACHE")
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
"""
|
||||
Context Budgeting Example
|
||||
|
||||
Demonstrates how to use the ContextBudgeter to allocate token budgets
|
||||
across different context segments for various models.
|
||||
Demonstrates how to use context budgeting with Agent via context= param,
|
||||
and also shows low-level ContextBudgeter usage for advanced scenarios.
|
||||
|
||||
Agent-Centric Quick Start:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(output_reserve=16000), # Custom output reserve
|
||||
)
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import (
|
||||
ManagerConfig,
|
||||
ContextBudgeter,
|
||||
BudgetAllocation,
|
||||
get_model_limit,
|
||||
@@ -14,6 +25,31 @@ from praisonaiagents.context import (
|
||||
)
|
||||
|
||||
|
||||
def agent_centric_example():
|
||||
"""Agent-centric usage - recommended approach."""
|
||||
print("=" * 60)
|
||||
print("Agent-Centric Context Budgeting")
|
||||
print("=" * 60)
|
||||
|
||||
# Simple: Enable context management with defaults
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
context=True,
|
||||
)
|
||||
print(f"Agent with context=True created")
|
||||
print(f"Context manager: {agent.context_manager is not None}")
|
||||
|
||||
# Custom: Specify output reserve
|
||||
agent2 = Agent(
|
||||
instructions="You are a code assistant.",
|
||||
context=ManagerConfig(output_reserve=16000),
|
||||
)
|
||||
print(f"Agent with custom output_reserve created")
|
||||
if agent2.context_manager:
|
||||
stats = agent2.context_manager.get_stats()
|
||||
print(f"Output reserve: {stats.get('output_reserve', 'N/A')}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Context Budgeting Example")
|
||||
@@ -92,6 +128,78 @@ def main():
|
||||
budget_dict = budgeter.to_dict()
|
||||
print(f"Budget as dict: {budget_dict}")
|
||||
|
||||
# Example 6: Overflow handling strategies overview
|
||||
print("\n6. Overflow Handling Strategies")
|
||||
print("-" * 40)
|
||||
strategies = [
|
||||
("truncate", "Remove oldest messages first", "Fast, simple", "Loses early context"),
|
||||
("sliding_window", "Keep N most recent messages", "Preserves recent", "Loses early context"),
|
||||
("prune_tools", "Truncate old tool outputs", "Keeps messages", "May lose tool details"),
|
||||
("summarize", "Replace old messages with summary", "Preserves meaning", "Slower, uses API"),
|
||||
("smart", "Combine strategies intelligently", "Best balance", "More complex"),
|
||||
]
|
||||
print(f"{'Strategy':<15} | {'Description':<30} | {'Pros':<18} | {'Cons'}")
|
||||
print("-" * 90)
|
||||
for name, desc, pros, cons in strategies:
|
||||
print(f"{name:<15} | {desc:<30} | {pros:<18} | {cons}")
|
||||
|
||||
# Example 7: Simulated overflow playbook
|
||||
print("\n7. Overflow Playbook (Thresholds → Actions)")
|
||||
print("-" * 40)
|
||||
playbook = [
|
||||
(0.70, "INFO", "Monitor usage, no action needed"),
|
||||
(0.80, "NOTICE", "Consider optimization soon"),
|
||||
(0.90, "WARNING", "Trigger auto-compact if enabled"),
|
||||
(0.95, "CRITICAL", "Aggressive optimization required"),
|
||||
(1.00, "OVERFLOW", "Immediate truncation to prevent API error"),
|
||||
]
|
||||
print(f"{'Usage':<8} | {'Level':<10} | {'Action'}")
|
||||
print("-" * 60)
|
||||
for threshold, level, action in playbook:
|
||||
print(f"{threshold:>6.0%} | {level:<10} | {action}")
|
||||
|
||||
# Example 8: Strategy comparison table
|
||||
print("\n8. Strategy Comparison")
|
||||
print("-" * 40)
|
||||
print("Strategy | Preserves | Loses | Best For")
|
||||
print("-" * 70)
|
||||
print("truncate | Recent msgs | Old msgs | Simple chats")
|
||||
print("sliding_window | Last N msgs | Earlier msgs | Long conversations")
|
||||
print("prune_tools | All messages | Tool details | Tool-heavy agents")
|
||||
print("summarize | Context meaning| Exact wording | Important history")
|
||||
print("smart | Balanced | Minimal | Production use")
|
||||
|
||||
# Example 9: Default strategy for interactive mode
|
||||
print("\n9. Default Strategy: Interactive Mode")
|
||||
print("-" * 40)
|
||||
print("Interactive mode default: context=False (zero overhead)")
|
||||
print("To enable context management in interactive mode:")
|
||||
print(" praisonai chat --context # Enable with defaults")
|
||||
print(" Or in code:")
|
||||
print(" agent = Agent(instructions='...', context=True)")
|
||||
print("")
|
||||
print("When enabled, defaults are:")
|
||||
print(" - auto_compact: True")
|
||||
print(" - compact_threshold: 0.8 (80% usage)")
|
||||
print(" - strategy: smart")
|
||||
print(" - output_reserve: model-specific (8K-16K)")
|
||||
|
||||
# Example 10: Default strategy for auto-agents mode
|
||||
print("\n10. Default Strategy: Auto-Agents Mode")
|
||||
print("-" * 40)
|
||||
print("Auto-agents mode default: context=False (zero overhead)")
|
||||
print("To enable for multi-agent workflows:")
|
||||
print(" from praisonaiagents import PraisonAIAgents")
|
||||
print(" agents = PraisonAIAgents(agents=[...], context=True)")
|
||||
print("")
|
||||
print("Recommended config for long tasks:")
|
||||
print(" context=ManagerConfig(")
|
||||
print(" auto_compact=True,")
|
||||
print(" compact_threshold=0.7, # Earlier trigger for safety")
|
||||
print(" strategy='smart',")
|
||||
print(" output_reserve=16384, # Larger reserve for complex outputs")
|
||||
print(" )")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ Context budgeting examples complete!")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -2,13 +2,27 @@
|
||||
"""
|
||||
Context Monitoring Example
|
||||
|
||||
Demonstrates how to use the ContextMonitor to write runtime context
|
||||
snapshots to disk for debugging and analysis.
|
||||
Demonstrates how to use context monitoring with Agent via context= param,
|
||||
and also shows low-level ContextMonitor usage for advanced scenarios.
|
||||
|
||||
Agent-Centric Quick Start:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(
|
||||
monitor_enabled=True,
|
||||
monitor_path="./debug/context.json",
|
||||
),
|
||||
)
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import (
|
||||
ManagerConfig,
|
||||
ContextMonitor,
|
||||
ContextLedger,
|
||||
BudgetAllocation,
|
||||
@@ -19,6 +33,28 @@ from praisonaiagents.context import (
|
||||
)
|
||||
|
||||
|
||||
def agent_centric_example():
|
||||
"""Agent-centric usage - recommended approach."""
|
||||
print("=" * 60)
|
||||
print("Agent-Centric Context Monitoring")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
monitor_path = os.path.join(tmpdir, "context.json")
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
context=ManagerConfig(
|
||||
monitor_enabled=True,
|
||||
monitor_path=monitor_path,
|
||||
monitor_format="json",
|
||||
),
|
||||
)
|
||||
print(f"Agent with monitoring enabled")
|
||||
print(f"Monitor path: {monitor_path}")
|
||||
print(f"Context manager: {agent.context_manager is not None}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Context Monitoring Example")
|
||||
|
||||
@@ -2,11 +2,25 @@
|
||||
"""
|
||||
Context Optimization Example
|
||||
|
||||
Demonstrates how to use various optimization strategies to reduce
|
||||
context size when approaching model limits.
|
||||
Demonstrates how to use context optimization with Agent via context= param,
|
||||
and also shows low-level optimizer usage for advanced scenarios.
|
||||
|
||||
Agent-Centric Quick Start:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are helpful.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
strategy="smart", # truncate, sliding_window, summarize, smart
|
||||
),
|
||||
)
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import (
|
||||
ManagerConfig,
|
||||
get_optimizer,
|
||||
OptimizerStrategy,
|
||||
TruncateOptimizer,
|
||||
@@ -17,6 +31,27 @@ from praisonaiagents.context import (
|
||||
)
|
||||
|
||||
|
||||
def agent_centric_example():
|
||||
"""Agent-centric usage - recommended approach."""
|
||||
print("=" * 60)
|
||||
print("Agent-Centric Context Optimization")
|
||||
print("=" * 60)
|
||||
|
||||
# Enable auto-compaction with smart strategy
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
context=ManagerConfig(
|
||||
auto_compact=True,
|
||||
compact_threshold=0.8,
|
||||
strategy="smart",
|
||||
),
|
||||
)
|
||||
print("Agent with auto-compaction enabled")
|
||||
print(f"Strategy: smart")
|
||||
print(f"Threshold: 0.8 (80% of context budget)")
|
||||
print(f"Context manager: {agent.context_manager is not None}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Context Optimization Example")
|
||||
|
||||
@@ -2,13 +2,24 @@
|
||||
"""
|
||||
Multi-Agent Context Isolation Example
|
||||
|
||||
Demonstrates how to use MultiAgentLedger and MultiAgentMonitor
|
||||
for managing context across multiple agents with isolation.
|
||||
Demonstrates how to use multi-agent context with PraisonAIAgents via context= param,
|
||||
and also shows low-level MultiAgentLedger usage for advanced scenarios.
|
||||
|
||||
Agent-Centric Quick Start:
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent1, agent2],
|
||||
context=ManagerConfig(policy="isolated"), # or "shared"
|
||||
)
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
from praisonaiagents.context import (
|
||||
ManagerConfig,
|
||||
MultiAgentLedger,
|
||||
MultiAgentMonitor,
|
||||
ContextLedgerManager,
|
||||
@@ -17,6 +28,31 @@ from praisonaiagents.context import (
|
||||
)
|
||||
|
||||
|
||||
def agent_centric_example():
|
||||
"""Agent-centric usage - recommended approach."""
|
||||
print("=" * 60)
|
||||
print("Agent-Centric Multi-Agent Context")
|
||||
print("=" * 60)
|
||||
|
||||
# Create agents with context enabled
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
instructions="You are a research specialist.",
|
||||
context=True,
|
||||
)
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
instructions="You are a technical writer.",
|
||||
context=True,
|
||||
)
|
||||
|
||||
# Multi-agent setup with isolated context policy
|
||||
# (each agent maintains its own context)
|
||||
print("Created agents with context=True")
|
||||
print("Use PraisonAIAgents(agents=[...], context=ManagerConfig(policy='isolated'))")
|
||||
print("for multi-agent context isolation")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Multi-Agent Context Isolation Example")
|
||||
|
||||
@@ -13,10 +13,10 @@ from praisonaiagents import Agent
|
||||
from praisonaiagents.context import (
|
||||
ContextBudgeter,
|
||||
ContextLedgerManager,
|
||||
ContextMonitor,
|
||||
get_optimizer,
|
||||
OptimizerStrategy,
|
||||
estimate_messages_tokens,
|
||||
format_percent,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def main():
|
||||
|
||||
# Get budget info
|
||||
budget = budgeter.allocate()
|
||||
print(f"\nModel: gpt-4o-mini")
|
||||
print("\nModel: gpt-4o-mini")
|
||||
print(f"Context budget: {budget.usable:,} tokens")
|
||||
print(f"Output reserve: {budget.output_reserve:,} tokens")
|
||||
|
||||
@@ -76,7 +76,7 @@ def main():
|
||||
current_tokens = ledger.get_total()
|
||||
utilization = budgeter.get_utilization(current_tokens)
|
||||
|
||||
print(f"[Context: {current_tokens} tokens, {utilization:.1%} used]")
|
||||
print(f"[Context: {current_tokens} tokens, {format_percent(utilization)} used]")
|
||||
|
||||
# Check if optimization needed
|
||||
if utilization > 0.8:
|
||||
@@ -101,7 +101,7 @@ def main():
|
||||
final_utilization = budgeter.get_utilization(final_tokens)
|
||||
|
||||
print(f"Total tokens used: {final_tokens:,}")
|
||||
print(f"Context utilization: {final_utilization:.1%}")
|
||||
print(f"Context utilization: {format_percent(final_utilization)}")
|
||||
print(f"Messages in conversation: {len(conversation)}")
|
||||
print(f"Remaining capacity: {budgeter.get_remaining(final_tokens):,} tokens")
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent-Centric Context Management Example
|
||||
|
||||
Demonstrates the recommended way to use context management with the new
|
||||
`context=` parameter. This is the simplest and most powerful approach.
|
||||
|
||||
The `context=` parameter accepts:
|
||||
- False (default): Zero overhead, no context management
|
||||
- True: Enable with safe defaults (auto-compact, smart strategy)
|
||||
- ManagerConfig: Custom configuration object
|
||||
- ContextManager: Pre-configured manager instance
|
||||
|
||||
Requires: OPENAI_API_KEY environment variable
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
|
||||
def example_1_simple_enable():
|
||||
"""Simplest usage - just enable context management."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Simple Enable (context=True)")
|
||||
print("=" * 60)
|
||||
|
||||
# Create agent with context management enabled
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant. Keep responses concise.",
|
||||
llm="gpt-4o-mini",
|
||||
context=True, # Enable with safe defaults
|
||||
)
|
||||
|
||||
# The agent now automatically:
|
||||
# - Tracks token usage
|
||||
# - Optimizes when approaching limits (80% threshold)
|
||||
# - Uses smart optimization strategy
|
||||
|
||||
print("Agent created with context management enabled")
|
||||
print(f"Context manager initialized: {agent.context_manager is not None}")
|
||||
|
||||
if agent.context_manager:
|
||||
print(f"Model: {agent.context_manager.model}")
|
||||
print(f"Auto-compact: {agent.context_manager.config.auto_compact}")
|
||||
print(f"Threshold: {agent.context_manager.config.compact_threshold}")
|
||||
print(f"Strategy: {agent.context_manager.config.strategy}")
|
||||
|
||||
# Make a chat call
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
response = agent.chat("What is 2+2?")
|
||||
print(f"\nResponse: {response}")
|
||||
else:
|
||||
print("\nSkipping API call (no OPENAI_API_KEY)")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def example_2_custom_config():
|
||||
"""Custom configuration with ManagerConfig."""
|
||||
print("=" * 60)
|
||||
print("Example 2: Custom Configuration (context=ManagerConfig)")
|
||||
print("=" * 60)
|
||||
|
||||
# Create custom configuration
|
||||
config = ManagerConfig(
|
||||
auto_compact=True,
|
||||
compact_threshold=0.7, # Trigger at 70% instead of 80%
|
||||
strategy="smart",
|
||||
monitor_enabled=True,
|
||||
monitor_path="./context_debug.txt",
|
||||
monitor_format="human",
|
||||
redact_sensitive=True,
|
||||
output_reserve=8000,
|
||||
)
|
||||
|
||||
# Create agent with custom config
|
||||
agent = Agent(
|
||||
instructions="You are a code assistant.",
|
||||
llm="gpt-4o-mini",
|
||||
context=config,
|
||||
)
|
||||
|
||||
print("Agent created with custom context config")
|
||||
print(f"Threshold: {agent.context_manager.config.compact_threshold}")
|
||||
print(f"Monitor enabled: {agent.context_manager.config.monitor_enabled}")
|
||||
print(f"Monitor path: {agent.context_manager.config.monitor_path}")
|
||||
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
response = agent.chat("Write a hello world in Python")
|
||||
print(f"\nResponse: {response[:200]}...")
|
||||
else:
|
||||
print("\nSkipping API call (no OPENAI_API_KEY)")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def example_3_disabled():
|
||||
"""Disabled context management - zero overhead."""
|
||||
print("=" * 60)
|
||||
print("Example 3: Disabled (context=False)")
|
||||
print("=" * 60)
|
||||
|
||||
# Create agent without context management (default)
|
||||
agent = Agent(
|
||||
instructions="You are a simple assistant.",
|
||||
llm="gpt-4o-mini",
|
||||
context=False, # Explicit disable (same as default)
|
||||
)
|
||||
|
||||
print("Agent created without context management")
|
||||
print(f"Context manager: {agent.context_manager}")
|
||||
print("Zero overhead: No token tracking, no optimization")
|
||||
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
response = agent.chat("Hello!")
|
||||
print(f"\nResponse: {response}")
|
||||
else:
|
||||
print("\nSkipping API call (no OPENAI_API_KEY)")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def example_4_access_stats():
|
||||
"""Access context statistics and history."""
|
||||
print("=" * 60)
|
||||
print("Example 4: Access Context Stats")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are helpful.",
|
||||
llm="gpt-4o-mini",
|
||||
context=True,
|
||||
)
|
||||
|
||||
manager = agent.context_manager
|
||||
|
||||
# Access budget info via internal attribute
|
||||
budget = manager._budget
|
||||
print(f"Model limit: {budget.model_limit:,} tokens")
|
||||
print(f"Output reserve: {budget.output_reserve:,} tokens")
|
||||
print(f"Usable budget: {budget.usable:,} tokens")
|
||||
|
||||
# Access config
|
||||
print("\nConfiguration:")
|
||||
print(f" Auto-compact: {manager.config.auto_compact}")
|
||||
print(f" Threshold: {manager.config.compact_threshold}")
|
||||
print(f" Strategy: {manager.config.strategy}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def example_5_multi_agent():
|
||||
"""Multi-agent with shared context management."""
|
||||
print("=" * 60)
|
||||
print("Example 5: Multi-Agent Context Management")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents import PraisonAIAgents, Task
|
||||
|
||||
config = ManagerConfig(
|
||||
auto_compact=True,
|
||||
compact_threshold=0.8,
|
||||
)
|
||||
|
||||
# Create agents
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
instructions="You research topics thoroughly.",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
instructions="You write clear summaries.",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
# Create tasks
|
||||
task1 = Task(description="Research AI trends", agent=researcher)
|
||||
task2 = Task(description="Write a summary", agent=writer)
|
||||
|
||||
# Create multi-agent system with context management
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
context=config, # Shared context config
|
||||
)
|
||||
|
||||
print("Multi-agent system created")
|
||||
print(f"Context manager: {agents.context_manager is not None}")
|
||||
|
||||
if agents.context_manager:
|
||||
print("Type: MultiAgentContextManager")
|
||||
print("Provides per-agent isolation with shared config")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
print("\n" + "=" * 60)
|
||||
print("Agent-Centric Context Management Examples")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
example_1_simple_enable()
|
||||
example_2_custom_config()
|
||||
example_3_disabled()
|
||||
example_4_access_stats()
|
||||
example_5_multi_agent()
|
||||
|
||||
print("=" * 60)
|
||||
print("✓ All examples complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -67,7 +67,7 @@ agent = Agent(
|
||||
goal="Summarize web content concisely",
|
||||
instructions="You are a helpful assistant that summarizes web content",
|
||||
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
verbose=True,
|
||||
tools=[get_url_context] # FIX: tools must be a list, not a single function
|
||||
)
|
||||
@@ -77,7 +77,7 @@ task_with_guardrailresult = Task(
|
||||
name="summarise article with GuardrailResult",
|
||||
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary below 500 characters",
|
||||
agent=agent,
|
||||
guardrail=validate_length_guardrailresult, # Using GuardrailResult
|
||||
guardrails=validate_length_guardrailresult, # Using GuardrailResult
|
||||
expected_output="summary of the article below 500 characters",
|
||||
max_retries=3 # Will retry up to 3 times if guardrail fails
|
||||
)
|
||||
@@ -87,7 +87,7 @@ task_with_tuple = Task(
|
||||
name="summarise article with tuple",
|
||||
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary below 500 characters",
|
||||
agent=agent,
|
||||
guardrail=validate_length_tuple, # Using Tuple[bool, Any]
|
||||
guardrails=validate_length_tuple, # Using Tuple[bool, Any]
|
||||
expected_output="summary of the article below 500 characters",
|
||||
max_retries=3
|
||||
)
|
||||
@@ -97,7 +97,7 @@ task_with_llm_guardrail = Task(
|
||||
name="summarise with LLM guardrail",
|
||||
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary",
|
||||
agent=agent,
|
||||
guardrail="Ensure the summary is professional, factual, and between 100-500 characters",
|
||||
guardrails="Ensure the summary is professional, factual, and between 100-500 characters",
|
||||
expected_output="professional summary of the article"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Context Compression Demo
|
||||
|
||||
This example demonstrates:
|
||||
1. Context compression for large retrieved content
|
||||
2. Token budget management
|
||||
3. Agent-centric knowledge retrieval with unique codes
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python compression_demo.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
# Unique verification code that CANNOT be guessed
|
||||
SECRET_CODE = "COMPRESS-8K4M2"
|
||||
|
||||
|
||||
def create_verbose_document(temp_dir: str) -> str:
|
||||
"""Create a verbose document with a hidden unique code."""
|
||||
filepath = os.path.join(temp_dir, 'verbose_policy.txt')
|
||||
|
||||
# Create a document with lots of filler text but one unique code
|
||||
with open(filepath, 'w') as f:
|
||||
f.write("""
|
||||
ACME CORPORATION COMPREHENSIVE POLICY MANUAL
|
||||
=============================================
|
||||
|
||||
CHAPTER 1: INTRODUCTION
|
||||
-----------------------
|
||||
|
||||
This comprehensive policy manual outlines all procedures, guidelines, and
|
||||
requirements for employees of Acme Corporation. All employees are expected
|
||||
to read and understand this document in its entirety. This manual supersedes
|
||||
all previous versions and is effective immediately upon publication.
|
||||
|
||||
The purpose of this manual is to provide clear guidance on company policies,
|
||||
procedures, and expectations. It covers a wide range of topics including but
|
||||
not limited to workplace conduct, benefits, leave policies, and security
|
||||
protocols.
|
||||
|
||||
CHAPTER 2: WORKPLACE CONDUCT
|
||||
----------------------------
|
||||
|
||||
All employees are expected to maintain professional conduct at all times.
|
||||
This includes treating colleagues with respect, maintaining a clean workspace,
|
||||
and adhering to the company dress code. Violations of workplace conduct
|
||||
policies may result in disciplinary action up to and including termination.
|
||||
|
||||
Professional behavior includes punctuality, meeting deadlines, and effective
|
||||
communication with team members. Employees should report any concerns about
|
||||
workplace conduct to their immediate supervisor or Human Resources.
|
||||
|
||||
CHAPTER 3: BENEFITS AND COMPENSATION
|
||||
------------------------------------
|
||||
|
||||
Acme Corporation offers a comprehensive benefits package to all full-time
|
||||
employees. This includes health insurance, dental coverage, vision care,
|
||||
and a 401(k) retirement plan with company matching up to 6% of salary.
|
||||
|
||||
Employees are eligible for benefits after completing 90 days of employment.
|
||||
Open enrollment occurs annually in November. Changes to benefits outside
|
||||
of open enrollment require a qualifying life event.
|
||||
|
||||
CHAPTER 4: LEAVE POLICIES
|
||||
-------------------------
|
||||
|
||||
Employees accrue paid time off (PTO) based on years of service:
|
||||
- 0-2 years: 15 days per year
|
||||
- 3-5 years: 20 days per year
|
||||
- 6+ years: 25 days per year
|
||||
|
||||
Sick leave is provided separately at 10 days per year. Unused sick leave
|
||||
may be carried over to the following year up to a maximum of 30 days.
|
||||
|
||||
CHAPTER 5: SECURITY PROTOCOLS
|
||||
-----------------------------
|
||||
|
||||
All employees must follow strict security protocols to protect company
|
||||
assets and confidential information. This includes using strong passwords,
|
||||
locking workstations when away, and reporting any suspicious activity.
|
||||
|
||||
IMPORTANT: The master security verification code is: COMPRESS-8K4M2
|
||||
|
||||
This code is required for accessing secure areas and systems. Do not share
|
||||
this code with unauthorized personnel. Misuse of security credentials is
|
||||
grounds for immediate termination.
|
||||
|
||||
CHAPTER 6: EMERGENCY PROCEDURES
|
||||
-------------------------------
|
||||
|
||||
In case of emergency, employees should follow the evacuation procedures
|
||||
posted throughout the building. Assembly points are located in the parking
|
||||
lot on the north side of the building.
|
||||
|
||||
Fire drills are conducted quarterly. All employees must participate in
|
||||
fire drills and know the location of the nearest emergency exit.
|
||||
|
||||
CHAPTER 7: CONCLUSION
|
||||
---------------------
|
||||
|
||||
This policy manual is a living document and may be updated periodically.
|
||||
Employees will be notified of any changes via email and updated copies
|
||||
will be made available on the company intranet.
|
||||
|
||||
Questions about any policy should be directed to Human Resources.
|
||||
|
||||
Document Version: 3.2.1
|
||||
Last Updated: 2024-12-01
|
||||
""")
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_compress_')
|
||||
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("Example: Context Compression Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create verbose document
|
||||
doc_path = create_verbose_document(temp_dir)
|
||||
print(f"\nCreated verbose document: {doc_path}")
|
||||
print(f"Secret code embedded: {SECRET_CODE}")
|
||||
|
||||
# Demonstrate compression
|
||||
print("\n" + "-" * 40)
|
||||
print("Compression Demo")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.rag import ContextCompressor
|
||||
|
||||
# Read the document
|
||||
with open(doc_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
print(f"\nOriginal document length: {len(content)} characters")
|
||||
|
||||
# Compress with different ratios
|
||||
for ratio in [0.3, 0.5, 0.7]:
|
||||
compressor = ContextCompressor(
|
||||
max_tokens=2000,
|
||||
target_ratio=ratio,
|
||||
)
|
||||
result = compressor.compress([content], query="security verification code")
|
||||
|
||||
print(f"\nCompression ratio {ratio}:")
|
||||
print(f" Original tokens: {result.original_tokens}")
|
||||
print(f" Compressed tokens: {result.compressed_tokens}")
|
||||
print(f" Actual ratio: {result.compressed_tokens / max(result.original_tokens, 1):.2f}")
|
||||
|
||||
# Check if secret code is preserved
|
||||
compressed_text = " ".join(result.chunks) if result.chunks else ""
|
||||
if SECRET_CODE in compressed_text:
|
||||
print(f" ✅ Secret code preserved in compressed output")
|
||||
else:
|
||||
print(f" ⚠️ Secret code may have been removed (query-focused compression)")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Note: Compression module not available: {e}")
|
||||
|
||||
# Create agent with knowledge
|
||||
print("\n" + "-" * 40)
|
||||
print("Agent Knowledge Retrieval Test")
|
||||
print("-" * 40)
|
||||
|
||||
agent = Agent(
|
||||
name="PolicyExpert",
|
||||
instructions="""You are a company policy expert.
|
||||
Answer questions based ONLY on the provided knowledge context.
|
||||
When asked about codes, provide the EXACT code from the documents.
|
||||
Be concise but accurate.""",
|
||||
knowledge=[temp_dir],
|
||||
user_id="compression_demo_user",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Test retrieval of the unique code
|
||||
print("\nQ: What is the master security verification code?")
|
||||
response = agent.chat("What is the master security verification code?")
|
||||
print(f"A: {response}")
|
||||
|
||||
if SECRET_CODE in response.upper():
|
||||
print(f"\n✅ VERIFIED: Agent correctly retrieved the secret code!")
|
||||
else:
|
||||
print(f"\n❌ WARNING: Expected code {SECRET_CODE} not found in response")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demo Complete")
|
||||
print("=" * 60)
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Knowledge Retrieval with Context-Required Questions
|
||||
|
||||
This example demonstrates:
|
||||
1. Agent answers questions that REQUIRE retrieved context (not general knowledge)
|
||||
2. Unique codes/secrets that cannot be guessed
|
||||
3. Verification that context is text, not file path
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python context_required_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_ctx_')
|
||||
|
||||
try:
|
||||
# Create test document with UNIQUE content that cannot be guessed
|
||||
doc = os.path.join(temp_dir, 'company_policy.txt')
|
||||
with open(doc, 'w') as f:
|
||||
f.write("""
|
||||
Acme Corporation Internal Policy Document
|
||||
==========================================
|
||||
|
||||
Remote Work Policy:
|
||||
- Employees may work from home up to 3 days per week
|
||||
- Manager approval is required for remote work
|
||||
- Manager approval code: ZEBRA-71
|
||||
- All requests must be submitted via the HR portal
|
||||
|
||||
Expense Reimbursement:
|
||||
- Maximum daily meal allowance: $47.50
|
||||
- Mileage rate: $0.655 per mile
|
||||
- Expense submission code: TIGER-42
|
||||
- Receipts required for amounts over $25
|
||||
|
||||
IT Security:
|
||||
- VPN access code prefix: FALCON-
|
||||
- Password reset hotline: 555-0199
|
||||
- Security incident code: HAWK-88
|
||||
|
||||
This document is confidential. Last updated: 2024-01-15.
|
||||
""")
|
||||
|
||||
print("=" * 60)
|
||||
print("Example: Context-Required Knowledge Retrieval")
|
||||
print("=" * 60)
|
||||
print(f"\nDocument location: {doc}")
|
||||
print("\nThis example asks questions that CANNOT be answered")
|
||||
print("without the retrieved context (unique codes/values).\n")
|
||||
|
||||
# Create agent with knowledge
|
||||
agent = Agent(
|
||||
name="PolicyExpert",
|
||||
instructions="""You are a company policy expert.
|
||||
Answer questions based ONLY on the provided knowledge context.
|
||||
If the answer is not in the context, say 'I don't have that information.'
|
||||
Be precise and quote exact values when asked about codes or numbers.""",
|
||||
knowledge=[temp_dir],
|
||||
user_id="demo_user",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Question 1: Unique code that requires context
|
||||
print("-" * 40)
|
||||
print("Question 1: What is the manager approval code for remote work?")
|
||||
print("-" * 40)
|
||||
response = agent.chat("What is the manager approval code for remote work?")
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
# Verify the answer contains the unique code
|
||||
if "ZEBRA-71" in response.upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the unique code from context!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
|
||||
# Question 2: Another unique value
|
||||
print("-" * 40)
|
||||
print("Question 2: What is the maximum daily meal allowance?")
|
||||
print("-" * 40)
|
||||
response = agent.chat("What is the maximum daily meal allowance?")
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
if "47.50" in response or "$47.50" in response:
|
||||
print("✅ VERIFIED: Agent correctly retrieved the specific amount!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the amount - check retrieval\n")
|
||||
|
||||
# Question 3: Security code
|
||||
print("-" * 40)
|
||||
print("Question 3: What is the security incident code?")
|
||||
print("-" * 40)
|
||||
response = agent.chat("What is the security incident code?")
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
if "HAWK-88" in response.upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the security code!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
|
||||
# Show context preview to prove it's text, not path
|
||||
print("=" * 60)
|
||||
print("Context Verification")
|
||||
print("=" * 60)
|
||||
context, _ = agent._get_knowledge_context("policy codes", use_rag=True)
|
||||
print(f"\nRetrieved context preview (first 300 chars):")
|
||||
print("-" * 40)
|
||||
print(context[:300] if context else "NO CONTEXT")
|
||||
print("-" * 40)
|
||||
|
||||
# Verify context is text, not path
|
||||
if temp_dir in context:
|
||||
print("\n❌ ERROR: Context contains file path instead of text!")
|
||||
else:
|
||||
print("\n✅ VERIFIED: Context contains actual text, not file path!")
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Hierarchical Summaries Demo
|
||||
|
||||
This example demonstrates:
|
||||
1. Building multi-level summaries (file → folder → project)
|
||||
2. Query routing to appropriate summary level
|
||||
3. Agent-centric knowledge retrieval with unique codes
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python hierarchical_summaries_demo.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
# Unique verification codes per department
|
||||
DEPT_CODES = {
|
||||
"engineering": "ENG-7X2K9",
|
||||
"marketing": "MKT-3M5N8",
|
||||
"finance": "FIN-9P4Q1",
|
||||
}
|
||||
|
||||
|
||||
def create_hierarchical_corpus(temp_dir: str) -> dict:
|
||||
"""Create a hierarchical corpus with departments and files."""
|
||||
structure = {}
|
||||
|
||||
for dept, code in DEPT_CODES.items():
|
||||
dept_dir = os.path.join(temp_dir, dept)
|
||||
os.makedirs(dept_dir, exist_ok=True)
|
||||
structure[dept] = []
|
||||
|
||||
# Create department overview
|
||||
overview_path = os.path.join(dept_dir, "overview.txt")
|
||||
with open(overview_path, 'w') as f:
|
||||
f.write(f"""
|
||||
{dept.upper()} DEPARTMENT OVERVIEW
|
||||
{'=' * 40}
|
||||
|
||||
Department: {dept.title()}
|
||||
Department Code: {code}
|
||||
|
||||
Mission Statement:
|
||||
The {dept.title()} department is responsible for driving excellence
|
||||
in our organization through innovative solutions and dedicated teamwork.
|
||||
|
||||
Key Responsibilities:
|
||||
- Strategic planning and execution
|
||||
- Cross-functional collaboration
|
||||
- Continuous improvement initiatives
|
||||
|
||||
Access Code: {code}
|
||||
Use this code for all department-related system access.
|
||||
|
||||
Last Updated: 2024-12-01
|
||||
""")
|
||||
structure[dept].append(overview_path)
|
||||
|
||||
# Create team files
|
||||
for i, team in enumerate(["alpha", "beta", "gamma"]):
|
||||
team_path = os.path.join(dept_dir, f"team_{team}.txt")
|
||||
with open(team_path, 'w') as f:
|
||||
f.write(f"""
|
||||
{dept.upper()} - TEAM {team.upper()}
|
||||
{'=' * 40}
|
||||
|
||||
Team: {team.title()}
|
||||
Department: {dept.title()}
|
||||
Team Size: {5 + i * 2} members
|
||||
|
||||
Current Projects:
|
||||
- Project {team.upper()}-001: In Progress
|
||||
- Project {team.upper()}-002: Planning
|
||||
- Project {team.upper()}-003: Completed
|
||||
|
||||
Team Lead: {team.title()} Lead
|
||||
Contact: {team}@acme.com
|
||||
|
||||
Note: For department access, use code: {code}
|
||||
""")
|
||||
structure[dept].append(team_path)
|
||||
|
||||
return structure
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_hierarchy_')
|
||||
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("Example: Hierarchical Summaries Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create hierarchical corpus
|
||||
structure = create_hierarchical_corpus(temp_dir)
|
||||
total_files = sum(len(files) for files in structure.values())
|
||||
print(f"\nCreated hierarchical corpus:")
|
||||
print(f" Departments: {len(structure)}")
|
||||
print(f" Total files: {total_files}")
|
||||
print(f" Location: {temp_dir}")
|
||||
|
||||
for dept, files in structure.items():
|
||||
print(f" - {dept}/: {len(files)} files (code: {DEPT_CODES[dept]})")
|
||||
|
||||
# Demonstrate hierarchical summarizer
|
||||
print("\n" + "-" * 40)
|
||||
print("Hierarchical Summarizer Demo")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.rag import HierarchicalSummarizer
|
||||
|
||||
# Collect all files
|
||||
all_files = []
|
||||
for files in structure.values():
|
||||
all_files.extend(files)
|
||||
|
||||
# Build hierarchy
|
||||
summarizer = HierarchicalSummarizer(max_levels=3)
|
||||
result = summarizer.build_hierarchy(all_files, base_path=temp_dir)
|
||||
|
||||
print(f"\nHierarchy built:")
|
||||
print(f" Levels: {summarizer.max_levels}")
|
||||
if hasattr(result, 'nodes'):
|
||||
print(f" Summary nodes: {len(result.nodes)}")
|
||||
|
||||
# Query at different levels
|
||||
print("\nQuery routing demonstration:")
|
||||
queries = [
|
||||
("What is the engineering department code?", "engineering"),
|
||||
("Tell me about marketing team alpha", "marketing"),
|
||||
("What are the finance department responsibilities?", "finance"),
|
||||
]
|
||||
|
||||
for query, expected_dept in queries:
|
||||
print(f"\n Query: {query}")
|
||||
# In a full implementation, this would route to the appropriate level
|
||||
print(f" Expected department: {expected_dept}")
|
||||
print(f" Expected code: {DEPT_CODES[expected_dept]}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Note: Hierarchical summarizer not available: {e}")
|
||||
except Exception as e:
|
||||
print(f"Note: Hierarchy building skipped: {e}")
|
||||
|
||||
# Create agent with knowledge
|
||||
print("\n" + "-" * 40)
|
||||
print("Agent Knowledge Retrieval Test")
|
||||
print("-" * 40)
|
||||
|
||||
agent = Agent(
|
||||
name="DepartmentExpert",
|
||||
instructions="""You are a department information expert.
|
||||
Answer questions based ONLY on the provided knowledge context.
|
||||
When asked about department codes, provide the EXACT code from the documents.
|
||||
Be specific about which department you're referring to.""",
|
||||
knowledge=[temp_dir],
|
||||
user_id="hierarchy_demo_user",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Test retrieval of department codes
|
||||
test_cases = [
|
||||
("What is the engineering department access code?", "ENG-7X2K9"),
|
||||
("What is the marketing department code?", "MKT-3M5N8"),
|
||||
("What is the finance department code?", "FIN-9P4Q1"),
|
||||
]
|
||||
|
||||
print("\nTesting retrieval of department codes:\n")
|
||||
|
||||
for question, expected_code in test_cases:
|
||||
print(f"Q: {question}")
|
||||
response = agent.chat(question)
|
||||
print(f"A: {response[:200]}...")
|
||||
|
||||
if expected_code in response.upper():
|
||||
print(f"✅ VERIFIED: Found code {expected_code}\n")
|
||||
else:
|
||||
print(f"❌ WARNING: Expected code {expected_code} not found\n")
|
||||
|
||||
print("=" * 60)
|
||||
print("Demo Complete")
|
||||
print("=" * 60)
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Large Corpus Strategy Selection Demo
|
||||
|
||||
This example demonstrates:
|
||||
1. Automatic strategy selection based on corpus size
|
||||
2. Different retrieval strategies (DIRECT, BASIC, HYBRID, RERANKED, COMPRESSED)
|
||||
3. Agent-centric knowledge retrieval with unique codes
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python large_corpus_strategy_demo.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
# Unique verification codes that CANNOT be guessed
|
||||
VERIFICATION_CODES = {
|
||||
"project_alpha": "ALPHA-7X9K2",
|
||||
"project_beta": "BETA-3M8N1",
|
||||
"project_gamma": "GAMMA-5P2Q7",
|
||||
"budget_code": "BUDGET-9R4T6",
|
||||
"security_clearance": "CLEARANCE-2W8Y3",
|
||||
}
|
||||
|
||||
|
||||
def create_test_corpus(temp_dir: str, num_files: int = 10) -> list:
|
||||
"""Create a test corpus with unique codes in each file."""
|
||||
files = []
|
||||
|
||||
# Create main project files with unique codes
|
||||
for i, (project, code) in enumerate(VERIFICATION_CODES.items()):
|
||||
filepath = os.path.join(temp_dir, f"{project}_doc.txt")
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(f"""
|
||||
{project.upper().replace('_', ' ')} Documentation
|
||||
{'=' * 50}
|
||||
|
||||
Project Overview:
|
||||
This document contains confidential information about {project}.
|
||||
The verification code for this project is: {code}
|
||||
|
||||
Key Details:
|
||||
- Project started: 2024-0{i+1}-15
|
||||
- Team size: {10 + i * 5} members
|
||||
- Status: Active
|
||||
- Priority: {'High' if i < 2 else 'Medium'}
|
||||
|
||||
Access Requirements:
|
||||
To access this project, use verification code: {code}
|
||||
All access attempts are logged for security purposes.
|
||||
|
||||
Last updated: 2024-12-01
|
||||
""")
|
||||
files.append(filepath)
|
||||
|
||||
# Create additional filler files to simulate larger corpus
|
||||
for i in range(num_files - len(VERIFICATION_CODES)):
|
||||
filepath = os.path.join(temp_dir, f"general_doc_{i}.txt")
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(f"""
|
||||
General Documentation File {i}
|
||||
{'=' * 40}
|
||||
|
||||
This is a general documentation file containing various information
|
||||
about company processes and procedures.
|
||||
|
||||
Section {i}.1: Overview
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
|
||||
Section {i}.2: Procedures
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse.
|
||||
|
||||
Section {i}.3: Guidelines
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa.
|
||||
Qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
Document ID: DOC-{1000 + i}
|
||||
""")
|
||||
files.append(filepath)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_strategy_')
|
||||
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("Example: Large Corpus Strategy Selection Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create test corpus
|
||||
files = create_test_corpus(temp_dir, num_files=15)
|
||||
print(f"\nCreated {len(files)} test documents in: {temp_dir}")
|
||||
print(f"Unique verification codes embedded: {len(VERIFICATION_CODES)}")
|
||||
|
||||
# Show strategy selection based on corpus size
|
||||
print("\n" + "-" * 40)
|
||||
print("Strategy Selection Demo")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.rag import select_strategy, RetrievalStrategy
|
||||
from praisonaiagents.knowledge.indexing import CorpusStats
|
||||
|
||||
# Get corpus stats
|
||||
stats = CorpusStats.from_directory(temp_dir)
|
||||
print(f"\nCorpus Statistics:")
|
||||
print(f" Files: {stats.file_count}")
|
||||
print(f" Estimated tokens: {stats.total_tokens}")
|
||||
print(f" Recommended strategy: {stats.strategy_recommendation}")
|
||||
|
||||
# Show strategy selection for different corpus sizes
|
||||
print(f"\nStrategy selection by corpus size:")
|
||||
for size in [100, 1000, 10000, 50000, 100000]:
|
||||
strategy = select_strategy(corpus_tokens=size)
|
||||
print(f" {size:>6} tokens -> {strategy.value}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Note: Strategy module not available: {e}")
|
||||
|
||||
# Create agent with knowledge
|
||||
print("\n" + "-" * 40)
|
||||
print("Agent Knowledge Retrieval Test")
|
||||
print("-" * 40)
|
||||
|
||||
agent = Agent(
|
||||
name="ProjectExpert",
|
||||
instructions="""You are a project documentation expert.
|
||||
Answer questions based ONLY on the provided knowledge context.
|
||||
When asked about verification codes, provide the EXACT code from the documents.
|
||||
If information is not in the context, say 'Information not found in documents.'""",
|
||||
knowledge=[temp_dir],
|
||||
user_id="strategy_demo_user",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Test retrieval with unique codes
|
||||
test_questions = [
|
||||
("What is the verification code for Project Alpha?", "ALPHA-7X9K2"),
|
||||
("What is the budget code?", "BUDGET-9R4T6"),
|
||||
("What is the security clearance code?", "CLEARANCE-2W8Y3"),
|
||||
]
|
||||
|
||||
print("\nTesting retrieval of unique codes:\n")
|
||||
|
||||
for question, expected_code in test_questions:
|
||||
print(f"Q: {question}")
|
||||
response = agent.chat(question)
|
||||
print(f"A: {response[:200]}...")
|
||||
|
||||
if expected_code in response.upper():
|
||||
print(f"✅ VERIFIED: Found code {expected_code}\n")
|
||||
else:
|
||||
print(f"❌ WARNING: Expected code {expected_code} not found\n")
|
||||
|
||||
print("=" * 60)
|
||||
print("Demo Complete")
|
||||
print("=" * 60)
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Using mem0 Backend with Agent Retrieval
|
||||
|
||||
This example demonstrates:
|
||||
1. Agent with knowledge using mem0 backend (default)
|
||||
2. Proper scope identifiers (user_id/agent_id)
|
||||
3. Handling of metadata normalization (mem0 returns metadata=None)
|
||||
4. Agent-first retrieval API
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python mem0_backend_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def main():
|
||||
# Create temporary directory with test documents
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_example_')
|
||||
|
||||
try:
|
||||
# Create test documents with UNIQUE content that requires retrieval
|
||||
doc1 = os.path.join(temp_dir, 'acme_policy.txt')
|
||||
with open(doc1, 'w') as f:
|
||||
f.write("""
|
||||
Acme Corp Travel Policy Document
|
||||
|
||||
Flight booking code: FALCON-77
|
||||
Hotel reservation prefix: HTL-
|
||||
Maximum daily meal allowance: $52.75
|
||||
Mileage reimbursement rate: $0.67 per mile
|
||||
Travel approval required for trips over: $500
|
||||
Emergency travel hotline: 555-ACME-911
|
||||
""")
|
||||
|
||||
doc2 = os.path.join(temp_dir, 'project_info.txt')
|
||||
with open(doc2, 'w') as f:
|
||||
f.write("""
|
||||
Project Phoenix Status Report
|
||||
|
||||
Project code: PHOENIX-2024
|
||||
Lead engineer: Dr. Sarah Chen
|
||||
Budget allocation: $2.4 million
|
||||
Target completion: Q3 2025
|
||||
Security clearance required: Level 3
|
||||
Weekly standup: Tuesdays at 10am PST
|
||||
""")
|
||||
|
||||
print("=" * 60)
|
||||
print("Example: Agent with Knowledge (mem0 backend)")
|
||||
print("=" * 60)
|
||||
|
||||
# Create agent with knowledge
|
||||
# mem0 is the default backend - it requires scope identifiers
|
||||
agent = Agent(
|
||||
name="PolicyExpert",
|
||||
instructions="You are a company policy expert. Answer questions based ONLY on the provided knowledge. Quote exact values when asked about codes or numbers.",
|
||||
knowledge=[temp_dir], # Add directory with documents
|
||||
user_id="example_user", # Required for mem0 backend
|
||||
)
|
||||
|
||||
print("\n1. Testing Agent.chat() with context-required question:")
|
||||
print("-" * 40)
|
||||
|
||||
# Question that REQUIRES the retrieved context (unique code)
|
||||
response = agent.chat("What is the flight booking code?")
|
||||
print("Q: What is the flight booking code?")
|
||||
print(f"A: {response}")
|
||||
|
||||
# Verify the answer contains the unique code
|
||||
if "FALCON-77" in response.upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the unique code!")
|
||||
|
||||
print("\n2. Testing Agent.chat() with another context-required question:")
|
||||
print("-" * 40)
|
||||
|
||||
response = agent.chat("What is the maximum daily meal allowance?")
|
||||
print("Q: What is the maximum daily meal allowance?")
|
||||
print(f"A: {response}")
|
||||
|
||||
if "52.75" in response:
|
||||
print("✅ VERIFIED: Agent correctly retrieved the specific amount!")
|
||||
|
||||
print("\n3. Testing Agent.retrieve() for context-only retrieval:")
|
||||
print("-" * 40)
|
||||
|
||||
# retrieve() returns context without LLM generation
|
||||
try:
|
||||
context_pack = agent.retrieve("Project Phoenix")
|
||||
print("Query: Project Phoenix")
|
||||
print(f"Context preview: {context_pack.context[:200]}...")
|
||||
print(f"Citations: {len(context_pack.citations)} found")
|
||||
except Exception as e:
|
||||
print(f"Note: retrieve() requires RAG module: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Using Scope Identifiers (user_id, agent_id, run_id)
|
||||
|
||||
This example demonstrates:
|
||||
1. Multi-user knowledge isolation with user_id
|
||||
2. Multi-agent knowledge sharing with agent_id
|
||||
3. Run-specific context with run_id
|
||||
4. Error handling for missing scope identifiers
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python scope_identifiers_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_scope_')
|
||||
|
||||
try:
|
||||
# Create test document
|
||||
doc = os.path.join(temp_dir, 'shared_knowledge.txt')
|
||||
with open(doc, 'w') as f:
|
||||
f.write("""
|
||||
Company Policy Document
|
||||
|
||||
1. All employees must complete security training annually.
|
||||
2. Remote work is allowed with manager approval.
|
||||
3. Expense reports must be submitted within 30 days.
|
||||
""")
|
||||
|
||||
print("=" * 60)
|
||||
print("Example: Scope Identifiers for Knowledge Isolation")
|
||||
print("=" * 60)
|
||||
|
||||
# Example 1: User-scoped knowledge
|
||||
print("\n1. User-scoped knowledge (user_id):")
|
||||
print("-" * 40)
|
||||
|
||||
user1_agent = Agent(
|
||||
name="HR_Assistant_User1",
|
||||
instructions="You are an HR assistant.",
|
||||
knowledge=[temp_dir],
|
||||
user_id="user_alice", # Knowledge scoped to Alice
|
||||
)
|
||||
|
||||
response = user1_agent.chat("What is the policy on remote work?")
|
||||
print("User Alice asks: What is the policy on remote work?")
|
||||
print(f"Response: {response[:200]}...")
|
||||
|
||||
# Example 2: Agent-scoped knowledge
|
||||
print("\n2. Agent-scoped knowledge (agent_id):")
|
||||
print("-" * 40)
|
||||
|
||||
shared_agent = Agent(
|
||||
name="Policy_Expert",
|
||||
instructions="You are a policy expert.",
|
||||
knowledge=[temp_dir],
|
||||
agent_id="policy_agent_v1", # Knowledge scoped to this agent
|
||||
)
|
||||
|
||||
response = shared_agent.chat("What are the expense report rules?")
|
||||
print("Agent policy_agent_v1 query: What are the expense report rules?")
|
||||
print(f"Response: {response[:200]}...")
|
||||
|
||||
# Example 3: Combined scoping
|
||||
print("\n3. Combined scoping (user_id + agent_id):")
|
||||
print("-" * 40)
|
||||
|
||||
combined_agent = Agent(
|
||||
name="Personal_HR_Bot",
|
||||
instructions="You are a personal HR assistant.",
|
||||
knowledge=[temp_dir],
|
||||
user_id="user_bob",
|
||||
agent_id="hr_bot_v2",
|
||||
)
|
||||
|
||||
response = combined_agent.chat("What training is required?")
|
||||
print("User Bob with hr_bot_v2: What training is required?")
|
||||
print(f"Response: {response[:200]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Scope Identifier Summary:")
|
||||
print("=" * 60)
|
||||
print("""
|
||||
- user_id: Isolates knowledge per user (multi-tenant)
|
||||
- agent_id: Isolates knowledge per agent type/version
|
||||
- run_id: Isolates knowledge per execution run (ephemeral)
|
||||
|
||||
Best Practices:
|
||||
1. Always provide at least one scope identifier
|
||||
2. Use user_id for user-specific data
|
||||
3. Use agent_id for shared agent knowledge
|
||||
4. Use run_id for temporary/session-specific context
|
||||
""")
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Multi-Agent Scope Isolation
|
||||
|
||||
This example demonstrates:
|
||||
1. Agent A ingests knowledge under user_id=U1
|
||||
2. Agent B queries under user_id=U2 - should NOT find the knowledge
|
||||
3. Agent B queries under user_id=U1 - should find the knowledge
|
||||
4. Proves scope isolation works correctly
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents[knowledge]
|
||||
- OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
python scope_isolation_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from praisonaiagents import Agent, Knowledge
|
||||
|
||||
|
||||
def main():
|
||||
temp_dir = tempfile.mkdtemp(prefix='praison_scope_')
|
||||
|
||||
try:
|
||||
# Create test document with UNIQUE secret
|
||||
doc = os.path.join(temp_dir, 'secret.txt')
|
||||
with open(doc, 'w') as f:
|
||||
f.write("""
|
||||
Project Codename: PHOENIX
|
||||
Secret access code: WRENCH-992
|
||||
Classification: Top Secret
|
||||
Only authorized personnel with code WRENCH-992 may access this project.
|
||||
""")
|
||||
|
||||
print("=" * 60)
|
||||
print("Example: Multi-Agent Scope Isolation")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 1: Agent A ingests knowledge under user_id=U1
|
||||
print("\n1. Agent A ingests knowledge under user_id='user_alice'")
|
||||
print("-" * 40)
|
||||
|
||||
agent_a = Agent(
|
||||
name="AgentA_Ingester",
|
||||
instructions="You ingest and store knowledge.",
|
||||
knowledge=[temp_dir],
|
||||
user_id="user_alice", # Scoped to Alice
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Ensure knowledge is processed
|
||||
agent_a._ensure_knowledge_processed()
|
||||
print(" Knowledge ingested by Agent A under user_alice scope")
|
||||
|
||||
# Step 2: Agent B queries under user_id=U2 - should NOT find
|
||||
print("\n2. Agent B queries under user_id='user_bob' (different scope)")
|
||||
print("-" * 40)
|
||||
|
||||
# Create a separate Knowledge instance for Agent B with different scope
|
||||
knowledge_b = Knowledge()
|
||||
search_result = knowledge_b.search("WRENCH-992", user_id="user_bob")
|
||||
|
||||
results = search_result.get('results', []) if isinstance(search_result, dict) else []
|
||||
|
||||
if len(results) == 0:
|
||||
print(" ✅ CORRECT: Agent B (user_bob) found NO results")
|
||||
print(" Scope isolation is working!")
|
||||
else:
|
||||
# Check if any result actually contains the secret
|
||||
found_secret = any('wrench-992' in str(r.get('memory', '')).lower() for r in results)
|
||||
if found_secret:
|
||||
print(" ❌ ERROR: Agent B found the secret - scope isolation failed!")
|
||||
else:
|
||||
print(" ✅ CORRECT: Agent B found no matching content")
|
||||
|
||||
# Step 3: Agent B queries under user_id=U1 - should find
|
||||
print("\n3. Agent B queries under user_id='user_alice' (same scope)")
|
||||
print("-" * 40)
|
||||
|
||||
search_result = knowledge_b.search("WRENCH-992", user_id="user_alice")
|
||||
results = search_result.get('results', []) if isinstance(search_result, dict) else []
|
||||
|
||||
if len(results) > 0:
|
||||
memory = results[0].get('memory', '')
|
||||
if 'wrench-992' in memory.lower():
|
||||
print(" ✅ CORRECT: Agent B (as user_alice) found the secret!")
|
||||
print(f" Retrieved: {memory[:80]}...")
|
||||
else:
|
||||
print(f" ⚠️ Found results but no secret: {memory[:80]}...")
|
||||
else:
|
||||
print(" ❌ ERROR: Agent B should have found results with user_alice scope")
|
||||
|
||||
# Step 4: Full Agent chat test with scope
|
||||
print("\n4. Full Agent chat test with correct scope")
|
||||
print("-" * 40)
|
||||
|
||||
agent_b = Agent(
|
||||
name="AgentB_Querier",
|
||||
instructions="Answer questions based on the provided knowledge. If you don't have the information, say so.",
|
||||
knowledge=[temp_dir],
|
||||
user_id="user_alice", # Same scope as Agent A
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
response = agent_b.chat("What is the secret access code for Project PHOENIX?")
|
||||
print(f" Question: What is the secret access code?")
|
||||
print(f" Answer: {response}")
|
||||
|
||||
if "WRENCH-992" in response.upper():
|
||||
print("\n ✅ VERIFIED: Agent correctly retrieved the secret code!")
|
||||
else:
|
||||
print("\n ⚠️ Agent may not have found the code")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Summary: Scope Isolation")
|
||||
print("=" * 60)
|
||||
print("""
|
||||
- user_id='user_alice': Can access knowledge ingested by user_alice
|
||||
- user_id='user_bob': Cannot access knowledge ingested by user_alice
|
||||
- This enables multi-tenant knowledge isolation
|
||||
""")
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,9 +2,9 @@
|
||||
"""
|
||||
Basic Policy Engine Example for PraisonAI Agents.
|
||||
|
||||
This example demonstrates how to use the policy engine to:
|
||||
1. Create policies with rules
|
||||
2. Check if actions are allowed
|
||||
This example demonstrates how to use the policy engine with Agent:
|
||||
1. Create an Agent with policy enforcement
|
||||
2. Define policies with rules
|
||||
3. Block dangerous operations
|
||||
4. Use convenience policy creators
|
||||
|
||||
@@ -12,126 +12,85 @@ Usage:
|
||||
python basic_policy.py
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.policy import (
|
||||
PolicyEngine, Policy, PolicyRule, PolicyAction, PolicyConfig,
|
||||
PolicyEngine, Policy, PolicyRule, PolicyAction,
|
||||
create_deny_tools_policy, create_read_only_policy
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Policy Engine Demo")
|
||||
print("Agent-Centric Policy Engine Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a policy engine
|
||||
# Create a policy engine with rules
|
||||
engine = PolicyEngine()
|
||||
|
||||
# ==========================================================================
|
||||
# Policy 1: Block dangerous tools
|
||||
# ==========================================================================
|
||||
# Add policy to block dangerous tools
|
||||
print("\n--- Creating Policies ---")
|
||||
|
||||
dangerous_tools_policy = Policy(
|
||||
engine.add_policy(Policy(
|
||||
name="block_dangerous",
|
||||
description="Block dangerous file operations",
|
||||
rules=[
|
||||
PolicyRule(
|
||||
action=PolicyAction.DENY,
|
||||
resource="tool:delete_*",
|
||||
reason="Delete operations are blocked",
|
||||
name="deny_delete"
|
||||
reason="Delete operations are blocked"
|
||||
),
|
||||
PolicyRule(
|
||||
action=PolicyAction.DENY,
|
||||
resource="tool:rm_*",
|
||||
reason="Remove operations are blocked",
|
||||
name="deny_rm"
|
||||
reason="Remove operations are blocked"
|
||||
),
|
||||
],
|
||||
priority=100
|
||||
)
|
||||
engine.add_policy(dangerous_tools_policy)
|
||||
print(f"✅ Added policy: {dangerous_tools_policy.name}")
|
||||
))
|
||||
print("✅ Added policy: block_dangerous")
|
||||
|
||||
# ==========================================================================
|
||||
# Policy 2: Allow read operations
|
||||
# ==========================================================================
|
||||
read_policy = Policy(
|
||||
name="allow_read",
|
||||
description="Allow read operations",
|
||||
rules=[
|
||||
PolicyRule(
|
||||
action=PolicyAction.ALLOW,
|
||||
resource="tool:read_*",
|
||||
name="allow_read"
|
||||
),
|
||||
PolicyRule(
|
||||
action=PolicyAction.ALLOW,
|
||||
resource="tool:list_*",
|
||||
name="allow_list"
|
||||
),
|
||||
]
|
||||
)
|
||||
engine.add_policy(read_policy)
|
||||
print(f"✅ Added policy: {read_policy.name}")
|
||||
# Add read-only policy using convenience function
|
||||
engine.add_policy(create_read_only_policy())
|
||||
print("✅ Added policy: read_only")
|
||||
|
||||
# ==========================================================================
|
||||
# Test policy checks
|
||||
# ==========================================================================
|
||||
# Create an Agent with policy enforcement
|
||||
agent = Agent(
|
||||
name="SecureAgent",
|
||||
instructions="You are a file management assistant.",
|
||||
policy=engine
|
||||
)
|
||||
|
||||
print("\n--- Agent with Policy Enforcement Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Policy engine: {agent.policy is not None}")
|
||||
|
||||
# Test policy checks via agent's policy
|
||||
print("\n--- Testing Policy Checks ---")
|
||||
|
||||
test_cases = [
|
||||
("tool:read_file", "Reading a file"),
|
||||
("tool:list_directory", "Listing directory"),
|
||||
("tool:delete_file", "Deleting a file"),
|
||||
("tool:rm_directory", "Removing directory"),
|
||||
("tool:write_file", "Writing a file"),
|
||||
]
|
||||
|
||||
for resource, description in test_cases:
|
||||
result = engine.check(resource, {})
|
||||
result = agent.policy.check(resource, {})
|
||||
status = "✅ ALLOWED" if result.allowed else "🚫 DENIED"
|
||||
reason = f" ({result.reason})" if result.reason else ""
|
||||
print(f" {status}: {description}{reason}")
|
||||
|
||||
# ==========================================================================
|
||||
# Using convenience functions
|
||||
# ==========================================================================
|
||||
print("\n--- Using Convenience Functions ---")
|
||||
print("\n--- Convenience Policy Creators ---")
|
||||
|
||||
# Create a read-only policy
|
||||
read_only = create_read_only_policy()
|
||||
print(f"✅ Created read-only policy with {len(read_only.rules)} rules")
|
||||
|
||||
# Create a deny tools policy
|
||||
deny_tools = create_deny_tools_policy(
|
||||
["execute_*", "shell_*", "system_*"],
|
||||
reason="System commands are blocked"
|
||||
)
|
||||
print(f"✅ Created deny-tools policy with {len(deny_tools.rules)} rules")
|
||||
print(f"✅ create_deny_tools_policy: {len(deny_tools.rules)} rules")
|
||||
|
||||
# ==========================================================================
|
||||
# Strict mode example
|
||||
# ==========================================================================
|
||||
print("\n--- Strict Mode Example ---")
|
||||
|
||||
strict_engine = PolicyEngine(PolicyConfig(strict_mode=True))
|
||||
|
||||
# In strict mode, unknown resources are denied
|
||||
result = strict_engine.check("tool:unknown_operation", {})
|
||||
print(f" Unknown operation in strict mode: {'DENIED' if not result.allowed else 'ALLOWED'}")
|
||||
|
||||
# ==========================================================================
|
||||
# Policy serialization
|
||||
# ==========================================================================
|
||||
print("\n--- Policy Serialization ---")
|
||||
|
||||
policy_dict = dangerous_tools_policy.to_dict()
|
||||
print(f" Serialized policy: {policy_dict['name']} with {len(policy_dict['rules'])} rules")
|
||||
|
||||
# Recreate from dict
|
||||
restored = Policy.from_dict(policy_dict)
|
||||
print(f" Restored policy: {restored.name}")
|
||||
read_only = create_read_only_policy()
|
||||
print(f"✅ create_read_only_policy: {len(read_only.rules)} rules")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demo Complete!")
|
||||
|
||||
@@ -32,9 +32,9 @@ code_writer = Agent(
|
||||
goal="Write clean, efficient, and well-documented Python code",
|
||||
backstory="You are a senior Python developer with 10+ years of experience in writing production-quality code.",
|
||||
instructions="Write Python code that follows PEP 8 standards, includes proper error handling, and has clear documentation.",
|
||||
self_reflect=True,
|
||||
min_reflect=2,
|
||||
max_reflect=5
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
code_reviewer = Agent(
|
||||
@@ -108,9 +108,9 @@ general_code_agent = Agent(
|
||||
4. Refactor code for better performance
|
||||
5. Add tests and documentation""",
|
||||
tools=[code_interpreter, analyze_code_complexity],
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=3
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -14,7 +14,7 @@ image_agent = Agent(
|
||||
backstory="""You are an expert in computer vision and image analysis.
|
||||
You excel at describing images, detecting objects, and understanding visual content.""",
|
||||
llm="gpt-5-nano",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# 1. Task with Image URL
|
||||
|
||||
@@ -14,7 +14,7 @@ image_agent = Agent(
|
||||
backstory="""You are an expert in computer vision and image analysis.
|
||||
You excel at describing images, detecting objects, and understanding visual content.""",
|
||||
llm="gpt-5-nano",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# 1. Task with Image URL
|
||||
|
||||
@@ -65,9 +65,9 @@ calculation_agent = Agent(
|
||||
backstory="You are an expert mathematician who can handle complex calculations with precision.",
|
||||
instructions="Perform the requested calculations step by step, showing your work clearly.",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
problem_solver_agent = Agent(
|
||||
@@ -81,9 +81,9 @@ problem_solver_agent = Agent(
|
||||
3. Determine what needs to be found
|
||||
4. Show each step of the solution
|
||||
5. Verify the answer makes sense""",
|
||||
self_reflect=True,
|
||||
min_reflect=2,
|
||||
max_reflect=4
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
math_tutor_agent = Agent(
|
||||
@@ -148,9 +148,9 @@ math_assistant = Agent(
|
||||
|
||||
Always show your work step by step.""",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=3
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from praisonaiagents import Agent, Tools
|
||||
from praisonaiagents import Agent, Tools, ReflectionConfig
|
||||
from praisonaiagents.tools import execute_code, analyze_code, format_code, lint_code, disassemble_code # Code Tools
|
||||
from praisonaiagents.tools import execute_command, list_processes, kill_process, get_system_info # Shell Tools
|
||||
from praisonaiagents.tools import duckduckgo # Web Search Tool
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a Programming Agent", self_reflect=True, min_reflect=5, max_reflect=10,
|
||||
instructions="You are a Programming Agent",
|
||||
reflection=ReflectionConfig(min_iterations=5, max_iterations=10),
|
||||
tools=[execute_code, analyze_code, format_code, lint_code, disassemble_code, execute_command, list_processes, kill_process, get_system_info, duckduckgo]
|
||||
)
|
||||
agent.start(
|
||||
|
||||
@@ -8,7 +8,7 @@ video_agent = Agent(
|
||||
backstory="""You are an expert in computer vision and image analysis.
|
||||
You excel at describing images, detecting objects, and understanding visual content.""",
|
||||
llm="gpt-5-nano",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Task with Video File
|
||||
|
||||
@@ -4,9 +4,9 @@ from praisonaiagents.tools import wiki_search, wiki_summary, wiki_page, wiki_ran
|
||||
agent = Agent(
|
||||
instructions="You are a Wikipedia Agent",
|
||||
tools=[wiki_search, wiki_summary, wiki_page, wiki_random, wiki_language],
|
||||
self_reflect=True,
|
||||
min_reflect=3,
|
||||
max_reflect=5,
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
agent.start(
|
||||
"What is the history of AI?"
|
||||
|
||||
@@ -27,9 +27,9 @@ pdf_agent = Agent(
|
||||
instructions="Read the provided PDF document and answer questions about its content. Be specific and cite relevant sections when possible.",
|
||||
knowledge=["document.pdf"], # Replace with your PDF file path
|
||||
knowledge_config=config,
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=3
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
# Example usage
|
||||
|
||||
@@ -49,9 +49,9 @@ cot_agent = Agent(
|
||||
6. Assess your confidence level
|
||||
|
||||
Always make your thinking process explicit and transparent.""",
|
||||
self_reflect=True,
|
||||
min_reflect=2,
|
||||
max_reflect=3
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
# Example 2: Reasoning Extractor Agent
|
||||
@@ -224,7 +224,7 @@ pattern_reasoning_agent = Agent(
|
||||
3. Counterfactual: Explore what-if scenarios
|
||||
4. Systems thinking: Consider interconnections
|
||||
5. Probabilistic: Assess likelihoods""",
|
||||
self_reflect=True
|
||||
reflection=True
|
||||
)
|
||||
|
||||
# Example 8: Interactive Reasoning Extraction
|
||||
|
||||
@@ -208,7 +208,7 @@ dynamic_router = Agent(
|
||||
5. Current system load
|
||||
|
||||
Provide clear routing decisions with reasoning.""",
|
||||
self_reflect=True
|
||||
reflection=True
|
||||
)
|
||||
|
||||
def dynamic_routing_example():
|
||||
|
||||
@@ -24,9 +24,9 @@ basic_reflection_agent = Agent(
|
||||
3. Identify areas for improvement
|
||||
4. Revise and enhance
|
||||
5. Repeat until satisfied""",
|
||||
self_reflect=True, # Enable self-reflection
|
||||
min_reflect=1, # Minimum 1 reflection iteration
|
||||
max_reflect=3 # Maximum 3 reflection iterations
|
||||
reflection=True, # Enable self-reflection
|
||||
# Minimum 1 reflection iteration
|
||||
# Maximum 3 reflection iterations
|
||||
)
|
||||
|
||||
# Example 2: Advanced Reflection with Different Strategies
|
||||
@@ -41,9 +41,9 @@ quality_focused_agent = Agent(
|
||||
- Clarity of presentation
|
||||
- Logical flow
|
||||
- Supporting evidence""",
|
||||
self_reflect=True,
|
||||
min_reflect=3, # Higher minimum for quality
|
||||
max_reflect=5 # Allow more iterations
|
||||
reflection=True,
|
||||
# Higher minimum for quality
|
||||
# Allow more iterations
|
||||
)
|
||||
|
||||
efficiency_balanced_agent = Agent(
|
||||
@@ -52,9 +52,9 @@ efficiency_balanced_agent = Agent(
|
||||
goal="Achieve good quality with reasonable reflection time",
|
||||
backstory="You balance quality with efficiency, knowing when good enough is sufficient.",
|
||||
instructions="Create good content with 1-2 rounds of improvement.",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2 # Limit iterations for efficiency
|
||||
reflection=True,
|
||||
|
||||
# Limit iterations for efficiency
|
||||
)
|
||||
|
||||
# Example 3: No Reflection for Comparison
|
||||
@@ -64,7 +64,7 @@ no_reflection_agent = Agent(
|
||||
goal="Provide immediate responses without reflection",
|
||||
backstory="You provide quick, first-draft responses.",
|
||||
instructions="Respond immediately with your first thought.",
|
||||
self_reflect=False # Disable reflection
|
||||
reflection=False # Disable reflection
|
||||
)
|
||||
|
||||
# Demonstration function to show reflection in action
|
||||
@@ -113,9 +113,9 @@ def reflection_workflow_example():
|
||||
role="Initial content creator",
|
||||
goal="Create first draft with self-improvement",
|
||||
backstory="You create initial drafts and refine them through reflection.",
|
||||
self_reflect=True,
|
||||
min_reflect=2,
|
||||
max_reflect=3
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
# Reviewer without reflection (provides feedback)
|
||||
@@ -124,7 +124,7 @@ def reflection_workflow_example():
|
||||
role="Content reviewer",
|
||||
goal="Provide constructive feedback",
|
||||
backstory="You review content and provide specific improvement suggestions.",
|
||||
self_reflect=False # No reflection needed for reviewing
|
||||
reflection=False # No reflection needed for reviewing
|
||||
)
|
||||
|
||||
# Final polisher with reflection
|
||||
@@ -133,9 +133,9 @@ def reflection_workflow_example():
|
||||
role="Final content polisher",
|
||||
goal="Perfect the content based on feedback",
|
||||
backstory="You take feedback and create the final, polished version.",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
# Workflow tasks
|
||||
@@ -202,9 +202,9 @@ custom_reflection_agent = Agent(
|
||||
backstory="You adapt your reflection process based on content quality.",
|
||||
instructions="""Create content and reflect on it.
|
||||
Continue improving until quality standards are met or maximum iterations reached.""",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=5
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
# Example 6: Reflection Impact Analysis
|
||||
|
||||
@@ -17,10 +17,10 @@ reflection_agent = Agent(
|
||||
goal="Demonstrate self-reflection and iterative improvement patterns",
|
||||
backstory="Expert researcher with strong self-reflection and continuous improvement capabilities",
|
||||
tools=[internet_search],
|
||||
self_reflect=True,
|
||||
min_reflect=2,
|
||||
max_reflect=4,
|
||||
reflect_llm="gpt-5-nano",
|
||||
reflection=True,
|
||||
|
||||
|
||||
|
||||
verbose=True
|
||||
)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ sync_agent = Agent(
|
||||
goal="Perform synchronous searches and return structured results",
|
||||
backstory="Expert in sync operations and data organization",
|
||||
tools=[sync_search_tool],
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
@@ -109,7 +109,7 @@ async_agent = Agent(
|
||||
goal="Perform asynchronous searches and return structured results",
|
||||
backstory="Expert in async operations and data organization",
|
||||
tools=[async_search_tool],
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ async_agent = Agent(
|
||||
goal="Perform fast parallel searches with structured results",
|
||||
backstory="Expert in efficient data retrieval and parallel search operations",
|
||||
tools=[async_search_tool],
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
@@ -63,7 +63,7 @@ summary_agent = Agent(
|
||||
role="Research Synthesizer",
|
||||
goal="Create concise summaries from multiple search results",
|
||||
backstory="Expert in analyzing and synthesizing information from multiple sources",
|
||||
self_reflect=True,
|
||||
reflection=True,
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ async_agent = Agent(
|
||||
goal="Perform fast and efficient asynchronous searches with structured results",
|
||||
backstory="Expert in parallel search operations and data retrieval",
|
||||
tools=[async_search_tool],
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
@@ -72,7 +72,7 @@ summary_agent = Agent(
|
||||
backstory="""Expert in analyzing and synthesizing information from multiple sources.
|
||||
Skilled at identifying patterns, trends, and connections between different topics.
|
||||
Specializes in creating clear, structured summaries that highlight key insights.""",
|
||||
self_reflect=True, # Enable self-reflection for better summary quality
|
||||
reflection=True, # Enable self-reflection for better summary quality
|
||||
verbose=True,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
@@ -25,13 +25,13 @@ code_agent = Agent(
|
||||
name="code_agent",
|
||||
llm="gpt-5-nano",
|
||||
backstory="Expert in writing Python scripts",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
execution_agent = Agent(
|
||||
name="execution_agent",
|
||||
llm="gpt-5-nano",
|
||||
backstory="Expert in executing Python scripts",
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
tools=[code_interpreter]
|
||||
)
|
||||
|
||||
|
||||
@@ -129,9 +129,9 @@ def main():
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
reflect_llm="gpt-5-nano",
|
||||
min_reflect=2,
|
||||
max_reflect=4
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
|
||||
@@ -112,7 +112,7 @@ data_agent = Agent(
|
||||
goal="Perform internet searches to collect relevant information.",
|
||||
backstory="Expert in finding and organizing internet data from multiple sources.",
|
||||
tools=[internet_search_tool, youtube_search_tool, wikipedia_search_tool],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# 3. Tasks
|
||||
|
||||
@@ -41,9 +41,9 @@ researcher = Agent(
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
reflect_llm="gpt-5-nano",
|
||||
min_reflect=2,
|
||||
max_reflect=4
|
||||
|
||||
|
||||
|
||||
)
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
|
||||
@@ -9,7 +9,7 @@ agent = Agent(
|
||||
goal="Search for information from multiple sources",
|
||||
backstory="I am an AI assistant that can search YouTube and Wikipedia.",
|
||||
tools=[YouTubeSearchTool, WikipediaAPIWrapper],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Create tasks to demonstrate both tools
|
||||
|
||||
@@ -8,7 +8,7 @@ vision_agent = Agent(
|
||||
backstory="""You are an expert in computer vision and image analysis.
|
||||
You excel at describing images, detecting objects, and understanding visual content.""",
|
||||
llm="gpt-5-nano",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# 1. Task with Image URL
|
||||
|
||||
@@ -15,7 +15,7 @@ researcher = Agent(
|
||||
verbose=True,
|
||||
llm="gpt-5-nano",
|
||||
tools=[Tools.internet_search],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Create an analyst agent
|
||||
@@ -26,7 +26,7 @@ analyst = Agent(
|
||||
backstory="Senior data analyst with expertise in pattern recognition",
|
||||
verbose=True,
|
||||
llm="gpt-5-nano",
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Define structured tasks
|
||||
|
||||
@@ -33,7 +33,7 @@ def get_structured_analysis(query: str, verbose: bool = True) -> AnalysisReport:
|
||||
goal="Analyze topics and provide structured reports",
|
||||
backstory="Expert at gathering information and providing structured analysis",
|
||||
verbose=verbose,
|
||||
self_reflect=True, # Enable self-reflection for better quality
|
||||
reflection=True, # Enable self-reflection for better quality
|
||||
markdown=True,
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
@@ -94,7 +94,7 @@ agent = Agent(
|
||||
goal="Search for information about 'AI Agents Framework'",
|
||||
backstory="I am an AI assistant that can search GitHub.",
|
||||
tools=[EXASearchTool],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Create tasks to demonstrate both tools
|
||||
|
||||
@@ -8,7 +8,7 @@ news_agent = Agent(
|
||||
goal="Collect and analyze news articles from various sources.",
|
||||
backstory="Expert in news gathering and content analysis.",
|
||||
tools=[get_article, get_news_sources, get_articles_from_source, get_trending_topics],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ data_agent = Agent(
|
||||
goal="Perform internet searches to collect relevant information.",
|
||||
backstory="Expert in finding and organising internet data.",
|
||||
tools=[internet_search_tool],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# 3. Tasks
|
||||
|
||||
@@ -158,7 +158,7 @@ data_agent = Agent(
|
||||
goal="Collect and validate data about individuals",
|
||||
backstory="Expert in gathering and validating demographic data",
|
||||
tools=[random_data_of_individuals],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
process_agent = Agent(
|
||||
@@ -167,7 +167,7 @@ process_agent = Agent(
|
||||
goal="Process and categorize individual data",
|
||||
backstory="Expert in data analysis and categorization",
|
||||
tools=[process_data_of_individuals],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Modify tasks to use Pydantic models
|
||||
|
||||
@@ -125,7 +125,7 @@ email_task = Task(
|
||||
description="Write a business proposal for a new software project, including contact information",
|
||||
expected_output="Professional business proposal with contact email addresses",
|
||||
agent=email_agent,
|
||||
guardrail=email_format_guardrail,
|
||||
guardrails=email_format_guardrail,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
@@ -135,7 +135,7 @@ content_task = Task(
|
||||
description="Write an analysis of cloud computing benefits for small businesses",
|
||||
expected_output="Comprehensive analysis of cloud computing benefits (minimum 50 words)",
|
||||
agent=content_agent,
|
||||
guardrail=word_count_guardrail,
|
||||
guardrails=word_count_guardrail,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
@@ -145,7 +145,7 @@ business_task = Task(
|
||||
description="Provide advice on handling difficult client relationships",
|
||||
expected_output="Professional advice maintaining appropriate business tone",
|
||||
agent=business_agent,
|
||||
guardrail=professional_tone_guardrail,
|
||||
guardrails=professional_tone_guardrail,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
@@ -155,7 +155,7 @@ research_task = Task(
|
||||
description="Analyze the impact of artificial intelligence on job markets",
|
||||
expected_output="Research-backed analysis with citations and evidence",
|
||||
agent=research_agent,
|
||||
guardrail=factual_accuracy_guardrail,
|
||||
guardrails=factual_accuracy_guardrail,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
@@ -176,7 +176,7 @@ llm_guardrail_task = Task(
|
||||
agent=llm_guardrail_agent,
|
||||
|
||||
# LLM-based guardrail using natural language validation
|
||||
guardrail="Validate that this marketing content is: 1) Factually accurate with no false claims, 2) Compliant with advertising standards (no misleading statements), 3) Engaging and professional in tone, 4) Includes a clear call-to-action. If any criteria are not met, explain what needs to be improved.",
|
||||
guardrails="Validate that this marketing content is: 1) Factually accurate with no false claims, 2) Compliant with advertising standards (no misleading statements), 3) Engaging and professional in tone, 4) Includes a clear call-to-action. If any criteria are not met, explain what needs to be improved.",
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ production_agent = Agent(
|
||||
goal="Generate safe, compliant content for production use",
|
||||
backstory="Production-ready agent with built-in safety and compliance checks",
|
||||
tools=[internet_search],
|
||||
guardrail=content_guardrail,
|
||||
guardrails=content_guardrail,
|
||||
max_retries=3,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ class DynamicAgentSystem:
|
||||
goal=f"Conduct {inputs['depth']} research on {inputs['topic']} in {inputs['language']}",
|
||||
backstory=f"Multilingual expert specializing in {inputs['depth']} analysis",
|
||||
tools=[duckduckgo],
|
||||
self_reflect=inputs['depth'] == 'detailed',
|
||||
reflection=inputs['depth'] == 'detailed',
|
||||
llm=self.llm_config
|
||||
)
|
||||
agents.append(research_agent)
|
||||
|
||||
@@ -19,7 +19,7 @@ search_agent = Agent(
|
||||
goal=f"Find {num_results} relevant results about {topic}",
|
||||
backstory="Expert in internet research and data collection",
|
||||
tools=[duckduckgo],
|
||||
self_reflect=False
|
||||
reflection=False
|
||||
)
|
||||
|
||||
# Create analysis agent
|
||||
|
||||
@@ -34,9 +34,7 @@ agent = Agent(
|
||||
llm=llm_config, # Pass the detailed configuration
|
||||
verbose=True, # Enable detailed output
|
||||
markdown=True, # Format responses in markdown
|
||||
self_reflect=True, # Enable self-reflection
|
||||
max_reflect=3, # Maximum reflection iterations
|
||||
min_reflect=1 # Minimum reflection iterations
|
||||
reflection=True # Enable self-reflection
|
||||
)
|
||||
|
||||
# Test the agent
|
||||
|
||||
@@ -4,7 +4,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gemini/gemini-1.5-flash-8b",
|
||||
self_reflect=True,
|
||||
reflection=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gemini/gemini-1.5-flash-8b",
|
||||
self_reflect=True,
|
||||
reflection=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ def main():
|
||||
and technology. You provide concise, accurate summaries of important news.""",
|
||||
tools=[search_tool],
|
||||
verbose=True,
|
||||
self_reflect=False # Disable reflection for faster execution
|
||||
reflection=False # Disable reflection for faster execution
|
||||
)
|
||||
|
||||
# Define the task
|
||||
|
||||
@@ -113,7 +113,7 @@ initial_research = Task(
|
||||
description="Research the current state of renewable energy adoption in the business sector, including trends, challenges, and opportunities",
|
||||
expected_output="Comprehensive research report on renewable energy adoption in business",
|
||||
agent=research_agent,
|
||||
guardrail=data_quality_validator,
|
||||
guardrails=data_quality_validator,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
@@ -124,7 +124,7 @@ quality_check = Task(
|
||||
expected_output="Quality assessment report with go/no-go decision for proceeding to analysis",
|
||||
agent=quality_checker,
|
||||
context=[initial_research],
|
||||
guardrail=completeness_validator,
|
||||
guardrails=completeness_validator,
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
@@ -153,7 +153,7 @@ additional_research = Task(
|
||||
agent=research_agent,
|
||||
context=[initial_research, quality_check],
|
||||
tools=[duckduckgo],
|
||||
guardrail=data_quality_validator,
|
||||
guardrails=data_quality_validator,
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
@@ -190,7 +190,7 @@ final_report = Task(
|
||||
expected_output="Executive report with strategic recommendations for renewable energy adoption",
|
||||
agent=report_writer,
|
||||
context=[initial_research, quality_check, data_analysis, decision_task],
|
||||
guardrail=completeness_validator,
|
||||
guardrails=completeness_validator,
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ system_ops_agent = Agent(
|
||||
backstory="""You are an expert system administrator with deep knowledge of Unix/Linux systems.
|
||||
You excel at running complex system commands, managing processes, and handling system operations.
|
||||
You always validate commands before execution and ensure they are safe to run.""",
|
||||
min_reflect=6,
|
||||
max_reflect=10,
|
||||
reflection=True,
|
||||
tools=[run_terminal_command, save_to_file],
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
@@ -53,8 +53,7 @@ web_scraper_agent = Agent(
|
||||
role="Web Scraper",
|
||||
goal="Extract URLs from https://news.ycombinator.com/",
|
||||
backstory="An expert in data extraction from websites, adept at navigating and retrieving detailed information and saving it to a file. and just check if the file is created using run_terminal_command tool",
|
||||
min_reflect=3,
|
||||
max_reflect=5,
|
||||
reflection=True,
|
||||
tools=[code_interpret, save_to_file, run_terminal_command],
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ hackathon_judge = Agent(
|
||||
You excel at evaluating innovation, technical implementation, and presentation quality.
|
||||
You provide constructive feedback and identify both strengths and areas for improvement.""",
|
||||
llm="gpt-5-nano", # Using vision-capable model
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
knowledge=""
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_hackathon_judge():
|
||||
You excel at evaluating innovation, technical implementation, and presentation quality.
|
||||
You provide constructive feedback and identify both strengths and areas for improvement.""",
|
||||
llm="gpt-5-nano", # Using vision-capable model
|
||||
self_reflect=False,
|
||||
reflection=False,
|
||||
knowledge=""
|
||||
)
|
||||
|
||||
|
||||
@@ -2,31 +2,29 @@
|
||||
"""
|
||||
Basic Thinking Budgets Example for PraisonAI Agents.
|
||||
|
||||
This example demonstrates how to use thinking budgets to:
|
||||
1. Configure token budgets for extended thinking
|
||||
This example demonstrates how to use thinking budgets with Agent:
|
||||
1. Create an Agent with thinking budget
|
||||
2. Use predefined budget levels
|
||||
3. Track thinking usage
|
||||
4. Adapt budgets based on complexity
|
||||
|
||||
Usage:
|
||||
python basic_thinking.py
|
||||
"""
|
||||
|
||||
from praisonaiagents.thinking import (
|
||||
ThinkingBudget, ThinkingConfig, ThinkingUsage, ThinkingTracker
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.thinking import ThinkingBudget, ThinkingTracker
|
||||
from praisonaiagents.thinking.budget import BudgetLevel
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Thinking Budgets Demo")
|
||||
print("Agent-Centric Thinking Budgets Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# ==========================================================================
|
||||
# Budget Levels
|
||||
# ==========================================================================
|
||||
print("\n--- Budget Levels ---")
|
||||
print("\n--- Available Budget Levels ---")
|
||||
|
||||
levels = [
|
||||
("Minimal", ThinkingBudget.minimal()),
|
||||
@@ -39,10 +37,25 @@ def main():
|
||||
for name, budget in levels:
|
||||
print(f" {name}: {budget.max_tokens:,} tokens")
|
||||
|
||||
# ==========================================================================
|
||||
# Agent with Thinking Budget
|
||||
# ==========================================================================
|
||||
print("\n--- Creating Agent with Thinking Budget ---")
|
||||
|
||||
agent = Agent(
|
||||
name="DeepThinker",
|
||||
instructions="You are a problem-solving assistant that thinks step by step.",
|
||||
thinking_budget=ThinkingBudget.high() # 16,000 tokens for reasoning
|
||||
)
|
||||
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Thinking budget: {agent.thinking_budget.max_tokens:,} tokens")
|
||||
print(f"Adaptive: {agent.thinking_budget.adaptive}")
|
||||
|
||||
# ==========================================================================
|
||||
# Custom Budget
|
||||
# ==========================================================================
|
||||
print("\n--- Custom Budget ---")
|
||||
print("\n--- Custom Thinking Budget ---")
|
||||
|
||||
custom_budget = ThinkingBudget(
|
||||
max_tokens=12000,
|
||||
@@ -50,22 +63,24 @@ def main():
|
||||
adaptive=True,
|
||||
max_time_seconds=120.0
|
||||
)
|
||||
print(f" Max tokens: {custom_budget.max_tokens:,}")
|
||||
print(f" Min tokens: {custom_budget.min_tokens:,}")
|
||||
print(f" Adaptive: {custom_budget.adaptive}")
|
||||
print(f" Time limit: {custom_budget.max_time_seconds}s")
|
||||
|
||||
agent_custom = Agent(
|
||||
name="CustomThinker",
|
||||
instructions="You solve complex problems.",
|
||||
thinking_budget=custom_budget
|
||||
)
|
||||
|
||||
print(f"Agent: {agent_custom.name}")
|
||||
print(f"Max tokens: {agent_custom.thinking_budget.max_tokens:,}")
|
||||
print(f"Min tokens: {agent_custom.thinking_budget.min_tokens:,}")
|
||||
print(f"Time limit: {agent_custom.thinking_budget.max_time_seconds}s")
|
||||
|
||||
# ==========================================================================
|
||||
# Complexity-Based Scaling
|
||||
# ==========================================================================
|
||||
print("\n--- Complexity-Based Token Allocation ---")
|
||||
|
||||
budget = ThinkingBudget(
|
||||
max_tokens=16000,
|
||||
min_tokens=2000,
|
||||
adaptive=True
|
||||
)
|
||||
|
||||
budget = agent.thinking_budget
|
||||
complexities = [0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
for complexity in complexities:
|
||||
tokens = budget.get_tokens_for_complexity(complexity)
|
||||
@@ -75,34 +90,15 @@ def main():
|
||||
# ==========================================================================
|
||||
# Usage Tracking
|
||||
# ==========================================================================
|
||||
print("\n--- Usage Tracking ---")
|
||||
|
||||
usage = ThinkingUsage(
|
||||
budget_tokens=8000,
|
||||
tokens_used=5000,
|
||||
time_seconds=30.0,
|
||||
budget_time=60.0
|
||||
)
|
||||
|
||||
print(f" Tokens used: {usage.tokens_used:,} / {usage.budget_tokens:,}")
|
||||
print(f" Tokens remaining: {usage.tokens_remaining:,}")
|
||||
print(f" Utilization: {usage.token_utilization:.1%}")
|
||||
print(f" Over budget: {usage.is_over_budget}")
|
||||
print(f" Time remaining: {usage.time_remaining}s")
|
||||
|
||||
# ==========================================================================
|
||||
# Session Tracking
|
||||
# ==========================================================================
|
||||
print("\n--- Session Tracking ---")
|
||||
|
||||
tracker = ThinkingTracker()
|
||||
|
||||
# Simulate multiple thinking sessions
|
||||
# Simulate thinking sessions
|
||||
sessions = [
|
||||
(8000, 4000, 20.0),
|
||||
(8000, 6000, 35.0),
|
||||
(16000, 12000, 60.0),
|
||||
(8000, 9000, 45.0), # Over budget
|
||||
]
|
||||
|
||||
for budget_tokens, tokens_used, time_seconds in sessions:
|
||||
@@ -112,21 +108,7 @@ def main():
|
||||
summary = tracker.get_summary()
|
||||
print(f" Total sessions: {summary['session_count']}")
|
||||
print(f" Total tokens used: {summary['total_tokens_used']:,}")
|
||||
print(f" Average tokens/session: {summary['average_tokens_per_session']:,.0f}")
|
||||
print(f" Average utilization: {summary['average_utilization']:.1%}")
|
||||
print(f" Over-budget sessions: {summary['over_budget_count']}")
|
||||
|
||||
# ==========================================================================
|
||||
# Serialization
|
||||
# ==========================================================================
|
||||
print("\n--- Serialization ---")
|
||||
|
||||
budget = ThinkingBudget.from_level(BudgetLevel.HIGH)
|
||||
data = budget.to_dict()
|
||||
print(f" Serialized: level={data['level']}, max_tokens={data['max_tokens']}")
|
||||
|
||||
restored = ThinkingBudget.from_dict(data)
|
||||
print(f" Restored: {restored.max_tokens:,} tokens")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demo Complete!")
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto-generate CLI reference documentation from --help output.
|
||||
|
||||
Usage:
|
||||
python scripts/generate_cli_docs.py > docs/cli/reference.mdx
|
||||
python scripts/generate_cli_docs.py --output docs/cli/reference.mdx
|
||||
|
||||
This script runs `praisonai <cmd> --help` for key commands and generates
|
||||
deterministic markdown documentation.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Commands to document
|
||||
COMMANDS = [
|
||||
# Top-level
|
||||
("praisonai", "--help"),
|
||||
# Knowledge
|
||||
("praisonai knowledge", "--help"),
|
||||
("praisonai knowledge index", "--help"),
|
||||
("praisonai knowledge search", "--help"),
|
||||
("praisonai knowledge add", "--help"),
|
||||
("praisonai knowledge list", "--help"),
|
||||
# RAG
|
||||
("praisonai rag", "--help"),
|
||||
("praisonai rag index", "--help"),
|
||||
("praisonai rag query", "--help"),
|
||||
("praisonai rag chat", "--help"),
|
||||
("praisonai rag eval", "--help"),
|
||||
("praisonai rag serve", "--help"),
|
||||
# Run
|
||||
("praisonai run", "--help"),
|
||||
# Chat
|
||||
("praisonai chat", "--help"),
|
||||
]
|
||||
|
||||
|
||||
def run_help(cmd: str, flag: str = "--help") -> str:
|
||||
"""Run a command with --help and capture output."""
|
||||
full_cmd = f"{cmd} {flag}"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
full_cmd.split(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
return result.stdout or result.stderr or "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "(timeout)"
|
||||
except FileNotFoundError:
|
||||
return "(command not found)"
|
||||
except Exception as e:
|
||||
return f"(error: {e})"
|
||||
|
||||
|
||||
def generate_markdown() -> str:
|
||||
"""Generate markdown documentation."""
|
||||
lines = [
|
||||
"---",
|
||||
"title: CLI Reference",
|
||||
"description: Auto-generated CLI command reference",
|
||||
"---",
|
||||
"",
|
||||
"# PraisonAI CLI Reference",
|
||||
"",
|
||||
"This page is auto-generated from `--help` output.",
|
||||
"",
|
||||
f"Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC",
|
||||
"",
|
||||
]
|
||||
|
||||
for cmd, flag in COMMANDS:
|
||||
# Section header
|
||||
cmd_name = cmd.replace("praisonai ", "").replace(" ", "-") or "main"
|
||||
lines.append(f"## `{cmd}`")
|
||||
lines.append("")
|
||||
lines.append("```")
|
||||
|
||||
# Get help output
|
||||
output = run_help(cmd, flag)
|
||||
lines.append(output.strip())
|
||||
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate CLI docs from --help")
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
type=Path,
|
||||
help="Output file path (default: stdout)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Check if output matches existing file (for CI)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
content = generate_markdown()
|
||||
|
||||
if args.check and args.output:
|
||||
if args.output.exists():
|
||||
existing = args.output.read_text()
|
||||
# Compare ignoring timestamp line
|
||||
existing_lines = [l for l in existing.split("\n") if not l.startswith("Generated:")]
|
||||
new_lines = [l for l in content.split("\n") if not l.startswith("Generated:")]
|
||||
if existing_lines != new_lines:
|
||||
print(f"ERROR: {args.output} is out of date. Run: python scripts/generate_cli_docs.py -o {args.output}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"OK: {args.output} is up to date")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"ERROR: {args.output} does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(content)
|
||||
print(f"Written to {args.output}")
|
||||
else:
|
||||
print(content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -78,26 +78,29 @@ def example_get_context_for_agent():
|
||||
# =============================================
|
||||
|
||||
def example_agent_with_fast_context():
|
||||
"""Use FastContext with an Agent."""
|
||||
"""Use FastContext with an Agent via context= param."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context.fast import FastContext
|
||||
|
||||
# Create agent with FastContext enabled
|
||||
# Create FastContext instance
|
||||
fc = FastContext(workspace_path=".")
|
||||
|
||||
# Search for code context
|
||||
result = fc.search("find authentication code")
|
||||
context = result.to_context_string() if result.files else None
|
||||
|
||||
# Create agent with context management enabled
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
instructions="You are a helpful code assistant.",
|
||||
fast_context=True,
|
||||
fast_context_path=".", # Current directory
|
||||
fast_context_model="gpt-4o-mini",
|
||||
fast_context_max_turns=4,
|
||||
fast_context_parallelism=8
|
||||
context=True, # Enable context management
|
||||
)
|
||||
|
||||
# Use delegate_to_fast_context for code search
|
||||
context = agent.delegate_to_fast_context("find authentication code")
|
||||
|
||||
if context:
|
||||
print("Found relevant code context:")
|
||||
print(context[:300] + "...")
|
||||
# Use the context in a chat
|
||||
print(agent.chat(f"Based on this code:\n{context}\n\nExplain the authentication flow."))
|
||||
else:
|
||||
print("No relevant code found")
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Large Context Knowledge Handling Example
|
||||
|
||||
Demonstrates the full pipeline:
|
||||
1. Token Budget calculation
|
||||
2. Incremental indexing with ignore patterns
|
||||
3. Strategy selection based on corpus size
|
||||
4. Smart retrieval with filtering
|
||||
5. Contextual compression
|
||||
6. Hierarchical summarization
|
||||
|
||||
Usage:
|
||||
python examples/large_context_knowledge.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_sample_corpus(base_path: str) -> None:
|
||||
"""Create a sample corpus for demonstration."""
|
||||
# Create directory structure
|
||||
src_dir = Path(base_path) / "src"
|
||||
docs_dir = Path(base_path) / "docs"
|
||||
src_dir.mkdir(parents=True, exist_ok=True)
|
||||
docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create Python files
|
||||
(src_dir / "main.py").write_text("""
|
||||
# Main application entry point
|
||||
def main():
|
||||
'''Start the application.'''
|
||||
print("Hello from PraisonAI!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
""")
|
||||
|
||||
(src_dir / "utils.py").write_text("""
|
||||
# Utility functions
|
||||
def helper_function(x: int) -> int:
|
||||
'''Double the input value.'''
|
||||
return x * 2
|
||||
|
||||
def format_output(text: str) -> str:
|
||||
'''Format text for display.'''
|
||||
return f"[OUTPUT] {text}"
|
||||
""")
|
||||
|
||||
# Create documentation
|
||||
(docs_dir / "readme.md").write_text("""
|
||||
# Project Documentation
|
||||
|
||||
This is a sample project demonstrating large context knowledge handling.
|
||||
|
||||
## Features
|
||||
- Token budget management
|
||||
- Incremental indexing
|
||||
- Smart retrieval
|
||||
- Contextual compression
|
||||
""")
|
||||
|
||||
(docs_dir / "api.md").write_text("""
|
||||
# API Reference
|
||||
|
||||
## Functions
|
||||
|
||||
### main()
|
||||
Entry point for the application.
|
||||
|
||||
### helper_function(x)
|
||||
Doubles the input value.
|
||||
|
||||
### format_output(text)
|
||||
Formats text for display.
|
||||
""")
|
||||
|
||||
# Create .praisonignore
|
||||
(Path(base_path) / ".praisonignore").write_text("""
|
||||
# Ignore patterns
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.git/
|
||||
""")
|
||||
|
||||
print(f"Created sample corpus at: {base_path}")
|
||||
|
||||
|
||||
def demo_token_budget():
|
||||
"""Demonstrate token budget calculation."""
|
||||
print("\n" + "="*60)
|
||||
print("1. TOKEN BUDGET CALCULATION")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.rag.budget import TokenBudget, get_model_context_window
|
||||
|
||||
# Get context window for different models
|
||||
models = ["gpt-4o", "claude-3.5-sonnet", "gemini-1.5-pro", "llama-3.1-70b"]
|
||||
print("\nModel context windows:")
|
||||
for model in models:
|
||||
window = get_model_context_window(model)
|
||||
print(f" {model}: {window:,} tokens")
|
||||
|
||||
# Create budget for GPT-4o
|
||||
budget = TokenBudget.from_model("gpt-4o")
|
||||
print(f"\nBudget for GPT-4o:")
|
||||
print(f" Max tokens: {budget.model_max_tokens:,}")
|
||||
print(f" Reserved for response: {budget.reserved_response_tokens:,}")
|
||||
print(f" Max context tokens: {budget.max_context_tokens:,}")
|
||||
|
||||
# Dynamic budget calculation
|
||||
available = budget.dynamic_budget(
|
||||
prompt_tokens=500,
|
||||
history_tokens=2000,
|
||||
system_tokens=1000,
|
||||
)
|
||||
print(f"\nDynamic budget (with 500 prompt + 2000 history + 1000 system):")
|
||||
print(f" Available for context: {available:,} tokens")
|
||||
|
||||
|
||||
def demo_indexing(corpus_path: str):
|
||||
"""Demonstrate incremental indexing."""
|
||||
print("\n" + "="*60)
|
||||
print("2. INCREMENTAL INDEXING")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
from praisonaiagents.knowledge.indexing import CorpusStats
|
||||
|
||||
# Get corpus stats before indexing
|
||||
stats = CorpusStats.from_directory(corpus_path)
|
||||
print(f"\nCorpus statistics:")
|
||||
print(f" Files: {stats.file_count}")
|
||||
print(f" Estimated tokens: {stats.total_tokens:,}")
|
||||
print(f" Recommended strategy: {stats.strategy_recommendation}")
|
||||
|
||||
# Index the corpus
|
||||
knowledge = Knowledge()
|
||||
result = knowledge.index(
|
||||
corpus_path,
|
||||
user_id="demo_user",
|
||||
incremental=False,
|
||||
exclude_glob=["*.pyc"],
|
||||
)
|
||||
|
||||
print(f"\nIndexing result:")
|
||||
print(f" Files indexed: {result.files_indexed}")
|
||||
print(f" Files skipped: {result.files_skipped}")
|
||||
print(f" Chunks created: {result.chunks_created}")
|
||||
print(f" Duration: {result.duration_seconds:.2f}s")
|
||||
|
||||
# Incremental re-index (should skip unchanged files)
|
||||
print("\nRe-indexing (incremental)...")
|
||||
result2 = knowledge.index(corpus_path, user_id="demo_user", incremental=True)
|
||||
print(f" Files indexed: {result2.files_indexed}")
|
||||
print(f" Files skipped: {result2.files_skipped}")
|
||||
|
||||
|
||||
def demo_strategy_selection():
|
||||
"""Demonstrate strategy selection."""
|
||||
print("\n" + "="*60)
|
||||
print("3. STRATEGY SELECTION")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.rag.strategy import select_strategy, get_strategy_description, RetrievalStrategy
|
||||
from praisonaiagents.knowledge.indexing import CorpusStats
|
||||
|
||||
# Show strategy for different corpus sizes
|
||||
sizes = [5, 50, 500, 5000, 50000, 500000]
|
||||
print("\nStrategy selection by corpus size:")
|
||||
for size in sizes:
|
||||
stats = CorpusStats(file_count=size)
|
||||
strategy = select_strategy(stats)
|
||||
print(f" {size:>7} files -> {strategy.value}")
|
||||
|
||||
# Show strategy descriptions
|
||||
print("\nStrategy descriptions:")
|
||||
for strategy in RetrievalStrategy:
|
||||
desc = get_strategy_description(strategy)
|
||||
print(f" {strategy.value}: {desc[:60]}...")
|
||||
|
||||
|
||||
def demo_smart_retriever():
|
||||
"""Demonstrate smart retriever with filtering."""
|
||||
print("\n" + "="*60)
|
||||
print("4. SMART RETRIEVER")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.rag.retriever import SmartRetriever, SimpleReranker
|
||||
|
||||
# Create retriever with reranker
|
||||
reranker = SimpleReranker()
|
||||
retriever = SmartRetriever(reranker=reranker)
|
||||
|
||||
# Demonstrate filtering
|
||||
chunks = [
|
||||
{"text": "Python code for data processing", "metadata": {"filename": "process.py"}},
|
||||
{"text": "JavaScript frontend code", "metadata": {"filename": "app.js"}},
|
||||
{"text": "Python utilities and helpers", "metadata": {"filename": "utils.py"}},
|
||||
{"text": "CSS styling rules", "metadata": {"filename": "styles.css"}},
|
||||
]
|
||||
|
||||
print("\nFiltering by include pattern (*.py):")
|
||||
filtered = retriever._apply_filters(chunks, include_glob=["*.py"])
|
||||
for chunk in filtered:
|
||||
print(f" - {chunk['metadata']['filename']}: {chunk['text'][:40]}...")
|
||||
|
||||
print("\nFiltering by exclude pattern (utils*):")
|
||||
filtered = retriever._apply_filters(chunks, exclude_glob=["utils*"])
|
||||
for chunk in filtered:
|
||||
print(f" - {chunk['metadata']['filename']}: {chunk['text'][:40]}...")
|
||||
|
||||
# Demonstrate reranking
|
||||
print("\nReranking for query 'Python data':")
|
||||
reranked = reranker.rerank("Python data", chunks)
|
||||
for i, chunk in enumerate(reranked[:3], 1):
|
||||
print(f" {i}. [{chunk['score']:.2f}] {chunk['text'][:40]}...")
|
||||
|
||||
|
||||
def demo_compression():
|
||||
"""Demonstrate contextual compression."""
|
||||
print("\n" + "="*60)
|
||||
print("5. CONTEXTUAL COMPRESSION")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.rag.compressor import ContextCompressor
|
||||
|
||||
compressor = ContextCompressor()
|
||||
|
||||
# Create chunks with some redundancy
|
||||
chunks = [
|
||||
{"text": "Python is a high-level programming language. It is known for its simplicity and readability. Python supports multiple programming paradigms."},
|
||||
{"text": "Python is a high-level programming language. It is known for its simplicity and readability. Python supports multiple programming paradigms."}, # Duplicate
|
||||
{"text": "JavaScript is the language of the web. It runs in browsers and on servers with Node.js."},
|
||||
{"text": "Python has extensive libraries for data science, machine learning, and web development."},
|
||||
]
|
||||
|
||||
print(f"\nOriginal chunks: {len(chunks)}")
|
||||
|
||||
result = compressor.compress(chunks, "Python programming", target_tokens=200)
|
||||
|
||||
print(f"After compression:")
|
||||
print(f" Chunks: {len(result.chunks)}")
|
||||
print(f" Original tokens: {result.original_tokens}")
|
||||
print(f" Compressed tokens: {result.compressed_tokens}")
|
||||
print(f" Compression ratio: {result.compression_ratio:.2%}")
|
||||
print(f" Method: {result.method_used}")
|
||||
|
||||
|
||||
def demo_hierarchical_summarizer(corpus_path: str):
|
||||
"""Demonstrate hierarchical summarization."""
|
||||
print("\n" + "="*60)
|
||||
print("6. HIERARCHICAL SUMMARIZATION")
|
||||
print("="*60)
|
||||
|
||||
from praisonaiagents.rag.summarizer import HierarchicalSummarizer
|
||||
|
||||
summarizer = HierarchicalSummarizer()
|
||||
|
||||
# Build hierarchy
|
||||
print(f"\nBuilding hierarchy for: {corpus_path}")
|
||||
result = summarizer.build_hierarchy(corpus_path, levels=2)
|
||||
|
||||
print(f"\nHierarchy result:")
|
||||
print(f" Total files: {result.total_files}")
|
||||
print(f" Total nodes: {len(result.nodes)}")
|
||||
print(f" Total tokens: {result.total_tokens}")
|
||||
|
||||
# Show summaries
|
||||
print("\nFile summaries:")
|
||||
for path, node in result.nodes.items():
|
||||
if node.level == 1: # File level
|
||||
filename = node.metadata.get("filename", os.path.basename(path))
|
||||
summary = node.summary[:60] + "..." if len(node.summary) > 60 else node.summary
|
||||
print(f" {filename}: {summary}")
|
||||
|
||||
# Query hierarchy
|
||||
print("\nQuerying hierarchy for 'function':")
|
||||
results = summarizer.query_hierarchy("function", corpus_path, max_results=3)
|
||||
for r in results:
|
||||
print(f" - {os.path.basename(r['path'])}: score={r['score']}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all demonstrations."""
|
||||
print("="*60)
|
||||
print("LARGE CONTEXT KNOWLEDGE HANDLING DEMO")
|
||||
print("="*60)
|
||||
|
||||
# Create temporary corpus
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
create_sample_corpus(tmpdir)
|
||||
|
||||
# Run demos
|
||||
demo_token_budget()
|
||||
demo_indexing(tmpdir)
|
||||
demo_strategy_selection()
|
||||
demo_smart_retriever()
|
||||
demo_compression()
|
||||
demo_hierarchical_summarizer(tmpdir)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("DEMO COMPLETE")
|
||||
print("="*60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -372,6 +372,26 @@ def __getattr__(name):
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
# Feature Config classes (agent-centric API)
|
||||
elif name in ("MemoryConfig", "KnowledgeConfig", "PlanningConfig",
|
||||
"ReflectionConfig", "GuardrailConfig", "WebConfig",
|
||||
"MemoryBackend", "ChunkingStrategy", "GuardrailAction",
|
||||
"WebSearchProvider"):
|
||||
from .config import feature_configs
|
||||
result = getattr(feature_configs, name)
|
||||
_lazy_cache[name] = result
|
||||
return result
|
||||
|
||||
# Context management config (already exists)
|
||||
elif name == "ManagerConfig":
|
||||
from .context.manager import ManagerConfig
|
||||
_lazy_cache[name] = ManagerConfig
|
||||
return ManagerConfig
|
||||
elif name == "ContextManager":
|
||||
from .context.manager import ContextManager
|
||||
_lazy_cache[name] = ContextManager
|
||||
return ContextManager
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
@@ -527,4 +547,18 @@ __all__ = [
|
||||
'RAGConfig',
|
||||
'RAGResult',
|
||||
'RAGCitation',
|
||||
# Feature Configs (agent-centric API)
|
||||
'MemoryConfig',
|
||||
'KnowledgeConfig',
|
||||
'PlanningConfig',
|
||||
'ReflectionConfig',
|
||||
'GuardrailConfig',
|
||||
'WebConfig',
|
||||
'MemoryBackend',
|
||||
'ChunkingStrategy',
|
||||
'GuardrailAction',
|
||||
'WebSearchProvider',
|
||||
# Context Management
|
||||
'ManagerConfig',
|
||||
'ContextManager',
|
||||
]
|
||||
@@ -267,44 +267,23 @@ class Agent:
|
||||
response_template: Optional[str] = None,
|
||||
allow_code_execution: Optional[bool] = False,
|
||||
max_retry_limit: int = 2,
|
||||
respect_context_window: bool = True,
|
||||
code_execution_mode: Literal["safe", "unsafe"] = "safe",
|
||||
embedder_config: Optional[Dict[str, Any]] = None,
|
||||
knowledge: Optional[Union[List[str], Any]] = None,
|
||||
knowledge: Optional[Union[bool, List[str], Any]] = None,
|
||||
retrieval_config: Optional[Union[Dict[str, Any], Any]] = None,
|
||||
knowledge_config: Optional[Dict[str, Any]] = None,
|
||||
rag_config: Optional[Dict[str, Any]] = None,
|
||||
embedder_config: Optional[Dict[str, Any]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
use_system_prompt: Optional[bool] = True,
|
||||
markdown: bool = True,
|
||||
stream: bool = False,
|
||||
metrics: bool = False,
|
||||
self_reflect: bool = False,
|
||||
max_reflect: int = 3,
|
||||
min_reflect: int = 1,
|
||||
reflect_llm: Optional[str] = None,
|
||||
reflect_prompt: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
reasoning_steps: bool = False,
|
||||
guardrail: Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]] = None,
|
||||
max_guardrail_retries: int = 3,
|
||||
handoffs: Optional[List[Union['Agent', 'Handoff']]] = None,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
web_search: Optional[Union[bool, Dict[str, Any]]] = None,
|
||||
web_fetch: Optional[Union[bool, Dict[str, Any]]] = None,
|
||||
prompt_caching: Optional[bool] = None,
|
||||
claude_memory: Optional[Union[bool, Any]] = None,
|
||||
plan_mode: bool = False,
|
||||
planning: bool = False,
|
||||
planning_tools: Optional[List[Any]] = None,
|
||||
planning_reasoning: bool = False,
|
||||
fast_context: bool = False,
|
||||
fast_context_path: Optional[str] = None,
|
||||
fast_context_model: str = "gpt-4o-mini",
|
||||
fast_context_max_turns: int = 4,
|
||||
fast_context_parallelism: int = 8,
|
||||
fast_context_timeout: float = 30.0,
|
||||
history_in_context: Optional[int] = None,
|
||||
planning: Optional[Union[bool, Any]] = False,
|
||||
auto_save: Optional[str] = None,
|
||||
skills: Optional[List[str]] = None,
|
||||
skills_dirs: Optional[List[str]] = None,
|
||||
@@ -313,8 +292,20 @@ class Agent:
|
||||
hooks: Optional[List[Any]] = None,
|
||||
llm_config: Optional[Dict[str, Any]] = None,
|
||||
rate_limiter: Optional[Any] = None,
|
||||
auto_summarize: bool = False,
|
||||
summarize_threshold: float = 0.8
|
||||
# Agent-centric feature parameters (lazy loaded)
|
||||
auto_memory: Optional[Any] = None,
|
||||
policy: Optional[Any] = None,
|
||||
background: Optional[Any] = None,
|
||||
checkpoints: Optional[Any] = None,
|
||||
output_style: Optional[Any] = None,
|
||||
thinking_budget: Optional[Any] = None,
|
||||
# Context management (single param for all context features)
|
||||
context: Optional[Union[bool, Any]] = False,
|
||||
# NEW: Consolidated feature params (agent-centric API)
|
||||
# These take precedence over individual params when provided
|
||||
reflection: Optional[Union[bool, Any]] = None, # Union[bool, ReflectionConfig]
|
||||
guardrails: Optional[Union[bool, Callable, Any]] = None, # Union[bool, Callable, GuardrailConfig]
|
||||
web: Optional[Union[bool, Any]] = None, # Union[bool, WebConfig]
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
@@ -363,8 +354,6 @@ class Agent:
|
||||
snippets during task completion. Use with caution for security. Defaults to False.
|
||||
max_retry_limit (int, optional): Maximum number of retry attempts for failed operations
|
||||
before giving up. Helps handle transient errors. Defaults to 2.
|
||||
respect_context_window (bool, optional): Automatically manage context window size
|
||||
to prevent token limit errors with large conversations. Defaults to True.
|
||||
code_execution_mode (Literal["safe", "unsafe"], optional): Safety mode for code execution.
|
||||
"safe" restricts dangerous operations, "unsafe" allows full code execution. Defaults to "safe".
|
||||
embedder_config (Optional[Dict[str, Any]], optional): Configuration dictionary for
|
||||
@@ -432,6 +421,178 @@ class Agent:
|
||||
Agent._configure_logging()
|
||||
Agent._logging_configured = True
|
||||
|
||||
# ============================================================
|
||||
# CONSOLIDATED PARAMS EXTRACTION (agent-centric API)
|
||||
# New consolidated params take precedence over individual params
|
||||
# Initialize removed params with defaults before extraction
|
||||
# ============================================================
|
||||
|
||||
# Initialize removed params with defaults (these were removed from signature)
|
||||
claude_memory = None
|
||||
self_reflect = False
|
||||
max_reflect = 3
|
||||
min_reflect = 1
|
||||
reflect_llm = None
|
||||
reflect_prompt = None
|
||||
guardrail = None
|
||||
max_guardrail_retries = 3
|
||||
web_search = None
|
||||
web_fetch = None
|
||||
plan_mode = False
|
||||
planning_tools = None
|
||||
planning_reasoning = False
|
||||
|
||||
# Extract memory settings from consolidated 'memory' param
|
||||
# memory can be: False, True, MemoryConfig, or MemoryManager instance
|
||||
_memory_user_id = user_id # Default from param
|
||||
_memory_session_id = session_id # Default from param
|
||||
_memory_db = db # Default from param
|
||||
_memory_auto_memory = auto_memory # Default from param
|
||||
_memory_claude_memory = claude_memory # Default from param
|
||||
|
||||
if memory is not None and memory is not False:
|
||||
if hasattr(memory, 'user_id') and hasattr(memory, 'backend'):
|
||||
# It's a MemoryConfig
|
||||
if memory.user_id:
|
||||
_memory_user_id = memory.user_id
|
||||
if memory.session_id:
|
||||
_memory_session_id = memory.session_id
|
||||
if memory.db:
|
||||
_memory_db = memory.db
|
||||
_memory_auto_memory = memory.auto_memory
|
||||
_memory_claude_memory = memory.claude_memory
|
||||
# Convert MemoryConfig to appropriate memory init value
|
||||
if memory.backend == "file" or (hasattr(memory.backend, 'value') and memory.backend.value == "file"):
|
||||
memory = True # Use FileMemory
|
||||
elif memory.config:
|
||||
memory = memory.config # Use config dict
|
||||
else:
|
||||
backend_val = memory.backend.value if hasattr(memory.backend, 'value') else memory.backend
|
||||
memory = backend_val # Use backend string
|
||||
|
||||
# Apply extracted memory values
|
||||
user_id = _memory_user_id
|
||||
session_id = _memory_session_id
|
||||
db = _memory_db
|
||||
auto_memory = _memory_auto_memory
|
||||
claude_memory = _memory_claude_memory
|
||||
|
||||
# Extract knowledge settings from consolidated 'knowledge' param
|
||||
# knowledge can be: False, True, List[str], KnowledgeConfig, or KnowledgeBase instance
|
||||
_knowledge_sources = None
|
||||
_knowledge_retrieval_config = retrieval_config
|
||||
_knowledge_embedder_config = embedder_config
|
||||
|
||||
if knowledge is not None and knowledge is not False:
|
||||
if isinstance(knowledge, list):
|
||||
# It's a list of sources - use directly
|
||||
_knowledge_sources = knowledge
|
||||
elif hasattr(knowledge, 'search') and hasattr(knowledge, 'add'):
|
||||
# It's a Knowledge/KnowledgeBase instance - pass through unchanged
|
||||
# Don't modify knowledge, let the existing logic handle it
|
||||
pass
|
||||
elif hasattr(knowledge, 'sources') and hasattr(knowledge, 'embedder'):
|
||||
# It's a KnowledgeConfig (has sources list and embedder string)
|
||||
_knowledge_sources = knowledge.sources
|
||||
_knowledge_embedder_config = knowledge.embedder_config or embedder_config
|
||||
# Build retrieval config from KnowledgeConfig
|
||||
if knowledge.config:
|
||||
_knowledge_retrieval_config = knowledge.config
|
||||
else:
|
||||
_knowledge_retrieval_config = {
|
||||
'top_k': knowledge.retrieval_k,
|
||||
'threshold': knowledge.retrieval_threshold,
|
||||
'rerank': knowledge.rerank,
|
||||
'rerank_model': knowledge.rerank_model,
|
||||
}
|
||||
# Update knowledge to be the sources list for internal processing
|
||||
knowledge = _knowledge_sources if _knowledge_sources else None
|
||||
elif knowledge is True:
|
||||
# Enable knowledge but no sources provided yet
|
||||
knowledge = None
|
||||
|
||||
# Apply extracted knowledge values
|
||||
retrieval_config = _knowledge_retrieval_config
|
||||
embedder_config = _knowledge_embedder_config
|
||||
|
||||
# Extract planning settings from consolidated 'planning' param
|
||||
# planning can be: False, True, or PlanningConfig
|
||||
_planning_enabled = planning
|
||||
_plan_mode = plan_mode
|
||||
_planning_tools = planning_tools
|
||||
_planning_reasoning = planning_reasoning
|
||||
|
||||
if planning is not None:
|
||||
if planning is True:
|
||||
_planning_enabled = True
|
||||
elif planning is False:
|
||||
_planning_enabled = False
|
||||
elif hasattr(planning, 'reasoning'):
|
||||
# It's a PlanningConfig
|
||||
_planning_enabled = True
|
||||
_planning_tools = planning.tools
|
||||
_planning_reasoning = planning.reasoning
|
||||
_plan_mode = planning.read_only
|
||||
# planning param becomes True for internal use
|
||||
planning = True
|
||||
|
||||
# Apply extracted planning values
|
||||
plan_mode = _plan_mode
|
||||
planning_tools = _planning_tools
|
||||
planning_reasoning = _planning_reasoning
|
||||
|
||||
# Extract reflection settings from consolidated 'reflection' param
|
||||
if reflection is not None:
|
||||
if reflection is True:
|
||||
# Enable with defaults
|
||||
self_reflect = True
|
||||
elif reflection is False:
|
||||
self_reflect = False
|
||||
elif hasattr(reflection, 'min_iterations'):
|
||||
# It's a ReflectionConfig
|
||||
self_reflect = True
|
||||
min_reflect = reflection.min_iterations
|
||||
max_reflect = reflection.max_iterations
|
||||
reflect_llm = reflection.llm
|
||||
reflect_prompt = reflection.prompt
|
||||
|
||||
# Extract guardrail settings from consolidated 'guardrails' param
|
||||
if guardrails is not None:
|
||||
if guardrails is True:
|
||||
# Enable with defaults (no-op, user must provide validator)
|
||||
pass
|
||||
elif guardrails is False:
|
||||
guardrail = None
|
||||
elif callable(guardrails):
|
||||
# Direct validator function
|
||||
guardrail = guardrails
|
||||
elif hasattr(guardrails, 'validator'):
|
||||
# It's a GuardrailConfig
|
||||
if guardrails.validator:
|
||||
guardrail = guardrails.validator
|
||||
elif guardrails.llm_validator:
|
||||
guardrail = guardrails.llm_validator
|
||||
max_guardrail_retries = guardrails.max_retries
|
||||
|
||||
# Extract web settings from consolidated 'web' param
|
||||
if web is not None:
|
||||
if web is True:
|
||||
# Enable both search and fetch with defaults
|
||||
web_search = True
|
||||
web_fetch = True
|
||||
elif web is False:
|
||||
web_search = False
|
||||
web_fetch = False
|
||||
elif hasattr(web, 'search'):
|
||||
# It's a WebConfig
|
||||
web_search = web.search
|
||||
web_fetch = web.fetch
|
||||
# Could also extract search_config, fetch_config if needed
|
||||
|
||||
# ============================================================
|
||||
# END CONSOLIDATED PARAMS EXTRACTION
|
||||
# ============================================================
|
||||
|
||||
# If instructions are provided, use them to set role, goal, and backstory
|
||||
if instructions:
|
||||
self.name = name or "Agent"
|
||||
@@ -466,12 +627,6 @@ class Agent:
|
||||
# Store rate limiter (optional, zero overhead when None)
|
||||
self._rate_limiter = rate_limiter
|
||||
|
||||
# Auto-summarization configuration
|
||||
if auto_summarize and not (0.0 < summarize_threshold <= 1.0):
|
||||
raise ValueError("summarize_threshold must be between 0.0 and 1.0")
|
||||
self.auto_summarize = auto_summarize
|
||||
self.summarize_threshold = summarize_threshold
|
||||
|
||||
# Store OpenAI client parameters for lazy initialization
|
||||
self._openai_api_key = api_key
|
||||
self._openai_base_url = base_url
|
||||
@@ -587,7 +742,6 @@ class Agent:
|
||||
self.response_template = response_template
|
||||
self.allow_code_execution = allow_code_execution
|
||||
self.max_retry_limit = max_retry_limit
|
||||
self.respect_context_window = respect_context_window
|
||||
self.code_execution_mode = code_execution_mode
|
||||
self.embedder_config = embedder_config
|
||||
self.knowledge = knowledge
|
||||
@@ -628,7 +782,6 @@ Your Goal: {self.goal}
|
||||
self.claude_memory = claude_memory
|
||||
|
||||
# Session management
|
||||
self.history_in_context = history_in_context # Number of past sessions to include
|
||||
self.auto_save = auto_save # Session name for auto-saving
|
||||
|
||||
# Initialize rules manager for persistent context (like Cursor/Windsurf)
|
||||
@@ -709,15 +862,6 @@ Your Goal: {self.goal}
|
||||
from ..rag.retrieval_config import RetrievalConfig
|
||||
self._retrieval_config = RetrievalConfig()
|
||||
|
||||
# Fast Context configuration (lazy loaded)
|
||||
self.fast_context_enabled = fast_context
|
||||
self._fast_context_path = fast_context_path # Store raw, resolve lazily
|
||||
self.fast_context_model = fast_context_model
|
||||
self.fast_context_max_turns = fast_context_max_turns
|
||||
self.fast_context_parallelism = fast_context_parallelism
|
||||
self.fast_context_timeout = fast_context_timeout
|
||||
self._fast_context_instance = None # Lazy loaded
|
||||
|
||||
# Agent Skills configuration (lazy loaded for zero performance impact)
|
||||
self._skills = skills
|
||||
self._skills_dirs = skills_dirs
|
||||
@@ -734,6 +878,135 @@ Your Goal: {self.goal}
|
||||
self._session_store = None
|
||||
self._session_store_initialized = False
|
||||
|
||||
# Agent-centric feature instances (lazy loaded for zero performance impact)
|
||||
self._auto_memory = auto_memory
|
||||
self._policy = policy
|
||||
self._background = background
|
||||
self._checkpoints = checkpoints
|
||||
self._output_style = output_style
|
||||
self._thinking_budget = thinking_budget
|
||||
|
||||
# Context management (lazy loaded for zero overhead when disabled)
|
||||
self._context_param = context # Store raw param for lazy init
|
||||
self._context_manager = None # Lazy initialized on first use
|
||||
self._context_manager_initialized = False
|
||||
|
||||
@property
|
||||
def auto_memory(self):
|
||||
"""AutoMemory instance for automatic memory extraction."""
|
||||
return self._auto_memory
|
||||
|
||||
@auto_memory.setter
|
||||
def auto_memory(self, value):
|
||||
self._auto_memory = value
|
||||
|
||||
@property
|
||||
def policy(self):
|
||||
"""PolicyEngine instance for execution control."""
|
||||
return self._policy
|
||||
|
||||
@policy.setter
|
||||
def policy(self, value):
|
||||
self._policy = value
|
||||
|
||||
@property
|
||||
def background(self):
|
||||
"""BackgroundRunner instance for async task execution."""
|
||||
return self._background
|
||||
|
||||
@background.setter
|
||||
def background(self, value):
|
||||
self._background = value
|
||||
|
||||
@property
|
||||
def checkpoints(self):
|
||||
"""CheckpointService instance for file-level undo/restore."""
|
||||
return self._checkpoints
|
||||
|
||||
@checkpoints.setter
|
||||
def checkpoints(self, value):
|
||||
self._checkpoints = value
|
||||
|
||||
@property
|
||||
def output_style(self):
|
||||
"""OutputStyle instance for response formatting."""
|
||||
return self._output_style
|
||||
|
||||
@output_style.setter
|
||||
def output_style(self, value):
|
||||
self._output_style = value
|
||||
|
||||
@property
|
||||
def thinking_budget(self):
|
||||
"""ThinkingBudget instance for extended thinking control."""
|
||||
return self._thinking_budget
|
||||
|
||||
@thinking_budget.setter
|
||||
def thinking_budget(self, value):
|
||||
self._thinking_budget = value
|
||||
|
||||
@property
|
||||
def context_manager(self):
|
||||
"""
|
||||
ContextManager instance for unified context management.
|
||||
|
||||
Lazy initialized on first access when context=True or context=ManagerConfig.
|
||||
Returns None when context=False (zero overhead).
|
||||
|
||||
Example:
|
||||
agent = Agent(instructions="...", context=True)
|
||||
# Access manager for advanced operations
|
||||
if agent.context_manager:
|
||||
stats = agent.context_manager.get_stats()
|
||||
"""
|
||||
if self._context_manager_initialized:
|
||||
return self._context_manager
|
||||
|
||||
# Initialize based on context param type
|
||||
if self._context_param is False or self._context_param is None:
|
||||
# Zero overhead - no context management
|
||||
self._context_manager = None
|
||||
self._context_manager_initialized = True
|
||||
return None
|
||||
|
||||
# Lazy import to avoid overhead when not used
|
||||
try:
|
||||
from ..context import ContextManager, ManagerConfig
|
||||
except ImportError:
|
||||
# Context module not available
|
||||
self._context_manager = None
|
||||
self._context_manager_initialized = True
|
||||
return None
|
||||
|
||||
if self._context_param is True:
|
||||
# Enable with safe defaults
|
||||
self._context_manager = ContextManager(
|
||||
model=self.llm if isinstance(self.llm, str) else "gpt-4o-mini",
|
||||
agent_name=self.name or "Agent",
|
||||
)
|
||||
elif isinstance(self._context_param, ManagerConfig):
|
||||
# Use provided config
|
||||
self._context_manager = ContextManager(
|
||||
model=self.llm if isinstance(self.llm, str) else "gpt-4o-mini",
|
||||
config=self._context_param,
|
||||
agent_name=self.name or "Agent",
|
||||
)
|
||||
elif hasattr(self._context_param, 'process'):
|
||||
# Already a ContextManager instance
|
||||
self._context_manager = self._context_param
|
||||
else:
|
||||
# Unknown type, disable
|
||||
self._context_manager = None
|
||||
|
||||
self._context_manager_initialized = True
|
||||
return self._context_manager
|
||||
|
||||
@context_manager.setter
|
||||
def context_manager(self, value):
|
||||
"""Set context manager directly."""
|
||||
self._context_manager = value
|
||||
self._context_manager_initialized = True
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
"""Lazily initialize Rich Console only when needed."""
|
||||
@@ -742,17 +1015,6 @@ Your Goal: {self.goal}
|
||||
self._console = Console()
|
||||
return self._console
|
||||
|
||||
@property
|
||||
def fast_context_path(self):
|
||||
"""Lazily resolve fast_context_path - avoids os.getcwd() on every init."""
|
||||
if self._fast_context_path is None:
|
||||
self._fast_context_path = os.getcwd()
|
||||
return self._fast_context_path
|
||||
|
||||
@fast_context_path.setter
|
||||
def fast_context_path(self, value):
|
||||
self._fast_context_path = value
|
||||
|
||||
@property
|
||||
def skill_manager(self):
|
||||
"""Lazily initialize SkillManager only when skills are accessed."""
|
||||
@@ -939,57 +1201,6 @@ Your Goal: {self.goal}
|
||||
|
||||
return supports_prompt_caching(model_name)
|
||||
|
||||
@property
|
||||
def fast_context(self):
|
||||
"""Lazily initialize FastContext instance when needed.
|
||||
|
||||
Returns:
|
||||
FastContext instance or None if not enabled
|
||||
"""
|
||||
if not self.fast_context_enabled:
|
||||
return None
|
||||
|
||||
if self._fast_context_instance is None:
|
||||
try:
|
||||
from ..context.fast import FastContext
|
||||
self._fast_context_instance = FastContext(
|
||||
workspace_path=self.fast_context_path,
|
||||
model=self.fast_context_model,
|
||||
max_turns=self.fast_context_max_turns,
|
||||
max_parallel=self.fast_context_parallelism,
|
||||
timeout=self.fast_context_timeout,
|
||||
verbose=self.verbose
|
||||
)
|
||||
except ImportError:
|
||||
logging.warning("FastContext not available")
|
||||
return None
|
||||
|
||||
return self._fast_context_instance
|
||||
|
||||
def delegate_to_fast_context(self, query: str) -> Optional[str]:
|
||||
"""Delegate a code search query to FastContext subagent.
|
||||
|
||||
This method uses the FastContext subagent to rapidly search
|
||||
the codebase and return relevant context.
|
||||
|
||||
Args:
|
||||
query: Natural language search query
|
||||
|
||||
Returns:
|
||||
Formatted context string or None if FastContext not available
|
||||
"""
|
||||
if not self.fast_context_enabled or self.fast_context is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
result = self.fast_context.search(query)
|
||||
if result.total_files > 0:
|
||||
return self.fast_context.get_context_for_agent(query)
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.warning(f"FastContext search failed: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def rules_manager(self):
|
||||
"""
|
||||
@@ -1358,7 +1569,14 @@ Your Goal: {self.goal}
|
||||
return "", None
|
||||
|
||||
if isinstance(search_results, dict) and 'results' in search_results:
|
||||
knowledge_content = "\n".join([result['memory'] for result in search_results['results']])
|
||||
# CRITICAL: Handle None results and get 'memory' field safely
|
||||
parts = []
|
||||
for result in search_results['results']:
|
||||
if result is not None:
|
||||
memory = result.get('memory', '') or ''
|
||||
if memory:
|
||||
parts.append(memory)
|
||||
knowledge_content = "\n".join(parts)
|
||||
else:
|
||||
knowledge_content = "\n".join(search_results) if isinstance(search_results, list) else str(search_results)
|
||||
|
||||
@@ -3504,7 +3722,7 @@ Write the complete compiled report:"""
|
||||
|
||||
def start(self, prompt: str, **kwargs):
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
# Load history from past sessions if history_in_context is set
|
||||
# Load history from past sessions (now handled via context= param)
|
||||
self._load_history_context()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
@@ -3524,37 +3742,13 @@ Write the complete compiled report:"""
|
||||
return result
|
||||
|
||||
def _load_history_context(self):
|
||||
"""Load history from past sessions into context if history_in_context is set."""
|
||||
if not self.history_in_context or not self._memory_instance:
|
||||
return
|
||||
"""Load history from past sessions into context.
|
||||
|
||||
try:
|
||||
sessions = self._memory_instance.list_sessions()
|
||||
if not sessions:
|
||||
return
|
||||
|
||||
# Get the last N sessions
|
||||
sessions_to_load = sessions[:self.history_in_context]
|
||||
|
||||
for session_info in reversed(sessions_to_load):
|
||||
try:
|
||||
session_data = self._memory_instance.resume_session(session_info["name"])
|
||||
history = session_data.get("conversation_history", [])
|
||||
|
||||
# Add history to chat_history with a marker
|
||||
for msg in history:
|
||||
if msg not in self.chat_history:
|
||||
# Mark as from history to avoid duplication
|
||||
msg_copy = msg.copy()
|
||||
msg_copy["_from_history"] = True
|
||||
self.chat_history.append(msg_copy)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if sessions_to_load:
|
||||
logging.debug(f"Loaded history from {len(sessions_to_load)} past session(s)")
|
||||
except Exception as e:
|
||||
logging.debug(f"Error loading history context: {e}")
|
||||
Note: This functionality is now handled via context= param with ManagerConfig.
|
||||
This method is kept for backward compatibility but is a no-op.
|
||||
Use context=ManagerConfig(history_sessions=N) to load past sessions.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _auto_save_session(self):
|
||||
"""Auto-save session if auto_save is enabled."""
|
||||
|
||||
@@ -127,7 +127,34 @@ def process_task_context(context_item, verbose=0, user_id=None):
|
||||
return str(context_item) # Fallback for unknown types
|
||||
|
||||
class PraisonAIAgents:
|
||||
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None, memory=False, memory_config=None, embedder=None, user_id=None, max_iter=10, stream=True, name: Optional[str] = None, planning: bool = False, planning_llm: Optional[str] = None, auto_approve_plan: bool = False, planning_tools: Optional[List] = None, planning_reasoning: bool = False, on_task_start: Optional[Callable] = None, on_task_complete: Optional[Callable] = None, variables: Optional[Dict[str, Any]] = None):
|
||||
def __init__(
|
||||
self,
|
||||
agents,
|
||||
tasks=None,
|
||||
verbose=0,
|
||||
completion_checker=None,
|
||||
max_retries=5,
|
||||
process="sequential",
|
||||
manager_llm=None,
|
||||
# Consolidated feature params (agent-centric API)
|
||||
memory: Optional[Any] = False, # Union[bool, MemoryConfig]
|
||||
planning: Optional[Any] = False, # Union[bool, PlanningConfig]
|
||||
context: Optional[Any] = False, # Union[bool, ManagerConfig, ContextManager]
|
||||
# Legacy params (kept for backward compatibility)
|
||||
memory_config=None,
|
||||
embedder=None,
|
||||
user_id=None,
|
||||
max_iter=10,
|
||||
stream=True,
|
||||
name: Optional[str] = None,
|
||||
planning_llm: Optional[str] = None,
|
||||
auto_approve_plan: bool = False,
|
||||
planning_tools: Optional[List] = None,
|
||||
planning_reasoning: bool = False,
|
||||
on_task_start: Optional[Callable] = None,
|
||||
on_task_complete: Optional[Callable] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
# Add check at the start if memory is requested
|
||||
if memory:
|
||||
try:
|
||||
@@ -218,6 +245,11 @@ class PraisonAIAgents:
|
||||
|
||||
self._state = {} # Add state storage at PraisonAIAgents level
|
||||
|
||||
# Context management (single param for all context features)
|
||||
self._context_param = context # Store raw param for lazy init
|
||||
self._context_manager = None # Lazy initialized on first use
|
||||
self._context_manager_initialized = False
|
||||
|
||||
# Initialize planning mode
|
||||
self.planning = planning
|
||||
self.planning_llm = planning_llm or "gpt-4o-mini"
|
||||
@@ -296,6 +328,51 @@ class PraisonAIAgents:
|
||||
cleaned = cleaned[:-3].strip()
|
||||
return cleaned
|
||||
|
||||
@property
|
||||
def context_manager(self):
|
||||
"""
|
||||
ContextManager instance for unified context management across all agents.
|
||||
|
||||
Lazy initialized on first access when context=True or context=ManagerConfig.
|
||||
Returns None when context=False (zero overhead).
|
||||
|
||||
For multi-agent scenarios, uses MultiAgentContextManager for per-agent isolation.
|
||||
"""
|
||||
if self._context_manager_initialized:
|
||||
return self._context_manager
|
||||
|
||||
# Initialize based on context param type
|
||||
if self._context_param is False or self._context_param is None:
|
||||
# Zero overhead - no context management
|
||||
self._context_manager = None
|
||||
self._context_manager_initialized = True
|
||||
return None
|
||||
|
||||
# Lazy import to avoid overhead when not used
|
||||
try:
|
||||
from ..context import MultiAgentContextManager, ManagerConfig
|
||||
except ImportError:
|
||||
# Context module not available
|
||||
self._context_manager = None
|
||||
self._context_manager_initialized = True
|
||||
return None
|
||||
|
||||
if self._context_param is True:
|
||||
# Enable with safe defaults for multi-agent
|
||||
self._context_manager = MultiAgentContextManager()
|
||||
elif isinstance(self._context_param, ManagerConfig):
|
||||
# Use provided config for all agents
|
||||
self._context_manager = MultiAgentContextManager(config=self._context_param)
|
||||
elif hasattr(self._context_param, 'get_agent_manager'):
|
||||
# Already a MultiAgentContextManager instance
|
||||
self._context_manager = self._context_param
|
||||
else:
|
||||
# Unknown type, disable
|
||||
self._context_manager = None
|
||||
|
||||
self._context_manager_initialized = True
|
||||
return self._context_manager
|
||||
|
||||
def default_completion_checker(self, task, agent_output):
|
||||
if task.output_json and task.result and task.result.json_dict:
|
||||
return True
|
||||
|
||||
@@ -56,7 +56,6 @@ class AutoAgents(PraisonAIAgents):
|
||||
min_reflect: int = 1,
|
||||
llm: Optional[str] = None,
|
||||
function_calling_llm: Optional[str] = None,
|
||||
respect_context_window: bool = True,
|
||||
code_execution_mode: str = "safe",
|
||||
embedder_config: Optional[Dict[str, Any]] = None,
|
||||
knowledge_sources: Optional[List[Any]] = None,
|
||||
@@ -94,7 +93,6 @@ class AutoAgents(PraisonAIAgents):
|
||||
self.min_reflect = min_reflect
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.respect_context_window = respect_context_window
|
||||
self.code_execution_mode = code_execution_mode
|
||||
self.embedder_config = embedder_config
|
||||
self.knowledge_sources = knowledge_sources
|
||||
@@ -446,7 +444,6 @@ DO NOT use strings for tasks. Each task MUST be a complete object with all four
|
||||
min_reflect=self.min_reflect,
|
||||
llm=self.llm,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
respect_context_window=self.respect_context_window,
|
||||
code_execution_mode=self.code_execution_mode,
|
||||
embedder_config=self.embedder_config,
|
||||
knowledge=self.knowledge_sources,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Configuration Module for PraisonAI Agents.
|
||||
|
||||
Provides feature configuration classes for agent-centric API:
|
||||
- MemoryConfig: Memory and session management
|
||||
- KnowledgeConfig: RAG and knowledge retrieval
|
||||
- PlanningConfig: Planning mode settings
|
||||
- ReflectionConfig: Self-reflection settings
|
||||
- GuardrailConfig: Safety and validation
|
||||
- WebConfig: Web search and fetch
|
||||
|
||||
All imports are lazy-loaded for zero performance impact.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"MemoryBackend",
|
||||
"ChunkingStrategy",
|
||||
"GuardrailAction",
|
||||
"WebSearchProvider",
|
||||
# Config classes
|
||||
"MemoryConfig",
|
||||
"KnowledgeConfig",
|
||||
"PlanningConfig",
|
||||
"ReflectionConfig",
|
||||
"GuardrailConfig",
|
||||
"WebConfig",
|
||||
# Type aliases
|
||||
"MemoryParam",
|
||||
"KnowledgeParam",
|
||||
"PlanningParam",
|
||||
"ReflectionParam",
|
||||
"GuardrailParam",
|
||||
"WebParam",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load config classes."""
|
||||
if name in __all__:
|
||||
from . import feature_configs
|
||||
return getattr(feature_configs, name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Feature Configuration Classes for PraisonAI Agents.
|
||||
|
||||
Provides dataclasses for consolidated feature configuration:
|
||||
- MemoryConfig: Memory and session management
|
||||
- KnowledgeConfig: RAG and knowledge retrieval
|
||||
- PlanningConfig: Planning mode settings
|
||||
- ReflectionConfig: Self-reflection settings
|
||||
- GuardrailConfig: Safety and validation
|
||||
- WebConfig: Web search and fetch
|
||||
|
||||
All configs follow the agent-centric pattern:
|
||||
- False: Feature disabled (zero overhead)
|
||||
- True: Feature enabled with safe defaults
|
||||
- Config: Custom configuration
|
||||
- Instance: Pre-configured manager/engine
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent, MemoryConfig, KnowledgeConfig
|
||||
|
||||
# Simple enable
|
||||
agent = Agent(instructions="...", memory=True)
|
||||
|
||||
# With config
|
||||
agent = Agent(
|
||||
instructions="...",
|
||||
memory=MemoryConfig(backend="redis", user_id="user123"),
|
||||
knowledge=KnowledgeConfig(sources=["docs/"], rerank=True),
|
||||
)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Any, Optional, Callable, Tuple, Union
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MemoryBackend(str, Enum):
|
||||
"""Memory storage backends."""
|
||||
FILE = "file"
|
||||
SQLITE = "sqlite"
|
||||
REDIS = "redis"
|
||||
POSTGRES = "postgres"
|
||||
MEM0 = "mem0"
|
||||
MONGODB = "mongodb"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryConfig:
|
||||
"""
|
||||
Configuration for agent memory and session management.
|
||||
|
||||
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db
|
||||
|
||||
Usage:
|
||||
# Simple enable (uses FileMemory)
|
||||
Agent(memory=True)
|
||||
|
||||
# With backend
|
||||
Agent(memory=MemoryConfig(backend="redis"))
|
||||
|
||||
# Full config
|
||||
Agent(memory=MemoryConfig(
|
||||
backend="sqlite",
|
||||
user_id="user123",
|
||||
session_id="session456",
|
||||
auto_memory=True,
|
||||
))
|
||||
"""
|
||||
# Backend selection
|
||||
backend: Union[str, MemoryBackend] = MemoryBackend.FILE
|
||||
|
||||
# Session identification
|
||||
user_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
|
||||
# Auto-memory extraction
|
||||
auto_memory: bool = False
|
||||
|
||||
# Claude-specific memory (for Anthropic models)
|
||||
claude_memory: bool = False
|
||||
|
||||
# Database adapter (for advanced use)
|
||||
db: Optional[Any] = None
|
||||
|
||||
# Memory configuration dict (for provider-specific settings)
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"backend": self.backend.value if isinstance(self.backend, MemoryBackend) else self.backend,
|
||||
"user_id": self.user_id,
|
||||
"session_id": self.session_id,
|
||||
"auto_memory": self.auto_memory,
|
||||
"claude_memory": self.claude_memory,
|
||||
"config": self.config,
|
||||
}
|
||||
|
||||
|
||||
class ChunkingStrategy(str, Enum):
|
||||
"""Knowledge chunking strategies."""
|
||||
FIXED = "fixed"
|
||||
SEMANTIC = "semantic"
|
||||
SENTENCE = "sentence"
|
||||
PARAGRAPH = "paragraph"
|
||||
|
||||
|
||||
@dataclass
|
||||
class KnowledgeConfig:
|
||||
"""
|
||||
Configuration for RAG and knowledge retrieval.
|
||||
|
||||
Consolidates: knowledge, retrieval_config, knowledge_config, rag_config, embedder_config
|
||||
|
||||
Usage:
|
||||
# Simple enable with sources
|
||||
Agent(knowledge=["docs/", "data.pdf"])
|
||||
|
||||
# With config
|
||||
Agent(knowledge=KnowledgeConfig(
|
||||
sources=["docs/"],
|
||||
embedder="openai",
|
||||
chunking_strategy="semantic",
|
||||
retrieval_k=5,
|
||||
rerank=True,
|
||||
))
|
||||
"""
|
||||
# Knowledge sources (files, directories, URLs)
|
||||
sources: List[str] = field(default_factory=list)
|
||||
|
||||
# Embedder configuration
|
||||
embedder: str = "openai"
|
||||
embedder_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Chunking
|
||||
chunking_strategy: Union[str, ChunkingStrategy] = ChunkingStrategy.SEMANTIC
|
||||
chunk_size: int = 1000
|
||||
chunk_overlap: int = 200
|
||||
|
||||
# Retrieval
|
||||
retrieval_k: int = 5
|
||||
retrieval_threshold: float = 0.0
|
||||
|
||||
# Reranking
|
||||
rerank: bool = False
|
||||
rerank_model: Optional[str] = None
|
||||
|
||||
# Auto-retrieval (inject context automatically)
|
||||
auto_retrieve: bool = True
|
||||
|
||||
# Full config dict (for advanced use)
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"sources": self.sources,
|
||||
"embedder": self.embedder,
|
||||
"embedder_config": self.embedder_config,
|
||||
"chunking_strategy": self.chunking_strategy.value if isinstance(self.chunking_strategy, ChunkingStrategy) else self.chunking_strategy,
|
||||
"chunk_size": self.chunk_size,
|
||||
"chunk_overlap": self.chunk_overlap,
|
||||
"retrieval_k": self.retrieval_k,
|
||||
"retrieval_threshold": self.retrieval_threshold,
|
||||
"rerank": self.rerank,
|
||||
"rerank_model": self.rerank_model,
|
||||
"auto_retrieve": self.auto_retrieve,
|
||||
"config": self.config,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanningConfig:
|
||||
"""
|
||||
Configuration for planning mode.
|
||||
|
||||
Consolidates: planning, plan_mode, planning_tools, planning_reasoning, planning_llm
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(planning=True)
|
||||
|
||||
# With config
|
||||
Agent(planning=PlanningConfig(
|
||||
llm="gpt-4o",
|
||||
tools=[search_tool],
|
||||
reasoning=True,
|
||||
auto_approve=False,
|
||||
))
|
||||
"""
|
||||
# Planning LLM (if different from main)
|
||||
llm: Optional[str] = None
|
||||
|
||||
# Tools available during planning
|
||||
tools: Optional[List[Any]] = None
|
||||
|
||||
# Enable reasoning during planning
|
||||
reasoning: bool = False
|
||||
|
||||
# Auto-approve plans without user confirmation
|
||||
auto_approve: bool = False
|
||||
|
||||
# Read-only mode (plan_mode) - only read operations allowed
|
||||
read_only: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"llm": self.llm,
|
||||
"tools": [str(t) for t in (self.tools or [])],
|
||||
"reasoning": self.reasoning,
|
||||
"auto_approve": self.auto_approve,
|
||||
"read_only": self.read_only,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReflectionConfig:
|
||||
"""
|
||||
Configuration for self-reflection.
|
||||
|
||||
Consolidates: self_reflect, max_reflect, min_reflect, reflect_llm, reflect_prompt
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(reflection=True)
|
||||
|
||||
# With config
|
||||
Agent(reflection=ReflectionConfig(
|
||||
min_iterations=1,
|
||||
max_iterations=3,
|
||||
llm="gpt-4o",
|
||||
prompt="Evaluate your response for accuracy...",
|
||||
))
|
||||
"""
|
||||
# Iteration limits
|
||||
min_iterations: int = 1
|
||||
max_iterations: int = 3
|
||||
|
||||
# Reflection LLM (if different from main)
|
||||
llm: Optional[str] = None
|
||||
|
||||
# Custom reflection prompt
|
||||
prompt: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"min_iterations": self.min_iterations,
|
||||
"max_iterations": self.max_iterations,
|
||||
"llm": self.llm,
|
||||
"prompt": self.prompt,
|
||||
}
|
||||
|
||||
|
||||
class GuardrailAction(str, Enum):
|
||||
"""Action to take when guardrail fails."""
|
||||
RETRY = "retry"
|
||||
SKIP = "skip"
|
||||
RAISE = "raise"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GuardrailConfig:
|
||||
"""
|
||||
Configuration for guardrails and safety validation.
|
||||
|
||||
Consolidates: guardrail, max_guardrail_retries, policy
|
||||
|
||||
Usage:
|
||||
# With validator function
|
||||
Agent(guardrails=my_validator_fn)
|
||||
|
||||
# With config
|
||||
Agent(guardrails=GuardrailConfig(
|
||||
validator=my_validator_fn,
|
||||
max_retries=3,
|
||||
on_fail="retry",
|
||||
))
|
||||
|
||||
# With LLM-based validation
|
||||
Agent(guardrails=GuardrailConfig(
|
||||
llm_validator="Ensure response is helpful and safe",
|
||||
max_retries=2,
|
||||
))
|
||||
"""
|
||||
# Validator function: (TaskOutput) -> Tuple[bool, Any]
|
||||
validator: Optional[Callable[..., Tuple[bool, Any]]] = None
|
||||
|
||||
# LLM-based validation prompt
|
||||
llm_validator: Optional[str] = None
|
||||
|
||||
# Retry settings
|
||||
max_retries: int = 3
|
||||
on_fail: Union[str, GuardrailAction] = GuardrailAction.RETRY
|
||||
|
||||
# Policy engine (for advanced use)
|
||||
policy: Optional[Any] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"validator": str(self.validator) if self.validator else None,
|
||||
"llm_validator": self.llm_validator,
|
||||
"max_retries": self.max_retries,
|
||||
"on_fail": self.on_fail.value if isinstance(self.on_fail, GuardrailAction) else self.on_fail,
|
||||
}
|
||||
|
||||
|
||||
class WebSearchProvider(str, Enum):
|
||||
"""Web search providers."""
|
||||
DUCKDUCKGO = "duckduckgo"
|
||||
GOOGLE = "google"
|
||||
BING = "bing"
|
||||
TAVILY = "tavily"
|
||||
SERPER = "serper"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebConfig:
|
||||
"""
|
||||
Configuration for web search and fetch capabilities.
|
||||
|
||||
Consolidates: web_search, web_fetch
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(web=True)
|
||||
|
||||
# With config
|
||||
Agent(web=WebConfig(
|
||||
search=True,
|
||||
fetch=True,
|
||||
search_provider="duckduckgo",
|
||||
max_results=5,
|
||||
))
|
||||
"""
|
||||
# Enable web search
|
||||
search: bool = True
|
||||
|
||||
# Enable web fetch (retrieve full page content)
|
||||
fetch: bool = True
|
||||
|
||||
# Search provider
|
||||
search_provider: Union[str, WebSearchProvider] = WebSearchProvider.DUCKDUCKGO
|
||||
|
||||
# Search settings
|
||||
max_results: int = 5
|
||||
|
||||
# Search config dict (for provider-specific settings)
|
||||
search_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Fetch config dict
|
||||
fetch_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"search": self.search,
|
||||
"fetch": self.fetch,
|
||||
"search_provider": self.search_provider.value if isinstance(self.search_provider, WebSearchProvider) else self.search_provider,
|
||||
"max_results": self.max_results,
|
||||
"search_config": self.search_config,
|
||||
"fetch_config": self.fetch_config,
|
||||
}
|
||||
|
||||
|
||||
# Type aliases for Union types used in Agent.__init__
|
||||
MemoryParam = Union[bool, MemoryConfig, Any] # Any = MemoryManager instance
|
||||
KnowledgeParam = Union[bool, List[str], KnowledgeConfig, Any] # Any = KnowledgeBase instance
|
||||
PlanningParam = Union[bool, PlanningConfig]
|
||||
ReflectionParam = Union[bool, ReflectionConfig]
|
||||
GuardrailParam = Union[bool, Callable[..., Tuple[bool, Any]], GuardrailConfig, Any] # Any = GuardrailEngine
|
||||
WebParam = Union[bool, WebConfig]
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"MemoryBackend",
|
||||
"ChunkingStrategy",
|
||||
"GuardrailAction",
|
||||
"WebSearchProvider",
|
||||
# Config classes
|
||||
"MemoryConfig",
|
||||
"KnowledgeConfig",
|
||||
"PlanningConfig",
|
||||
"ReflectionConfig",
|
||||
"GuardrailConfig",
|
||||
"WebConfig",
|
||||
# Type aliases
|
||||
"MemoryParam",
|
||||
"KnowledgeParam",
|
||||
"PlanningParam",
|
||||
"ReflectionParam",
|
||||
"GuardrailParam",
|
||||
"WebParam",
|
||||
]
|
||||
@@ -41,7 +41,32 @@ Usage:
|
||||
monitor.snapshot(ledger=result.ledger, budget=budget, messages=result.messages)
|
||||
"""
|
||||
|
||||
def format_percent(value: float) -> str:
|
||||
"""
|
||||
Smart percentage formatting for context utilization display.
|
||||
|
||||
- For values < 0.1%: shows "<0.1%"
|
||||
- For values < 1%: shows 2 decimal places (e.g., "0.02%")
|
||||
- For values >= 1%: shows 1 decimal place (e.g., "5.3%")
|
||||
|
||||
Args:
|
||||
value: A ratio (0.0 to 1.0+), NOT already multiplied by 100
|
||||
|
||||
Returns:
|
||||
Formatted percentage string
|
||||
"""
|
||||
pct = value * 100
|
||||
if pct < 0.1 and pct > 0:
|
||||
return "<0.1%"
|
||||
elif pct < 1.0:
|
||||
return f"{pct:.2f}%"
|
||||
else:
|
||||
return f"{pct:.1f}%"
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Utilities
|
||||
"format_percent",
|
||||
# Models
|
||||
"ContextSegment",
|
||||
"ContextLedger",
|
||||
@@ -83,6 +108,23 @@ __all__ = [
|
||||
"redact_sensitive",
|
||||
"format_human_snapshot",
|
||||
"format_json_snapshot",
|
||||
"validate_monitor_path",
|
||||
"should_include_content",
|
||||
"load_ignore_patterns",
|
||||
# Manager Facade (NEW)
|
||||
"ContextManager",
|
||||
"MultiAgentContextManager",
|
||||
"create_context_manager",
|
||||
"ManagerConfig",
|
||||
"ContextPolicy",
|
||||
"EstimationMode",
|
||||
"ContextShareMode",
|
||||
"ToolShareMode",
|
||||
"OptimizationEvent",
|
||||
"OptimizationEventType",
|
||||
"EstimationMetrics",
|
||||
"PerToolBudget",
|
||||
"SnapshotHookData",
|
||||
# Fast Context (legacy)
|
||||
"FastContext",
|
||||
"FastContextResult",
|
||||
@@ -130,10 +172,19 @@ def __getattr__(name: str):
|
||||
|
||||
# Monitoring
|
||||
if name in ("ContextMonitor", "MultiAgentMonitor", "redact_sensitive",
|
||||
"format_human_snapshot", "format_json_snapshot"):
|
||||
"format_human_snapshot", "format_json_snapshot",
|
||||
"validate_monitor_path", "should_include_content", "load_ignore_patterns"):
|
||||
from . import monitor
|
||||
return getattr(monitor, name)
|
||||
|
||||
# Manager Facade
|
||||
if name in ("ContextManager", "MultiAgentContextManager", "create_context_manager",
|
||||
"ManagerConfig", "ContextPolicy", "EstimationMode", "ContextShareMode",
|
||||
"ToolShareMode", "OptimizationEvent", "OptimizationEventType",
|
||||
"EstimationMetrics", "PerToolBudget", "SnapshotHookData"):
|
||||
from . import manager
|
||||
return getattr(manager, name)
|
||||
|
||||
# Fast Context (legacy)
|
||||
if name == "FastContext":
|
||||
from praisonaiagents.context.fast.fast_context import FastContext
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FastContext - High-level API for fast parallel code search.
|
||||
|
||||
This module provides the main FastContext class that can be used
|
||||
standalone or integrated with the main Agent class.
|
||||
standalone or integrated with the main Agent class via context= param.
|
||||
|
||||
Example usage:
|
||||
# Standalone usage
|
||||
@@ -12,13 +12,13 @@ Example usage:
|
||||
result = fc.search("find authentication handlers")
|
||||
print(result.to_context_string())
|
||||
|
||||
# With Agent integration
|
||||
# With Agent integration via context= param
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.context import ManagerConfig
|
||||
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
fast_context=True,
|
||||
fast_context_path="/path/to/project"
|
||||
context=True, # Enable context management
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ Context Monitor for PraisonAI Agents.
|
||||
Writes runtime context to disk for inspection and debugging.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Literal
|
||||
from typing import Dict, List, Any, Optional, Literal, Tuple
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import json
|
||||
@@ -15,16 +15,42 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
def _format_pct(value: float) -> str:
|
||||
"""Smart percentage formatting: <0.1% for tiny, 2 decimals for <1%, 1 decimal otherwise."""
|
||||
if value < 0.1 and value > 0:
|
||||
return "<0.1%"
|
||||
elif value < 1.0:
|
||||
return f"{value:.2f}%"
|
||||
else:
|
||||
return f"{value:.1f}%"
|
||||
|
||||
|
||||
# Sensitive patterns for redaction
|
||||
SENSITIVE_PATTERNS = [
|
||||
r'sk-[a-zA-Z0-9]{20,}', # OpenAI API keys
|
||||
r'sk-proj-[a-zA-Z0-9_-]+', # OpenAI project keys
|
||||
# OpenAI API keys
|
||||
r'sk-[a-zA-Z0-9]{20,}',
|
||||
r'sk-proj-[a-zA-Z0-9_-]+',
|
||||
# Anthropic API keys
|
||||
r'sk-ant-[a-zA-Z0-9_-]+',
|
||||
r'sk-ant-api\d+-[a-zA-Z0-9_-]+',
|
||||
# Google API keys and tokens
|
||||
r'AIza[a-zA-Z0-9_-]{30,}', # Google API keys (30+ chars after prefix)
|
||||
r'ya29\.[a-zA-Z0-9_-]+', # Google OAuth tokens
|
||||
r'goog-[a-zA-Z0-9_-]{20,}', # Google service tokens
|
||||
# AWS credentials
|
||||
r'AKIA[0-9A-Z]{16}', # AWS access keys
|
||||
r'[a-zA-Z0-9]{32,}', # Generic long tokens (conservative)
|
||||
r'ASIA[0-9A-Z]{16}', # AWS temporary access keys
|
||||
r'aws_secret_access_key["\s:=]+["\']?[^"\'\s]+',
|
||||
# Azure credentials
|
||||
r'[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}', # Azure GUIDs
|
||||
# Generic patterns
|
||||
r'[a-zA-Z0-9]{40,}', # Generic long tokens (conservative)
|
||||
r'password["\s:=]+["\']?[^"\'\s]+', # Password patterns
|
||||
r'secret["\s:=]+["\']?[^"\'\s]+', # Secret patterns
|
||||
r'token["\s:=]+["\']?[^"\'\s]+', # Token patterns
|
||||
r'api[_-]?key["\s:=]+["\']?[^"\'\s]+', # API key patterns
|
||||
r'bearer\s+[a-zA-Z0-9_-]+', # Bearer tokens
|
||||
r'authorization["\s:=]+["\']?[^"\'\s]+', # Authorization headers
|
||||
]
|
||||
|
||||
|
||||
@@ -44,6 +70,153 @@ def redact_sensitive(text: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def validate_monitor_path(
|
||||
path: str,
|
||||
allow_absolute: bool = False,
|
||||
base_dir: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Validate monitor output path for security.
|
||||
|
||||
Prevents path traversal attacks and enforces path restrictions.
|
||||
|
||||
Args:
|
||||
path: Path to validate
|
||||
allow_absolute: Whether to allow absolute paths
|
||||
base_dir: Base directory for relative paths
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
from pathlib import Path as PathLib
|
||||
|
||||
path_obj = PathLib(path)
|
||||
|
||||
# Check for path traversal attempts
|
||||
if '..' in path_obj.parts:
|
||||
return False, "Path traversal (..) not allowed"
|
||||
|
||||
# Check absolute path restriction
|
||||
if path_obj.is_absolute() and not allow_absolute:
|
||||
return False, "Absolute paths not allowed (set allow_absolute_paths=True to override)"
|
||||
|
||||
# If base_dir specified, ensure path stays within it
|
||||
if base_dir:
|
||||
base = PathLib(base_dir).resolve()
|
||||
try:
|
||||
# For relative paths, resolve against base
|
||||
if not path_obj.is_absolute():
|
||||
full_path = (base / path_obj).resolve()
|
||||
else:
|
||||
full_path = path_obj.resolve()
|
||||
|
||||
# Check if resolved path is under base
|
||||
if not str(full_path).startswith(str(base)):
|
||||
return False, f"Path must be within {base_dir}"
|
||||
except Exception as e:
|
||||
return False, f"Path validation error: {e}"
|
||||
|
||||
# Check for suspicious patterns
|
||||
suspicious = ['/etc/', '/var/', '/usr/', '/root/', '/home/', '~']
|
||||
path_str = str(path_obj).lower()
|
||||
for pattern in suspicious:
|
||||
if pattern in path_str and not allow_absolute:
|
||||
return False, f"Suspicious path pattern: {pattern}"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def should_include_content(
|
||||
file_path: str,
|
||||
ignore_patterns: Optional[List[str]] = None,
|
||||
include_patterns: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if file content should be included based on ignore/include patterns.
|
||||
|
||||
Respects .praisonignore and .praisoninclude rules.
|
||||
|
||||
Args:
|
||||
file_path: Path to check
|
||||
ignore_patterns: Patterns to ignore (glob-style)
|
||||
include_patterns: Patterns to include (glob-style)
|
||||
|
||||
Returns:
|
||||
True if content should be included
|
||||
"""
|
||||
import fnmatch
|
||||
from pathlib import Path as PathLib
|
||||
|
||||
path = PathLib(file_path)
|
||||
name = path.name
|
||||
|
||||
# Default: include everything
|
||||
if not ignore_patterns and not include_patterns:
|
||||
return True
|
||||
|
||||
# Check include patterns first (whitelist)
|
||||
if include_patterns:
|
||||
included = False
|
||||
for pattern in include_patterns:
|
||||
if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(str(path), pattern):
|
||||
included = True
|
||||
break
|
||||
if not included:
|
||||
return False
|
||||
|
||||
# Check ignore patterns (blacklist)
|
||||
if ignore_patterns:
|
||||
for pattern in ignore_patterns:
|
||||
if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(str(path), pattern):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def load_ignore_patterns(base_dir: str = ".") -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Load ignore and include patterns from .praisonignore and .praisoninclude files.
|
||||
|
||||
Args:
|
||||
base_dir: Directory to search for pattern files
|
||||
|
||||
Returns:
|
||||
Tuple of (ignore_patterns, include_patterns)
|
||||
"""
|
||||
from pathlib import Path as PathLib
|
||||
|
||||
ignore_patterns = []
|
||||
include_patterns = []
|
||||
|
||||
base = PathLib(base_dir)
|
||||
|
||||
# Load .praisonignore
|
||||
ignore_file = base / ".praisonignore"
|
||||
if ignore_file.exists():
|
||||
try:
|
||||
content = ignore_file.read_text()
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
ignore_patterns.append(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Load .praisoninclude
|
||||
include_file = base / ".praisoninclude"
|
||||
if include_file.exists():
|
||||
try:
|
||||
content = include_file.read_text()
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
include_patterns.append(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ignore_patterns, include_patterns
|
||||
|
||||
|
||||
def format_human_snapshot(snapshot: ContextSnapshot, redact: bool = True) -> str:
|
||||
"""
|
||||
Format snapshot as human-readable text.
|
||||
@@ -95,10 +268,10 @@ def format_human_snapshot(snapshot: ContextSnapshot, redact: bool = True) -> str
|
||||
tokens = snapshot.ledger.get_segment(seg)
|
||||
budget = snapshot.budget.get_segment_budget(seg)
|
||||
pct = (tokens / budget * 100) if budget > 0 else 0
|
||||
lines.append(f"{name:<20} | {tokens:>10,} | {budget:>10,} | {pct:>7.1f}%")
|
||||
lines.append(f"{name:<20} | {tokens:>10,} | {budget:>10,} | {_format_pct(pct):>8}")
|
||||
|
||||
lines.append("-" * 20 + "-+-" + "-" * 10 + "-+-" + "-" * 10 + "-+-" + "-" * 8)
|
||||
lines.append(f"{'TOTAL':<20} | {snapshot.ledger.total:>10,} | {snapshot.budget.usable:>10,} | {snapshot.utilization * 100:>7.1f}%")
|
||||
lines.append(f"{'TOTAL':<20} | {snapshot.ledger.total:>10,} | {snapshot.budget.usable:>10,} | {_format_pct(snapshot.utilization * 100):>8}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
@@ -365,17 +538,17 @@ class ContextMonitor:
|
||||
utilization = ledger.total / budget.usable if budget.usable > 0 else 0
|
||||
|
||||
if utilization >= 1.0:
|
||||
warnings.append(f"CRITICAL: Context overflow! {utilization*100:.1f}% of usable budget")
|
||||
warnings.append(f"CRITICAL: Context overflow! {_format_pct(utilization*100)} of usable budget")
|
||||
elif utilization >= 0.9:
|
||||
warnings.append(f"WARNING: Context at {utilization*100:.1f}% of usable budget")
|
||||
warnings.append(f"WARNING: Context at {_format_pct(utilization*100)} of usable budget")
|
||||
elif utilization >= 0.8:
|
||||
warnings.append(f"NOTICE: Context at {utilization*100:.1f}% of usable budget")
|
||||
warnings.append(f"NOTICE: Context at {_format_pct(utilization*100)} of usable budget")
|
||||
|
||||
# Check individual segments
|
||||
if budget.tool_outputs > 0:
|
||||
tool_util = ledger.tool_outputs / budget.tool_outputs
|
||||
if tool_util >= 0.9:
|
||||
warnings.append(f"Tool outputs at {tool_util*100:.1f}% of budget")
|
||||
warnings.append(f"Tool outputs at {_format_pct(tool_util*100)} of budget")
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
@@ -16,6 +16,18 @@ from praisonaiagents.knowledge.chunking import Chunking
|
||||
|
||||
# Lazy loading for protocols and registries to avoid import overhead
|
||||
_LAZY_IMPORTS = {
|
||||
# Models and Protocols (new)
|
||||
"SearchResultItem": ("praisonaiagents.knowledge.models", "SearchResultItem"),
|
||||
"SearchResult": ("praisonaiagents.knowledge.models", "SearchResult"),
|
||||
"AddResult": ("praisonaiagents.knowledge.models", "AddResult"),
|
||||
"normalize_search_item": ("praisonaiagents.knowledge.models", "normalize_search_item"),
|
||||
"normalize_search_result": ("praisonaiagents.knowledge.models", "normalize_search_result"),
|
||||
"normalize_to_dict": ("praisonaiagents.knowledge.models", "normalize_to_dict"),
|
||||
"KnowledgeStoreProtocol": ("praisonaiagents.knowledge.protocols", "KnowledgeStoreProtocol"),
|
||||
"KnowledgeBackendError": ("praisonaiagents.knowledge.protocols", "KnowledgeBackendError"),
|
||||
"ScopeRequiredError": ("praisonaiagents.knowledge.protocols", "ScopeRequiredError"),
|
||||
"BackendNotAvailableError": ("praisonaiagents.knowledge.protocols", "BackendNotAvailableError"),
|
||||
|
||||
# Readers
|
||||
"Document": ("praisonaiagents.knowledge.readers", "Document"),
|
||||
"ReaderProtocol": ("praisonaiagents.knowledge.readers", "ReaderProtocol"),
|
||||
@@ -85,6 +97,17 @@ __all__ = [
|
||||
# Core
|
||||
"Knowledge",
|
||||
"Chunking",
|
||||
# Models and Protocols
|
||||
"SearchResultItem",
|
||||
"SearchResult",
|
||||
"AddResult",
|
||||
"normalize_search_item",
|
||||
"normalize_search_result",
|
||||
"normalize_to_dict",
|
||||
"KnowledgeStoreProtocol",
|
||||
"KnowledgeBackendError",
|
||||
"ScopeRequiredError",
|
||||
"BackendNotAvailableError",
|
||||
# Readers
|
||||
"Document",
|
||||
"ReaderProtocol",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Knowledge Store Adapters for PraisonAI Agents.
|
||||
|
||||
Provides adapter implementations for various knowledge backends.
|
||||
All adapters implement KnowledgeStoreProtocol.
|
||||
|
||||
Adapters are lazy-loaded to avoid importing heavy dependencies.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .mem0_adapter import Mem0Adapter
|
||||
|
||||
# Lazy loading for adapters
|
||||
_LAZY_IMPORTS = {
|
||||
"Mem0Adapter": ("praisonaiagents.knowledge.adapters.mem0_adapter", "Mem0Adapter"),
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load adapters."""
|
||||
if name in _LAZY_IMPORTS:
|
||||
module_path, attr_name = _LAZY_IMPORTS[name]
|
||||
import importlib
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, attr_name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def __dir__():
|
||||
"""List available attributes."""
|
||||
return list(_LAZY_IMPORTS.keys())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Mem0Adapter",
|
||||
]
|
||||
@@ -0,0 +1,459 @@
|
||||
"""
|
||||
Mem0 Adapter for PraisonAI Knowledge Store.
|
||||
|
||||
Implements KnowledgeStoreProtocol using mem0 as the backend.
|
||||
Handles all normalization to ensure metadata is ALWAYS a dict (never None).
|
||||
|
||||
LAZY IMPORT: mem0 is only imported when this adapter is instantiated.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..models import (
|
||||
AddResult,
|
||||
SearchResult,
|
||||
SearchResultItem,
|
||||
normalize_search_item,
|
||||
)
|
||||
from ..protocols import ScopeRequiredError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mem0Adapter:
|
||||
"""
|
||||
Adapter implementing KnowledgeStoreProtocol for mem0 backend.
|
||||
|
||||
Features:
|
||||
- Lazy imports mem0 only when instantiated
|
||||
- Normalizes all results (metadata None → {})
|
||||
- Enforces scope requirements (user_id/agent_id/run_id)
|
||||
- Maps mem0 field names to standard schema (memory → text)
|
||||
- Disables telemetry by default
|
||||
|
||||
Usage:
|
||||
adapter = Mem0Adapter(config={...})
|
||||
results = adapter.search("query", user_id="user123")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
disable_telemetry: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize Mem0 adapter.
|
||||
|
||||
Args:
|
||||
config: mem0 configuration dict
|
||||
disable_telemetry: Whether to disable mem0 telemetry (default True)
|
||||
"""
|
||||
self._config = config or {}
|
||||
self._memory = None
|
||||
self._disable_telemetry = disable_telemetry
|
||||
|
||||
# Disable telemetry before importing mem0
|
||||
if disable_telemetry:
|
||||
os.environ.setdefault("ANONYMIZED_TELEMETRY", "False")
|
||||
|
||||
@property
|
||||
def memory(self):
|
||||
"""Lazy load mem0 Memory instance."""
|
||||
if self._memory is None:
|
||||
self._memory = self._init_mem0()
|
||||
return self._memory
|
||||
|
||||
def _init_mem0(self):
|
||||
"""Initialize mem0 Memory with config."""
|
||||
try:
|
||||
from mem0 import Memory
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"mem0 is not installed. Install with: pip install mem0ai"
|
||||
)
|
||||
|
||||
if self._config:
|
||||
return Memory.from_config(self._config)
|
||||
return Memory()
|
||||
|
||||
def _check_scope(
|
||||
self,
|
||||
user_id: Optional[str],
|
||||
agent_id: Optional[str],
|
||||
run_id: Optional[str],
|
||||
operation: str = "operation",
|
||||
) -> None:
|
||||
"""
|
||||
Check that at least one scope identifier is provided.
|
||||
|
||||
mem0 requires at least one of user_id, agent_id, or run_id.
|
||||
|
||||
Raises:
|
||||
ScopeRequiredError: If no scope identifier is provided
|
||||
"""
|
||||
if not any([user_id, agent_id, run_id]):
|
||||
raise ScopeRequiredError(
|
||||
message=(
|
||||
f"mem0 {operation} requires at least one of 'user_id', "
|
||||
f"'agent_id', or 'run_id'. Please provide at least one "
|
||||
f"identifier to scope the {operation}."
|
||||
),
|
||||
backend="mem0",
|
||||
)
|
||||
|
||||
def _normalize_mem0_item(self, raw: Dict[str, Any]) -> SearchResultItem:
|
||||
"""
|
||||
Normalize a mem0 result item to SearchResultItem.
|
||||
|
||||
Handles mem0-specific field names and ensures metadata is dict.
|
||||
|
||||
Args:
|
||||
raw: Raw mem0 result dict
|
||||
|
||||
Returns:
|
||||
SearchResultItem with guaranteed schema
|
||||
"""
|
||||
if raw is None:
|
||||
return SearchResultItem()
|
||||
|
||||
# Use the canonical normalization function
|
||||
item = normalize_search_item(raw)
|
||||
|
||||
# mem0 specific: extract user_id/agent_id/run_id from top level
|
||||
# and add to metadata if present
|
||||
for key in ["user_id", "agent_id", "run_id"]:
|
||||
if key in raw and raw[key] is not None:
|
||||
item.metadata[key] = raw[key]
|
||||
|
||||
return item
|
||||
|
||||
def _normalize_mem0_results(self, raw: Any) -> SearchResult:
|
||||
"""
|
||||
Normalize mem0 search results to SearchResult.
|
||||
|
||||
Args:
|
||||
raw: Raw mem0 results (dict with 'results' key or list)
|
||||
|
||||
Returns:
|
||||
SearchResult with normalized items
|
||||
"""
|
||||
if raw is None:
|
||||
return SearchResult()
|
||||
|
||||
# Handle dict with 'results' key (standard mem0 format)
|
||||
if isinstance(raw, dict):
|
||||
items = raw.get("results", []) or []
|
||||
normalized_items = [
|
||||
self._normalize_mem0_item(item)
|
||||
for item in items
|
||||
if item is not None
|
||||
]
|
||||
return SearchResult(
|
||||
results=normalized_items,
|
||||
metadata=raw.get("relations", {}) if "relations" in raw else {},
|
||||
)
|
||||
|
||||
# Handle list of results
|
||||
if isinstance(raw, list):
|
||||
normalized_items = [
|
||||
self._normalize_mem0_item(item)
|
||||
for item in raw
|
||||
if item is not None
|
||||
]
|
||||
return SearchResult(results=normalized_items)
|
||||
|
||||
return SearchResult()
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
rerank: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Search for relevant content in mem0.
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
user_id: User identifier for scoping
|
||||
agent_id: Agent identifier for scoping
|
||||
run_id: Run identifier for scoping
|
||||
limit: Maximum number of results
|
||||
filters: Optional metadata filters
|
||||
rerank: Whether to use mem0 reranking (default True)
|
||||
**kwargs: Additional mem0 search options
|
||||
|
||||
Returns:
|
||||
SearchResult with normalized items (metadata always dict)
|
||||
|
||||
Raises:
|
||||
ScopeRequiredError: If no scope identifier is provided
|
||||
"""
|
||||
self._check_scope(user_id, agent_id, run_id, "search")
|
||||
|
||||
try:
|
||||
raw_results = self.memory.search(
|
||||
query=query,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
limit=limit,
|
||||
filters=filters,
|
||||
rerank=rerank,
|
||||
**kwargs,
|
||||
)
|
||||
return self._normalize_mem0_results(raw_results)
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 search failed: {e}")
|
||||
return SearchResult(metadata={"error": str(e)})
|
||||
|
||||
def add(
|
||||
self,
|
||||
content: Any,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AddResult:
|
||||
"""
|
||||
Add content to mem0.
|
||||
|
||||
Args:
|
||||
content: Content to add (string or messages list)
|
||||
user_id: User identifier for scoping
|
||||
agent_id: Agent identifier for scoping
|
||||
run_id: Run identifier for scoping
|
||||
metadata: Optional metadata to attach
|
||||
**kwargs: Additional mem0 add options
|
||||
|
||||
Returns:
|
||||
AddResult with operation status
|
||||
|
||||
Raises:
|
||||
ScopeRequiredError: If no scope identifier is provided
|
||||
"""
|
||||
self._check_scope(user_id, agent_id, run_id, "add")
|
||||
|
||||
try:
|
||||
# Convert content to messages format for mem0
|
||||
if isinstance(content, str):
|
||||
messages = [{"role": "user", "content": content}]
|
||||
elif isinstance(content, list):
|
||||
messages = content
|
||||
else:
|
||||
messages = [{"role": "user", "content": str(content)}]
|
||||
|
||||
result = self.memory.add(
|
||||
messages=messages,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
metadata=metadata,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Extract ID from result
|
||||
result_id = ""
|
||||
if isinstance(result, dict):
|
||||
result_id = result.get("id", "")
|
||||
elif isinstance(result, list) and result:
|
||||
result_id = result[0].get("id", "") if isinstance(result[0], dict) else ""
|
||||
|
||||
return AddResult(
|
||||
id=str(result_id),
|
||||
success=True,
|
||||
message="Content added successfully",
|
||||
metadata={"raw_result": result} if result else {},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"mem0 add failed: {e}")
|
||||
return AddResult(
|
||||
success=False,
|
||||
message=str(e),
|
||||
metadata={"error": str(e)},
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
item_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Optional[SearchResultItem]:
|
||||
"""
|
||||
Get a specific item by ID from mem0.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to retrieve
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
SearchResultItem if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
raw = self.memory.get(item_id)
|
||||
if raw is None:
|
||||
return None
|
||||
return self._normalize_mem0_item(raw)
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 get failed: {e}")
|
||||
return None
|
||||
|
||||
def get_all(
|
||||
self,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
**kwargs: Any,
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Get all items from mem0, optionally filtered by scope.
|
||||
|
||||
Args:
|
||||
user_id: User identifier for scoping
|
||||
agent_id: Agent identifier for scoping
|
||||
run_id: Run identifier for scoping
|
||||
limit: Maximum number of results
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
SearchResult with all matching items
|
||||
|
||||
Raises:
|
||||
ScopeRequiredError: If no scope identifier is provided
|
||||
"""
|
||||
self._check_scope(user_id, agent_id, run_id, "get_all")
|
||||
|
||||
try:
|
||||
raw_results = self.memory.get_all(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
**kwargs,
|
||||
)
|
||||
return self._normalize_mem0_results(raw_results)
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 get_all failed: {e}")
|
||||
return SearchResult(metadata={"error": str(e)})
|
||||
|
||||
def update(
|
||||
self,
|
||||
item_id: str,
|
||||
content: Any,
|
||||
**kwargs: Any,
|
||||
) -> AddResult:
|
||||
"""
|
||||
Update an existing item in mem0.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to update
|
||||
content: New content
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
AddResult with operation status
|
||||
"""
|
||||
try:
|
||||
result = self.memory.update(item_id, str(content))
|
||||
return AddResult(
|
||||
id=item_id,
|
||||
success=True,
|
||||
message=result.get("message", "Updated successfully") if isinstance(result, dict) else "Updated",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"mem0 update failed: {e}")
|
||||
return AddResult(
|
||||
id=item_id,
|
||||
success=False,
|
||||
message=str(e),
|
||||
metadata={"error": str(e)},
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
item_id: str,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete an item by ID from mem0.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to delete
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
True if deleted, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.memory.delete(item_id)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 delete failed: {e}")
|
||||
return False
|
||||
|
||||
def delete_all(
|
||||
self,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete all items matching scope from mem0.
|
||||
|
||||
Args:
|
||||
user_id: User identifier for scoping
|
||||
agent_id: Agent identifier for scoping
|
||||
run_id: Run identifier for scoping
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
True if operation succeeded
|
||||
|
||||
Raises:
|
||||
ScopeRequiredError: If no scope identifier is provided
|
||||
"""
|
||||
self._check_scope(user_id, agent_id, run_id, "delete_all")
|
||||
|
||||
try:
|
||||
self.memory.delete_all(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 delete_all failed: {e}")
|
||||
return False
|
||||
|
||||
def history(
|
||||
self,
|
||||
item_id: str,
|
||||
**kwargs: Any,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get history of changes for an item.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
List of history entries
|
||||
"""
|
||||
try:
|
||||
return self.memory.history(item_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"mem0 history failed: {e}")
|
||||
return []
|
||||
@@ -0,0 +1,439 @@
|
||||
"""
|
||||
Incremental Indexing Infrastructure for PraisonAI Agents.
|
||||
|
||||
Provides:
|
||||
- CorpusStats: Statistics about indexed corpus
|
||||
- IndexResult: Result of indexing operation
|
||||
- IgnoreMatcher: .praisonignore pattern matching
|
||||
- FileTracker: File hash/mtime tracking for incremental updates
|
||||
|
||||
No heavy imports - only stdlib.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
# Strategy thresholds based on corpus size
|
||||
STRATEGY_THRESHOLDS = {
|
||||
10: "direct", # < 10 files: load all
|
||||
100: "basic", # < 100 files: semantic search only
|
||||
1000: "hybrid", # < 1000 files: keyword + semantic
|
||||
10000: "reranked", # < 10000 files: hybrid + rerank
|
||||
100000: "compressed", # < 100000 files: reranked + compression
|
||||
}
|
||||
DEFAULT_STRATEGY = "hierarchical" # > 100000 files
|
||||
|
||||
|
||||
def estimate_tokens_simple(text: str) -> int:
|
||||
"""Simple token estimation (~4 chars per token)."""
|
||||
if not text:
|
||||
return 0
|
||||
return len(text) // 4 + 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class CorpusStats:
|
||||
"""
|
||||
Statistics about an indexed corpus.
|
||||
|
||||
Used for strategy selection and monitoring.
|
||||
|
||||
Attributes:
|
||||
file_count: Number of files in corpus
|
||||
chunk_count: Number of chunks created
|
||||
total_tokens: Estimated total tokens
|
||||
indexed_at: Timestamp of last indexing
|
||||
path: Path to corpus root
|
||||
strategy_recommendation: Recommended retrieval strategy
|
||||
"""
|
||||
file_count: int = 0
|
||||
chunk_count: int = 0
|
||||
total_tokens: int = 0
|
||||
indexed_at: Optional[str] = None
|
||||
path: Optional[str] = None
|
||||
|
||||
@property
|
||||
def strategy_recommendation(self) -> str:
|
||||
"""Recommend retrieval strategy based on corpus size."""
|
||||
for threshold, strategy in sorted(STRATEGY_THRESHOLDS.items()):
|
||||
if self.file_count < threshold:
|
||||
return strategy
|
||||
return DEFAULT_STRATEGY
|
||||
|
||||
@classmethod
|
||||
def from_directory(
|
||||
cls,
|
||||
path: str,
|
||||
extensions: Optional[List[str]] = None,
|
||||
) -> "CorpusStats":
|
||||
"""
|
||||
Create CorpusStats from directory scan.
|
||||
|
||||
Args:
|
||||
path: Directory path to scan
|
||||
extensions: Optional list of extensions to include
|
||||
|
||||
Returns:
|
||||
CorpusStats with file count and token estimate
|
||||
"""
|
||||
if extensions is None:
|
||||
extensions = [
|
||||
".txt", ".md", ".py", ".js", ".ts", ".json", ".yaml", ".yml",
|
||||
".html", ".css", ".csv", ".xml", ".rst", ".pdf", ".docx",
|
||||
]
|
||||
|
||||
file_count = 0
|
||||
total_tokens = 0
|
||||
|
||||
for root, _, files in os.walk(path):
|
||||
for filename in files:
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext in extensions:
|
||||
file_count += 1
|
||||
filepath = os.path.join(root, filename)
|
||||
try:
|
||||
size = os.path.getsize(filepath)
|
||||
# Rough estimate: 4 chars per token
|
||||
total_tokens += size // 4
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return cls(
|
||||
file_count=file_count,
|
||||
total_tokens=total_tokens,
|
||||
path=path,
|
||||
indexed_at=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"file_count": self.file_count,
|
||||
"chunk_count": self.chunk_count,
|
||||
"total_tokens": self.total_tokens,
|
||||
"indexed_at": self.indexed_at,
|
||||
"path": self.path,
|
||||
"strategy_recommendation": self.strategy_recommendation,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "CorpusStats":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
file_count=data.get("file_count", 0),
|
||||
chunk_count=data.get("chunk_count", 0),
|
||||
total_tokens=data.get("total_tokens", 0),
|
||||
indexed_at=data.get("indexed_at"),
|
||||
path=data.get("path"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndexResult:
|
||||
"""
|
||||
Result of an indexing operation.
|
||||
|
||||
Attributes:
|
||||
success: Whether indexing succeeded
|
||||
files_indexed: Number of files indexed
|
||||
files_skipped: Number of files skipped (unchanged)
|
||||
chunks_created: Number of chunks created
|
||||
errors: List of error messages
|
||||
duration_seconds: Time taken for indexing
|
||||
corpus_stats: Stats about the indexed corpus
|
||||
"""
|
||||
success: bool = True
|
||||
files_indexed: int = 0
|
||||
files_skipped: int = 0
|
||||
chunks_created: int = 0
|
||||
errors: List[str] = field(default_factory=list)
|
||||
duration_seconds: float = 0.0
|
||||
corpus_stats: Optional[CorpusStats] = None
|
||||
|
||||
@property
|
||||
def total_files(self) -> int:
|
||||
"""Total files processed (indexed + skipped)."""
|
||||
return self.files_indexed + self.files_skipped
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"success": self.success,
|
||||
"files_indexed": self.files_indexed,
|
||||
"files_skipped": self.files_skipped,
|
||||
"chunks_created": self.chunks_created,
|
||||
"total_files": self.total_files,
|
||||
"errors": self.errors,
|
||||
"duration_seconds": self.duration_seconds,
|
||||
"corpus_stats": self.corpus_stats.to_dict() if self.corpus_stats else None,
|
||||
}
|
||||
|
||||
|
||||
class IgnoreMatcher:
|
||||
"""
|
||||
Matcher for .praisonignore patterns.
|
||||
|
||||
Supports gitignore-style patterns:
|
||||
- * matches any sequence of characters
|
||||
- ** matches any path segments
|
||||
- ! negates a pattern
|
||||
- # starts a comment
|
||||
"""
|
||||
|
||||
def __init__(self, patterns: Optional[List[str]] = None):
|
||||
"""
|
||||
Initialize with patterns.
|
||||
|
||||
Args:
|
||||
patterns: List of gitignore-style patterns
|
||||
"""
|
||||
self._include_patterns: List[str] = []
|
||||
self._exclude_patterns: List[str] = []
|
||||
|
||||
if patterns:
|
||||
for pattern in patterns:
|
||||
pattern = pattern.strip()
|
||||
if not pattern or pattern.startswith("#"):
|
||||
continue
|
||||
if pattern.startswith("!"):
|
||||
self._include_patterns.append(pattern[1:])
|
||||
else:
|
||||
self._exclude_patterns.append(pattern)
|
||||
|
||||
def should_ignore(self, path: str) -> bool:
|
||||
"""
|
||||
Check if path should be ignored.
|
||||
|
||||
Args:
|
||||
path: Path to check (relative or absolute)
|
||||
|
||||
Returns:
|
||||
True if path should be ignored
|
||||
"""
|
||||
# Normalize path
|
||||
path = path.replace("\\", "/")
|
||||
basename = os.path.basename(path)
|
||||
|
||||
# Check exclude patterns
|
||||
ignored = False
|
||||
for pattern in self._exclude_patterns:
|
||||
if self._matches(path, pattern) or self._matches(basename, pattern):
|
||||
ignored = True
|
||||
break
|
||||
|
||||
# Check include patterns (negation)
|
||||
if ignored:
|
||||
for pattern in self._include_patterns:
|
||||
if self._matches(path, pattern) or self._matches(basename, pattern):
|
||||
return False
|
||||
|
||||
return ignored
|
||||
|
||||
def _matches(self, path: str, pattern: str) -> bool:
|
||||
"""Check if path matches pattern."""
|
||||
# Handle ** for recursive matching
|
||||
if "**" in pattern:
|
||||
# Convert gitignore pattern to regex
|
||||
# First escape special regex chars except * and /
|
||||
regex_pattern = re.escape(pattern)
|
||||
# Restore * and convert to regex
|
||||
regex_pattern = regex_pattern.replace(r"\*\*", ".*")
|
||||
regex_pattern = regex_pattern.replace(r"\*", "[^/]*")
|
||||
# Match anywhere in path
|
||||
try:
|
||||
return bool(re.search(regex_pattern, path))
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
# Handle trailing slash for directories
|
||||
if pattern.endswith("/"):
|
||||
pattern = pattern[:-1]
|
||||
return path == pattern or path.startswith(pattern + "/") or ("/" + pattern + "/") in ("/" + path + "/")
|
||||
|
||||
# Standard fnmatch - check full path and basename
|
||||
if fnmatch.fnmatch(path, pattern):
|
||||
return True
|
||||
if fnmatch.fnmatch(os.path.basename(path), pattern):
|
||||
return True
|
||||
# Also check if pattern matches any path segment
|
||||
for segment in path.split("/"):
|
||||
if fnmatch.fnmatch(segment, pattern):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath: str) -> "IgnoreMatcher":
|
||||
"""
|
||||
Load patterns from file.
|
||||
|
||||
Args:
|
||||
filepath: Path to .praisonignore file
|
||||
|
||||
Returns:
|
||||
IgnoreMatcher with loaded patterns
|
||||
"""
|
||||
patterns = []
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
patterns = f.read().splitlines()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return cls(patterns=patterns)
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, directory: str) -> "IgnoreMatcher":
|
||||
"""
|
||||
Auto-detect and load .praisonignore from directory.
|
||||
|
||||
Also checks for .gitignore as fallback.
|
||||
|
||||
Args:
|
||||
directory: Directory to search
|
||||
|
||||
Returns:
|
||||
IgnoreMatcher with loaded patterns
|
||||
"""
|
||||
patterns = []
|
||||
|
||||
# Check for .praisonignore
|
||||
praison_ignore = os.path.join(directory, ".praisonignore")
|
||||
if os.path.exists(praison_ignore):
|
||||
try:
|
||||
with open(praison_ignore, "r", encoding="utf-8") as f:
|
||||
patterns.extend(f.read().splitlines())
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
# Also check .gitignore
|
||||
git_ignore = os.path.join(directory, ".gitignore")
|
||||
if os.path.exists(git_ignore):
|
||||
try:
|
||||
with open(git_ignore, "r", encoding="utf-8") as f:
|
||||
patterns.extend(f.read().splitlines())
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
return cls(patterns=patterns)
|
||||
|
||||
|
||||
class FileTracker:
|
||||
"""
|
||||
Track file hashes and modification times for incremental indexing.
|
||||
|
||||
Persists state to a JSON file for cross-session tracking.
|
||||
"""
|
||||
|
||||
def __init__(self, state_file: Optional[str] = None):
|
||||
"""
|
||||
Initialize tracker.
|
||||
|
||||
Args:
|
||||
state_file: Path to state file for persistence
|
||||
"""
|
||||
self._state_file = state_file
|
||||
self._tracked: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def get_file_info(self, filepath: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get file information (hash, mtime, size).
|
||||
|
||||
Args:
|
||||
filepath: Path to file
|
||||
|
||||
Returns:
|
||||
Dict with path, hash, mtime, size
|
||||
"""
|
||||
stat = os.stat(filepath)
|
||||
|
||||
# Calculate hash for small files, use mtime+size for large files
|
||||
if stat.st_size < 1024 * 1024: # 1MB
|
||||
with open(filepath, "rb") as f:
|
||||
file_hash = hashlib.md5(f.read()).hexdigest()
|
||||
else:
|
||||
# For large files, use mtime + size as proxy
|
||||
file_hash = f"{stat.st_mtime}:{stat.st_size}"
|
||||
|
||||
return {
|
||||
"path": filepath,
|
||||
"hash": file_hash,
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size,
|
||||
}
|
||||
|
||||
def mark_indexed(self, filepath: str, info: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Mark file as indexed.
|
||||
|
||||
Args:
|
||||
filepath: Path to file
|
||||
info: File info from get_file_info()
|
||||
"""
|
||||
self._tracked[filepath] = info
|
||||
|
||||
def has_changed(self, filepath: str) -> bool:
|
||||
"""
|
||||
Check if file has changed since last indexing.
|
||||
|
||||
Args:
|
||||
filepath: Path to file
|
||||
|
||||
Returns:
|
||||
True if file is new or changed
|
||||
"""
|
||||
if filepath not in self._tracked:
|
||||
return True
|
||||
|
||||
try:
|
||||
current = self.get_file_info(filepath)
|
||||
tracked = self._tracked[filepath]
|
||||
|
||||
# Compare hash
|
||||
if current["hash"] != tracked["hash"]:
|
||||
return True
|
||||
|
||||
# Compare mtime as backup
|
||||
if current["mtime"] != tracked["mtime"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
|
||||
def save(self) -> None:
|
||||
"""Save state to file."""
|
||||
if not self._state_file:
|
||||
return
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._state_file), exist_ok=True)
|
||||
with open(self._state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(self._tracked, f, indent=2)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load state from file."""
|
||||
if not self._state_file or not os.path.exists(self._state_file):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self._state_file, "r", encoding="utf-8") as f:
|
||||
self._tracked = json.load(f)
|
||||
except (OSError, IOError, json.JSONDecodeError):
|
||||
self._tracked = {}
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all tracked files."""
|
||||
self._tracked = {}
|
||||
if self._state_file and os.path.exists(self._state_file):
|
||||
try:
|
||||
os.remove(self._state_file)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -683,7 +683,7 @@ class Knowledge:
|
||||
return self._process_single_input(file_path, user_id, agent_id, run_id, metadata)
|
||||
|
||||
def _process_single_input(self, input_path, user_id=None, agent_id=None, run_id=None, metadata=None):
|
||||
"""Process a single input which can be a file path or URL."""
|
||||
"""Process a single input which can be a file path, directory, or URL."""
|
||||
try:
|
||||
# Define supported file extensions
|
||||
DOCUMENT_EXTENSIONS = {
|
||||
@@ -692,11 +692,44 @@ class Knowledge:
|
||||
'text': ('.txt', '.csv', '.json', '.xml', '.md', '.html', '.htm'),
|
||||
'archive': '.zip'
|
||||
}
|
||||
|
||||
# Get all supported extensions as a flat tuple
|
||||
all_extensions = []
|
||||
for exts in DOCUMENT_EXTENSIONS.values():
|
||||
if isinstance(exts, tuple):
|
||||
all_extensions.extend(exts)
|
||||
else:
|
||||
all_extensions.append(exts)
|
||||
all_extensions = tuple(all_extensions)
|
||||
|
||||
# Check if input is URL
|
||||
if isinstance(input_path, str) and (input_path.startswith('http://') or input_path.startswith('https://')):
|
||||
self._log(f"Processing URL: {input_path}")
|
||||
raise NotImplementedError("URL processing not yet implemented")
|
||||
|
||||
# CRITICAL FIX: Check if input is a directory - recursively process all files
|
||||
if os.path.isdir(input_path):
|
||||
self._log(f"Processing directory: {input_path}")
|
||||
all_results = []
|
||||
|
||||
# Walk through directory and process all supported files
|
||||
for root, dirs, files in os.walk(input_path):
|
||||
for filename in files:
|
||||
file_path = os.path.join(root, filename)
|
||||
# Only process files with supported extensions
|
||||
if filename.lower().endswith(all_extensions):
|
||||
try:
|
||||
result = self._process_single_input(
|
||||
file_path, user_id, agent_id, run_id, metadata
|
||||
)
|
||||
all_results.extend(result.get('results', []))
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process file {file_path}: {e}")
|
||||
|
||||
if not all_results:
|
||||
logger.warning(f"No supported files found in directory: {input_path}")
|
||||
|
||||
return {'results': all_results, 'relations': []}
|
||||
|
||||
# Check if input ends with any supported extension
|
||||
is_supported_file = any(input_path.lower().endswith(ext)
|
||||
@@ -770,4 +803,158 @@ class Knowledge:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing input {input_path}: {str(e)}", exc_info=True)
|
||||
raise
|
||||
raise
|
||||
|
||||
def index(
|
||||
self,
|
||||
path: str,
|
||||
incremental: bool = True,
|
||||
force: bool = False,
|
||||
include_glob: list = None,
|
||||
exclude_glob: list = None,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
):
|
||||
"""
|
||||
Index a directory or file for knowledge retrieval.
|
||||
|
||||
Supports incremental indexing - only changed files are re-indexed.
|
||||
|
||||
Args:
|
||||
path: Directory or file path to index
|
||||
incremental: If True, only index changed files (default: True)
|
||||
force: If True, re-index all files regardless of changes
|
||||
include_glob: List of glob patterns to include (e.g., ["*.py", "*.md"])
|
||||
exclude_glob: List of glob patterns to exclude (e.g., ["*.log", "test_*"])
|
||||
user_id: Optional user ID for scoping
|
||||
agent_id: Optional agent ID for scoping
|
||||
run_id: Optional run ID for scoping
|
||||
|
||||
Returns:
|
||||
IndexResult with indexing statistics
|
||||
"""
|
||||
from .indexing import IndexResult, CorpusStats, IgnoreMatcher, FileTracker
|
||||
import time as time_module
|
||||
import fnmatch as fnmatch_module
|
||||
|
||||
start_time = time_module.time()
|
||||
|
||||
result = IndexResult()
|
||||
|
||||
# Initialize file tracker for incremental indexing
|
||||
state_dir = os.path.join(os.path.dirname(path) if os.path.isfile(path) else path, ".praison")
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
state_file = os.path.join(state_dir, ".index_state.json")
|
||||
|
||||
tracker = FileTracker(state_file=state_file)
|
||||
if incremental and not force:
|
||||
tracker.load()
|
||||
|
||||
# Initialize ignore matcher
|
||||
ignore_matcher = IgnoreMatcher.from_directory(path if os.path.isdir(path) else os.path.dirname(path))
|
||||
|
||||
# Collect files to index
|
||||
files_to_index = []
|
||||
|
||||
if os.path.isfile(path):
|
||||
files_to_index = [path]
|
||||
else:
|
||||
for root, dirs, files in os.walk(path):
|
||||
# Skip hidden directories
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
|
||||
for filename in files:
|
||||
filepath = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(filepath, path)
|
||||
|
||||
# Check ignore patterns
|
||||
if ignore_matcher.should_ignore(rel_path):
|
||||
continue
|
||||
|
||||
# Check include patterns
|
||||
if include_glob:
|
||||
matched = False
|
||||
for pattern in include_glob:
|
||||
if fnmatch_module.fnmatch(filename, pattern):
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
continue
|
||||
|
||||
# Check exclude patterns
|
||||
if exclude_glob:
|
||||
excluded = False
|
||||
for pattern in exclude_glob:
|
||||
if fnmatch_module.fnmatch(filename, pattern) or fnmatch_module.fnmatch(rel_path, pattern):
|
||||
excluded = True
|
||||
break
|
||||
if excluded:
|
||||
continue
|
||||
|
||||
files_to_index.append(filepath)
|
||||
|
||||
# Index files
|
||||
total_chunks = 0
|
||||
for filepath in files_to_index:
|
||||
try:
|
||||
# Check if file has changed (for incremental indexing)
|
||||
if incremental and not force and not tracker.has_changed(filepath):
|
||||
result.files_skipped += 1
|
||||
continue
|
||||
|
||||
# Index the file
|
||||
add_result = self.add(
|
||||
filepath,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
|
||||
# Count chunks
|
||||
if add_result and isinstance(add_result, dict):
|
||||
chunks = len(add_result.get('results', []))
|
||||
total_chunks += chunks
|
||||
|
||||
# Mark as indexed
|
||||
file_info = tracker.get_file_info(filepath)
|
||||
tracker.mark_indexed(filepath, file_info)
|
||||
|
||||
result.files_indexed += 1
|
||||
|
||||
except Exception as e:
|
||||
result.errors.append(f"{filepath}: {str(e)}")
|
||||
|
||||
# Save tracker state
|
||||
if incremental:
|
||||
tracker.save()
|
||||
|
||||
# Calculate stats
|
||||
result.chunks_created = total_chunks
|
||||
result.duration_seconds = time_module.time() - start_time
|
||||
result.corpus_stats = CorpusStats(
|
||||
file_count=result.files_indexed + result.files_skipped,
|
||||
chunk_count=total_chunks,
|
||||
path=path,
|
||||
indexed_at=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
# Store corpus stats for later retrieval
|
||||
self._corpus_stats = result.corpus_stats
|
||||
|
||||
return result
|
||||
|
||||
def get_corpus_stats(self):
|
||||
"""
|
||||
Get statistics about the indexed corpus.
|
||||
|
||||
Returns:
|
||||
CorpusStats with file count, chunk count, and strategy recommendation
|
||||
"""
|
||||
from .indexing import CorpusStats
|
||||
|
||||
if hasattr(self, '_corpus_stats') and self._corpus_stats:
|
||||
return self._corpus_stats
|
||||
|
||||
# Return empty stats if not indexed
|
||||
return CorpusStats()
|
||||
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
Knowledge Models for PraisonAI Agents.
|
||||
|
||||
Typed dataclasses for knowledge retrieval results.
|
||||
These models guarantee metadata is ALWAYS a dict (never None).
|
||||
|
||||
No heavy imports - only stdlib.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResultItem:
|
||||
"""
|
||||
A single search result item with guaranteed schema.
|
||||
|
||||
CRITICAL: metadata is ALWAYS a dict, never None.
|
||||
This is enforced at construction time.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the result
|
||||
text: The text content (normalized from 'memory' field if needed)
|
||||
score: Relevance score (default 0.0)
|
||||
metadata: Additional metadata (ALWAYS dict, never None)
|
||||
source: Optional source identifier
|
||||
filename: Optional filename
|
||||
created_at: Optional creation timestamp
|
||||
updated_at: Optional update timestamp
|
||||
"""
|
||||
id: str = ""
|
||||
text: str = ""
|
||||
score: float = 0.0
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
source: Optional[str] = None
|
||||
filename: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Ensure metadata is never None."""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"text": self.text,
|
||||
"score": self.score,
|
||||
"metadata": self.metadata,
|
||||
"source": self.source,
|
||||
"filename": self.filename,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
"""
|
||||
Container for search results with guaranteed schema.
|
||||
|
||||
Attributes:
|
||||
results: List of SearchResultItem objects
|
||||
metadata: Additional metadata about the search (ALWAYS dict)
|
||||
query: The original query string
|
||||
total_count: Total number of results (may differ from len(results) if paginated)
|
||||
"""
|
||||
results: List[SearchResultItem] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
query: str = ""
|
||||
total_count: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Ensure metadata is never None."""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
if self.results is None:
|
||||
self.results = []
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format compatible with legacy code."""
|
||||
return {
|
||||
"results": [r.to_dict() if hasattr(r, 'to_dict') else r for r in self.results],
|
||||
"metadata": self.metadata,
|
||||
"query": self.query,
|
||||
"total_count": self.total_count or len(self.results),
|
||||
}
|
||||
|
||||
def to_legacy_format(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert to legacy format for backward compatibility.
|
||||
|
||||
Returns dict with 'results' key containing list of dicts.
|
||||
"""
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"id": r.id,
|
||||
"memory": r.text, # Legacy field name
|
||||
"text": r.text,
|
||||
"score": r.score,
|
||||
"metadata": r.metadata,
|
||||
"source": r.source,
|
||||
"filename": r.filename,
|
||||
"created_at": r.created_at,
|
||||
"updated_at": r.updated_at,
|
||||
}
|
||||
for r in self.results
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddResult:
|
||||
"""
|
||||
Result of adding content to knowledge store.
|
||||
|
||||
Attributes:
|
||||
id: ID of the added item
|
||||
success: Whether the operation succeeded
|
||||
message: Optional message about the operation
|
||||
metadata: Additional metadata (ALWAYS dict)
|
||||
"""
|
||||
id: str = ""
|
||||
success: bool = True
|
||||
message: str = ""
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Ensure metadata is never None."""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
|
||||
|
||||
def normalize_search_item(raw: Dict[str, Any]) -> SearchResultItem:
|
||||
"""
|
||||
Normalize a raw search result item to SearchResultItem.
|
||||
|
||||
This is the SINGLE canonical normalization function.
|
||||
Handles mem0 format, internal format, and any other variations.
|
||||
|
||||
CRITICAL: Ensures metadata is ALWAYS a dict, never None.
|
||||
|
||||
Args:
|
||||
raw: Raw result dict from any backend
|
||||
|
||||
Returns:
|
||||
SearchResultItem with guaranteed schema
|
||||
"""
|
||||
if raw is None:
|
||||
return SearchResultItem()
|
||||
|
||||
# Handle text field (mem0 uses 'memory', others use 'text')
|
||||
text = raw.get("text") or raw.get("memory", "") or ""
|
||||
|
||||
# Handle metadata - CRITICAL: ensure never None
|
||||
metadata = raw.get("metadata")
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
elif not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
|
||||
# Extract source/filename from metadata if not at top level
|
||||
source = raw.get("source") or metadata.get("source")
|
||||
filename = raw.get("filename") or metadata.get("filename")
|
||||
|
||||
return SearchResultItem(
|
||||
id=str(raw.get("id", "")),
|
||||
text=str(text),
|
||||
score=float(raw.get("score", 0.0) or 0.0),
|
||||
metadata=metadata,
|
||||
source=source,
|
||||
filename=filename,
|
||||
created_at=raw.get("created_at"),
|
||||
updated_at=raw.get("updated_at"),
|
||||
)
|
||||
|
||||
|
||||
def normalize_search_result(raw: Any) -> SearchResult:
|
||||
"""
|
||||
Normalize raw search results to SearchResult.
|
||||
|
||||
Handles various input formats:
|
||||
- dict with 'results' key (mem0 format)
|
||||
- list of results
|
||||
- single result dict
|
||||
- None
|
||||
|
||||
Args:
|
||||
raw: Raw search results from any backend
|
||||
|
||||
Returns:
|
||||
SearchResult with guaranteed schema
|
||||
"""
|
||||
if raw is None:
|
||||
return SearchResult()
|
||||
|
||||
# Handle dict with 'results' key
|
||||
if isinstance(raw, dict):
|
||||
if "results" in raw:
|
||||
items = raw.get("results", []) or []
|
||||
normalized_items = [
|
||||
normalize_search_item(item)
|
||||
for item in items
|
||||
if item is not None
|
||||
]
|
||||
return SearchResult(
|
||||
results=normalized_items,
|
||||
metadata=raw.get("metadata") or {},
|
||||
query=raw.get("query", ""),
|
||||
)
|
||||
else:
|
||||
# Single result dict
|
||||
return SearchResult(
|
||||
results=[normalize_search_item(raw)],
|
||||
metadata={},
|
||||
)
|
||||
|
||||
# Handle list of results
|
||||
if isinstance(raw, list):
|
||||
normalized_items = [
|
||||
normalize_search_item(item)
|
||||
for item in raw
|
||||
if item is not None
|
||||
]
|
||||
return SearchResult(results=normalized_items)
|
||||
|
||||
# Fallback
|
||||
return SearchResult()
|
||||
|
||||
|
||||
def normalize_to_dict(raw: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Normalize raw search results to legacy dict format.
|
||||
|
||||
Convenience function for code that expects dict format.
|
||||
Ensures all items have metadata as dict (never None).
|
||||
|
||||
Args:
|
||||
raw: Raw search results from any backend
|
||||
|
||||
Returns:
|
||||
Dict with 'results' key containing normalized items
|
||||
"""
|
||||
result = normalize_search_result(raw)
|
||||
return result.to_legacy_format()
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Knowledge Store Protocol for PraisonAI Agents.
|
||||
|
||||
Defines the interface that knowledge backends must implement.
|
||||
Uses typing.Protocol for structural subtyping (duck typing with type hints).
|
||||
|
||||
No heavy imports - only stdlib typing.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
||||
|
||||
from .models import AddResult, SearchResult, SearchResultItem
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class KnowledgeStoreProtocol(Protocol):
|
||||
"""
|
||||
Protocol defining the interface for knowledge store backends.
|
||||
|
||||
Implementations include:
|
||||
- Mem0Adapter (mem0 backend)
|
||||
- ChromaAdapter (direct chromadb)
|
||||
- InternalAdapter (built-in storage)
|
||||
|
||||
All implementations MUST:
|
||||
1. Return SearchResult/SearchResultItem with metadata as dict (never None)
|
||||
2. Handle identifier scoping (user_id/agent_id/run_id) appropriately
|
||||
3. Provide clear error messages for missing required parameters
|
||||
"""
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 10,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Search for relevant content.
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
user_id: Optional user identifier for scoping
|
||||
agent_id: Optional agent identifier for scoping
|
||||
run_id: Optional run identifier for scoping
|
||||
limit: Maximum number of results
|
||||
filters: Optional metadata filters
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
SearchResult with normalized items (metadata always dict)
|
||||
"""
|
||||
...
|
||||
|
||||
def add(
|
||||
self,
|
||||
content: Any,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AddResult:
|
||||
"""
|
||||
Add content to the knowledge store.
|
||||
|
||||
Args:
|
||||
content: Content to add (string, file path, or structured data)
|
||||
user_id: Optional user identifier for scoping
|
||||
agent_id: Optional agent identifier for scoping
|
||||
run_id: Optional run identifier for scoping
|
||||
metadata: Optional metadata to attach
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
AddResult with operation status
|
||||
"""
|
||||
...
|
||||
|
||||
def get(
|
||||
self,
|
||||
item_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Optional[SearchResultItem]:
|
||||
"""
|
||||
Get a specific item by ID.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to retrieve
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
SearchResultItem if found, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def get_all(
|
||||
self,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
**kwargs: Any,
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Get all items, optionally filtered by scope.
|
||||
|
||||
Args:
|
||||
user_id: Optional user identifier for scoping
|
||||
agent_id: Optional agent identifier for scoping
|
||||
run_id: Optional run identifier for scoping
|
||||
limit: Maximum number of results
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
SearchResult with all matching items
|
||||
"""
|
||||
...
|
||||
|
||||
def update(
|
||||
self,
|
||||
item_id: str,
|
||||
content: Any,
|
||||
**kwargs: Any,
|
||||
) -> AddResult:
|
||||
"""
|
||||
Update an existing item.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to update
|
||||
content: New content
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
AddResult with operation status
|
||||
"""
|
||||
...
|
||||
|
||||
def delete(
|
||||
self,
|
||||
item_id: str,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete an item by ID.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to delete
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
True if deleted, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def delete_all(
|
||||
self,
|
||||
*,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete all items matching scope.
|
||||
|
||||
Args:
|
||||
user_id: Optional user identifier for scoping
|
||||
agent_id: Optional agent identifier for scoping
|
||||
run_id: Optional run identifier for scoping
|
||||
**kwargs: Backend-specific options
|
||||
|
||||
Returns:
|
||||
True if operation succeeded
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class KnowledgeBackendError(Exception):
|
||||
"""Base exception for knowledge backend errors."""
|
||||
pass
|
||||
|
||||
|
||||
class ScopeRequiredError(KnowledgeBackendError):
|
||||
"""Raised when required scope identifiers are missing."""
|
||||
|
||||
def __init__(self, message: str = None, backend: str = None):
|
||||
self.backend = backend
|
||||
default_msg = (
|
||||
f"At least one of 'user_id', 'agent_id', or 'run_id' must be provided"
|
||||
f"{f' for {backend} backend' if backend else ''}."
|
||||
)
|
||||
super().__init__(message or default_msg)
|
||||
|
||||
|
||||
class BackendNotAvailableError(KnowledgeBackendError):
|
||||
"""Raised when a requested backend is not available."""
|
||||
|
||||
def __init__(self, backend: str, reason: str = None):
|
||||
self.backend = backend
|
||||
self.reason = reason
|
||||
msg = f"Knowledge backend '{backend}' is not available"
|
||||
if reason:
|
||||
msg += f": {reason}"
|
||||
super().__init__(msg)
|
||||
@@ -26,12 +26,36 @@ _LAZY_IMPORTS = {
|
||||
"ContextPack": ("praisonaiagents.rag.models", "ContextPack"),
|
||||
"RAGResult": ("praisonaiagents.rag.models", "RAGResult"),
|
||||
"RAGConfig": ("praisonaiagents.rag.models", "RAGConfig"),
|
||||
"RetrievalStrategy": ("praisonaiagents.rag.models", "RetrievalStrategy"),
|
||||
# Unified retrieval config (Agent-first)
|
||||
"RetrievalConfig": ("praisonaiagents.rag.retrieval_config", "RetrievalConfig"),
|
||||
"RetrievalPolicy": ("praisonaiagents.rag.retrieval_config", "RetrievalPolicy"),
|
||||
"CitationsMode": ("praisonaiagents.rag.retrieval_config", "CitationsMode"),
|
||||
"create_retrieval_config": ("praisonaiagents.rag.retrieval_config", "create_retrieval_config"),
|
||||
# Token Budget (Phase 1)
|
||||
"TokenBudget": ("praisonaiagents.rag.budget", "TokenBudget"),
|
||||
"get_model_context_window": ("praisonaiagents.rag.budget", "get_model_context_window"),
|
||||
"BudgetEnforcerProtocol": ("praisonaiagents.rag.budget", "BudgetEnforcerProtocol"),
|
||||
"DefaultBudgetEnforcer": ("praisonaiagents.rag.budget", "DefaultBudgetEnforcer"),
|
||||
"estimate_tokens": ("praisonaiagents.rag.budget", "estimate_tokens"),
|
||||
# Strategy Selection (Phase 3) - RetrievalStrategy now from strategy module
|
||||
"RetrievalStrategy": ("praisonaiagents.rag.strategy", "RetrievalStrategy"),
|
||||
"select_strategy": ("praisonaiagents.rag.strategy", "select_strategy"),
|
||||
"get_strategy_description": ("praisonaiagents.rag.strategy", "get_strategy_description"),
|
||||
"STRATEGY_THRESHOLDS": ("praisonaiagents.rag.strategy", "STRATEGY_THRESHOLDS"),
|
||||
# SmartRetriever (Phase 4)
|
||||
"SmartRetriever": ("praisonaiagents.rag.retriever", "SmartRetriever"),
|
||||
"RetrievalResult": ("praisonaiagents.rag.retriever", "RetrievalResult"),
|
||||
"SimpleReranker": ("praisonaiagents.rag.retriever", "SimpleReranker"),
|
||||
"RetrieverProtocol": ("praisonaiagents.rag.retriever", "RetrieverProtocol"),
|
||||
"RerankerProtocol": ("praisonaiagents.rag.retriever", "RerankerProtocol"),
|
||||
# Compressor (Phase 5)
|
||||
"ContextCompressor": ("praisonaiagents.rag.compressor", "ContextCompressor"),
|
||||
"CompressionResult": ("praisonaiagents.rag.compressor", "CompressionResult"),
|
||||
"CompressorProtocol": ("praisonaiagents.rag.compressor", "CompressorProtocol"),
|
||||
# Summarizer (Phase 6)
|
||||
"HierarchicalSummarizer": ("praisonaiagents.rag.summarizer", "HierarchicalSummarizer"),
|
||||
"SummaryNode": ("praisonaiagents.rag.summarizer", "SummaryNode"),
|
||||
"HierarchyResult": ("praisonaiagents.rag.summarizer", "HierarchyResult"),
|
||||
# Protocols
|
||||
"ContextBuilderProtocol": ("praisonaiagents.rag.protocols", "ContextBuilderProtocol"),
|
||||
"CitationFormatterProtocol": ("praisonaiagents.rag.protocols", "CitationFormatterProtocol"),
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Token Budget Infrastructure for PraisonAI Agents.
|
||||
|
||||
Provides dynamic token budget calculation for context building.
|
||||
Ensures retrieved context never exceeds available token budget.
|
||||
|
||||
No heavy imports - only stdlib and typing.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
# Model context window mapping (tokens)
|
||||
# Updated with latest model information
|
||||
MODEL_CONTEXT_WINDOWS: Dict[str, int] = {
|
||||
# OpenAI models
|
||||
"gpt-4": 8192,
|
||||
"gpt-4-32k": 32768,
|
||||
"gpt-4-turbo": 128000,
|
||||
"gpt-4-turbo-preview": 128000,
|
||||
"gpt-4o": 128000,
|
||||
"gpt-4o-mini": 128000,
|
||||
"gpt-4.1": 1000000,
|
||||
"gpt-4.1-mini": 1000000,
|
||||
"gpt-4.1-nano": 1000000,
|
||||
"gpt-3.5-turbo": 16385,
|
||||
"gpt-3.5-turbo-16k": 16385,
|
||||
"o1": 200000,
|
||||
"o1-mini": 128000,
|
||||
"o1-preview": 128000,
|
||||
"o3": 200000,
|
||||
"o3-mini": 200000,
|
||||
"o4-mini": 200000,
|
||||
|
||||
# Anthropic models
|
||||
"claude-3-opus": 200000,
|
||||
"claude-3-sonnet": 200000,
|
||||
"claude-3-haiku": 200000,
|
||||
"claude-3.5-sonnet": 200000,
|
||||
"claude-3.5-haiku": 200000,
|
||||
"claude-3.7-sonnet": 200000,
|
||||
"claude-4-sonnet": 200000,
|
||||
"claude-sonnet-4": 200000,
|
||||
|
||||
# Google models
|
||||
"gemini-pro": 32768,
|
||||
"gemini-1.0-pro": 32768,
|
||||
"gemini-1.5-pro": 1000000,
|
||||
"gemini-1.5-flash": 1000000,
|
||||
"gemini-2.0-flash": 1000000,
|
||||
"gemini-2.5-pro": 1000000,
|
||||
"gemini-2.5-flash": 1000000,
|
||||
|
||||
# Mistral models
|
||||
"mistral-large": 128000,
|
||||
"mistral-medium": 32768,
|
||||
"mistral-small": 32768,
|
||||
"mixtral-8x7b": 32768,
|
||||
|
||||
# Llama models
|
||||
"llama-3.1-405b": 128000,
|
||||
"llama-3.1-70b": 128000,
|
||||
"llama-3.1-8b": 128000,
|
||||
"llama-3.2-90b": 128000,
|
||||
"llama-3.2-11b": 128000,
|
||||
"llama-3.3-70b": 128000,
|
||||
"llama-4-maverick": 1000000,
|
||||
"llama-4-scout": 10000000,
|
||||
|
||||
# DeepSeek models
|
||||
"deepseek-chat": 64000,
|
||||
"deepseek-coder": 64000,
|
||||
"deepseek-r1": 64000,
|
||||
|
||||
# Cohere models
|
||||
"command-r": 128000,
|
||||
"command-r-plus": 128000,
|
||||
}
|
||||
|
||||
# Default fallback for unknown models
|
||||
DEFAULT_CONTEXT_WINDOW = 8192
|
||||
|
||||
|
||||
def get_model_context_window(model_name: Optional[str]) -> int:
|
||||
"""
|
||||
Get the context window size for a model.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model (e.g., "gpt-4-turbo", "claude-3-opus")
|
||||
|
||||
Returns:
|
||||
Context window size in tokens
|
||||
"""
|
||||
if not model_name:
|
||||
return DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
# Normalize model name
|
||||
model_lower = model_name.lower().strip()
|
||||
|
||||
# Direct lookup
|
||||
if model_lower in MODEL_CONTEXT_WINDOWS:
|
||||
return MODEL_CONTEXT_WINDOWS[model_lower]
|
||||
|
||||
# Prefix matching for versioned models
|
||||
for prefix, tokens in MODEL_CONTEXT_WINDOWS.items():
|
||||
if model_lower.startswith(prefix):
|
||||
return tokens
|
||||
|
||||
# Pattern matching for common patterns
|
||||
if "gpt-4o" in model_lower or "gpt-4-turbo" in model_lower:
|
||||
return 128000
|
||||
if "gpt-4" in model_lower:
|
||||
return 8192
|
||||
if "gpt-3.5" in model_lower:
|
||||
return 16385
|
||||
if "claude-3" in model_lower or "claude-4" in model_lower:
|
||||
return 200000
|
||||
if "gemini-1.5" in model_lower or "gemini-2" in model_lower:
|
||||
return 1000000
|
||||
if "gemini" in model_lower:
|
||||
return 32768
|
||||
if "llama-4" in model_lower:
|
||||
return 1000000
|
||||
if "llama-3" in model_lower:
|
||||
return 128000
|
||||
if "mistral" in model_lower or "mixtral" in model_lower:
|
||||
return 32768
|
||||
if "deepseek" in model_lower:
|
||||
return 64000
|
||||
if "command" in model_lower:
|
||||
return 128000
|
||||
|
||||
return DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenBudget:
|
||||
"""
|
||||
Dynamic token budget calculation for context building.
|
||||
|
||||
Calculates available tokens for retrieved context based on:
|
||||
- Model's maximum context window
|
||||
- Reserved tokens for response generation
|
||||
- Reserved tokens for system prompt
|
||||
- Reserved tokens for conversation history
|
||||
|
||||
Attributes:
|
||||
model_max_tokens: Model's maximum context window
|
||||
reserved_response_tokens: Tokens reserved for response generation
|
||||
reserved_system_tokens: Tokens reserved for system prompt
|
||||
reserved_history_tokens: Tokens reserved for conversation history
|
||||
"""
|
||||
model_max_tokens: int = 128000
|
||||
reserved_response_tokens: int = 4096
|
||||
reserved_system_tokens: int = 1000
|
||||
reserved_history_tokens: int = 2000
|
||||
|
||||
@property
|
||||
def max_context_tokens(self) -> int:
|
||||
"""
|
||||
Calculate maximum tokens available for retrieved context.
|
||||
|
||||
This is the static budget based on reserved allocations.
|
||||
"""
|
||||
available = (
|
||||
self.model_max_tokens
|
||||
- self.reserved_response_tokens
|
||||
- self.reserved_system_tokens
|
||||
- self.reserved_history_tokens
|
||||
)
|
||||
return max(0, available)
|
||||
|
||||
def dynamic_budget(
|
||||
self,
|
||||
prompt_tokens: int = 0,
|
||||
history_tokens: int = 0,
|
||||
system_tokens: int = 0,
|
||||
) -> int:
|
||||
"""
|
||||
Calculate remaining tokens for context after accounting for actual usage.
|
||||
|
||||
Args:
|
||||
prompt_tokens: Actual tokens used by current prompt
|
||||
history_tokens: Actual tokens used by conversation history
|
||||
system_tokens: Actual tokens used by system prompt
|
||||
|
||||
Returns:
|
||||
Remaining tokens available for retrieved context
|
||||
"""
|
||||
used = (
|
||||
prompt_tokens
|
||||
+ history_tokens
|
||||
+ system_tokens
|
||||
+ self.reserved_response_tokens
|
||||
)
|
||||
remaining = self.model_max_tokens - used
|
||||
return max(0, remaining)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model_name: Optional[str], **kwargs) -> "TokenBudget":
|
||||
"""
|
||||
Create TokenBudget from model name.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model
|
||||
**kwargs: Override default reserved token values
|
||||
|
||||
Returns:
|
||||
TokenBudget configured for the model
|
||||
"""
|
||||
context_window = get_model_context_window(model_name)
|
||||
return cls(model_max_tokens=context_window, **kwargs)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"model_max_tokens": self.model_max_tokens,
|
||||
"reserved_response_tokens": self.reserved_response_tokens,
|
||||
"reserved_system_tokens": self.reserved_system_tokens,
|
||||
"reserved_history_tokens": self.reserved_history_tokens,
|
||||
"max_context_tokens": self.max_context_tokens,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "TokenBudget":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
model_max_tokens=data.get("model_max_tokens", 128000),
|
||||
reserved_response_tokens=data.get("reserved_response_tokens", 4096),
|
||||
reserved_system_tokens=data.get("reserved_system_tokens", 1000),
|
||||
reserved_history_tokens=data.get("reserved_history_tokens", 2000),
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class BudgetEnforcerProtocol(Protocol):
|
||||
"""
|
||||
Protocol for enforcing token budgets on retrieved chunks.
|
||||
|
||||
Implementations must provide an enforce method that truncates
|
||||
chunks to fit within the available token budget.
|
||||
"""
|
||||
|
||||
def enforce(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
budget: TokenBudget,
|
||||
prompt_tokens: int = 0,
|
||||
history_tokens: int = 0,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Enforce token budget on chunks.
|
||||
|
||||
Args:
|
||||
chunks: List of chunks with 'text' and 'metadata' keys
|
||||
budget: TokenBudget instance
|
||||
prompt_tokens: Tokens used by current prompt
|
||||
history_tokens: Tokens used by conversation history
|
||||
|
||||
Returns:
|
||||
Truncated list of chunks that fit within budget
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
"""
|
||||
Estimate token count for text.
|
||||
|
||||
Uses a simple heuristic: ~4 characters per token for English text.
|
||||
This is a fast approximation; for exact counts, use tiktoken.
|
||||
|
||||
Args:
|
||||
text: Text to estimate tokens for
|
||||
|
||||
Returns:
|
||||
Estimated token count
|
||||
"""
|
||||
if not text:
|
||||
return 0
|
||||
# Average of 4 characters per token for English
|
||||
# This is a conservative estimate
|
||||
return len(text) // 4 + 1
|
||||
|
||||
|
||||
class DefaultBudgetEnforcer:
|
||||
"""
|
||||
Default implementation of BudgetEnforcerProtocol.
|
||||
|
||||
Truncates chunks to fit within available token budget.
|
||||
Uses simple token estimation for speed.
|
||||
"""
|
||||
|
||||
def __init__(self, token_estimator=None):
|
||||
"""
|
||||
Initialize enforcer.
|
||||
|
||||
Args:
|
||||
token_estimator: Optional custom token estimation function
|
||||
"""
|
||||
self._token_estimator = token_estimator or estimate_tokens
|
||||
|
||||
def enforce(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
budget: TokenBudget,
|
||||
prompt_tokens: int = 0,
|
||||
history_tokens: int = 0,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Enforce token budget on chunks.
|
||||
|
||||
Iteratively adds chunks until budget is exhausted.
|
||||
"""
|
||||
available = budget.dynamic_budget(
|
||||
prompt_tokens=prompt_tokens,
|
||||
history_tokens=history_tokens,
|
||||
)
|
||||
|
||||
result = []
|
||||
used_tokens = 0
|
||||
|
||||
for chunk in chunks:
|
||||
text = chunk.get("text", "")
|
||||
chunk_tokens = self._token_estimator(text)
|
||||
|
||||
if used_tokens + chunk_tokens <= available:
|
||||
result.append(chunk)
|
||||
used_tokens += chunk_tokens
|
||||
else:
|
||||
# Try to fit partial chunk if there's remaining space
|
||||
remaining = available - used_tokens
|
||||
if remaining > 100: # Only if meaningful space remains
|
||||
# Truncate text to fit
|
||||
chars_to_keep = remaining * 4 # Approximate
|
||||
truncated_text = text[:chars_to_keep]
|
||||
if truncated_text:
|
||||
truncated_chunk = {
|
||||
**chunk,
|
||||
"text": truncated_text + "...",
|
||||
"metadata": {
|
||||
**chunk.get("metadata", {}),
|
||||
"_truncated": True,
|
||||
},
|
||||
}
|
||||
result.append(truncated_chunk)
|
||||
break
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
Contextual Compression for PraisonAI Agents (Phase 5).
|
||||
|
||||
Provides context compression to fit within token budgets:
|
||||
- Redundancy removal / deduplication
|
||||
- Query-relevant sentence extraction
|
||||
- LLM-based summarization fallback
|
||||
|
||||
No heavy imports at module level.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CompressorProtocol(Protocol):
|
||||
"""Protocol for compression implementations."""
|
||||
|
||||
def compress(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
query: str,
|
||||
target_tokens: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Compress chunks to fit target token budget."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompressionResult:
|
||||
"""Result of compression operation."""
|
||||
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
||||
original_tokens: int = 0
|
||||
compressed_tokens: int = 0
|
||||
compression_ratio: float = 1.0
|
||||
method_used: str = "none"
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
"""Estimate token count (~4 chars per token)."""
|
||||
if not text:
|
||||
return 0
|
||||
return len(text) // 4 + 1
|
||||
|
||||
|
||||
class ContextCompressor:
|
||||
"""
|
||||
Compresses retrieved context to fit within token budget.
|
||||
|
||||
Compression strategies (applied in order):
|
||||
1. Deduplication - remove redundant chunks
|
||||
2. Sentence extraction - keep query-relevant sentences
|
||||
3. Truncation - cut to fit budget
|
||||
4. Summarization - LLM fallback (optional)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm=None,
|
||||
similarity_threshold: float = 0.85,
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize compressor.
|
||||
|
||||
Args:
|
||||
llm: Optional LLM for summarization fallback
|
||||
similarity_threshold: Threshold for deduplication
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
self._llm = llm
|
||||
self._similarity_threshold = similarity_threshold
|
||||
self._verbose = verbose
|
||||
|
||||
def compress(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
query: str,
|
||||
target_tokens: int,
|
||||
compression_ratio: float = 0.5,
|
||||
) -> CompressionResult:
|
||||
"""
|
||||
Compress chunks to fit target token budget.
|
||||
|
||||
Args:
|
||||
chunks: List of chunks with 'text' and 'metadata'
|
||||
query: Original query for relevance scoring
|
||||
target_tokens: Target token budget
|
||||
compression_ratio: Target compression ratio (0.5 = 50% reduction)
|
||||
|
||||
Returns:
|
||||
CompressionResult with compressed chunks
|
||||
"""
|
||||
result = CompressionResult()
|
||||
|
||||
if not chunks:
|
||||
return result
|
||||
|
||||
# Calculate original tokens
|
||||
result.original_tokens = sum(
|
||||
estimate_tokens(c.get('text', '')) for c in chunks
|
||||
)
|
||||
|
||||
# Step 1: Deduplicate
|
||||
deduped = self._deduplicate(chunks)
|
||||
|
||||
# Step 2: Extract relevant sentences
|
||||
extracted = self._extract_relevant(deduped, query)
|
||||
|
||||
# Step 3: Truncate to fit budget
|
||||
truncated = self._truncate_to_budget(extracted, target_tokens)
|
||||
|
||||
# Calculate compressed tokens
|
||||
result.compressed_tokens = sum(
|
||||
estimate_tokens(c.get('text', '')) for c in truncated
|
||||
)
|
||||
|
||||
if result.original_tokens > 0:
|
||||
result.compression_ratio = result.compressed_tokens / result.original_tokens
|
||||
|
||||
result.chunks = truncated
|
||||
result.method_used = "dedupe+extract+truncate"
|
||||
|
||||
return result
|
||||
|
||||
def _deduplicate(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Remove duplicate or near-duplicate chunks."""
|
||||
if not chunks:
|
||||
return []
|
||||
|
||||
seen_texts = set()
|
||||
unique = []
|
||||
|
||||
for chunk in chunks:
|
||||
text = chunk.get('text', '').strip()
|
||||
|
||||
# Skip empty
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# Simple dedup by exact match
|
||||
text_key = text[:200].lower() # First 200 chars as key
|
||||
if text_key in seen_texts:
|
||||
continue
|
||||
|
||||
seen_texts.add(text_key)
|
||||
unique.append(chunk)
|
||||
|
||||
return unique
|
||||
|
||||
def _extract_relevant(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
query: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Extract query-relevant sentences from chunks."""
|
||||
if not chunks or not query:
|
||||
return chunks
|
||||
|
||||
query_words = set(query.lower().split())
|
||||
extracted = []
|
||||
|
||||
for chunk in chunks:
|
||||
text = chunk.get('text', '')
|
||||
|
||||
# Split into sentences
|
||||
sentences = self._split_sentences(text)
|
||||
|
||||
# Score sentences by relevance
|
||||
relevant_sentences = []
|
||||
for sentence in sentences:
|
||||
sentence_words = set(sentence.lower().split())
|
||||
overlap = len(query_words & sentence_words)
|
||||
|
||||
# Keep sentences with any overlap or important ones
|
||||
if overlap > 0 or len(sentence) > 50:
|
||||
relevant_sentences.append(sentence)
|
||||
|
||||
# Reconstruct text from relevant sentences
|
||||
if relevant_sentences:
|
||||
extracted.append({
|
||||
**chunk,
|
||||
'text': ' '.join(relevant_sentences),
|
||||
'metadata': {
|
||||
**chunk.get('metadata', {}),
|
||||
'_compressed': True,
|
||||
},
|
||||
})
|
||||
|
||||
return extracted
|
||||
|
||||
def _split_sentences(self, text: str) -> List[str]:
|
||||
"""Split text into sentences."""
|
||||
import re
|
||||
|
||||
# Simple sentence splitting
|
||||
sentences = re.split(r'(?<=[.!?])\s+', text)
|
||||
return [s.strip() for s in sentences if s.strip()]
|
||||
|
||||
def _truncate_to_budget(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
target_tokens: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Truncate chunks to fit token budget."""
|
||||
if not chunks:
|
||||
return []
|
||||
|
||||
result = []
|
||||
used_tokens = 0
|
||||
|
||||
for chunk in chunks:
|
||||
text = chunk.get('text', '')
|
||||
chunk_tokens = estimate_tokens(text)
|
||||
|
||||
if used_tokens + chunk_tokens <= target_tokens:
|
||||
result.append(chunk)
|
||||
used_tokens += chunk_tokens
|
||||
else:
|
||||
# Partial fit
|
||||
remaining = target_tokens - used_tokens
|
||||
if remaining > 50: # Only if meaningful space
|
||||
chars_to_keep = remaining * 4
|
||||
truncated_text = text[:chars_to_keep] + "..."
|
||||
result.append({
|
||||
**chunk,
|
||||
'text': truncated_text,
|
||||
'metadata': {
|
||||
**chunk.get('metadata', {}),
|
||||
'_truncated': True,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def summarize(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
query: str,
|
||||
target_tokens: int,
|
||||
) -> str:
|
||||
"""
|
||||
Summarize chunks using LLM (fallback method).
|
||||
|
||||
Args:
|
||||
chunks: Chunks to summarize
|
||||
query: Original query for context
|
||||
target_tokens: Target token budget
|
||||
|
||||
Returns:
|
||||
Summarized text
|
||||
"""
|
||||
if not self._llm or not chunks:
|
||||
return ""
|
||||
|
||||
# Combine chunk texts
|
||||
combined = "\n\n".join(c.get('text', '') for c in chunks)
|
||||
|
||||
# Create summarization prompt
|
||||
prompt = f"""Summarize the following content to answer this query: {query}
|
||||
|
||||
Content:
|
||||
{combined}
|
||||
|
||||
Provide a concise summary that captures the key information relevant to the query.
|
||||
Keep the summary under {target_tokens * 4} characters."""
|
||||
|
||||
try:
|
||||
response = self._llm.generate(prompt)
|
||||
return response if isinstance(response, str) else str(response)
|
||||
except Exception:
|
||||
return combined[:target_tokens * 4]
|
||||
@@ -57,7 +57,9 @@ def deduplicate_chunks(
|
||||
continue
|
||||
# Handle different result formats
|
||||
text = result.get("text") or result.get("memory", "")
|
||||
source = result.get("metadata", {}).get("source", "")
|
||||
# CRITICAL: Handle metadata=None from mem0 - ensure always dict
|
||||
metadata = result.get("metadata") or {}
|
||||
source = metadata.get("source", "")
|
||||
|
||||
chunk_id = _chunk_hash(text, source)
|
||||
|
||||
@@ -146,8 +148,10 @@ def build_context(
|
||||
|
||||
# Build chunk text with optional source
|
||||
if include_source:
|
||||
source = result.get("metadata", {}).get("source", "")
|
||||
filename = result.get("metadata", {}).get("filename", "")
|
||||
# CRITICAL: Handle metadata=None from mem0 - ensure always dict
|
||||
metadata = result.get("metadata") or {}
|
||||
source = metadata.get("source", "")
|
||||
filename = metadata.get("filename", "")
|
||||
source_label = filename or source or f"Source {i + 1}"
|
||||
chunk_text = f"[{source_label}]\n{text}"
|
||||
else:
|
||||
|
||||
@@ -33,7 +33,8 @@ class DefaultCitationFormatter:
|
||||
for i, result in enumerate(results):
|
||||
# Handle different result formats
|
||||
text = result.get("text") or result.get("memory", "")
|
||||
metadata = result.get("metadata", {})
|
||||
# CRITICAL: Handle metadata=None from mem0 - ensure always dict
|
||||
metadata = result.get("metadata") or {}
|
||||
score = result.get("score", 0.0)
|
||||
|
||||
# Extract source info
|
||||
|
||||
@@ -71,6 +71,23 @@ class RetrievalConfig:
|
||||
citations: bool = True
|
||||
citations_mode: CitationsMode = CitationsMode.APPEND
|
||||
|
||||
# Token budget settings (Phase 1)
|
||||
model_context_window: Optional[int] = None # Auto-detect if None
|
||||
reserved_response_tokens: int = 4096
|
||||
dynamic_budget: bool = True # Use dynamic budget calculation
|
||||
|
||||
# Strategy selection (Phase 3)
|
||||
strategy: str = "auto" # auto, direct, basic, hybrid, reranked, compressed, hierarchical
|
||||
|
||||
# Compression settings (Phase 5)
|
||||
compress: bool = False
|
||||
compression_ratio: float = 0.5 # Target compression ratio
|
||||
|
||||
# Filtering settings (Phase 4)
|
||||
include_glob: Optional[list] = None # Glob patterns to include
|
||||
exclude_glob: Optional[list] = None # Glob patterns to exclude
|
||||
path_filter: Optional[str] = None # Regex for path filtering
|
||||
|
||||
# Vector store configuration
|
||||
vector_store_provider: str = "chroma"
|
||||
vector_store_config: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -96,6 +113,54 @@ class RetrievalConfig:
|
||||
</retrieved_context>"""
|
||||
system_separation: bool = True # Use system prompt separation for safety
|
||||
|
||||
def get_token_budget(self, model_name: Optional[str] = None):
|
||||
"""
|
||||
Get TokenBudget instance for this config.
|
||||
|
||||
Args:
|
||||
model_name: Optional model name for context window detection
|
||||
|
||||
Returns:
|
||||
TokenBudget configured for this retrieval config
|
||||
"""
|
||||
from .budget import TokenBudget, get_model_context_window
|
||||
|
||||
# Use configured context window or auto-detect from model
|
||||
if self.model_context_window is not None:
|
||||
context_window = self.model_context_window
|
||||
elif model_name:
|
||||
context_window = get_model_context_window(model_name)
|
||||
else:
|
||||
context_window = 128000 # Default fallback
|
||||
|
||||
return TokenBudget(
|
||||
model_max_tokens=context_window,
|
||||
reserved_response_tokens=self.reserved_response_tokens,
|
||||
)
|
||||
|
||||
def get_strategy(self, corpus_stats=None):
|
||||
"""
|
||||
Get retrieval strategy based on config and corpus stats.
|
||||
|
||||
Args:
|
||||
corpus_stats: Optional CorpusStats for auto-selection
|
||||
|
||||
Returns:
|
||||
RetrievalStrategy enum value
|
||||
"""
|
||||
from .strategy import select_strategy, RetrievalStrategy
|
||||
|
||||
if corpus_stats is None:
|
||||
# No stats, use explicit strategy or default
|
||||
if self.strategy and self.strategy != "auto":
|
||||
try:
|
||||
return RetrievalStrategy(self.strategy.lower())
|
||||
except ValueError:
|
||||
pass
|
||||
return RetrievalStrategy.BASIC
|
||||
|
||||
return select_strategy(corpus_stats, override=self.strategy)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
@@ -108,6 +173,15 @@ class RetrievalConfig:
|
||||
"hybrid": self.hybrid,
|
||||
"citations": self.citations,
|
||||
"citations_mode": self.citations_mode.value if isinstance(self.citations_mode, CitationsMode) else self.citations_mode,
|
||||
"model_context_window": self.model_context_window,
|
||||
"reserved_response_tokens": self.reserved_response_tokens,
|
||||
"dynamic_budget": self.dynamic_budget,
|
||||
"strategy": self.strategy,
|
||||
"compress": self.compress,
|
||||
"compression_ratio": self.compression_ratio,
|
||||
"include_glob": self.include_glob,
|
||||
"exclude_glob": self.exclude_glob,
|
||||
"path_filter": self.path_filter,
|
||||
"vector_store_provider": self.vector_store_provider,
|
||||
"vector_store_config": self.vector_store_config,
|
||||
"collection_name": self.collection_name,
|
||||
@@ -147,6 +221,15 @@ class RetrievalConfig:
|
||||
hybrid=data.get("hybrid", False),
|
||||
citations=data.get("citations", True),
|
||||
citations_mode=citations_mode,
|
||||
model_context_window=data.get("model_context_window"),
|
||||
reserved_response_tokens=data.get("reserved_response_tokens", 4096),
|
||||
dynamic_budget=data.get("dynamic_budget", True),
|
||||
strategy=data.get("strategy", "auto"),
|
||||
compress=data.get("compress", False),
|
||||
compression_ratio=data.get("compression_ratio", 0.5),
|
||||
include_glob=data.get("include_glob"),
|
||||
exclude_glob=data.get("exclude_glob"),
|
||||
path_filter=data.get("path_filter"),
|
||||
vector_store_provider=data.get("vector_store_provider", "chroma"),
|
||||
vector_store_config=data.get("vector_store_config", {}),
|
||||
collection_name=data.get("collection_name"),
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Smart Retriever for PraisonAI Agents (Phase 4).
|
||||
|
||||
Provides enhanced retrieval with:
|
||||
- Hybrid retrieval (keyword + semantic)
|
||||
- Metadata filtering
|
||||
- Path/glob filtering
|
||||
- Pluggable reranking
|
||||
|
||||
No heavy imports at module level.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RetrieverProtocol(Protocol):
|
||||
"""Protocol for retrieval implementations."""
|
||||
|
||||
def retrieve(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Retrieve relevant chunks for query."""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RerankerProtocol(Protocol):
|
||||
"""Protocol for reranking implementations."""
|
||||
|
||||
name: str
|
||||
|
||||
def rerank(
|
||||
self,
|
||||
query: str,
|
||||
results: List[Dict[str, Any]],
|
||||
top_k: Optional[int] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Rerank results by relevance to query."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetrievalResult:
|
||||
"""Result of a retrieval operation."""
|
||||
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
||||
total_found: int = 0
|
||||
strategy_used: str = "basic"
|
||||
reranked: bool = False
|
||||
filtered: bool = False
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class SmartRetriever:
|
||||
"""
|
||||
Enhanced retriever with hybrid search, filtering, and reranking.
|
||||
|
||||
Implements the retrieval ladder based on strategy:
|
||||
- DIRECT: Return all content
|
||||
- BASIC: Semantic search only
|
||||
- HYBRID: Keyword + semantic search
|
||||
- RERANKED: Hybrid + cross-encoder reranking
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
knowledge=None,
|
||||
reranker: Optional[RerankerProtocol] = None,
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize SmartRetriever.
|
||||
|
||||
Args:
|
||||
knowledge: Knowledge instance for retrieval
|
||||
reranker: Optional reranker implementation
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
self._knowledge = knowledge
|
||||
self._reranker = reranker
|
||||
self._verbose = verbose
|
||||
|
||||
def retrieve(
|
||||
self,
|
||||
query: str,
|
||||
strategy: str = "basic",
|
||||
top_k: int = 10,
|
||||
rerank_top_k: Optional[int] = None,
|
||||
include_glob: Optional[List[str]] = None,
|
||||
exclude_glob: Optional[List[str]] = None,
|
||||
path_filter: Optional[str] = None,
|
||||
metadata_filters: Optional[Dict[str, Any]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
) -> RetrievalResult:
|
||||
"""
|
||||
Retrieve relevant chunks using specified strategy.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
strategy: Retrieval strategy (direct, basic, hybrid, reranked)
|
||||
top_k: Number of results to return
|
||||
rerank_top_k: Number of results after reranking
|
||||
include_glob: Glob patterns to include
|
||||
exclude_glob: Glob patterns to exclude
|
||||
path_filter: Regex for path filtering
|
||||
metadata_filters: Metadata key-value filters
|
||||
user_id: User ID for scoping
|
||||
agent_id: Agent ID for scoping
|
||||
|
||||
Returns:
|
||||
RetrievalResult with chunks and metadata
|
||||
"""
|
||||
result = RetrievalResult(strategy_used=strategy)
|
||||
|
||||
if not self._knowledge:
|
||||
return result
|
||||
|
||||
# Get initial results from knowledge store
|
||||
search_results = self._search(
|
||||
query=query,
|
||||
top_k=top_k * 3 if strategy in ("reranked", "hybrid") else top_k,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
chunks = self._normalize_results(search_results)
|
||||
result.total_found = len(chunks)
|
||||
|
||||
# Apply filters
|
||||
if include_glob or exclude_glob or path_filter or metadata_filters:
|
||||
chunks = self._apply_filters(
|
||||
chunks,
|
||||
include_glob=include_glob,
|
||||
exclude_glob=exclude_glob,
|
||||
path_filter=path_filter,
|
||||
metadata_filters=metadata_filters,
|
||||
)
|
||||
result.filtered = True
|
||||
|
||||
# Apply reranking if strategy requires it
|
||||
if strategy in ("reranked", "compressed", "hierarchical") and self._reranker:
|
||||
chunks = self._reranker.rerank(query, chunks, top_k=rerank_top_k or top_k)
|
||||
result.reranked = True
|
||||
|
||||
# Limit to top_k
|
||||
result.chunks = chunks[:top_k]
|
||||
|
||||
return result
|
||||
|
||||
def _search(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int,
|
||||
user_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
) -> Any:
|
||||
"""Perform search on knowledge store."""
|
||||
try:
|
||||
return self._knowledge.search(
|
||||
query,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
limit=top_k,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _normalize_results(self, results: Any) -> List[Dict[str, Any]]:
|
||||
"""Normalize search results to list of dicts."""
|
||||
if not results:
|
||||
return []
|
||||
|
||||
# Handle dict with 'results' key
|
||||
if isinstance(results, dict):
|
||||
if 'results' in results:
|
||||
items = results.get('results', [])
|
||||
else:
|
||||
items = [results]
|
||||
elif isinstance(results, list):
|
||||
items = results
|
||||
else:
|
||||
return []
|
||||
|
||||
normalized = []
|
||||
for item in items:
|
||||
if item is None:
|
||||
continue
|
||||
if isinstance(item, dict):
|
||||
text = item.get('text') or item.get('memory', '')
|
||||
metadata = item.get('metadata') or {}
|
||||
normalized.append({
|
||||
'text': str(text),
|
||||
'metadata': metadata if isinstance(metadata, dict) else {},
|
||||
'score': float(item.get('score', 0.0) or 0.0),
|
||||
})
|
||||
|
||||
return normalized
|
||||
|
||||
def _apply_filters(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
include_glob: Optional[List[str]] = None,
|
||||
exclude_glob: Optional[List[str]] = None,
|
||||
path_filter: Optional[str] = None,
|
||||
metadata_filters: Optional[Dict[str, Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Apply filters to chunks."""
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
filtered = []
|
||||
|
||||
for chunk in chunks:
|
||||
metadata = chunk.get('metadata', {})
|
||||
filename = metadata.get('filename', '')
|
||||
filepath = metadata.get('filepath', metadata.get('source', ''))
|
||||
|
||||
# Check include patterns
|
||||
if include_glob:
|
||||
matched = False
|
||||
for pattern in include_glob:
|
||||
if fnmatch.fnmatch(filename, pattern) or fnmatch.fnmatch(filepath, pattern):
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
continue
|
||||
|
||||
# Check exclude patterns
|
||||
if exclude_glob:
|
||||
excluded = False
|
||||
for pattern in exclude_glob:
|
||||
if fnmatch.fnmatch(filename, pattern) or fnmatch.fnmatch(filepath, pattern):
|
||||
excluded = True
|
||||
break
|
||||
if excluded:
|
||||
continue
|
||||
|
||||
# Check path filter regex
|
||||
if path_filter:
|
||||
try:
|
||||
if not re.search(path_filter, filepath):
|
||||
continue
|
||||
except re.error:
|
||||
pass
|
||||
|
||||
# Check metadata filters
|
||||
if metadata_filters:
|
||||
match = True
|
||||
for key, value in metadata_filters.items():
|
||||
if metadata.get(key) != value:
|
||||
match = False
|
||||
break
|
||||
if not match:
|
||||
continue
|
||||
|
||||
filtered.append(chunk)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
class SimpleReranker:
|
||||
"""
|
||||
Simple reranker using keyword matching.
|
||||
|
||||
For production, use cross-encoder models like:
|
||||
- sentence-transformers/ms-marco-MiniLM-L-6-v2
|
||||
- Cohere Rerank API
|
||||
"""
|
||||
|
||||
name = "simple"
|
||||
|
||||
def rerank(
|
||||
self,
|
||||
query: str,
|
||||
results: List[Dict[str, Any]],
|
||||
top_k: Optional[int] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Rerank results by keyword overlap with query."""
|
||||
query_words = set(query.lower().split())
|
||||
|
||||
scored = []
|
||||
for result in results:
|
||||
text = result.get('text', '').lower()
|
||||
text_words = set(text.split())
|
||||
|
||||
# Calculate overlap score
|
||||
overlap = len(query_words & text_words)
|
||||
original_score = result.get('score', 0.0)
|
||||
|
||||
# Combined score
|
||||
combined_score = original_score + (overlap * 0.1)
|
||||
|
||||
scored.append({
|
||||
**result,
|
||||
'score': combined_score,
|
||||
'_rerank_overlap': overlap,
|
||||
})
|
||||
|
||||
# Sort by combined score
|
||||
scored.sort(key=lambda x: x['score'], reverse=True)
|
||||
|
||||
if top_k:
|
||||
return scored[:top_k]
|
||||
return scored
|
||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user