mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4e1d8b9e | ||
|
|
69740baa7c | ||
|
|
5035978a29 | ||
|
|
4ddb043324 | ||
|
|
337014f4db | ||
|
|
35c371b25e | ||
|
|
bba75d9fdd | ||
|
|
bf8cd488da | ||
|
|
28905db46d | ||
|
|
3e7aa44dfa | ||
|
|
2f7a65ab92 | ||
|
|
e6e2379b02 | ||
|
|
f31f9a04aa | ||
|
|
5a8adf295e | ||
|
|
8dc243fd4f | ||
|
|
548b4afb88 | ||
|
|
ffa938b3df | ||
|
|
8a9beed264 | ||
|
|
daca5a1762 | ||
|
|
b183ac5cbd | ||
|
|
5f57e1d2f1 | ||
|
|
5114165944 | ||
|
|
389442ea04 | ||
|
|
6d7cc9c3ba | ||
|
|
690e4497cc | ||
|
|
041f5f0b08 | ||
|
|
bc0d9a6213 | ||
|
|
a4427fa135 | ||
|
|
51e8c65c8c | ||
|
|
285ed672c5 | ||
|
|
c03e51e337 | ||
|
|
f3ba05f6aa | ||
|
|
9cea3edcd6 | ||
|
|
02a3bd3597 | ||
|
|
128c77926d | ||
|
|
3aa1b825e4 | ||
|
|
b4ee30d837 | ||
|
|
825648aead | ||
|
|
1532c31e72 | ||
|
|
b313fefd86 | ||
|
|
435e1afb55 | ||
|
|
4cb3c0565a | ||
|
|
a7634b66dc | ||
|
|
3f0eaccbcb | ||
|
|
47713d933a | ||
|
|
c82bfccd1d | ||
|
|
ef7d1264e2 | ||
|
|
2799304270 | ||
|
|
4a85c135a6 | ||
|
|
cdb42a3f08 | ||
|
|
c8378118e7 | ||
|
|
0213764057 | ||
|
|
2220114809 | ||
|
|
9fe4dbc172 | ||
|
|
180503fcd5 | ||
|
|
0d8f936b19 | ||
|
|
8b53ebf5e3 | ||
|
|
504c5a9759 | ||
|
|
8fef42fc03 | ||
|
|
2f18cc1ea2 | ||
|
|
ff7969cdbf | ||
|
|
5fa84dc3eb | ||
|
|
4bf8b980ed | ||
|
|
53e0fd19ab | ||
|
|
0cb6307d3c | ||
|
|
adce94410b | ||
|
|
42aec561b1 | ||
|
|
acc805768d | ||
|
|
6d9fa167e3 | ||
|
|
9e178b70e6 | ||
|
|
ed406973cd | ||
|
|
f91918f4f6 | ||
|
|
ee21df9a6a | ||
|
|
c9159337fa | ||
|
|
2258685c32 | ||
|
|
7210788135 |
No files matched your search
@@ -27,6 +27,9 @@ jobs:
|
||||
comprehensive-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL_NAME: gpt-4o-mini
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
@@ -55,11 +58,13 @@ jobs:
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
- name: Verify environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
if [ -n "$OPENAI_API_KEY" ]; then
|
||||
echo "✅ OPENAI_API_KEY is set (length: ${#OPENAI_API_KEY})"
|
||||
else
|
||||
echo "⚠️ OPENAI_API_KEY is NOT set"
|
||||
fi
|
||||
|
||||
- name: Run Comprehensive Test Suite
|
||||
run: |
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
export OPENAI_API_KEY="$OPENAI_API_KEY"
|
||||
export OPENAI_API_BASE="$OPENAI_API_BASE"
|
||||
export OPENAI_MODEL_NAME="$OPENAI_MODEL_NAME"
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: matrix.python-version == '3.11'
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
|
||||
- name: Run Legacy Example Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
|
||||
@@ -53,6 +53,17 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
|
||||
- 🛠️ 100+ Custom Tools
|
||||
- 📄 YAML Configuration
|
||||
- 💯 100+ LLM Support
|
||||
- 🔬 **Deep Research Agents** (OpenAI & Gemini)
|
||||
- 🔄 **Query Rewriter Agent** (HyDE, Step-back, Multi-query)
|
||||
- 🌐 **Native Web Search** (OpenAI, Gemini, Anthropic, xAI, Perplexity)
|
||||
- 📥 **Web Fetch** (Retrieve full content from URLs - Anthropic)
|
||||
- 💾 **Prompt Caching** (Reduce costs & latency - OpenAI, Anthropic, Bedrock, Deepseek)
|
||||
- 🧠 **Claude Memory Tool** (Persistent cross-conversation memory - Anthropic Beta)
|
||||
- 💾 **File-Based Memory** (Zero-dependency persistent memory for all agents)
|
||||
- 🔍 **Built-in Search Tools** (Tavily, You.com, Exa - web search, news, content extraction)
|
||||
- 📋 **Planning Mode** (Plan before execution - Agent & Multi-Agent)
|
||||
- 🔧 **Planning Tools** (Research with tools during planning)
|
||||
- 🧠 **Planning Reasoning** (Chain-of-thought planning)
|
||||
|
||||
## Using Python Code
|
||||
|
||||
@@ -96,6 +107,260 @@ Run:
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 3. Agent with Planning Mode
|
||||
|
||||
Enable planning for any agent - the agent creates a plan, then executes step by step:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return f"Search results for: {query}"
|
||||
|
||||
agent = Agent(
|
||||
name="AI Assistant",
|
||||
instructions="Research and write about topics",
|
||||
planning=True, # Enable planning mode
|
||||
planning_tools=[search_web], # Tools for planning research
|
||||
planning_reasoning=True # Chain-of-thought reasoning
|
||||
)
|
||||
|
||||
result = agent.start("Research AI trends in 2025 and write a summary")
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. 📋 Agent creates a multi-step plan
|
||||
2. 🚀 Executes each step sequentially
|
||||
3. 📊 Shows progress with context passing
|
||||
4. ✅ Returns final result
|
||||
|
||||
### 4. Deep Research Agent
|
||||
|
||||
Automated research with real-time streaming, web search, and citations using OpenAI or Gemini Deep Research APIs.
|
||||
|
||||
```python
|
||||
from praisonaiagents import DeepResearchAgent
|
||||
|
||||
# OpenAI Deep Research
|
||||
agent = DeepResearchAgent(
|
||||
model="o4-mini-deep-research", # or "o3-deep-research"
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research("What are the latest AI trends in 2025?")
|
||||
print(result.report)
|
||||
print(f"Citations: {len(result.citations)}")
|
||||
```
|
||||
|
||||
```python
|
||||
# Gemini Deep Research
|
||||
from praisonaiagents import DeepResearchAgent
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
model="deep-research-pro", # Auto-detected as Gemini
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research("Research quantum computing advances")
|
||||
print(result.report)
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- 🔍 Multi-provider support (OpenAI, Gemini, LiteLLM)
|
||||
- 📡 Real-time streaming with reasoning summaries
|
||||
- 📚 Structured citations with URLs
|
||||
- 🛠️ Built-in tools: web search, code interpreter, MCP, file search
|
||||
- 🔄 Automatic provider detection from model name
|
||||
|
||||
### 5. Query Rewriter Agent
|
||||
|
||||
Transform user queries to improve RAG retrieval quality using multiple strategies.
|
||||
|
||||
```python
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
|
||||
# Basic - expands abbreviations, adds context
|
||||
result = agent.rewrite("AI trends")
|
||||
print(result.primary_query) # "What are the current trends in Artificial Intelligence?"
|
||||
|
||||
# HyDE - generates hypothetical document for semantic matching
|
||||
result = agent.rewrite("What is quantum computing?", strategy=RewriteStrategy.HYDE)
|
||||
|
||||
# Step-back - generates broader context question
|
||||
result = agent.rewrite("GPT-4 vs Claude 3?", strategy=RewriteStrategy.STEP_BACK)
|
||||
|
||||
# Sub-queries - decomposes complex questions
|
||||
result = agent.rewrite("RAG setup and best embedding models?", strategy=RewriteStrategy.SUB_QUERIES)
|
||||
|
||||
# Contextual - resolves references using chat history
|
||||
result = agent.rewrite("What about cost?", chat_history=[...])
|
||||
```
|
||||
|
||||
**Strategies:**
|
||||
- **BASIC**: Expand abbreviations, fix typos, add context
|
||||
- **HYDE**: Generate hypothetical document for semantic matching
|
||||
- **STEP_BACK**: Generate higher-level concept questions
|
||||
- **SUB_QUERIES**: Decompose multi-part questions
|
||||
- **MULTI_QUERY**: Generate multiple paraphrased versions
|
||||
- **CONTEXTUAL**: Resolve references using conversation history
|
||||
- **AUTO**: Automatically detect best strategy
|
||||
|
||||
### 6. Agent Memory (Zero Dependencies)
|
||||
|
||||
Enable persistent memory for agents - works out of the box without any extra packages.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.memory import FileMemory
|
||||
|
||||
# Enable memory with a single parameter
|
||||
agent = Agent(
|
||||
name="Personal Assistant",
|
||||
instructions="You are a helpful assistant that remembers user preferences.",
|
||||
memory=True, # Enables file-based memory (no extra deps!)
|
||||
user_id="user123" # Isolate memory per user
|
||||
)
|
||||
|
||||
# Memory is automatically injected into conversations
|
||||
result = agent.start("My name is John and I prefer Python")
|
||||
# Agent will remember this for future conversations
|
||||
```
|
||||
|
||||
**Memory Types:**
|
||||
- **Short-term**: Rolling buffer of recent context (auto-expires)
|
||||
- **Long-term**: Persistent important facts (sorted by importance)
|
||||
- **Entity**: People, places, organizations with attributes
|
||||
- **Episodic**: Date-based interaction history
|
||||
|
||||
**Advanced Features:**
|
||||
```python
|
||||
from praisonaiagents.memory import FileMemory
|
||||
|
||||
memory = FileMemory(user_id="user123")
|
||||
|
||||
# Session Save/Resume
|
||||
memory.save_session("project_session", conversation_history=[...])
|
||||
memory.resume_session("project_session")
|
||||
|
||||
# Context Compression
|
||||
memory.compress(llm_func=lambda p: agent.chat(p), max_items=10)
|
||||
|
||||
# Checkpointing
|
||||
memory.create_checkpoint("before_refactor", include_files=["main.py"])
|
||||
memory.restore_checkpoint("before_refactor", restore_files=True)
|
||||
|
||||
# Slash Commands
|
||||
memory.handle_command("/memory show")
|
||||
memory.handle_command("/memory save my_session")
|
||||
```
|
||||
|
||||
**Storage Options:**
|
||||
| Option | Dependencies | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `memory=True` | None | File-based JSON storage (default) |
|
||||
| `memory="file"` | None | Explicit file-based storage |
|
||||
| `memory="sqlite"` | Built-in | SQLite with indexing |
|
||||
| `memory="chromadb"` | chromadb | Vector/semantic search |
|
||||
|
||||
### 6. Rules & Instructions
|
||||
|
||||
PraisonAI auto-discovers instruction files from your project root and git root:
|
||||
|
||||
| File | Description | Priority |
|
||||
|------|-------------|----------|
|
||||
| `PRAISON.md` | PraisonAI native instructions | High |
|
||||
| `PRAISON.local.md` | Local overrides (gitignored) | Higher |
|
||||
| `CLAUDE.md` | Claude Code memory file | High |
|
||||
| `CLAUDE.local.md` | Local overrides (gitignored) | Higher |
|
||||
| `AGENTS.md` | OpenAI Codex CLI instructions | High |
|
||||
| `GEMINI.md` | Gemini CLI memory file | High |
|
||||
| `.cursorrules` | Cursor IDE rules | High |
|
||||
| `.windsurfrules` | Windsurf IDE rules | High |
|
||||
| `.claude/rules/*.md` | Claude Code modular rules | Medium |
|
||||
| `.windsurf/rules/*.md` | Windsurf modular rules | Medium |
|
||||
| `.cursor/rules/*.mdc` | Cursor modular rules | Medium |
|
||||
| `.praison/rules/*.md` | Workspace rules | Medium |
|
||||
| `~/.praison/rules/*.md` | Global rules | Low |
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Agent auto-discovers CLAUDE.md, AGENTS.md, GEMINI.md, etc.
|
||||
agent = Agent(name="Assistant", instructions="You are helpful.")
|
||||
# Rules are injected into system prompt automatically
|
||||
```
|
||||
|
||||
**@Import Syntax:**
|
||||
```markdown
|
||||
# CLAUDE.md
|
||||
See @README for project overview
|
||||
See @docs/architecture.md for system design
|
||||
@~/.praison/my-preferences.md
|
||||
```
|
||||
|
||||
**Rule File Format (with YAML frontmatter):**
|
||||
```markdown
|
||||
---
|
||||
description: Python coding guidelines
|
||||
globs: ["**/*.py"]
|
||||
activation: always # always, glob, manual, ai_decision
|
||||
---
|
||||
|
||||
# Guidelines
|
||||
- Use type hints
|
||||
- Follow PEP 8
|
||||
```
|
||||
|
||||
### 7. Auto-Generated Memories
|
||||
|
||||
```python
|
||||
from praisonaiagents.memory import FileMemory, AutoMemory
|
||||
|
||||
memory = FileMemory(user_id="user123")
|
||||
auto = AutoMemory(memory, enabled=True)
|
||||
|
||||
# Automatically extracts and stores memories from conversations
|
||||
memories = auto.process_interaction(
|
||||
"My name is John and I prefer Python for backend work"
|
||||
)
|
||||
# Extracts: name="John", preference="Python for backend"
|
||||
```
|
||||
|
||||
### 8. Workflows
|
||||
|
||||
Create reusable multi-step workflows in `.praison/workflows/`:
|
||||
|
||||
```python
|
||||
from praisonaiagents.memory import WorkflowManager
|
||||
|
||||
manager = WorkflowManager()
|
||||
|
||||
# Execute a workflow
|
||||
result = manager.execute(
|
||||
"deploy",
|
||||
executor=lambda prompt: agent.chat(prompt),
|
||||
variables={"environment": "production"}
|
||||
)
|
||||
```
|
||||
|
||||
### 9. Hooks
|
||||
|
||||
Configure in `.praison/hooks.json`:
|
||||
|
||||
```python
|
||||
from praisonaiagents.memory import HooksManager
|
||||
|
||||
hooks = HooksManager()
|
||||
|
||||
# Register Python hooks
|
||||
hooks.register("pre_write_code", lambda ctx: print(f"Writing {ctx['file']}"))
|
||||
|
||||
# Execute hooks
|
||||
result = hooks.execute("pre_write_code", {"file": "main.py"})
|
||||
```
|
||||
|
||||
## Using No Code
|
||||
|
||||
### Auto Mode:
|
||||
@@ -105,6 +370,146 @@ export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
|
||||
praisonai --auto create a movie script about Robots in Mars
|
||||
```
|
||||
|
||||
### Query Rewriting (works with any command):
|
||||
```bash
|
||||
# Rewrite query for better results (uses QueryRewriterAgent)
|
||||
praisonai "AI trends" --query-rewrite
|
||||
|
||||
# Rewrite with search tools (agent decides when to search)
|
||||
praisonai "latest developments" --query-rewrite --rewrite-tools "internet_search"
|
||||
|
||||
# Works with any prompt
|
||||
praisonai "explain quantum computing" --query-rewrite -v
|
||||
```
|
||||
|
||||
### Deep Research CLI:
|
||||
```bash
|
||||
# Default: OpenAI (o4-mini-deep-research)
|
||||
praisonai research "What are the latest AI trends in 2025?"
|
||||
|
||||
# Use Gemini
|
||||
praisonai research --model deep-research-pro "Your research query"
|
||||
|
||||
# Rewrite query before research
|
||||
praisonai research --query-rewrite "AI trends"
|
||||
|
||||
# Rewrite with search tools
|
||||
praisonai research --query-rewrite --rewrite-tools "internet_search" "AI trends"
|
||||
|
||||
# Use custom tools from file (gathers context before deep research)
|
||||
praisonai research --tools tools.py "Your research query"
|
||||
praisonai research -t my_tools.py "Your research query"
|
||||
|
||||
# Use built-in tools by name (comma-separated)
|
||||
praisonai research --tools "internet_search,wiki_search" "Your query"
|
||||
praisonai research -t "yfinance,calculator_tools" "Stock analysis query"
|
||||
|
||||
# Save output to file (output/research/{query}.md)
|
||||
praisonai research --save "Your research query"
|
||||
praisonai research -s "Your research query"
|
||||
|
||||
# Combine options
|
||||
praisonai research --query-rewrite --tools tools.py --save "Your research query"
|
||||
|
||||
# Verbose mode (show debug logs)
|
||||
praisonai research -v "Your research query"
|
||||
```
|
||||
|
||||
### Planning Mode CLI:
|
||||
```bash
|
||||
# Enable planning mode - agent creates a plan before execution
|
||||
praisonai "Research AI trends and write a summary" --planning
|
||||
|
||||
# Planning with tools for research
|
||||
praisonai "Analyze market trends" --planning --planning-tools tools.py
|
||||
|
||||
# Planning with chain-of-thought reasoning
|
||||
praisonai "Complex analysis task" --planning --planning-reasoning
|
||||
|
||||
# Auto-approve plans without confirmation
|
||||
praisonai "Task" --planning --auto-approve-plan
|
||||
```
|
||||
|
||||
### Memory CLI:
|
||||
```bash
|
||||
# Enable memory for agent (persists across sessions)
|
||||
praisonai "My name is John" --memory
|
||||
|
||||
# Memory with user isolation
|
||||
praisonai "Remember my preferences" --memory --user-id user123
|
||||
|
||||
# Memory management commands
|
||||
praisonai memory show # Show memory statistics
|
||||
praisonai memory add "User prefers Python" # Add to long-term memory
|
||||
praisonai memory search "Python" # Search memories
|
||||
praisonai memory clear # Clear short-term memory
|
||||
praisonai memory clear all # Clear all memory
|
||||
praisonai memory save my_session # Save session
|
||||
praisonai memory resume my_session # Resume session
|
||||
praisonai memory sessions # List saved sessions
|
||||
praisonai memory checkpoint # Create checkpoint
|
||||
praisonai memory restore <checkpoint_id> # Restore checkpoint
|
||||
praisonai memory checkpoints # List checkpoints
|
||||
praisonai memory help # Show all commands
|
||||
```
|
||||
|
||||
### Rules CLI:
|
||||
```bash
|
||||
# List all loaded rules (from PRAISON.md, CLAUDE.md, etc.)
|
||||
praisonai rules list
|
||||
|
||||
# Show specific rule details
|
||||
praisonai rules show <rule_name>
|
||||
|
||||
# Create a new rule
|
||||
praisonai rules create my_rule "Always use type hints"
|
||||
|
||||
# Delete a rule
|
||||
praisonai rules delete my_rule
|
||||
|
||||
# Show rules statistics
|
||||
praisonai rules stats
|
||||
|
||||
# Include manual rules with prompts
|
||||
praisonai "Task" --include-rules security,testing
|
||||
```
|
||||
|
||||
### Workflow CLI:
|
||||
```bash
|
||||
# List available workflows
|
||||
praisonai workflow list
|
||||
|
||||
# Execute a workflow
|
||||
praisonai workflow run deploy
|
||||
|
||||
# Execute with variables
|
||||
praisonai workflow run deploy --workflow-var environment=staging --workflow-var branch=main
|
||||
|
||||
# Show workflow details
|
||||
praisonai workflow show deploy
|
||||
|
||||
# Create a new workflow template
|
||||
praisonai workflow create my_workflow
|
||||
```
|
||||
|
||||
### Hooks CLI:
|
||||
```bash
|
||||
# List configured hooks
|
||||
praisonai hooks list
|
||||
|
||||
# Show hooks statistics
|
||||
praisonai hooks stats
|
||||
|
||||
# Create hooks.json template
|
||||
praisonai hooks init
|
||||
```
|
||||
|
||||
### Claude Memory Tool CLI:
|
||||
```bash
|
||||
# Enable Claude Memory Tool (Anthropic models only)
|
||||
praisonai "Research and remember findings" --claude-memory --llm anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
## Using JavaScript Code
|
||||
|
||||
```bash
|
||||
@@ -523,6 +928,172 @@ praisonai agents.yaml
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
## Custom Tools
|
||||
|
||||
### Using `@tool` Decorator
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
@tool
|
||||
def search(query: str) -> str:
|
||||
"""Search the web for information."""
|
||||
return f"Results for: {query}"
|
||||
|
||||
@tool
|
||||
def calculate(expression: str) -> float:
|
||||
"""Evaluate a math expression."""
|
||||
return eval(expression)
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
tools=[search, calculate]
|
||||
)
|
||||
agent.start("Search for AI news and calculate 15*4")
|
||||
```
|
||||
|
||||
### Using `BaseTool` Class
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, BaseTool
|
||||
|
||||
class WeatherTool(BaseTool):
|
||||
name = "weather"
|
||||
description = "Get current weather for a location"
|
||||
|
||||
def run(self, location: str) -> str:
|
||||
return f"Weather in {location}: 72°F, Sunny"
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a weather assistant",
|
||||
tools=[WeatherTool()]
|
||||
)
|
||||
agent.start("What's the weather in Paris?")
|
||||
```
|
||||
|
||||
### Creating a Tool Package (pip installable)
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[project]
|
||||
name = "my-praisonai-tools"
|
||||
version = "1.0.0"
|
||||
dependencies = ["praisonaiagents"]
|
||||
|
||||
[project.entry-points."praisonaiagents.tools"]
|
||||
my_tool = "my_package:MyTool"
|
||||
```
|
||||
|
||||
```python
|
||||
# my_package/__init__.py
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "My custom tool"
|
||||
|
||||
def run(self, param: str) -> str:
|
||||
return f"Result: {param}"
|
||||
```
|
||||
|
||||
After `pip install`, tools are auto-discovered:
|
||||
```python
|
||||
agent = Agent(tools=["my_tool"]) # Works automatically!
|
||||
```
|
||||
|
||||
|
||||
## Prompt Expansion
|
||||
|
||||
Expand short prompts into detailed, actionable prompts:
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Expand a short prompt into detailed prompt
|
||||
praisonai "write a movie script in 3 lines" --expand-prompt
|
||||
|
||||
# With verbose output
|
||||
praisonai "blog about AI" --expand-prompt -v
|
||||
|
||||
# With tools for context gathering
|
||||
praisonai "latest AI trends" --expand-prompt --expand-tools tools.py
|
||||
|
||||
# Combine with query rewrite
|
||||
praisonai "AI news" --query-rewrite --expand-prompt
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```python
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
# Basic usage
|
||||
agent = PromptExpanderAgent()
|
||||
result = agent.expand("write a movie script in 3 lines")
|
||||
print(result.expanded_prompt)
|
||||
|
||||
# With specific strategy
|
||||
result = agent.expand("blog about AI", strategy=ExpandStrategy.DETAILED)
|
||||
|
||||
# Available strategies: BASIC, DETAILED, STRUCTURED, CREATIVE, AUTO
|
||||
```
|
||||
|
||||
**Key Difference:**
|
||||
- `--query-rewrite`: Optimizes queries for **search/retrieval** (RAG)
|
||||
- `--expand-prompt`: Expands prompts for **detailed task execution**
|
||||
|
||||
|
||||
## Web Search, Web Fetch & Prompt Caching
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Web Search - Get real-time information
|
||||
praisonai "What are the latest AI news today?" --web-search --llm openai/gpt-4o-search-preview
|
||||
|
||||
# Web Fetch - Retrieve and analyze URL content (Anthropic only)
|
||||
praisonai "Summarize https://docs.praison.ai" --web-fetch --llm anthropic/claude-sonnet-4-20250514
|
||||
|
||||
# Prompt Caching - Reduce costs for repeated prompts
|
||||
praisonai "Analyze this document..." --prompt-caching --llm anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Web Search
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant",
|
||||
llm="openai/gpt-4o-search-preview",
|
||||
web_search=True
|
||||
)
|
||||
|
||||
# Web Fetch (Anthropic only)
|
||||
agent = Agent(
|
||||
instructions="You are a content analyzer",
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
web_fetch=True
|
||||
)
|
||||
|
||||
# Prompt Caching
|
||||
agent = Agent(
|
||||
instructions="You are an AI assistant..." * 50, # Long system prompt
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
prompt_caching=True
|
||||
)
|
||||
```
|
||||
|
||||
**Supported Providers:**
|
||||
| Feature | Providers |
|
||||
|---------|-----------|
|
||||
| Web Search | OpenAI, Gemini, Anthropic, xAI, Perplexity |
|
||||
| Web Fetch | Anthropic |
|
||||
| Prompt Caching | OpenAI (auto), Anthropic, Bedrock, Deepseek |
|
||||
|
||||
|
||||
## Development:
|
||||
|
||||
Below is used for development only.
|
||||
@@ -540,6 +1111,19 @@ uv pip install -r pyproject.toml --extra code
|
||||
uv pip install -r pyproject.toml --extra "crewai,autogen"
|
||||
```
|
||||
|
||||
### Bump and Release
|
||||
|
||||
```bash
|
||||
# From project root - bumps version and releases in one command
|
||||
python src/praisonai/scripts/bump_and_release.py 2.2.99
|
||||
|
||||
# With praisonaiagents dependency
|
||||
python src/praisonai/scripts/bump_and_release.py 2.2.99 --agents 0.0.169
|
||||
|
||||
# Then publish
|
||||
cd src/praisonai && uv publish
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
- Fork on GitHub: Use the "Fork" button on the repository page.
|
||||
@@ -557,7 +1141,7 @@ uv pip install -r pyproject.toml --extra "crewai,autogen"
|
||||
- 🎨 Interactive UIs
|
||||
- 📄 YAML-based Configuration
|
||||
- 🛠️ Custom Tool Integration
|
||||
- 🔍 Internet Search Capability (using Crawl4AI and Tavily)
|
||||
- 🔍 Internet Search Capability (Tavily, You.com, Exa, DuckDuckGo, Crawl4AI)
|
||||
- 🖼️ Vision Language Model (VLM) Support
|
||||
- 🎙️ Real-time Voice Interaction
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
"praisonai>=2.2.84" \
|
||||
"praisonai>=2.3.8" \
|
||||
"praisonai[api]" \
|
||||
gunicorn \
|
||||
markdown
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.2.84" \
|
||||
"praisonai>=2.3.8" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.2.84" \
|
||||
"praisonai>=2.3.8" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.2.84" \
|
||||
"praisonai>=2.3.8" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
+2
-2
@@ -121,7 +121,7 @@ healthcheck:
|
||||
## 📦 Package Versions
|
||||
|
||||
All Docker images use consistent, up-to-date versions:
|
||||
- PraisonAI: `>=2.2.84`
|
||||
- PraisonAI: `>=2.2.95`
|
||||
- PraisonAI Agents: `>=0.0.92`
|
||||
- Python: `3.11-slim`
|
||||
|
||||
@@ -218,7 +218,7 @@ docker-compose up -d
|
||||
### Version Pinning
|
||||
To use specific versions, update the Dockerfile:
|
||||
```dockerfile
|
||||
RUN pip install "praisonai==2.2.84" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.95" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -137,22 +137,11 @@ services:
|
||||
POSTGRES_USER: praisonai
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-praisonai_dev_pass}
|
||||
volumes:
|
||||
database:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: praisonai_dev
|
||||
POSTGRES_USER: praisonai
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-praisonai_dev_pass}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/tmp
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/tmp
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
" - Property comparison\n",
|
||||
" - Cost of living and taxes\n",
|
||||
" - First-time homebuyer guidance\n",
|
||||
"llm: gpt-4o\n",
|
||||
"llm: gpt-5-nano\n",
|
||||
"verbosity: true\n",
|
||||
"format: markdown\n",
|
||||
"\"\"\"\n"
|
||||
@@ -188,7 +188,7 @@
|
||||
"Help users evaluate neighborhoods, estimate mortgage payments, compare homes, and understand homeownership costs.\n",
|
||||
"Explain clearly, format in markdown, and be friendly but professional.\n",
|
||||
"\"\"\",\n",
|
||||
" llm=\"gpt-4o\",\n",
|
||||
" llm=\"gpt-5-nano\",\n",
|
||||
" verbose=True,\n",
|
||||
" markdown=True\n",
|
||||
")\n"
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #008080; text-decoration-color: #008080\">╭───────────────────────────────────────────────────── Task ──────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> You are an expert health advisor using the GPT-4o Mini model. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> You are an expert health advisor using the gpt-5-nano Mini model. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"background-color: #272822\"> </span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"background-color: #272822\"> </span><span style=\"color: #f8f8f2; text-decoration-color: #f8f8f2; background-color: #272822\">Based on the following air quality conditions in Mumbai, Maharashtra, India:</span><span style=\"background-color: #272822\"> </span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
@@ -340,7 +340,7 @@
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[36m╭─\u001b[0m\u001b[36m────────────────────────────────────────────────────\u001b[0m\u001b[36m Task \u001b[0m\u001b[36m─────────────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m You are an expert health advisor using the GPT-4o Mini model. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m You are an expert health advisor using the gpt-5-nano Mini model. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mBased on the following air quality conditions in Mumbai, Maharashtra, India:\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
@@ -519,7 +519,7 @@
|
||||
" aqi_data = aqi_data.data # Convert to dict\n",
|
||||
"\n",
|
||||
" prompt = f\"\"\"\n",
|
||||
" You are an expert health advisor using the GPT-4o Mini model.\n",
|
||||
" You are an expert health advisor using the gpt-5-nano Mini model.\n",
|
||||
"\n",
|
||||
" Based on the following air quality conditions in {user_city}, {user_state}, {user_country}:\n",
|
||||
" - Overall AQI: {aqi_data.get('aqi', 'N/A')}\n",
|
||||
|
||||
+2
-2
@@ -152,7 +152,7 @@
|
||||
" - Evaluate risk factors\n",
|
||||
" - Identify scams or red flags\n",
|
||||
" - Summarize project fundamentals\n",
|
||||
"llm: gpt-4o\n",
|
||||
"llm: gpt-5-nano\n",
|
||||
"verbosity: true\n",
|
||||
"format: markdown\n",
|
||||
"\"\"\"\n"
|
||||
@@ -184,7 +184,7 @@
|
||||
"and market trends. Provide critical insight into legitimacy, potential growth, and associated risks.\n",
|
||||
"Use markdown formatting and structure your research clearly.\n",
|
||||
"\"\"\",\n",
|
||||
" llm=\"gpt-4o\",\n",
|
||||
" llm=\"gpt-5-nano\",\n",
|
||||
" verbose=True,\n",
|
||||
" markdown=True\n",
|
||||
")\n"
|
||||
|
||||
@@ -627,7 +627,7 @@
|
||||
" )\n",
|
||||
" else: # OpenAI\n",
|
||||
" llm = ChatOpenAI(\n",
|
||||
" model=\"gpt-4o\",\n",
|
||||
" model=\"gpt-5-nano\",\n",
|
||||
" api_key=api_key,\n",
|
||||
" temperature=0.0\n",
|
||||
" )\n",
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@
|
||||
" - Spot content patterns and trends\n",
|
||||
" - Recommend growth strategies\n",
|
||||
" - Detect potential monetization methods\n",
|
||||
"llm: gpt-4o\n",
|
||||
"llm: gpt-5-nano\n",
|
||||
"verbosity: true\n",
|
||||
"format: markdown\n",
|
||||
"\"\"\"\n"
|
||||
@@ -184,7 +184,7 @@
|
||||
"You are a YouTube intelligence analyst. Your job is to research a specific channel, niche, or influencer topic.\n",
|
||||
"Provide growth recommendations, engagement analysis, trending strategies, and monetization tips. Format in markdown.\n",
|
||||
"\"\"\",\n",
|
||||
" llm=\"gpt-4o\",\n",
|
||||
" llm=\"gpt-5-nano\",\n",
|
||||
" verbose=True,\n",
|
||||
" markdown=True\n",
|
||||
")\n"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'name': 'Travel Research Agent', 'description': 'A world-class travel researcher that searches for destinations, activities, and accommodations', 'instructions': ['Given a travel destination and number of days, generate 3 relevant search terms', 'Search the web for each term using web_search tool', 'Analyze results and return 10 most relevant findings', 'Focus on activities, accommodations, attractions, and local insights', 'Ensure high-quality, accurate information', 'Use travel_budget_calculator to estimate costs'], 'tools': ['web_search', 'travel_budget_calculator'], 'model': 'gpt-4o', 'temperature': 0.7}\n"
|
||||
"{'name': 'Travel Research Agent', 'description': 'A world-class travel researcher that searches for destinations, activities, and accommodations', 'instructions': ['Given a travel destination and number of days, generate 3 relevant search terms', 'Search the web for each term using web_search tool', 'Analyze results and return 10 most relevant findings', 'Focus on activities, accommodations, attractions, and local insights', 'Ensure high-quality, accurate information', 'Use travel_budget_calculator to estimate costs'], 'tools': ['web_search', 'travel_budget_calculator'], 'model': 'gpt-5-nano', 'temperature': 0.7}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -156,7 +156,7 @@
|
||||
"tools:\n",
|
||||
" - web_search\n",
|
||||
" - travel_budget_calculator\n",
|
||||
"model: gpt-4o\n",
|
||||
"model: gpt-5-nano\n",
|
||||
"temperature: 0.7\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"id": "Y9FkDzt1lF47"
|
||||
},
|
||||
"source": [
|
||||
"This notebook implements a conversational restaurant booking assistant using the Praison AI Agent framework. It handles natural interruptions such as modifying guest count or changing time mid-conversation. Built for Google Colab Free Tier, it uses OpenAI's GPT-4o model and follows a YAML-based agent design for clarity and modularity."
|
||||
"This notebook implements a conversational restaurant booking assistant using the Praison AI Agent framework. It handles natural interruptions such as modifying guest count or changing time mid-conversation. Built for Google Colab Free Tier, it uses OpenAI's gpt-5-nano model and follows a YAML-based agent design for clarity and modularity."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -135,7 +135,7 @@
|
||||
" - Context management\n",
|
||||
" - Handling mid-conversation changes\n",
|
||||
" - Summarizing confirmed reservations\n",
|
||||
"llm: gpt-4o\n",
|
||||
"llm: gpt-5-nano\n",
|
||||
"verbosity: true\n",
|
||||
"format: markdown\n",
|
||||
"\"\"\"\n"
|
||||
@@ -167,7 +167,7 @@
|
||||
"manage corrections like “change to 3 guests” or “move to 8 PM,”\n",
|
||||
"confirm the details once the user is satisfied. Respond in markdown.\n",
|
||||
"\"\"\",\n",
|
||||
" llm=\"gpt-4o\",\n",
|
||||
" llm=\"gpt-5-nano\",\n",
|
||||
" verbose=True,\n",
|
||||
" markdown=True\n",
|
||||
")\n"
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "LJRJRQtFTagH"
|
||||
},
|
||||
"source": [
|
||||
"# AI RAG Agent with Web Access using PraisonAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Ph4RVTBCTcmB"
|
||||
},
|
||||
"source": [
|
||||
"This notebook demonstrates how to build a Retrieval-Augmented Generation (RAG) agent with web access using GPT-4o and PraisonAI. The agent specializes in Thai recipes and can search the web for additional information."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "tMxfwPPATy_o"
|
||||
},
|
||||
"source": [
|
||||
"This RAG agent:\n",
|
||||
"* Creates an AI assistant specialized in Thai recipes and cooking\n",
|
||||
"* Has access to a PDF knowledge base about Thai recipes\n",
|
||||
"* Can search the web using DuckDuckGo for additional information\n",
|
||||
"* Provides comprehensive answers about Thai cuisine\n",
|
||||
"* Uses PraisonAI framework for agent management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "h68oMaCITmqt"
|
||||
},
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/Dhivya-Bharathy/PraisonAI/blob/main/examples/cookbooks/rag_examples/agentic_rag/PraisonAI_RecipeAgent.ipynb)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "cAdzoWXJUMfF"
|
||||
},
|
||||
"source": [
|
||||
"# Dependencies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "9D-2TlOrUNcu"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install praisonaiagents openai requests"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "yY-4-kEIUW40"
|
||||
},
|
||||
"source": [
|
||||
"# Set Up API Key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "f5bhQ05yUX6J",
|
||||
"outputId": "dcf9d0d9-332e-4136-9289-4c39d898005c"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Enter your OpenAI API key: ··········\n",
|
||||
"✅ API key set successfully!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# Set your OpenAI API key here\n",
|
||||
"os.environ['OPENAI_API_KEY'] = 'Enter your api key here'\n",
|
||||
"\n",
|
||||
"# Or use this method to input it securely\n",
|
||||
"from getpass import getpass\n",
|
||||
"api_key = getpass('Enter your OpenAI API key: ')\n",
|
||||
"os.environ['OPENAI_API_KEY'] = api_key\n",
|
||||
"\n",
|
||||
"print(\"✅ API key set successfully!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Eh0j-SegUi13"
|
||||
},
|
||||
"source": [
|
||||
"# Tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "Z1A9oifIUkQN",
|
||||
"outputId": "9be78226-36fa-43c4-865d-2f7e6e2ba9de"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Tools imported successfully!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from praisonaiagents import Agent\n",
|
||||
"import requests\n",
|
||||
"import tempfile\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"print(\"✅ Tools imported successfully!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "efnnREHXUnYA"
|
||||
},
|
||||
"source": [
|
||||
"# YAML Prompt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "kcM14-yQUqG-",
|
||||
"outputId": "c5b6288d-a789-4a93-e20c-1d285370b667"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Agent instructions configured!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Agent configuration and instructions\n",
|
||||
"AGENT_INSTRUCTIONS = \"\"\"\n",
|
||||
"You are a helpful AI assistant specialized in Thai recipes and cooking.\n",
|
||||
"\n",
|
||||
"You have access to a PDF knowledge base about Thai recipes from: https://phi-public.s3.amazonaws.com/recipes/ThaiRecipes.pdf\n",
|
||||
"\n",
|
||||
"You can also search the web for additional information about Thai cooking, ingredients, and techniques.\n",
|
||||
"\n",
|
||||
"When answering questions:\n",
|
||||
"1. Use your knowledge about Thai cuisine to provide helpful information\n",
|
||||
"2. If needed, search the web for additional details, current information, or clarification\n",
|
||||
"3. Provide comprehensive, helpful answers about Thai cuisine\n",
|
||||
"4. Always be informative and helpful about Thai cooking!\n",
|
||||
"\n",
|
||||
"You can use the internet_search function to search the web when needed.\n",
|
||||
"\n",
|
||||
"Focus on providing practical cooking advice, ingredient information, and authentic Thai recipe guidance.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"print(\"✅ Agent instructions configured!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "MCBRcXFJUulY"
|
||||
},
|
||||
"source": [
|
||||
"# Main"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "YaRLbgMmUvXv",
|
||||
"outputId": "8ed8d9f8-85a7-48de-ccc6-f617cd672a85"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🤖 Thai Recipe RAG Agent created successfully!\n",
|
||||
"The agent is ready to answer questions about Thai cooking.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Create the RAG agent\n",
|
||||
"rag_agent = Agent(\n",
|
||||
" instructions=AGENT_INSTRUCTIONS,\n",
|
||||
" llm=\"gpt-4o\",\n",
|
||||
" markdown=True,\n",
|
||||
" verbose=True\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"🤖 Thai Recipe RAG Agent created successfully!\")\n",
|
||||
"print(\"The agent is ready to answer questions about Thai cooking.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "QvZM94pGU0IX"
|
||||
},
|
||||
"source": [
|
||||
"# Test the Agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000,
|
||||
"referenced_widgets": [
|
||||
"47739975c9f04ee5b4e69e8f445cbeda",
|
||||
"08888d583cba4c01b3fdf8f30b06ca4f"
|
||||
]
|
||||
},
|
||||
"id": "QPdrleKxU1VN",
|
||||
"outputId": "a71a86cd-eff8-4309-9b50-173a30066c90"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🤔 Question: What are the essential ingredients for Thai cooking?\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">╭─ </span><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8; font-weight: bold\">Agent Info</span><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\"> ────────────────────────────────────────────────────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #ff9b9b; text-decoration-color: #ff9b9b; font-weight: bold\">👤 Agent:</span> <span style=\"color: #ffe5e5; text-decoration-color: #ffe5e5\">Agent</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #b4b4b3; text-decoration-color: #b4b4b3; font-weight: bold\">Role:</span> <span style=\"color: #ffe5e5; text-decoration-color: #ffe5e5\">Assistant</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[38;2;210;227;200m╭─\u001b[0m\u001b[38;2;210;227;200m \u001b[0m\u001b[1;38;2;210;227;200mAgent Info\u001b[0m\u001b[38;2;210;227;200m \u001b[0m\u001b[38;2;210;227;200m───────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\u001b[38;2;210;227;200m─╮\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[1;38;2;255;155;155m👤 Agent:\u001b[0m \u001b[38;2;255;229;229mAgent\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[1;38;2;180;180;179mRole:\u001b[0m \u001b[38;2;255;229;229mAssistant\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "47739975c9f04ee5b4e69e8f445cbeda",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Output()"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"></pre>\n"
|
||||
],
|
||||
"text/plain": []
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #7f7f7f; text-decoration-color: #7f7f7f\">Response generated in 8.4s</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[2mResponse generated in 8.4s\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #008080; text-decoration-color: #008080\">╭───────────────────────────────────────────────────── Task ──────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> What are the essential ingredients for Thai cooking? <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[36m╭─\u001b[0m\u001b[36m────────────────────────────────────────────────────\u001b[0m\u001b[36m Task \u001b[0m\u001b[36m─────────────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m What are the essential ingredients for Thai cooking? \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #008080; text-decoration-color: #008080\">╭─────────────────────────────────────────────────── Response ────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> Thai cooking is known for its balance of five fundamental taste senses in each dish or the overall meal: sour, <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> sweet, salty, bitter, and spicy. Essential ingredients that help achieve this balance include: <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 1 </span><span style=\"font-weight: bold\">Fish Sauce (Nam Pla)</span>: A salty, savory sauce made from fermented fish. It's a cornerstone of Thai cooking, <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>used to add depth of flavor. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 2 </span><span style=\"font-weight: bold\">Lime Juice</span>: Used to add a fresh, tangy citrus flavor to dishes, especially in salads and some curries. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 3 </span><span style=\"font-weight: bold\">Chilies</span>: Thai cuisine often features fresh Thai bird's eye chilies for heat, as well as dried chilies for <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>certain recipes. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 4 </span><span style=\"font-weight: bold\">Lemongrass</span>: A citrusy, fragrant herb that is often used in soups, curries, and teas. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 5 </span><span style=\"font-weight: bold\">Galangal</span>: Similar to ginger, galangal has a sharp, peppery flavor and is used in soups and curries. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 6 </span><span style=\"font-weight: bold\">Kaffir Lime Leaves</span>: They add a distinct, zesty citrus aroma and flavor to Thai dishes. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 7 </span><span style=\"font-weight: bold\">Coconut Milk</span>: Adds richness and creaminess, commonly used in curries and desserts. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 8 </span><span style=\"font-weight: bold\">Palm Sugar</span>: Used as a sweetener, palm sugar has a nuanced flavor, unlike regular sugar. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 9 </span><span style=\"font-weight: bold\">Tamarind Paste</span>: Provides a sour element to many dishes, particularly pad Thai and some sauces. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 10 </span><span style=\"font-weight: bold\">Thai Basil</span>: Adds a spicy, peppery flavor, different from the sweet basil used in Western cooking. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 11 </span><span style=\"font-weight: bold\">Coriander (Cilantro)</span>: Used as a garnish and for its fresh, citrusy leaves and roots, which add depth to <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>sauces and stocks. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> 12 </span><span style=\"font-weight: bold\">Shrimp Paste</span>: A very pungent paste made from fermented shrimp, used sparingly to enrich the flavor of <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>dishes. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> These ingredients are commonly used in combination to create the complex and rich flavors characteristic of <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> Thai cuisine. Fresh and high-quality versions of these ingredients contribute significantly to the authenticity <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> of Thai dishes. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[36m╭─\u001b[0m\u001b[36m──────────────────────────────────────────────────\u001b[0m\u001b[36m Response \u001b[0m\u001b[36m───────────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m Thai cooking is known for its balance of five fundamental taste senses in each dish or the overall meal: sour, \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m sweet, salty, bitter, and spicy. Essential ingredients that help achieve this balance include: \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 1 \u001b[0m\u001b[1mFish Sauce (Nam Pla)\u001b[0m: A salty, savory sauce made from fermented fish. It's a cornerstone of Thai cooking, \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mused to add depth of flavor. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 2 \u001b[0m\u001b[1mLime Juice\u001b[0m: Used to add a fresh, tangy citrus flavor to dishes, especially in salads and some curries. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 3 \u001b[0m\u001b[1mChilies\u001b[0m: Thai cuisine often features fresh Thai bird's eye chilies for heat, as well as dried chilies for \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mcertain recipes. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 4 \u001b[0m\u001b[1mLemongrass\u001b[0m: A citrusy, fragrant herb that is often used in soups, curries, and teas. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 5 \u001b[0m\u001b[1mGalangal\u001b[0m: Similar to ginger, galangal has a sharp, peppery flavor and is used in soups and curries. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 6 \u001b[0m\u001b[1mKaffir Lime Leaves\u001b[0m: They add a distinct, zesty citrus aroma and flavor to Thai dishes. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 7 \u001b[0m\u001b[1mCoconut Milk\u001b[0m: Adds richness and creaminess, commonly used in curries and desserts. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 8 \u001b[0m\u001b[1mPalm Sugar\u001b[0m: Used as a sweetener, palm sugar has a nuanced flavor, unlike regular sugar. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 9 \u001b[0m\u001b[1mTamarind Paste\u001b[0m: Provides a sour element to many dishes, particularly pad Thai and some sauces. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 10 \u001b[0m\u001b[1mThai Basil\u001b[0m: Adds a spicy, peppery flavor, different from the sweet basil used in Western cooking. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 11 \u001b[0m\u001b[1mCoriander (Cilantro)\u001b[0m: Used as a garnish and for its fresh, citrusy leaves and roots, which add depth to \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0msauces and stocks. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m 12 \u001b[0m\u001b[1mShrimp Paste\u001b[0m: A very pungent paste made from fermented shrimp, used sparingly to enrich the flavor of \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mdishes. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m These ingredients are commonly used in combination to create the complex and rich flavors characteristic of \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m Thai cuisine. Fresh and high-quality versions of these ingredients contribute significantly to the authenticity \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m of Thai dishes. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"�� Answer: Thai cooking is known for its balance of five fundamental taste senses in each dish or the overall meal: sour, sweet, salty, bitter, and spicy. Essential ingredients that help achieve this balance include:\n",
|
||||
"\n",
|
||||
"1. **Fish Sauce (Nam Pla)**: A salty, savory sauce made from fermented fish. It's a cornerstone of Thai cooking, used to add depth of flavor.\n",
|
||||
"\n",
|
||||
"2. **Lime Juice**: Used to add a fresh, tangy citrus flavor to dishes, especially in salads and some curries.\n",
|
||||
"\n",
|
||||
"3. **Chilies**: Thai cuisine often features fresh Thai bird's eye chilies for heat, as well as dried chilies for certain recipes.\n",
|
||||
"\n",
|
||||
"4. **Lemongrass**: A citrusy, fragrant herb that is often used in soups, curries, and teas.\n",
|
||||
"\n",
|
||||
"5. **Galangal**: Similar to ginger, galangal has a sharp, peppery flavor and is used in soups and curries.\n",
|
||||
"\n",
|
||||
"6. **Kaffir Lime Leaves**: They add a distinct, zesty citrus aroma and flavor to Thai dishes.\n",
|
||||
"\n",
|
||||
"7. **Coconut Milk**: Adds richness and creaminess, commonly used in curries and desserts.\n",
|
||||
"\n",
|
||||
"8. **Palm Sugar**: Used as a sweetener, palm sugar has a nuanced flavor, unlike regular sugar.\n",
|
||||
"\n",
|
||||
"9. **Tamarind Paste**: Provides a sour element to many dishes, particularly pad Thai and some sauces.\n",
|
||||
"\n",
|
||||
"10. **Thai Basil**: Adds a spicy, peppery flavor, different from the sweet basil used in Western cooking.\n",
|
||||
"\n",
|
||||
"11. **Coriander (Cilantro)**: Used as a garnish and for its fresh, citrusy leaves and roots, which add depth to sauces and stocks.\n",
|
||||
"\n",
|
||||
"12. **Shrimp Paste**: A very pungent paste made from fermented shrimp, used sparingly to enrich the flavor of dishes.\n",
|
||||
"\n",
|
||||
"These ingredients are commonly used in combination to create the complex and rich flavors characteristic of Thai cuisine. Fresh and high-quality versions of these ingredients contribute significantly to the authenticity of Thai dishes.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Test question 1: Basic Thai cooking\n",
|
||||
"question1 = \"What are the essential ingredients for Thai cooking?\"\n",
|
||||
"print(f\"🤔 Question: {question1}\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" response1 = rag_agent.start(question1)\n",
|
||||
" print(f\"�� Answer: {response1}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error: {e}\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EA8l4cSVl3Lx"
|
||||
},
|
||||
"source": [
|
||||
"# Agentic RAG with GPT-5 - Colab Notebook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "I-8nOxJ2l7nO"
|
||||
},
|
||||
"source": [
|
||||
"This Colab notebook sets up a Streamlit app using PraisonAI Agents framework.\n",
|
||||
"It allows you to interact with an AI agent powered by GPT-5 for Q&A.\n",
|
||||
"Currently, the knowledge base functionality is disabled due to compatibility issues."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "g5I1tMkxmDQe"
|
||||
},
|
||||
"source": [
|
||||
"- GPT-5 Integration (via OpenAI API)\n",
|
||||
"- Streamlit Web App\n",
|
||||
"- Agent Framework: PraisonAI Agents\n",
|
||||
"- Markdown-formatted responses"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "yeEno7U0q2U_"
|
||||
},
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/Dhivya-Bharathy/PraisonAI/blob/main/examples/cookbooks/Qwen2.5_InstructionAgent.ipynb)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "WV2GnzZCmGBR"
|
||||
},
|
||||
"source": [
|
||||
"# Dependencies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "20wAadXemJ0k"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install praisonaiagents python-dotenv streamlit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "l71uCRarndvy"
|
||||
},
|
||||
"source": [
|
||||
"# Set API Key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"id": "brfc-ZZKnfre"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"OPENAI_API_KEY = \"Enter your api key here\"\n",
|
||||
"\n",
|
||||
"os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "d1fGzyQCn1aQ"
|
||||
},
|
||||
"source": [
|
||||
"# Tools and Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"id": "GcNPLkZXntIS"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import streamlit as st\n",
|
||||
"from praisonaiagents import Agent\n",
|
||||
"\n",
|
||||
"# YAML Prompt Configuration\n",
|
||||
"agent_config = {\n",
|
||||
" \"name\": \"Knowledge Agent\",\n",
|
||||
" \"instructions\": [\n",
|
||||
" \"You are a helpful AI assistant. Answer questions based on your general knowledge.\",\n",
|
||||
" \"Provide clear, well-structured answers in markdown format.\",\n",
|
||||
" \"Use proper markdown formatting with headers, lists, and emphasis where appropriate.\",\n",
|
||||
" \"Structure your response with clear sections and bullet points when helpful.\",\n",
|
||||
" ],\n",
|
||||
" \"llm\": \"gpt-5-nano\",\n",
|
||||
" \"markdown\": True,\n",
|
||||
" \"verbose\": True\n",
|
||||
"}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "jkmrh0gIn_4a"
|
||||
},
|
||||
"source": [
|
||||
"# Initialize Agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"id": "qaJvf8RyoAsB"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def create_agent():\n",
|
||||
" \"\"\"Create an agent with reasoning capabilities\"\"\"\n",
|
||||
" return Agent(\n",
|
||||
" name=agent_config[\"name\"],\n",
|
||||
" instructions=agent_config[\"instructions\"],\n",
|
||||
" llm=agent_config[\"llm\"],\n",
|
||||
" markdown=agent_config[\"markdown\"],\n",
|
||||
" verbose=agent_config[\"verbose\"]\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kvGJh3bbqTmh"
|
||||
},
|
||||
"source": [
|
||||
"# Main Function"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000,
|
||||
"referenced_widgets": [
|
||||
"ad296d97510246de8eb0138ce8a8a166",
|
||||
"f6e502b3dca442f0ae6c5739f5a6d159"
|
||||
]
|
||||
},
|
||||
"id": "_QyxzqKKqUrW",
|
||||
"outputId": "97be7e02-b034-4004-e9e3-56337e3f2887"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"�� Initializing Agentic RAG with GPT-5...\n",
|
||||
"📚 Loading agent...\n",
|
||||
"✅ Agent loaded successfully!\n",
|
||||
"\n",
|
||||
"🎯 Example queries you can try:\n",
|
||||
"1. What is PraisonAI and how do Agents work?\n",
|
||||
"2. What are Teams in PraisonAI and how do they work?\n",
|
||||
"3. Give me a step-by-step guide to building a RAG system.\n",
|
||||
"4. What are AI Agents?\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
"\n",
|
||||
"🤔 Enter your question (or 'quit' to exit):\n",
|
||||
"> What is PraisonAI and how do Agents work?\n",
|
||||
"🔍 Processing your question...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">╭─ </span><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8; font-weight: bold\">Agent Info</span><span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\"> ────────────────────────────────────────────────────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #ff9b9b; text-decoration-color: #ff9b9b; font-weight: bold\">👤 Agent:</span> <span style=\"color: #ffe5e5; text-decoration-color: #ffe5e5\">Knowledge Agent</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #b4b4b3; text-decoration-color: #b4b4b3; font-weight: bold\">Role:</span> <span style=\"color: #ffe5e5; text-decoration-color: #ffe5e5\">Assistant</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span> <span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">│</span>\n",
|
||||
"<span style=\"color: #d2e3c8; text-decoration-color: #d2e3c8\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[38;2;210;227;200m╭─\u001b[0m\u001b[38;2;210;227;200m \u001b[0m\u001b[1;38;2;210;227;200mAgent Info\u001b[0m\u001b[38;2;210;227;200m \u001b[0m\u001b[38;2;210;227;200m───────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\u001b[38;2;210;227;200m─╮\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[1;38;2;255;155;155m👤 Agent:\u001b[0m \u001b[38;2;255;229;229mKnowledge Agent\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[1;38;2;180;180;179mRole:\u001b[0m \u001b[38;2;255;229;229mAssistant\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m│\u001b[0m \u001b[38;2;210;227;200m│\u001b[0m\n",
|
||||
"\u001b[38;2;210;227;200m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "ad296d97510246de8eb0138ce8a8a166",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Output()"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"></pre>\n"
|
||||
],
|
||||
"text/plain": []
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #7f7f7f; text-decoration-color: #7f7f7f\">Response generated in 9.4s</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[2mResponse generated in 9.4s\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #008080; text-decoration-color: #008080\">╭───────────────────────────────────────────────────── Task ──────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> What is PraisonAI and how do Agents work? <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[36m╭─\u001b[0m\u001b[36m────────────────────────────────────────────────────\u001b[0m\u001b[36m Task \u001b[0m\u001b[36m─────────────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m What is PraisonAI and how do Agents work? \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #008080; text-decoration-color: #008080\">╭─────────────────────────────────────────────────── Response ────────────────────────────────────────────────────╮</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┃ <span style=\"font-weight: bold\">What is PraisonAI?</span> ┃ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> PraisonAI is a platform designed to leverage artificial intelligence to enhance personal and business <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> productivity. It typically involves developing AI models that can assist in various tasks, automate processes, <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> and provide insights based on data analysis. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"font-weight: bold; text-decoration: underline\">Key Features of PraisonAI</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Task Automation</span>: Automates repetitive tasks, allowing users to focus on more strategic activities. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Data Insights</span>: Analyzes large sets of data to provide actionable insights. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Natural Language Processing</span>: Enables users to interact with the AI through natural language, making it <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>user-friendly. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Customizable Solutions</span>: Tailors AI applications to meet specific user needs across various industries. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┃ <span style=\"font-weight: bold\">How Do Agents Work in PraisonAI?</span> ┃ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> Agents in PraisonAI refer to autonomous or semi-autonomous AI systems that perform designated tasks or <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> functions. Here’s how they typically operate: <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"font-weight: bold; text-decoration: underline\">1. Types of Agents</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Reactive Agents</span>: Respond to the current environment without internal memory. They act solely based on <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>current inputs. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Deliberative Agents</span>: Have a model of their environment, plan their actions, and retain memories of past <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>interactions. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Learning Agents</span>: Improve their performance over time through experience, adapting to new information. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"font-weight: bold; text-decoration: underline\">2. Functionality of Agents</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Task Execution</span>: Agents can carry out specific tasks as programmed, such as scheduling, data entry, or <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>customer support. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Interaction</span>: They often interact with users through chat interfaces or APIs, providing a seamless user <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>experience. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Integration</span>: Can be integrated with other systems or software to enhance functionality, such as CRM systems, <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>databases, or other AI tools. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Continuous Learning</span>: Agents may utilize machine learning algorithms to adapt and evolve based on user <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> </span>interactions and data received. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"font-weight: bold; text-decoration: underline\">3. Benefits of Using Agents</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Increased Efficiency</span>: Automate routine tasks to reduce workload. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">24/7 Availability</span>: Provide consistent support and functionality without downtime. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Scalability</span>: Easily scale operations by deploying more agents as needed. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #808000; text-decoration-color: #808000; font-weight: bold\"> • </span><span style=\"font-weight: bold\">Data-Driven Decisions</span>: Use data insights to inform strategy and operations. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"font-weight: bold; text-decoration: underline\">Conclusion</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> PraisonAI represents a robust platform for enhancing productivity through AI, with agents serving as the heart <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> of this solution, performing various tasks and supporting users in achieving their goals efficiently. Whether <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> they are simple reactive agents or complex learning entities, their design focuses on making operations <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">│</span> smoother and more effective. <span style=\"color: #008080; text-decoration-color: #008080\">│</span>\n",
|
||||
"<span style=\"color: #008080; text-decoration-color: #008080\">╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[36m╭─\u001b[0m\u001b[36m──────────────────────────────────────────────────\u001b[0m\u001b[36m Response \u001b[0m\u001b[36m───────────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┃ \u001b[1mWhat is PraisonAI?\u001b[0m ┃ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m PraisonAI is a platform designed to leverage artificial intelligence to enhance personal and business \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m productivity. It typically involves developing AI models that can assist in various tasks, automate processes, \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m and provide insights based on data analysis. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;4mKey Features of PraisonAI\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mTask Automation\u001b[0m: Automates repetitive tasks, allowing users to focus on more strategic activities. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mData Insights\u001b[0m: Analyzes large sets of data to provide actionable insights. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mNatural Language Processing\u001b[0m: Enables users to interact with the AI through natural language, making it \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0muser-friendly. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mCustomizable Solutions\u001b[0m: Tailors AI applications to meet specific user needs across various industries. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┃ \u001b[1mHow Do Agents Work in PraisonAI?\u001b[0m ┃ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m Agents in PraisonAI refer to autonomous or semi-autonomous AI systems that perform designated tasks or \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m functions. Here’s how they typically operate: \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;4m1. \u001b[0m\u001b[1;4mTypes of Agents\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mReactive Agents\u001b[0m: Respond to the current environment without internal memory. They act solely based on \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mcurrent inputs. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mDeliberative Agents\u001b[0m: Have a model of their environment, plan their actions, and retain memories of past \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0minteractions. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mLearning Agents\u001b[0m: Improve their performance over time through experience, adapting to new information. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;4m2. \u001b[0m\u001b[1;4mFunctionality of Agents\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mTask Execution\u001b[0m: Agents can carry out specific tasks as programmed, such as scheduling, data entry, or \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mcustomer support. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mInteraction\u001b[0m: They often interact with users through chat interfaces or APIs, providing a seamless user \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mexperience. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mIntegration\u001b[0m: Can be integrated with other systems or software to enhance functionality, such as CRM systems, \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0mdatabases, or other AI tools. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mContinuous Learning\u001b[0m: Agents may utilize machine learning algorithms to adapt and evolve based on user \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m \u001b[0minteractions and data received. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;4m3. \u001b[0m\u001b[1;4mBenefits of Using Agents\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mIncreased Efficiency\u001b[0m: Automate routine tasks to reduce workload. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1m24/7 Availability\u001b[0m: Provide consistent support and functionality without downtime. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mScalability\u001b[0m: Easily scale operations by deploying more agents as needed. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;33m • \u001b[0m\u001b[1mData-Driven Decisions\u001b[0m: Use data insights to inform strategy and operations. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[1;4mConclusion\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m PraisonAI represents a robust platform for enhancing productivity through AI, with agents serving as the heart \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m of this solution, performing various tasks and supporting users in achieving their goals efficiently. Whether \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m they are simple reactive agents or complex learning entities, their design focuses on making operations \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m│\u001b[0m smoother and more effective. \u001b[36m│\u001b[0m\n",
|
||||
"\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"💡 Answer:\n",
|
||||
"========================================\n",
|
||||
"# What is PraisonAI?\n",
|
||||
"\n",
|
||||
"PraisonAI is a platform designed to leverage artificial intelligence to enhance personal and business productivity. It typically involves developing AI models that can assist in various tasks, automate processes, and provide insights based on data analysis.\n",
|
||||
"\n",
|
||||
"## Key Features of PraisonAI\n",
|
||||
"\n",
|
||||
"- **Task Automation**: Automates repetitive tasks, allowing users to focus on more strategic activities.\n",
|
||||
"- **Data Insights**: Analyzes large sets of data to provide actionable insights.\n",
|
||||
"- **Natural Language Processing**: Enables users to interact with the AI through natural language, making it user-friendly.\n",
|
||||
"- **Customizable Solutions**: Tailors AI applications to meet specific user needs across various industries.\n",
|
||||
"\n",
|
||||
"# How Do Agents Work in PraisonAI?\n",
|
||||
"\n",
|
||||
"Agents in PraisonAI refer to autonomous or semi-autonomous AI systems that perform designated tasks or functions. Here’s how they typically operate:\n",
|
||||
"\n",
|
||||
"## 1. **Types of Agents**\n",
|
||||
"\n",
|
||||
"- **Reactive Agents**: Respond to the current environment without internal memory. They act solely based on current inputs.\n",
|
||||
"- **Deliberative Agents**: Have a model of their environment, plan their actions, and retain memories of past interactions.\n",
|
||||
"- **Learning Agents**: Improve their performance over time through experience, adapting to new information.\n",
|
||||
"\n",
|
||||
"## 2. **Functionality of Agents**\n",
|
||||
"\n",
|
||||
"- **Task Execution**: Agents can carry out specific tasks as programmed, such as scheduling, data entry, or customer support.\n",
|
||||
"- **Interaction**: They often interact with users through chat interfaces or APIs, providing a seamless user experience.\n",
|
||||
"- **Integration**: Can be integrated with other systems or software to enhance functionality, such as CRM systems, databases, or other AI tools.\n",
|
||||
"- **Continuous Learning**: Agents may utilize machine learning algorithms to adapt and evolve based on user interactions and data received.\n",
|
||||
"\n",
|
||||
"## 3. **Benefits of Using Agents**\n",
|
||||
"\n",
|
||||
"- **Increased Efficiency**: Automate routine tasks to reduce workload.\n",
|
||||
"- **24/7 Availability**: Provide consistent support and functionality without downtime.\n",
|
||||
"- **Scalability**: Easily scale operations by deploying more agents as needed.\n",
|
||||
"- **Data-Driven Decisions**: Use data insights to inform strategy and operations.\n",
|
||||
"\n",
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"PraisonAI represents a robust platform for enhancing productivity through AI, with agents serving as the heart of this solution, performing various tasks and supporting users in achieving their goals efficiently. Whether they are simple reactive agents or complex learning entities, their design focuses on making operations smoother and more effective.\n",
|
||||
"========================================\n",
|
||||
"\n",
|
||||
"🤔 Enter your question (or 'quit' to exit):\n",
|
||||
"> quit\n",
|
||||
"👋 Goodbye!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def main():\n",
|
||||
" print(\"�� Initializing Agentic RAG with GPT-5...\")\n",
|
||||
"\n",
|
||||
" # Check if API key is set\n",
|
||||
" if not OPENAI_API_KEY or OPENAI_API_KEY == \"your_openai_api_key_here\":\n",
|
||||
" print(\"❌ Please set your OpenAI API key first!\")\n",
|
||||
" print(\"�� Get your key from: https://platform.openai.com/\")\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" # Create agent\n",
|
||||
" print(\"📚 Loading agent...\")\n",
|
||||
" agent = create_agent()\n",
|
||||
" print(\"✅ Agent loaded successfully!\")\n",
|
||||
"\n",
|
||||
" # Example queries\n",
|
||||
" example_queries = [\n",
|
||||
" \"What is PraisonAI and how do Agents work?\",\n",
|
||||
" \"What are Teams in PraisonAI and how do they work?\",\n",
|
||||
" \"Give me a step-by-step guide to building a RAG system.\",\n",
|
||||
" \"What are AI Agents?\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" print(\"\\n🎯 Example queries you can try:\")\n",
|
||||
" for i, query in enumerate(example_queries, 1):\n",
|
||||
" print(f\"{i}. {query}\")\n",
|
||||
"\n",
|
||||
" print(\"\\n\" + \"=\"*60)\n",
|
||||
"\n",
|
||||
" # Interactive query loop\n",
|
||||
" while True:\n",
|
||||
" print(\"\\n🤔 Enter your question (or 'quit' to exit):\")\n",
|
||||
" user_query = input(\"> \")\n",
|
||||
"\n",
|
||||
" if user_query.lower() in ['quit', 'exit', 'q']:\n",
|
||||
" print(\"👋 Goodbye!\")\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" if user_query.strip():\n",
|
||||
" print(\"🔍 Processing your question...\")\n",
|
||||
" try:\n",
|
||||
" response = agent.start(user_query)\n",
|
||||
" print(\"\\n💡 Answer:\")\n",
|
||||
" print(\"=\"*40)\n",
|
||||
" print(response)\n",
|
||||
" print(\"=\"*40)\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Error: {str(e)}\")\n",
|
||||
" print(f\"🔍 Error type: {type(e).__name__}\")\n",
|
||||
" print(\"💡 This might be due to API issues or model access problems.\")\n",
|
||||
" else:\n",
|
||||
" print(\"❌ Please enter a valid question.\")\n",
|
||||
"\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Failed to initialize agent: {str(e)}\")\n",
|
||||
" print(\"�� Make sure your API key is correct and you have sufficient credits.\")\n",
|
||||
"\n",
|
||||
"# Run the main function\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" main()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -252,7 +252,7 @@
|
||||
"text": [
|
||||
"\u001b[95m \n",
|
||||
"\n",
|
||||
"[{'title': 'CrewAI uses third-party models to automate business tasks', 'href': 'https://techcrunch.com/2024/10/22/crewai-uses-third-party-models-to-automate-business-tasks/', 'body': 'CrewAI, a startup developing a hub where companies can build automations using third-party AI models, has raised $18 million from investors including Andrew Ng. ... 4:00 AM PDT · October 22, 2024 ...'}, {'title': 'CrewAI Launches Multi-Agentic Platform to Deliver on the Promise of ...', 'href': 'https://markets.businessinsider.com/news/stocks/crewai-launches-multi-agentic-platform-to-deliver-on-the-promise-of-generative-ai-for-enterprise-1033870246?op=1', 'body': 'SAN FRANCISCO and SÃO PAULO, Oct. 22, 2024 (GLOBE NEWSWIRE) -- CrewAI, a leading multi-agent platform, today announced $18 million in total funding that included an inception round led by ...'}, {'title': 'Agentic AI startup CrewAI closes $18M funding round', 'href': 'https://siliconangle.com/2024/10/22/agentic-ai-startup-crewai-closes-18m-funding-round/', 'body': \"An e-commerce company, for example, could create an agent that uses GPT-4o to generate shopping advice based on customers' past purchases. CrewAI has created an open-source framework of the same ...\"}, {'title': 'CrewAI Company Profile 2024: Valuation, Funding & Investors - PitchBook', 'href': 'https://pitchbook.com/profiles/company/590845-78', 'body': \"Explore institutional-grade private market research from our team of analysts. ... Artificial Intelligence & Machine Learning; Artificial Intelligence & Machine Learning Report. September 19, 2024. CrewAI Executive Team (2) Update this profile Name ... CrewAI's primary industry is Business/Productivity Software. Is CrewAI a private or public ...\"}, {'title': 'CrewAI - Autonomous AI Agents in action for a market ... - LinkedIn', 'href': 'https://www.linkedin.com/pulse/crewai-autonomous-ai-agents-action-market-research-task-kithion-ovnrc', 'body': 'The will provide a detailed market trends report, the will generate a comprehensive analysis of top competitors, and the will develop a thorough marketing plan. Conclusion. CrewAI, combined with ...'}]\n",
|
||||
"[{'title': 'CrewAI uses third-party models to automate business tasks', 'href': 'https://techcrunch.com/2024/10/22/crewai-uses-third-party-models-to-automate-business-tasks/', 'body': 'CrewAI, a startup developing a hub where companies can build automations using third-party AI models, has raised $18 million from investors including Andrew Ng. ... 4:00 AM PDT · October 22, 2024 ...'}, {'title': 'CrewAI Launches Multi-Agentic Platform to Deliver on the Promise of ...', 'href': 'https://markets.businessinsider.com/news/stocks/crewai-launches-multi-agentic-platform-to-deliver-on-the-promise-of-generative-ai-for-enterprise-1033870246?op=1', 'body': 'SAN FRANCISCO and SÃO PAULO, Oct. 22, 2024 (GLOBE NEWSWIRE) -- CrewAI, a leading multi-agent platform, today announced $18 million in total funding that included an inception round led by ...'}, {'title': 'Agentic AI startup CrewAI closes $18M funding round', 'href': 'https://siliconangle.com/2024/10/22/agentic-ai-startup-crewai-closes-18m-funding-round/', 'body': \"An e-commerce company, for example, could create an agent that uses gpt-5-nano to generate shopping advice based on customers' past purchases. CrewAI has created an open-source framework of the same ...\"}, {'title': 'CrewAI Company Profile 2024: Valuation, Funding & Investors - PitchBook', 'href': 'https://pitchbook.com/profiles/company/590845-78', 'body': \"Explore institutional-grade private market research from our team of analysts. ... Artificial Intelligence & Machine Learning; Artificial Intelligence & Machine Learning Report. September 19, 2024. CrewAI Executive Team (2) Update this profile Name ... CrewAI's primary industry is Business/Productivity Software. Is CrewAI a private or public ...\"}, {'title': 'CrewAI - Autonomous AI Agents in action for a market ... - LinkedIn', 'href': 'https://www.linkedin.com/pulse/crewai-autonomous-ai-agents-action-market-research-task-kithion-ovnrc', 'body': 'The will provide a detailed market trends report, the will generate a comprehensive analysis of top competitors, and the will develop a thorough marketing plan. Conclusion. CrewAI, combined with ...'}]\n",
|
||||
"\u001b[00m\n",
|
||||
"\u001b[32;1m\u001b[1;3mThought: I have found more information about CrewAI's target customers and market positioning. CrewAI is a startup that has developed a hub where companies can build automations using third-party AI models, and it has recently raised $18 million in funding. Their target customers are likely businesses looking to automate complex tasks and improve efficiency using AI. They are positioned as a platform for deploying and managing AI agents to automate a variety of tasks.\n",
|
||||
"\n",
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
" url=url,\n",
|
||||
" word_count_threshold=1,\n",
|
||||
" extraction_strategy= LLMExtractionStrategy(\n",
|
||||
" provider=\"openai/gpt-4o\",\n",
|
||||
" provider=\"openai/gpt-5-nano\",\n",
|
||||
" api_token=os.getenv('OPENAI_API_KEY'),\n",
|
||||
" schema=ModelFee.schema(),\n",
|
||||
" extraction_type=\"schema\",\n",
|
||||
|
||||
@@ -46,7 +46,7 @@ agent = RouterAgent(
|
||||
name="Smart Assistant",
|
||||
role="Adaptive AI Assistant",
|
||||
goal="Complete tasks using the most appropriate model",
|
||||
models=["gpt-5-nano", "gpt-4o", "claude-3-5-sonnet-20241022"],
|
||||
models=["gpt-5-nano", "gpt-5-nano", "claude-3-5-sonnet-20241022"],
|
||||
routing_strategy="auto" # Automatic model selection
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ The system includes pre-configured profiles for popular models:
|
||||
| gpt-5-nano | OpenAI | Simple tasks, speed | $0.00075 |
|
||||
| gemini-1.5-flash | Google | Cost-effective, multimodal | $0.000125 |
|
||||
| claude-3-haiku | Anthropic | Fast responses | $0.0008 |
|
||||
| gpt-4o | OpenAI | General purpose | $0.0075 |
|
||||
| gpt-5-nano | OpenAI | General purpose | $0.0075 |
|
||||
| claude-3-5-sonnet-20241022 | Anthropic | Complex reasoning | $0.009 |
|
||||
| deepseek-chat | DeepSeek | Code & math | $0.0014 |
|
||||
|
||||
@@ -161,7 +161,7 @@ print(report)
|
||||
'routing_strategy': 'auto',
|
||||
'model_usage': {
|
||||
'gpt-5-nano': {'calls': 5, 'tokens': 1500, 'cost': 0.0011},
|
||||
'gpt-4o': {'calls': 2, 'tokens': 3000, 'cost': 0.0225}
|
||||
'gpt-5-nano': {'calls': 2, 'tokens': 3000, 'cost': 0.0225}
|
||||
},
|
||||
'total_cost_estimate': 0.0236,
|
||||
'total_calls': 7
|
||||
@@ -207,7 +207,7 @@ for i, agent in enumerate(auto.agents):
|
||||
name=agent.name,
|
||||
role=agent.role,
|
||||
goal=agent.goal,
|
||||
models=["gpt-5-nano", "gpt-4o", "claude-3-5-sonnet"],
|
||||
models=["gpt-5-nano", "gpt-5-nano", "claude-3-5-sonnet"],
|
||||
routing_strategy="auto"
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def example_auto_routing():
|
||||
role="Adaptive Research Assistant",
|
||||
goal="Research topics using the most appropriate AI model",
|
||||
backstory="I analyze task complexity and route to the best model",
|
||||
models=["gpt-5-nano", "gpt-4o", "claude-3-5-sonnet-20241022"],
|
||||
models=["gpt-5-nano", "gpt-5-nano", "claude-3-5-sonnet-20241022"],
|
||||
routing_strategy="auto", # Automatic model selection
|
||||
verbose=True
|
||||
)
|
||||
@@ -95,7 +95,7 @@ def example_cost_optimized_workflow():
|
||||
goal="Create high-quality content",
|
||||
models={
|
||||
"claude-3-5-sonnet-20241022": {},
|
||||
"gpt-4o": {}
|
||||
"gpt-5-nano": {}
|
||||
},
|
||||
routing_strategy="performance-optimized", # Prefer better models
|
||||
verbose=True
|
||||
@@ -158,7 +158,7 @@ def example_auto_agents_multi_provider():
|
||||
goal=agent.goal,
|
||||
backstory=agent.backstory,
|
||||
tools=agent.tools,
|
||||
models=["gpt-5-nano", "gemini/gemini-1.5-flash", "claude-3-haiku-20240307", "gpt-4o"],
|
||||
models=["gpt-5-nano", "gemini/gemini-1.5-flash", "claude-3-haiku-20240307", "gpt-5-nano"],
|
||||
routing_strategy="auto",
|
||||
verbose=True
|
||||
)
|
||||
@@ -192,7 +192,7 @@ def example_custom_routing():
|
||||
# Create custom model profiles for specific use cases
|
||||
custom_models = [
|
||||
ModelProfile(
|
||||
name="gpt-4o",
|
||||
name="gpt-5-nano",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.VERY_COMPLEX),
|
||||
cost_per_1k_tokens=0.0075,
|
||||
|
||||
@@ -19,7 +19,7 @@ from praisonaiagents.tools import duckduckgo
|
||||
# Define model options with cost considerations
|
||||
# Using different models for different complexity levels
|
||||
cheap_model = "gpt-5-nano" # For simple tasks
|
||||
balanced_model = "gpt-4o" # For moderate complexity
|
||||
balanced_model = "gpt-5-nano" # For moderate complexity
|
||||
premium_model = "claude-3-5-sonnet-20241022" # For complex tasks
|
||||
|
||||
# Create RouterAgent with cost optimization strategy
|
||||
@@ -77,6 +77,6 @@ print("COST OPTIMIZATION ROUTER DEMONSTRATION COMPLETED")
|
||||
print("="*80)
|
||||
print("The RouterAgent automatically selected appropriate models based on task complexity:")
|
||||
print("- Simple factual questions → Cheaper model (gpt-5-nano)")
|
||||
print("- Moderate analysis tasks → Balanced model (gpt-4o)")
|
||||
print("- Moderate analysis tasks → Balanced model (gpt-5-nano)")
|
||||
print("- Complex strategic analysis → Premium model (claude-3-5-sonnet)")
|
||||
print("- This approach optimizes costs while maintaining quality for each task type")
|
||||
@@ -127,9 +127,9 @@ def main():
|
||||
verbose=True,
|
||||
allow_delegation=False,
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
reflect_llm="gpt-4o",
|
||||
reflect_llm="gpt-5-nano",
|
||||
min_reflect=2,
|
||||
max_reflect=4
|
||||
)
|
||||
@@ -142,7 +142,7 @@ def main():
|
||||
making complex tech topics interesting and easy to understand.""",
|
||||
verbose=True,
|
||||
allow_delegation=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
tools=[],
|
||||
markdown=True
|
||||
)
|
||||
@@ -195,7 +195,7 @@ def main():
|
||||
tasks=[task1, task2, task3, task4],
|
||||
verbose=True,
|
||||
process="sequential",
|
||||
manager_llm="gpt-4o"
|
||||
manager_llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
agents.start()
|
||||
|
||||
@@ -39,9 +39,9 @@ researcher = Agent(
|
||||
verbose=True,
|
||||
allow_delegation=False,
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
reflect_llm="gpt-4o",
|
||||
reflect_llm="gpt-5-nano",
|
||||
min_reflect=2,
|
||||
max_reflect=4
|
||||
)
|
||||
@@ -53,7 +53,7 @@ writer = Agent(
|
||||
making complex tech topics interesting and easy to understand.""",
|
||||
verbose=True,
|
||||
allow_delegation=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
tools=[],
|
||||
markdown=True
|
||||
)
|
||||
@@ -104,7 +104,7 @@ agents = PraisonAIAgents(
|
||||
tasks=[task1, task2, task3, task4],
|
||||
verbose=False,
|
||||
process="sequential", # "sequential" or "hierarchical"
|
||||
manager_llm="gpt-4o"
|
||||
manager_llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
|
||||
@@ -30,7 +30,7 @@ with st.sidebar:
|
||||
# Model selection
|
||||
model = st.selectbox(
|
||||
"Select Model",
|
||||
["gpt-5-nano", "gpt-4o", "gpt-3.5-turbo", "claude-3-opus"],
|
||||
["gpt-5-nano", "gpt-5-nano", "gpt-3.5-turbo", "claude-3-opus"],
|
||||
index=0
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ agent = Agent(
|
||||
"Help users understand security concepts, analyze security threats, "
|
||||
"and provide guidance on security best practices, risk assessment, "
|
||||
"and security architecture design.",
|
||||
llm="openai/gpt-4o"
|
||||
llm="openai/gpt-5-nano"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your cybersecurity assistant. "
|
||||
|
||||
@@ -5,7 +5,7 @@ agent = Agent(
|
||||
"Help users design data pipelines, optimize database performance, "
|
||||
"create ETL processes, and provide guidance on data architecture, "
|
||||
"data modeling, and big data technologies.",
|
||||
llm="openai/gpt-4o"
|
||||
llm="openai/gpt-5-nano"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your data engineer assistant. "
|
||||
|
||||
@@ -5,7 +5,7 @@ agent = Agent(
|
||||
"Help users develop marketing strategies, analyze market trends, "
|
||||
"create campaign plans, and provide guidance on digital marketing, "
|
||||
"brand positioning, and customer acquisition strategies.",
|
||||
llm="openai/gpt-4o"
|
||||
llm="openai/gpt-5-nano"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your marketing strategist assistant. "
|
||||
|
||||
@@ -5,7 +5,7 @@ agent = Agent(
|
||||
"Help users with product strategy, feature prioritization, "
|
||||
"user research analysis, and product roadmap planning. Provide "
|
||||
"guidance on market analysis, competitive positioning, and user experience.",
|
||||
llm="openai/gpt-4o"
|
||||
llm="openai/gpt-5-nano"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your product manager assistant. "
|
||||
|
||||
@@ -5,7 +5,7 @@ agent = Agent(
|
||||
"Help users design software systems, create architecture diagrams, "
|
||||
"plan system integrations, and provide guidance on scalability, "
|
||||
"security, and best practices for software development.",
|
||||
llm="openai/gpt-4o"
|
||||
llm="openai/gpt-5-nano"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your software architect assistant. "
|
||||
|
||||
@@ -7,7 +7,7 @@ from praisonaiagents import Agent
|
||||
# Initialize Agent with OpenAI GPT-4
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="openai/gpt-4o",
|
||||
llm="openai/gpt-5-nano",
|
||||
)
|
||||
|
||||
# Example conversation
|
||||
|
||||
@@ -28,7 +28,7 @@ web_scraper_agent = Agent(
|
||||
goal="Extract URLs from https://quotes.toscrape.com/",
|
||||
backstory="An expert in data extraction from websites, adept at navigating and retrieving detailed information.",
|
||||
tools=[code_interpret],
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
url_data_extractor_agent = Agent(
|
||||
@@ -37,7 +37,7 @@ url_data_extractor_agent = Agent(
|
||||
goal="Crawl each URL for data extraction",
|
||||
backstory="Specializes in crawling websites to gather comprehensive data, ensuring nothing is missed from each link.",
|
||||
tools=[code_interpret],
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
blog_writer_agent = Agent(
|
||||
@@ -46,7 +46,7 @@ blog_writer_agent = Agent(
|
||||
goal="Create engaging and insightful blog posts from provided data",
|
||||
backstory="An experienced content creator and storyteller with a knack for weaving compelling narratives. Skilled at analyzing quotes and creating meaningful connections that resonate with readers.",
|
||||
tools=[code_interpret],
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
# 2) Create Tasks
|
||||
@@ -88,7 +88,7 @@ agents_manager = PraisonAIAgents(
|
||||
tasks=[task_url_extraction, task_data_extraction, blog_writing_task],
|
||||
verbose=True,
|
||||
process="hierarchical",
|
||||
manager_llm="gpt-4o"
|
||||
manager_llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
result = agents_manager.start()
|
||||
@@ -56,7 +56,7 @@ web_scraper_agent = Agent(
|
||||
min_reflect=3,
|
||||
max_reflect=5,
|
||||
tools=[code_interpret, save_to_file, run_terminal_command],
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
task = Task(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from praisonaiagents import Agent
|
||||
import os
|
||||
|
||||
# Set OpenAI API key if not already set
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Please set your OpenAI API key: export OPENAI_API_KEY='your-api-key-here'")
|
||||
exit(1)
|
||||
|
||||
# Create the RAG agent with web search capabilities
|
||||
rag_agent = Agent(
|
||||
instructions="""You are a helpful AI assistant specialized in Thai recipes and cooking.
|
||||
|
||||
You have access to a PDF knowledge base about Thai recipes from: https://phi-public.s3.amazonaws.com/recipes/ThaiRecipes.pdf
|
||||
|
||||
You can also search the web for additional information about Thai cooking, ingredients, and techniques.
|
||||
|
||||
When answering questions:
|
||||
1. Use your knowledge about Thai cuisine to provide helpful information
|
||||
2. If needed, search the web for additional details, current information, or clarification
|
||||
3. Provide comprehensive, helpful answers about Thai cuisine
|
||||
4. Always be informative and helpful about Thai cooking!
|
||||
|
||||
You can use the internet_search function to search the web when needed.""",
|
||||
llm="gpt-4o",
|
||||
markdown=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🤖 Thai Recipe RAG Agent is ready!")
|
||||
print("Ask me anything about Thai recipes or cooking!")
|
||||
print("Type 'quit' to exit.\n")
|
||||
|
||||
while True:
|
||||
user_input = input("You: ")
|
||||
if user_input.lower() in ['quit', 'exit', 'bye']:
|
||||
print("👋 Goodbye! Happy cooking!")
|
||||
break
|
||||
|
||||
try:
|
||||
response = rag_agent.start(user_input)
|
||||
print(f"\n🤖 Assistant: {response}\n")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}\n")
|
||||
@@ -0,0 +1,154 @@
|
||||
# 🧠 Agentic RAG with GPT-5
|
||||
|
||||
An agentic RAG application built with the PraisonAI Agents framework, featuring GPT-5 and built-in vector search for efficient knowledge retrieval and question answering.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🤖 GPT-5**: Latest OpenAI model for intelligent responses
|
||||
- **🗄️ Built-in Vector Search**: ChromaDB integration for fast similarity search
|
||||
- **🔍 Agentic RAG**: Intelligent retrieval augmented generation
|
||||
- **📝 Markdown Formatting**: Beautiful, structured responses
|
||||
- **🌐 Dynamic Knowledge**: Add URLs to expand knowledge base
|
||||
- **⚡ Real-time Responses**: Fast answer generation- **🎯 Clean Interface**: Simplified UI without configuration complexity
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- OpenAI API key with GPT-5 access
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone and navigate to the project**
|
||||
```bash
|
||||
cd rag_examples/agentic_rag_gpt5
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Set up your OpenAI API key**
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-api-key-here"
|
||||
```
|
||||
Or create a `.env` file:
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key-here
|
||||
```
|
||||
|
||||
4. **Run the application**
|
||||
```bash
|
||||
streamlit run agentic_rag_gpt5.py
|
||||
```
|
||||
|
||||
## 🎯 How to Use
|
||||
|
||||
1. **Enter your OpenAI API key** in the sidebar
|
||||
2. **Add knowledge sources** by entering URLs in the sidebar
|
||||
3. **Ask questions** using the text area or suggested prompts
|
||||
4. **Get answers** with markdown formatting
|
||||
|
||||
### Suggested Questions
|
||||
|
||||
- **"What is PraisonAI?"** - Learn about the PraisonAI Agents framework
|
||||
- **"Teams in PraisonAI"** - Understand how teams work in PraisonAI
|
||||
- **"Build RAG system"** - Get a step-by-step guide to building RAG systems
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **`Agent`**: PraisonAI Agents framework for intelligent Q&A
|
||||
- **`knowledge`**: Built-in knowledge base that handles URLs and documents
|
||||
- **`llm`**: OpenAI GPT-5-nano for generating responses
|
||||
- **Built-in Vector Search**: Automatic similarity search without external setup
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Knowledge Loading**: URLs are processed and stored in the built-in vector database
|
||||
2. **Vector Search**: OpenAI embeddings enable semantic search
|
||||
3. **Response Generation**: GPT-5-nano processes information and generates answers
|
||||
4. **Formatted Output**: Markdown-formatted responses
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Database Settings
|
||||
- **Vector DB**: Built-in vector database with automatic indexing
|
||||
- **Storage**: Local storage managed by PraisonAI Agents
|
||||
- **Search**: Automatic similarity search
|
||||
|
||||
### Model Configuration
|
||||
- **LLM**: OpenAI GPT-5-nano
|
||||
- **Embeddings**: Automatic handling by PraisonAI Agents
|
||||
- **Vector Store**: Built-in with automatic document processing
|
||||
|
||||
## 📚 Knowledge Management
|
||||
|
||||
### Adding Sources
|
||||
- Use the sidebar to add new URLs
|
||||
- Sources are automatically processed and indexed
|
||||
- Current sources are displayed as numbered list
|
||||
|
||||
### Default Knowledge
|
||||
- Starts with PraisonAI documentation: `https://docs.praisonai.com/introduction/agents.md`
|
||||
- Expandable with any web-based documentation
|
||||
|
||||
## 🎨 UI Features
|
||||
|
||||
### Sidebar
|
||||
- **API Key Management**: Secure input for OpenAI credentials
|
||||
- **URL Addition**: Dynamic knowledge base expansion
|
||||
- **Current Sources**: Numbered list of loaded URLs
|
||||
|
||||
### Main Interface
|
||||
- **Suggested Prompts**: Quick access to common questions
|
||||
- **Query Input**: Large text area for custom questions
|
||||
- **Fast Responses**: Quick answer generation
|
||||
- **Markdown Rendering**: Beautiful formatted responses
|
||||
|
||||
## 🛠️ Technical Details
|
||||
|
||||
### Dependencies
|
||||
```
|
||||
streamlit>=1.28.0
|
||||
praisonaiagents>=0.1.0
|
||||
openai>=1.0.0
|
||||
python-dotenv>=1.0.0
|
||||
```
|
||||
|
||||
### Key Features
|
||||
- **Built-in Knowledge Base**: Automatic document processing and indexing
|
||||
- **Vector Search**: Efficient similarity search with built-in database
|
||||
- **Caching**: Efficient resource loading with Streamlit caching
|
||||
- **Error Handling**: Graceful handling of API and processing errors
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Knowledge base not loading**
|
||||
- Check OpenAI API key is valid
|
||||
- Ensure URLs are accessible
|
||||
- Verify internet connection
|
||||
|
||||
**Agent initialization errors**
|
||||
- Check if PraisonAI Agents is properly installed
|
||||
- Verify OpenAI API key has sufficient credits
|
||||
- Ensure Python version is 3.11+
|
||||
|
||||
### Performance Tips
|
||||
- **Cache Resources**: Knowledge base and agent are cached for efficiency
|
||||
- **Built-in Vector Search**: Fast similarity search without external setup
|
||||
- **Local Storage**: Optimized local storage for optimal performance
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
- **Documentation Q&A**: Ask questions about technical documentation
|
||||
- **Research Assistant**: Get answers from multiple knowledge sources
|
||||
- **Learning Tool**: Interactive exploration of complex topics
|
||||
- **Content Discovery**: Find relevant information across multiple sources
|
||||
|
||||
**Built with ❤️ using PraisonAI Agents and GPT-5**
|
||||
@@ -0,0 +1,201 @@
|
||||
import streamlit as st
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Page configuration
|
||||
st.set_page_config(
|
||||
page_title="Agentic RAG with GPT-5",
|
||||
page_icon="🧠",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# Main title and description
|
||||
st.title("🧠 Agentic RAG with GPT-5")
|
||||
st.markdown("""
|
||||
This app demonstrates an intelligent AI agent that:
|
||||
1. **Answers** your questions clearly and concisely using GPT-5
|
||||
|
||||
⚠️ **Note**: Knowledge base functionality is temporarily disabled due to a compatibility issue with the current version of PraisonAI Agents.
|
||||
|
||||
Enter your OpenAI API key in the sidebar to get started!
|
||||
""")
|
||||
|
||||
# Sidebar for API key and settings
|
||||
with st.sidebar:
|
||||
st.header("🔧 Configuration")
|
||||
|
||||
# OpenAI API Key
|
||||
openai_key = st.text_input(
|
||||
"OpenAI API Key",
|
||||
type="password",
|
||||
value=os.getenv("OPENAI_API_KEY", ""),
|
||||
help="Get your key from https://platform.openai.com/"
|
||||
)
|
||||
|
||||
# Add URLs to knowledge base
|
||||
st.subheader("🌐 Add Knowledge Sources")
|
||||
st.info("⚠️ Knowledge base functionality is temporarily disabled due to compatibility issues.")
|
||||
new_url = st.text_input(
|
||||
"Add URL",
|
||||
placeholder="https://docs.praisonai.com/introduction",
|
||||
help="Enter a URL to add to the knowledge base (currently disabled)"
|
||||
)
|
||||
|
||||
if st.button("➕ Add URL", type="primary", disabled=True):
|
||||
st.info("Knowledge base functionality is temporarily disabled.")
|
||||
|
||||
# Check if API key is provided
|
||||
if openai_key:
|
||||
# Initialize knowledge base (cached to avoid reloading)
|
||||
@st.cache_resource(show_spinner="📚 Loading knowledge base...")
|
||||
def load_knowledge() -> list:
|
||||
"""Load and initialize the knowledge base with default URL"""
|
||||
return ["https://docs.praisonai.com/introduction/agents.md"] # Default URL
|
||||
|
||||
# Initialize agent (cached to avoid reloading)
|
||||
@st.cache_resource(show_spinner="🤖 Loading agent...")
|
||||
def load_agent(_knowledge: list) -> Agent:
|
||||
"""Create an agent with reasoning capabilities"""
|
||||
# Note: Temporarily removed knowledge parameter to avoid rerank error
|
||||
# TODO: Re-enable when PraisonAI Agents knowledge issue is resolved
|
||||
return Agent(
|
||||
name="Knowledge Agent",
|
||||
instructions=[
|
||||
"You are a helpful AI assistant. Answer questions based on your general knowledge.",
|
||||
"Provide clear, well-structured answers in markdown format.",
|
||||
"Use proper markdown formatting with headers, lists, and emphasis where appropriate.",
|
||||
"Structure your response with clear sections and bullet points when helpful.",
|
||||
],
|
||||
llm="gpt-5-nano
|
||||
markdown=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Load knowledge and agent
|
||||
knowledge = load_knowledge()
|
||||
agent = load_agent(knowledge)
|
||||
|
||||
# Display current URLs in knowledge base
|
||||
if knowledge:
|
||||
st.sidebar.subheader("📚 Current Knowledge Sources")
|
||||
for i, url in enumerate(knowledge, 1):
|
||||
st.sidebar.markdown(f"{i}. {url}")
|
||||
|
||||
# Handle URL additions
|
||||
if hasattr(st.session_state, 'urls_to_add') and st.session_state.urls_to_add:
|
||||
with st.spinner("📥 Loading new documents..."):
|
||||
knowledge.append(st.session_state.urls_to_add)
|
||||
# Reinitialize agent with new knowledge
|
||||
agent = load_agent(knowledge)
|
||||
st.success(f"✅ Added: {st.session_state.urls_to_add}")
|
||||
del st.session_state.urls_to_add
|
||||
st.rerun()
|
||||
|
||||
# Main query section
|
||||
st.divider()
|
||||
st.subheader("🤔 Ask a Question")
|
||||
|
||||
# Suggested prompts
|
||||
st.markdown("**Try these prompts:**")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
if st.button("What is PraisonAI?", use_container_width=True):
|
||||
st.session_state.query = "What is PraisonAI and how do Agents work?"
|
||||
with col2:
|
||||
if st.button("Teams in PraisonAI", use_container_width=True):
|
||||
st.session_state.query = "What are Teams in PraisonAI and how do they work?"
|
||||
with col3:
|
||||
if st.button("Build RAG system", use_container_width=True):
|
||||
st.session_state.query = "Give me a step-by-step guide to building a RAG system."
|
||||
|
||||
# Query input
|
||||
query = st.text_area(
|
||||
"Your question:",
|
||||
value=st.session_state.get("query", "What are AI Agents?"),
|
||||
height=100,
|
||||
help="Ask anything about the loaded knowledge sources"
|
||||
)
|
||||
|
||||
# Run button
|
||||
if st.button("🚀 Get Answer", type="primary"):
|
||||
if query:
|
||||
# Create container for answer
|
||||
st.markdown("### 💡 Answer")
|
||||
answer_container = st.container()
|
||||
answer_placeholder = answer_container.empty()
|
||||
|
||||
# Get the agent's response
|
||||
with st.spinner("🔍 Searching and generating answer..."):
|
||||
try:
|
||||
st.info("🤖 Agent is processing your question...")
|
||||
response = agent.start(query)
|
||||
answer_placeholder.markdown(
|
||||
response,
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Error getting response: {str(e)}")
|
||||
st.error(f"Error type: {type(e).__name__}")
|
||||
st.error(f"Full error details: {repr(e)}")
|
||||
# Try to provide a helpful response
|
||||
answer_placeholder.markdown("""
|
||||
**⚠️ Error occurred while processing your question.**
|
||||
|
||||
This might be due to:
|
||||
- Knowledge base configuration issues
|
||||
- Model access problems
|
||||
- Network connectivity issues
|
||||
|
||||
Please try again or check your OpenAI API key.
|
||||
""")
|
||||
else:
|
||||
st.error("Please enter a question")
|
||||
|
||||
else:
|
||||
# Show instructions if API key is missing
|
||||
st.info("""
|
||||
👋 **Welcome! To use this app, you need:**
|
||||
|
||||
- **OpenAI API Key** (set it in the sidebar)
|
||||
- Sign up at [platform.openai.com](https://platform.openai.com/)
|
||||
- Generate a new API key
|
||||
|
||||
Once you enter the key, the app will load the knowledge base and agent.
|
||||
""")
|
||||
|
||||
# Footer with explanation
|
||||
st.divider()
|
||||
with st.expander("📖 How This Works"):
|
||||
st.markdown("""
|
||||
**This app uses the PraisonAI Agents framework to create an intelligent Q&A system:**
|
||||
|
||||
⚠️ **Current Status**: Knowledge base functionality is temporarily disabled due to compatibility issues with the current version of PraisonAI Agents (v0.0.157).
|
||||
|
||||
**What Works Now:**
|
||||
1. **GPT-5 Integration**: OpenAI's GPT-5 model for generating intelligent responses
|
||||
2. **Markdown Formatting**: Beautiful, structured responses
|
||||
3. **Agent Framework**: PraisonAI Agents for orchestration
|
||||
|
||||
**What's Temporarily Disabled:**
|
||||
- URL-based knowledge base
|
||||
- Vector search and retrieval
|
||||
- Document processing
|
||||
|
||||
**Key Components:**
|
||||
- `Agent`: PraisonAI Agents framework for creating intelligent agents
|
||||
- `llm`: OpenAI GPT-5-nano for generating responses
|
||||
|
||||
**Why PraisonAI Agents?**
|
||||
- Easy-to-use agent framework
|
||||
- Built-in error handling
|
||||
- Clean API design
|
||||
- Perfect for prototyping and production applications
|
||||
|
||||
**Next Steps:**
|
||||
- Monitor PraisonAI Agents updates for knowledge base fixes
|
||||
- Re-enable knowledge functionality when compatible version is available
|
||||
""")
|
||||
@@ -23,7 +23,7 @@ with st.sidebar:
|
||||
"Choose LLM Model",
|
||||
options=[
|
||||
"gpt-5-nano",
|
||||
"gpt-4o",
|
||||
"gpt-5-nano",
|
||||
"gpt-3.5-turbo",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022"
|
||||
|
||||
@@ -162,7 +162,7 @@ manager_agent = Agent(
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
@@ -186,7 +186,7 @@ tool_agent = Agent(
|
||||
""",
|
||||
tools=domain_tools, # Use domain-aware tools
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
@@ -209,7 +209,7 @@ sr_pentester_agent = Agent(
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
@@ -232,7 +232,7 @@ ciso_agent = Agent(
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
@@ -268,7 +268,7 @@ agents = PraisonAIAgents(
|
||||
tasks=[tool_query_task, pentester_analysis_task, ciso_review_task],
|
||||
verbose=True,
|
||||
process="hierarchical",
|
||||
manager_llm="gpt-4o",
|
||||
manager_llm="gpt-5-nano",
|
||||
memory=True, # Enable shared memory for domain context
|
||||
memory_config={
|
||||
"provider": "rag",
|
||||
|
||||
@@ -86,3 +86,4 @@ build
|
||||
graph.py
|
||||
chroma_db/
|
||||
.qodo
|
||||
*_COMPARISON_ANALYSIS.md
|
||||
+1063
-54
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Gemini Deep Research Agent Example
|
||||
|
||||
Uses Google's deep-research-pro model via the Interactions API
|
||||
for automated research with web search.
|
||||
|
||||
Supports two modes:
|
||||
1. Polling (default): Checks status periodically, shows progress logs
|
||||
2. Streaming: Real-time output with thinking summaries (recommended)
|
||||
|
||||
Requirements:
|
||||
- GEMINI_API_KEY or GOOGLE_API_KEY environment variable
|
||||
- google-genai >= 1.55.0 (pip install google-genai)
|
||||
"""
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from praisonaiagents import DeepResearchAgent
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Gemini Researcher",
|
||||
instructions="""
|
||||
You are a professional researcher. Focus on:
|
||||
- Comprehensive analysis with multiple perspectives
|
||||
- Data-backed insights and trends
|
||||
- Clear structure with executive summary
|
||||
""",
|
||||
model="deep-research-pro", # Auto-detected as Gemini
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
print(f"Agent: {agent}")
|
||||
print(f"Provider: {agent.provider}")
|
||||
|
||||
# Streaming is enabled by default for real-time progress updates
|
||||
# This shows thinking summaries and streams the report as it's generated
|
||||
result = agent.research(
|
||||
"What is the current price of Ethereum?"
|
||||
# stream=True is the default, set stream=False to disable
|
||||
)
|
||||
|
||||
# The report is already printed during streaming when verbose=True
|
||||
# But we can also access it programmatically:
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Report length: {len(result.report)} characters")
|
||||
print(f"Interaction ID: {result.interaction_id}")
|
||||
print(f"Reasoning steps captured: {len(result.reasoning_steps)}")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
OpenAI Deep Research Agent Example
|
||||
|
||||
Uses OpenAI's o3-deep-research or o4-mini-deep-research models
|
||||
via the Responses API for automated research with web search.
|
||||
|
||||
Supports two modes:
|
||||
1. Non-streaming (default): Waits for completion, returns full report
|
||||
2. Streaming: Real-time progress with reasoning summaries and web searches
|
||||
|
||||
Requirements:
|
||||
- OPENAI_API_KEY environment variable
|
||||
"""
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from praisonaiagents import DeepResearchAgent
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Research Assistant",
|
||||
instructions="""
|
||||
You are a professional researcher. Focus on:
|
||||
- Data-rich insights with specific figures
|
||||
- Reliable sources and citations
|
||||
- Clear, structured responses
|
||||
""",
|
||||
model="o4-mini-deep-research", # or "o3-deep-research" for higher quality
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Agent: {agent}")
|
||||
print(f"Provider: {agent.provider}")
|
||||
|
||||
# Streaming is enabled by default for real-time progress updates
|
||||
# Shows: reasoning summaries, web search queries, and report text as generated
|
||||
result = agent.research(
|
||||
"What is the current price of Bitcoin?"
|
||||
# stream=True is the default, set stream=False to disable
|
||||
)
|
||||
|
||||
# The report is already printed during streaming when verbose=True
|
||||
# But we can also access it programmatically:
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Report length: {len(result.report)} characters")
|
||||
print(f"Citations: {len(result.citations)}")
|
||||
print(f"Web searches: {len(result.web_searches)}")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,94 @@
|
||||
# PraisonAI Example Plugin
|
||||
|
||||
This is a template for creating tool plugins for PraisonAI Agents.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install in development mode
|
||||
pip install -e .
|
||||
|
||||
# Or install from PyPI (after publishing)
|
||||
pip install praisonai-example-plugin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, tools are automatically discovered:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Use tools by name - they're auto-discovered!
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
tools=["example_greet", "example_math", "example_reverse"]
|
||||
)
|
||||
|
||||
# Or import and use directly
|
||||
from praisonai_example_plugin import GreetTool, MathTool, reverse_text
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
tools=[GreetTool(), MathTool(), reverse_text]
|
||||
)
|
||||
```
|
||||
|
||||
## Creating Your Own Plugin
|
||||
|
||||
1. **Copy this template**
|
||||
2. **Rename the package** in `pyproject.toml` and `src/` directory
|
||||
3. **Add your tools** following the patterns in `__init__.py`
|
||||
4. **Update entry_points** in `pyproject.toml` to register your tools
|
||||
|
||||
### Tool Patterns
|
||||
|
||||
**Class-based tool:**
|
||||
```python
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "What this tool does"
|
||||
|
||||
def run(self, param: str) -> str:
|
||||
return f"Result: {param}"
|
||||
```
|
||||
|
||||
**Decorator-based tool:**
|
||||
```python
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool
|
||||
def my_function(param: str) -> str:
|
||||
"""What this function does."""
|
||||
return f"Result: {param}"
|
||||
```
|
||||
|
||||
### Registering Tools
|
||||
|
||||
In `pyproject.toml`:
|
||||
```toml
|
||||
[project.entry-points."praisonaiagents.tools"]
|
||||
my_tool = "my_package:MyTool"
|
||||
my_function = "my_package:my_function"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev]"
|
||||
pytest
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
```bash
|
||||
pip install build twine
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,49 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonai-example-plugin"
|
||||
version = "1.0.0"
|
||||
description = "Example plugin for PraisonAI Agents - demonstrates tool plugin development"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "Your Name", email = "[email protected]"}
|
||||
]
|
||||
keywords = ["praisonai", "agents", "tools", "plugin"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"praisonaiagents>=0.0.168",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-mock>=3.10.0",
|
||||
]
|
||||
|
||||
# This is the key part - register your tools as entry points
|
||||
# The format is: tool_name = "module.path:ClassName" or "module.path:function_name"
|
||||
[project.entry-points."praisonaiagents.tools"]
|
||||
example_greet = "praisonai_example_plugin:GreetTool"
|
||||
example_math = "praisonai_example_plugin:MathTool"
|
||||
example_reverse = "praisonai_example_plugin:reverse_text"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/praisonai-example-plugin"
|
||||
Documentation = "https://github.com/yourusername/praisonai-example-plugin#readme"
|
||||
Repository = "https://github.com/yourusername/praisonai-example-plugin"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""PraisonAI Example Plugin - Demonstrates how to create tool plugins.
|
||||
|
||||
This package shows how external developers can create pip-installable
|
||||
tool plugins for PraisonAI Agents.
|
||||
|
||||
Usage after installation:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Tools are auto-discovered via entry_points!
|
||||
agent = Agent(tools=["example_greet", "example_math"])
|
||||
|
||||
# Or import directly
|
||||
from praisonai_example_plugin import GreetTool, MathTool
|
||||
agent = Agent(tools=[GreetTool(), MathTool()])
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from praisonaiagents import BaseTool, tool
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Class-Based Tools
|
||||
# =============================================================================
|
||||
|
||||
class GreetTool(BaseTool):
|
||||
"""A tool that generates personalized greetings.
|
||||
|
||||
This demonstrates a simple class-based tool with multiple parameters.
|
||||
"""
|
||||
name = "example_greet"
|
||||
description = "Generate a personalized greeting message"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(self, name: str, greeting: str = "Hello", punctuation: str = "!") -> str:
|
||||
"""Generate a greeting.
|
||||
|
||||
Args:
|
||||
name: The name of the person to greet
|
||||
greeting: The greeting word (default: "Hello")
|
||||
punctuation: End punctuation (default: "!")
|
||||
|
||||
Returns:
|
||||
A formatted greeting string
|
||||
"""
|
||||
return f"{greeting}, {name}{punctuation}"
|
||||
|
||||
|
||||
class MathTool(BaseTool):
|
||||
"""A tool for basic mathematical operations.
|
||||
|
||||
Demonstrates returning structured data from a tool.
|
||||
"""
|
||||
name = "example_math"
|
||||
description = "Perform basic math operations: add, subtract, multiply, divide"
|
||||
version = "1.0.0"
|
||||
|
||||
def run(
|
||||
self,
|
||||
a: float,
|
||||
b: float,
|
||||
operation: str = "add"
|
||||
) -> Dict[str, Any]:
|
||||
"""Perform a math operation.
|
||||
|
||||
Args:
|
||||
a: First number
|
||||
b: Second number
|
||||
operation: One of 'add', 'subtract', 'multiply', 'divide'
|
||||
|
||||
Returns:
|
||||
Dict containing result and operation details
|
||||
"""
|
||||
ops = {
|
||||
"add": ("+", lambda x, y: x + y),
|
||||
"subtract": ("-", lambda x, y: x - y),
|
||||
"multiply": ("*", lambda x, y: x * y),
|
||||
"divide": ("/", lambda x, y: x / y if y != 0 else float('inf')),
|
||||
}
|
||||
|
||||
if operation not in ops:
|
||||
return {"error": f"Unknown operation: {operation}. Use: {list(ops.keys())}"}
|
||||
|
||||
symbol, func = ops[operation]
|
||||
result = func(a, b)
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"expression": f"{a} {symbol} {b} = {result}",
|
||||
"operation": operation
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Decorator-Based Tools
|
||||
# =============================================================================
|
||||
|
||||
@tool(name="example_reverse", description="Reverse a string of text")
|
||||
def reverse_text(text: str) -> str:
|
||||
"""Reverse the input text.
|
||||
|
||||
Args:
|
||||
text: The text to reverse
|
||||
|
||||
Returns:
|
||||
The reversed text
|
||||
"""
|
||||
return text[::-1]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Package Exports
|
||||
# =============================================================================
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__all__ = [
|
||||
"GreetTool",
|
||||
"MathTool",
|
||||
"reverse_text",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
agent.start("Why sky is Blue?")
|
||||
agent.start("What was my previous question?")
|
||||
@@ -0,0 +1,14 @@
|
||||
from praisonaiagents import Agent
|
||||
import asyncio
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
agent.run("Why sea is Blue?")
|
||||
|
||||
async def main():
|
||||
await agent.arun("Why sky is Blue?")
|
||||
await agent.arun("What was my previous question?")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -22,7 +22,7 @@ def multiply(a: int, b: int) -> int:
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
tools=[get_stock_price, multiply]
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Simple Planning Mode Example: Plan first, then execute todo items."""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.planning import PlanningAgent, TodoList
|
||||
|
||||
# Step 1: Create agents
|
||||
researcher = Agent(name="Researcher", role="Research Analyst", verbose=True)
|
||||
writer = Agent(name="Writer", role="Content Writer", verbose=True)
|
||||
|
||||
# Step 2: Create a plan using PlanningAgent
|
||||
planner = PlanningAgent(llm="gpt-4o-mini", verbose=1)
|
||||
plan = planner.create_plan_sync(
|
||||
request="Write a short article about the top 3 benefits of meditation",
|
||||
agents=[researcher, writer],
|
||||
context="Keep it concise"
|
||||
)
|
||||
|
||||
# Step 3: Create todo list from the plan
|
||||
todo = TodoList.from_plan(plan)
|
||||
|
||||
# Step 4: Execute each todo item one by one
|
||||
for item in todo.items:
|
||||
todo.start(item.id)
|
||||
|
||||
# Pick the right agent based on the item's assigned agent
|
||||
if item.agent == "Researcher":
|
||||
agent = researcher
|
||||
else:
|
||||
agent = writer
|
||||
|
||||
# Execute the task
|
||||
result = agent.chat(item.description)
|
||||
|
||||
todo.complete(item.id)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Simplest Planning Mode Example - Just enable planning=True"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# Create agents
|
||||
researcher = Agent(name="Researcher", role="Research Analyst")
|
||||
writer = Agent(name="Writer", role="Content Writer")
|
||||
|
||||
# Create tasks
|
||||
task1 = Task(description="Research the benefits of meditation", agent=researcher)
|
||||
task2 = Task(description="Write a short article about meditation benefits", agent=writer)
|
||||
|
||||
# Run with planning enabled - that's it!
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
planning=True,
|
||||
auto_approve_plan=True # Auto-approve the generated plan
|
||||
)
|
||||
|
||||
agents.start()
|
||||
@@ -22,7 +22,7 @@ def multiply(a: int, b: int) -> int:
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
|
||||
llm="gpt-4o",
|
||||
llm="gpt-5-nano",
|
||||
tools=[get_stock_price, multiply]
|
||||
)
|
||||
|
||||
|
||||
@@ -15,9 +15,33 @@ _logging.configure_root_logger()
|
||||
from .agent.agent import Agent
|
||||
from .agent.image_agent import ImageAgent
|
||||
from .agent.context_agent import ContextAgent, create_context_agent
|
||||
from .agent.deep_research_agent import (
|
||||
DeepResearchAgent,
|
||||
DeepResearchResponse,
|
||||
Citation,
|
||||
ReasoningStep,
|
||||
WebSearchCall,
|
||||
CodeExecutionStep,
|
||||
MCPCall,
|
||||
FileSearchCall,
|
||||
Provider
|
||||
)
|
||||
from .agent.query_rewriter_agent import (
|
||||
QueryRewriterAgent,
|
||||
RewriteStrategy,
|
||||
RewriteResult
|
||||
)
|
||||
from .agent.prompt_expander_agent import (
|
||||
PromptExpanderAgent,
|
||||
ExpandStrategy,
|
||||
ExpandResult
|
||||
)
|
||||
from .agents.agents import PraisonAIAgents
|
||||
from .task.task import Task
|
||||
from .tools.tools import Tools
|
||||
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
|
||||
from .tools.decorator import tool, FunctionTool
|
||||
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
|
||||
from .agents.autoagents import AutoAgents
|
||||
from .knowledge.knowledge import Knowledge
|
||||
from .knowledge.chunking import Chunking
|
||||
@@ -31,6 +55,32 @@ except ImportError:
|
||||
from .session import Session
|
||||
from .memory.memory import Memory
|
||||
from .guardrails import GuardrailResult, LLMGuardrail
|
||||
|
||||
# Planning mode support
|
||||
try:
|
||||
from .planning import (
|
||||
Plan,
|
||||
PlanStep,
|
||||
TodoList,
|
||||
TodoItem,
|
||||
PlanStorage,
|
||||
PlanningAgent,
|
||||
ApprovalCallback,
|
||||
READ_ONLY_TOOLS,
|
||||
RESTRICTED_TOOLS,
|
||||
)
|
||||
_planning_available = True
|
||||
except ImportError:
|
||||
_planning_available = False
|
||||
Plan = None
|
||||
PlanStep = None
|
||||
TodoList = None
|
||||
TodoItem = None
|
||||
PlanStorage = None
|
||||
PlanningAgent = None
|
||||
ApprovalCallback = None
|
||||
READ_ONLY_TOOLS = []
|
||||
RESTRICTED_TOOLS = []
|
||||
from .agent.handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
||||
# Flow display
|
||||
try:
|
||||
@@ -154,6 +204,17 @@ __all__ = [
|
||||
'Agents',
|
||||
'Tools',
|
||||
'Task',
|
||||
# Plugin system
|
||||
'BaseTool',
|
||||
'ToolResult',
|
||||
'ToolValidationError',
|
||||
'validate_tool',
|
||||
'tool',
|
||||
'FunctionTool',
|
||||
'ToolRegistry',
|
||||
'get_registry',
|
||||
'register_tool',
|
||||
'get_tool',
|
||||
'TaskOutput',
|
||||
'ReflectionOutput',
|
||||
'AutoAgents',
|
||||
@@ -186,7 +247,22 @@ __all__ = [
|
||||
'disable_performance_mode',
|
||||
'cleanup_telemetry_resources',
|
||||
'MinimalTelemetry',
|
||||
'TelemetryCollector'
|
||||
'TelemetryCollector',
|
||||
'DeepResearchAgent',
|
||||
'DeepResearchResponse',
|
||||
'Citation',
|
||||
'ReasoningStep',
|
||||
'WebSearchCall',
|
||||
'CodeExecutionStep',
|
||||
'MCPCall',
|
||||
'FileSearchCall',
|
||||
'Provider',
|
||||
'QueryRewriterAgent',
|
||||
'RewriteStrategy',
|
||||
'RewriteResult',
|
||||
'PromptExpanderAgent',
|
||||
'ExpandStrategy',
|
||||
'ExpandResult'
|
||||
]
|
||||
|
||||
# Add MCP to __all__ if available
|
||||
|
||||
@@ -4,5 +4,52 @@ from .image_agent import ImageAgent
|
||||
from .context_agent import ContextAgent, create_context_agent
|
||||
from .handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
||||
from .router_agent import RouterAgent
|
||||
from .deep_research_agent import (
|
||||
DeepResearchAgent,
|
||||
DeepResearchResponse,
|
||||
Citation,
|
||||
ReasoningStep,
|
||||
WebSearchCall,
|
||||
CodeExecutionStep,
|
||||
MCPCall,
|
||||
FileSearchCall,
|
||||
Provider
|
||||
)
|
||||
from .query_rewriter_agent import (
|
||||
QueryRewriterAgent,
|
||||
RewriteStrategy,
|
||||
RewriteResult
|
||||
)
|
||||
from .prompt_expander_agent import (
|
||||
PromptExpanderAgent,
|
||||
ExpandStrategy,
|
||||
ExpandResult
|
||||
)
|
||||
|
||||
__all__ = ['Agent', 'ImageAgent', 'ContextAgent', 'create_context_agent', 'Handoff', 'handoff', 'handoff_filters', 'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions', 'RouterAgent']
|
||||
__all__ = [
|
||||
'Agent',
|
||||
'ImageAgent',
|
||||
'ContextAgent',
|
||||
'create_context_agent',
|
||||
'Handoff',
|
||||
'handoff',
|
||||
'handoff_filters',
|
||||
'RECOMMENDED_PROMPT_PREFIX',
|
||||
'prompt_with_handoff_instructions',
|
||||
'RouterAgent',
|
||||
'DeepResearchAgent',
|
||||
'DeepResearchResponse',
|
||||
'Citation',
|
||||
'ReasoningStep',
|
||||
'WebSearchCall',
|
||||
'CodeExecutionStep',
|
||||
'MCPCall',
|
||||
'FileSearchCall',
|
||||
'Provider',
|
||||
'QueryRewriterAgent',
|
||||
'RewriteStrategy',
|
||||
'RewriteResult',
|
||||
'PromptExpanderAgent',
|
||||
'ExpandStrategy',
|
||||
'ExpandResult'
|
||||
]
|
||||
@@ -234,7 +234,15 @@ class Agent:
|
||||
max_guardrail_retries: int = 3,
|
||||
handoffs: Optional[List[Union['Agent', 'Handoff']]] = None,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: 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
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
@@ -250,8 +258,8 @@ class Agent:
|
||||
instructions (Optional[str], optional): Direct instructions that override role, goal,
|
||||
and backstory when provided. Used for simple, task-specific agents. Defaults to None.
|
||||
llm (Optional[Union[str, Any]], optional): Language model configuration. Can be a model
|
||||
name string (e.g., "gpt-4o", "anthropic/claude-3-sonnet") or a configured LLM object.
|
||||
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-4o".
|
||||
name string (e.g., "gpt-5-nano", "anthropic/claude-3-sonnet") or a configured LLM object.
|
||||
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-5-nano".
|
||||
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
|
||||
available to the agent for task execution. Can include callables, tool objects,
|
||||
or MCP instances. Defaults to None.
|
||||
@@ -395,12 +403,16 @@ class Agent:
|
||||
self.llm_instance = LLM(**llm_config)
|
||||
else:
|
||||
# Create LLM with model string and base_url
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.llm_instance = LLM(
|
||||
model=model_name,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
metrics=metrics
|
||||
metrics=metrics,
|
||||
web_search=web_search,
|
||||
web_fetch=web_fetch,
|
||||
prompt_caching=prompt_caching,
|
||||
claude_memory=claude_memory
|
||||
)
|
||||
self._using_custom_llm = True
|
||||
except ImportError as e:
|
||||
@@ -435,6 +447,10 @@ class Agent:
|
||||
if api_key:
|
||||
llm_params['api_key'] = api_key
|
||||
llm_params['metrics'] = metrics
|
||||
llm_params['web_search'] = web_search
|
||||
llm_params['web_fetch'] = web_fetch
|
||||
llm_params['prompt_caching'] = prompt_caching
|
||||
llm_params['claude_memory'] = claude_memory
|
||||
self.llm_instance = LLM(**llm_params)
|
||||
self._using_custom_llm = True
|
||||
|
||||
@@ -450,11 +466,20 @@ class Agent:
|
||||
) from e
|
||||
# Otherwise, fall back to OpenAI environment/name
|
||||
else:
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
# Handle tools parameter - ensure it's always a list
|
||||
if callable(tools):
|
||||
# If a single function/callable is passed, wrap it in a list
|
||||
self.tools = [tools]
|
||||
elif isinstance(tools, str):
|
||||
# Single tool name string - resolve from registry
|
||||
self.tools = self._resolve_tool_names([tools])
|
||||
elif isinstance(tools, (list, tuple)):
|
||||
# Check if list contains strings (tool names) that need resolution
|
||||
if tools and all(isinstance(t, str) for t in tools):
|
||||
self.tools = self._resolve_tool_names(tools)
|
||||
else:
|
||||
self.tools = list(tools)
|
||||
else:
|
||||
# Handle all falsy values (None, False, 0, "", etc.) by defaulting to empty list
|
||||
self.tools = tools or []
|
||||
@@ -462,7 +487,8 @@ class Agent:
|
||||
self.max_iter = max_iter
|
||||
self.max_rpm = max_rpm
|
||||
self.max_execution_time = max_execution_time
|
||||
self.memory = memory
|
||||
self._memory_instance = None
|
||||
self._init_memory(memory, user_id)
|
||||
self.verbose = verbose
|
||||
self.allow_delegation = allow_delegation
|
||||
self.step_callback = step_callback
|
||||
@@ -487,7 +513,7 @@ class Agent:
|
||||
self.min_reflect = min_reflect
|
||||
self.reflect_prompt = reflect_prompt
|
||||
# Use the same model selection logic for reflect_llm
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self._console = None # Lazy load console when needed
|
||||
|
||||
# Initialize system prompt
|
||||
@@ -502,6 +528,32 @@ Your Goal: {self.goal}
|
||||
# Store user_id
|
||||
self.user_id = user_id or "praison"
|
||||
self.reasoning_steps = reasoning_steps
|
||||
self.plan_mode = plan_mode # Read-only mode for planning
|
||||
self.planning = planning # Enable planning mode
|
||||
self.planning_tools = planning_tools # Tools for planning phase
|
||||
self.planning_reasoning = planning_reasoning # Enable reasoning during planning
|
||||
self._planning_agent = None # Lazy loaded PlanningAgent
|
||||
self.web_search = web_search
|
||||
self.web_fetch = web_fetch
|
||||
self.prompt_caching = prompt_caching
|
||||
self.claude_memory = claude_memory
|
||||
|
||||
# Initialize rules manager for persistent context (like Cursor/Windsurf)
|
||||
self._rules_manager = None
|
||||
self._init_rules_manager()
|
||||
|
||||
# Handle web_search fallback: inject DuckDuckGo tool for unsupported models
|
||||
if web_search and not self._model_supports_web_search():
|
||||
from ..tools.duckduckgo_tools import internet_search
|
||||
# Check if internet_search is not already in tools
|
||||
tool_names = [getattr(t, '__name__', str(t)) for t in self.tools]
|
||||
if 'internet_search' not in tool_names and 'duckduckgo' not in tool_names:
|
||||
self.tools.append(internet_search)
|
||||
logging.info("Model does not support native web search. Added DuckDuckGo fallback tool.")
|
||||
|
||||
# Log warning if web_fetch is enabled but model doesn't support it
|
||||
if web_fetch and not self._model_supports_web_fetch():
|
||||
logging.warning(f"Model '{self.llm}' does not support native web fetch. Web fetch will be ignored.")
|
||||
|
||||
# Initialize guardrail settings
|
||||
self.guardrail = guardrail
|
||||
@@ -567,13 +619,301 @@ Your Goal: {self.goal}
|
||||
self._agent_id = str(uuid.uuid4())
|
||||
return self._agent_id
|
||||
|
||||
def get_available_tools(self) -> List[Any]:
|
||||
"""
|
||||
Get tools available to this agent, filtered by plan_mode if enabled.
|
||||
|
||||
In plan_mode, only read-only tools are available to prevent
|
||||
modifications during the planning phase.
|
||||
|
||||
Returns:
|
||||
List of available tools
|
||||
"""
|
||||
if not self.plan_mode:
|
||||
return self.tools
|
||||
|
||||
# Filter to read-only tools only
|
||||
from ..planning import READ_ONLY_TOOLS, RESTRICTED_TOOLS
|
||||
|
||||
filtered_tools = []
|
||||
for tool in self.tools:
|
||||
tool_name = getattr(tool, '__name__', str(tool)).lower()
|
||||
|
||||
# Check if tool is in restricted list
|
||||
is_restricted = any(
|
||||
restricted.lower() in tool_name
|
||||
for restricted in RESTRICTED_TOOLS
|
||||
)
|
||||
|
||||
if not is_restricted:
|
||||
filtered_tools.append(tool)
|
||||
|
||||
return filtered_tools
|
||||
|
||||
def _model_supports_web_search(self) -> bool:
|
||||
"""
|
||||
Check if the agent's model supports native web search via LiteLLM.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports native web search, False otherwise
|
||||
"""
|
||||
from ..llm.model_capabilities import supports_web_search
|
||||
|
||||
# Get the model name
|
||||
if hasattr(self, 'llm_instance') and self.llm_instance:
|
||||
model_name = self.llm_instance.model
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
|
||||
return supports_web_search(model_name)
|
||||
|
||||
def _model_supports_web_fetch(self) -> bool:
|
||||
"""
|
||||
Check if the agent's model supports web fetch via LiteLLM.
|
||||
|
||||
Web fetch allows the model to retrieve full content from specific URLs.
|
||||
Currently only supported by Anthropic Claude models.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports web fetch, False otherwise
|
||||
"""
|
||||
from ..llm.model_capabilities import supports_web_fetch
|
||||
|
||||
# Get the model name
|
||||
if hasattr(self, 'llm_instance') and self.llm_instance:
|
||||
model_name = self.llm_instance.model
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
|
||||
return supports_web_fetch(model_name)
|
||||
|
||||
def _model_supports_prompt_caching(self) -> bool:
|
||||
"""
|
||||
Check if the agent's model supports prompt caching via LiteLLM.
|
||||
|
||||
Prompt caching allows caching parts of prompts to reduce costs and latency.
|
||||
Supported by OpenAI, Anthropic, Bedrock, and Deepseek.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports prompt caching, False otherwise
|
||||
"""
|
||||
from ..llm.model_capabilities import supports_prompt_caching
|
||||
|
||||
# Get the model name
|
||||
if hasattr(self, 'llm_instance') and self.llm_instance:
|
||||
model_name = self.llm_instance.model
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-4o-mini"
|
||||
|
||||
return supports_prompt_caching(model_name)
|
||||
|
||||
def _init_rules_manager(self):
|
||||
"""
|
||||
Initialize RulesManager for persistent rules/instructions.
|
||||
|
||||
Automatically discovers rules from:
|
||||
- ~/.praison/rules/ (global)
|
||||
- .praison/rules/ (workspace)
|
||||
- Subdirectory rules
|
||||
"""
|
||||
try:
|
||||
from ..memory.rules_manager import RulesManager
|
||||
import os
|
||||
|
||||
# Get workspace path (current working directory)
|
||||
workspace_path = os.getcwd()
|
||||
|
||||
self._rules_manager = RulesManager(
|
||||
workspace_path=workspace_path,
|
||||
verbose=1 if self.verbose else 0
|
||||
)
|
||||
|
||||
# Log discovered rules
|
||||
stats = self._rules_manager.get_stats()
|
||||
if stats["total_rules"] > 0:
|
||||
logging.debug(f"RulesManager: Discovered {stats['total_rules']} rules")
|
||||
except ImportError:
|
||||
logging.debug("RulesManager not available")
|
||||
self._rules_manager = None
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not initialize RulesManager: {e}")
|
||||
self._rules_manager = None
|
||||
|
||||
def get_rules_context(self, file_path: Optional[str] = None, include_manual: Optional[List[str]] = None) -> str:
|
||||
"""
|
||||
Get rules context for the current conversation.
|
||||
|
||||
Args:
|
||||
file_path: Optional file path for glob-based rule matching
|
||||
include_manual: Optional list of manual rule names to include (via @mention)
|
||||
|
||||
Returns:
|
||||
Formatted rules context string
|
||||
"""
|
||||
if not self._rules_manager:
|
||||
return ""
|
||||
|
||||
return self._rules_manager.build_rules_context(
|
||||
file_path=file_path,
|
||||
include_manual=include_manual
|
||||
)
|
||||
|
||||
def _init_memory(self, memory, user_id: Optional[str] = None):
|
||||
"""
|
||||
Initialize memory based on the memory parameter.
|
||||
|
||||
Args:
|
||||
memory: Can be:
|
||||
- True: Use FileMemory with default settings
|
||||
- False/None: No memory
|
||||
- "file": Use FileMemory
|
||||
- "sqlite": Use existing Memory class with SQLite
|
||||
- dict: Configuration for memory
|
||||
- Memory/FileMemory instance: Use directly
|
||||
user_id: User identifier for memory isolation
|
||||
"""
|
||||
self.memory = memory
|
||||
|
||||
if memory is None or memory is False:
|
||||
self._memory_instance = None
|
||||
return
|
||||
|
||||
# Determine user_id
|
||||
mem_user_id = user_id or getattr(self, 'user_id', None) or "default"
|
||||
|
||||
if memory is True or memory == "file":
|
||||
# Use FileMemory (zero dependencies)
|
||||
from ..memory.file_memory import FileMemory
|
||||
self._memory_instance = FileMemory(
|
||||
user_id=mem_user_id,
|
||||
verbose=1 if getattr(self, 'verbose', False) else 0
|
||||
)
|
||||
elif isinstance(memory, str) and memory in ("sqlite", "chromadb", "mem0", "mongodb"):
|
||||
# Use full Memory class with specific provider
|
||||
try:
|
||||
from ..memory.memory import Memory
|
||||
config = {"provider": memory if memory != "sqlite" else "rag"}
|
||||
self._memory_instance = Memory(config)
|
||||
except ImportError:
|
||||
logging.warning(f"Memory provider '{memory}' requires additional dependencies. Falling back to FileMemory.")
|
||||
from ..memory.file_memory import FileMemory
|
||||
self._memory_instance = FileMemory(user_id=mem_user_id)
|
||||
elif isinstance(memory, dict):
|
||||
# Configuration dict
|
||||
provider = memory.get("provider", "file")
|
||||
if provider == "file":
|
||||
from ..memory.file_memory import FileMemory
|
||||
self._memory_instance = FileMemory(
|
||||
user_id=memory.get("user_id", mem_user_id),
|
||||
config=memory
|
||||
)
|
||||
else:
|
||||
try:
|
||||
from ..memory.memory import Memory
|
||||
self._memory_instance = Memory(memory)
|
||||
except ImportError:
|
||||
logging.warning("Full Memory class requires additional dependencies. Falling back to FileMemory.")
|
||||
from ..memory.file_memory import FileMemory
|
||||
self._memory_instance = FileMemory(user_id=mem_user_id)
|
||||
else:
|
||||
# Assume it's already a memory instance
|
||||
self._memory_instance = memory
|
||||
|
||||
def get_memory_context(self, query: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get memory context for the current conversation.
|
||||
|
||||
Args:
|
||||
query: Optional query to focus the context
|
||||
|
||||
Returns:
|
||||
Formatted memory context string
|
||||
"""
|
||||
if not self._memory_instance:
|
||||
return ""
|
||||
|
||||
if hasattr(self._memory_instance, 'get_context'):
|
||||
return self._memory_instance.get_context(query=query)
|
||||
|
||||
return ""
|
||||
|
||||
def store_memory(self, content: str, memory_type: str = "short_term", **kwargs):
|
||||
"""
|
||||
Store content in memory.
|
||||
|
||||
Args:
|
||||
content: Content to store
|
||||
memory_type: Type of memory (short_term, long_term, entity, episodic)
|
||||
**kwargs: Additional arguments for the memory method
|
||||
"""
|
||||
if not self._memory_instance:
|
||||
return
|
||||
|
||||
if memory_type == "short_term" and hasattr(self._memory_instance, 'add_short_term'):
|
||||
self._memory_instance.add_short_term(content, **kwargs)
|
||||
elif memory_type == "long_term" and hasattr(self._memory_instance, 'add_long_term'):
|
||||
self._memory_instance.add_long_term(content, **kwargs)
|
||||
elif memory_type == "entity" and hasattr(self._memory_instance, 'add_entity'):
|
||||
self._memory_instance.add_entity(content, **kwargs)
|
||||
elif memory_type == "episodic" and hasattr(self._memory_instance, 'add_episodic'):
|
||||
self._memory_instance.add_episodic(content, **kwargs)
|
||||
|
||||
def _display_memory_info(self):
|
||||
"""Display memory information to user in a friendly format."""
|
||||
if not self._memory_instance:
|
||||
return
|
||||
|
||||
# Only display once per chat session
|
||||
if hasattr(self, '_memory_displayed') and self._memory_displayed:
|
||||
return
|
||||
self._memory_displayed = True
|
||||
|
||||
stats = self._memory_instance.get_stats()
|
||||
short_count = stats.get('short_term_count', 0)
|
||||
long_count = stats.get('long_term_count', 0)
|
||||
entity_count = stats.get('entity_count', 0)
|
||||
storage_path = stats.get('storage_path', '')
|
||||
|
||||
total_memories = short_count + long_count + entity_count
|
||||
|
||||
if total_memories > 0:
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
# Build memory info text
|
||||
info_parts = []
|
||||
if long_count > 0:
|
||||
info_parts.append(f"💾 {long_count} long-term")
|
||||
if short_count > 0:
|
||||
info_parts.append(f"⚡ {short_count} short-term")
|
||||
if entity_count > 0:
|
||||
info_parts.append(f"👤 {entity_count} entities")
|
||||
|
||||
memory_text = Text()
|
||||
memory_text.append("🧠 Memory loaded: ", style="bold cyan")
|
||||
memory_text.append(" | ".join(info_parts))
|
||||
memory_text.append(f"\n📁 Storage: {storage_path}", style="dim")
|
||||
|
||||
self.console.print(Panel(
|
||||
memory_text,
|
||||
title="[bold]Agent Memory[/bold]",
|
||||
border_style="cyan",
|
||||
expand=False
|
||||
))
|
||||
|
||||
@property
|
||||
def llm_model(self):
|
||||
"""Unified property to get the LLM model regardless of configuration type.
|
||||
|
||||
Returns:
|
||||
The LLM model/instance being used by this agent.
|
||||
- For standard models: returns the model string (e.g., "gpt-4o")
|
||||
- For standard models: returns the model string (e.g., "gpt-5-nano")
|
||||
- For custom LLM instances: returns the LLM instance object
|
||||
- For provider models: returns the LLM instance object
|
||||
"""
|
||||
@@ -583,7 +923,7 @@ Your Goal: {self.goal}
|
||||
return self.llm
|
||||
else:
|
||||
# Default fallback
|
||||
return "gpt-4o"
|
||||
return "gpt-5-nano"
|
||||
|
||||
def _ensure_knowledge_processed(self):
|
||||
"""Ensure knowledge is initialized and processed when first accessed."""
|
||||
@@ -813,17 +1153,35 @@ Your Goal: {self.goal}
|
||||
if not self.use_system_prompt:
|
||||
return None
|
||||
|
||||
# Check cache first
|
||||
tools_key = self._get_tools_cache_key(tools)
|
||||
cache_key = f"{self.role}:{self.goal}:{tools_key}"
|
||||
|
||||
if cache_key in self._system_prompt_cache:
|
||||
return self._system_prompt_cache[cache_key]
|
||||
# Check cache first (skip cache if memory is enabled since context is dynamic)
|
||||
if not self._memory_instance:
|
||||
tools_key = self._get_tools_cache_key(tools)
|
||||
cache_key = f"{self.role}:{self.goal}:{tools_key}"
|
||||
|
||||
if cache_key in self._system_prompt_cache:
|
||||
return self._system_prompt_cache[cache_key]
|
||||
else:
|
||||
cache_key = None # Don't cache when memory is enabled
|
||||
|
||||
system_prompt = f"""{self.backstory}\n
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}"""
|
||||
|
||||
# Add rules context if rules manager is enabled
|
||||
if self._rules_manager:
|
||||
rules_context = self.get_rules_context()
|
||||
if rules_context:
|
||||
system_prompt += f"\n\n## Rules (Guidelines you must follow)\n{rules_context}"
|
||||
|
||||
# Add memory context if memory is enabled
|
||||
if self._memory_instance:
|
||||
memory_context = self.get_memory_context()
|
||||
if memory_context:
|
||||
system_prompt += f"\n\n## Memory (Information you remember about the user)\n{memory_context}"
|
||||
# Display memory info to user if verbose
|
||||
if self.verbose:
|
||||
self._display_memory_info()
|
||||
|
||||
# Add tool usage instructions if tools are available
|
||||
# Use provided tools or fall back to self.tools
|
||||
tools_to_use = tools if tools is not None else self.tools
|
||||
@@ -853,9 +1211,9 @@ Your Goal: {self.goal}"""
|
||||
if tool_names:
|
||||
system_prompt += f"\n\nYou have access to the following tools: {', '.join(tool_names)}. Use these tools when appropriate to help complete your tasks. Always use tools when they can help provide accurate information or perform actions."
|
||||
|
||||
# Cache the generated system prompt
|
||||
# Cache the generated system prompt (only if cache_key is set, i.e., memory not enabled)
|
||||
# Simple cache size limit to prevent unbounded growth
|
||||
if len(self._system_prompt_cache) < self._max_cache_size:
|
||||
if cache_key and len(self._system_prompt_cache) < self._max_cache_size:
|
||||
self._system_prompt_cache[cache_key] = system_prompt
|
||||
return system_prompt
|
||||
|
||||
@@ -1008,6 +1366,31 @@ Your Goal: {self.goal}"""
|
||||
tools=self.tools
|
||||
)
|
||||
|
||||
def _resolve_tool_names(self, tool_names):
|
||||
"""Resolve tool names to actual tool instances from registry.
|
||||
|
||||
Args:
|
||||
tool_names: List of tool name strings
|
||||
|
||||
Returns:
|
||||
List of resolved tool instances
|
||||
"""
|
||||
resolved = []
|
||||
try:
|
||||
from ..tools.registry import get_registry
|
||||
registry = get_registry()
|
||||
|
||||
for name in tool_names:
|
||||
tool = registry.get(name)
|
||||
if tool is not None:
|
||||
resolved.append(tool)
|
||||
else:
|
||||
logging.warning(f"Tool '{name}' not found in registry")
|
||||
except ImportError:
|
||||
logging.warning("Tool registry not available, cannot resolve tool names")
|
||||
|
||||
return resolved
|
||||
|
||||
def _cast_arguments(self, func, arguments):
|
||||
"""Cast arguments to their expected types based on function signature."""
|
||||
if not callable(func) or not arguments:
|
||||
@@ -1105,13 +1488,31 @@ Your Goal: {self.goal}"""
|
||||
# Try to find the function in the agent's tools list first
|
||||
func = None
|
||||
for tool in self.tools if isinstance(self.tools, (list, tuple)) else []:
|
||||
# Check for BaseTool instances (plugin system)
|
||||
from ..tools.base import BaseTool
|
||||
if isinstance(tool, BaseTool) and tool.name == function_name:
|
||||
func = tool
|
||||
break
|
||||
# Check for FunctionTool (decorated functions)
|
||||
if hasattr(tool, 'name') and getattr(tool, 'name', None) == function_name:
|
||||
func = tool
|
||||
break
|
||||
if (callable(tool) and getattr(tool, '__name__', '') == function_name) or \
|
||||
(inspect.isclass(tool) and tool.__name__ == function_name):
|
||||
func = tool
|
||||
break
|
||||
|
||||
if func is None:
|
||||
# If not found in tools, try globals and main
|
||||
# Check the global tool registry for plugins
|
||||
try:
|
||||
from ..tools.registry import get_registry
|
||||
registry = get_registry()
|
||||
func = registry.get(function_name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if func is None:
|
||||
# If not found in tools or registry, try globals and main
|
||||
func = globals().get(function_name)
|
||||
if not func:
|
||||
import __main__
|
||||
@@ -1119,6 +1520,12 @@ Your Goal: {self.goal}"""
|
||||
|
||||
if func:
|
||||
try:
|
||||
# BaseTool instances (plugin system) - call run() method
|
||||
from ..tools.base import BaseTool
|
||||
if isinstance(func, BaseTool):
|
||||
casted_arguments = self._cast_arguments(func.run, arguments)
|
||||
return func.run(**casted_arguments)
|
||||
|
||||
# Langchain: If it's a class with run but not _run, instantiate and call run
|
||||
if inspect.isclass(func) and hasattr(func, 'run') and not hasattr(func, '_run'):
|
||||
instance = func()
|
||||
@@ -2082,16 +2489,98 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
display_error(f"Error in _achat_completion: {e}")
|
||||
return None
|
||||
|
||||
async def arun(self, prompt: str, **kwargs):
|
||||
"""Async alias for astart() method"""
|
||||
return await self.astart(prompt, **kwargs)
|
||||
|
||||
async def astart(self, prompt: str, **kwargs):
|
||||
"""Async version of start method"""
|
||||
return await self.achat(prompt, **kwargs)
|
||||
|
||||
def run(self):
|
||||
def run(self, prompt: str, **kwargs):
|
||||
"""Alias for start() method"""
|
||||
return self.start()
|
||||
return self.start(prompt, **kwargs)
|
||||
|
||||
def _get_planning_agent(self):
|
||||
"""Lazy load PlanningAgent for planning mode."""
|
||||
if self._planning_agent is None and self.planning:
|
||||
from ..planning import PlanningAgent
|
||||
self._planning_agent = PlanningAgent(
|
||||
llm=self.llm if hasattr(self, 'llm') else (self.llm_instance.model if hasattr(self, 'llm_instance') else "gpt-4o-mini"),
|
||||
tools=self.planning_tools,
|
||||
reasoning=self.planning_reasoning,
|
||||
verbose=1 if self.verbose else 0
|
||||
)
|
||||
return self._planning_agent
|
||||
|
||||
def _start_with_planning(self, prompt: str, **kwargs):
|
||||
"""Execute with planning mode - creates plan then executes each step."""
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.markdown import Markdown
|
||||
|
||||
console = Console()
|
||||
|
||||
# Step 1: Create the plan
|
||||
console.print("\n[bold blue]📋 PLANNING PHASE[/bold blue]")
|
||||
console.print("[dim]Creating implementation plan...[/dim]\n")
|
||||
|
||||
planner = self._get_planning_agent()
|
||||
plan = planner.create_plan_sync(request=prompt, agents=[self])
|
||||
|
||||
if not plan or not plan.steps:
|
||||
console.print("[yellow]⚠️ Planning failed, falling back to direct execution[/yellow]")
|
||||
return self.chat(prompt, **kwargs)
|
||||
|
||||
# Display the plan
|
||||
console.print(Panel(
|
||||
Markdown(plan.to_markdown()),
|
||||
title="[bold green]Generated Plan[/bold green]",
|
||||
border_style="green"
|
||||
))
|
||||
|
||||
# Step 2: Execute each step
|
||||
console.print("\n[bold blue]🚀 EXECUTION PHASE[/bold blue]\n")
|
||||
|
||||
results = []
|
||||
context = ""
|
||||
|
||||
for i, step in enumerate(plan.steps):
|
||||
progress = (i + 1) / len(plan.steps)
|
||||
bar_length = 30
|
||||
filled = int(bar_length * progress)
|
||||
bar = "█" * filled + "░" * (bar_length - filled)
|
||||
|
||||
console.print(f"[dim]Progress: [{bar}] {progress * 100:.0f}%[/dim]")
|
||||
console.print(f"\n[bold]📌 Step {i + 1}/{len(plan.steps)}:[/bold] {step.description[:60]}...")
|
||||
|
||||
# Build prompt with context from previous steps
|
||||
step_prompt = step.description
|
||||
if context:
|
||||
step_prompt = f"{step.description}\n\nContext from previous steps:\n{context}"
|
||||
|
||||
# Execute the step
|
||||
result = self.chat(step_prompt, **kwargs)
|
||||
results.append(result)
|
||||
|
||||
# Update context for next step
|
||||
context += f"\n\nStep {i + 1} result: {result[:500] if result else 'No result'}"
|
||||
|
||||
console.print(f" [green]✅ Completed[/green]")
|
||||
|
||||
console.print(f"\n[bold green]🎉 EXECUTION COMPLETE[/bold green]")
|
||||
console.print(f"[dim]Progress: [{'█' * bar_length}] 100%[/dim]")
|
||||
console.print(f"Completed {len(plan.steps)}/{len(plan.steps)} steps!\n")
|
||||
|
||||
# Return the final result
|
||||
return results[-1] if results else None
|
||||
|
||||
def start(self, prompt: str, **kwargs):
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
return self._start_with_planning(prompt, **kwargs)
|
||||
|
||||
# Check if streaming is enabled (either from kwargs or agent's stream attribute)
|
||||
stream_enabled = kwargs.get('stream', getattr(self, 'stream', False))
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Prompt Expander Agent Module
|
||||
|
||||
This module provides the PromptExpanderAgent class for expanding short prompts
|
||||
into detailed, comprehensive prompts for better task execution.
|
||||
|
||||
Unlike QueryRewriterAgent (which optimizes queries for search/retrieval),
|
||||
PromptExpanderAgent focuses on enriching prompts for task execution.
|
||||
|
||||
Supported Expansion Strategies:
|
||||
- **BASIC**: Simple expansion with clarity improvements
|
||||
- **DETAILED**: Rich expansion with context, constraints, and examples
|
||||
- **STRUCTURED**: Expansion with clear structure (task, format, requirements)
|
||||
- **CREATIVE**: Expansion with creative flair and vivid language
|
||||
- **AUTO**: Automatically selects the best strategy based on prompt analysis
|
||||
|
||||
Example:
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent(verbose=True)
|
||||
|
||||
# Basic expansion
|
||||
result = agent.expand("write a movie script in 3 lines")
|
||||
print(result.expanded_prompt)
|
||||
|
||||
# Detailed expansion
|
||||
result = agent.expand("blog about AI", strategy=ExpandStrategy.DETAILED)
|
||||
print(result.expanded_prompt)
|
||||
|
||||
# With tools for context gathering
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = PromptExpanderAgent(tools=[internet_search], verbose=True)
|
||||
result = agent.expand("latest AI developments")
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
class ExpandStrategy(Enum):
|
||||
"""Enumeration of available prompt expansion strategies."""
|
||||
BASIC = "basic"
|
||||
DETAILED = "detailed"
|
||||
STRUCTURED = "structured"
|
||||
CREATIVE = "creative"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpandResult:
|
||||
"""Result of a prompt expansion operation."""
|
||||
original_prompt: str
|
||||
expanded_prompt: str
|
||||
strategy_used: ExpandStrategy
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ExpandResult(strategy={self.strategy_used.value}, original_len={len(self.original_prompt)}, expanded_len={len(self.expanded_prompt)})"
|
||||
|
||||
|
||||
class PromptExpanderAgent:
|
||||
"""
|
||||
Agent for expanding short prompts into detailed, comprehensive prompts.
|
||||
|
||||
This agent transforms brief task descriptions into rich, detailed prompts
|
||||
that provide better context and guidance for task execution.
|
||||
|
||||
Key difference from QueryRewriterAgent:
|
||||
- QueryRewriterAgent: Optimizes queries for search/retrieval (RAG)
|
||||
- PromptExpanderAgent: Expands prompts for detailed task execution
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for expansion
|
||||
verbose: Whether to print detailed logs
|
||||
temperature: Temperature for LLM generation (higher = more creative)
|
||||
tools: Optional tools for context gathering
|
||||
|
||||
Example:
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini", verbose=True)
|
||||
result = agent.expand("write a poem", strategy=ExpandStrategy.CREATIVE)
|
||||
print(result.expanded_prompt)
|
||||
"""
|
||||
|
||||
# Default prompts for each strategy
|
||||
PROMPTS = {
|
||||
"basic": """You are an expert at improving task prompts.
|
||||
|
||||
Current date: {current_date}
|
||||
|
||||
Given the following task prompt, expand it to be clearer and more actionable while preserving the original intent.
|
||||
|
||||
Guidelines:
|
||||
- Keep it as a TASK/COMMAND (do NOT convert to a question)
|
||||
- Add clarity without changing the core request
|
||||
- Fix any ambiguity
|
||||
- Keep it concise but complete
|
||||
- Use the current date for any time-sensitive context
|
||||
|
||||
Original prompt: {prompt}
|
||||
|
||||
Expanded prompt:""",
|
||||
|
||||
"detailed": """You are an expert at creating comprehensive task prompts.
|
||||
|
||||
Current date: {current_date}
|
||||
|
||||
Given the following brief task prompt, expand it into a detailed, actionable prompt that provides rich context and clear guidance.
|
||||
|
||||
Guidelines:
|
||||
- Keep it as a TASK/COMMAND (do NOT convert to a question)
|
||||
- Add specific details about what to include
|
||||
- Specify quality expectations
|
||||
- Add relevant constraints or requirements
|
||||
- Include format guidance if applicable
|
||||
- Mention style or tone if relevant
|
||||
- Use the current date for any time-sensitive context
|
||||
|
||||
Original prompt: {prompt}
|
||||
|
||||
Detailed expanded prompt:""",
|
||||
|
||||
"structured": """You are an expert at creating well-structured task prompts.
|
||||
|
||||
Current date: {current_date}
|
||||
|
||||
Given the following task prompt, expand it into a structured format with clear sections.
|
||||
|
||||
Guidelines:
|
||||
- Keep it as a TASK/COMMAND (do NOT convert to a question)
|
||||
- Use a clear structure:
|
||||
* Task: What to do
|
||||
* Format: Expected output format
|
||||
* Requirements: Specific requirements
|
||||
* Style: Tone or style guidance
|
||||
* Constraints: Any limitations
|
||||
- Be specific and actionable
|
||||
- Use the current date for any time-sensitive context
|
||||
|
||||
Original prompt: {prompt}
|
||||
|
||||
Structured expanded prompt:""",
|
||||
|
||||
"creative": """You are an expert at crafting inspiring, creative task prompts.
|
||||
|
||||
Current date: {current_date}
|
||||
|
||||
Given the following task prompt, expand it with vivid language and creative direction while keeping it actionable.
|
||||
|
||||
Guidelines:
|
||||
- Keep it as a TASK/COMMAND (do NOT convert to a question)
|
||||
- Use evocative, inspiring language
|
||||
- Paint a picture of the desired outcome
|
||||
- Add creative direction and artistic guidance
|
||||
- Make it exciting and engaging
|
||||
- Preserve the core task intent
|
||||
- Use the current date for any time-sensitive context
|
||||
|
||||
Original prompt: {prompt}
|
||||
|
||||
Creative expanded prompt:""",
|
||||
|
||||
"auto": """You are an expert at analyzing and expanding task prompts.
|
||||
|
||||
Current date: {current_date}
|
||||
|
||||
Analyze the following prompt and expand it appropriately based on its nature.
|
||||
|
||||
For short/vague prompts: Add significant detail and context
|
||||
For task prompts: Preserve the action-oriented nature
|
||||
For creative tasks: Add artistic direction
|
||||
For technical tasks: Add precision and requirements
|
||||
|
||||
IMPORTANT: Keep it as a TASK/COMMAND. Never convert to a question.
|
||||
Use the current date for any time-sensitive context.
|
||||
|
||||
Original prompt: {prompt}
|
||||
{context}
|
||||
|
||||
Expanded prompt:"""
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "PromptExpanderAgent",
|
||||
model: str = "gpt-4o-mini",
|
||||
instructions: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
temperature: float = 0.7, # Higher for creativity
|
||||
max_tokens: int = 1000,
|
||||
tools: Optional[List[Any]] = None
|
||||
):
|
||||
"""
|
||||
Initialize the PromptExpanderAgent.
|
||||
|
||||
Args:
|
||||
name: Name of the agent
|
||||
model: LLM model to use (default: gpt-4o-mini)
|
||||
instructions: Custom instructions for the agent
|
||||
verbose: Whether to print detailed logs
|
||||
temperature: Temperature for LLM generation (default: 0.7 for creativity)
|
||||
max_tokens: Maximum tokens for LLM response
|
||||
tools: Optional list of tools for context gathering
|
||||
"""
|
||||
self.name = name
|
||||
self.model = model
|
||||
self.instructions = instructions
|
||||
self.verbose = verbose
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.tools = tools or []
|
||||
self._agent = None # Lazy initialized
|
||||
|
||||
# Setup logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
if self.verbose:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
@property
|
||||
def agent(self):
|
||||
"""Lazy initialization of internal Agent."""
|
||||
if self._agent is None:
|
||||
from .agent import Agent
|
||||
self._agent = Agent(
|
||||
name=self.name,
|
||||
role="Prompt Expansion Specialist",
|
||||
goal="Expand brief prompts into detailed, actionable prompts for better task execution. Use available tools when needed to gather context.",
|
||||
backstory="You are an expert at understanding user intent and transforming brief requests into comprehensive, detailed prompts.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
)
|
||||
return self._agent
|
||||
|
||||
def _call_agent(self, prompt: str) -> str:
|
||||
"""Call the internal Agent with the given prompt."""
|
||||
try:
|
||||
response = self.agent.chat(prompt)
|
||||
return response if response else ""
|
||||
except Exception as e:
|
||||
self.logger.error(f"Agent call failed: {e}")
|
||||
raise
|
||||
|
||||
def _is_short_prompt(self, prompt: str, threshold: int = 10) -> bool:
|
||||
"""Check if prompt is considered short."""
|
||||
return len(prompt.split()) < threshold
|
||||
|
||||
def _is_task_prompt(self, prompt: str) -> bool:
|
||||
"""Check if prompt is a task/command (vs a question)."""
|
||||
task_verbs = [
|
||||
"write", "create", "generate", "build", "make", "design",
|
||||
"develop", "implement", "compose", "draft", "produce",
|
||||
"craft", "construct", "prepare", "draw", "paint", "code"
|
||||
]
|
||||
prompt_lower = prompt.lower().strip()
|
||||
return any(prompt_lower.startswith(verb) for verb in task_verbs)
|
||||
|
||||
def _is_creative_task(self, prompt: str) -> bool:
|
||||
"""Check if prompt is a creative task."""
|
||||
creative_indicators = [
|
||||
"poem", "story", "script", "song", "art", "creative",
|
||||
"fiction", "novel", "essay", "blog", "article", "write"
|
||||
]
|
||||
prompt_lower = prompt.lower()
|
||||
return any(ind in prompt_lower for ind in creative_indicators)
|
||||
|
||||
def _detect_strategy(self, prompt: str) -> ExpandStrategy:
|
||||
"""Auto-detect the best expansion strategy for a prompt."""
|
||||
# Very short prompts need detailed expansion
|
||||
if self._is_short_prompt(prompt, threshold=5):
|
||||
return ExpandStrategy.DETAILED
|
||||
|
||||
# Creative tasks benefit from creative expansion
|
||||
if self._is_creative_task(prompt):
|
||||
return ExpandStrategy.CREATIVE
|
||||
|
||||
# Task prompts get structured expansion
|
||||
if self._is_task_prompt(prompt):
|
||||
return ExpandStrategy.STRUCTURED
|
||||
|
||||
# Default to basic
|
||||
return ExpandStrategy.BASIC
|
||||
|
||||
def expand(
|
||||
self,
|
||||
prompt: str,
|
||||
strategy: ExpandStrategy = ExpandStrategy.AUTO,
|
||||
context: Optional[str] = None
|
||||
) -> ExpandResult:
|
||||
"""
|
||||
Expand a prompt using the specified strategy.
|
||||
|
||||
All operations go through an internal Agent which automatically handles
|
||||
tool calling when tools are provided. The agent decides when to use tools
|
||||
based on the prompt context.
|
||||
|
||||
Args:
|
||||
prompt: The original user prompt
|
||||
strategy: Expansion strategy to use
|
||||
context: Additional context to help with expansion
|
||||
|
||||
Returns:
|
||||
ExpandResult containing the expanded prompt
|
||||
"""
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Expanding prompt: '{prompt}' with strategy: {strategy.value}")
|
||||
if self.tools:
|
||||
print(f"[cyan]Agent has {len(self.tools)} tools available (will use if needed)...[/cyan]")
|
||||
|
||||
# Auto-detect strategy if needed
|
||||
if strategy == ExpandStrategy.AUTO:
|
||||
strategy = self._detect_strategy(prompt)
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Auto-detected strategy: {strategy.value}")
|
||||
|
||||
# Build the expansion prompt
|
||||
if strategy == ExpandStrategy.AUTO:
|
||||
# Fallback if auto-detect returns AUTO
|
||||
strategy = ExpandStrategy.DETAILED
|
||||
|
||||
# Get current date for time-sensitive context
|
||||
current_date = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
expansion_prompt = self.PROMPTS[strategy.value].format(
|
||||
prompt=prompt,
|
||||
context=context or "",
|
||||
current_date=current_date
|
||||
)
|
||||
|
||||
# Call the agent
|
||||
expanded = self._call_agent(expansion_prompt).strip()
|
||||
|
||||
# If expansion failed, return original
|
||||
if not expanded:
|
||||
expanded = prompt
|
||||
|
||||
return ExpandResult(
|
||||
original_prompt=prompt,
|
||||
expanded_prompt=expanded,
|
||||
strategy_used=strategy,
|
||||
metadata={
|
||||
"original_length": len(prompt),
|
||||
"expanded_length": len(expanded),
|
||||
"has_context": bool(context),
|
||||
"has_tools": bool(self.tools)
|
||||
}
|
||||
)
|
||||
|
||||
# Convenience methods for direct strategy access
|
||||
def expand_basic(self, prompt: str, context: Optional[str] = None) -> ExpandResult:
|
||||
"""Convenience method for basic expansion."""
|
||||
return self.expand(prompt, strategy=ExpandStrategy.BASIC, context=context)
|
||||
|
||||
def expand_detailed(self, prompt: str, context: Optional[str] = None) -> ExpandResult:
|
||||
"""Convenience method for detailed expansion."""
|
||||
return self.expand(prompt, strategy=ExpandStrategy.DETAILED, context=context)
|
||||
|
||||
def expand_structured(self, prompt: str, context: Optional[str] = None) -> ExpandResult:
|
||||
"""Convenience method for structured expansion."""
|
||||
return self.expand(prompt, strategy=ExpandStrategy.STRUCTURED, context=context)
|
||||
|
||||
def expand_creative(self, prompt: str, context: Optional[str] = None) -> ExpandResult:
|
||||
"""Convenience method for creative expansion."""
|
||||
return self.expand(prompt, strategy=ExpandStrategy.CREATIVE, context=context)
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Query Rewriter Agent Module
|
||||
|
||||
This module provides the QueryRewriterAgent class for transforming user queries
|
||||
to improve retrieval quality in RAG applications.
|
||||
|
||||
Supported Rewriting Strategies:
|
||||
- **BASIC**: Simple rephrasing for clarity and keyword optimization
|
||||
- **HYDE**: Hypothetical Document Embeddings - generates a hypothetical answer
|
||||
- **STEP_BACK**: Generates higher-level concept questions for complex queries
|
||||
- **SUB_QUERIES**: Decomposes multi-part questions into focused sub-queries
|
||||
- **MULTI_QUERY**: Generates multiple paraphrased versions for ensemble retrieval
|
||||
- **CONTEXTUAL**: Uses conversation history to resolve references and context
|
||||
|
||||
Example:
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent(verbose=True)
|
||||
|
||||
# Basic rewriting
|
||||
result = agent.rewrite("AI trends")
|
||||
print(result.rewritten_queries)
|
||||
|
||||
# HyDE for better semantic matching
|
||||
result = agent.rewrite("What is quantum computing?", strategy=RewriteStrategy.HYDE)
|
||||
print(result.hypothetical_document)
|
||||
|
||||
# Contextual with chat history
|
||||
chat_history = [
|
||||
{"role": "user", "content": "Tell me about Python"},
|
||||
{"role": "assistant", "content": "Python is a programming language..."}
|
||||
]
|
||||
result = agent.rewrite("What about its performance?", chat_history=chat_history)
|
||||
|
||||
# With search tools for context-aware rewriting
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = QueryRewriterAgent(tools=[internet_search], verbose=True)
|
||||
result = agent.rewrite("latest AI developments") # Searches first, then rewrites
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import List, Optional, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RewriteStrategy(Enum):
|
||||
"""Enumeration of available query rewriting strategies."""
|
||||
BASIC = "basic"
|
||||
HYDE = "hyde"
|
||||
STEP_BACK = "step_back"
|
||||
SUB_QUERIES = "sub_queries"
|
||||
MULTI_QUERY = "multi_query"
|
||||
CONTEXTUAL = "contextual"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RewriteResult:
|
||||
"""Result of a query rewriting operation."""
|
||||
original_query: str
|
||||
rewritten_queries: List[str]
|
||||
strategy_used: RewriteStrategy
|
||||
hypothetical_document: Optional[str] = None
|
||||
step_back_question: Optional[str] = None
|
||||
sub_queries: Optional[List[str]] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __repr__(self):
|
||||
return f"RewriteResult(strategy={self.strategy_used.value}, queries={len(self.rewritten_queries)})"
|
||||
|
||||
@property
|
||||
def primary_query(self) -> str:
|
||||
"""Returns the primary rewritten query."""
|
||||
return self.rewritten_queries[0] if self.rewritten_queries else self.original_query
|
||||
|
||||
@property
|
||||
def all_queries(self) -> List[str]:
|
||||
"""Returns all queries including original and rewritten."""
|
||||
queries = [self.original_query]
|
||||
queries.extend(self.rewritten_queries)
|
||||
if self.step_back_question:
|
||||
queries.append(self.step_back_question)
|
||||
if self.sub_queries:
|
||||
queries.extend(self.sub_queries)
|
||||
return list(set(queries)) # Remove duplicates
|
||||
|
||||
|
||||
class QueryRewriterAgent:
|
||||
"""
|
||||
Agent for rewriting queries to improve retrieval quality in RAG applications.
|
||||
|
||||
This agent transforms user queries using various strategies to bridge the gap
|
||||
between how users ask questions and how information is stored in knowledge bases.
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for rewriting
|
||||
verbose: Whether to print detailed logs
|
||||
max_queries: Maximum number of queries to generate for multi-query strategy
|
||||
abbreviations: Dictionary of common abbreviations to expand
|
||||
|
||||
Example:
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini", verbose=True)
|
||||
result = agent.rewrite("ML best practices", strategy=RewriteStrategy.MULTI_QUERY)
|
||||
for query in result.rewritten_queries:
|
||||
print(query)
|
||||
"""
|
||||
|
||||
# Default prompts for each strategy
|
||||
PROMPTS = {
|
||||
"basic": """You are an expert at improving prompts and queries.
|
||||
|
||||
Given the following input, improve it while preserving its original intent and type.
|
||||
|
||||
Guidelines:
|
||||
- If it's a TASK (e.g., "write X", "create Y", "generate Z"), keep it as a task - do NOT convert to a question
|
||||
- If it's a QUESTION (e.g., "what is X?", "how does Y work?"), keep it as a question
|
||||
- Expand abbreviations and acronyms
|
||||
- Fix typos and grammatical errors
|
||||
- Add relevant context if too short
|
||||
- Keep the original intent and action intact
|
||||
|
||||
IMPORTANT: If the input is a command/task to DO something, the output must also be a command/task to DO that thing.
|
||||
|
||||
Input: {query}
|
||||
|
||||
Improved version:""",
|
||||
|
||||
"hyde": """You are an expert at generating hypothetical documents.
|
||||
|
||||
Given the following query, write a detailed hypothetical document that would perfectly answer this query. This document will be used to find similar real documents in a knowledge base.
|
||||
|
||||
Guidelines:
|
||||
- Write as if you are answering the query directly
|
||||
- Include specific details, facts, and terminology
|
||||
- Use the same style and vocabulary as the target documents
|
||||
- Be comprehensive but focused on the query topic
|
||||
|
||||
Query: {query}
|
||||
|
||||
Hypothetical document:""",
|
||||
|
||||
"step_back": """You are an expert at generating step-back questions.
|
||||
|
||||
Given the following query, generate a more general, higher-level question that captures the broader concept. This helps retrieve background information needed to answer the specific query.
|
||||
|
||||
Guidelines:
|
||||
- Identify the underlying concept or principle
|
||||
- Create a question about fundamentals or general knowledge
|
||||
- The step-back question should provide context for the original query
|
||||
|
||||
Query: {query}
|
||||
|
||||
Step-back question:""",
|
||||
|
||||
"sub_queries": """You are an expert at decomposing complex queries.
|
||||
|
||||
Given the following query, break it down into simpler, focused sub-queries. Each sub-query should address one specific aspect of the original question.
|
||||
|
||||
Guidelines:
|
||||
- Identify distinct aspects or components of the query
|
||||
- Create independent, self-contained sub-queries
|
||||
- Each sub-query should be answerable on its own
|
||||
- Return as a JSON array of strings
|
||||
|
||||
Query: {query}
|
||||
|
||||
Sub-queries (JSON array):""",
|
||||
|
||||
"multi_query": """You are an expert at query expansion and paraphrasing.
|
||||
|
||||
Given the following query, generate {num_queries} different versions that capture the same intent but use different words, phrases, or perspectives.
|
||||
|
||||
Guidelines:
|
||||
- Use synonyms and alternative phrasings
|
||||
- Try different question formats (what, how, why, etc.)
|
||||
- Include both formal and informal versions
|
||||
- Add relevant context or specificity
|
||||
- Return as a JSON array of strings
|
||||
|
||||
Query: {query}
|
||||
|
||||
Alternative queries (JSON array):""",
|
||||
|
||||
"contextual": """You are an expert at understanding conversational context.
|
||||
|
||||
Given the conversation history and a follow-up query, rewrite the query as a standalone question that captures all relevant context.
|
||||
|
||||
Guidelines:
|
||||
- Resolve pronouns and references (it, this, that, etc.)
|
||||
- Include relevant context from the conversation
|
||||
- Keep entity names and important details
|
||||
- Make the query self-contained and clear
|
||||
|
||||
Conversation history:
|
||||
{chat_history}
|
||||
|
||||
Follow-up query: {query}
|
||||
|
||||
Standalone question:""",
|
||||
|
||||
"auto": """You are an expert at analyzing and improving prompts and queries.
|
||||
|
||||
Analyze the following input and improve it while preserving its original intent and type.
|
||||
|
||||
Input types to consider:
|
||||
1. TASK/COMMAND (write, create, generate, build) → Keep as a task, make it clearer
|
||||
2. QUESTION (what, how, why, when) → Keep as a question, make it more specific
|
||||
3. Short/keyword inputs → Expand with context but preserve type
|
||||
4. Complex multi-part inputs → Clarify structure
|
||||
5. Well-formed inputs → Minor optimization only
|
||||
|
||||
IMPORTANT: Never convert a task into a question. "Write X" should remain "Write X", not "How to write X?"
|
||||
|
||||
Input: {query}
|
||||
{context}
|
||||
|
||||
Improved version:"""
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "QueryRewriterAgent",
|
||||
model: str = "gpt-4o-mini",
|
||||
instructions: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
max_queries: int = 5,
|
||||
abbreviations: Optional[Dict[str, str]] = None,
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 500,
|
||||
tools: Optional[List[Any]] = None
|
||||
):
|
||||
"""
|
||||
Initialize the QueryRewriterAgent.
|
||||
|
||||
Args:
|
||||
name: Name of the agent
|
||||
model: LLM model to use (default: gpt-4o-mini)
|
||||
instructions: Custom instructions for the agent
|
||||
verbose: Whether to print detailed logs
|
||||
max_queries: Maximum queries for multi-query strategy
|
||||
abbreviations: Custom abbreviation expansions
|
||||
temperature: Temperature for LLM generation
|
||||
max_tokens: Maximum tokens for LLM response
|
||||
tools: Optional list of tools (agent decides when to use them)
|
||||
"""
|
||||
self.name = name
|
||||
self.model = model
|
||||
self.instructions = instructions
|
||||
self.verbose = verbose
|
||||
self.max_queries = max_queries
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.tools = tools or []
|
||||
self._agent = None # Lazy initialized
|
||||
|
||||
# Default abbreviations (can be extended)
|
||||
self.abbreviations = abbreviations or {
|
||||
"AI": "Artificial Intelligence",
|
||||
"ML": "Machine Learning",
|
||||
"NLP": "Natural Language Processing",
|
||||
"LLM": "Large Language Model",
|
||||
"RAG": "Retrieval Augmented Generation",
|
||||
"API": "Application Programming Interface",
|
||||
"DB": "Database",
|
||||
"UI": "User Interface",
|
||||
"UX": "User Experience",
|
||||
"SDK": "Software Development Kit",
|
||||
"CLI": "Command Line Interface",
|
||||
}
|
||||
|
||||
# Setup logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
if self.verbose:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
@property
|
||||
def agent(self):
|
||||
"""Lazy initialization of internal Agent."""
|
||||
if self._agent is None:
|
||||
from .agent import Agent
|
||||
self._agent = Agent(
|
||||
name=self.name,
|
||||
role="Query Rewriting Specialist",
|
||||
goal="Rewrite queries for better search and retrieval results. Use available tools when needed to gather context.",
|
||||
backstory="You are an expert at understanding user intent and transforming queries for optimal information retrieval.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
)
|
||||
return self._agent
|
||||
|
||||
def _call_agent(self, prompt: str) -> str:
|
||||
"""Call the internal Agent with the given prompt."""
|
||||
try:
|
||||
response = self.agent.chat(prompt)
|
||||
return response if response else ""
|
||||
except Exception as e:
|
||||
self.logger.error(f"Agent call failed: {e}")
|
||||
raise
|
||||
|
||||
def _expand_abbreviations(self, query: str) -> str:
|
||||
"""Expand known abbreviations in the query."""
|
||||
words = query.split()
|
||||
expanded = []
|
||||
for word in words:
|
||||
clean_word = word.strip(".,!?;:")
|
||||
if clean_word.upper() in self.abbreviations:
|
||||
expanded.append(f"{clean_word} ({self.abbreviations[clean_word.upper()]})")
|
||||
else:
|
||||
expanded.append(word)
|
||||
return " ".join(expanded)
|
||||
|
||||
def _is_short_query(self, query: str, threshold: int = 5) -> bool:
|
||||
"""Check if query is considered short (keyword-style)."""
|
||||
return len(query.split()) < threshold
|
||||
|
||||
def _parse_json_array(self, text: str) -> List[str]:
|
||||
"""Parse a JSON array from LLM response."""
|
||||
try:
|
||||
# Try direct JSON parsing
|
||||
result = json.loads(text)
|
||||
if isinstance(result, list):
|
||||
return [str(item) for item in result]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Try to extract JSON array from text
|
||||
import re
|
||||
match = re.search(r'\[.*?\]', text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
result = json.loads(match.group())
|
||||
if isinstance(result, list):
|
||||
return [str(item) for item in result]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Fallback: split by newlines and clean up
|
||||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
lines = [line.lstrip('0123456789.-) ').strip('"\'') for line in lines]
|
||||
return [line for line in lines if line and len(line) > 3]
|
||||
|
||||
def rewrite(
|
||||
self,
|
||||
query: str,
|
||||
strategy: RewriteStrategy = RewriteStrategy.AUTO,
|
||||
chat_history: Optional[List[Dict[str, str]]] = None,
|
||||
context: Optional[str] = None,
|
||||
num_queries: int = None
|
||||
) -> RewriteResult:
|
||||
"""
|
||||
Rewrite a query using the specified strategy.
|
||||
|
||||
All operations go through an internal Agent which automatically handles
|
||||
tool calling when tools are provided. The agent decides when to use tools
|
||||
based on the query context.
|
||||
|
||||
Args:
|
||||
query: The original user query
|
||||
strategy: Rewriting strategy to use
|
||||
chat_history: Previous conversation messages for contextual rewriting
|
||||
context: Additional context about the knowledge base
|
||||
num_queries: Number of queries for multi-query strategy
|
||||
|
||||
Returns:
|
||||
RewriteResult containing rewritten queries
|
||||
"""
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Rewriting query: '{query}' with strategy: {strategy.value}")
|
||||
if self.tools:
|
||||
print(f"[cyan]Agent has {len(self.tools)} tools available (will use if needed)...[/cyan]")
|
||||
|
||||
# Auto-detect strategy if needed
|
||||
if strategy == RewriteStrategy.AUTO:
|
||||
strategy = self._detect_strategy(query, chat_history)
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Auto-detected strategy: {strategy.value}")
|
||||
|
||||
# All calls go through the internal Agent which handles tool calling
|
||||
combined_context = context or ""
|
||||
|
||||
# Dispatch to appropriate method
|
||||
if strategy == RewriteStrategy.BASIC:
|
||||
return self._rewrite_basic(query, combined_context)
|
||||
elif strategy == RewriteStrategy.HYDE:
|
||||
return self._rewrite_hyde(query, combined_context)
|
||||
elif strategy == RewriteStrategy.STEP_BACK:
|
||||
return self._rewrite_step_back(query, combined_context)
|
||||
elif strategy == RewriteStrategy.SUB_QUERIES:
|
||||
return self._rewrite_sub_queries(query, combined_context)
|
||||
elif strategy == RewriteStrategy.MULTI_QUERY:
|
||||
return self._rewrite_multi_query(query, num_queries or self.max_queries, combined_context)
|
||||
elif strategy == RewriteStrategy.CONTEXTUAL:
|
||||
return self._rewrite_contextual(query, chat_history or [], combined_context)
|
||||
else:
|
||||
# Default to basic
|
||||
return self._rewrite_basic(query, combined_context)
|
||||
|
||||
def _detect_strategy(
|
||||
self,
|
||||
query: str,
|
||||
chat_history: Optional[List[Dict[str, str]]] = None
|
||||
) -> RewriteStrategy:
|
||||
"""Auto-detect the best rewriting strategy for a query."""
|
||||
# If chat history exists and query seems like a follow-up
|
||||
if chat_history and self._is_follow_up(query):
|
||||
return RewriteStrategy.CONTEXTUAL
|
||||
|
||||
# Short keyword queries
|
||||
if self._is_short_query(query):
|
||||
return RewriteStrategy.BASIC
|
||||
|
||||
# Complex multi-part queries
|
||||
if self._is_complex_query(query):
|
||||
return RewriteStrategy.SUB_QUERIES
|
||||
|
||||
# Questions that might benefit from step-back
|
||||
if self._needs_step_back(query):
|
||||
return RewriteStrategy.STEP_BACK
|
||||
|
||||
# Default to basic
|
||||
return RewriteStrategy.BASIC
|
||||
|
||||
def _is_follow_up(self, query: str) -> bool:
|
||||
"""Check if query appears to be a follow-up question."""
|
||||
follow_up_indicators = [
|
||||
"it", "this", "that", "they", "them", "their",
|
||||
"what about", "how about", "and", "also", "more",
|
||||
"same", "similar", "related"
|
||||
]
|
||||
query_lower = query.lower()
|
||||
return any(indicator in query_lower for indicator in follow_up_indicators)
|
||||
|
||||
def _is_complex_query(self, query: str) -> bool:
|
||||
"""Check if query contains multiple distinct questions."""
|
||||
# Check for multiple question marks or conjunctions
|
||||
if query.count('?') > 1:
|
||||
return True
|
||||
complex_indicators = [" and ", " or ", " also ", " as well as ", " plus "]
|
||||
return any(ind in query.lower() for ind in complex_indicators)
|
||||
|
||||
def _needs_step_back(self, query: str) -> bool:
|
||||
"""Check if query would benefit from a step-back question."""
|
||||
# Queries about specific details often benefit from broader context
|
||||
specific_indicators = [
|
||||
"specific", "exactly", "precisely", "particular",
|
||||
"difference between", "compare", "versus", "vs"
|
||||
]
|
||||
return any(ind in query.lower() for ind in specific_indicators)
|
||||
|
||||
def _rewrite_basic(self, query: str, context: str = "") -> RewriteResult:
|
||||
"""Basic query rewriting for clarity and keyword optimization."""
|
||||
# First expand abbreviations
|
||||
expanded_query = self._expand_abbreviations(query)
|
||||
|
||||
# Build prompt with optional context
|
||||
prompt = self.PROMPTS["basic"].format(query=expanded_query)
|
||||
if context:
|
||||
prompt += f"\n\nAdditional context to help with rewriting:\n{context}"
|
||||
|
||||
rewritten = self._call_agent(prompt).strip()
|
||||
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=[rewritten],
|
||||
strategy_used=RewriteStrategy.BASIC,
|
||||
metadata={"expanded_abbreviations": expanded_query != query, "has_context": bool(context)}
|
||||
)
|
||||
|
||||
def _rewrite_hyde(self, query: str, context: str = "") -> RewriteResult:
|
||||
"""HyDE: Generate hypothetical document for better semantic matching."""
|
||||
prompt = self.PROMPTS["hyde"].format(query=query)
|
||||
if context:
|
||||
prompt += f"\n\nUse this context to make the hypothetical document more accurate:\n{context}"
|
||||
|
||||
hypothetical_doc = self._call_agent(prompt).strip()
|
||||
|
||||
# The hypothetical document itself becomes the search query
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=[hypothetical_doc[:500]], # Truncate for embedding
|
||||
strategy_used=RewriteStrategy.HYDE,
|
||||
hypothetical_document=hypothetical_doc,
|
||||
metadata={"full_document_length": len(hypothetical_doc), "has_context": bool(context)}
|
||||
)
|
||||
|
||||
def _rewrite_step_back(self, query: str, context: str = "") -> RewriteResult:
|
||||
"""Generate step-back question for broader context retrieval."""
|
||||
# Generate step-back question
|
||||
prompt = self.PROMPTS["step_back"].format(query=query)
|
||||
if context:
|
||||
prompt += f"\n\nContext:\n{context}"
|
||||
step_back = self._call_agent(prompt).strip()
|
||||
|
||||
# Also do basic rewriting of original
|
||||
basic_result = self._rewrite_basic(query, context)
|
||||
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=basic_result.rewritten_queries,
|
||||
strategy_used=RewriteStrategy.STEP_BACK,
|
||||
step_back_question=step_back,
|
||||
metadata={"includes_step_back": True, "has_context": bool(context)}
|
||||
)
|
||||
|
||||
def _rewrite_sub_queries(self, query: str, context: str = "") -> RewriteResult:
|
||||
"""Decompose complex query into focused sub-queries."""
|
||||
prompt = self.PROMPTS["sub_queries"].format(query=query)
|
||||
if context:
|
||||
prompt += f"\n\nContext to help with decomposition:\n{context}"
|
||||
response = self._call_agent(prompt).strip()
|
||||
|
||||
sub_queries = self._parse_json_array(response)
|
||||
|
||||
# Ensure we have at least one sub-query
|
||||
if not sub_queries:
|
||||
sub_queries = [query]
|
||||
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=sub_queries,
|
||||
strategy_used=RewriteStrategy.SUB_QUERIES,
|
||||
sub_queries=sub_queries,
|
||||
metadata={"num_sub_queries": len(sub_queries), "has_context": bool(context)}
|
||||
)
|
||||
|
||||
def _rewrite_multi_query(self, query: str, num_queries: int, context: str = "") -> RewriteResult:
|
||||
"""Generate multiple paraphrased versions for ensemble retrieval."""
|
||||
prompt = self.PROMPTS["multi_query"].format(
|
||||
query=query,
|
||||
num_queries=num_queries
|
||||
)
|
||||
if context:
|
||||
prompt += f"\n\nContext to help with paraphrasing:\n{context}"
|
||||
response = self._call_agent(prompt).strip()
|
||||
|
||||
queries = self._parse_json_array(response)
|
||||
|
||||
# Ensure we have queries and limit to max
|
||||
if not queries:
|
||||
queries = [query]
|
||||
queries = queries[:num_queries]
|
||||
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=queries,
|
||||
strategy_used=RewriteStrategy.MULTI_QUERY,
|
||||
metadata={"num_generated": len(queries), "has_context": bool(context)}
|
||||
)
|
||||
|
||||
def _rewrite_contextual(
|
||||
self,
|
||||
query: str,
|
||||
chat_history: List[Dict[str, str]],
|
||||
context: str = ""
|
||||
) -> RewriteResult:
|
||||
"""Rewrite query using conversation context."""
|
||||
# Format chat history
|
||||
history_str = ""
|
||||
for msg in chat_history[-10:]: # Last 10 messages
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
history_str += f"{role.capitalize()}: {content}\n"
|
||||
|
||||
prompt = self.PROMPTS["contextual"].format(
|
||||
chat_history=history_str,
|
||||
query=query
|
||||
)
|
||||
if context:
|
||||
prompt += f"\n\nAdditional context:\n{context}"
|
||||
rewritten = self._call_agent(prompt).strip()
|
||||
|
||||
return RewriteResult(
|
||||
original_query=query,
|
||||
rewritten_queries=[rewritten],
|
||||
strategy_used=RewriteStrategy.CONTEXTUAL,
|
||||
metadata={
|
||||
"history_length": len(chat_history),
|
||||
"history_used": min(10, len(chat_history)),
|
||||
"has_context": bool(context)
|
||||
}
|
||||
)
|
||||
|
||||
# Convenience methods for direct strategy access
|
||||
def rewrite_basic(self, query: str) -> RewriteResult:
|
||||
"""Convenience method for basic rewriting."""
|
||||
return self.rewrite(query, strategy=RewriteStrategy.BASIC)
|
||||
|
||||
def rewrite_hyde(self, query: str) -> RewriteResult:
|
||||
"""Convenience method for HyDE rewriting."""
|
||||
return self.rewrite(query, strategy=RewriteStrategy.HYDE)
|
||||
|
||||
def rewrite_step_back(self, query: str) -> RewriteResult:
|
||||
"""Convenience method for step-back rewriting."""
|
||||
return self.rewrite(query, strategy=RewriteStrategy.STEP_BACK)
|
||||
|
||||
def rewrite_sub_queries(self, query: str) -> RewriteResult:
|
||||
"""Convenience method for sub-query decomposition."""
|
||||
return self.rewrite(query, strategy=RewriteStrategy.SUB_QUERIES)
|
||||
|
||||
def rewrite_multi_query(self, query: str, num_queries: int = None) -> RewriteResult:
|
||||
"""Convenience method for multi-query generation."""
|
||||
return self.rewrite(
|
||||
query,
|
||||
strategy=RewriteStrategy.MULTI_QUERY,
|
||||
num_queries=num_queries
|
||||
)
|
||||
|
||||
def rewrite_contextual(
|
||||
self,
|
||||
query: str,
|
||||
chat_history: List[Dict[str, str]]
|
||||
) -> RewriteResult:
|
||||
"""Convenience method for contextual rewriting."""
|
||||
return self.rewrite(
|
||||
query,
|
||||
strategy=RewriteStrategy.CONTEXTUAL,
|
||||
chat_history=chat_history
|
||||
)
|
||||
|
||||
def add_abbreviation(self, abbrev: str, expansion: str) -> None:
|
||||
"""Add a custom abbreviation expansion."""
|
||||
self.abbreviations[abbrev.upper()] = expansion
|
||||
|
||||
def add_abbreviations(self, abbreviations: Dict[str, str]) -> None:
|
||||
"""Add multiple custom abbreviation expansions."""
|
||||
for abbrev, expansion in abbreviations.items():
|
||||
self.abbreviations[abbrev.upper()] = expansion
|
||||
@@ -127,7 +127,7 @@ 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):
|
||||
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):
|
||||
# Add check at the start if memory is requested
|
||||
if memory:
|
||||
try:
|
||||
@@ -163,7 +163,7 @@ class PraisonAIAgents:
|
||||
self.name = name # Store the name for the Agents collection
|
||||
|
||||
# Check for manager_llm in environment variable if not provided
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
|
||||
# Set logger level based on verbose
|
||||
if verbose >= 5:
|
||||
@@ -213,6 +213,16 @@ class PraisonAIAgents:
|
||||
|
||||
self._state = {} # Add state storage at PraisonAIAgents level
|
||||
|
||||
# Initialize planning mode
|
||||
self.planning = planning
|
||||
self.planning_llm = planning_llm or "gpt-4o-mini"
|
||||
self.auto_approve_plan = auto_approve_plan
|
||||
self.planning_tools = planning_tools
|
||||
self.planning_reasoning = planning_reasoning
|
||||
self._current_plan = None
|
||||
self._todo_list = None
|
||||
self._planning_agent = None
|
||||
|
||||
# Initialize memory system
|
||||
self.shared_memory = None
|
||||
if memory:
|
||||
@@ -929,9 +939,13 @@ Context:
|
||||
task.context = []
|
||||
# Add content to context
|
||||
task.context.append(content)
|
||||
|
||||
# Run tasks as before
|
||||
self.run_all_tasks()
|
||||
|
||||
# Planning Mode: Create plan and todo list before execution
|
||||
if self.planning:
|
||||
self._run_with_planning()
|
||||
else:
|
||||
# Run tasks as before
|
||||
self.run_all_tasks()
|
||||
|
||||
# Auto-display token metrics if any agent has metrics=True
|
||||
metrics_enabled = any(getattr(agent, 'metrics', False) for agent in self.agents)
|
||||
@@ -1532,3 +1546,354 @@ Context:
|
||||
else:
|
||||
display_error(f"Invalid protocol: {protocol}. Choose 'http' or 'mcp'.")
|
||||
return None
|
||||
|
||||
# =========================================================================
|
||||
# Planning Mode Properties and Methods
|
||||
# =========================================================================
|
||||
|
||||
@property
|
||||
def current_plan(self):
|
||||
"""Get the current plan."""
|
||||
return self._current_plan
|
||||
|
||||
@property
|
||||
def todo_list(self):
|
||||
"""Get the current todo list."""
|
||||
return self._todo_list
|
||||
|
||||
def _get_planning_agent(self):
|
||||
"""Lazy load PlanningAgent."""
|
||||
if self._planning_agent is None and self.planning:
|
||||
from ..planning import PlanningAgent
|
||||
self._planning_agent = PlanningAgent(
|
||||
llm=self.planning_llm,
|
||||
read_only=True,
|
||||
verbose=self.verbose,
|
||||
tools=self.planning_tools,
|
||||
reasoning=self.planning_reasoning
|
||||
)
|
||||
return self._planning_agent
|
||||
|
||||
async def _create_plan(self, request: str = None, context: str = None):
|
||||
"""
|
||||
Create an implementation plan.
|
||||
|
||||
Args:
|
||||
request: The request/goal to plan for
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
Plan instance
|
||||
"""
|
||||
planner = self._get_planning_agent()
|
||||
if planner is None:
|
||||
return None
|
||||
|
||||
# Use first task description as request if not provided
|
||||
if request is None and self.tasks:
|
||||
first_task = list(self.tasks.values())[0]
|
||||
request = first_task.description
|
||||
|
||||
plan = await planner.create_plan(
|
||||
request=request,
|
||||
agents=self.agents,
|
||||
tasks=list(self.tasks.values()),
|
||||
context=context
|
||||
)
|
||||
|
||||
self._current_plan = plan
|
||||
|
||||
# Create todo list from plan
|
||||
from ..planning import TodoList
|
||||
self._todo_list = TodoList.from_plan(plan)
|
||||
|
||||
return plan
|
||||
|
||||
def _create_plan_sync(self, request: str = None, context: str = None):
|
||||
"""
|
||||
Synchronous version of _create_plan.
|
||||
|
||||
Args:
|
||||
request: The request/goal to plan for
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
Plan instance
|
||||
"""
|
||||
planner = self._get_planning_agent()
|
||||
if planner is None:
|
||||
return None
|
||||
|
||||
# Use first task description as request if not provided
|
||||
if request is None and self.tasks:
|
||||
first_task = list(self.tasks.values())[0]
|
||||
request = first_task.description
|
||||
|
||||
plan = planner.create_plan_sync(
|
||||
request=request,
|
||||
agents=self.agents,
|
||||
tasks=list(self.tasks.values()),
|
||||
context=context
|
||||
)
|
||||
|
||||
self._current_plan = plan
|
||||
|
||||
# Create todo list from plan
|
||||
from ..planning import TodoList
|
||||
self._todo_list = TodoList.from_plan(plan)
|
||||
|
||||
return plan
|
||||
|
||||
async def _request_approval(self, plan):
|
||||
"""
|
||||
Request approval for a plan.
|
||||
|
||||
Args:
|
||||
plan: Plan to approve
|
||||
|
||||
Returns:
|
||||
True if approved, False if rejected
|
||||
"""
|
||||
if self.auto_approve_plan:
|
||||
plan.approve()
|
||||
return True
|
||||
|
||||
from ..planning import ApprovalCallback
|
||||
callback = ApprovalCallback(auto_approve=self.auto_approve_plan)
|
||||
return await callback.async_call(plan)
|
||||
|
||||
def _request_approval_sync(self, plan):
|
||||
"""
|
||||
Synchronous version of _request_approval.
|
||||
|
||||
Args:
|
||||
plan: Plan to approve
|
||||
|
||||
Returns:
|
||||
True if approved, False if rejected
|
||||
"""
|
||||
if self.auto_approve_plan:
|
||||
plan.approve()
|
||||
return True
|
||||
|
||||
from ..planning import ApprovalCallback
|
||||
callback = ApprovalCallback(auto_approve=self.auto_approve_plan)
|
||||
return callback(plan)
|
||||
|
||||
def get_plan_markdown(self) -> str:
|
||||
"""
|
||||
Get the current plan as markdown.
|
||||
|
||||
Returns:
|
||||
Markdown string or empty string if no plan
|
||||
"""
|
||||
if self._current_plan:
|
||||
return self._current_plan.to_markdown()
|
||||
return ""
|
||||
|
||||
def get_todo_markdown(self) -> str:
|
||||
"""
|
||||
Get the current todo list as markdown.
|
||||
|
||||
Returns:
|
||||
Markdown string or empty string if no todo list
|
||||
"""
|
||||
if self._todo_list:
|
||||
return self._todo_list.to_markdown()
|
||||
return ""
|
||||
|
||||
def update_plan_step_status(self, step_id: str, status: str) -> bool:
|
||||
"""
|
||||
Update the status of a plan step.
|
||||
|
||||
Args:
|
||||
step_id: ID of the step to update
|
||||
status: New status
|
||||
|
||||
Returns:
|
||||
True if updated, False if not found
|
||||
"""
|
||||
if self._current_plan:
|
||||
result = self._current_plan.update_step_status(step_id, status)
|
||||
# Sync todo list
|
||||
if self._todo_list:
|
||||
self._todo_list.sync_with_plan(self._current_plan)
|
||||
return result
|
||||
return False
|
||||
|
||||
def _run_with_planning(self):
|
||||
"""
|
||||
Run tasks with planning mode enabled.
|
||||
|
||||
This method:
|
||||
1. Creates a plan using PlanningAgent
|
||||
2. Generates a todo list from the plan
|
||||
3. Creates proper Task objects from plan steps
|
||||
4. Executes each task using the full Task execution system
|
||||
(with memory, callbacks, guardrails, structured output, etc.)
|
||||
5. Tracks progress as items are completed
|
||||
"""
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.markdown import Markdown
|
||||
from ..task import Task
|
||||
|
||||
console = Console()
|
||||
|
||||
# Step 1: Create the plan
|
||||
console.print("\n[bold blue]📋 PLANNING PHASE[/bold blue]")
|
||||
console.print("[dim]Creating implementation plan...[/dim]\n")
|
||||
|
||||
# Build request from tasks
|
||||
task_descriptions = [task.description for task in self.tasks.values()]
|
||||
request = " AND ".join(task_descriptions)
|
||||
|
||||
plan = self._create_plan_sync(request=request)
|
||||
|
||||
if not plan:
|
||||
console.print("[yellow]⚠️ Planning failed, falling back to normal execution[/yellow]")
|
||||
self.run_all_tasks()
|
||||
return
|
||||
|
||||
# Display the plan
|
||||
console.print(Panel(
|
||||
Markdown(plan.to_markdown()),
|
||||
title="[bold green]Generated Plan[/bold green]",
|
||||
border_style="green"
|
||||
))
|
||||
|
||||
# Step 2: Request approval if not auto-approve
|
||||
if not self.auto_approve_plan:
|
||||
approved = self._request_approval_sync(plan)
|
||||
if not approved:
|
||||
console.print("[red]❌ Plan rejected. Aborting execution.[/red]")
|
||||
return
|
||||
else:
|
||||
plan.approve()
|
||||
console.print("[green]✅ Plan auto-approved[/green]")
|
||||
|
||||
# Step 3: Create todo list for tracking
|
||||
from ..planning import TodoList
|
||||
self._todo_list = TodoList.from_plan(plan)
|
||||
|
||||
console.print("\n[bold blue]📝 TODO LIST[/bold blue]")
|
||||
console.print(Panel(
|
||||
Markdown(self._todo_list.to_markdown()),
|
||||
title="[bold cyan]Tasks to Complete[/bold cyan]",
|
||||
border_style="cyan"
|
||||
))
|
||||
|
||||
# Step 4: Create proper Task objects from plan steps
|
||||
console.print("\n[bold blue]🚀 EXECUTION PHASE[/bold blue]\n")
|
||||
|
||||
# Map agent names to agent instances
|
||||
agent_map = {agent.name: agent for agent in self.agents}
|
||||
|
||||
# Store original tasks and create new tasks from plan
|
||||
original_tasks = self.tasks.copy()
|
||||
self.tasks = {}
|
||||
self.task_id_counter = 0
|
||||
|
||||
# Create Task objects from plan steps
|
||||
plan_tasks = []
|
||||
step_to_task = {} # Map step_id to task for context chaining
|
||||
|
||||
for i, step in enumerate(plan.steps):
|
||||
# Get the appropriate agent
|
||||
agent = agent_map.get(step.agent, self.agents[0] if self.agents else None)
|
||||
|
||||
if not agent:
|
||||
console.print(f"[yellow]⚠️ No agent found for '{step.agent}', using first available[/yellow]")
|
||||
agent = self.agents[0] if self.agents else None
|
||||
|
||||
if not agent:
|
||||
console.print(f"[red]❌ No agents available for step: {step.description}[/red]")
|
||||
continue
|
||||
|
||||
# Build context from dependencies (previous task results)
|
||||
context = []
|
||||
for dep_id in step.dependencies:
|
||||
# Convert step_X format to actual step index
|
||||
if dep_id.startswith("step_"):
|
||||
try:
|
||||
dep_index = int(dep_id.split("_")[1])
|
||||
if dep_index < len(plan_tasks):
|
||||
context.append(plan_tasks[dep_index])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
elif dep_id in step_to_task:
|
||||
context.append(step_to_task[dep_id])
|
||||
|
||||
# Find matching original task for additional config (memory, callbacks, etc.)
|
||||
original_task = None
|
||||
for orig_task in original_tasks.values():
|
||||
if orig_task.agent and orig_task.agent.name == agent.name:
|
||||
original_task = orig_task
|
||||
break
|
||||
|
||||
# Create Task with full features from original task if available
|
||||
task = Task(
|
||||
description=step.description,
|
||||
expected_output=f"Complete: {step.description}",
|
||||
agent=agent,
|
||||
name=f"Plan Step {i + 1}",
|
||||
tools=agent.tools if agent.tools else [],
|
||||
context=context if context else None,
|
||||
# Inherit from original task if available
|
||||
memory=original_task.memory if original_task else None,
|
||||
callback=original_task.callback if original_task else None,
|
||||
guardrail=original_task.guardrail if original_task else None,
|
||||
max_retries=original_task.max_retries if original_task else 3,
|
||||
output_json=original_task.output_json if original_task else None,
|
||||
output_pydantic=original_task.output_pydantic if original_task else None,
|
||||
config=original_task.config if original_task else {}
|
||||
)
|
||||
|
||||
# Add task to our task list
|
||||
task_id = self.add_task(task)
|
||||
plan_tasks.append(task)
|
||||
step_to_task[step.id] = task
|
||||
|
||||
# Step 5: Execute tasks using the proper Task execution system
|
||||
for i, (task_id, task) in enumerate(self.tasks.items()):
|
||||
# Update todo list progress
|
||||
if i < len(self._todo_list.items):
|
||||
item = self._todo_list.items[i]
|
||||
|
||||
# Display progress bar
|
||||
progress = self._todo_list.progress
|
||||
bar_length = 30
|
||||
filled = int(bar_length * progress)
|
||||
bar = "█" * filled + "░" * (bar_length - filled)
|
||||
console.print(f"[dim]Progress: [{bar}] {progress * 100:.0f}%[/dim]")
|
||||
|
||||
console.print(f"\n[bold]📌 Step {i + 1}/{len(self.tasks)}:[/bold] {task.description[:60]}...")
|
||||
console.print(f"[dim] Agent: {task.agent.name if task.agent else 'Unknown'}[/dim]")
|
||||
|
||||
# Mark as in progress
|
||||
self._todo_list.start(item.id)
|
||||
|
||||
# Execute using the full Task execution system
|
||||
# This includes: memory, callbacks, guardrails, structured output, retry logic
|
||||
try:
|
||||
self.run_task(task_id)
|
||||
|
||||
if task.status == "completed":
|
||||
if i < len(self._todo_list.items):
|
||||
self._todo_list.complete(self._todo_list.items[i].id)
|
||||
console.print("[green] ✅ Completed[/green]")
|
||||
else:
|
||||
console.print(f"[yellow] ⚠️ Task status: {task.status}[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"[red] ❌ Error: {e}[/red]")
|
||||
logger.error(f"Error executing plan task {task_id}: {e}")
|
||||
|
||||
# Final progress
|
||||
completed_count = len([t for t in self.tasks.values() if t.status == "completed"])
|
||||
console.print(f"\n[bold green]🎉 EXECUTION COMPLETE[/bold green]")
|
||||
console.print(f"[dim]Progress: [{'█' * 30}] 100%[/dim]")
|
||||
console.print(f"[green]Completed {completed_count}/{len(self.tasks)} tasks![/green]\n")
|
||||
|
||||
# Restore original tasks reference for result retrieval
|
||||
self._plan_tasks = self.tasks.copy()
|
||||
# Keep plan tasks for results but note original tasks are preserved in _plan_tasks
|
||||
@@ -92,7 +92,7 @@ class AutoAgents(PraisonAIAgents):
|
||||
self.self_reflect = self_reflect
|
||||
self.max_reflect = max_reflect
|
||||
self.min_reflect = min_reflect
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
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
|
||||
|
||||
@@ -61,7 +61,7 @@ class LLM:
|
||||
MODEL_WINDOWS = {
|
||||
# OpenAI
|
||||
"gpt-4": 6144, # 8,192 actual
|
||||
"gpt-4o": 96000, # 128,000 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-4-turbo": 96000, # 128,000 actual
|
||||
"o1-preview": 96000, # 128,000 actual
|
||||
@@ -239,6 +239,10 @@ class LLM:
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
events: List[Any] = [],
|
||||
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,
|
||||
**extra_settings
|
||||
):
|
||||
# Configure logging only once at the class level
|
||||
@@ -273,6 +277,11 @@ class LLM:
|
||||
self.base_url = base_url
|
||||
self.events = events
|
||||
self.extra_settings = extra_settings
|
||||
self.web_search = web_search
|
||||
self.web_fetch = web_fetch
|
||||
self.prompt_caching = prompt_caching
|
||||
self.claude_memory = claude_memory
|
||||
self._claude_memory_tool = None # Lazy initialized
|
||||
self._console = None # Lazy load console when needed
|
||||
self.chat_history = []
|
||||
self.verbose = extra_settings.get('verbose', True)
|
||||
@@ -381,6 +390,109 @@ class LLM:
|
||||
else:
|
||||
return False
|
||||
|
||||
def _supports_web_search(self) -> bool:
|
||||
"""
|
||||
Check if the current model supports native web search via LiteLLM.
|
||||
|
||||
Native web search allows the model to search the web in real-time
|
||||
without requiring external tools like DuckDuckGo.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports native web search, False otherwise
|
||||
"""
|
||||
from .model_capabilities import supports_web_search
|
||||
return supports_web_search(self.model)
|
||||
|
||||
def _supports_web_fetch(self) -> bool:
|
||||
"""
|
||||
Check if the current model supports web fetch via LiteLLM.
|
||||
|
||||
Web fetch allows the model to retrieve full content from specific URLs
|
||||
(web pages and PDF documents). Currently only supported by Anthropic Claude models.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports web fetch, False otherwise
|
||||
"""
|
||||
from .model_capabilities import supports_web_fetch
|
||||
return supports_web_fetch(self.model)
|
||||
|
||||
def _supports_prompt_caching(self) -> bool:
|
||||
"""
|
||||
Check if the current model supports prompt caching via LiteLLM.
|
||||
|
||||
Prompt caching allows caching parts of prompts to reduce costs and latency.
|
||||
Supported by OpenAI, Anthropic, Bedrock, and Deepseek.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports prompt caching, False otherwise
|
||||
"""
|
||||
from .model_capabilities import supports_prompt_caching
|
||||
return supports_prompt_caching(self.model)
|
||||
|
||||
def _get_claude_memory_tool(self):
|
||||
"""
|
||||
Get or initialize the Claude Memory Tool instance.
|
||||
|
||||
Returns:
|
||||
ClaudeMemoryTool instance if claude_memory is enabled, None otherwise
|
||||
"""
|
||||
if not self.claude_memory:
|
||||
return None
|
||||
|
||||
if self._claude_memory_tool is None:
|
||||
from ..tools.claude_memory_tool import ClaudeMemoryTool
|
||||
|
||||
if isinstance(self.claude_memory, bool):
|
||||
# Create default memory tool
|
||||
self._claude_memory_tool = ClaudeMemoryTool()
|
||||
else:
|
||||
# Use provided memory tool instance
|
||||
self._claude_memory_tool = self.claude_memory
|
||||
|
||||
return self._claude_memory_tool
|
||||
|
||||
def _supports_claude_memory(self) -> bool:
|
||||
"""
|
||||
Check if the current model supports Claude Memory Tool.
|
||||
|
||||
Claude Memory Tool is only supported by Anthropic Claude models.
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports Claude Memory Tool
|
||||
"""
|
||||
if not self.claude_memory:
|
||||
return False
|
||||
|
||||
return self._is_anthropic_model()
|
||||
|
||||
def _is_memory_tool_call(self, function_name: str) -> bool:
|
||||
"""
|
||||
Check if a tool call is for the Claude Memory Tool.
|
||||
|
||||
Args:
|
||||
function_name: Name of the function being called
|
||||
|
||||
Returns:
|
||||
bool: True if this is a memory tool call
|
||||
"""
|
||||
return function_name == "memory" and self._supports_claude_memory()
|
||||
|
||||
def _execute_memory_tool_call(self, arguments: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Execute a Claude Memory Tool call.
|
||||
|
||||
Args:
|
||||
arguments: The arguments from Claude's tool call
|
||||
|
||||
Returns:
|
||||
Result string from the memory operation
|
||||
"""
|
||||
memory_tool = self._get_claude_memory_tool()
|
||||
if not memory_tool:
|
||||
return "Error: Memory tool not initialized"
|
||||
|
||||
return memory_tool.process_tool_call(arguments)
|
||||
|
||||
def _generate_ollama_tool_summary(self, tool_results: List[Any], response_text: str) -> Optional[str]:
|
||||
"""
|
||||
Generate a summary from tool results for Ollama to prevent infinite loops.
|
||||
@@ -724,7 +836,21 @@ class LLM:
|
||||
|
||||
# Skip system messages for legacy o1 models as they don't support them
|
||||
if not self._needs_system_message_skip():
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
# Apply prompt caching for Anthropic models if enabled
|
||||
if self.prompt_caching and self._supports_prompt_caching() and self._is_anthropic_model():
|
||||
# Anthropic requires cache_control in content array format
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": system_prompt,
|
||||
"cache_control": {"type": "ephemeral"}
|
||||
}
|
||||
]
|
||||
})
|
||||
else:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
|
||||
# Add chat history if provided
|
||||
if chat_history:
|
||||
@@ -907,7 +1033,7 @@ class LLM:
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
temperature: float = 0.2,
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
output_json: Optional[BaseModel] = None,
|
||||
output_pydantic: Optional[BaseModel] = None,
|
||||
@@ -2042,7 +2168,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
temperature: float = 0.2,
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
output_json: Optional[BaseModel] = None,
|
||||
output_pydantic: Optional[BaseModel] = None,
|
||||
@@ -2295,6 +2421,12 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
return False
|
||||
return any(prefix in self.model.lower() for prefix in ['gemini', 'gemini/', 'google/gemini'])
|
||||
|
||||
def _is_anthropic_model(self) -> bool:
|
||||
"""Check if the model is an Anthropic Claude model."""
|
||||
if not self.model:
|
||||
return False
|
||||
return any(prefix in self.model.lower() for prefix in ['claude', 'anthropic/', 'anthropic/claude'])
|
||||
|
||||
def _is_streaming_error_recoverable(self, error: Exception) -> bool:
|
||||
"""Check if a streaming error is recoverable (e.g., malformed chunk vs connection error)."""
|
||||
error_msg = str(error).lower()
|
||||
@@ -2317,7 +2449,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
temperature: float = 0.2,
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
output_json: Optional[BaseModel] = None,
|
||||
output_pydantic: Optional[BaseModel] = None,
|
||||
@@ -3126,7 +3258,9 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
# Add extra settings for provider-specific parameters (e.g., num_ctx for Ollama)
|
||||
if self.extra_settings:
|
||||
params.update(self.extra_settings)
|
||||
# Filter out internal parameters that shouldn't be passed to the API
|
||||
filtered_extra_settings = {k: v for k, v in self.extra_settings.items() if k != 'metrics'}
|
||||
params.update(filtered_extra_settings)
|
||||
|
||||
# Override with any provided parameters
|
||||
params.update(override_params)
|
||||
@@ -3169,6 +3303,79 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
logging.debug(f"Could not verify function calling support: {e}. Setting tool_choice anyway.")
|
||||
params['tool_choice'] = 'auto'
|
||||
|
||||
# Add web_search_options if web_search is enabled and model supports it
|
||||
if self.web_search and self._supports_web_search():
|
||||
if isinstance(self.web_search, dict):
|
||||
# Use custom web_search_options provided by user
|
||||
params['web_search_options'] = self.web_search
|
||||
else:
|
||||
# Use default web_search_options
|
||||
params['web_search_options'] = {"search_context_size": "medium"}
|
||||
logging.debug(f"Web search enabled with options: {params['web_search_options']}")
|
||||
|
||||
# Some web search models (like gpt-4o-search-preview) don't support certain parameters
|
||||
# Remove incompatible parameters to avoid BadRequestError
|
||||
incompatible_params = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'logit_bias']
|
||||
for param in incompatible_params:
|
||||
if param in params:
|
||||
del params[param]
|
||||
logging.debug(f"Removed incompatible parameter '{param}' for web search model")
|
||||
|
||||
# Add web_fetch tool if web_fetch is enabled and model supports it
|
||||
# Web fetch is passed as a tool (unlike web_search which uses web_search_options)
|
||||
if self.web_fetch and self._supports_web_fetch():
|
||||
# Build the web_fetch tool definition
|
||||
web_fetch_tool = {
|
||||
"type": "web_fetch_20250910",
|
||||
"name": "web_fetch",
|
||||
}
|
||||
|
||||
# Add optional parameters from web_fetch dict if provided
|
||||
if isinstance(self.web_fetch, dict):
|
||||
if 'max_uses' in self.web_fetch:
|
||||
web_fetch_tool['max_uses'] = self.web_fetch['max_uses']
|
||||
if 'allowed_domains' in self.web_fetch:
|
||||
web_fetch_tool['allowed_domains'] = self.web_fetch['allowed_domains']
|
||||
if 'blocked_domains' in self.web_fetch:
|
||||
web_fetch_tool['blocked_domains'] = self.web_fetch['blocked_domains']
|
||||
if 'citations' in self.web_fetch:
|
||||
web_fetch_tool['citations'] = self.web_fetch['citations']
|
||||
if 'max_content_tokens' in self.web_fetch:
|
||||
web_fetch_tool['max_content_tokens'] = self.web_fetch['max_content_tokens']
|
||||
else:
|
||||
# Default max_uses if web_fetch=True
|
||||
web_fetch_tool['max_uses'] = 5
|
||||
|
||||
# Add web_fetch tool to existing tools or create tools list
|
||||
if 'tools' in params and params['tools']:
|
||||
params['tools'].append(web_fetch_tool)
|
||||
else:
|
||||
params['tools'] = [web_fetch_tool]
|
||||
|
||||
logging.debug(f"Web fetch enabled with tool: {web_fetch_tool}")
|
||||
|
||||
# Add Claude Memory Tool if enabled and model supports it
|
||||
if self._supports_claude_memory():
|
||||
memory_tool = self._get_claude_memory_tool()
|
||||
if memory_tool:
|
||||
# Add the memory tool definition
|
||||
memory_tool_def = memory_tool.get_tool_definition()
|
||||
|
||||
# Add memory tool to existing tools or create tools list
|
||||
if 'tools' in params and params['tools']:
|
||||
params['tools'].append(memory_tool_def)
|
||||
else:
|
||||
params['tools'] = [memory_tool_def]
|
||||
|
||||
# Add the beta header for Anthropic
|
||||
beta_header = memory_tool.get_beta_header()
|
||||
if 'extra_headers' in params:
|
||||
params['extra_headers']['anthropic-beta'] = beta_header
|
||||
else:
|
||||
params['extra_headers'] = {'anthropic-beta': beta_header}
|
||||
|
||||
logging.debug(f"Claude memory tool enabled with beta header: {beta_header}")
|
||||
|
||||
return params
|
||||
|
||||
def _prepare_response_logging(self, temperature: float, stream: bool, verbose: bool, markdown: bool, **kwargs) -> Optional[Dict[str, Any]]:
|
||||
@@ -3260,7 +3467,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
self,
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: float = 0.2,
|
||||
temperature: float = 1.0,
|
||||
stream: bool = True,
|
||||
verbose: bool = True,
|
||||
markdown: bool = True,
|
||||
@@ -3355,7 +3562,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
self,
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: float = 0.2,
|
||||
temperature: float = 1.0,
|
||||
stream: bool = True,
|
||||
verbose: bool = True,
|
||||
markdown: bool = True,
|
||||
|
||||
@@ -1,62 +1,30 @@
|
||||
"""
|
||||
Model capabilities configuration for different LLM providers.
|
||||
This module defines which models support specific features like structured outputs.
|
||||
|
||||
This module uses LiteLLM's helper functions as the primary source for model capability detection.
|
||||
LiteLLM is maintained by many contributors and is more accurate and up-to-date.
|
||||
|
||||
LiteLLM Helper Functions:
|
||||
- litellm.supports_web_search(model=) - Check web search support
|
||||
- litellm.supports_function_calling(model=) - Check function calling support
|
||||
- litellm.supports_parallel_function_calling(model=) - Check parallel function calling
|
||||
- litellm.supports_response_schema(model=) - Check JSON schema/structured outputs support
|
||||
- litellm.utils.supports_prompt_caching(model=) - Check prompt caching support
|
||||
- litellm.get_supported_openai_params(model=) - Get all supported params
|
||||
|
||||
Sources:
|
||||
- https://docs.litellm.ai/docs/completion/web_search
|
||||
- https://docs.litellm.ai/docs/completion/json_mode
|
||||
- https://docs.litellm.ai/docs/completion/function_call
|
||||
- https://docs.litellm.ai/docs/completion/prompt_caching
|
||||
"""
|
||||
|
||||
# Models that support OpenAI-style structured outputs (response_format with Pydantic models)
|
||||
MODELS_SUPPORTING_STRUCTURED_OUTPUTS = {
|
||||
# OpenAI models
|
||||
"gpt-4o",
|
||||
"gpt-5-nano",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106",
|
||||
"gpt-3.5-turbo-0125",
|
||||
|
||||
# New/Future OpenAI models (as mentioned by user)
|
||||
"codex-mini",
|
||||
"o3-pro",
|
||||
"gpt-4.5-preview",
|
||||
"o3-mini",
|
||||
"o1",
|
||||
"o1-preview",
|
||||
"o1-mini",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
"gpt-4.1-mini",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
|
||||
# Gemini models that support structured outputs
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-latest",
|
||||
"gemini-1.5-flash-8b",
|
||||
"gemini-1.5-flash-8b-latest",
|
||||
}
|
||||
|
||||
# Models that explicitly DON'T support structured outputs
|
||||
MODELS_NOT_SUPPORTING_STRUCTURED_OUTPUTS = {
|
||||
# Audio preview models
|
||||
"gpt-4o-audio-preview",
|
||||
"gpt-5-nano-audio-preview",
|
||||
|
||||
# Legacy o1 models (don't support system messages either)
|
||||
"o1-preview-2024-09-12",
|
||||
"o1-mini-2024-09-12",
|
||||
}
|
||||
|
||||
|
||||
def supports_structured_outputs(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports OpenAI-style structured outputs.
|
||||
Check if a model supports structured outputs (JSON schema).
|
||||
|
||||
Uses LiteLLM's supports_response_schema() as the primary check.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check
|
||||
@@ -67,34 +35,72 @@ def supports_structured_outputs(model_name: str) -> bool:
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
# Strip provider prefixes (e.g., 'google/', 'openai/', etc.)
|
||||
model_without_provider = model_name
|
||||
for prefix in ['google/', 'openai/', 'anthropic/', 'gemini/', 'mistral/', 'deepseek/', 'groq/']:
|
||||
if model_name.startswith(prefix):
|
||||
model_without_provider = model_name[len(prefix):]
|
||||
break
|
||||
try:
|
||||
import litellm
|
||||
# Use LiteLLM's built-in check - most accurate and up-to-date
|
||||
if hasattr(litellm, 'supports_response_schema'):
|
||||
return litellm.supports_response_schema(model=model_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# First check if it's explicitly in the NOT supporting list
|
||||
if model_without_provider in MODELS_NOT_SUPPORTING_STRUCTURED_OUTPUTS:
|
||||
return False
|
||||
|
||||
|
||||
def supports_function_calling(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports function calling.
|
||||
|
||||
Uses LiteLLM's supports_function_calling() as the primary check.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports function calling, False otherwise
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
# Then check if it's in the supporting list
|
||||
if model_without_provider in MODELS_SUPPORTING_STRUCTURED_OUTPUTS:
|
||||
return True
|
||||
try:
|
||||
import litellm
|
||||
# Use LiteLLM's built-in check - most accurate and up-to-date
|
||||
if hasattr(litellm, 'supports_function_calling'):
|
||||
return litellm.supports_function_calling(model=model_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# For models with version suffixes, check the base model name
|
||||
base_model = model_without_provider.split('-2024-')[0].split('-2025-')[0]
|
||||
if base_model in MODELS_SUPPORTING_STRUCTURED_OUTPUTS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def supports_parallel_function_calling(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports parallel function calling.
|
||||
|
||||
Uses LiteLLM's supports_parallel_function_calling() as the primary check.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports parallel function calling, False otherwise
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
try:
|
||||
import litellm
|
||||
# Use LiteLLM's built-in check - most accurate and up-to-date
|
||||
if hasattr(litellm, 'supports_parallel_function_calling'):
|
||||
return litellm.supports_parallel_function_calling(model=model_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Default to False for unknown models
|
||||
return False
|
||||
|
||||
|
||||
def supports_streaming_with_tools(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports streaming when tools are provided.
|
||||
Most models that support structured outputs also support streaming with tools.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check
|
||||
@@ -102,15 +108,153 @@ def supports_streaming_with_tools(model_name: str) -> bool:
|
||||
Returns:
|
||||
bool: True if the model supports streaming with tools, False otherwise
|
||||
"""
|
||||
# For now, use the same logic as structured outputs
|
||||
# In the future, this could be a separate list if needed
|
||||
return supports_structured_outputs(model_name)
|
||||
# Models that support function calling generally support streaming with tools
|
||||
return supports_function_calling(model_name)
|
||||
|
||||
|
||||
# Supported Gemini internal tools
|
||||
GEMINI_INTERNAL_TOOLS = {'googleSearch', 'urlContext', 'codeExecution'}
|
||||
|
||||
|
||||
def supports_web_search(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports native web search via LiteLLM.
|
||||
|
||||
Uses LiteLLM's supports_web_search() as the primary check.
|
||||
|
||||
Native web search allows the model to search the web in real-time
|
||||
without requiring external tools like DuckDuckGo.
|
||||
|
||||
Supported providers:
|
||||
- OpenAI (gpt-4o-search-preview, gpt-4o-mini-search-preview)
|
||||
- xAI (grok-3)
|
||||
- Anthropic (claude-3-5-sonnet-latest, claude-sonnet-4, etc.)
|
||||
- Google/Vertex AI (gemini-2.0-flash, gemini-2.5-*, etc.)
|
||||
- Perplexity (all models)
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check (with or without provider prefix)
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports native web search, False otherwise
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
try:
|
||||
import litellm
|
||||
# Use LiteLLM's built-in check - most accurate and up-to-date
|
||||
if hasattr(litellm, 'supports_web_search'):
|
||||
return litellm.supports_web_search(model=model_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def supports_prompt_caching(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports prompt caching via LiteLLM.
|
||||
|
||||
Uses LiteLLM's supports_prompt_caching() as the primary check.
|
||||
|
||||
Prompt caching allows caching parts of prompts to reduce costs and latency
|
||||
on subsequent requests with similar prompts.
|
||||
|
||||
Supported providers:
|
||||
- OpenAI (openai/) - Automatic caching for prompts ≥1024 tokens
|
||||
- Anthropic (anthropic/) - Manual caching with cache_control
|
||||
- Bedrock (bedrock/) - All models that support prompt caching
|
||||
- Deepseek (deepseek/) - Works like OpenAI
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check (with or without provider prefix)
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports prompt caching, False otherwise
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
try:
|
||||
from litellm.utils import supports_prompt_caching as litellm_supports_prompt_caching
|
||||
return litellm_supports_prompt_caching(model=model_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Models that support web fetch via LiteLLM (Anthropic only)
|
||||
# Web fetch retrieves full content from specific URLs (web pages and PDFs)
|
||||
# Source: https://docs.litellm.ai/docs/completion/web_fetch
|
||||
# Note: LiteLLM doesn't have a supports_web_fetch() helper yet, so we maintain a static list
|
||||
MODELS_SUPPORTING_WEB_FETCH = {
|
||||
# Anthropic Claude models with web fetch support
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-sonnet-4",
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-3-7-sonnet-latest",
|
||||
"claude-3-5-sonnet-latest",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-latest",
|
||||
"claude-3-5-haiku-20241022",
|
||||
}
|
||||
|
||||
|
||||
def supports_web_fetch(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a model supports web fetch via LiteLLM.
|
||||
|
||||
Web fetch allows the model to retrieve full content from specific URLs
|
||||
(web pages and PDF documents). Currently only supported by Anthropic Claude models.
|
||||
|
||||
Note: LiteLLM doesn't have a supports_web_fetch() helper yet, so we use a static list
|
||||
with auto-detection for Claude 4+ models.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model to check (with or without provider prefix)
|
||||
|
||||
Returns:
|
||||
bool: True if the model supports web fetch, False otherwise
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
# Strip provider prefixes
|
||||
model_without_provider = model_name
|
||||
for prefix in ['anthropic/', 'bedrock/', 'vertex_ai/']:
|
||||
if model_name.startswith(prefix):
|
||||
model_without_provider = model_name[len(prefix):]
|
||||
break
|
||||
|
||||
# Check our static list
|
||||
if model_without_provider in MODELS_SUPPORTING_WEB_FETCH:
|
||||
return True
|
||||
|
||||
# Check base model name (without version suffix)
|
||||
base_model = model_without_provider.split('-2024-')[0].split('-2025-')[0]
|
||||
if base_model in MODELS_SUPPORTING_WEB_FETCH:
|
||||
return True
|
||||
|
||||
# Auto-support for Claude 4+ models (Anthropic only)
|
||||
model_lower = model_without_provider.lower()
|
||||
if 'claude' in model_lower:
|
||||
import re
|
||||
# Match patterns like claude-4, claude-5, claude-sonnet-4, claude-opus-4, etc.
|
||||
version_match = re.search(r'claude-(?:sonnet-|opus-|haiku-)?(\d+)', model_lower)
|
||||
if version_match:
|
||||
version = int(version_match.group(1))
|
||||
if version >= 4: # Claude 4 and later
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_gemini_internal_tool(tool) -> bool:
|
||||
"""
|
||||
Check if a tool is a Gemini internal tool and should be included in formatted tools.
|
||||
|
||||
@@ -80,7 +80,7 @@ class ModelRouter:
|
||||
|
||||
# Mid-tier models for moderate complexity
|
||||
ModelProfile(
|
||||
name="gpt-4o",
|
||||
name="gpt-5-nano",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
|
||||
cost_per_1k_tokens=0.0075, # Average of $0.0025 input, $0.01 output
|
||||
@@ -145,7 +145,7 @@ class ModelRouter:
|
||||
preferred_providers: List of preferred providers in order
|
||||
"""
|
||||
self.models = models or self.DEFAULT_MODELS
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.cost_threshold = cost_threshold
|
||||
self.preferred_providers = preferred_providers or []
|
||||
|
||||
|
||||
@@ -740,7 +740,7 @@ class OpenAIClient:
|
||||
def create_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -785,7 +785,7 @@ class OpenAIClient:
|
||||
async def acreate_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -830,7 +830,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1008,7 +1008,7 @@ class OpenAIClient:
|
||||
async def achat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1189,7 +1189,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools_stream(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1334,7 +1334,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
@@ -1368,7 +1368,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-4o",
|
||||
model: str = "gpt-5-nano",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
|
||||
@@ -8,8 +8,83 @@ This module provides memory management capabilities including:
|
||||
- User memory for preferences/history
|
||||
- Quality-based storage decisions
|
||||
- Graph memory support via Mem0
|
||||
- Session save/resume (like Gemini CLI)
|
||||
- Context compression (like Gemini CLI)
|
||||
- Checkpointing (like Gemini CLI)
|
||||
- Rules management (like Cursor/Windsurf)
|
||||
- Auto-generated memories (like Windsurf Cascade)
|
||||
- Workflows (like Windsurf)
|
||||
- Hooks (like Windsurf Cascade Hooks)
|
||||
|
||||
Memory Providers:
|
||||
- FileMemory: Zero-dependency JSON file-based storage (default)
|
||||
- Memory: Full-featured with SQLite, ChromaDB, Mem0, MongoDB support
|
||||
- RulesManager: Rules/instructions file management
|
||||
- AutoMemory: Automatic memory extraction from conversations
|
||||
- WorkflowManager: Multi-step workflow execution
|
||||
- HooksManager: Pre/post operation hooks
|
||||
"""
|
||||
|
||||
from .memory import Memory
|
||||
from .file_memory import FileMemory, create_memory
|
||||
from .rules_manager import RulesManager, Rule, create_rules_manager
|
||||
|
||||
__all__ = ["Memory"]
|
||||
# Lazy imports for optional modules to avoid dependency issues and improve startup time
|
||||
def __getattr__(name):
|
||||
if name == "Memory":
|
||||
from .memory import Memory
|
||||
return Memory
|
||||
if name == "AutoMemory":
|
||||
from .auto_memory import AutoMemory
|
||||
return AutoMemory
|
||||
if name == "AutoMemoryExtractor":
|
||||
from .auto_memory import AutoMemoryExtractor
|
||||
return AutoMemoryExtractor
|
||||
if name == "create_auto_memory":
|
||||
from .auto_memory import create_auto_memory
|
||||
return create_auto_memory
|
||||
if name == "WorkflowManager":
|
||||
from .workflows import WorkflowManager
|
||||
return WorkflowManager
|
||||
if name == "Workflow":
|
||||
from .workflows import Workflow
|
||||
return Workflow
|
||||
if name == "WorkflowStep":
|
||||
from .workflows import WorkflowStep
|
||||
return WorkflowStep
|
||||
if name == "create_workflow_manager":
|
||||
from .workflows import create_workflow_manager
|
||||
return create_workflow_manager
|
||||
if name == "HooksManager":
|
||||
from .hooks import HooksManager
|
||||
return HooksManager
|
||||
if name == "HookResult":
|
||||
from .hooks import HookResult
|
||||
return HookResult
|
||||
if name == "create_hooks_manager":
|
||||
from .hooks import create_hooks_manager
|
||||
return create_hooks_manager
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
__all__ = [
|
||||
# Core memory
|
||||
"FileMemory",
|
||||
"Memory",
|
||||
"create_memory",
|
||||
# Rules management
|
||||
"RulesManager",
|
||||
"Rule",
|
||||
"create_rules_manager",
|
||||
# Auto memory
|
||||
"AutoMemory",
|
||||
"AutoMemoryExtractor",
|
||||
"create_auto_memory",
|
||||
# Workflows
|
||||
"WorkflowManager",
|
||||
"Workflow",
|
||||
"WorkflowStep",
|
||||
"create_workflow_manager",
|
||||
# Hooks
|
||||
"HooksManager",
|
||||
"HookResult",
|
||||
"create_hooks_manager",
|
||||
]
|
||||
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Auto-Generated Memory System for PraisonAI Agents.
|
||||
|
||||
Provides automatic memory generation similar to Windsurf's Cascade,
|
||||
where the AI automatically decides what context is worth remembering.
|
||||
|
||||
Features:
|
||||
- Automatic extraction of important facts from conversations
|
||||
- Entity recognition and storage
|
||||
- Preference detection
|
||||
- Lazy evaluation to avoid performance impact
|
||||
- Configurable importance thresholds
|
||||
|
||||
This module is designed to be lightweight and only activates
|
||||
when explicitly enabled to avoid impacting performance.
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Callable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .file_memory import FileMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutoMemoryExtractor:
|
||||
"""
|
||||
Automatically extracts memorable content from conversations.
|
||||
|
||||
Uses pattern matching for fast extraction without LLM calls,
|
||||
with optional LLM enhancement for better accuracy.
|
||||
|
||||
Example:
|
||||
```python
|
||||
extractor = AutoMemoryExtractor()
|
||||
|
||||
# Extract from conversation
|
||||
memories = extractor.extract(
|
||||
"My name is John and I prefer Python for backend work."
|
||||
)
|
||||
# Returns: [
|
||||
# {"type": "entity", "name": "John", "entity_type": "person"},
|
||||
# {"type": "preference", "content": "prefers Python for backend work"}
|
||||
# ]
|
||||
```
|
||||
"""
|
||||
|
||||
# Patterns for fast extraction (no LLM needed)
|
||||
PATTERNS = {
|
||||
# Name patterns
|
||||
"name": [
|
||||
r"(?:my name is|i'm|i am|call me)\s+([A-Z][a-z]+)",
|
||||
r"(?:name|Name):\s*([A-Z][a-z]+)",
|
||||
],
|
||||
# Preference patterns
|
||||
"preference": [
|
||||
r"i (?:prefer|like|love|enjoy|use)\s+(.+?)(?:\.|$)",
|
||||
r"(?:my favorite|i always use)\s+(.+?)(?:\.|$)",
|
||||
],
|
||||
# Role/job patterns
|
||||
"role": [
|
||||
r"i (?:am|work as|'m)\s+(?:a|an)\s+([a-z\s]+?)(?:\.|,|$)",
|
||||
r"(?:my job|my role|i do)\s+(?:is|as)?\s*(.+?)(?:\.|$)",
|
||||
],
|
||||
# Location patterns
|
||||
"location": [
|
||||
r"i (?:live|work|am based|am located)\s+(?:in|at)\s+([A-Z][a-z\s]+)",
|
||||
r"(?:from|based in)\s+([A-Z][a-z\s,]+)",
|
||||
],
|
||||
# Project/task patterns
|
||||
"project": [
|
||||
r"(?:working on|building|developing|creating)\s+(?:a|an|the)?\s*(.+?)(?:\.|$)",
|
||||
r"(?:my project|the project)\s+(?:is|called)?\s*(.+?)(?:\.|$)",
|
||||
],
|
||||
# Technology preferences
|
||||
"technology": [
|
||||
r"(?:using|use|prefer)\s+(Python|JavaScript|TypeScript|Go|Rust|Java|C\+\+|Ruby|PHP)",
|
||||
r"(?:framework|library|tool)(?:s)?\s+(?:like|such as|including)\s+(.+?)(?:\.|$)",
|
||||
],
|
||||
}
|
||||
|
||||
# Importance scores by type
|
||||
IMPORTANCE_SCORES = {
|
||||
"name": 0.95,
|
||||
"preference": 0.7,
|
||||
"role": 0.85,
|
||||
"location": 0.6,
|
||||
"project": 0.75,
|
||||
"technology": 0.7,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_importance: float = 0.5,
|
||||
use_llm: bool = False,
|
||||
llm_func: Optional[Callable[[str], str]] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize AutoMemoryExtractor.
|
||||
|
||||
Args:
|
||||
min_importance: Minimum importance score to extract (0.0-1.0)
|
||||
use_llm: Whether to use LLM for enhanced extraction
|
||||
llm_func: LLM function for extraction (if use_llm=True)
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.min_importance = min_importance
|
||||
self.use_llm = use_llm
|
||||
self.llm_func = llm_func
|
||||
self.verbose = verbose
|
||||
|
||||
# Compile patterns for performance
|
||||
self._compiled_patterns = {}
|
||||
for category, patterns in self.PATTERNS.items():
|
||||
self._compiled_patterns[category] = [
|
||||
re.compile(p, re.IGNORECASE) for p in patterns
|
||||
]
|
||||
|
||||
def extract(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract memorable content from text.
|
||||
|
||||
Args:
|
||||
text: Text to extract from (conversation, message, etc.)
|
||||
|
||||
Returns:
|
||||
List of extracted memories with type, content, and importance
|
||||
"""
|
||||
memories = []
|
||||
|
||||
# Fast pattern-based extraction
|
||||
for category, patterns in self._compiled_patterns.items():
|
||||
importance = self.IMPORTANCE_SCORES.get(category, 0.5)
|
||||
|
||||
if importance < self.min_importance:
|
||||
continue
|
||||
|
||||
for pattern in patterns:
|
||||
matches = pattern.findall(text)
|
||||
for match in matches:
|
||||
if isinstance(match, tuple):
|
||||
match = match[0]
|
||||
|
||||
match = match.strip()
|
||||
if len(match) < 2 or len(match) > 200:
|
||||
continue
|
||||
|
||||
memory = {
|
||||
"type": category,
|
||||
"content": match,
|
||||
"importance": importance,
|
||||
"source": "pattern"
|
||||
}
|
||||
|
||||
# Avoid duplicates
|
||||
if not any(m["content"].lower() == match.lower() for m in memories):
|
||||
memories.append(memory)
|
||||
|
||||
# Optional LLM enhancement
|
||||
if self.use_llm and self.llm_func and len(text) > 50:
|
||||
llm_memories = self._extract_with_llm(text)
|
||||
for mem in llm_memories:
|
||||
if mem.get("importance", 0) >= self.min_importance:
|
||||
if not any(m["content"].lower() == mem["content"].lower() for m in memories):
|
||||
memories.append(mem)
|
||||
|
||||
return memories
|
||||
|
||||
def _extract_with_llm(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""Extract memories using LLM for better accuracy."""
|
||||
if not self.llm_func:
|
||||
return []
|
||||
|
||||
prompt = f"""Extract important facts from this text that should be remembered.
|
||||
Return a JSON array of objects with: type (name/preference/role/location/project/technology), content, importance (0.0-1.0).
|
||||
Only extract clear, specific facts. Be concise.
|
||||
|
||||
Text: {text[:1500]}
|
||||
|
||||
JSON array (no markdown, just the array):"""
|
||||
|
||||
try:
|
||||
response = self.llm_func(prompt)
|
||||
|
||||
# Parse JSON from response
|
||||
import json
|
||||
# Find JSON array in response
|
||||
start = response.find("[")
|
||||
end = response.rfind("]") + 1
|
||||
if start >= 0 and end > start:
|
||||
return json.loads(response[start:end])
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
logger.warning(f"LLM extraction failed: {e}")
|
||||
|
||||
return []
|
||||
|
||||
def should_remember(self, text: str) -> bool:
|
||||
"""
|
||||
Quick check if text likely contains memorable content.
|
||||
|
||||
Useful for filtering before full extraction.
|
||||
|
||||
Args:
|
||||
text: Text to check
|
||||
|
||||
Returns:
|
||||
True if text likely contains memorable content
|
||||
"""
|
||||
# Quick keyword check
|
||||
keywords = [
|
||||
"my name", "i am", "i'm", "i prefer", "i like", "i use",
|
||||
"i work", "i live", "working on", "building", "my project",
|
||||
"favorite", "always use"
|
||||
]
|
||||
|
||||
text_lower = text.lower()
|
||||
return any(kw in text_lower for kw in keywords)
|
||||
|
||||
|
||||
class AutoMemory:
|
||||
"""
|
||||
Wrapper that adds auto-generation capabilities to FileMemory.
|
||||
|
||||
Intercepts interactions and automatically extracts/stores memories.
|
||||
Designed to be lightweight - only processes when enabled.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from praisonaiagents.memory import FileMemory
|
||||
|
||||
base_memory = FileMemory(user_id="user123")
|
||||
auto_memory = AutoMemory(base_memory, enabled=True)
|
||||
|
||||
# Process conversation and auto-store memories
|
||||
auto_memory.process_interaction(
|
||||
user_message="My name is John and I prefer Python",
|
||||
assistant_response="Nice to meet you, John!"
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
memory: "FileMemory",
|
||||
enabled: bool = True,
|
||||
extractor: Optional[AutoMemoryExtractor] = None,
|
||||
min_importance: float = 0.6,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize AutoMemory wrapper.
|
||||
|
||||
Args:
|
||||
memory: Base FileMemory instance
|
||||
enabled: Whether auto-generation is enabled
|
||||
extractor: Custom extractor (or use default)
|
||||
min_importance: Minimum importance to auto-store
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.memory = memory
|
||||
self.enabled = enabled
|
||||
self.min_importance = min_importance
|
||||
self.verbose = verbose
|
||||
|
||||
self.extractor = extractor or AutoMemoryExtractor(
|
||||
min_importance=min_importance,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
# Track what we've already processed to avoid duplicates
|
||||
self._processed_hashes: set = set()
|
||||
|
||||
def process_interaction(
|
||||
self,
|
||||
user_message: str,
|
||||
assistant_response: Optional[str] = None,
|
||||
store: bool = True
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Process an interaction and extract memories.
|
||||
|
||||
Args:
|
||||
user_message: User's message
|
||||
assistant_response: Optional assistant response
|
||||
store: Whether to store extracted memories
|
||||
|
||||
Returns:
|
||||
List of extracted memories
|
||||
"""
|
||||
if not self.enabled:
|
||||
return []
|
||||
|
||||
# Combine messages for extraction
|
||||
text = user_message
|
||||
if assistant_response:
|
||||
text += "\n" + assistant_response
|
||||
|
||||
# Check if already processed
|
||||
import hashlib
|
||||
text_hash = hashlib.md5(text.encode()).hexdigest()[:16]
|
||||
if text_hash in self._processed_hashes:
|
||||
return []
|
||||
self._processed_hashes.add(text_hash)
|
||||
|
||||
# Quick filter
|
||||
if not self.extractor.should_remember(text):
|
||||
return []
|
||||
|
||||
# Extract memories
|
||||
memories = self.extractor.extract(text)
|
||||
|
||||
if store and memories:
|
||||
self._store_memories(memories)
|
||||
|
||||
return memories
|
||||
|
||||
def _store_memories(self, memories: List[Dict[str, Any]]):
|
||||
"""Store extracted memories in the base memory."""
|
||||
for mem in memories:
|
||||
mem_type = mem.get("type", "")
|
||||
content = mem.get("content", "")
|
||||
importance = mem.get("importance", 0.5)
|
||||
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Store based on type
|
||||
if mem_type == "name":
|
||||
# Store as entity
|
||||
self.memory.add_entity(
|
||||
name=content,
|
||||
entity_type="person",
|
||||
attributes={"relationship": "user"}
|
||||
)
|
||||
elif mem_type in ("role", "location"):
|
||||
# Store as entity attribute
|
||||
self.memory.add_long_term(
|
||||
f"User's {mem_type}: {content}",
|
||||
importance=importance
|
||||
)
|
||||
else:
|
||||
# Store as long-term memory
|
||||
self.memory.add_long_term(content, importance=importance)
|
||||
|
||||
if self.verbose:
|
||||
logger.info(f"Auto-stored memory: {mem_type} - {content[:50]}...")
|
||||
|
||||
def enable(self):
|
||||
"""Enable auto-generation."""
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
"""Disable auto-generation."""
|
||||
self.enabled = False
|
||||
|
||||
def set_llm(self, llm_func: Callable[[str], str]):
|
||||
"""Set LLM function for enhanced extraction."""
|
||||
self.extractor.use_llm = True
|
||||
self.extractor.llm_func = llm_func
|
||||
|
||||
|
||||
def create_auto_memory(
|
||||
memory: "FileMemory",
|
||||
enabled: bool = True,
|
||||
**kwargs
|
||||
) -> AutoMemory:
|
||||
"""
|
||||
Create an AutoMemory wrapper.
|
||||
|
||||
Args:
|
||||
memory: Base FileMemory instance
|
||||
enabled: Whether auto-generation is enabled
|
||||
**kwargs: Additional configuration
|
||||
|
||||
Returns:
|
||||
AutoMemory instance
|
||||
"""
|
||||
return AutoMemory(memory, enabled=enabled, **kwargs)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,417 @@
|
||||
"""
|
||||
Cascade Hooks System for PraisonAI Agents.
|
||||
|
||||
Provides event hooks similar to Windsurf's Cascade Hooks,
|
||||
allowing custom actions before/after agent operations.
|
||||
|
||||
Features:
|
||||
- Pre/post hooks for read, write, command, prompt operations
|
||||
- JSON configuration (.praison/hooks.json)
|
||||
- Script execution with timeout
|
||||
- Exit code handling
|
||||
- Lazy loading for performance
|
||||
|
||||
Configuration (.praison/hooks.json):
|
||||
{
|
||||
"hooks": {
|
||||
"pre_write_code": "./scripts/lint.sh",
|
||||
"post_write_code": "./scripts/format.sh",
|
||||
"pre_run_command": "./scripts/validate.sh",
|
||||
"post_user_prompt": "./scripts/log.sh"
|
||||
},
|
||||
"timeout": 30,
|
||||
"enabled": true
|
||||
}
|
||||
|
||||
Hook Events:
|
||||
- pre_read_code: Before reading a file
|
||||
- post_read_code: After reading a file
|
||||
- pre_write_code: Before writing to a file
|
||||
- post_write_code: After writing to a file
|
||||
- pre_run_command: Before running a terminal command
|
||||
- post_run_command: After running a terminal command
|
||||
- pre_user_prompt: Before processing user prompt
|
||||
- post_user_prompt: After processing user prompt
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Callable, Literal
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Hook event types
|
||||
HookEvent = Literal[
|
||||
"pre_read_code",
|
||||
"post_read_code",
|
||||
"pre_write_code",
|
||||
"post_write_code",
|
||||
"pre_run_command",
|
||||
"post_run_command",
|
||||
"pre_user_prompt",
|
||||
"post_user_prompt",
|
||||
"pre_mcp_tool_use",
|
||||
"post_mcp_tool_use"
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookResult:
|
||||
"""Result of a hook execution."""
|
||||
success: bool
|
||||
exit_code: int = 0
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
blocked: bool = False # If True, the operation should be blocked
|
||||
modified_input: Optional[str] = None # Modified input if applicable
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookConfig:
|
||||
"""Configuration for a single hook."""
|
||||
event: HookEvent
|
||||
command: str
|
||||
timeout: int = 30
|
||||
enabled: bool = True
|
||||
block_on_failure: bool = False
|
||||
pass_input: bool = True
|
||||
|
||||
|
||||
class HooksManager:
|
||||
"""
|
||||
Manages hook discovery, loading, and execution.
|
||||
|
||||
Hooks are configured in .praison/hooks.json and can execute
|
||||
scripts before/after agent operations.
|
||||
|
||||
Example:
|
||||
```python
|
||||
hooks = HooksManager(workspace_path="/path/to/project")
|
||||
|
||||
# Execute pre-write hook
|
||||
result = hooks.execute(
|
||||
"pre_write_code",
|
||||
context={"file_path": "main.py", "content": "..."}
|
||||
)
|
||||
|
||||
if result.blocked:
|
||||
print("Write blocked by hook")
|
||||
```
|
||||
"""
|
||||
|
||||
CONFIG_FILE = ".praison/hooks.json"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize HooksManager.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.verbose = verbose
|
||||
|
||||
self._hooks: Dict[HookEvent, List[HookConfig]] = {}
|
||||
self._global_timeout = 30
|
||||
self._enabled = True
|
||||
self._loaded = False
|
||||
|
||||
# Python callable hooks (for programmatic use)
|
||||
self._callable_hooks: Dict[HookEvent, List[Callable]] = {}
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _ensure_loaded(self):
|
||||
"""Lazy load hooks on first access."""
|
||||
if not self._loaded:
|
||||
self._load_config()
|
||||
self._loaded = True
|
||||
|
||||
def _load_config(self):
|
||||
"""Load hooks configuration from file."""
|
||||
config_path = self.workspace_path / self.CONFIG_FILE.replace("/", os.sep)
|
||||
|
||||
if not config_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
self._enabled = config.get("enabled", True)
|
||||
self._global_timeout = config.get("timeout", 30)
|
||||
|
||||
hooks_config = config.get("hooks", {})
|
||||
|
||||
for event, command in hooks_config.items():
|
||||
if event not in self._hooks:
|
||||
self._hooks[event] = []
|
||||
|
||||
if isinstance(command, str):
|
||||
# Simple command string
|
||||
self._hooks[event].append(HookConfig(
|
||||
event=event,
|
||||
command=command,
|
||||
timeout=self._global_timeout
|
||||
))
|
||||
elif isinstance(command, dict):
|
||||
# Detailed config
|
||||
self._hooks[event].append(HookConfig(
|
||||
event=event,
|
||||
command=command.get("command", ""),
|
||||
timeout=command.get("timeout", self._global_timeout),
|
||||
enabled=command.get("enabled", True),
|
||||
block_on_failure=command.get("block_on_failure", False),
|
||||
pass_input=command.get("pass_input", True)
|
||||
))
|
||||
elif isinstance(command, list):
|
||||
# Multiple commands for same event
|
||||
for cmd in command:
|
||||
if isinstance(cmd, str):
|
||||
self._hooks[event].append(HookConfig(
|
||||
event=event,
|
||||
command=cmd,
|
||||
timeout=self._global_timeout
|
||||
))
|
||||
|
||||
self._log(f"Loaded {sum(len(h) for h in self._hooks.values())} hooks")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error loading hooks config: {e}", logging.WARNING)
|
||||
|
||||
def reload(self):
|
||||
"""Reload hooks configuration."""
|
||||
self._hooks = {}
|
||||
self._loaded = False
|
||||
self._ensure_loaded()
|
||||
|
||||
def register(
|
||||
self,
|
||||
event: HookEvent,
|
||||
handler: Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
|
||||
):
|
||||
"""
|
||||
Register a Python callable as a hook.
|
||||
|
||||
Args:
|
||||
event: Hook event type
|
||||
handler: Callable that receives context dict and optionally returns modified context
|
||||
"""
|
||||
if event not in self._callable_hooks:
|
||||
self._callable_hooks[event] = []
|
||||
self._callable_hooks[event].append(handler)
|
||||
|
||||
def unregister(self, event: HookEvent, handler: Callable):
|
||||
"""Unregister a callable hook."""
|
||||
if event in self._callable_hooks:
|
||||
self._callable_hooks[event] = [
|
||||
h for h in self._callable_hooks[event] if h != handler
|
||||
]
|
||||
|
||||
def execute(
|
||||
self,
|
||||
event: HookEvent,
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
) -> HookResult:
|
||||
"""
|
||||
Execute hooks for an event.
|
||||
|
||||
Args:
|
||||
event: Hook event type
|
||||
context: Context data to pass to hooks
|
||||
|
||||
Returns:
|
||||
HookResult with execution status
|
||||
"""
|
||||
self._ensure_loaded()
|
||||
|
||||
if not self._enabled:
|
||||
return HookResult(success=True)
|
||||
|
||||
context = context or {}
|
||||
combined_result = HookResult(success=True)
|
||||
|
||||
# Execute callable hooks first
|
||||
if event in self._callable_hooks:
|
||||
for handler in self._callable_hooks[event]:
|
||||
try:
|
||||
result = handler(context)
|
||||
if isinstance(result, dict):
|
||||
context.update(result)
|
||||
except Exception as e:
|
||||
self._log(f"Callable hook error for {event}: {e}", logging.WARNING)
|
||||
combined_result.success = False
|
||||
|
||||
# Execute script hooks
|
||||
if event in self._hooks:
|
||||
for hook in self._hooks[event]:
|
||||
if not hook.enabled:
|
||||
continue
|
||||
|
||||
result = self._execute_script(hook, context)
|
||||
|
||||
if not result.success:
|
||||
combined_result.success = False
|
||||
combined_result.stderr += result.stderr
|
||||
|
||||
if hook.block_on_failure:
|
||||
combined_result.blocked = True
|
||||
break
|
||||
|
||||
combined_result.stdout += result.stdout
|
||||
|
||||
if result.modified_input:
|
||||
combined_result.modified_input = result.modified_input
|
||||
|
||||
return combined_result
|
||||
|
||||
def _execute_script(
|
||||
self,
|
||||
hook: HookConfig,
|
||||
context: Dict[str, Any]
|
||||
) -> HookResult:
|
||||
"""Execute a script hook."""
|
||||
try:
|
||||
# Prepare environment
|
||||
env = os.environ.copy()
|
||||
|
||||
# Pass context as environment variables
|
||||
if hook.pass_input:
|
||||
for key, value in context.items():
|
||||
if isinstance(value, (str, int, float, bool)):
|
||||
env[f"PRAISON_HOOK_{key.upper()}"] = str(value)
|
||||
|
||||
# Also pass as JSON
|
||||
env["PRAISON_HOOK_CONTEXT"] = json.dumps(context)
|
||||
|
||||
# Resolve command path
|
||||
command = hook.command
|
||||
if not os.path.isabs(command) and not command.startswith("./"):
|
||||
command = str(self.workspace_path / command)
|
||||
|
||||
# Execute
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=str(self.workspace_path),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=hook.timeout
|
||||
)
|
||||
|
||||
success = result.returncode == 0
|
||||
|
||||
# Check for special exit codes
|
||||
blocked = result.returncode == 1 and hook.block_on_failure
|
||||
|
||||
# Check stdout for modified input
|
||||
modified_input = None
|
||||
if result.stdout.startswith("MODIFIED:"):
|
||||
modified_input = result.stdout[9:].strip()
|
||||
|
||||
self._log(f"Hook {hook.event} executed: exit={result.returncode}")
|
||||
|
||||
return HookResult(
|
||||
success=success,
|
||||
exit_code=result.returncode,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
blocked=blocked,
|
||||
modified_input=modified_input
|
||||
)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self._log(f"Hook {hook.event} timed out after {hook.timeout}s", logging.WARNING)
|
||||
return HookResult(
|
||||
success=False,
|
||||
exit_code=-1,
|
||||
stderr=f"Timeout after {hook.timeout}s"
|
||||
)
|
||||
except Exception as e:
|
||||
self._log(f"Hook {hook.event} error: {e}", logging.WARNING)
|
||||
return HookResult(
|
||||
success=False,
|
||||
exit_code=-1,
|
||||
stderr=str(e)
|
||||
)
|
||||
|
||||
def has_hooks(self, event: HookEvent) -> bool:
|
||||
"""Check if any hooks are registered for an event."""
|
||||
self._ensure_loaded()
|
||||
return (
|
||||
event in self._hooks and len(self._hooks[event]) > 0 or
|
||||
event in self._callable_hooks and len(self._callable_hooks[event]) > 0
|
||||
)
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get hooks statistics."""
|
||||
self._ensure_loaded()
|
||||
|
||||
script_hooks = sum(len(h) for h in self._hooks.values())
|
||||
callable_hooks = sum(len(h) for h in self._callable_hooks.values())
|
||||
|
||||
return {
|
||||
"enabled": self._enabled,
|
||||
"script_hooks": script_hooks,
|
||||
"callable_hooks": callable_hooks,
|
||||
"total_hooks": script_hooks + callable_hooks,
|
||||
"events": list(set(list(self._hooks.keys()) + list(self._callable_hooks.keys())))
|
||||
}
|
||||
|
||||
def create_config(
|
||||
self,
|
||||
hooks: Optional[Dict[str, str]] = None,
|
||||
timeout: int = 30,
|
||||
enabled: bool = True
|
||||
):
|
||||
"""
|
||||
Create a hooks configuration file.
|
||||
|
||||
Args:
|
||||
hooks: Dict mapping event names to commands
|
||||
timeout: Global timeout in seconds
|
||||
enabled: Whether hooks are enabled
|
||||
"""
|
||||
config_dir = self.workspace_path / ".praison"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config = {
|
||||
"enabled": enabled,
|
||||
"timeout": timeout,
|
||||
"hooks": hooks or {}
|
||||
}
|
||||
|
||||
config_path = config_dir / "hooks.json"
|
||||
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
||||
|
||||
self._log(f"Created hooks config at {config_path}")
|
||||
self.reload()
|
||||
|
||||
|
||||
def create_hooks_manager(
|
||||
workspace_path: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> HooksManager:
|
||||
"""
|
||||
Create a HooksManager instance.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace
|
||||
**kwargs: Additional configuration
|
||||
|
||||
Returns:
|
||||
HooksManager instance
|
||||
"""
|
||||
return HooksManager(workspace_path=workspace_path, **kwargs)
|
||||
@@ -109,7 +109,7 @@ class Memory:
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {"model": "gpt-4o", "api_key": "..."}
|
||||
"config": {"model": "gpt-5-nano", "api_key": "..."}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
"""
|
||||
Rules Manager for PraisonAI Agents.
|
||||
|
||||
Provides persistent rules/instructions support similar to:
|
||||
- Cursor (.cursor/rules/*.mdc)
|
||||
- Windsurf (.windsurf/rules/)
|
||||
- Claude Code (CLAUDE.md, .claude/rules/)
|
||||
- Codex CLI (AGENTS.md)
|
||||
- Gemini CLI (GEMINI.md)
|
||||
|
||||
Features:
|
||||
- Auto-discovery of root instruction files (CLAUDE.md, AGENTS.md, etc.)
|
||||
- Git root discovery for monorepo support
|
||||
- @Import syntax for including other files
|
||||
- Local override files (CLAUDE.local.md)
|
||||
- Multiple activation modes (always, glob, manual, ai_decision)
|
||||
- Character limits for context window management
|
||||
- Symlink support for shared rules
|
||||
|
||||
Storage Structure:
|
||||
.praison/rules/
|
||||
├── global.md # Always applied (global)
|
||||
├── python.md # Glob: **/*.py
|
||||
├── testing.md # Glob: **/*.test.*
|
||||
└── security.md # Activation: manual (@security)
|
||||
|
||||
Rule File Format (YAML frontmatter + Markdown):
|
||||
---
|
||||
description: Python coding guidelines
|
||||
globs: ["**/*.py", "**/*.pyx"]
|
||||
activation: always # always, glob, manual, ai_decision
|
||||
priority: 10
|
||||
---
|
||||
|
||||
# Python Guidelines
|
||||
- Use type hints
|
||||
- Follow PEP 8
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import fnmatch
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Literal, Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rule:
|
||||
"""A single rule with metadata and content."""
|
||||
name: str
|
||||
content: str
|
||||
description: str = ""
|
||||
globs: List[str] = field(default_factory=list)
|
||||
activation: Literal["always", "glob", "manual", "ai_decision"] = "always"
|
||||
priority: int = 0
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def matches_file(self, file_path: str) -> bool:
|
||||
"""Check if this rule matches a given file path."""
|
||||
if self.activation == "always":
|
||||
return True
|
||||
if self.activation == "manual":
|
||||
return False # Only activated via @mention
|
||||
if self.activation == "glob" and self.globs:
|
||||
for pattern in self.globs:
|
||||
if fnmatch.fnmatch(file_path, pattern):
|
||||
return True
|
||||
# Also try with ** expansion
|
||||
if "**" in pattern:
|
||||
# Convert ** to regex for recursive matching
|
||||
regex_pattern = pattern.replace("**", ".*").replace("*", "[^/]*")
|
||||
if re.match(regex_pattern, file_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": self.content,
|
||||
"description": self.description,
|
||||
"globs": self.globs,
|
||||
"activation": self.activation,
|
||||
"priority": self.priority,
|
||||
"file_path": self.file_path
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Rule':
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class RulesManager:
|
||||
"""
|
||||
Manages rules/instructions for AI agents.
|
||||
|
||||
Supports multiple rule sources:
|
||||
- Global rules (~/.praison/rules/)
|
||||
- Workspace rules (.praison/rules/)
|
||||
- Subdirectory rules (subdir/.praison/rules/)
|
||||
- Root instruction files (CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, .windsurfrules)
|
||||
|
||||
Example:
|
||||
```python
|
||||
rules = RulesManager(workspace_path="/path/to/project")
|
||||
|
||||
# Get all active rules
|
||||
active_rules = rules.get_active_rules()
|
||||
|
||||
# Get rules for specific file
|
||||
python_rules = rules.get_rules_for_file("src/main.py")
|
||||
|
||||
# Get manually invoked rule
|
||||
security_rule = rules.get_rule_by_name("security")
|
||||
|
||||
# Build context string for LLM
|
||||
context = rules.build_rules_context(file_path="src/main.py")
|
||||
```
|
||||
"""
|
||||
|
||||
RULES_DIR_NAME = ".praison/rules"
|
||||
SUPPORTED_EXTENSIONS = [".md", ".mdc", ".txt"]
|
||||
|
||||
# Root-level instruction files (like Claude, Codex, Gemini, Cursor, Windsurf)
|
||||
ROOT_INSTRUCTION_FILES = [
|
||||
"CLAUDE.md", # Claude Code memory file
|
||||
"CLAUDE.local.md", # Claude Code local overrides (gitignored)
|
||||
"AGENTS.md", # OpenAI Codex CLI instructions
|
||||
"GEMINI.md", # Gemini CLI memory file
|
||||
".cursorrules", # Cursor legacy rules (deprecated but supported)
|
||||
".windsurfrules", # Windsurf legacy rules
|
||||
"PRAISON.md", # PraisonAI native instructions
|
||||
"PRAISON.local.md", # PraisonAI local overrides (gitignored)
|
||||
]
|
||||
|
||||
# Additional rules directories to discover (like Claude, Windsurf)
|
||||
ADDITIONAL_RULES_DIRS = [
|
||||
".claude/rules", # Claude Code modular rules
|
||||
".windsurf/rules", # Windsurf rules
|
||||
".cursor/rules", # Cursor rules
|
||||
]
|
||||
|
||||
# Maximum characters per rule (like Windsurf's 12000 limit)
|
||||
MAX_RULE_CHARS = 12000
|
||||
|
||||
# Import pattern for @path/to/file syntax
|
||||
IMPORT_PATTERN = re.compile(r'(?<!`)@([\w./-]+)(?!`)')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
global_rules_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize RulesManager.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
global_rules_path: Path to global rules (default: ~/.praison/rules)
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.global_rules_path = Path(global_rules_path) if global_rules_path else Path.home() / ".praison" / "rules"
|
||||
self.verbose = verbose
|
||||
|
||||
self._rules: Dict[str, Rule] = {}
|
||||
self._load_all_rules()
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _find_git_root(self, start_path: Optional[Path] = None) -> Optional[Path]:
|
||||
"""
|
||||
Find the git repository root from a starting path.
|
||||
|
||||
Uses pure Python (no gitpython dependency) for performance.
|
||||
Falls back to subprocess if needed.
|
||||
|
||||
Args:
|
||||
start_path: Starting path (defaults to workspace_path)
|
||||
|
||||
Returns:
|
||||
Path to git root, or None if not in a git repo
|
||||
"""
|
||||
start = start_path or self.workspace_path
|
||||
|
||||
# Method 1: Walk up looking for .git directory (fastest, no subprocess)
|
||||
current = Path(start).resolve()
|
||||
while current != current.parent:
|
||||
git_dir = current / ".git"
|
||||
if git_dir.exists():
|
||||
return current
|
||||
current = current.parent
|
||||
|
||||
# Method 2: Fallback to git command (handles edge cases like worktrees)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
cwd=str(start),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2 # Prevent hanging
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _process_imports(self, content: str, base_path: Path, depth: int = 0) -> str:
|
||||
"""
|
||||
Process @import syntax in rule content.
|
||||
|
||||
Supports:
|
||||
- @path/to/file - Import relative to workspace
|
||||
- @~/path/to/file - Import from home directory
|
||||
- @./path/to/file - Import relative to current file
|
||||
|
||||
Args:
|
||||
content: Rule content with potential @imports
|
||||
base_path: Base path for relative imports
|
||||
depth: Current recursion depth (max 5 to prevent loops)
|
||||
|
||||
Returns:
|
||||
Content with imports resolved
|
||||
"""
|
||||
if depth > 5:
|
||||
self._log("Import depth exceeded (max 5), stopping", logging.WARNING)
|
||||
return content
|
||||
|
||||
def replace_import(match):
|
||||
import_path = match.group(1)
|
||||
|
||||
# Skip if it looks like a mention (@username) or package (@org/pkg)
|
||||
if "/" not in import_path and "." not in import_path:
|
||||
return match.group(0)
|
||||
|
||||
# Resolve path
|
||||
if import_path.startswith("~/"):
|
||||
file_path = Path.home() / import_path[2:]
|
||||
elif import_path.startswith("./"):
|
||||
file_path = base_path / import_path[2:]
|
||||
else:
|
||||
file_path = self.workspace_path / import_path
|
||||
|
||||
# Try with and without .md extension
|
||||
if not file_path.exists() and not file_path.suffix:
|
||||
file_path = file_path.with_suffix(".md")
|
||||
|
||||
if file_path.exists() and file_path.is_file():
|
||||
try:
|
||||
imported_content = file_path.read_text(encoding="utf-8")
|
||||
# Recursively process imports in imported content
|
||||
imported_content = self._process_imports(
|
||||
imported_content,
|
||||
file_path.parent,
|
||||
depth + 1
|
||||
)
|
||||
return imported_content
|
||||
except Exception as e:
|
||||
self._log(f"Failed to import {file_path}: {e}", logging.WARNING)
|
||||
return match.group(0)
|
||||
else:
|
||||
# Not a file import, leave as-is (might be @mention)
|
||||
return match.group(0)
|
||||
|
||||
return self.IMPORT_PATTERN.sub(replace_import, content)
|
||||
|
||||
def _truncate_rule_content(self, content: str) -> str:
|
||||
"""Truncate rule content to MAX_RULE_CHARS if needed."""
|
||||
if len(content) <= self.MAX_RULE_CHARS:
|
||||
return content
|
||||
|
||||
truncated = content[:self.MAX_RULE_CHARS - 50]
|
||||
return truncated + "\n\n... (truncated, exceeded 12000 char limit)"
|
||||
|
||||
def _parse_frontmatter(self, content: str) -> tuple[Dict[str, Any], str]:
|
||||
"""Parse YAML frontmatter from markdown content."""
|
||||
frontmatter = {}
|
||||
body = content
|
||||
|
||||
# Check for YAML frontmatter (--- ... ---)
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
yaml_content = parts[1].strip()
|
||||
body = parts[2].strip()
|
||||
|
||||
# Simple YAML parsing (avoid dependency)
|
||||
for line in yaml_content.split("\n"):
|
||||
line = line.strip()
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Handle lists
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
# Parse simple list: ["*.py", "*.pyx"]
|
||||
value = [v.strip().strip('"\'') for v in value[1:-1].split(",") if v.strip()]
|
||||
# Handle booleans
|
||||
elif value.lower() in ("true", "false"):
|
||||
value = value.lower() == "true"
|
||||
# Handle numbers
|
||||
elif value.isdigit():
|
||||
value = int(value)
|
||||
# Handle quoted strings
|
||||
elif value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
elif value.startswith("'") and value.endswith("'"):
|
||||
value = value[1:-1]
|
||||
|
||||
frontmatter[key] = value
|
||||
|
||||
return frontmatter, body
|
||||
|
||||
def _load_rule_file(self, file_path: Path) -> Optional[Rule]:
|
||||
"""Load a single rule file with @import processing and truncation."""
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
frontmatter, body = self._parse_frontmatter(content)
|
||||
|
||||
# Process @imports in the body
|
||||
body = self._process_imports(body, file_path.parent)
|
||||
|
||||
# Truncate if exceeds limit
|
||||
body = self._truncate_rule_content(body)
|
||||
|
||||
# Extract rule name from filename
|
||||
name = file_path.stem
|
||||
|
||||
# Build Rule object
|
||||
rule = Rule(
|
||||
name=name,
|
||||
content=body,
|
||||
description=frontmatter.get("description", ""),
|
||||
globs=frontmatter.get("globs", []),
|
||||
activation=frontmatter.get("activation", "always"),
|
||||
priority=frontmatter.get("priority", 0),
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._log(f"Loaded rule '{name}' from {file_path}")
|
||||
return rule
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error loading rule file {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
|
||||
def _discover_rules_in_dir(self, rules_dir: Path) -> List[Rule]:
|
||||
"""Discover all rule files in a directory."""
|
||||
rules = []
|
||||
|
||||
if not rules_dir.exists():
|
||||
return rules
|
||||
|
||||
for ext in self.SUPPORTED_EXTENSIONS:
|
||||
for file_path in rules_dir.glob(f"*{ext}"):
|
||||
rule = self._load_rule_file(file_path)
|
||||
if rule:
|
||||
rules.append(rule)
|
||||
|
||||
return rules
|
||||
|
||||
def _load_root_instruction_file(self, file_path: Path) -> Optional[Rule]:
|
||||
"""Load a root instruction file (CLAUDE.md, AGENTS.md, etc.) with @import processing."""
|
||||
try:
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
# Handle symlinks (like Claude Code supports)
|
||||
if file_path.is_symlink():
|
||||
file_path = file_path.resolve()
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
frontmatter, body = self._parse_frontmatter(content)
|
||||
|
||||
# Process @imports in the body
|
||||
body_content = body if body else content
|
||||
body_content = self._process_imports(body_content, file_path.parent)
|
||||
|
||||
# Truncate if exceeds limit
|
||||
body_content = self._truncate_rule_content(body_content)
|
||||
|
||||
# Use filename (without extension) as rule name
|
||||
name = file_path.stem.lower().replace(".", "_")
|
||||
|
||||
# Determine description based on file type
|
||||
descriptions = {
|
||||
"claude": "Claude Code memory instructions",
|
||||
"claude_local": "Claude Code local overrides",
|
||||
"agents": "OpenAI Codex CLI instructions",
|
||||
"gemini": "Gemini CLI memory instructions",
|
||||
"cursorrules": "Cursor IDE rules (legacy)",
|
||||
"windsurfrules": "Windsurf IDE rules (legacy)",
|
||||
"praison": "PraisonAI native instructions",
|
||||
"praison_local": "PraisonAI local overrides",
|
||||
}
|
||||
description = descriptions.get(name, f"Instructions from {file_path.name}")
|
||||
|
||||
# Local files get higher priority (override main files)
|
||||
base_priority = 600 if "_local" in name else 500
|
||||
|
||||
rule = Rule(
|
||||
name=name,
|
||||
content=body_content,
|
||||
description=frontmatter.get("description", description),
|
||||
globs=frontmatter.get("globs", []),
|
||||
activation=frontmatter.get("activation", "always"),
|
||||
priority=frontmatter.get("priority", base_priority),
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._log(f"Loaded root instruction file '{file_path.name}'")
|
||||
return rule
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error loading root instruction file {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
|
||||
def _load_all_rules(self):
|
||||
"""Load rules from all sources including git root and additional directories."""
|
||||
self._rules = {}
|
||||
|
||||
# Find git root for monorepo support
|
||||
git_root = self._find_git_root()
|
||||
|
||||
# 1. Load global rules (lowest priority)
|
||||
global_rules = self._discover_rules_in_dir(self.global_rules_path)
|
||||
for rule in global_rules:
|
||||
rule.priority = rule.priority - 1000 # Lower priority for global
|
||||
self._rules[f"global:{rule.name}"] = rule
|
||||
|
||||
# 2. Load root instruction files from git root (if different from workspace)
|
||||
if git_root and git_root != self.workspace_path:
|
||||
for filename in self.ROOT_INSTRUCTION_FILES:
|
||||
file_path = git_root / filename
|
||||
rule = self._load_root_instruction_file(file_path)
|
||||
if rule:
|
||||
rule.priority = rule.priority - 100 # Slightly lower than workspace
|
||||
self._rules[f"gitroot:{rule.name}"] = rule
|
||||
|
||||
# 3. Load root instruction files from workspace (CLAUDE.md, AGENTS.md, etc.)
|
||||
for filename in self.ROOT_INSTRUCTION_FILES:
|
||||
file_path = self.workspace_path / filename
|
||||
rule = self._load_root_instruction_file(file_path)
|
||||
if rule:
|
||||
self._rules[f"root:{rule.name}"] = rule
|
||||
|
||||
# 4. Load workspace rules from .praison/rules/
|
||||
workspace_rules_dir = self.workspace_path / self.RULES_DIR_NAME.replace("/", os.sep)
|
||||
workspace_rules = self._discover_rules_in_dir(workspace_rules_dir)
|
||||
for rule in workspace_rules:
|
||||
self._rules[f"workspace:{rule.name}"] = rule
|
||||
|
||||
# 5. Load additional rules directories (.claude/rules, .windsurf/rules, .cursor/rules)
|
||||
for rules_dir_name in self.ADDITIONAL_RULES_DIRS:
|
||||
# Check workspace
|
||||
rules_dir = self.workspace_path / rules_dir_name.replace("/", os.sep)
|
||||
additional_rules = self._discover_rules_in_dir(rules_dir)
|
||||
for rule in additional_rules:
|
||||
rule.priority = rule.priority + 50 # Slightly higher priority
|
||||
dir_prefix = rules_dir_name.split("/")[0].strip(".")
|
||||
self._rules[f"{dir_prefix}:{rule.name}"] = rule
|
||||
|
||||
# Check git root if different
|
||||
if git_root and git_root != self.workspace_path:
|
||||
rules_dir = git_root / rules_dir_name.replace("/", os.sep)
|
||||
additional_rules = self._discover_rules_in_dir(rules_dir)
|
||||
for rule in additional_rules:
|
||||
dir_prefix = rules_dir_name.split("/")[0].strip(".")
|
||||
self._rules[f"gitroot_{dir_prefix}:{rule.name}"] = rule
|
||||
|
||||
# 6. Load subdirectory rules (walk up from cwd to workspace/git root)
|
||||
stop_at = git_root if git_root else self.workspace_path
|
||||
current = Path.cwd()
|
||||
while current != stop_at and current != current.parent:
|
||||
# Check .praison/rules/
|
||||
subdir_rules_dir = current / self.RULES_DIR_NAME.replace("/", os.sep)
|
||||
subdir_rules = self._discover_rules_in_dir(subdir_rules_dir)
|
||||
for rule in subdir_rules:
|
||||
rule.priority = rule.priority + 100 # Higher priority for closer dirs
|
||||
self._rules[f"subdir:{rule.name}"] = rule
|
||||
|
||||
# Check additional rules directories in subdirs
|
||||
for rules_dir_name in self.ADDITIONAL_RULES_DIRS:
|
||||
subdir_rules_dir = current / rules_dir_name.replace("/", os.sep)
|
||||
subdir_rules = self._discover_rules_in_dir(subdir_rules_dir)
|
||||
for rule in subdir_rules:
|
||||
rule.priority = rule.priority + 150 # Even higher for subdir additional
|
||||
dir_prefix = rules_dir_name.split("/")[0].strip(".")
|
||||
self._rules[f"subdir_{dir_prefix}:{rule.name}"] = rule
|
||||
|
||||
current = current.parent
|
||||
|
||||
self._log(f"Loaded {len(self._rules)} rules total")
|
||||
|
||||
def reload(self):
|
||||
"""Reload all rules from disk."""
|
||||
self._load_all_rules()
|
||||
|
||||
def get_all_rules(self) -> List[Rule]:
|
||||
"""Get all loaded rules, sorted by priority."""
|
||||
rules = list(self._rules.values())
|
||||
rules.sort(key=lambda r: r.priority, reverse=True)
|
||||
return rules
|
||||
|
||||
def get_active_rules(self, file_path: Optional[str] = None) -> List[Rule]:
|
||||
"""
|
||||
Get rules that are currently active.
|
||||
|
||||
Args:
|
||||
file_path: Optional file path to filter glob-based rules
|
||||
|
||||
Returns:
|
||||
List of active rules sorted by priority
|
||||
"""
|
||||
active = []
|
||||
|
||||
for rule in self._rules.values():
|
||||
if rule.activation == "always":
|
||||
active.append(rule)
|
||||
elif rule.activation == "glob" and file_path:
|
||||
if rule.matches_file(file_path):
|
||||
active.append(rule)
|
||||
elif rule.activation == "ai_decision":
|
||||
# AI will decide - include for context
|
||||
active.append(rule)
|
||||
# Skip "manual" - only activated via @mention
|
||||
|
||||
active.sort(key=lambda r: r.priority, reverse=True)
|
||||
return active
|
||||
|
||||
def get_rules_for_file(self, file_path: str) -> List[Rule]:
|
||||
"""Get rules that apply to a specific file."""
|
||||
return self.get_active_rules(file_path=file_path)
|
||||
|
||||
def get_rule_by_name(self, name: str) -> Optional[Rule]:
|
||||
"""Get a rule by name (for manual @mention invocation)."""
|
||||
# Check all scopes
|
||||
for scope in ["subdir", "workspace", "global"]:
|
||||
key = f"{scope}:{name}"
|
||||
if key in self._rules:
|
||||
return self._rules[key]
|
||||
|
||||
# Also check without scope prefix
|
||||
for rule in self._rules.values():
|
||||
if rule.name == name:
|
||||
return rule
|
||||
|
||||
return None
|
||||
|
||||
def get_manual_rules(self) -> List[Rule]:
|
||||
"""Get all rules that require manual activation."""
|
||||
return [r for r in self._rules.values() if r.activation == "manual"]
|
||||
|
||||
def build_rules_context(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
include_manual: Optional[List[str]] = None,
|
||||
max_chars: int = 10000
|
||||
) -> str:
|
||||
"""
|
||||
Build context string from active rules for LLM.
|
||||
|
||||
Args:
|
||||
file_path: Optional file path for glob matching
|
||||
include_manual: List of manual rule names to include
|
||||
max_chars: Maximum characters in output
|
||||
|
||||
Returns:
|
||||
Formatted rules context string
|
||||
"""
|
||||
parts = []
|
||||
total_chars = 0
|
||||
|
||||
# Get active rules
|
||||
active_rules = self.get_active_rules(file_path=file_path)
|
||||
|
||||
# Add manually invoked rules
|
||||
if include_manual:
|
||||
for name in include_manual:
|
||||
rule = self.get_rule_by_name(name)
|
||||
if rule and rule not in active_rules:
|
||||
active_rules.append(rule)
|
||||
|
||||
# Sort by priority
|
||||
active_rules.sort(key=lambda r: r.priority, reverse=True)
|
||||
|
||||
for rule in active_rules:
|
||||
if total_chars >= max_chars:
|
||||
break
|
||||
|
||||
rule_text = rule.content.strip()
|
||||
if not rule_text:
|
||||
continue
|
||||
|
||||
# Add rule with header
|
||||
if rule.description:
|
||||
header = f"## {rule.name}: {rule.description}"
|
||||
else:
|
||||
header = f"## {rule.name}"
|
||||
|
||||
section = f"{header}\n{rule_text}\n"
|
||||
|
||||
if total_chars + len(section) <= max_chars:
|
||||
parts.append(section)
|
||||
total_chars += len(section)
|
||||
else:
|
||||
# Truncate last rule
|
||||
remaining = max_chars - total_chars
|
||||
if remaining > 100:
|
||||
parts.append(section[:remaining] + "\n... (truncated)")
|
||||
break
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def create_rule(
|
||||
self,
|
||||
name: str,
|
||||
content: str,
|
||||
description: str = "",
|
||||
globs: Optional[List[str]] = None,
|
||||
activation: Literal["always", "glob", "manual", "ai_decision"] = "always",
|
||||
priority: int = 0,
|
||||
scope: Literal["global", "workspace"] = "workspace"
|
||||
) -> Rule:
|
||||
"""
|
||||
Create a new rule file.
|
||||
|
||||
Args:
|
||||
name: Rule name (used as filename)
|
||||
content: Rule content (markdown)
|
||||
description: Short description
|
||||
globs: Glob patterns for activation
|
||||
activation: Activation mode
|
||||
priority: Priority (higher = applied first)
|
||||
scope: Where to save (global or workspace)
|
||||
|
||||
Returns:
|
||||
Created Rule object
|
||||
"""
|
||||
# Determine save path
|
||||
if scope == "global":
|
||||
rules_dir = self.global_rules_path
|
||||
else:
|
||||
rules_dir = self.workspace_path / self.RULES_DIR_NAME.replace("/", os.sep)
|
||||
|
||||
rules_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = rules_dir / f"{name}.md"
|
||||
|
||||
# Build frontmatter
|
||||
frontmatter_lines = ["---"]
|
||||
if description:
|
||||
frontmatter_lines.append(f'description: "{description}"')
|
||||
if globs:
|
||||
globs_str = ", ".join(f'"{g}"' for g in globs)
|
||||
frontmatter_lines.append(f"globs: [{globs_str}]")
|
||||
frontmatter_lines.append(f"activation: {activation}")
|
||||
if priority != 0:
|
||||
frontmatter_lines.append(f"priority: {priority}")
|
||||
frontmatter_lines.append("---")
|
||||
frontmatter_lines.append("")
|
||||
|
||||
# Write file
|
||||
full_content = "\n".join(frontmatter_lines) + content
|
||||
file_path.write_text(full_content, encoding="utf-8")
|
||||
|
||||
# Create and register rule
|
||||
rule = Rule(
|
||||
name=name,
|
||||
content=content,
|
||||
description=description,
|
||||
globs=globs or [],
|
||||
activation=activation,
|
||||
priority=priority,
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._rules[f"{scope}:{name}"] = rule
|
||||
self._log(f"Created rule '{name}' at {file_path}")
|
||||
|
||||
return rule
|
||||
|
||||
def delete_rule(self, name: str, scope: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Delete a rule file.
|
||||
|
||||
Args:
|
||||
name: Rule name
|
||||
scope: Scope to delete from (global, workspace, or None for any)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
# Find the rule
|
||||
rule = None
|
||||
key = None
|
||||
|
||||
if scope:
|
||||
key = f"{scope}:{name}"
|
||||
rule = self._rules.get(key)
|
||||
else:
|
||||
for s in ["subdir", "workspace", "global"]:
|
||||
key = f"{s}:{name}"
|
||||
if key in self._rules:
|
||||
rule = self._rules[key]
|
||||
break
|
||||
|
||||
if not rule or not rule.file_path:
|
||||
return False
|
||||
|
||||
# Delete file
|
||||
try:
|
||||
Path(rule.file_path).unlink()
|
||||
del self._rules[key]
|
||||
self._log(f"Deleted rule '{name}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._log(f"Error deleting rule '{name}': {e}", logging.ERROR)
|
||||
return False
|
||||
|
||||
def evaluate_ai_decision(
|
||||
self,
|
||||
rule: Rule,
|
||||
context: str,
|
||||
llm_func: Optional[Callable[[str], str]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Evaluate if an ai_decision rule should be applied.
|
||||
|
||||
Uses the rule's description to determine relevance to current context.
|
||||
|
||||
Args:
|
||||
rule: Rule with activation="ai_decision"
|
||||
context: Current conversation/task context
|
||||
llm_func: Optional LLM function for evaluation (if None, returns True)
|
||||
|
||||
Returns:
|
||||
True if rule should be applied, False otherwise
|
||||
"""
|
||||
if rule.activation != "ai_decision":
|
||||
return rule.activation == "always"
|
||||
|
||||
if not llm_func:
|
||||
# Without LLM, include ai_decision rules by default
|
||||
return True
|
||||
|
||||
# Use LLM to decide
|
||||
prompt = f"""Determine if the following rule should be applied to the current context.
|
||||
|
||||
Rule Name: {rule.name}
|
||||
Rule Description: {rule.description}
|
||||
|
||||
Current Context:
|
||||
{context[:1000]}
|
||||
|
||||
Should this rule be applied? Answer only "yes" or "no"."""
|
||||
|
||||
try:
|
||||
response = llm_func(prompt).strip().lower()
|
||||
return response.startswith("yes")
|
||||
except Exception as e:
|
||||
self._log(f"Error evaluating ai_decision for rule '{rule.name}': {e}", logging.WARNING)
|
||||
return True # Default to including the rule
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get rules statistics."""
|
||||
rules = list(self._rules.values())
|
||||
|
||||
# Count rules by source
|
||||
source_counts = {}
|
||||
for key in self._rules.keys():
|
||||
source = key.split(":")[0]
|
||||
source_counts[source] = source_counts.get(source, 0) + 1
|
||||
|
||||
return {
|
||||
"total_rules": len(rules),
|
||||
"always_rules": len([r for r in rules if r.activation == "always"]),
|
||||
"glob_rules": len([r for r in rules if r.activation == "glob"]),
|
||||
"manual_rules": len([r for r in rules if r.activation == "manual"]),
|
||||
"ai_decision_rules": len([r for r in rules if r.activation == "ai_decision"]),
|
||||
"global_rules": source_counts.get("global", 0),
|
||||
"root_rules": source_counts.get("root", 0),
|
||||
"gitroot_rules": source_counts.get("gitroot", 0),
|
||||
"workspace_rules": source_counts.get("workspace", 0),
|
||||
"subdir_rules": source_counts.get("subdir", 0),
|
||||
"claude_rules": source_counts.get("claude", 0),
|
||||
"windsurf_rules": source_counts.get("windsurf", 0),
|
||||
"cursor_rules": source_counts.get("cursor", 0),
|
||||
"sources": source_counts
|
||||
}
|
||||
|
||||
|
||||
def create_rules_manager(
|
||||
workspace_path: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> RulesManager:
|
||||
"""
|
||||
Create a RulesManager instance.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace
|
||||
**kwargs: Additional configuration
|
||||
|
||||
Returns:
|
||||
RulesManager instance
|
||||
"""
|
||||
return RulesManager(workspace_path=workspace_path, **kwargs)
|
||||
@@ -0,0 +1,514 @@
|
||||
"""
|
||||
Workflows System for PraisonAI Agents.
|
||||
|
||||
Provides reusable multi-step workflows similar to Windsurf's Workflows,
|
||||
allowing users to define and execute complex task sequences.
|
||||
|
||||
Features:
|
||||
- YAML/Markdown workflow definitions
|
||||
- Auto-discovery from .praison/workflows/
|
||||
- Step-by-step execution with context passing
|
||||
- Variable substitution
|
||||
- Conditional steps
|
||||
- Lazy loading for performance
|
||||
|
||||
Storage Structure:
|
||||
.praison/workflows/
|
||||
├── deploy.md # Deployment workflow
|
||||
├── test.md # Testing workflow
|
||||
└── review.md # Code review workflow
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Callable, Literal
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowStep:
|
||||
"""A single step in a workflow."""
|
||||
name: str
|
||||
description: str = ""
|
||||
action: str = "" # The action/prompt to execute
|
||||
condition: Optional[str] = None # Optional condition for execution
|
||||
on_error: Literal["stop", "continue", "retry"] = "stop"
|
||||
max_retries: int = 1
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"action": self.action,
|
||||
"condition": self.condition,
|
||||
"on_error": self.on_error,
|
||||
"max_retries": self.max_retries
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Workflow:
|
||||
"""A complete workflow with multiple steps."""
|
||||
name: str
|
||||
description: str = ""
|
||||
steps: List[WorkflowStep] = field(default_factory=list)
|
||||
variables: Dict[str, Any] = field(default_factory=dict)
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"steps": [s.to_dict() for s in self.steps],
|
||||
"variables": self.variables,
|
||||
"file_path": self.file_path
|
||||
}
|
||||
|
||||
|
||||
class WorkflowManager:
|
||||
"""
|
||||
Manages workflow discovery, loading, and execution.
|
||||
|
||||
Workflows are defined in markdown files with YAML frontmatter
|
||||
and step definitions.
|
||||
|
||||
Example workflow file (.praison/workflows/deploy.md):
|
||||
```markdown
|
||||
---
|
||||
name: Deploy to Production
|
||||
description: Deploy the application to production
|
||||
variables:
|
||||
environment: production
|
||||
branch: main
|
||||
---
|
||||
|
||||
## Step 1: Run Tests
|
||||
Run all tests to ensure code quality.
|
||||
|
||||
```action
|
||||
Run the test suite with pytest
|
||||
```
|
||||
|
||||
## Step 2: Build
|
||||
Build the application for production.
|
||||
|
||||
```action
|
||||
Build the application with production settings
|
||||
```
|
||||
|
||||
## Step 3: Deploy
|
||||
Deploy to the production server.
|
||||
|
||||
```action
|
||||
Deploy to {{environment}} from {{branch}}
|
||||
```
|
||||
```
|
||||
|
||||
Example usage:
|
||||
```python
|
||||
manager = WorkflowManager(workspace_path="/path/to/project")
|
||||
|
||||
# List available workflows
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
# Get a specific workflow
|
||||
deploy = manager.get_workflow("deploy")
|
||||
|
||||
# Execute a workflow
|
||||
results = manager.execute(
|
||||
"deploy",
|
||||
executor=lambda prompt: agent.chat(prompt),
|
||||
variables={"branch": "release-1.0"}
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
WORKFLOWS_DIR = ".praison/workflows"
|
||||
SUPPORTED_EXTENSIONS = [".md", ".yaml", ".yml"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize WorkflowManager.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.verbose = verbose
|
||||
|
||||
self._workflows: Dict[str, Workflow] = {}
|
||||
self._loaded = False
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _ensure_loaded(self):
|
||||
"""Lazy load workflows on first access."""
|
||||
if not self._loaded:
|
||||
self._discover_workflows()
|
||||
self._loaded = True
|
||||
|
||||
def _parse_frontmatter(self, content: str) -> tuple[Dict[str, Any], str]:
|
||||
"""Parse YAML frontmatter from markdown content."""
|
||||
frontmatter = {}
|
||||
body = content
|
||||
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
yaml_content = parts[1].strip()
|
||||
body = parts[2].strip()
|
||||
|
||||
# Simple YAML parsing
|
||||
for line in yaml_content.split("\n"):
|
||||
line = line.strip()
|
||||
if ":" in line and not line.startswith("#"):
|
||||
key, value = line.split(":", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Handle basic types
|
||||
if value.lower() in ("true", "false"):
|
||||
value = value.lower() == "true"
|
||||
elif value.isdigit():
|
||||
value = int(value)
|
||||
elif value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
|
||||
frontmatter[key] = value
|
||||
|
||||
return frontmatter, body
|
||||
|
||||
def _parse_steps(self, body: str) -> List[WorkflowStep]:
|
||||
"""Parse workflow steps from markdown body."""
|
||||
steps = []
|
||||
|
||||
# Split by ## headers
|
||||
step_pattern = re.compile(r'^##\s+(?:Step\s+\d+[:\s]*)?(.+)$', re.MULTILINE)
|
||||
action_pattern = re.compile(r'```action\s*\n(.*?)\n```', re.DOTALL)
|
||||
condition_pattern = re.compile(r'```condition\s*\n(.*?)\n```', re.DOTALL)
|
||||
|
||||
# Find all step headers
|
||||
matches = list(step_pattern.finditer(body))
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
step_name = match.group(1).strip()
|
||||
|
||||
# Get content until next step or end
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(body)
|
||||
step_content = body[start:end].strip()
|
||||
|
||||
# Extract action
|
||||
action_match = action_pattern.search(step_content)
|
||||
action = action_match.group(1).strip() if action_match else ""
|
||||
|
||||
# Extract condition
|
||||
condition_match = condition_pattern.search(step_content)
|
||||
condition = condition_match.group(1).strip() if condition_match else None
|
||||
|
||||
# Get description (text before action block)
|
||||
description = step_content
|
||||
if action_match:
|
||||
description = step_content[:action_match.start()].strip()
|
||||
|
||||
# Clean up description (remove code blocks)
|
||||
description = re.sub(r'```.*?```', '', description, flags=re.DOTALL).strip()
|
||||
|
||||
steps.append(WorkflowStep(
|
||||
name=step_name,
|
||||
description=description,
|
||||
action=action,
|
||||
condition=condition
|
||||
))
|
||||
|
||||
return steps
|
||||
|
||||
def _load_workflow(self, file_path: Path) -> Optional[Workflow]:
|
||||
"""Load a workflow from a file."""
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
frontmatter, body = self._parse_frontmatter(content)
|
||||
|
||||
name = frontmatter.get("name", file_path.stem)
|
||||
description = frontmatter.get("description", "")
|
||||
variables = frontmatter.get("variables", {})
|
||||
|
||||
# Parse variables if it's a string (simple YAML)
|
||||
if isinstance(variables, str):
|
||||
variables = {}
|
||||
|
||||
steps = self._parse_steps(body)
|
||||
|
||||
workflow = Workflow(
|
||||
name=name,
|
||||
description=description,
|
||||
steps=steps,
|
||||
variables=variables,
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._log(f"Loaded workflow '{name}' with {len(steps)} steps")
|
||||
return workflow
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error loading workflow {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
|
||||
def _discover_workflows(self):
|
||||
"""Discover all workflows in the workspace."""
|
||||
self._workflows = {}
|
||||
|
||||
workflows_dir = self.workspace_path / self.WORKFLOWS_DIR.replace("/", os.sep)
|
||||
|
||||
if not workflows_dir.exists():
|
||||
return
|
||||
|
||||
for ext in self.SUPPORTED_EXTENSIONS:
|
||||
for file_path in workflows_dir.glob(f"*{ext}"):
|
||||
workflow = self._load_workflow(file_path)
|
||||
if workflow:
|
||||
self._workflows[workflow.name.lower()] = workflow
|
||||
|
||||
self._log(f"Discovered {len(self._workflows)} workflows")
|
||||
|
||||
def reload(self):
|
||||
"""Reload all workflows from disk."""
|
||||
self._loaded = False
|
||||
self._ensure_loaded()
|
||||
|
||||
def list_workflows(self) -> List[Workflow]:
|
||||
"""List all available workflows."""
|
||||
self._ensure_loaded()
|
||||
return list(self._workflows.values())
|
||||
|
||||
def get_workflow(self, name: str) -> Optional[Workflow]:
|
||||
"""Get a workflow by name."""
|
||||
self._ensure_loaded()
|
||||
return self._workflows.get(name.lower())
|
||||
|
||||
def _substitute_variables(
|
||||
self,
|
||||
text: str,
|
||||
variables: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Substitute {{variable}} placeholders in text."""
|
||||
def replace(match):
|
||||
var_name = match.group(1).strip()
|
||||
return str(variables.get(var_name, match.group(0)))
|
||||
|
||||
return re.sub(r'\{\{(\w+)\}\}', replace, text)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
workflow_name: str,
|
||||
executor: Callable[[str], str],
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
on_step: Optional[Callable[[WorkflowStep, int], None]] = None,
|
||||
on_result: Optional[Callable[[WorkflowStep, str], None]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a workflow.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow to execute
|
||||
executor: Function to execute each step (e.g., agent.chat)
|
||||
variables: Variables to substitute in steps
|
||||
on_step: Callback before each step (step, index)
|
||||
on_result: Callback after each step (step, result)
|
||||
|
||||
Returns:
|
||||
Execution results with step outputs and status
|
||||
"""
|
||||
workflow = self.get_workflow(workflow_name)
|
||||
if not workflow:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Workflow '{workflow_name}' not found",
|
||||
"results": []
|
||||
}
|
||||
|
||||
# Merge variables
|
||||
all_variables = {**workflow.variables}
|
||||
if variables:
|
||||
all_variables.update(variables)
|
||||
|
||||
results = []
|
||||
success = True
|
||||
|
||||
for i, step in enumerate(workflow.steps):
|
||||
# Check condition
|
||||
if step.condition:
|
||||
condition = self._substitute_variables(step.condition, all_variables)
|
||||
# Simple condition evaluation (could be enhanced)
|
||||
if condition.lower() in ("false", "no", "skip", "0"):
|
||||
results.append({
|
||||
"step": step.name,
|
||||
"status": "skipped",
|
||||
"output": None
|
||||
})
|
||||
continue
|
||||
|
||||
# Callback before step
|
||||
if on_step:
|
||||
on_step(step, i)
|
||||
|
||||
# Substitute variables in action
|
||||
action = self._substitute_variables(step.action, all_variables)
|
||||
|
||||
# Execute step
|
||||
retries = 0
|
||||
step_success = False
|
||||
output = None
|
||||
error = None
|
||||
|
||||
while retries <= step.max_retries and not step_success:
|
||||
try:
|
||||
output = executor(action)
|
||||
step_success = True
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
retries += 1
|
||||
if retries <= step.max_retries:
|
||||
self._log(f"Step '{step.name}' failed, retrying ({retries}/{step.max_retries})")
|
||||
|
||||
# Callback after step
|
||||
if on_result and output:
|
||||
on_result(step, output)
|
||||
|
||||
results.append({
|
||||
"step": step.name,
|
||||
"status": "success" if step_success else "failed",
|
||||
"output": output,
|
||||
"error": error
|
||||
})
|
||||
|
||||
# Handle failure
|
||||
if not step_success:
|
||||
if step.on_error == "stop":
|
||||
success = False
|
||||
break
|
||||
elif step.on_error == "continue":
|
||||
continue
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"workflow": workflow.name,
|
||||
"results": results
|
||||
}
|
||||
|
||||
def create_workflow(
|
||||
self,
|
||||
name: str,
|
||||
description: str = "",
|
||||
steps: Optional[List[Dict[str, str]]] = None,
|
||||
variables: Optional[Dict[str, Any]] = None
|
||||
) -> Workflow:
|
||||
"""
|
||||
Create a new workflow file.
|
||||
|
||||
Args:
|
||||
name: Workflow name
|
||||
description: Workflow description
|
||||
steps: List of step dicts with name, description, action
|
||||
variables: Default variables
|
||||
|
||||
Returns:
|
||||
Created Workflow object
|
||||
"""
|
||||
workflows_dir = self.workspace_path / self.WORKFLOWS_DIR.replace("/", os.sep)
|
||||
workflows_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = workflows_dir / f"{name.lower().replace(' ', '_')}.md"
|
||||
|
||||
# Build content
|
||||
lines = ["---"]
|
||||
lines.append(f"name: {name}")
|
||||
if description:
|
||||
lines.append(f"description: {description}")
|
||||
if variables:
|
||||
lines.append("variables:")
|
||||
for k, v in variables.items():
|
||||
lines.append(f" {k}: {v}")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# Add steps
|
||||
workflow_steps = []
|
||||
if steps:
|
||||
for i, step in enumerate(steps, 1):
|
||||
step_name = step.get("name", f"Step {i}")
|
||||
step_desc = step.get("description", "")
|
||||
step_action = step.get("action", "")
|
||||
|
||||
lines.append(f"## Step {i}: {step_name}")
|
||||
if step_desc:
|
||||
lines.append(step_desc)
|
||||
lines.append("")
|
||||
lines.append("```action")
|
||||
lines.append(step_action)
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
workflow_steps.append(WorkflowStep(
|
||||
name=step_name,
|
||||
description=step_desc,
|
||||
action=step_action
|
||||
))
|
||||
|
||||
# Write file
|
||||
file_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
# Create and register workflow
|
||||
workflow = Workflow(
|
||||
name=name,
|
||||
description=description,
|
||||
steps=workflow_steps,
|
||||
variables=variables or {},
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._workflows[name.lower()] = workflow
|
||||
self._log(f"Created workflow '{name}' at {file_path}")
|
||||
|
||||
return workflow
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get workflow statistics."""
|
||||
self._ensure_loaded()
|
||||
|
||||
total_steps = sum(len(w.steps) for w in self._workflows.values())
|
||||
|
||||
return {
|
||||
"total_workflows": len(self._workflows),
|
||||
"total_steps": total_steps,
|
||||
"workflows": [w.name for w in self._workflows.values()]
|
||||
}
|
||||
|
||||
|
||||
def create_workflow_manager(
|
||||
workspace_path: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> WorkflowManager:
|
||||
"""
|
||||
Create a WorkflowManager instance.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace
|
||||
**kwargs: Additional configuration
|
||||
|
||||
Returns:
|
||||
WorkflowManager instance
|
||||
"""
|
||||
return WorkflowManager(workspace_path=workspace_path, **kwargs)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Planning Module for PraisonAI Agents.
|
||||
|
||||
Provides Planning Mode functionality similar to:
|
||||
- Cursor Plan Mode
|
||||
- Windsurf Planning Mode
|
||||
- Claude Code Plan Mode
|
||||
- Gemini CLI Plan Mode
|
||||
- Codex CLI /plan command
|
||||
|
||||
Features:
|
||||
- PlanningAgent for creating implementation plans
|
||||
- Plan and PlanStep dataclasses for plan structure
|
||||
- TodoList for tracking progress
|
||||
- PlanStorage for persistence
|
||||
- ApprovalCallback for plan approval flow
|
||||
- Read-only mode for safe research
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent1, agent2],
|
||||
tasks=[task1, task2],
|
||||
planning=True, # Enable planning mode
|
||||
planning_llm="gpt-4o-mini" # Optional: custom LLM
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
"""
|
||||
|
||||
from .plan import Plan, PlanStep
|
||||
from .todo import TodoList, TodoItem
|
||||
from .storage import PlanStorage
|
||||
from .planner import PlanningAgent
|
||||
from .approval import ApprovalCallback
|
||||
|
||||
# Read-only tools that are allowed in plan mode
|
||||
READ_ONLY_TOOLS = [
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_codebase",
|
||||
"search_files",
|
||||
"grep_search",
|
||||
"find_files",
|
||||
"web_search",
|
||||
"get_file_content",
|
||||
"list_files",
|
||||
"read_document",
|
||||
"search_web",
|
||||
"fetch_url",
|
||||
"get_context",
|
||||
]
|
||||
|
||||
# Restricted tools that are blocked in plan mode
|
||||
RESTRICTED_TOOLS = [
|
||||
"write_file",
|
||||
"create_file",
|
||||
"delete_file",
|
||||
"execute_command",
|
||||
"run_command",
|
||||
"shell_command",
|
||||
"modify_file",
|
||||
"edit_file",
|
||||
"remove_file",
|
||||
"move_file",
|
||||
"copy_file",
|
||||
"mkdir",
|
||||
"rmdir",
|
||||
"git_commit",
|
||||
"git_push",
|
||||
"npm_install",
|
||||
"pip_install",
|
||||
]
|
||||
|
||||
# Research tools that are safe for planning research
|
||||
RESEARCH_TOOLS = [
|
||||
"web_search",
|
||||
"search_web",
|
||||
"duckduckgo_search",
|
||||
"tavily_search",
|
||||
"brave_search",
|
||||
"google_search",
|
||||
"read_url",
|
||||
"fetch_url",
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_codebase",
|
||||
"grep_search",
|
||||
"find_files",
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
"Plan",
|
||||
"PlanStep",
|
||||
"TodoList",
|
||||
"TodoItem",
|
||||
"PlanStorage",
|
||||
"PlanningAgent",
|
||||
"ApprovalCallback",
|
||||
"READ_ONLY_TOOLS",
|
||||
"RESTRICTED_TOOLS",
|
||||
"RESEARCH_TOOLS",
|
||||
]
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
ApprovalCallback for plan approval flow.
|
||||
|
||||
Provides approval functionality similar to:
|
||||
- Cursor Plan Mode approval
|
||||
- Claude Code plan approval
|
||||
- Codex CLI /approvals
|
||||
|
||||
Features:
|
||||
- Auto-approve option
|
||||
- Custom approval functions
|
||||
- Async support
|
||||
- Plan modification during approval
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Callable, Optional, Union, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .plan import Plan
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApprovalCallback:
|
||||
"""
|
||||
Callback for plan approval flow.
|
||||
|
||||
Handles the approval process for plans before execution.
|
||||
Can be configured for auto-approval or custom approval logic.
|
||||
|
||||
Attributes:
|
||||
auto_approve: Whether to automatically approve all plans
|
||||
approve_fn: Custom approval function
|
||||
on_reject: Callback when plan is rejected
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auto_approve: bool = False,
|
||||
approve_fn: Optional[Callable[['Plan'], Union[bool, 'asyncio.Future']]] = None,
|
||||
on_reject: Optional[Callable[['Plan'], None]] = None
|
||||
):
|
||||
"""
|
||||
Initialize ApprovalCallback.
|
||||
|
||||
Args:
|
||||
auto_approve: If True, automatically approve all plans
|
||||
approve_fn: Custom function to determine approval
|
||||
Should return True to approve, False to reject
|
||||
Can be sync or async
|
||||
on_reject: Optional callback when plan is rejected
|
||||
"""
|
||||
self.auto_approve = auto_approve
|
||||
self.approve_fn = approve_fn
|
||||
self.on_reject = on_reject
|
||||
|
||||
def __call__(self, plan: 'Plan') -> bool:
|
||||
"""
|
||||
Synchronously check if plan is approved.
|
||||
|
||||
Args:
|
||||
plan: Plan to check for approval
|
||||
|
||||
Returns:
|
||||
True if approved, False if rejected
|
||||
"""
|
||||
if self.auto_approve:
|
||||
logger.debug(f"Auto-approving plan: {plan.name}")
|
||||
plan.approve()
|
||||
return True
|
||||
|
||||
if self.approve_fn is not None:
|
||||
# Check if function is async
|
||||
if inspect.iscoroutinefunction(self.approve_fn):
|
||||
# Run async function synchronously
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
result = loop.run_until_complete(self.approve_fn(plan))
|
||||
finally:
|
||||
loop.close()
|
||||
else:
|
||||
result = self.approve_fn(plan)
|
||||
|
||||
if result:
|
||||
plan.approve()
|
||||
return True
|
||||
else:
|
||||
if self.on_reject:
|
||||
self.on_reject(plan)
|
||||
return False
|
||||
|
||||
# Default: require explicit approval (return False)
|
||||
logger.debug(f"Plan requires explicit approval: {plan.name}")
|
||||
return False
|
||||
|
||||
async def async_call(self, plan: 'Plan') -> bool:
|
||||
"""
|
||||
Asynchronously check if plan is approved.
|
||||
|
||||
Args:
|
||||
plan: Plan to check for approval
|
||||
|
||||
Returns:
|
||||
True if approved, False if rejected
|
||||
"""
|
||||
if self.auto_approve:
|
||||
logger.debug(f"Auto-approving plan: {plan.name}")
|
||||
plan.approve()
|
||||
return True
|
||||
|
||||
if self.approve_fn is not None:
|
||||
# Check if function is async
|
||||
if inspect.iscoroutinefunction(self.approve_fn):
|
||||
result = await self.approve_fn(plan)
|
||||
else:
|
||||
result = self.approve_fn(plan)
|
||||
|
||||
if result:
|
||||
plan.approve()
|
||||
return True
|
||||
else:
|
||||
if self.on_reject:
|
||||
self.on_reject(plan)
|
||||
return False
|
||||
|
||||
# Default: require explicit approval (return False)
|
||||
logger.debug(f"Plan requires explicit approval: {plan.name}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def console_approval(plan: 'Plan') -> bool:
|
||||
"""
|
||||
Interactive console approval.
|
||||
|
||||
Displays the plan and prompts for approval.
|
||||
|
||||
Args:
|
||||
plan: Plan to approve
|
||||
|
||||
Returns:
|
||||
True if user approves, False otherwise
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("PLAN APPROVAL REQUIRED")
|
||||
print("=" * 60)
|
||||
print(plan.to_markdown())
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
response = input("\nApprove this plan? [y/n/e(dit)]: ").strip().lower()
|
||||
|
||||
if response in ['y', 'yes']:
|
||||
return True
|
||||
elif response in ['n', 'no']:
|
||||
return False
|
||||
elif response in ['e', 'edit']:
|
||||
print("\nPlan editing not implemented in console mode.")
|
||||
print("Please modify the plan programmatically and re-submit.")
|
||||
continue
|
||||
else:
|
||||
print("Please enter 'y' for yes, 'n' for no, or 'e' to edit.")
|
||||
|
||||
@staticmethod
|
||||
def always_approve(plan: 'Plan') -> bool:
|
||||
"""Always approve plans."""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def always_reject(plan: 'Plan') -> bool:
|
||||
"""Always reject plans."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def approve_if_small(plan: 'Plan', max_steps: int = 5) -> bool:
|
||||
"""
|
||||
Approve plans with few steps automatically.
|
||||
|
||||
Args:
|
||||
plan: Plan to check
|
||||
max_steps: Maximum number of steps to auto-approve
|
||||
|
||||
Returns:
|
||||
True if plan has <= max_steps
|
||||
"""
|
||||
return len(plan.steps) <= max_steps
|
||||
|
||||
@staticmethod
|
||||
def approve_if_no_dangerous_tools(plan: 'Plan') -> bool:
|
||||
"""
|
||||
Approve plans that don't use dangerous tools.
|
||||
|
||||
Args:
|
||||
plan: Plan to check
|
||||
|
||||
Returns:
|
||||
True if no dangerous tools are used
|
||||
"""
|
||||
from . import RESTRICTED_TOOLS
|
||||
|
||||
for step in plan.steps:
|
||||
for tool in step.tools:
|
||||
if tool in RESTRICTED_TOOLS:
|
||||
logger.warning(f"Plan uses restricted tool: {tool}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InteractiveApproval:
|
||||
"""
|
||||
Interactive approval handler with rich UI support.
|
||||
|
||||
Provides a more sophisticated approval interface
|
||||
with plan editing capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self, use_rich: bool = True):
|
||||
"""
|
||||
Initialize InteractiveApproval.
|
||||
|
||||
Args:
|
||||
use_rich: Whether to use Rich library for display
|
||||
"""
|
||||
self.use_rich = use_rich
|
||||
self._rich_available = False
|
||||
|
||||
if use_rich:
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
self._rich_available = True
|
||||
self._console = Console()
|
||||
except ImportError:
|
||||
self._rich_available = False
|
||||
|
||||
def __call__(self, plan: 'Plan') -> bool:
|
||||
"""
|
||||
Display plan and get approval.
|
||||
|
||||
Args:
|
||||
plan: Plan to approve
|
||||
|
||||
Returns:
|
||||
True if approved
|
||||
"""
|
||||
if self._rich_available:
|
||||
return self._rich_approval(plan)
|
||||
else:
|
||||
return ApprovalCallback.console_approval(plan)
|
||||
|
||||
def _rich_approval(self, plan: 'Plan') -> bool:
|
||||
"""Rich library approval display."""
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
from rich.markdown import Markdown
|
||||
|
||||
# Display plan
|
||||
md = Markdown(plan.to_markdown())
|
||||
self._console.print(Panel(md, title="[bold blue]Plan Approval Required[/bold blue]"))
|
||||
|
||||
# Get approval
|
||||
approved = Confirm.ask("Approve this plan?")
|
||||
|
||||
return approved
|
||||
|
||||
async def async_call(self, plan: 'Plan') -> bool:
|
||||
"""Async version of approval."""
|
||||
# For now, just call sync version
|
||||
# Could be extended for async UI frameworks
|
||||
return self(plan)
|
||||
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
Plan and PlanStep dataclasses for Planning Mode.
|
||||
|
||||
Provides structured representation of implementation plans
|
||||
with support for:
|
||||
- Step dependencies
|
||||
- Status tracking
|
||||
- Markdown serialization
|
||||
- Progress calculation
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import re
|
||||
import yaml
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any, Literal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanStep:
|
||||
"""
|
||||
A single step in an implementation plan.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the step
|
||||
description: What this step does
|
||||
agent: Name of the agent to execute this step
|
||||
tools: List of tools needed for this step
|
||||
dependencies: List of step IDs that must complete first
|
||||
status: Current status (pending, in_progress, completed, skipped)
|
||||
estimated_tokens: Estimated token usage for this step
|
||||
"""
|
||||
description: str
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
agent: Optional[str] = None
|
||||
tools: List[str] = field(default_factory=list)
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
status: Literal["pending", "in_progress", "completed", "skipped"] = "pending"
|
||||
estimated_tokens: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert PlanStep to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"description": self.description,
|
||||
"agent": self.agent,
|
||||
"tools": self.tools,
|
||||
"dependencies": self.dependencies,
|
||||
"status": self.status,
|
||||
"estimated_tokens": self.estimated_tokens
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'PlanStep':
|
||||
"""Create PlanStep from dictionary."""
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())[:8]),
|
||||
description=data.get("description", ""),
|
||||
agent=data.get("agent"),
|
||||
tools=data.get("tools", []),
|
||||
dependencies=data.get("dependencies", []),
|
||||
status=data.get("status", "pending"),
|
||||
estimated_tokens=data.get("estimated_tokens", 0)
|
||||
)
|
||||
|
||||
def mark_complete(self) -> None:
|
||||
"""Mark this step as completed."""
|
||||
self.status = "completed"
|
||||
|
||||
def mark_in_progress(self) -> None:
|
||||
"""Mark this step as in progress."""
|
||||
self.status = "in_progress"
|
||||
|
||||
def mark_skipped(self) -> None:
|
||||
"""Mark this step as skipped."""
|
||||
self.status = "skipped"
|
||||
|
||||
def is_ready(self, completed_steps: List[str]) -> bool:
|
||||
"""
|
||||
Check if this step is ready to execute.
|
||||
|
||||
Args:
|
||||
completed_steps: List of completed step IDs
|
||||
|
||||
Returns:
|
||||
True if all dependencies are satisfied
|
||||
"""
|
||||
if self.status != "pending":
|
||||
return False
|
||||
return all(dep in completed_steps for dep in self.dependencies)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plan:
|
||||
"""
|
||||
An implementation plan with multiple steps.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the plan
|
||||
name: Human-readable name
|
||||
description: Detailed description of what the plan accomplishes
|
||||
steps: List of PlanStep objects
|
||||
status: Current status (draft, approved, executing, completed)
|
||||
created_at: When the plan was created
|
||||
approved_at: When the plan was approved (if applicable)
|
||||
file_path: Path where the plan is stored (if saved)
|
||||
"""
|
||||
name: str
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
description: str = ""
|
||||
steps: List[PlanStep] = field(default_factory=list)
|
||||
status: Literal["draft", "approved", "executing", "completed", "cancelled"] = "draft"
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
approved_at: Optional[datetime] = None
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert Plan to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"steps": [step.to_dict() for step in self.steps],
|
||||
"status": self.status,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"approved_at": self.approved_at.isoformat() if self.approved_at else None,
|
||||
"file_path": self.file_path
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Plan':
|
||||
"""Create Plan from dictionary."""
|
||||
steps = [
|
||||
PlanStep.from_dict(s) if isinstance(s, dict) else s
|
||||
for s in data.get("steps", [])
|
||||
]
|
||||
|
||||
created_at = data.get("created_at")
|
||||
if isinstance(created_at, str):
|
||||
created_at = datetime.fromisoformat(created_at)
|
||||
elif created_at is None:
|
||||
created_at = datetime.now()
|
||||
|
||||
approved_at = data.get("approved_at")
|
||||
if isinstance(approved_at, str):
|
||||
approved_at = datetime.fromisoformat(approved_at)
|
||||
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())[:8]),
|
||||
name=data.get("name", "Untitled Plan"),
|
||||
description=data.get("description", ""),
|
||||
steps=steps,
|
||||
status=data.get("status", "draft"),
|
||||
created_at=created_at,
|
||||
approved_at=approved_at,
|
||||
file_path=data.get("file_path")
|
||||
)
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""
|
||||
Convert Plan to markdown format.
|
||||
|
||||
Returns:
|
||||
Markdown string representation of the plan
|
||||
"""
|
||||
# YAML frontmatter
|
||||
frontmatter = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"status": self.status,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"approved_at": self.approved_at.isoformat() if self.approved_at else None
|
||||
}
|
||||
|
||||
lines = [
|
||||
"---",
|
||||
yaml.dump(frontmatter, default_flow_style=False).strip(),
|
||||
"---",
|
||||
"",
|
||||
f"# {self.name}",
|
||||
"",
|
||||
]
|
||||
|
||||
if self.description:
|
||||
lines.extend([self.description, ""])
|
||||
|
||||
lines.extend(["## Steps", ""])
|
||||
|
||||
status_icons = {
|
||||
"pending": "⬜",
|
||||
"in_progress": "🔄",
|
||||
"completed": "✅",
|
||||
"skipped": "⏭️"
|
||||
}
|
||||
|
||||
for i, step in enumerate(self.steps, 1):
|
||||
icon = status_icons.get(step.status, "⬜")
|
||||
lines.append(f"{i}. {icon} {step.description}")
|
||||
|
||||
if step.agent:
|
||||
lines.append(f" - Agent: {step.agent}")
|
||||
if step.tools:
|
||||
lines.append(f" - Tools: {', '.join(step.tools)}")
|
||||
if step.dependencies:
|
||||
lines.append(f" - Depends on: {', '.join(step.dependencies)}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@classmethod
|
||||
def from_markdown(cls, markdown: str) -> 'Plan':
|
||||
"""
|
||||
Create Plan from markdown format.
|
||||
|
||||
Args:
|
||||
markdown: Markdown string with YAML frontmatter
|
||||
|
||||
Returns:
|
||||
Plan instance
|
||||
"""
|
||||
# Extract YAML frontmatter
|
||||
frontmatter_match = re.match(r'^---\n(.*?)\n---', markdown, re.DOTALL)
|
||||
|
||||
data = {}
|
||||
if frontmatter_match:
|
||||
try:
|
||||
data = yaml.safe_load(frontmatter_match.group(1))
|
||||
except yaml.YAMLError:
|
||||
logger.warning("Failed to parse YAML frontmatter")
|
||||
|
||||
# Extract steps from markdown
|
||||
steps = []
|
||||
# Pattern to capture status icon and description
|
||||
step_pattern = r'^\d+\.\s*([⬜🔄✅⏭️]?)\s*(.+?)$'
|
||||
|
||||
# Map icons to status
|
||||
icon_to_status = {
|
||||
"⬜": "pending",
|
||||
"🔄": "in_progress",
|
||||
"✅": "completed",
|
||||
"⏭️": "skipped",
|
||||
"": "pending"
|
||||
}
|
||||
|
||||
for match in re.finditer(step_pattern, markdown, re.MULTILINE):
|
||||
icon = match.group(1).strip()
|
||||
description = match.group(2).strip()
|
||||
status = icon_to_status.get(icon, "pending")
|
||||
steps.append(PlanStep(description=description, status=status))
|
||||
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())[:8]),
|
||||
name=data.get("name", "Untitled Plan"),
|
||||
description=data.get("description", ""),
|
||||
steps=steps,
|
||||
status=data.get("status", "draft")
|
||||
)
|
||||
|
||||
def add_step(self, step: PlanStep) -> None:
|
||||
"""Add a step to the plan."""
|
||||
self.steps.append(step)
|
||||
|
||||
def remove_step(self, step_id: str) -> bool:
|
||||
"""
|
||||
Remove a step from the plan.
|
||||
|
||||
Args:
|
||||
step_id: ID of the step to remove
|
||||
|
||||
Returns:
|
||||
True if step was removed, False if not found
|
||||
"""
|
||||
for i, step in enumerate(self.steps):
|
||||
if step.id == step_id:
|
||||
self.steps.pop(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_step(self, step_id: str) -> Optional[PlanStep]:
|
||||
"""
|
||||
Get a step by ID.
|
||||
|
||||
Args:
|
||||
step_id: ID of the step to find
|
||||
|
||||
Returns:
|
||||
PlanStep if found, None otherwise
|
||||
"""
|
||||
for step in self.steps:
|
||||
if step.id == step_id:
|
||||
return step
|
||||
return None
|
||||
|
||||
def approve(self) -> None:
|
||||
"""Approve the plan for execution."""
|
||||
self.status = "approved"
|
||||
self.approved_at = datetime.now()
|
||||
|
||||
def start_execution(self) -> None:
|
||||
"""Mark the plan as executing."""
|
||||
self.status = "executing"
|
||||
|
||||
def complete(self) -> None:
|
||||
"""Mark the plan as completed."""
|
||||
self.status = "completed"
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel the plan."""
|
||||
self.status = "cancelled"
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
"""
|
||||
Calculate plan progress.
|
||||
|
||||
Returns:
|
||||
Float between 0.0 and 1.0 representing completion percentage
|
||||
"""
|
||||
if not self.steps:
|
||||
return 1.0
|
||||
|
||||
completed = sum(1 for s in self.steps if s.status == "completed")
|
||||
return completed / len(self.steps)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if all steps are completed."""
|
||||
return all(s.status == "completed" for s in self.steps)
|
||||
|
||||
@property
|
||||
def completed_step_ids(self) -> List[str]:
|
||||
"""Get list of completed step IDs."""
|
||||
return [s.id for s in self.steps if s.status == "completed"]
|
||||
|
||||
def get_next_steps(self) -> List[PlanStep]:
|
||||
"""
|
||||
Get steps that are ready to execute.
|
||||
|
||||
Returns:
|
||||
List of steps with satisfied dependencies
|
||||
"""
|
||||
completed = self.completed_step_ids
|
||||
return [s for s in self.steps if s.is_ready(completed)]
|
||||
|
||||
def update_step_status(self, step_id: str, status: str) -> bool:
|
||||
"""
|
||||
Update the status of a step.
|
||||
|
||||
Args:
|
||||
step_id: ID of the step to update
|
||||
status: New status
|
||||
|
||||
Returns:
|
||||
True if step was updated, False if not found
|
||||
"""
|
||||
step = self.get_step(step_id)
|
||||
if step:
|
||||
step.status = status
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,525 @@
|
||||
"""
|
||||
PlanningAgent for creating implementation plans.
|
||||
|
||||
Provides planning functionality similar to:
|
||||
- CrewAI AgentPlanner
|
||||
- Cursor Plan Mode
|
||||
- Claude Code Plan Mode
|
||||
|
||||
Features:
|
||||
- Analyze requests and create detailed plans
|
||||
- Read-only mode for safe research
|
||||
- Plan refinement based on feedback
|
||||
- Context analysis
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
from .plan import Plan, PlanStep
|
||||
|
||||
# Define READ_ONLY_TOOLS locally to avoid circular import
|
||||
READ_ONLY_TOOLS = [
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_codebase",
|
||||
"search_files",
|
||||
"grep_search",
|
||||
"find_files",
|
||||
"web_search",
|
||||
"get_file_content",
|
||||
"list_files",
|
||||
"read_document",
|
||||
"search_web",
|
||||
"fetch_url",
|
||||
"get_context",
|
||||
]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agent.agent import Agent
|
||||
from ..task.task import Task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlanningAgent:
|
||||
"""
|
||||
Agent specialized in creating implementation plans.
|
||||
|
||||
The PlanningAgent analyzes requests, researches the codebase,
|
||||
and creates detailed step-by-step plans before execution.
|
||||
|
||||
Attributes:
|
||||
llm_model: LLM model to use for planning
|
||||
read_only: Whether to restrict to read-only tools
|
||||
allowed_tools: List of tools available in planning mode
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
|
||||
PLANNING_PROMPT = '''You are an expert planning specialist. Your task is to analyze the request and create a detailed, actionable implementation plan.
|
||||
|
||||
## Request
|
||||
{request}
|
||||
|
||||
## Available Agents
|
||||
{agents_info}
|
||||
|
||||
## Available Tasks (if any)
|
||||
{tasks_info}
|
||||
|
||||
## Context
|
||||
{context}
|
||||
|
||||
## Instructions
|
||||
1. Analyze the request thoroughly
|
||||
2. Break it down into clear, actionable steps
|
||||
3. Assign appropriate agents to each step
|
||||
4. Identify dependencies between steps
|
||||
5. Consider potential risks and edge cases
|
||||
|
||||
## Output Format
|
||||
Respond with a JSON object in this exact format:
|
||||
{{
|
||||
"name": "Brief plan name",
|
||||
"description": "Detailed description of what this plan accomplishes",
|
||||
"steps": [
|
||||
{{
|
||||
"description": "Clear description of what this step does",
|
||||
"agent": "Name of the agent to execute this step (or null)",
|
||||
"tools": ["list", "of", "tools", "needed"],
|
||||
"dependencies": ["list of step IDs this depends on"]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
Important:
|
||||
- Each step should be atomic and clearly defined
|
||||
- Dependencies should reference step indices (e.g., "step_0", "step_1")
|
||||
- Be specific about what each step accomplishes
|
||||
- Consider the order of operations carefully'''
|
||||
|
||||
REFINEMENT_PROMPT = '''You are refining an existing implementation plan based on feedback.
|
||||
|
||||
## Original Plan
|
||||
{original_plan}
|
||||
|
||||
## Feedback
|
||||
{feedback}
|
||||
|
||||
## Instructions
|
||||
1. Review the original plan
|
||||
2. Apply the feedback to improve the plan
|
||||
3. Maintain the same JSON format
|
||||
4. Keep steps that are still valid
|
||||
5. Add, modify, or remove steps as needed
|
||||
|
||||
## Output Format
|
||||
Respond with a JSON object in the same format as the original plan:
|
||||
{{
|
||||
"name": "Updated plan name",
|
||||
"description": "Updated description",
|
||||
"steps": [...]
|
||||
}}'''
|
||||
|
||||
ANALYSIS_PROMPT = '''Analyze the following context and provide insights for planning:
|
||||
|
||||
## Context
|
||||
{context}
|
||||
|
||||
## Instructions
|
||||
1. Identify the key components and their relationships
|
||||
2. Note any patterns or conventions
|
||||
3. Highlight potential challenges
|
||||
4. Suggest best practices to follow
|
||||
|
||||
Provide a concise analysis that will help in creating an implementation plan.'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: str = "gpt-4o-mini",
|
||||
read_only: bool = True,
|
||||
verbose: int = 0,
|
||||
tools: Optional[List] = None,
|
||||
reasoning: bool = False,
|
||||
max_reasoning_steps: int = 5
|
||||
):
|
||||
"""
|
||||
Initialize PlanningAgent.
|
||||
|
||||
Args:
|
||||
llm: LLM model to use for planning
|
||||
read_only: Whether to restrict to read-only tools
|
||||
verbose: Verbosity level
|
||||
tools: Optional list of tools for research during planning
|
||||
reasoning: Whether to enable reasoning mode with chain-of-thought
|
||||
max_reasoning_steps: Maximum reasoning iterations (default 5)
|
||||
"""
|
||||
self.llm_model = llm
|
||||
self.read_only = read_only
|
||||
self.verbose = verbose
|
||||
self.tools = tools or []
|
||||
self.reasoning = reasoning
|
||||
self.max_reasoning_steps = max_reasoning_steps
|
||||
self.allowed_tools = READ_ONLY_TOOLS.copy() if read_only else []
|
||||
self._llm = None
|
||||
self._agent = None
|
||||
|
||||
def _get_llm(self):
|
||||
"""Lazy load LLM instance."""
|
||||
if self._llm is None:
|
||||
from ..llm import LLM
|
||||
self._llm = LLM(model=self.llm_model)
|
||||
return self._llm
|
||||
|
||||
def _get_agent(self):
|
||||
"""
|
||||
Lazy load Agent instance for tool-enabled planning.
|
||||
|
||||
Returns:
|
||||
Agent instance if tools are provided, None otherwise
|
||||
"""
|
||||
if not self.tools:
|
||||
return None
|
||||
|
||||
if self._agent is None:
|
||||
from ..agent.agent import Agent
|
||||
self._agent = Agent(
|
||||
name="Planning Research Agent",
|
||||
role="Research and Planning Specialist",
|
||||
goal="Research information and create detailed implementation plans",
|
||||
llm=self.llm_model,
|
||||
tools=self.tools,
|
||||
verbose=self.verbose > 0
|
||||
)
|
||||
return self._agent
|
||||
|
||||
def _format_agents_info(self, agents: List['Agent']) -> str:
|
||||
"""
|
||||
Format agent information for the prompt.
|
||||
|
||||
Args:
|
||||
agents: List of Agent instances
|
||||
|
||||
Returns:
|
||||
Formatted string describing agents
|
||||
"""
|
||||
if not agents:
|
||||
return "No agents specified"
|
||||
|
||||
lines = []
|
||||
for agent in agents:
|
||||
name = getattr(agent, 'name', 'Unnamed')
|
||||
role = getattr(agent, 'role', 'General')
|
||||
goal = getattr(agent, 'goal', '')
|
||||
|
||||
line = f"- **{name}** ({role})"
|
||||
if goal:
|
||||
line += f": {goal}"
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_tasks_info(self, tasks: Optional[List['Task']]) -> str:
|
||||
"""
|
||||
Format task information for the prompt.
|
||||
|
||||
Args:
|
||||
tasks: List of Task instances
|
||||
|
||||
Returns:
|
||||
Formatted string describing tasks
|
||||
"""
|
||||
if not tasks:
|
||||
return "No predefined tasks"
|
||||
|
||||
lines = []
|
||||
for task in tasks:
|
||||
name = getattr(task, 'name', None) or getattr(task, 'description', 'Unnamed')[:50]
|
||||
desc = getattr(task, 'description', '')[:100]
|
||||
|
||||
lines.append(f"- {name}: {desc}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _parse_plan_response(self, response: str) -> Plan:
|
||||
"""
|
||||
Parse LLM response into a Plan object.
|
||||
|
||||
Args:
|
||||
response: JSON string from LLM
|
||||
|
||||
Returns:
|
||||
Plan instance
|
||||
"""
|
||||
try:
|
||||
# Try to extract JSON from response
|
||||
# Handle cases where LLM includes extra text
|
||||
json_start = response.find('{')
|
||||
json_end = response.rfind('}') + 1
|
||||
|
||||
if json_start >= 0 and json_end > json_start:
|
||||
json_str = response[json_start:json_end]
|
||||
data = json.loads(json_str)
|
||||
else:
|
||||
raise ValueError("No JSON found in response")
|
||||
|
||||
# Create steps
|
||||
steps = []
|
||||
for i, step_data in enumerate(data.get("steps", [])):
|
||||
step = PlanStep(
|
||||
id=f"step_{i}",
|
||||
description=step_data.get("description", ""),
|
||||
agent=step_data.get("agent"),
|
||||
tools=step_data.get("tools", []),
|
||||
dependencies=step_data.get("dependencies", [])
|
||||
)
|
||||
steps.append(step)
|
||||
|
||||
return Plan(
|
||||
name=data.get("name", "Implementation Plan"),
|
||||
description=data.get("description", ""),
|
||||
steps=steps
|
||||
)
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning(f"Failed to parse plan response: {e}")
|
||||
# Return a simple plan with the response as a single step
|
||||
return Plan(
|
||||
name="Implementation Plan",
|
||||
description="Auto-generated plan",
|
||||
steps=[PlanStep(description=response[:500])]
|
||||
)
|
||||
|
||||
async def create_plan(
|
||||
self,
|
||||
request: str,
|
||||
agents: List['Agent'],
|
||||
tasks: Optional[List['Task']] = None,
|
||||
context: Optional[str] = None
|
||||
) -> Plan:
|
||||
"""
|
||||
Create an implementation plan for the given request.
|
||||
|
||||
Args:
|
||||
request: The user's request or goal
|
||||
agents: List of available agents
|
||||
tasks: Optional list of predefined tasks
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
Plan instance with steps
|
||||
"""
|
||||
llm = self._get_llm()
|
||||
|
||||
# Format the prompt
|
||||
prompt = self.PLANNING_PROMPT.format(
|
||||
request=request,
|
||||
agents_info=self._format_agents_info(agents),
|
||||
tasks_info=self._format_tasks_info(tasks),
|
||||
context=context or "No additional context provided"
|
||||
)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info(f"Creating plan for: {request[:100]}...")
|
||||
|
||||
# Get LLM response
|
||||
response = await llm.get_response_async(prompt)
|
||||
|
||||
# Parse response into Plan
|
||||
plan = self._parse_plan_response(response)
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Created plan '{plan.name}' with {len(plan.steps)} steps")
|
||||
|
||||
return plan
|
||||
|
||||
def create_plan_sync(
|
||||
self,
|
||||
request: str,
|
||||
agents: List['Agent'],
|
||||
tasks: Optional[List['Task']] = None,
|
||||
context: Optional[str] = None
|
||||
) -> Plan:
|
||||
"""
|
||||
Synchronous version of create_plan.
|
||||
|
||||
Args:
|
||||
request: The user's request or goal
|
||||
agents: List of available agents
|
||||
tasks: Optional list of predefined tasks
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
Plan instance with steps
|
||||
"""
|
||||
# Format the prompt
|
||||
prompt = self.PLANNING_PROMPT.format(
|
||||
request=request,
|
||||
agents_info=self._format_agents_info(agents),
|
||||
tasks_info=self._format_tasks_info(tasks),
|
||||
context=context or "No additional context provided"
|
||||
)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info(f"Creating plan for: {request[:100]}...")
|
||||
|
||||
# Use Agent with tools if available, otherwise use bare LLM
|
||||
agent = self._get_agent()
|
||||
if agent is not None:
|
||||
# Use Agent.chat() for tool-enabled planning
|
||||
response = agent.chat(prompt)
|
||||
else:
|
||||
# Use bare LLM for simple planning
|
||||
llm = self._get_llm()
|
||||
response = llm.get_response(prompt)
|
||||
|
||||
# Parse response into Plan
|
||||
plan = self._parse_plan_response(response)
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Created plan '{plan.name}' with {len(plan.steps)} steps")
|
||||
|
||||
return plan
|
||||
|
||||
async def refine_plan(
|
||||
self,
|
||||
plan: Plan,
|
||||
feedback: str
|
||||
) -> Plan:
|
||||
"""
|
||||
Refine an existing plan based on feedback.
|
||||
|
||||
Args:
|
||||
plan: Original plan to refine
|
||||
feedback: User feedback for refinement
|
||||
|
||||
Returns:
|
||||
Refined Plan instance
|
||||
"""
|
||||
llm = self._get_llm()
|
||||
|
||||
# Format the prompt
|
||||
prompt = self.REFINEMENT_PROMPT.format(
|
||||
original_plan=json.dumps(plan.to_dict(), indent=2),
|
||||
feedback=feedback
|
||||
)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info(f"Refining plan based on feedback: {feedback[:100]}...")
|
||||
|
||||
# Get LLM response
|
||||
response = await llm.get_response_async(prompt)
|
||||
|
||||
# Parse response into Plan
|
||||
refined = self._parse_plan_response(response)
|
||||
|
||||
# Preserve original ID
|
||||
refined.id = plan.id
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Refined plan '{refined.name}' with {len(refined.steps)} steps")
|
||||
|
||||
return refined
|
||||
|
||||
def refine_plan_sync(
|
||||
self,
|
||||
plan: Plan,
|
||||
feedback: str
|
||||
) -> Plan:
|
||||
"""
|
||||
Synchronous version of refine_plan.
|
||||
|
||||
Args:
|
||||
plan: Original plan to refine
|
||||
feedback: User feedback for refinement
|
||||
|
||||
Returns:
|
||||
Refined Plan instance
|
||||
"""
|
||||
llm = self._get_llm()
|
||||
|
||||
# Format the prompt
|
||||
prompt = self.REFINEMENT_PROMPT.format(
|
||||
original_plan=json.dumps(plan.to_dict(), indent=2),
|
||||
feedback=feedback
|
||||
)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info(f"Refining plan based on feedback: {feedback[:100]}...")
|
||||
|
||||
# Get LLM response
|
||||
response = llm.get_response(prompt)
|
||||
|
||||
# Parse response into Plan
|
||||
refined = self._parse_plan_response(response)
|
||||
|
||||
# Preserve original ID
|
||||
refined.id = plan.id
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Refined plan '{refined.name}' with {len(refined.steps)} steps")
|
||||
|
||||
return refined
|
||||
|
||||
async def analyze_context(
|
||||
self,
|
||||
context: str
|
||||
) -> str:
|
||||
"""
|
||||
Analyze context and provide insights for planning.
|
||||
|
||||
Args:
|
||||
context: Context to analyze (e.g., codebase summary)
|
||||
|
||||
Returns:
|
||||
Analysis string
|
||||
"""
|
||||
llm = self._get_llm()
|
||||
|
||||
prompt = self.ANALYSIS_PROMPT.format(context=context)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info("Analyzing context...")
|
||||
|
||||
response = await llm.get_response_async(prompt)
|
||||
|
||||
return response
|
||||
|
||||
def analyze_context_sync(
|
||||
self,
|
||||
context: str
|
||||
) -> str:
|
||||
"""
|
||||
Synchronous version of analyze_context.
|
||||
|
||||
Args:
|
||||
context: Context to analyze
|
||||
|
||||
Returns:
|
||||
Analysis string
|
||||
"""
|
||||
llm = self._get_llm()
|
||||
|
||||
prompt = self.ANALYSIS_PROMPT.format(context=context)
|
||||
|
||||
if self.verbose >= 2:
|
||||
logger.info("Analyzing context...")
|
||||
|
||||
response = llm.get_response(prompt)
|
||||
|
||||
return response
|
||||
|
||||
def is_tool_allowed(self, tool_name: str) -> bool:
|
||||
"""
|
||||
Check if a tool is allowed in planning mode.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to check
|
||||
|
||||
Returns:
|
||||
True if tool is allowed
|
||||
"""
|
||||
if not self.read_only:
|
||||
return True
|
||||
return tool_name in self.allowed_tools
|
||||
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
PlanStorage for persisting plans and todo lists.
|
||||
|
||||
Provides storage functionality similar to:
|
||||
- Cursor .cursor/plans/
|
||||
- Windsurf ~/.codeium/windsurf/brain/
|
||||
- Codex ~/.codex/sessions/
|
||||
|
||||
Features:
|
||||
- Save/load plans as markdown files
|
||||
- Save/load todo lists as JSON
|
||||
- Auto-discovery of existing plans
|
||||
- Plan versioning
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from .plan import Plan
|
||||
from .todo import TodoList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlanStorage:
|
||||
"""
|
||||
Storage manager for plans and todo lists.
|
||||
|
||||
Attributes:
|
||||
base_path: Base directory for storage
|
||||
plans_dir: Directory for plan files
|
||||
todos_dir: Directory for todo files
|
||||
"""
|
||||
|
||||
DEFAULT_BASE_PATH = ".praison"
|
||||
|
||||
def __init__(self, base_path: Optional[str] = None):
|
||||
"""
|
||||
Initialize PlanStorage.
|
||||
|
||||
Args:
|
||||
base_path: Base directory for storage. Defaults to .praison/
|
||||
"""
|
||||
self.base_path = base_path or self.DEFAULT_BASE_PATH
|
||||
self.plans_dir = os.path.join(self.base_path, "plans")
|
||||
self.todos_dir = os.path.join(self.base_path, "todos")
|
||||
|
||||
# Create directories if they don't exist
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self) -> None:
|
||||
"""Create storage directories if they don't exist."""
|
||||
os.makedirs(self.plans_dir, exist_ok=True)
|
||||
os.makedirs(self.todos_dir, exist_ok=True)
|
||||
|
||||
def _get_plan_filename(self, plan: Plan) -> str:
|
||||
"""
|
||||
Generate filename for a plan.
|
||||
|
||||
Args:
|
||||
plan: Plan to generate filename for
|
||||
|
||||
Returns:
|
||||
Filename string
|
||||
"""
|
||||
date_str = plan.created_at.strftime("%Y-%m-%d") if plan.created_at else datetime.now().strftime("%Y-%m-%d")
|
||||
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in plan.name[:30])
|
||||
return f"plan_{date_str}_{plan.id}_{safe_name}.md"
|
||||
|
||||
def _get_plan_path(self, plan_id: str) -> Optional[str]:
|
||||
"""
|
||||
Find the path for a plan by ID.
|
||||
|
||||
Args:
|
||||
plan_id: ID of the plan to find
|
||||
|
||||
Returns:
|
||||
Path to the plan file, or None if not found
|
||||
"""
|
||||
for filename in os.listdir(self.plans_dir):
|
||||
if plan_id in filename and filename.endswith(".md"):
|
||||
return os.path.join(self.plans_dir, filename)
|
||||
return None
|
||||
|
||||
def save_plan(self, plan: Plan, filename: Optional[str] = None) -> str:
|
||||
"""
|
||||
Save a plan to storage.
|
||||
|
||||
Args:
|
||||
plan: Plan to save
|
||||
filename: Optional custom filename
|
||||
|
||||
Returns:
|
||||
Path to the saved file
|
||||
"""
|
||||
if filename is None:
|
||||
filename = self._get_plan_filename(plan)
|
||||
|
||||
path = os.path.join(self.plans_dir, filename)
|
||||
|
||||
# Convert plan to markdown
|
||||
markdown = plan.to_markdown()
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown)
|
||||
|
||||
plan.file_path = path
|
||||
logger.debug(f"Saved plan to {path}")
|
||||
|
||||
return path
|
||||
|
||||
def load_plan(self, plan_id: str) -> Optional[Plan]:
|
||||
"""
|
||||
Load a plan from storage.
|
||||
|
||||
Args:
|
||||
plan_id: ID of the plan to load
|
||||
|
||||
Returns:
|
||||
Plan instance, or None if not found
|
||||
"""
|
||||
path = self._get_plan_path(plan_id)
|
||||
|
||||
if path is None or not os.path.exists(path):
|
||||
logger.debug(f"Plan {plan_id} not found")
|
||||
return None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
markdown = f.read()
|
||||
|
||||
plan = Plan.from_markdown(markdown)
|
||||
plan.file_path = path
|
||||
|
||||
logger.debug(f"Loaded plan from {path}")
|
||||
return plan
|
||||
|
||||
def load_plan_from_file(self, path: str) -> Optional[Plan]:
|
||||
"""
|
||||
Load a plan from a specific file path.
|
||||
|
||||
Args:
|
||||
path: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Plan instance, or None if file doesn't exist
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
markdown = f.read()
|
||||
|
||||
plan = Plan.from_markdown(markdown)
|
||||
plan.file_path = path
|
||||
|
||||
return plan
|
||||
|
||||
def list_plans(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all saved plans.
|
||||
|
||||
Returns:
|
||||
List of plan metadata dictionaries
|
||||
"""
|
||||
plans = []
|
||||
|
||||
for filename in os.listdir(self.plans_dir):
|
||||
if filename.endswith(".md"):
|
||||
path = os.path.join(self.plans_dir, filename)
|
||||
|
||||
# Try to load and get metadata
|
||||
try:
|
||||
plan = self.load_plan_from_file(path)
|
||||
if plan:
|
||||
plans.append({
|
||||
"id": plan.id,
|
||||
"name": plan.name,
|
||||
"status": plan.status,
|
||||
"created_at": plan.created_at.isoformat() if plan.created_at else None,
|
||||
"file_path": path
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load plan from {path}: {e}")
|
||||
|
||||
# Sort by creation date (newest first)
|
||||
plans.sort(key=lambda p: p.get("created_at", ""), reverse=True)
|
||||
|
||||
return plans
|
||||
|
||||
def delete_plan(self, plan_id: str) -> bool:
|
||||
"""
|
||||
Delete a plan from storage.
|
||||
|
||||
Args:
|
||||
plan_id: ID of the plan to delete
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
path = self._get_plan_path(plan_id)
|
||||
|
||||
if path and os.path.exists(path):
|
||||
os.remove(path)
|
||||
logger.debug(f"Deleted plan {plan_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def save_todo(self, todo: TodoList, name: str = "current") -> str:
|
||||
"""
|
||||
Save a todo list to storage.
|
||||
|
||||
Args:
|
||||
todo: TodoList to save
|
||||
name: Name for the todo file (without extension)
|
||||
|
||||
Returns:
|
||||
Path to the saved file
|
||||
"""
|
||||
filename = f"{name}.json"
|
||||
path = os.path.join(self.todos_dir, filename)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(todo.to_json())
|
||||
|
||||
logger.debug(f"Saved todo list to {path}")
|
||||
return path
|
||||
|
||||
def load_todo(self, name: str = "current") -> Optional[TodoList]:
|
||||
"""
|
||||
Load a todo list from storage.
|
||||
|
||||
Args:
|
||||
name: Name of the todo file (without extension)
|
||||
|
||||
Returns:
|
||||
TodoList instance, or None if not found
|
||||
"""
|
||||
filename = f"{name}.json"
|
||||
path = os.path.join(self.todos_dir, filename)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
json_str = f.read()
|
||||
|
||||
logger.debug(f"Loaded todo list from {path}")
|
||||
return TodoList.from_json(json_str)
|
||||
|
||||
def list_todos(self) -> List[str]:
|
||||
"""
|
||||
List all saved todo lists.
|
||||
|
||||
Returns:
|
||||
List of todo list names
|
||||
"""
|
||||
todos = []
|
||||
|
||||
for filename in os.listdir(self.todos_dir):
|
||||
if filename.endswith(".json"):
|
||||
name = filename[:-5] # Remove .json extension
|
||||
todos.append(name)
|
||||
|
||||
return todos
|
||||
|
||||
def delete_todo(self, name: str) -> bool:
|
||||
"""
|
||||
Delete a todo list from storage.
|
||||
|
||||
Args:
|
||||
name: Name of the todo file (without extension)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
filename = f"{name}.json"
|
||||
path = os.path.join(self.todos_dir, filename)
|
||||
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
logger.debug(f"Deleted todo list {name}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_latest_plan(self) -> Optional[Plan]:
|
||||
"""
|
||||
Get the most recently created plan.
|
||||
|
||||
Returns:
|
||||
Most recent Plan, or None if no plans exist
|
||||
"""
|
||||
plans = self.list_plans()
|
||||
|
||||
if not plans:
|
||||
return None
|
||||
|
||||
return self.load_plan(plans[0]["id"])
|
||||
|
||||
def cleanup_old_plans(self, keep_count: int = 10) -> int:
|
||||
"""
|
||||
Remove old plans, keeping only the most recent ones.
|
||||
|
||||
Args:
|
||||
keep_count: Number of plans to keep
|
||||
|
||||
Returns:
|
||||
Number of plans deleted
|
||||
"""
|
||||
plans = self.list_plans()
|
||||
|
||||
if len(plans) <= keep_count:
|
||||
return 0
|
||||
|
||||
deleted = 0
|
||||
for plan in plans[keep_count:]:
|
||||
if self.delete_plan(plan["id"]):
|
||||
deleted += 1
|
||||
|
||||
return deleted
|
||||
@@ -0,0 +1,415 @@
|
||||
"""
|
||||
TodoList and TodoItem classes for Planning Mode.
|
||||
|
||||
Provides task tracking functionality similar to:
|
||||
- Cursor Agent To-Dos
|
||||
- Windsurf Todo Lists
|
||||
- Claude Code TodoRead/TodoWrite
|
||||
|
||||
Features:
|
||||
- Create todo lists from plans
|
||||
- Track item completion
|
||||
- Dependencies between items
|
||||
- Markdown and JSON serialization
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any, Literal, Union, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .plan import Plan
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoItem:
|
||||
"""
|
||||
A single todo item.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier
|
||||
description: What needs to be done
|
||||
status: Current status (pending, in_progress, completed)
|
||||
dependencies: List of item IDs that must complete first
|
||||
agent: Name of the agent responsible
|
||||
priority: Priority level (low, medium, high)
|
||||
notes: Additional notes
|
||||
"""
|
||||
description: str
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
status: Literal["pending", "in_progress", "completed"] = "pending"
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
agent: Optional[str] = None
|
||||
priority: Literal["low", "medium", "high"] = "medium"
|
||||
notes: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert TodoItem to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"description": self.description,
|
||||
"status": self.status,
|
||||
"dependencies": self.dependencies,
|
||||
"agent": self.agent,
|
||||
"priority": self.priority,
|
||||
"notes": self.notes
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'TodoItem':
|
||||
"""Create TodoItem from dictionary."""
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())[:8]),
|
||||
description=data.get("description", ""),
|
||||
status=data.get("status", "pending"),
|
||||
dependencies=data.get("dependencies", []),
|
||||
agent=data.get("agent"),
|
||||
priority=data.get("priority", "medium"),
|
||||
notes=data.get("notes", "")
|
||||
)
|
||||
|
||||
def complete(self) -> None:
|
||||
"""Mark this item as completed."""
|
||||
self.status = "completed"
|
||||
|
||||
def start(self) -> None:
|
||||
"""Mark this item as in progress."""
|
||||
self.status = "in_progress"
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset this item to pending."""
|
||||
self.status = "pending"
|
||||
|
||||
def is_ready(self, completed_ids: List[str]) -> bool:
|
||||
"""
|
||||
Check if this item is ready to start.
|
||||
|
||||
Args:
|
||||
completed_ids: List of completed item IDs
|
||||
|
||||
Returns:
|
||||
True if all dependencies are satisfied
|
||||
"""
|
||||
if self.status != "pending":
|
||||
return False
|
||||
return all(dep in completed_ids for dep in self.dependencies)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoList:
|
||||
"""
|
||||
A list of todo items with tracking capabilities.
|
||||
|
||||
Attributes:
|
||||
items: List of TodoItem objects
|
||||
auto_update: Whether to auto-update from plan changes
|
||||
name: Optional name for the todo list
|
||||
"""
|
||||
items: List[TodoItem] = field(default_factory=list)
|
||||
auto_update: bool = True
|
||||
name: str = "Todo List"
|
||||
|
||||
def add(self, item: Union[TodoItem, str]) -> TodoItem:
|
||||
"""
|
||||
Add an item to the list.
|
||||
|
||||
Args:
|
||||
item: TodoItem or description string
|
||||
|
||||
Returns:
|
||||
The added TodoItem
|
||||
"""
|
||||
if isinstance(item, str):
|
||||
item = TodoItem(description=item)
|
||||
self.items.append(item)
|
||||
return item
|
||||
|
||||
def remove(self, item_id: str) -> bool:
|
||||
"""
|
||||
Remove an item from the list.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to remove
|
||||
|
||||
Returns:
|
||||
True if item was removed, False if not found
|
||||
"""
|
||||
for i, item in enumerate(self.items):
|
||||
if item.id == item_id:
|
||||
self.items.pop(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, item_id: str) -> Optional[TodoItem]:
|
||||
"""
|
||||
Get an item by ID.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to find
|
||||
|
||||
Returns:
|
||||
TodoItem if found, None otherwise
|
||||
"""
|
||||
for item in self.items:
|
||||
if item.id == item_id:
|
||||
return item
|
||||
return None
|
||||
|
||||
def complete(self, item_id: str) -> bool:
|
||||
"""
|
||||
Mark an item as completed.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to complete
|
||||
|
||||
Returns:
|
||||
True if item was completed, False if not found
|
||||
"""
|
||||
item = self.get(item_id)
|
||||
if item:
|
||||
item.complete()
|
||||
return True
|
||||
return False
|
||||
|
||||
def start(self, item_id: str) -> bool:
|
||||
"""
|
||||
Mark an item as in progress.
|
||||
|
||||
Args:
|
||||
item_id: ID of the item to start
|
||||
|
||||
Returns:
|
||||
True if item was started, False if not found
|
||||
"""
|
||||
item = self.get(item_id)
|
||||
if item:
|
||||
item.start()
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def pending(self) -> List[TodoItem]:
|
||||
"""Get all pending items."""
|
||||
return [item for item in self.items if item.status == "pending"]
|
||||
|
||||
@property
|
||||
def in_progress(self) -> List[TodoItem]:
|
||||
"""Get all in-progress items."""
|
||||
return [item for item in self.items if item.status == "in_progress"]
|
||||
|
||||
@property
|
||||
def completed(self) -> List[TodoItem]:
|
||||
"""Get all completed items."""
|
||||
return [item for item in self.items if item.status == "completed"]
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
"""
|
||||
Calculate completion progress.
|
||||
|
||||
Returns:
|
||||
Float between 0.0 and 1.0
|
||||
"""
|
||||
if not self.items:
|
||||
return 1.0
|
||||
return len(self.completed) / len(self.items)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if all items are completed."""
|
||||
return all(item.status == "completed" for item in self.items)
|
||||
|
||||
@property
|
||||
def completed_ids(self) -> List[str]:
|
||||
"""Get list of completed item IDs."""
|
||||
return [item.id for item in self.completed]
|
||||
|
||||
def get_ready_items(self) -> List[TodoItem]:
|
||||
"""
|
||||
Get items that are ready to start.
|
||||
|
||||
Returns:
|
||||
List of items with satisfied dependencies
|
||||
"""
|
||||
completed = self.completed_ids
|
||||
return [item for item in self.items if item.is_ready(completed)]
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""
|
||||
Convert TodoList to markdown format.
|
||||
|
||||
Returns:
|
||||
Markdown string with checkbox format
|
||||
"""
|
||||
lines = [f"# {self.name}", ""]
|
||||
|
||||
for item in self.items:
|
||||
checkbox = "[x]" if item.status == "completed" else "[ ]"
|
||||
lines.append(f"- {checkbox} {item.description}")
|
||||
|
||||
if item.agent:
|
||||
lines.append(f" - Agent: {item.agent}")
|
||||
if item.dependencies:
|
||||
lines.append(f" - Depends on: {', '.join(item.dependencies)}")
|
||||
if item.notes:
|
||||
lines.append(f" - Notes: {item.notes}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@classmethod
|
||||
def from_markdown(cls, markdown: str) -> 'TodoList':
|
||||
"""
|
||||
Create TodoList from markdown format.
|
||||
|
||||
Args:
|
||||
markdown: Markdown string with checkbox format
|
||||
|
||||
Returns:
|
||||
TodoList instance
|
||||
"""
|
||||
import re
|
||||
|
||||
items = []
|
||||
# Match checkbox items: - [ ] or - [x]
|
||||
pattern = r'^-\s*\[([ x])\]\s*(.+?)$'
|
||||
|
||||
for match in re.finditer(pattern, markdown, re.MULTILINE):
|
||||
checked = match.group(1) == 'x'
|
||||
description = match.group(2).strip()
|
||||
|
||||
item = TodoItem(
|
||||
description=description,
|
||||
status="completed" if checked else "pending"
|
||||
)
|
||||
items.append(item)
|
||||
|
||||
return cls(items=items)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""
|
||||
Serialize TodoList to JSON.
|
||||
|
||||
Returns:
|
||||
JSON string
|
||||
"""
|
||||
data = {
|
||||
"name": self.name,
|
||||
"auto_update": self.auto_update,
|
||||
"items": [item.to_dict() for item in self.items]
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> 'TodoList':
|
||||
"""
|
||||
Create TodoList from JSON.
|
||||
|
||||
Args:
|
||||
json_str: JSON string
|
||||
|
||||
Returns:
|
||||
TodoList instance
|
||||
"""
|
||||
data = json.loads(json_str)
|
||||
items = [TodoItem.from_dict(item) for item in data.get("items", [])]
|
||||
|
||||
return cls(
|
||||
items=items,
|
||||
auto_update=data.get("auto_update", True),
|
||||
name=data.get("name", "Todo List")
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert TodoList to dictionary."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"auto_update": self.auto_update,
|
||||
"items": [item.to_dict() for item in self.items]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'TodoList':
|
||||
"""Create TodoList from dictionary."""
|
||||
items = [TodoItem.from_dict(item) for item in data.get("items", [])]
|
||||
return cls(
|
||||
items=items,
|
||||
auto_update=data.get("auto_update", True),
|
||||
name=data.get("name", "Todo List")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_plan(cls, plan: 'Plan') -> 'TodoList':
|
||||
"""
|
||||
Create TodoList from a Plan.
|
||||
|
||||
Args:
|
||||
plan: Plan instance
|
||||
|
||||
Returns:
|
||||
TodoList with items matching plan steps
|
||||
"""
|
||||
items = []
|
||||
for step in plan.steps:
|
||||
item = TodoItem(
|
||||
id=step.id,
|
||||
description=step.description,
|
||||
status=step.status if step.status in ["pending", "in_progress", "completed"] else "pending",
|
||||
dependencies=step.dependencies,
|
||||
agent=step.agent
|
||||
)
|
||||
items.append(item)
|
||||
|
||||
return cls(
|
||||
items=items,
|
||||
name=f"Todo: {plan.name}"
|
||||
)
|
||||
|
||||
def sync_with_plan(self, plan: 'Plan') -> None:
|
||||
"""
|
||||
Synchronize TodoList with a Plan.
|
||||
|
||||
Updates item statuses to match plan step statuses.
|
||||
|
||||
Args:
|
||||
plan: Plan to sync with
|
||||
"""
|
||||
step_statuses = {step.id: step.status for step in plan.steps}
|
||||
|
||||
for item in self.items:
|
||||
if item.id in step_statuses:
|
||||
status = step_statuses[item.id]
|
||||
if status in ["pending", "in_progress", "completed"]:
|
||||
item.status = status
|
||||
|
||||
def update_from_plan(self, plan: 'Plan') -> None:
|
||||
"""
|
||||
Update TodoList to match Plan structure.
|
||||
|
||||
Adds new items for new steps, removes items for removed steps.
|
||||
|
||||
Args:
|
||||
plan: Plan to update from
|
||||
"""
|
||||
# Get current step IDs
|
||||
step_ids = {step.id for step in plan.steps}
|
||||
item_ids = {item.id for item in self.items}
|
||||
|
||||
# Remove items for deleted steps
|
||||
self.items = [item for item in self.items if item.id in step_ids]
|
||||
|
||||
# Add items for new steps
|
||||
for step in plan.steps:
|
||||
if step.id not in item_ids:
|
||||
self.add(TodoItem(
|
||||
id=step.id,
|
||||
description=step.description,
|
||||
dependencies=step.dependencies,
|
||||
agent=step.agent
|
||||
))
|
||||
|
||||
# Sync statuses
|
||||
self.sync_with_plan(plan)
|
||||
@@ -9,6 +9,85 @@ A tool is a piece of code that helps our AI agents perform specific tasks. Think
|
||||
- A stock market tool lets agents check stock prices
|
||||
- A weather tool lets agents check the weather
|
||||
|
||||
## Plugin System (NEW!)
|
||||
|
||||
PraisonAI now supports a plugin system for creating and distributing tools. External developers can create pip-installable tool packages!
|
||||
|
||||
### Quick Start
|
||||
|
||||
```python
|
||||
from praisonaiagents import BaseTool, tool, Agent
|
||||
|
||||
# Method 1: Class-based tool
|
||||
class WeatherTool(BaseTool):
|
||||
name = "get_weather"
|
||||
description = "Get weather for a location"
|
||||
|
||||
def run(self, location: str) -> dict:
|
||||
return {"temp": 72, "condition": "sunny"}
|
||||
|
||||
# Method 2: Decorator-based tool
|
||||
@tool
|
||||
def search(query: str) -> list:
|
||||
"""Search the web."""
|
||||
return [{"title": "Result", "url": "https://..."}]
|
||||
|
||||
# Use with Agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
tools=[WeatherTool(), search]
|
||||
)
|
||||
```
|
||||
|
||||
### Creating a Plugin Package
|
||||
|
||||
External developers can create pip-installable plugins:
|
||||
|
||||
**1. Create your tool package:**
|
||||
```
|
||||
praisonai-weather/
|
||||
├── pyproject.toml
|
||||
├── src/
|
||||
│ └── praisonai_weather/
|
||||
│ ├── __init__.py
|
||||
│ └── tools.py
|
||||
```
|
||||
|
||||
**2. Define your tool in `tools.py`:**
|
||||
```python
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class WeatherTool(BaseTool):
|
||||
name = "weather"
|
||||
description = "Get current weather"
|
||||
|
||||
def run(self, location: str) -> dict:
|
||||
# Your implementation
|
||||
return {"temp": 72}
|
||||
```
|
||||
|
||||
**3. Register via entry_points in `pyproject.toml`:**
|
||||
```toml
|
||||
[project]
|
||||
name = "praisonai-weather"
|
||||
version = "1.0.0"
|
||||
|
||||
[project.entry-points."praisonaiagents.tools"]
|
||||
weather = "praisonai_weather.tools:WeatherTool"
|
||||
```
|
||||
|
||||
**4. Users install and use:**
|
||||
```bash
|
||||
pip install praisonai-weather
|
||||
```
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Tool is auto-discovered!
|
||||
agent = Agent(tools=["weather"])
|
||||
```
|
||||
|
||||
## Creating New Tools: The Two Approaches
|
||||
|
||||
### 1. Function-Based Approach (Simple Tools)
|
||||
|
||||
@@ -173,6 +173,51 @@ TOOL_MAPPINGS = {
|
||||
'cot_save': ('.train.data.generatecot', 'GenerateCOT'), # Save single QA to file
|
||||
'cot_upload_to_huggingface': ('.train.data.generatecot', 'GenerateCOT'), # Upload dataset to HuggingFace
|
||||
'cot_tools': ('.train.data.generatecot', 'GenerateCOT'), # Full toolkit access
|
||||
|
||||
# Claude Memory Tool (Anthropic Beta)
|
||||
'ClaudeMemoryTool': ('.claude_memory_tool', 'ClaudeMemoryTool'),
|
||||
|
||||
# Tavily Tools (requires TAVILY_API_KEY)
|
||||
'tavily': ('.tavily_tools', None),
|
||||
'tavily_search': ('.tavily_tools', None),
|
||||
'tavily_extract': ('.tavily_tools', None),
|
||||
'tavily_crawl': ('.tavily_tools', None),
|
||||
'tavily_map': ('.tavily_tools', None),
|
||||
'tavily_search_async': ('.tavily_tools', None),
|
||||
'tavily_extract_async': ('.tavily_tools', None),
|
||||
'TavilyTools': ('.tavily_tools', 'TavilyTools'),
|
||||
'tavily_tools': ('.tavily_tools', None),
|
||||
|
||||
# You.com Tools (requires YDC_API_KEY)
|
||||
'ydc': ('.youdotcom_tools', None),
|
||||
'ydc_search': ('.youdotcom_tools', None),
|
||||
'ydc_contents': ('.youdotcom_tools', None),
|
||||
'ydc_news': ('.youdotcom_tools', None),
|
||||
'ydc_images': ('.youdotcom_tools', None),
|
||||
'YouTools': ('.youdotcom_tools', 'YouTools'),
|
||||
'youdotcom_tools': ('.youdotcom_tools', None),
|
||||
|
||||
# Exa Tools (requires EXA_API_KEY)
|
||||
'exa': ('.exa_tools', None),
|
||||
'exa_search': ('.exa_tools', None),
|
||||
'exa_search_contents': ('.exa_tools', None),
|
||||
'exa_find_similar': ('.exa_tools', None),
|
||||
'exa_answer': ('.exa_tools', None),
|
||||
'exa_search_async': ('.exa_tools', None),
|
||||
'exa_search_contents_async': ('.exa_tools', None),
|
||||
'exa_answer_async': ('.exa_tools', None),
|
||||
'ExaTools': ('.exa_tools', 'ExaTools'),
|
||||
'exa_tools': ('.exa_tools', None),
|
||||
|
||||
# Crawl4AI Tools (async web crawling)
|
||||
'crawl4ai': ('.crawl4ai_tools', None),
|
||||
'crawl4ai_many': ('.crawl4ai_tools', None),
|
||||
'crawl4ai_extract': ('.crawl4ai_tools', None),
|
||||
'crawl4ai_llm_extract': ('.crawl4ai_tools', None),
|
||||
'crawl4ai_sync': ('.crawl4ai_tools', None),
|
||||
'crawl4ai_extract_sync': ('.crawl4ai_tools', None),
|
||||
'Crawl4AITools': ('.crawl4ai_tools', 'Crawl4AITools'),
|
||||
'crawl4ai_tools': ('.crawl4ai_tools', None),
|
||||
}
|
||||
|
||||
_instances = {} # Cache for class instances
|
||||
@@ -184,6 +229,11 @@ def __getattr__(name: str) -> Any:
|
||||
|
||||
module_path, class_name = TOOL_MAPPINGS[name]
|
||||
|
||||
# Return class itself (not instance) for ClaudeMemoryTool, TavilyTools, YouTools, ExaTools, Crawl4AITools
|
||||
if name in ('ClaudeMemoryTool', 'TavilyTools', 'YouTools', 'ExaTools', 'Crawl4AITools'):
|
||||
module = import_module(module_path, __package__)
|
||||
return getattr(module, class_name)
|
||||
|
||||
if class_name is None:
|
||||
# Direct function import
|
||||
module = import_module(module_path, __package__)
|
||||
@@ -197,11 +247,18 @@ def __getattr__(name: str) -> Any:
|
||||
'insert_document', 'insert_documents', 'find_documents', 'update_document', 'delete_document',
|
||||
'create_vector_index', 'vector_search', 'store_with_embedding', 'text_search', 'get_stats', 'connect_mongodb',
|
||||
'execute_command', 'list_processes', 'kill_process', 'get_system_info',
|
||||
'evaluate', 'solve_equation', 'convert_units', 'calculate_statistics', 'calculate_financial'
|
||||
'evaluate', 'solve_equation', 'convert_units', 'calculate_statistics', 'calculate_financial',
|
||||
'tavily', 'tavily_search', 'tavily_extract', 'tavily_crawl', 'tavily_map',
|
||||
'tavily_search_async', 'tavily_extract_async',
|
||||
'ydc', 'ydc_search', 'ydc_contents', 'ydc_news', 'ydc_images',
|
||||
'exa', 'exa_search', 'exa_search_contents', 'exa_find_similar', 'exa_answer',
|
||||
'exa_search_async', 'exa_search_contents_async', 'exa_answer_async',
|
||||
'crawl4ai', 'crawl4ai_many', 'crawl4ai_extract', 'crawl4ai_llm_extract',
|
||||
'crawl4ai_sync', 'crawl4ai_extract_sync'
|
||||
]:
|
||||
return getattr(module, name)
|
||||
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
|
||||
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools']:
|
||||
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools']:
|
||||
return module # Returns the callable module
|
||||
return getattr(module, name)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
"""Base classes for PraisonAI Agent tools.
|
||||
|
||||
This module provides the foundation for creating tools that can be used by agents.
|
||||
External developers can create plugins by subclassing BaseTool.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "Does something useful"
|
||||
|
||||
def run(self, query: str) -> str:
|
||||
return f"Result for {query}"
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Type, get_type_hints
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
|
||||
class ToolValidationError(Exception):
|
||||
"""Raised when a tool fails validation."""
|
||||
pass
|
||||
|
||||
|
||||
class ToolResult:
|
||||
"""Wrapper for tool execution results."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
output: Any,
|
||||
success: bool = True,
|
||||
error: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
self.output = output
|
||||
self.success = success
|
||||
self.error = error
|
||||
self.metadata = metadata or {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.success:
|
||||
return str(self.output)
|
||||
return f"Error: {self.error}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ToolResult(success={self.success}, output={self.output!r})"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"output": self.output,
|
||||
"success": self.success,
|
||||
"error": self.error,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
|
||||
class BaseTool(ABC):
|
||||
"""Abstract base class for all PraisonAI tools.
|
||||
|
||||
Subclass this to create custom tools that can be:
|
||||
- Used directly by agents
|
||||
- Distributed as pip-installable plugins
|
||||
- Auto-discovered via entry_points
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the tool
|
||||
description: Human-readable description (used by LLM)
|
||||
version: Tool version string (default: "1.0.0")
|
||||
parameters: JSON Schema for parameters (auto-generated if not provided)
|
||||
|
||||
Example:
|
||||
class WeatherTool(BaseTool):
|
||||
name = "get_weather"
|
||||
description = "Get current weather for a location"
|
||||
|
||||
def run(self, location: str, units: str = "celsius") -> dict:
|
||||
# Implementation here
|
||||
return {"temp": 22, "condition": "sunny"}
|
||||
"""
|
||||
|
||||
# Required class attributes (must be overridden)
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
|
||||
# Optional class attributes
|
||||
version: str = "1.0.0"
|
||||
parameters: Optional[Dict[str, Any]] = None # JSON Schema, auto-generated if None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the tool and validate configuration."""
|
||||
if not self.name:
|
||||
# Use class name as default
|
||||
self.name = self.__class__.__name__.lower().replace("tool", "")
|
||||
|
||||
if not self.description:
|
||||
# Use docstring as default
|
||||
self.description = self.__class__.__doc__ or f"Tool: {self.name}"
|
||||
|
||||
# Auto-generate parameters schema if not provided
|
||||
if self.parameters is None:
|
||||
self.parameters = self._generate_parameters_schema()
|
||||
|
||||
def _generate_parameters_schema(self) -> Dict[str, Any]:
|
||||
"""Generate JSON Schema from run() method signature."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
try:
|
||||
sig = inspect.signature(self.run)
|
||||
hints = get_type_hints(self.run) if hasattr(self.run, '__annotations__') else {}
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name == 'self':
|
||||
continue
|
||||
|
||||
# Get type hint
|
||||
param_type = hints.get(param_name, Any)
|
||||
json_type = self._python_type_to_json(param_type)
|
||||
|
||||
# Build property schema
|
||||
prop_schema = {"type": json_type}
|
||||
|
||||
# Add description from docstring if available
|
||||
# (Could parse docstring for param descriptions)
|
||||
|
||||
schema["properties"][param_name] = prop_schema
|
||||
|
||||
# Check if required (no default value)
|
||||
if param.default is inspect.Parameter.empty:
|
||||
schema["required"].append(param_name)
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not generate schema for {self.name}: {e}")
|
||||
|
||||
return schema
|
||||
|
||||
@staticmethod
|
||||
def _python_type_to_json(python_type: Type) -> str:
|
||||
"""Convert Python type to JSON Schema type."""
|
||||
type_map = {
|
||||
str: "string",
|
||||
int: "integer",
|
||||
float: "number",
|
||||
bool: "boolean",
|
||||
list: "array",
|
||||
dict: "object",
|
||||
type(None): "null"
|
||||
}
|
||||
|
||||
# Handle Optional, Union, etc.
|
||||
origin = getattr(python_type, '__origin__', None)
|
||||
if origin is not None:
|
||||
# For List[X], return "array"
|
||||
if origin is list:
|
||||
return "array"
|
||||
# For Dict[X, Y], return "object"
|
||||
if origin is dict:
|
||||
return "object"
|
||||
|
||||
return type_map.get(python_type, "string")
|
||||
|
||||
@abstractmethod
|
||||
def run(self, **kwargs) -> Any:
|
||||
"""Execute the tool with given arguments.
|
||||
|
||||
This method must be implemented by subclasses.
|
||||
|
||||
Args:
|
||||
**kwargs: Tool-specific arguments
|
||||
|
||||
Returns:
|
||||
Tool output (any type, will be converted to string for LLM)
|
||||
"""
|
||||
pass
|
||||
|
||||
def __call__(self, **kwargs) -> Any:
|
||||
"""Allow tool to be called directly like a function."""
|
||||
return self.run(**kwargs)
|
||||
|
||||
def safe_run(self, **kwargs) -> ToolResult:
|
||||
"""Execute tool with error handling, returning ToolResult."""
|
||||
try:
|
||||
output = self.run(**kwargs)
|
||||
return ToolResult(output=output, success=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Tool {self.name} failed: {e}")
|
||||
return ToolResult(
|
||||
output=None,
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def get_schema(self) -> Dict[str, Any]:
|
||||
"""Get OpenAI-compatible function schema for this tool."""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": self.parameters
|
||||
}
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}(name='{self.name}')"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(name='{self.name}', description='{self.description[:50]}...')"
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Validate the tool configuration.
|
||||
|
||||
Raises:
|
||||
ToolValidationError: If validation fails
|
||||
|
||||
Returns:
|
||||
True if validation passes
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check required fields
|
||||
if not self.name or not isinstance(self.name, str):
|
||||
errors.append("Tool must have a non-empty string 'name'")
|
||||
|
||||
if not self.description or not isinstance(self.description, str):
|
||||
errors.append("Tool must have a non-empty string 'description'")
|
||||
|
||||
# Check that run() is implemented (not abstract)
|
||||
if getattr(self.run, '__isabstractmethod__', False):
|
||||
errors.append("Tool must implement the 'run()' method")
|
||||
|
||||
# Check parameters schema is valid
|
||||
if self.parameters:
|
||||
if not isinstance(self.parameters, dict):
|
||||
errors.append("'parameters' must be a dictionary")
|
||||
elif "type" not in self.parameters:
|
||||
errors.append("'parameters' must have a 'type' field")
|
||||
|
||||
if errors:
|
||||
raise ToolValidationError(f"Tool '{self.name}' validation failed: {'; '.join(errors)}")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def validate_class(cls) -> bool:
|
||||
"""Validate a tool class before instantiation.
|
||||
|
||||
This can be used to check if a class is a valid tool without creating an instance.
|
||||
|
||||
Returns:
|
||||
True if the class appears to be a valid tool
|
||||
"""
|
||||
# Check it's a subclass of BaseTool
|
||||
if not issubclass(cls, BaseTool):
|
||||
return False
|
||||
|
||||
# Check it has required class attributes or will get them from __init__
|
||||
# (name and description can be set in __init__ so we can't strictly require them)
|
||||
|
||||
# Check run() is defined (not just inherited abstract)
|
||||
if 'run' not in cls.__dict__:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_tool(tool: Any) -> bool:
|
||||
"""Validate any tool-like object.
|
||||
|
||||
Args:
|
||||
tool: Object to validate (BaseTool, callable, etc.)
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
|
||||
Raises:
|
||||
ToolValidationError: If validation fails
|
||||
"""
|
||||
if isinstance(tool, BaseTool):
|
||||
return tool.validate()
|
||||
|
||||
if callable(tool):
|
||||
# For plain functions, check they have a name
|
||||
name = getattr(tool, '__name__', None) or getattr(tool, 'name', None)
|
||||
if not name:
|
||||
raise ToolValidationError("Callable tool must have a __name__ or name attribute")
|
||||
return True
|
||||
|
||||
raise ToolValidationError(f"Invalid tool type: {type(tool)}")
|
||||
|
||||
|
||||
# For backward compatibility - tools can also just be functions
|
||||
# The @tool decorator (in decorator.py) wraps functions into BaseTool instances
|
||||
@@ -0,0 +1,529 @@
|
||||
"""
|
||||
Claude Memory Tool - File-based persistent memory for Claude models.
|
||||
|
||||
This module implements Anthropic's Memory Tool API (beta) which enables Claude
|
||||
to store and retrieve information across conversations through a memory file directory.
|
||||
|
||||
The memory tool operates client-side - you control where and how the data is stored.
|
||||
|
||||
Beta Header: context-management-2025-06-27
|
||||
Tool Type: memory_20250818
|
||||
|
||||
Supported Models:
|
||||
- Claude Sonnet 4.5 (claude-sonnet-4-5-20250929)
|
||||
- Claude Sonnet 4 (claude-sonnet-4-20250514)
|
||||
- Claude Haiku 4.5 (claude-haiku-4-5-20251001)
|
||||
- Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
- Claude Opus 4.1 (claude-opus-4-1-20250805)
|
||||
- Claude Opus 4 (claude-opus-4-20250514)
|
||||
|
||||
Reference: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/memory-tool
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Memory system prompt that Claude uses to manage memory
|
||||
MEMORY_SYSTEM_PROMPT = """
|
||||
IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.
|
||||
|
||||
MEMORY PROTOCOL:
|
||||
1. Use the `view` command of your `memory` tool to check for earlier progress.
|
||||
2. ... (work on the task) ...
|
||||
- As you make progress, record status / progress / thoughts etc in your memory.
|
||||
|
||||
ASSUME INTERRUPTION: Your context window might be reset at any moment, so you risk losing any progress that is not recorded in your memory directory.
|
||||
|
||||
When editing your memory folder, always try to keep its content up-to-date, coherent and organized. You can rename or delete files that are no longer relevant. Do not create new files unless necessary.
|
||||
"""
|
||||
|
||||
|
||||
class ClaudeMemoryTool:
|
||||
"""
|
||||
File-based memory tool for Claude models.
|
||||
|
||||
Implements Anthropic's Memory Tool API (beta). Claude autonomously decides
|
||||
what to store and retrieve from the memory directory.
|
||||
|
||||
Storage: {base_path}/memories/
|
||||
|
||||
Commands:
|
||||
- view: Show directory contents or file contents with optional line ranges
|
||||
- create: Create or overwrite a file
|
||||
- str_replace: Replace text in a file
|
||||
- insert: Insert text at a specific line
|
||||
- delete: Delete a file or directory
|
||||
- rename: Rename or move a file/directory
|
||||
|
||||
Example:
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import ClaudeMemoryTool
|
||||
|
||||
# Use default storage
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
claude_memory=True
|
||||
)
|
||||
|
||||
# Or with custom storage path
|
||||
memory = ClaudeMemoryTool(base_path="./my_project/memory")
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
claude_memory=memory
|
||||
)
|
||||
```
|
||||
|
||||
Security:
|
||||
- All paths are validated to prevent directory traversal attacks
|
||||
- Paths must start with /memories
|
||||
- Symlinks are not followed outside the memory directory
|
||||
"""
|
||||
|
||||
# Beta header required for memory tool
|
||||
BETA_HEADER = "context-management-2025-06-27"
|
||||
|
||||
# Tool definition for API requests
|
||||
TOOL_DEFINITION = {
|
||||
"type": "memory_20250818",
|
||||
"name": "memory"
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_path: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
max_file_size: int = 1024 * 1024, # 1MB default
|
||||
max_files: int = 100
|
||||
):
|
||||
"""
|
||||
Initialize the Claude Memory Tool.
|
||||
|
||||
Args:
|
||||
base_path: Base directory for memory storage.
|
||||
Defaults to .praison/claude_memory/{user_id}
|
||||
user_id: User identifier for user-specific memory.
|
||||
Defaults to "default"
|
||||
max_file_size: Maximum size per file in bytes (default 1MB)
|
||||
max_files: Maximum number of files allowed (default 100)
|
||||
"""
|
||||
self.user_id = user_id or "default"
|
||||
|
||||
if base_path:
|
||||
self.base_path = Path(base_path)
|
||||
else:
|
||||
self.base_path = Path(f".praison/claude_memory/{self.user_id}")
|
||||
|
||||
self.memory_root = self.base_path / "memories"
|
||||
self.max_file_size = max_file_size
|
||||
self.max_files = max_files
|
||||
|
||||
# Create memory directory
|
||||
self.memory_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.debug(f"ClaudeMemoryTool initialized at {self.memory_root}")
|
||||
|
||||
def _validate_path(self, path: str) -> Path:
|
||||
"""
|
||||
Validate and resolve memory paths securely.
|
||||
|
||||
Args:
|
||||
path: Path string starting with /memories
|
||||
|
||||
Returns:
|
||||
Resolved Path object within memory directory
|
||||
|
||||
Raises:
|
||||
ValueError: If path is invalid or attempts directory traversal
|
||||
"""
|
||||
if not path:
|
||||
raise ValueError("Path cannot be empty")
|
||||
|
||||
if not path.startswith("/memories"):
|
||||
raise ValueError(f"Path must start with /memories, got: {path}")
|
||||
|
||||
# Remove /memories prefix and leading slashes
|
||||
relative_path = path[len("/memories"):].lstrip("/")
|
||||
|
||||
# Resolve the full path
|
||||
if relative_path:
|
||||
full_path = self.memory_root / relative_path
|
||||
else:
|
||||
full_path = self.memory_root
|
||||
|
||||
# Security check: ensure resolved path is within memory_root
|
||||
try:
|
||||
resolved = full_path.resolve()
|
||||
resolved.relative_to(self.memory_root.resolve())
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Path {path} would escape /memories directory") from e
|
||||
|
||||
# Check for suspicious patterns
|
||||
if ".." in path or "~" in path:
|
||||
raise ValueError(f"Invalid path pattern in: {path}")
|
||||
|
||||
return full_path
|
||||
|
||||
def execute(self, command: str, **kwargs) -> str:
|
||||
"""
|
||||
Execute a memory command.
|
||||
|
||||
Args:
|
||||
command: Command name (view, create, str_replace, insert, delete, rename)
|
||||
**kwargs: Command-specific arguments
|
||||
|
||||
Returns:
|
||||
Result string from the command
|
||||
|
||||
Raises:
|
||||
ValueError: If command is unknown
|
||||
"""
|
||||
commands = {
|
||||
"view": self.view,
|
||||
"create": self.create,
|
||||
"str_replace": self.str_replace,
|
||||
"insert": self.insert,
|
||||
"delete": self.delete,
|
||||
"rename": self.rename
|
||||
}
|
||||
|
||||
if command not in commands:
|
||||
raise ValueError(f"Unknown command: {command}. Valid commands: {list(commands.keys())}")
|
||||
|
||||
return commands[command](**kwargs)
|
||||
|
||||
def view(self, path: str, view_range: Optional[List[int]] = None) -> str:
|
||||
"""
|
||||
Show directory contents or file contents with optional line ranges.
|
||||
|
||||
Args:
|
||||
path: Path to view (e.g., "/memories" or "/memories/notes.txt")
|
||||
view_range: Optional [start_line, end_line] for file viewing.
|
||||
Use -1 for end_line to read to end of file.
|
||||
|
||||
Returns:
|
||||
Directory listing or file contents with line numbers
|
||||
|
||||
Raises:
|
||||
RuntimeError: If path cannot be read
|
||||
"""
|
||||
full_path = self._validate_path(path)
|
||||
|
||||
if full_path.is_dir():
|
||||
# List directory contents
|
||||
items: List[str] = []
|
||||
try:
|
||||
for item in sorted(full_path.iterdir()):
|
||||
# Skip hidden files
|
||||
if item.name.startswith("."):
|
||||
continue
|
||||
# Add trailing slash for directories
|
||||
items.append(f"{item.name}/" if item.is_dir() else item.name)
|
||||
|
||||
result = f"Directory: {path}\n"
|
||||
if items:
|
||||
result += "\n".join([f"- {item}" for item in items])
|
||||
else:
|
||||
result += "(empty directory)"
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Cannot read directory {path}: {e}") from e
|
||||
|
||||
elif full_path.is_file():
|
||||
# Read file contents
|
||||
try:
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
|
||||
# Apply view range if specified
|
||||
if view_range:
|
||||
start_line = max(1, view_range[0]) - 1 # Convert to 0-indexed
|
||||
end_line = len(lines) if view_range[1] == -1 else view_range[1]
|
||||
lines = lines[start_line:end_line]
|
||||
start_num = start_line + 1
|
||||
else:
|
||||
start_num = 1
|
||||
|
||||
# Add line numbers
|
||||
numbered_lines = [
|
||||
f"{i + start_num:4d}: {line}"
|
||||
for i, line in enumerate(lines)
|
||||
]
|
||||
return "\n".join(numbered_lines) if numbered_lines else "(empty file)"
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Cannot read file {path}: {e}") from e
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Path not found: {path}")
|
||||
|
||||
def create(self, path: str, file_text: str) -> str:
|
||||
"""
|
||||
Create or overwrite a file.
|
||||
|
||||
Args:
|
||||
path: Path for the new file (e.g., "/memories/notes.txt")
|
||||
file_text: Content to write to the file
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Raises:
|
||||
ValueError: If file would exceed size limit or max files reached
|
||||
"""
|
||||
full_path = self._validate_path(path)
|
||||
|
||||
# Check file size limit
|
||||
if len(file_text.encode('utf-8')) > self.max_file_size:
|
||||
raise ValueError(
|
||||
f"File content exceeds maximum size of {self.max_file_size} bytes"
|
||||
)
|
||||
|
||||
# Check max files limit (only for new files)
|
||||
if not full_path.exists():
|
||||
current_files = sum(1 for _ in self.memory_root.rglob("*") if _.is_file())
|
||||
if current_files >= self.max_files:
|
||||
raise ValueError(
|
||||
f"Maximum number of files ({self.max_files}) reached. "
|
||||
"Delete some files before creating new ones."
|
||||
)
|
||||
|
||||
# Create parent directories if needed
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write file
|
||||
full_path.write_text(file_text, encoding="utf-8")
|
||||
|
||||
return f"File created successfully at {path}"
|
||||
|
||||
def str_replace(self, path: str, old_str: str, new_str: str) -> str:
|
||||
"""
|
||||
Replace text in a file.
|
||||
|
||||
Args:
|
||||
path: Path to the file
|
||||
old_str: Text to find (must be unique in file)
|
||||
new_str: Replacement text
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If file doesn't exist
|
||||
ValueError: If old_str not found or appears multiple times
|
||||
"""
|
||||
full_path = self._validate_path(path)
|
||||
|
||||
if not full_path.is_file():
|
||||
raise FileNotFoundError(f"File not found: {path}")
|
||||
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
|
||||
# Count occurrences
|
||||
count = content.count(old_str)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(f"Text not found in {path}")
|
||||
elif count > 1:
|
||||
raise ValueError(
|
||||
f"Text appears {count} times in {path}. Must be unique for replacement."
|
||||
)
|
||||
|
||||
# Replace and write
|
||||
new_content = content.replace(old_str, new_str)
|
||||
|
||||
# Check size limit
|
||||
if len(new_content.encode('utf-8')) > self.max_file_size:
|
||||
raise ValueError(
|
||||
f"Resulting file would exceed maximum size of {self.max_file_size} bytes"
|
||||
)
|
||||
|
||||
full_path.write_text(new_content, encoding="utf-8")
|
||||
|
||||
return f"File {path} has been edited"
|
||||
|
||||
def insert(self, path: str, insert_line: int, insert_text: str) -> str:
|
||||
"""
|
||||
Insert text at a specific line.
|
||||
|
||||
Args:
|
||||
path: Path to the file
|
||||
insert_line: Line number to insert at (0-indexed)
|
||||
insert_text: Text to insert
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If file doesn't exist
|
||||
ValueError: If line number is invalid
|
||||
"""
|
||||
full_path = self._validate_path(path)
|
||||
|
||||
if not full_path.is_file():
|
||||
raise FileNotFoundError(f"File not found: {path}")
|
||||
|
||||
lines = full_path.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
# Validate line number
|
||||
if insert_line < 0 or insert_line > len(lines):
|
||||
raise ValueError(
|
||||
f"Invalid insert_line {insert_line}. Must be 0-{len(lines)}"
|
||||
)
|
||||
|
||||
# Insert the text (strip trailing newline to avoid double newlines)
|
||||
lines.insert(insert_line, insert_text.rstrip("\n"))
|
||||
|
||||
# Reconstruct content
|
||||
new_content = "\n".join(lines) + "\n"
|
||||
|
||||
# Check size limit
|
||||
if len(new_content.encode('utf-8')) > self.max_file_size:
|
||||
raise ValueError(
|
||||
f"Resulting file would exceed maximum size of {self.max_file_size} bytes"
|
||||
)
|
||||
|
||||
full_path.write_text(new_content, encoding="utf-8")
|
||||
|
||||
return f"Text inserted at line {insert_line} in {path}"
|
||||
|
||||
def delete(self, path: str) -> str:
|
||||
"""
|
||||
Delete a file or directory.
|
||||
|
||||
Args:
|
||||
path: Path to delete
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Raises:
|
||||
ValueError: If trying to delete /memories root
|
||||
FileNotFoundError: If path doesn't exist
|
||||
"""
|
||||
# Prevent deleting the root memories directory
|
||||
if path == "/memories" or path == "/memories/":
|
||||
raise ValueError("Cannot delete the /memories directory itself")
|
||||
|
||||
full_path = self._validate_path(path)
|
||||
|
||||
if full_path.is_file():
|
||||
full_path.unlink()
|
||||
return f"File deleted: {path}"
|
||||
elif full_path.is_dir():
|
||||
shutil.rmtree(full_path)
|
||||
return f"Directory deleted: {path}"
|
||||
else:
|
||||
raise FileNotFoundError(f"Path not found: {path}")
|
||||
|
||||
def rename(self, old_path: str, new_path: str) -> str:
|
||||
"""
|
||||
Rename or move a file/directory.
|
||||
|
||||
Args:
|
||||
old_path: Current path
|
||||
new_path: New path
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If source doesn't exist
|
||||
ValueError: If destination already exists
|
||||
"""
|
||||
old_full_path = self._validate_path(old_path)
|
||||
new_full_path = self._validate_path(new_path)
|
||||
|
||||
if not old_full_path.exists():
|
||||
raise FileNotFoundError(f"Source path not found: {old_path}")
|
||||
|
||||
if new_full_path.exists():
|
||||
raise ValueError(f"Destination already exists: {new_path}")
|
||||
|
||||
# Create parent directories if needed
|
||||
new_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Rename/move
|
||||
old_full_path.rename(new_full_path)
|
||||
|
||||
return f"Renamed {old_path} to {new_path}"
|
||||
|
||||
def clear_all(self) -> str:
|
||||
"""
|
||||
Clear all memory files.
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
"""
|
||||
if self.memory_root.exists():
|
||||
shutil.rmtree(self.memory_root)
|
||||
self.memory_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return "All memory cleared"
|
||||
|
||||
def get_tool_definition(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the tool definition for API requests.
|
||||
|
||||
Returns:
|
||||
Tool definition dict for Anthropic API
|
||||
"""
|
||||
return self.TOOL_DEFINITION.copy()
|
||||
|
||||
def get_beta_header(self) -> str:
|
||||
"""
|
||||
Get the beta header value for API requests.
|
||||
|
||||
Returns:
|
||||
Beta header string
|
||||
"""
|
||||
return self.BETA_HEADER
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
"""
|
||||
Get the memory system prompt to append to agent instructions.
|
||||
|
||||
Returns:
|
||||
Memory protocol system prompt
|
||||
"""
|
||||
return MEMORY_SYSTEM_PROMPT
|
||||
|
||||
def process_tool_call(self, tool_input: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Process a memory tool call from Claude.
|
||||
|
||||
Args:
|
||||
tool_input: The input dict from Claude's tool_use block
|
||||
Contains 'command' and command-specific parameters
|
||||
|
||||
Returns:
|
||||
Result string to return to Claude
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Claude sends:
|
||||
# {"command": "view", "path": "/memories"}
|
||||
result = memory_tool.process_tool_call(tool_input)
|
||||
```
|
||||
"""
|
||||
command = tool_input.get("command")
|
||||
if not command:
|
||||
return "Error: No command specified in tool input"
|
||||
|
||||
try:
|
||||
# Extract command-specific parameters
|
||||
params = {k: v for k, v in tool_input.items() if k != "command"}
|
||||
return self.execute(command, **params)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ClaudeMemoryTool(base_path='{self.base_path}', user_id='{self.user_id}')"
|
||||
@@ -0,0 +1,630 @@
|
||||
"""Crawl4AI web crawling and extraction tools.
|
||||
|
||||
Crawl4AI provides async web crawling with JavaScript rendering, content extraction,
|
||||
and LLM-based data extraction capabilities.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import crawl4ai, crawl4ai_extract
|
||||
|
||||
# Simple crawl
|
||||
result = await crawl4ai("https://example.com")
|
||||
print(result["markdown"])
|
||||
|
||||
# Extract structured data
|
||||
result = await crawl4ai_extract(
|
||||
"https://example.com/products",
|
||||
schema={"name": "Products", "baseSelector": ".product", ...}
|
||||
)
|
||||
|
||||
# Or use the class directly
|
||||
from praisonaiagents.tools import Crawl4AITools
|
||||
tools = Crawl4AITools()
|
||||
result = await tools.crawl("https://example.com")
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import logging
|
||||
import asyncio
|
||||
from importlib import util
|
||||
|
||||
|
||||
def _check_crawl4ai_available() -> tuple[bool, Optional[str]]:
|
||||
"""Check if Crawl4AI is available.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, error_message)
|
||||
"""
|
||||
if util.find_spec("crawl4ai") is None:
|
||||
return False, "crawl4ai package is not installed. Install it with: pip install crawl4ai && crawl4ai-setup"
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
class Crawl4AITools:
|
||||
"""Crawl4AI web crawling and extraction tools.
|
||||
|
||||
Provides methods for:
|
||||
- crawl: Basic web crawling with markdown output
|
||||
- crawl_many: Crawl multiple URLs concurrently
|
||||
- extract_css: Extract data using CSS selectors
|
||||
- extract_llm: Extract data using LLM
|
||||
|
||||
Example:
|
||||
from praisonaiagents.tools import Crawl4AITools
|
||||
|
||||
tools = Crawl4AITools()
|
||||
result = await tools.crawl("https://example.com")
|
||||
print(result["markdown"])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
headless: bool = True,
|
||||
verbose: bool = False,
|
||||
browser_type: str = "chromium"
|
||||
):
|
||||
"""Initialize Crawl4AITools.
|
||||
|
||||
Args:
|
||||
headless: Run browser in headless mode (default True)
|
||||
verbose: Enable verbose logging (default False)
|
||||
browser_type: Browser type - "chromium", "firefox", or "webkit"
|
||||
"""
|
||||
self.headless = headless
|
||||
self.verbose = verbose
|
||||
self.browser_type = browser_type
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._crawler = None
|
||||
|
||||
async def _get_crawler(self):
|
||||
"""Get or create AsyncWebCrawler instance."""
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
raise ImportError(error)
|
||||
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig
|
||||
|
||||
if self._crawler is None:
|
||||
browser_config = BrowserConfig(
|
||||
headless=self.headless,
|
||||
verbose=self.verbose,
|
||||
browser_type=self.browser_type
|
||||
)
|
||||
self._crawler = AsyncWebCrawler(config=browser_config)
|
||||
await self._crawler.start()
|
||||
|
||||
return self._crawler
|
||||
|
||||
async def close(self):
|
||||
"""Close the crawler and release resources."""
|
||||
if self._crawler is not None:
|
||||
await self._crawler.close()
|
||||
self._crawler = None
|
||||
|
||||
async def crawl(
|
||||
self,
|
||||
url: str,
|
||||
word_count_threshold: int = 10,
|
||||
css_selector: Optional[str] = None,
|
||||
excluded_tags: Optional[List[str]] = None,
|
||||
exclude_external_links: bool = False,
|
||||
js_code: Optional[Union[str, List[str]]] = None,
|
||||
wait_for: Optional[str] = None,
|
||||
screenshot: bool = False,
|
||||
pdf: bool = False,
|
||||
bypass_cache: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Crawl a single URL and return content.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
word_count_threshold: Minimum words per text block (default 10)
|
||||
css_selector: CSS selector to focus on specific content
|
||||
excluded_tags: HTML tags to exclude (e.g., ["nav", "footer"])
|
||||
exclude_external_links: Remove external links from content
|
||||
js_code: JavaScript code to execute before crawling
|
||||
wait_for: Wait condition - "css:selector" or "js:() => boolean"
|
||||
screenshot: Capture screenshot (base64)
|
||||
pdf: Generate PDF
|
||||
bypass_cache: Skip cache and fetch fresh content (default True)
|
||||
|
||||
Returns:
|
||||
Dict with 'markdown', 'html', 'cleaned_html', 'links', 'media', etc.
|
||||
"""
|
||||
try:
|
||||
crawler = await self._get_crawler()
|
||||
|
||||
from crawl4ai import CrawlerRunConfig, CacheMode
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
word_count_threshold=word_count_threshold,
|
||||
css_selector=css_selector,
|
||||
excluded_tags=excluded_tags or [],
|
||||
exclude_external_links=exclude_external_links,
|
||||
js_code=js_code,
|
||||
wait_for=wait_for,
|
||||
screenshot=screenshot,
|
||||
pdf=pdf,
|
||||
cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED
|
||||
)
|
||||
|
||||
result = await crawler.arun(url=url, config=config)
|
||||
|
||||
if not result.success:
|
||||
return {"error": f"Crawl failed: {result.error_message}", "success": False}
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"url": result.url,
|
||||
"markdown": result.markdown.raw_markdown if hasattr(result.markdown, 'raw_markdown') else str(result.markdown),
|
||||
"html": result.html,
|
||||
"cleaned_html": result.cleaned_html,
|
||||
"links": result.links,
|
||||
"media": result.media,
|
||||
"metadata": result.metadata
|
||||
}
|
||||
|
||||
if hasattr(result.markdown, 'fit_markdown') and result.markdown.fit_markdown:
|
||||
response["fit_markdown"] = result.markdown.fit_markdown
|
||||
|
||||
if screenshot and result.screenshot:
|
||||
response["screenshot"] = result.screenshot
|
||||
|
||||
if pdf and result.pdf:
|
||||
response["pdf"] = result.pdf
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Crawl4AI crawl error: {e}")
|
||||
return {"error": f"Crawl4AI crawl error: {str(e)}", "success": False}
|
||||
|
||||
async def crawl_many(
|
||||
self,
|
||||
urls: List[str],
|
||||
word_count_threshold: int = 10,
|
||||
css_selector: Optional[str] = None,
|
||||
bypass_cache: bool = True,
|
||||
stream: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Crawl multiple URLs concurrently.
|
||||
|
||||
Args:
|
||||
urls: List of URLs to crawl
|
||||
word_count_threshold: Minimum words per text block
|
||||
css_selector: CSS selector to focus on specific content
|
||||
bypass_cache: Skip cache and fetch fresh content
|
||||
stream: If True, returns async generator
|
||||
|
||||
Returns:
|
||||
List of crawl results
|
||||
"""
|
||||
try:
|
||||
crawler = await self._get_crawler()
|
||||
|
||||
from crawl4ai import CrawlerRunConfig, CacheMode
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
word_count_threshold=word_count_threshold,
|
||||
css_selector=css_selector,
|
||||
cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED,
|
||||
stream=stream
|
||||
)
|
||||
|
||||
results = []
|
||||
|
||||
if stream:
|
||||
async for result in await crawler.arun_many(urls, config=config):
|
||||
if result.success:
|
||||
results.append({
|
||||
"success": True,
|
||||
"url": result.url,
|
||||
"markdown": result.markdown.raw_markdown if hasattr(result.markdown, 'raw_markdown') else str(result.markdown),
|
||||
"html": result.html
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"success": False,
|
||||
"url": result.url,
|
||||
"error": result.error_message
|
||||
})
|
||||
else:
|
||||
raw_results = await crawler.arun_many(urls, config=config)
|
||||
for result in raw_results:
|
||||
if result.success:
|
||||
results.append({
|
||||
"success": True,
|
||||
"url": result.url,
|
||||
"markdown": result.markdown.raw_markdown if hasattr(result.markdown, 'raw_markdown') else str(result.markdown),
|
||||
"html": result.html
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"success": False,
|
||||
"url": result.url,
|
||||
"error": result.error_message
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Crawl4AI crawl_many error: {e}")
|
||||
return [{"error": f"Crawl4AI crawl_many error: {str(e)}", "success": False}]
|
||||
|
||||
async def extract_css(
|
||||
self,
|
||||
url: str,
|
||||
schema: Dict[str, Any],
|
||||
js_code: Optional[Union[str, List[str]]] = None,
|
||||
wait_for: Optional[str] = None,
|
||||
bypass_cache: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract structured data using CSS selectors.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl (or "raw://<html>" for raw HTML)
|
||||
schema: Extraction schema with baseSelector and fields
|
||||
js_code: JavaScript to execute before extraction
|
||||
wait_for: Wait condition before extraction
|
||||
bypass_cache: Skip cache
|
||||
|
||||
Returns:
|
||||
Dict with 'data' containing extracted items
|
||||
|
||||
Example schema:
|
||||
{
|
||||
"name": "Products",
|
||||
"baseSelector": "div.product",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2", "type": "text"},
|
||||
{"name": "price", "selector": ".price", "type": "text"},
|
||||
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
crawler = await self._get_crawler()
|
||||
|
||||
from crawl4ai import CrawlerRunConfig, CacheMode
|
||||
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
|
||||
|
||||
extraction_strategy = JsonCssExtractionStrategy(schema, verbose=self.verbose)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
extraction_strategy=extraction_strategy,
|
||||
js_code=js_code,
|
||||
wait_for=wait_for,
|
||||
cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED
|
||||
)
|
||||
|
||||
result = await crawler.arun(url=url, config=config)
|
||||
|
||||
if not result.success:
|
||||
return {"error": f"Extraction failed: {result.error_message}", "success": False}
|
||||
|
||||
import json
|
||||
try:
|
||||
data = json.loads(result.extracted_content) if result.extracted_content else []
|
||||
except json.JSONDecodeError:
|
||||
data = result.extracted_content
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"url": result.url,
|
||||
"data": data,
|
||||
"count": len(data) if isinstance(data, list) else 1
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Crawl4AI extract_css error: {e}")
|
||||
return {"error": f"Crawl4AI extract_css error: {str(e)}", "success": False}
|
||||
|
||||
async def extract_llm(
|
||||
self,
|
||||
url: str,
|
||||
instruction: str,
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
provider: str = "openai/gpt-4o-mini",
|
||||
api_token: Optional[str] = None,
|
||||
js_code: Optional[Union[str, List[str]]] = None,
|
||||
wait_for: Optional[str] = None,
|
||||
bypass_cache: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract structured data using LLM.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
instruction: Instructions for the LLM on what to extract
|
||||
schema: Optional Pydantic-style JSON schema for structured output
|
||||
provider: LLM provider string (e.g., "openai/gpt-4o-mini", "ollama/llama3.3")
|
||||
api_token: API token for the provider (uses env var if not provided)
|
||||
js_code: JavaScript to execute before extraction
|
||||
wait_for: Wait condition before extraction
|
||||
bypass_cache: Skip cache
|
||||
|
||||
Returns:
|
||||
Dict with 'data' containing extracted content
|
||||
"""
|
||||
try:
|
||||
crawler = await self._get_crawler()
|
||||
|
||||
import os
|
||||
from crawl4ai import CrawlerRunConfig, CacheMode, LLMConfig
|
||||
from crawl4ai.extraction_strategy import LLMExtractionStrategy
|
||||
|
||||
# Get API token from env if not provided
|
||||
if api_token is None and "openai" in provider.lower():
|
||||
api_token = os.environ.get("OPENAI_API_KEY")
|
||||
|
||||
llm_config = LLMConfig(provider=provider, api_token=api_token)
|
||||
|
||||
extraction_strategy = LLMExtractionStrategy(
|
||||
llm_config=llm_config,
|
||||
schema=schema,
|
||||
extraction_type="schema" if schema else "block",
|
||||
instruction=instruction,
|
||||
verbose=self.verbose
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
extraction_strategy=extraction_strategy,
|
||||
js_code=js_code,
|
||||
wait_for=wait_for,
|
||||
cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED
|
||||
)
|
||||
|
||||
result = await crawler.arun(url=url, config=config)
|
||||
|
||||
if not result.success:
|
||||
return {"error": f"LLM extraction failed: {result.error_message}", "success": False}
|
||||
|
||||
import json
|
||||
try:
|
||||
data = json.loads(result.extracted_content) if result.extracted_content else None
|
||||
except json.JSONDecodeError:
|
||||
data = result.extracted_content
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"url": result.url,
|
||||
"data": data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Crawl4AI extract_llm error: {e}")
|
||||
return {"error": f"Crawl4AI extract_llm error: {str(e)}", "success": False}
|
||||
|
||||
|
||||
# Standalone async functions for easy access
|
||||
|
||||
async def crawl4ai(
|
||||
url: str,
|
||||
css_selector: Optional[str] = None,
|
||||
js_code: Optional[str] = None,
|
||||
wait_for: Optional[str] = None,
|
||||
screenshot: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Crawl a URL and return markdown content.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
css_selector: CSS selector to focus on specific content
|
||||
js_code: JavaScript to execute before crawling
|
||||
wait_for: Wait condition - "css:selector" or "js:() => boolean"
|
||||
screenshot: Capture screenshot
|
||||
|
||||
Returns:
|
||||
Dict with 'markdown', 'html', 'links', 'media', etc.
|
||||
|
||||
Example:
|
||||
result = await crawl4ai("https://example.com")
|
||||
print(result["markdown"])
|
||||
"""
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error, "success": False}
|
||||
|
||||
tools = Crawl4AITools()
|
||||
try:
|
||||
result = await tools.crawl(
|
||||
url=url,
|
||||
css_selector=css_selector,
|
||||
js_code=js_code,
|
||||
wait_for=wait_for,
|
||||
screenshot=screenshot
|
||||
)
|
||||
return result
|
||||
finally:
|
||||
await tools.close()
|
||||
|
||||
|
||||
async def crawl4ai_many(
|
||||
urls: List[str],
|
||||
css_selector: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Crawl multiple URLs concurrently.
|
||||
|
||||
Args:
|
||||
urls: List of URLs to crawl
|
||||
css_selector: CSS selector to focus on specific content
|
||||
|
||||
Returns:
|
||||
List of crawl results
|
||||
|
||||
Example:
|
||||
results = await crawl4ai_many(["https://example.com", "https://example.org"])
|
||||
"""
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return [{"error": error, "success": False}]
|
||||
|
||||
tools = Crawl4AITools()
|
||||
try:
|
||||
return await tools.crawl_many(urls=urls, css_selector=css_selector)
|
||||
finally:
|
||||
await tools.close()
|
||||
|
||||
|
||||
async def crawl4ai_extract(
|
||||
url: str,
|
||||
schema: Dict[str, Any],
|
||||
js_code: Optional[str] = None,
|
||||
wait_for: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract structured data using CSS selectors.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
schema: Extraction schema with baseSelector and fields
|
||||
js_code: JavaScript to execute before extraction
|
||||
wait_for: Wait condition before extraction
|
||||
|
||||
Returns:
|
||||
Dict with 'data' containing extracted items
|
||||
|
||||
Example:
|
||||
schema = {
|
||||
"name": "Products",
|
||||
"baseSelector": "div.product",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2", "type": "text"},
|
||||
{"name": "price", "selector": ".price", "type": "text"}
|
||||
]
|
||||
}
|
||||
result = await crawl4ai_extract("https://example.com/products", schema)
|
||||
"""
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error, "success": False}
|
||||
|
||||
tools = Crawl4AITools()
|
||||
try:
|
||||
return await tools.extract_css(
|
||||
url=url,
|
||||
schema=schema,
|
||||
js_code=js_code,
|
||||
wait_for=wait_for
|
||||
)
|
||||
finally:
|
||||
await tools.close()
|
||||
|
||||
|
||||
async def crawl4ai_llm_extract(
|
||||
url: str,
|
||||
instruction: str,
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
provider: str = "openai/gpt-4o-mini"
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract data using LLM.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
instruction: Instructions for the LLM
|
||||
schema: Optional JSON schema for structured output
|
||||
provider: LLM provider (default "openai/gpt-4o-mini")
|
||||
|
||||
Returns:
|
||||
Dict with 'data' containing extracted content
|
||||
|
||||
Example:
|
||||
result = await crawl4ai_llm_extract(
|
||||
"https://example.com/about",
|
||||
instruction="Extract company name, founding year, and description"
|
||||
)
|
||||
"""
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error, "success": False}
|
||||
|
||||
tools = Crawl4AITools()
|
||||
try:
|
||||
return await tools.extract_llm(
|
||||
url=url,
|
||||
instruction=instruction,
|
||||
schema=schema,
|
||||
provider=provider
|
||||
)
|
||||
finally:
|
||||
await tools.close()
|
||||
|
||||
|
||||
# Synchronous wrappers for convenience
|
||||
|
||||
def crawl4ai_sync(
|
||||
url: str,
|
||||
css_selector: Optional[str] = None,
|
||||
js_code: Optional[str] = None,
|
||||
wait_for: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Synchronous version of crawl4ai.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
css_selector: CSS selector to focus on specific content
|
||||
js_code: JavaScript to execute
|
||||
wait_for: Wait condition
|
||||
|
||||
Returns:
|
||||
Dict with crawl results
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
crawl4ai(url, css_selector, js_code, wait_for)
|
||||
)
|
||||
|
||||
|
||||
def crawl4ai_extract_sync(
|
||||
url: str,
|
||||
schema: Dict[str, Any],
|
||||
js_code: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Synchronous version of crawl4ai_extract.
|
||||
|
||||
Args:
|
||||
url: The URL to crawl
|
||||
schema: Extraction schema
|
||||
js_code: JavaScript to execute
|
||||
|
||||
Returns:
|
||||
Dict with extracted data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
crawl4ai_extract(url, schema, js_code)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("Crawl4AI Tools - Example Usage")
|
||||
print("=" * 60)
|
||||
|
||||
is_available, error = _check_crawl4ai_available()
|
||||
if not is_available:
|
||||
print(f"\nCrawl4AI is not available: {error}")
|
||||
print("\nTo use Crawl4AI tools:")
|
||||
print("1. Install the package: pip install crawl4ai")
|
||||
print("2. Run setup: crawl4ai-setup")
|
||||
else:
|
||||
print("Crawl4AI is available!")
|
||||
|
||||
# Example crawl
|
||||
print("\n1. Basic Crawl")
|
||||
print("-" * 30)
|
||||
result = await crawl4ai("https://example.com")
|
||||
if result.get("success"):
|
||||
print(f"URL: {result['url']}")
|
||||
print(f"Markdown length: {len(result.get('markdown', ''))}")
|
||||
print(f"Links found: {len(result.get('links', {}).get('internal', []))}")
|
||||
else:
|
||||
print(f"Error: {result.get('error')}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demonstration Complete")
|
||||
print("=" * 60)
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Tool decorator for converting functions into tools.
|
||||
|
||||
This module provides the @tool decorator for easily creating tools from functions.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool
|
||||
def search(query: str) -> list:
|
||||
'''Search the web for information.'''
|
||||
return [...]
|
||||
|
||||
# Or with explicit parameters:
|
||||
@tool(name="web_search", description="Search the internet")
|
||||
def search(query: str, max_results: int = 5) -> list:
|
||||
return [...]
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Optional, Union, get_type_hints
|
||||
|
||||
from .base import BaseTool
|
||||
|
||||
|
||||
class FunctionTool(BaseTool):
|
||||
"""A BaseTool wrapper for plain functions.
|
||||
|
||||
Created automatically by the @tool decorator.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
version: str = "1.0.0"
|
||||
):
|
||||
self._func = func
|
||||
self.name = name or func.__name__
|
||||
self.description = description or func.__doc__ or f"Tool: {self.name}"
|
||||
self.version = version
|
||||
|
||||
# Generate schema from the original function, not run()
|
||||
self.parameters = self._generate_schema_from_func(func)
|
||||
|
||||
# Copy function metadata
|
||||
functools.update_wrapper(self, func)
|
||||
|
||||
# Skip parent's schema generation since we already did it
|
||||
# Just set defaults that parent would set
|
||||
if not self.name:
|
||||
self.name = self.__class__.__name__.lower().replace("tool", "")
|
||||
if not self.description:
|
||||
self.description = self.__class__.__doc__ or f"Tool: {self.name}"
|
||||
|
||||
def _generate_schema_from_func(self, func: Callable) -> Dict[str, Any]:
|
||||
"""Generate JSON Schema from the wrapped function's signature."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name in ('self', 'cls'):
|
||||
continue
|
||||
|
||||
# Get type hint
|
||||
param_type = hints.get(param_name, Any)
|
||||
json_type = BaseTool._python_type_to_json(param_type)
|
||||
|
||||
schema["properties"][param_name] = {"type": json_type}
|
||||
|
||||
# Check if required (no default value)
|
||||
if param.default is inspect.Parameter.empty:
|
||||
schema["required"].append(param_name)
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not generate schema for {func.__name__}: {e}")
|
||||
|
||||
return schema
|
||||
|
||||
def run(self, **kwargs) -> Any:
|
||||
"""Execute the wrapped function."""
|
||||
return self._func(**kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs) -> Any:
|
||||
"""Allow calling with positional args like the original function."""
|
||||
return self._func(*args, **kwargs)
|
||||
|
||||
|
||||
def tool(
|
||||
func: Optional[Callable] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
version: str = "1.0.0"
|
||||
) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]:
|
||||
"""Decorator to convert a function into a tool.
|
||||
|
||||
Can be used with or without arguments:
|
||||
|
||||
@tool
|
||||
def my_func(x: str) -> str:
|
||||
'''Does something.'''
|
||||
return x
|
||||
|
||||
@tool(name="custom_name", description="Custom description")
|
||||
def my_func(x: str) -> str:
|
||||
return x
|
||||
|
||||
Args:
|
||||
func: The function to wrap (when used without parentheses)
|
||||
name: Override the tool name (default: function name)
|
||||
description: Override description (default: function docstring)
|
||||
version: Tool version (default: "1.0.0")
|
||||
|
||||
Returns:
|
||||
FunctionTool instance that wraps the function
|
||||
"""
|
||||
def decorator(fn: Callable) -> FunctionTool:
|
||||
tool_instance = FunctionTool(
|
||||
func=fn,
|
||||
name=name,
|
||||
description=description,
|
||||
version=version
|
||||
)
|
||||
|
||||
# Register with global registry if available
|
||||
try:
|
||||
from .registry import get_registry
|
||||
registry = get_registry()
|
||||
if registry:
|
||||
registry.register(tool_instance)
|
||||
except ImportError:
|
||||
pass # Registry not yet available
|
||||
|
||||
return tool_instance
|
||||
|
||||
# Handle both @tool and @tool(...) syntax
|
||||
if func is not None:
|
||||
# Called without parentheses: @tool
|
||||
return decorator(func)
|
||||
else:
|
||||
# Called with parentheses: @tool(...)
|
||||
return decorator
|
||||
|
||||
|
||||
def is_tool(obj: Any) -> bool:
|
||||
"""Check if an object is a tool (BaseTool instance or decorated function)."""
|
||||
if isinstance(obj, BaseTool):
|
||||
return True
|
||||
if isinstance(obj, FunctionTool):
|
||||
return True
|
||||
# Check for tools created by other frameworks
|
||||
if hasattr(obj, 'run') and hasattr(obj, 'name'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_tool_schema(obj: Any) -> Optional[Dict[str, Any]]:
|
||||
"""Get OpenAI-compatible schema for any tool-like object.
|
||||
|
||||
Supports:
|
||||
- BaseTool instances
|
||||
- FunctionTool instances
|
||||
- Plain functions (generates schema from signature)
|
||||
- LangChain tools
|
||||
- CrewAI tools
|
||||
"""
|
||||
# BaseTool or FunctionTool
|
||||
if isinstance(obj, BaseTool):
|
||||
return obj.get_schema()
|
||||
|
||||
# Plain callable
|
||||
if callable(obj):
|
||||
return _schema_from_function(obj)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _schema_from_function(func: Callable) -> Dict[str, Any]:
|
||||
"""Generate OpenAI function schema from a plain function."""
|
||||
name = getattr(func, '__name__', 'unknown')
|
||||
description = func.__doc__ or f"Function: {name}"
|
||||
|
||||
# Build parameters schema
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name == 'self':
|
||||
continue
|
||||
|
||||
param_type = hints.get(param_name, Any)
|
||||
json_type = BaseTool._python_type_to_json(param_type)
|
||||
|
||||
properties[param_name] = {"type": json_type}
|
||||
|
||||
if param.default is inspect.Parameter.empty:
|
||||
required.append(param_name)
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not generate schema for {name}: {e}")
|
||||
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": description.strip(),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
"""Exa search and research tools.
|
||||
|
||||
Exa provides AI-powered web search, content retrieval, and research capabilities.
|
||||
This module requires the EXA_API_KEY environment variable to be set.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import exa_search, exa_search_contents
|
||||
|
||||
# Search the web
|
||||
results = exa_search("AI startups")
|
||||
|
||||
# Search with contents
|
||||
results = exa_search_contents("AI in healthcare", text=True)
|
||||
|
||||
# Or use the class directly
|
||||
from praisonaiagents.tools import ExaTools
|
||||
exa = ExaTools()
|
||||
results = exa.search("AI news")
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import logging
|
||||
import os
|
||||
from importlib import util
|
||||
|
||||
|
||||
def _check_exa_available() -> tuple[bool, Optional[str]]:
|
||||
"""Check if Exa is available and API key is set.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, error_message)
|
||||
"""
|
||||
# Check if exa_py package is installed
|
||||
if util.find_spec("exa_py") is None:
|
||||
return False, "exa_py package is not installed. Install it with: pip install exa_py"
|
||||
|
||||
# Check if API key is set
|
||||
if not os.environ.get("EXA_API_KEY"):
|
||||
return False, "EXA_API_KEY environment variable is not set. Please set it to use Exa tools."
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
class ExaTools:
|
||||
"""Exa search and research tools.
|
||||
|
||||
Provides methods for:
|
||||
- search: Basic web search
|
||||
- search_and_contents: Search with full text/highlights
|
||||
- find_similar: Find similar pages to a URL
|
||||
- find_similar_and_contents: Find similar with content
|
||||
- answer: Get AI-generated answers with citations
|
||||
|
||||
Example:
|
||||
from praisonaiagents.tools import ExaTools
|
||||
|
||||
exa = ExaTools()
|
||||
results = exa.search("AI startups", num_results=5)
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""Initialize ExaTools.
|
||||
|
||||
Args:
|
||||
api_key: Exa API key. If not provided, uses EXA_API_KEY env var.
|
||||
"""
|
||||
self.api_key = api_key or os.environ.get("EXA_API_KEY")
|
||||
self._client = None
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _get_client(self):
|
||||
"""Get or create Exa client."""
|
||||
if self._client is None:
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
raise ImportError(error)
|
||||
|
||||
from exa_py import Exa
|
||||
self._client = Exa(self.api_key)
|
||||
return self._client
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
num_results: int = 10,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
start_crawl_date: Optional[str] = None,
|
||||
end_crawl_date: Optional[str] = None,
|
||||
start_published_date: Optional[str] = None,
|
||||
end_published_date: Optional[str] = None,
|
||||
include_text: Optional[List[str]] = None,
|
||||
exclude_text: Optional[List[str]] = None,
|
||||
additional_queries: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Perform an Exa search.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
num_results: Number of results to return (default 10, max 100)
|
||||
type: Search type - "auto", "neural", "fast", or "deep"
|
||||
category: Data category - company, research paper, news, linkedin profile,
|
||||
github, tweet, movie, song, personal site, pdf, financial report
|
||||
include_domains: List of domains to include
|
||||
exclude_domains: List of domains to exclude
|
||||
start_crawl_date: Only include links crawled after this date
|
||||
end_crawl_date: Only include links crawled before this date
|
||||
start_published_date: Only include links published after this date
|
||||
end_published_date: Only include links published before this date
|
||||
include_text: Strings that must be present in results
|
||||
exclude_text: Strings that must not be present in results
|
||||
additional_queries: Additional query variations for deep search
|
||||
|
||||
Returns:
|
||||
Dict with 'results' list and optional 'autopromptString'
|
||||
"""
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
kwargs = {
|
||||
"query": query,
|
||||
"num_results": num_results
|
||||
}
|
||||
|
||||
if type:
|
||||
kwargs["type"] = type
|
||||
if category:
|
||||
kwargs["category"] = category
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if start_crawl_date:
|
||||
kwargs["start_crawl_date"] = start_crawl_date
|
||||
if end_crawl_date:
|
||||
kwargs["end_crawl_date"] = end_crawl_date
|
||||
if start_published_date:
|
||||
kwargs["start_published_date"] = start_published_date
|
||||
if end_published_date:
|
||||
kwargs["end_published_date"] = end_published_date
|
||||
if include_text:
|
||||
kwargs["include_text"] = include_text
|
||||
if exclude_text:
|
||||
kwargs["exclude_text"] = exclude_text
|
||||
if additional_queries:
|
||||
kwargs["additional_queries"] = additional_queries
|
||||
|
||||
response = client.search(**kwargs)
|
||||
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"url": r.url,
|
||||
"id": r.id,
|
||||
"title": getattr(r, "title", None),
|
||||
"published_date": getattr(r, "published_date", None),
|
||||
"author": getattr(r, "author", None)
|
||||
}
|
||||
for r in response.results
|
||||
],
|
||||
"autopromptString": getattr(response, "autoprompt_string", None),
|
||||
"requestId": getattr(response, "request_id", None)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exa search error: {e}")
|
||||
return {"error": f"Exa search error: {str(e)}"}
|
||||
|
||||
def search_and_contents(
|
||||
self,
|
||||
query: str,
|
||||
text: Union[bool, Dict] = True,
|
||||
highlights: Union[bool, Dict] = False,
|
||||
summary: Optional[Dict] = None,
|
||||
num_results: int = 10,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
start_crawl_date: Optional[str] = None,
|
||||
end_crawl_date: Optional[str] = None,
|
||||
start_published_date: Optional[str] = None,
|
||||
end_published_date: Optional[str] = None,
|
||||
include_text: Optional[List[str]] = None,
|
||||
exclude_text: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Search and retrieve content from results.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
text: Include full text content (True or TextContentsOptions dict)
|
||||
highlights: Include highlights (True or HighlightsContentsOptions dict)
|
||||
summary: Summary options with optional schema for structured output
|
||||
num_results: Number of results to return
|
||||
type: Search type - "auto", "neural", "fast", or "deep"
|
||||
category: Data category filter
|
||||
include_domains: List of domains to include
|
||||
exclude_domains: List of domains to exclude
|
||||
start_crawl_date: Only include links crawled after this date
|
||||
end_crawl_date: Only include links crawled before this date
|
||||
start_published_date: Only include links published after this date
|
||||
end_published_date: Only include links published before this date
|
||||
include_text: Strings that must be present in results
|
||||
exclude_text: Strings that must not be present in results
|
||||
|
||||
Returns:
|
||||
Dict with 'results' list containing text/highlights
|
||||
"""
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
kwargs = {
|
||||
"query": query,
|
||||
"num_results": num_results
|
||||
}
|
||||
|
||||
if text:
|
||||
kwargs["text"] = text
|
||||
if highlights:
|
||||
kwargs["highlights"] = highlights
|
||||
if summary:
|
||||
kwargs["summary"] = summary
|
||||
if type:
|
||||
kwargs["type"] = type
|
||||
if category:
|
||||
kwargs["category"] = category
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if start_crawl_date:
|
||||
kwargs["start_crawl_date"] = start_crawl_date
|
||||
if end_crawl_date:
|
||||
kwargs["end_crawl_date"] = end_crawl_date
|
||||
if start_published_date:
|
||||
kwargs["start_published_date"] = start_published_date
|
||||
if end_published_date:
|
||||
kwargs["end_published_date"] = end_published_date
|
||||
if include_text:
|
||||
kwargs["include_text"] = include_text
|
||||
if exclude_text:
|
||||
kwargs["exclude_text"] = exclude_text
|
||||
|
||||
response = client.search_and_contents(**kwargs)
|
||||
|
||||
results = []
|
||||
for r in response.results:
|
||||
result = {
|
||||
"url": r.url,
|
||||
"id": r.id,
|
||||
"title": getattr(r, "title", None),
|
||||
"published_date": getattr(r, "published_date", None),
|
||||
"author": getattr(r, "author", None)
|
||||
}
|
||||
if hasattr(r, "text") and r.text:
|
||||
result["text"] = r.text
|
||||
if hasattr(r, "highlights") and r.highlights:
|
||||
result["highlights"] = r.highlights
|
||||
if hasattr(r, "highlight_scores") and r.highlight_scores:
|
||||
result["highlight_scores"] = r.highlight_scores
|
||||
if hasattr(r, "summary") and r.summary:
|
||||
result["summary"] = r.summary
|
||||
results.append(result)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"requestId": getattr(response, "request_id", None)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exa search_and_contents error: {e}")
|
||||
return {"error": f"Exa search_and_contents error: {str(e)}"}
|
||||
|
||||
def find_similar(
|
||||
self,
|
||||
url: str,
|
||||
num_results: int = 10,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
start_crawl_date: Optional[str] = None,
|
||||
end_crawl_date: Optional[str] = None,
|
||||
start_published_date: Optional[str] = None,
|
||||
end_published_date: Optional[str] = None,
|
||||
exclude_source_domain: bool = False,
|
||||
category: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Find similar pages to a given URL.
|
||||
|
||||
Args:
|
||||
url: The URL to find similar pages for
|
||||
num_results: Number of results to return
|
||||
include_domains: List of domains to include
|
||||
exclude_domains: List of domains to exclude
|
||||
start_crawl_date: Only include links crawled after this date
|
||||
end_crawl_date: Only include links crawled before this date
|
||||
start_published_date: Only include links published after this date
|
||||
end_published_date: Only include links published before this date
|
||||
exclude_source_domain: Exclude results from the same domain
|
||||
category: Data category filter
|
||||
|
||||
Returns:
|
||||
Dict with 'results' list of similar pages
|
||||
"""
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
kwargs = {
|
||||
"url": url,
|
||||
"num_results": num_results
|
||||
}
|
||||
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if start_crawl_date:
|
||||
kwargs["start_crawl_date"] = start_crawl_date
|
||||
if end_crawl_date:
|
||||
kwargs["end_crawl_date"] = end_crawl_date
|
||||
if start_published_date:
|
||||
kwargs["start_published_date"] = start_published_date
|
||||
if end_published_date:
|
||||
kwargs["end_published_date"] = end_published_date
|
||||
if exclude_source_domain:
|
||||
kwargs["exclude_source_domain"] = exclude_source_domain
|
||||
if category:
|
||||
kwargs["category"] = category
|
||||
|
||||
response = client.find_similar(**kwargs)
|
||||
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"url": r.url,
|
||||
"id": r.id,
|
||||
"title": getattr(r, "title", None),
|
||||
"published_date": getattr(r, "published_date", None),
|
||||
"author": getattr(r, "author", None)
|
||||
}
|
||||
for r in response.results
|
||||
],
|
||||
"requestId": getattr(response, "request_id", None)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exa find_similar error: {e}")
|
||||
return {"error": f"Exa find_similar error: {str(e)}"}
|
||||
|
||||
def find_similar_and_contents(
|
||||
self,
|
||||
url: str,
|
||||
text: Union[bool, Dict] = True,
|
||||
highlights: Union[bool, Dict] = False,
|
||||
num_results: int = 10,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
start_crawl_date: Optional[str] = None,
|
||||
end_crawl_date: Optional[str] = None,
|
||||
start_published_date: Optional[str] = None,
|
||||
end_published_date: Optional[str] = None,
|
||||
exclude_source_domain: bool = False,
|
||||
category: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Find similar pages with content.
|
||||
|
||||
Args:
|
||||
url: The URL to find similar pages for
|
||||
text: Include full text content
|
||||
highlights: Include highlights
|
||||
num_results: Number of results to return
|
||||
include_domains: List of domains to include
|
||||
exclude_domains: List of domains to exclude
|
||||
start_crawl_date: Only include links crawled after this date
|
||||
end_crawl_date: Only include links crawled before this date
|
||||
start_published_date: Only include links published after this date
|
||||
end_published_date: Only include links published before this date
|
||||
exclude_source_domain: Exclude results from the same domain
|
||||
category: Data category filter
|
||||
|
||||
Returns:
|
||||
Dict with 'results' list containing text/highlights
|
||||
"""
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
kwargs = {
|
||||
"url": url,
|
||||
"num_results": num_results
|
||||
}
|
||||
|
||||
if text:
|
||||
kwargs["text"] = text
|
||||
if highlights:
|
||||
kwargs["highlights"] = highlights
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if start_crawl_date:
|
||||
kwargs["start_crawl_date"] = start_crawl_date
|
||||
if end_crawl_date:
|
||||
kwargs["end_crawl_date"] = end_crawl_date
|
||||
if start_published_date:
|
||||
kwargs["start_published_date"] = start_published_date
|
||||
if end_published_date:
|
||||
kwargs["end_published_date"] = end_published_date
|
||||
if exclude_source_domain:
|
||||
kwargs["exclude_source_domain"] = exclude_source_domain
|
||||
if category:
|
||||
kwargs["category"] = category
|
||||
|
||||
response = client.find_similar_and_contents(**kwargs)
|
||||
|
||||
results = []
|
||||
for r in response.results:
|
||||
result = {
|
||||
"url": r.url,
|
||||
"id": r.id,
|
||||
"title": getattr(r, "title", None),
|
||||
"published_date": getattr(r, "published_date", None),
|
||||
"author": getattr(r, "author", None)
|
||||
}
|
||||
if hasattr(r, "text") and r.text:
|
||||
result["text"] = r.text
|
||||
if hasattr(r, "highlights") and r.highlights:
|
||||
result["highlights"] = r.highlights
|
||||
if hasattr(r, "highlight_scores") and r.highlight_scores:
|
||||
result["highlight_scores"] = r.highlight_scores
|
||||
results.append(result)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"requestId": getattr(response, "request_id", None)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exa find_similar_and_contents error: {e}")
|
||||
return {"error": f"Exa find_similar_and_contents error: {str(e)}"}
|
||||
|
||||
def answer(
|
||||
self,
|
||||
query: str,
|
||||
text: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate an answer using Exa's search and LLM capabilities.
|
||||
|
||||
Args:
|
||||
query: The question to answer
|
||||
text: If True, include full text of citations
|
||||
|
||||
Returns:
|
||||
Dict with 'answer' and 'citations'
|
||||
"""
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
response = client.answer(query=query, text=text)
|
||||
|
||||
citations = []
|
||||
for c in response.citations:
|
||||
citation = {
|
||||
"id": c.id,
|
||||
"url": c.url,
|
||||
"title": getattr(c, "title", None),
|
||||
"published_date": getattr(c, "published_date", None),
|
||||
"author": getattr(c, "author", None)
|
||||
}
|
||||
if text and hasattr(c, "text") and c.text:
|
||||
citation["text"] = c.text
|
||||
citations.append(citation)
|
||||
|
||||
return {
|
||||
"answer": response.answer,
|
||||
"citations": citations
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exa answer error: {e}")
|
||||
return {"error": f"Exa answer error: {str(e)}"}
|
||||
|
||||
|
||||
# Standalone functions for easy access
|
||||
|
||||
def exa_search(
|
||||
query: str,
|
||||
num_results: int = 10,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
start_published_date: Optional[str] = None,
|
||||
end_published_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Search the web using Exa.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
num_results: Number of results (default 10)
|
||||
type: Search type - "auto", "neural", "fast", or "deep"
|
||||
category: Data category filter
|
||||
include_domains: Domains to include
|
||||
exclude_domains: Domains to exclude
|
||||
start_published_date: Only include links published after this date
|
||||
end_published_date: Only include links published before this date
|
||||
|
||||
Returns:
|
||||
Dict with search results or error
|
||||
|
||||
Example:
|
||||
results = exa_search("AI startups", num_results=5)
|
||||
"""
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
tools = ExaTools()
|
||||
return tools.search(
|
||||
query=query,
|
||||
num_results=num_results,
|
||||
type=type,
|
||||
category=category,
|
||||
include_domains=include_domains,
|
||||
exclude_domains=exclude_domains,
|
||||
start_published_date=start_published_date,
|
||||
end_published_date=end_published_date
|
||||
)
|
||||
|
||||
|
||||
def exa_search_contents(
|
||||
query: str,
|
||||
text: bool = True,
|
||||
highlights: bool = False,
|
||||
num_results: int = 10,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Search and get content from results.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
text: Include full text content
|
||||
highlights: Include highlights
|
||||
num_results: Number of results
|
||||
type: Search type
|
||||
category: Data category filter
|
||||
include_domains: Domains to include
|
||||
exclude_domains: Domains to exclude
|
||||
|
||||
Returns:
|
||||
Dict with results containing text/highlights
|
||||
|
||||
Example:
|
||||
results = exa_search_contents("AI in healthcare", text=True, highlights=True)
|
||||
"""
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
tools = ExaTools()
|
||||
return tools.search_and_contents(
|
||||
query=query,
|
||||
text=text,
|
||||
highlights=highlights,
|
||||
num_results=num_results,
|
||||
type=type,
|
||||
category=category,
|
||||
include_domains=include_domains,
|
||||
exclude_domains=exclude_domains
|
||||
)
|
||||
|
||||
|
||||
def exa_find_similar(
|
||||
url: str,
|
||||
num_results: int = 10,
|
||||
exclude_source_domain: bool = True,
|
||||
category: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Find pages similar to a given URL.
|
||||
|
||||
Args:
|
||||
url: The URL to find similar pages for
|
||||
num_results: Number of results
|
||||
exclude_source_domain: Exclude same domain results
|
||||
category: Data category filter
|
||||
|
||||
Returns:
|
||||
Dict with similar results
|
||||
|
||||
Example:
|
||||
results = exa_find_similar("https://openai.com", num_results=5)
|
||||
"""
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
tools = ExaTools()
|
||||
return tools.find_similar(
|
||||
url=url,
|
||||
num_results=num_results,
|
||||
exclude_source_domain=exclude_source_domain,
|
||||
category=category
|
||||
)
|
||||
|
||||
|
||||
def exa_answer(
|
||||
query: str,
|
||||
text: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Get an AI-generated answer with citations.
|
||||
|
||||
Args:
|
||||
query: The question to answer
|
||||
text: Include full text of citations
|
||||
|
||||
Returns:
|
||||
Dict with 'answer' and 'citations'
|
||||
|
||||
Example:
|
||||
result = exa_answer("What is the capital of France?")
|
||||
print(result["answer"])
|
||||
"""
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
tools = ExaTools()
|
||||
return tools.answer(query=query, text=text)
|
||||
|
||||
|
||||
# Async versions
|
||||
|
||||
async def exa_search_async(
|
||||
query: str,
|
||||
num_results: int = 10,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of exa_search.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
num_results: Number of results
|
||||
type: Search type
|
||||
category: Data category filter
|
||||
|
||||
Returns:
|
||||
Dict with search results
|
||||
"""
|
||||
import asyncio
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: exa_search(query, num_results, type, category)
|
||||
)
|
||||
|
||||
|
||||
async def exa_search_contents_async(
|
||||
query: str,
|
||||
text: bool = True,
|
||||
highlights: bool = False,
|
||||
num_results: int = 10
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of exa_search_contents.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
text: Include full text
|
||||
highlights: Include highlights
|
||||
num_results: Number of results
|
||||
|
||||
Returns:
|
||||
Dict with results containing content
|
||||
"""
|
||||
import asyncio
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: exa_search_contents(query, text, highlights, num_results)
|
||||
)
|
||||
|
||||
|
||||
async def exa_answer_async(
|
||||
query: str,
|
||||
text: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of exa_answer.
|
||||
|
||||
Args:
|
||||
query: The question to answer
|
||||
text: Include citation text
|
||||
|
||||
Returns:
|
||||
Dict with answer and citations
|
||||
"""
|
||||
import asyncio
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: exa_answer(query, text)
|
||||
)
|
||||
|
||||
|
||||
# Alias for simple usage
|
||||
exa = exa_search
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
print("=" * 60)
|
||||
print("Exa Tools - Example Usage")
|
||||
print("=" * 60)
|
||||
|
||||
is_available, error = _check_exa_available()
|
||||
if not is_available:
|
||||
print(f"\nExa is not available: {error}")
|
||||
print("\nTo use Exa tools:")
|
||||
print("1. Install the package: pip install exa_py")
|
||||
print("2. Set environment variable: export EXA_API_KEY=your_api_key")
|
||||
else:
|
||||
print("Exa SDK is available!")
|
||||
|
||||
# Example search
|
||||
print("\n1. Basic Search")
|
||||
print("-" * 30)
|
||||
results = exa_search("Latest AI developments 2024", num_results=3)
|
||||
if "error" not in results:
|
||||
print(f"Found {len(results.get('results', []))} results")
|
||||
for r in results.get("results", [])[:3]:
|
||||
print(f" - {r.get('title', 'No title')[:50]}...")
|
||||
print(f" URL: {r.get('url', 'No URL')}")
|
||||
else:
|
||||
print(f"Error: {results['error']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demonstration Complete")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Example plugin tools demonstrating the BaseTool and @tool patterns.
|
||||
|
||||
This module shows how to create tools using the new plugin system.
|
||||
External developers can use these patterns to create pip-installable plugins.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import BaseTool, tool, Agent
|
||||
|
||||
# Method 1: Class-based tool
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "Does something useful"
|
||||
|
||||
def run(self, param: str) -> str:
|
||||
return f"Result: {param}"
|
||||
|
||||
# Method 2: Decorator-based tool
|
||||
@tool
|
||||
def my_function(param: str) -> str:
|
||||
'''Does something useful.'''
|
||||
return f"Result: {param}"
|
||||
|
||||
# Use with Agent
|
||||
agent = Agent(tools=[MyTool(), my_function])
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from .base import BaseTool
|
||||
from .decorator import tool
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 1: Simple Class-Based Tool
|
||||
# =============================================================================
|
||||
|
||||
class EchoTool(BaseTool):
|
||||
"""A simple tool that echoes input back.
|
||||
|
||||
This demonstrates the minimal BaseTool implementation.
|
||||
"""
|
||||
name = "echo"
|
||||
description = "Echo the input message back"
|
||||
|
||||
def run(self, message: str) -> str:
|
||||
"""Echo the message.
|
||||
|
||||
Args:
|
||||
message: The message to echo
|
||||
|
||||
Returns:
|
||||
The same message
|
||||
"""
|
||||
return message
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 2: Tool with Multiple Parameters
|
||||
# =============================================================================
|
||||
|
||||
class MathTool(BaseTool):
|
||||
"""A tool for basic math operations.
|
||||
|
||||
Demonstrates:
|
||||
- Multiple parameters with types
|
||||
- Default parameter values
|
||||
- Returning structured data
|
||||
"""
|
||||
name = "basic_math"
|
||||
description = "Perform basic math operations (add, subtract, multiply, divide)"
|
||||
|
||||
def run(
|
||||
self,
|
||||
a: float,
|
||||
b: float,
|
||||
operation: str = "add"
|
||||
) -> Dict[str, Any]:
|
||||
"""Perform a math operation.
|
||||
|
||||
Args:
|
||||
a: First number
|
||||
b: Second number
|
||||
operation: One of 'add', 'subtract', 'multiply', 'divide'
|
||||
|
||||
Returns:
|
||||
Dict with result and operation details
|
||||
"""
|
||||
operations = {
|
||||
"add": lambda x, y: x + y,
|
||||
"subtract": lambda x, y: x - y,
|
||||
"multiply": lambda x, y: x * y,
|
||||
"divide": lambda x, y: x / y if y != 0 else float('inf')
|
||||
}
|
||||
|
||||
if operation not in operations:
|
||||
return {"error": f"Unknown operation: {operation}"}
|
||||
|
||||
result = operations[operation](a, b)
|
||||
return {
|
||||
"result": result,
|
||||
"operation": operation,
|
||||
"expression": f"{a} {operation} {b} = {result}"
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 3: Tool with State (Instance Variables)
|
||||
# =============================================================================
|
||||
|
||||
class CounterTool(BaseTool):
|
||||
"""A tool that maintains state between calls.
|
||||
|
||||
Demonstrates:
|
||||
- Instance variables for state
|
||||
- Custom __init__ with super() call
|
||||
"""
|
||||
name = "counter"
|
||||
description = "A counter that increments each time it's called"
|
||||
|
||||
def __init__(self, start: int = 0):
|
||||
self._count = start
|
||||
super().__init__() # Important: call parent init
|
||||
|
||||
def run(self, increment: int = 1) -> Dict[str, int]:
|
||||
"""Increment the counter.
|
||||
|
||||
Args:
|
||||
increment: Amount to increment by (default: 1)
|
||||
|
||||
Returns:
|
||||
Dict with previous and current count
|
||||
"""
|
||||
previous = self._count
|
||||
self._count += increment
|
||||
return {
|
||||
"previous": previous,
|
||||
"current": self._count,
|
||||
"increment": increment
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 4: Decorator-Based Tools
|
||||
# =============================================================================
|
||||
|
||||
@tool
|
||||
def greet(name: str, greeting: str = "Hello") -> str:
|
||||
"""Generate a greeting message.
|
||||
|
||||
Args:
|
||||
name: Name of the person to greet
|
||||
greeting: The greeting word (default: "Hello")
|
||||
|
||||
Returns:
|
||||
A greeting string
|
||||
"""
|
||||
return f"{greeting}, {name}!"
|
||||
|
||||
|
||||
@tool(name="reverse_text", description="Reverse a string of text")
|
||||
def reverse_string(text: str) -> str:
|
||||
"""Reverse the input text."""
|
||||
return text[::-1]
|
||||
|
||||
|
||||
@tool
|
||||
def list_numbers(start: int, end: int, step: int = 1) -> List[int]:
|
||||
"""Generate a list of numbers.
|
||||
|
||||
Args:
|
||||
start: Starting number (inclusive)
|
||||
end: Ending number (exclusive)
|
||||
step: Step between numbers (default: 1)
|
||||
|
||||
Returns:
|
||||
List of integers
|
||||
"""
|
||||
return list(range(start, end, step))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: Tool with Error Handling
|
||||
# =============================================================================
|
||||
|
||||
class SafeDivideTool(BaseTool):
|
||||
"""A division tool with proper error handling.
|
||||
|
||||
Demonstrates:
|
||||
- Error handling in run()
|
||||
- Returning error information
|
||||
"""
|
||||
name = "safe_divide"
|
||||
description = "Safely divide two numbers with error handling"
|
||||
|
||||
def run(self, numerator: float, denominator: float) -> Dict[str, Any]:
|
||||
"""Divide numerator by denominator.
|
||||
|
||||
Args:
|
||||
numerator: The number to divide
|
||||
denominator: The number to divide by
|
||||
|
||||
Returns:
|
||||
Dict with result or error message
|
||||
"""
|
||||
if denominator == 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Division by zero is not allowed"
|
||||
}
|
||||
|
||||
result = numerator / denominator
|
||||
return {
|
||||
"success": True,
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Convenience exports
|
||||
# =============================================================================
|
||||
|
||||
# These can be imported directly:
|
||||
# from praisonaiagents.tools.example_plugin import echo_tool, math_tool, greet
|
||||
|
||||
echo_tool = EchoTool()
|
||||
math_tool = MathTool()
|
||||
counter_tool = CounterTool()
|
||||
safe_divide_tool = SafeDivideTool()
|
||||
|
||||
__all__ = [
|
||||
# Classes
|
||||
'EchoTool',
|
||||
'MathTool',
|
||||
'CounterTool',
|
||||
'SafeDivideTool',
|
||||
# Instances
|
||||
'echo_tool',
|
||||
'math_tool',
|
||||
'counter_tool',
|
||||
'safe_divide_tool',
|
||||
# Decorated functions
|
||||
'greet',
|
||||
'reverse_string',
|
||||
'list_numbers',
|
||||
]
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Tool registry for managing and discovering tools.
|
||||
|
||||
This module provides centralized tool management with support for:
|
||||
- Manual registration
|
||||
- Auto-discovery via entry_points
|
||||
- Tool lookup by name
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools.registry import get_registry
|
||||
|
||||
registry = get_registry()
|
||||
registry.register(my_tool)
|
||||
tool = registry.get("my_tool")
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
from .base import BaseTool
|
||||
|
||||
|
||||
# Entry point group name for external plugins
|
||||
ENTRY_POINT_GROUP = "praisonaiagents.tools"
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Central registry for all tools.
|
||||
|
||||
Provides:
|
||||
- Tool registration (manual and auto-discovery)
|
||||
- Tool lookup by name
|
||||
- Tool listing and filtering
|
||||
- Entry points discovery for external plugins
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: Dict[str, BaseTool] = {}
|
||||
self._functions: Dict[str, Callable] = {} # For backward compat with plain functions
|
||||
self._discovered: bool = False
|
||||
|
||||
def register(
|
||||
self,
|
||||
tool: Union[BaseTool, Callable],
|
||||
name: Optional[str] = None,
|
||||
overwrite: bool = False
|
||||
) -> None:
|
||||
"""Register a tool with the registry.
|
||||
|
||||
Args:
|
||||
tool: BaseTool instance or callable function
|
||||
name: Override name (default: tool.name or function.__name__)
|
||||
overwrite: If True, overwrite existing tool with same name
|
||||
|
||||
Raises:
|
||||
ValueError: If tool with same name exists and overwrite=False
|
||||
"""
|
||||
# Handle BaseTool instances
|
||||
if isinstance(tool, BaseTool):
|
||||
tool_name = name or tool.name
|
||||
if tool_name in self._tools and not overwrite:
|
||||
logging.debug(f"Tool '{tool_name}' already registered, skipping")
|
||||
return
|
||||
self._tools[tool_name] = tool
|
||||
logging.debug(f"Registered tool: {tool_name}")
|
||||
return
|
||||
|
||||
# Handle plain callables
|
||||
if callable(tool):
|
||||
tool_name = name or getattr(tool, '__name__', str(id(tool)))
|
||||
if tool_name in self._functions and not overwrite:
|
||||
logging.debug(f"Function '{tool_name}' already registered, skipping")
|
||||
return
|
||||
self._functions[tool_name] = tool
|
||||
logging.debug(f"Registered function: {tool_name}")
|
||||
return
|
||||
|
||||
raise TypeError(f"Cannot register {type(tool)}, expected BaseTool or callable")
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""Remove a tool from the registry.
|
||||
|
||||
Args:
|
||||
name: Tool name to remove
|
||||
|
||||
Returns:
|
||||
True if tool was removed, False if not found
|
||||
"""
|
||||
if name in self._tools:
|
||||
del self._tools[name]
|
||||
return True
|
||||
if name in self._functions:
|
||||
del self._functions[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, name: str) -> Optional[Union[BaseTool, Callable]]:
|
||||
"""Get a tool by name.
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
|
||||
Returns:
|
||||
BaseTool instance, callable, or None if not found
|
||||
"""
|
||||
# Check BaseTool registry first
|
||||
if name in self._tools:
|
||||
return self._tools[name]
|
||||
|
||||
# Check functions registry
|
||||
if name in self._functions:
|
||||
return self._functions[name]
|
||||
|
||||
# Try auto-discovery if not found
|
||||
if not self._discovered:
|
||||
self.discover_plugins()
|
||||
if name in self._tools:
|
||||
return self._tools[name]
|
||||
|
||||
return None
|
||||
|
||||
def list_tools(self) -> List[str]:
|
||||
"""List all registered tool names."""
|
||||
return list(self._tools.keys()) + list(self._functions.keys())
|
||||
|
||||
def list_base_tools(self) -> List[BaseTool]:
|
||||
"""List all registered BaseTool instances."""
|
||||
return list(self._tools.values())
|
||||
|
||||
def get_all(self) -> Dict[str, Union[BaseTool, Callable]]:
|
||||
"""Get all registered tools as a dict."""
|
||||
result = dict(self._tools)
|
||||
result.update(self._functions)
|
||||
return result
|
||||
|
||||
def discover_plugins(self) -> int:
|
||||
"""Discover and register tools from entry_points.
|
||||
|
||||
External packages can register tools by adding to pyproject.toml:
|
||||
|
||||
[project.entry-points."praisonaiagents.tools"]
|
||||
my_tool = "my_package.tools:MyTool"
|
||||
|
||||
Returns:
|
||||
Number of tools discovered
|
||||
"""
|
||||
if self._discovered:
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
try:
|
||||
# Python 3.10+ style
|
||||
eps = entry_points(group=ENTRY_POINT_GROUP)
|
||||
except TypeError:
|
||||
# Python 3.9 fallback
|
||||
try:
|
||||
all_eps = entry_points()
|
||||
eps = all_eps.get(ENTRY_POINT_GROUP, [])
|
||||
except Exception:
|
||||
eps = []
|
||||
|
||||
for ep in eps:
|
||||
try:
|
||||
tool_class_or_func = ep.load()
|
||||
|
||||
# If it's a class, instantiate it
|
||||
if isinstance(tool_class_or_func, type) and issubclass(tool_class_or_func, BaseTool):
|
||||
tool_instance = tool_class_or_func()
|
||||
self.register(tool_instance, name=ep.name)
|
||||
# If it's already an instance or callable
|
||||
elif isinstance(tool_class_or_func, BaseTool):
|
||||
self.register(tool_class_or_func, name=ep.name)
|
||||
elif callable(tool_class_or_func):
|
||||
self.register(tool_class_or_func, name=ep.name)
|
||||
else:
|
||||
logging.warning(f"Entry point '{ep.name}' is not a valid tool")
|
||||
continue
|
||||
|
||||
count += 1
|
||||
logging.info(f"Discovered plugin tool: {ep.name}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to load plugin '{ep.name}': {e}")
|
||||
|
||||
self._discovered = True
|
||||
return count
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all registered tools."""
|
||||
self._tools.clear()
|
||||
self._functions.clear()
|
||||
self._discovered = False
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self._tools or name in self._functions
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._tools) + len(self._functions)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ToolRegistry(tools={len(self._tools)}, functions={len(self._functions)})"
|
||||
|
||||
|
||||
# Global registry instance
|
||||
_global_registry: Optional[ToolRegistry] = None
|
||||
|
||||
|
||||
def get_registry() -> ToolRegistry:
|
||||
"""Get the global tool registry instance."""
|
||||
global _global_registry
|
||||
if _global_registry is None:
|
||||
_global_registry = ToolRegistry()
|
||||
return _global_registry
|
||||
|
||||
|
||||
def register_tool(
|
||||
tool: Union[BaseTool, Callable],
|
||||
name: Optional[str] = None
|
||||
) -> None:
|
||||
"""Convenience function to register a tool with the global registry."""
|
||||
get_registry().register(tool, name=name)
|
||||
|
||||
|
||||
def get_tool(name: str) -> Optional[Union[BaseTool, Callable]]:
|
||||
"""Convenience function to get a tool from the global registry."""
|
||||
return get_registry().get(name)
|
||||
@@ -0,0 +1,591 @@
|
||||
"""Tavily search and extraction tools.
|
||||
|
||||
Tavily provides AI-powered web search, content extraction, and crawling capabilities.
|
||||
This module requires the TAVILY_API_KEY environment variable to be set.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import tavily_search, tavily_extract
|
||||
|
||||
# Search the web
|
||||
results = tavily_search("AI news 2024")
|
||||
|
||||
# Extract content from URLs
|
||||
content = tavily_extract("https://example.com")
|
||||
|
||||
# Or use the class directly
|
||||
from praisonaiagents.tools import TavilyTools
|
||||
tavily = TavilyTools()
|
||||
results = tavily.search("AI news")
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import logging
|
||||
import os
|
||||
from importlib import util
|
||||
|
||||
|
||||
def _check_tavily_available() -> tuple[bool, Optional[str]]:
|
||||
"""Check if Tavily is available and API key is set.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, error_message)
|
||||
"""
|
||||
# Check if tavily package is installed
|
||||
if util.find_spec("tavily") is None:
|
||||
return False, "Tavily package is not installed. Please install it using: pip install tavily-python"
|
||||
|
||||
# Check if API key is set
|
||||
api_key = os.environ.get("TAVILY_API_KEY")
|
||||
if not api_key:
|
||||
return False, "TAVILY_API_KEY environment variable is not set. Please set it to use Tavily tools."
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
class TavilyTools:
|
||||
"""Comprehensive tools for web search and content extraction using Tavily API.
|
||||
|
||||
Tavily provides AI-powered search capabilities optimized for LLM applications.
|
||||
|
||||
Features:
|
||||
- Web search with advanced filtering and ranking
|
||||
- Content extraction from URLs
|
||||
- Website crawling with intelligent navigation
|
||||
- Site mapping
|
||||
|
||||
Requires:
|
||||
- tavily-python package: pip install tavily-python
|
||||
- TAVILY_API_KEY environment variable
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""Initialize TavilyTools.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key. If not provided, uses TAVILY_API_KEY env var.
|
||||
"""
|
||||
self._api_key = api_key
|
||||
self._client = None
|
||||
self._async_client = None
|
||||
|
||||
def _get_api_key(self) -> Optional[str]:
|
||||
"""Get API key from instance or environment."""
|
||||
return self._api_key or os.environ.get("TAVILY_API_KEY")
|
||||
|
||||
def _get_client(self):
|
||||
"""Get or create synchronous Tavily client."""
|
||||
if self._client is None:
|
||||
is_available, error = _check_tavily_available()
|
||||
if not is_available:
|
||||
return None, error
|
||||
|
||||
from tavily import TavilyClient
|
||||
api_key = self._get_api_key()
|
||||
if not api_key:
|
||||
return None, "TAVILY_API_KEY not set"
|
||||
self._client = TavilyClient(api_key=api_key)
|
||||
return self._client, None
|
||||
|
||||
def _get_async_client(self):
|
||||
"""Get or create asynchronous Tavily client."""
|
||||
if self._async_client is None:
|
||||
is_available, error = _check_tavily_available()
|
||||
if not is_available:
|
||||
return None, error
|
||||
|
||||
from tavily import AsyncTavilyClient
|
||||
api_key = self._get_api_key()
|
||||
if not api_key:
|
||||
return None, "TAVILY_API_KEY not set"
|
||||
self._async_client = AsyncTavilyClient(api_key=api_key)
|
||||
return self._async_client, None
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
search_depth: str = "basic",
|
||||
topic: str = "general",
|
||||
max_results: int = 5,
|
||||
include_answer: bool = False,
|
||||
include_raw_content: bool = False,
|
||||
include_images: bool = False,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
time_range: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search the web using Tavily's AI-powered search.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
search_depth: "basic" or "advanced" (advanced uses more credits but better results)
|
||||
topic: "general", "news", or "finance"
|
||||
max_results: Maximum number of results (1-20)
|
||||
include_answer: Include LLM-generated answer based on results
|
||||
include_raw_content: Include cleaned HTML content of each result
|
||||
include_images: Include query-related images
|
||||
include_domains: List of domains to include (max 300)
|
||||
exclude_domains: List of domains to exclude (max 150)
|
||||
time_range: Filter by time - "day", "week", "month", "year" or "d", "w", "m", "y"
|
||||
|
||||
Returns:
|
||||
Dict containing search results with keys:
|
||||
- results: List of search results with title, url, content, score
|
||||
- query: The search query
|
||||
- response_time: Time taken for search
|
||||
- answer: (optional) LLM-generated answer if include_answer=True
|
||||
- images: (optional) List of image URLs if include_images=True
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {
|
||||
"query": query,
|
||||
"search_depth": search_depth,
|
||||
"topic": topic,
|
||||
"max_results": max_results,
|
||||
"include_answer": include_answer,
|
||||
"include_raw_content": include_raw_content,
|
||||
"include_images": include_images,
|
||||
}
|
||||
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if time_range:
|
||||
kwargs["time_range"] = time_range
|
||||
|
||||
response = client.search(**kwargs)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily search error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def extract(
|
||||
self,
|
||||
urls: Union[str, List[str]],
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract content from one or more URLs.
|
||||
|
||||
Args:
|
||||
urls: Single URL or list of URLs (max 20)
|
||||
include_images: Include extracted images
|
||||
extract_depth: "basic" or "advanced" (advanced retrieves more data)
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- results: List of successful extractions with url, raw_content
|
||||
- failed_results: List of URLs that failed with error messages
|
||||
- response_time: Time taken for extraction
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
response = client.extract(
|
||||
urls=urls,
|
||||
include_images=include_images,
|
||||
extract_depth=extract_depth,
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily extract error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def crawl(
|
||||
self,
|
||||
url: str,
|
||||
max_depth: int = 1,
|
||||
max_breadth: int = 20,
|
||||
limit: int = 50,
|
||||
instructions: Optional[str] = None,
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Crawl a website starting from a base URL.
|
||||
|
||||
Args:
|
||||
url: The root URL to begin crawling
|
||||
max_depth: Maximum depth from base URL (default: 1)
|
||||
max_breadth: Max links to follow per page (default: 20)
|
||||
limit: Total links to process before stopping (default: 50)
|
||||
instructions: Natural language instructions for the crawler
|
||||
include_images: Extract image URLs from pages
|
||||
extract_depth: "basic" or "advanced"
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- base_url: The starting URL
|
||||
- results: List of crawled pages with url, raw_content, images
|
||||
- response_time: Time taken for crawl
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {
|
||||
"url": url,
|
||||
"max_depth": max_depth,
|
||||
"max_breadth": max_breadth,
|
||||
"limit": limit,
|
||||
"include_images": include_images,
|
||||
"extract_depth": extract_depth,
|
||||
}
|
||||
|
||||
if instructions:
|
||||
kwargs["instructions"] = instructions
|
||||
|
||||
response = client.crawl(**kwargs)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily crawl error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def map(
|
||||
self,
|
||||
url: str,
|
||||
max_depth: int = 1,
|
||||
max_breadth: int = 20,
|
||||
limit: int = 50,
|
||||
instructions: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a sitemap starting from a base URL.
|
||||
|
||||
Args:
|
||||
url: The root URL to begin mapping
|
||||
max_depth: Maximum depth from base URL (default: 1)
|
||||
max_breadth: Max links to follow per page (default: 20)
|
||||
limit: Total links to process before stopping (default: 50)
|
||||
instructions: Natural language instructions for the mapper
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- base_url: The starting URL
|
||||
- results: List of discovered URLs
|
||||
- response_time: Time taken for mapping
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {
|
||||
"url": url,
|
||||
"max_depth": max_depth,
|
||||
"max_breadth": max_breadth,
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
if instructions:
|
||||
kwargs["instructions"] = instructions
|
||||
|
||||
response = client.map(**kwargs)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily map error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
# Async methods
|
||||
async def asearch(
|
||||
self,
|
||||
query: str,
|
||||
search_depth: str = "basic",
|
||||
topic: str = "general",
|
||||
max_results: int = 5,
|
||||
include_answer: bool = False,
|
||||
include_raw_content: bool = False,
|
||||
include_images: bool = False,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
time_range: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of search. See search() for documentation."""
|
||||
client, error = self._get_async_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {
|
||||
"query": query,
|
||||
"search_depth": search_depth,
|
||||
"topic": topic,
|
||||
"max_results": max_results,
|
||||
"include_answer": include_answer,
|
||||
"include_raw_content": include_raw_content,
|
||||
"include_images": include_images,
|
||||
}
|
||||
|
||||
if include_domains:
|
||||
kwargs["include_domains"] = include_domains
|
||||
if exclude_domains:
|
||||
kwargs["exclude_domains"] = exclude_domains
|
||||
if time_range:
|
||||
kwargs["time_range"] = time_range
|
||||
|
||||
response = await client.search(**kwargs)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily async search error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
async def aextract(
|
||||
self,
|
||||
urls: Union[str, List[str]],
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of extract. See extract() for documentation."""
|
||||
client, error = self._get_async_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
response = await client.extract(
|
||||
urls=urls,
|
||||
include_images=include_images,
|
||||
extract_depth=extract_depth,
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Tavily async extract error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
|
||||
# Standalone functions for direct import
|
||||
|
||||
def tavily_search(
|
||||
query: str,
|
||||
search_depth: str = "basic",
|
||||
topic: str = "general",
|
||||
max_results: int = 5,
|
||||
include_answer: bool = False,
|
||||
include_raw_content: bool = False,
|
||||
include_images: bool = False,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
time_range: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search the web using Tavily's AI-powered search.
|
||||
|
||||
This is a convenience function that creates a TavilyTools instance.
|
||||
For repeated calls, consider using TavilyTools class directly.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
search_depth: "basic" or "advanced"
|
||||
topic: "general", "news", or "finance"
|
||||
max_results: Maximum number of results (1-20)
|
||||
include_answer: Include LLM-generated answer
|
||||
include_raw_content: Include cleaned HTML content
|
||||
include_images: Include query-related images
|
||||
include_domains: List of domains to include
|
||||
exclude_domains: List of domains to exclude
|
||||
time_range: Filter by time - "day", "week", "month", "year"
|
||||
|
||||
Returns:
|
||||
Dict containing search results
|
||||
"""
|
||||
tools = TavilyTools()
|
||||
return tools.search(
|
||||
query=query,
|
||||
search_depth=search_depth,
|
||||
topic=topic,
|
||||
max_results=max_results,
|
||||
include_answer=include_answer,
|
||||
include_raw_content=include_raw_content,
|
||||
include_images=include_images,
|
||||
include_domains=include_domains,
|
||||
exclude_domains=exclude_domains,
|
||||
time_range=time_range,
|
||||
)
|
||||
|
||||
|
||||
def tavily_extract(
|
||||
urls: Union[str, List[str]],
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract content from one or more URLs using Tavily.
|
||||
|
||||
Args:
|
||||
urls: Single URL or list of URLs (max 20)
|
||||
include_images: Include extracted images
|
||||
extract_depth: "basic" or "advanced"
|
||||
|
||||
Returns:
|
||||
Dict containing extraction results
|
||||
"""
|
||||
tools = TavilyTools()
|
||||
return tools.extract(
|
||||
urls=urls,
|
||||
include_images=include_images,
|
||||
extract_depth=extract_depth,
|
||||
)
|
||||
|
||||
|
||||
def tavily_crawl(
|
||||
url: str,
|
||||
max_depth: int = 1,
|
||||
max_breadth: int = 20,
|
||||
limit: int = 50,
|
||||
instructions: Optional[str] = None,
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Crawl a website starting from a base URL using Tavily.
|
||||
|
||||
Args:
|
||||
url: The root URL to begin crawling
|
||||
max_depth: Maximum depth from base URL
|
||||
max_breadth: Max links to follow per page
|
||||
limit: Total links to process before stopping
|
||||
instructions: Natural language instructions for the crawler
|
||||
include_images: Extract image URLs from pages
|
||||
extract_depth: "basic" or "advanced"
|
||||
|
||||
Returns:
|
||||
Dict containing crawl results
|
||||
"""
|
||||
tools = TavilyTools()
|
||||
return tools.crawl(
|
||||
url=url,
|
||||
max_depth=max_depth,
|
||||
max_breadth=max_breadth,
|
||||
limit=limit,
|
||||
instructions=instructions,
|
||||
include_images=include_images,
|
||||
extract_depth=extract_depth,
|
||||
)
|
||||
|
||||
|
||||
def tavily_map(
|
||||
url: str,
|
||||
max_depth: int = 1,
|
||||
max_breadth: int = 20,
|
||||
limit: int = 50,
|
||||
instructions: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a sitemap starting from a base URL using Tavily.
|
||||
|
||||
Args:
|
||||
url: The root URL to begin mapping
|
||||
max_depth: Maximum depth from base URL
|
||||
max_breadth: Max links to follow per page
|
||||
limit: Total links to process before stopping
|
||||
instructions: Natural language instructions for the mapper
|
||||
|
||||
Returns:
|
||||
Dict containing map results with discovered URLs
|
||||
"""
|
||||
tools = TavilyTools()
|
||||
return tools.map(
|
||||
url=url,
|
||||
max_depth=max_depth,
|
||||
max_breadth=max_breadth,
|
||||
limit=limit,
|
||||
instructions=instructions,
|
||||
)
|
||||
|
||||
|
||||
# Async standalone functions
|
||||
async def tavily_search_async(
|
||||
query: str,
|
||||
search_depth: str = "basic",
|
||||
topic: str = "general",
|
||||
max_results: int = 5,
|
||||
include_answer: bool = False,
|
||||
include_raw_content: bool = False,
|
||||
include_images: bool = False,
|
||||
include_domains: Optional[List[str]] = None,
|
||||
exclude_domains: Optional[List[str]] = None,
|
||||
time_range: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of tavily_search. See tavily_search() for documentation."""
|
||||
tools = TavilyTools()
|
||||
return await tools.asearch(
|
||||
query=query,
|
||||
search_depth=search_depth,
|
||||
topic=topic,
|
||||
max_results=max_results,
|
||||
include_answer=include_answer,
|
||||
include_raw_content=include_raw_content,
|
||||
include_images=include_images,
|
||||
include_domains=include_domains,
|
||||
exclude_domains=exclude_domains,
|
||||
time_range=time_range,
|
||||
)
|
||||
|
||||
|
||||
async def tavily_extract_async(
|
||||
urls: Union[str, List[str]],
|
||||
include_images: bool = False,
|
||||
extract_depth: str = "basic",
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of tavily_extract. See tavily_extract() for documentation."""
|
||||
tools = TavilyTools()
|
||||
return await tools.aextract(
|
||||
urls=urls,
|
||||
include_images=include_images,
|
||||
extract_depth=extract_depth,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
print("\n==================================================")
|
||||
print("Tavily Tools Demonstration")
|
||||
print("==================================================\n")
|
||||
|
||||
# Check if API key is available
|
||||
is_available, error = _check_tavily_available()
|
||||
if not is_available:
|
||||
print(f"Error: {error}")
|
||||
print("\nTo use Tavily tools:")
|
||||
print("1. Install: pip install tavily-python")
|
||||
print("2. Set environment variable: export TAVILY_API_KEY=your_api_key")
|
||||
else:
|
||||
print("Tavily is available!")
|
||||
|
||||
# Example search
|
||||
print("\n1. Basic Search")
|
||||
print("-" * 30)
|
||||
results = tavily_search("Latest AI developments 2024", max_results=3)
|
||||
if "error" not in results:
|
||||
print(f"Found {len(results.get('results', []))} results")
|
||||
for r in results.get("results", [])[:3]:
|
||||
print(f" - {r.get('title', 'No title')}")
|
||||
print(f" URL: {r.get('url', 'No URL')}")
|
||||
else:
|
||||
print(f"Error: {results['error']}")
|
||||
|
||||
print("\n==================================================")
|
||||
print("Demonstration Complete")
|
||||
print("==================================================")
|
||||
|
||||
|
||||
# Alias for simple usage: from praisonaiagents.tools import tavily
|
||||
tavily = tavily_search
|
||||
@@ -0,0 +1,533 @@
|
||||
"""You.com search and content tools.
|
||||
|
||||
You.com provides AI-powered web search, news search, content extraction, and image search.
|
||||
This module requires the YDC_API_KEY environment variable to be set.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import ydc_search, ydc_news, ydc_contents
|
||||
|
||||
# Search the web (unified web + news)
|
||||
results = ydc_search("AI news 2024")
|
||||
|
||||
# Get live news
|
||||
news = ydc_news("technology trends")
|
||||
|
||||
# Extract content from URLs
|
||||
content = ydc_contents(["https://example.com"])
|
||||
|
||||
# Or use the class directly
|
||||
from praisonaiagents.tools import YouTools
|
||||
you = YouTools()
|
||||
results = you.search("AI news")
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import logging
|
||||
import os
|
||||
from importlib import util
|
||||
|
||||
|
||||
def _check_youdotcom_available() -> tuple[bool, Optional[str]]:
|
||||
"""Check if You.com SDK is available and API key is set.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, error_message)
|
||||
"""
|
||||
# Check if youdotcom package is installed
|
||||
if util.find_spec("youdotcom") is None:
|
||||
return False, "youdotcom package is not installed. Please install it using: pip install youdotcom"
|
||||
|
||||
# Check if API key is set
|
||||
api_key = os.environ.get("YDC_API_KEY")
|
||||
if not api_key:
|
||||
return False, "YDC_API_KEY environment variable is not set. Please set it to use You.com tools."
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
class YouTools:
|
||||
"""Comprehensive tools for web search and content using You.com API.
|
||||
|
||||
You.com provides AI-powered search capabilities optimized for LLM applications.
|
||||
|
||||
Features:
|
||||
- Unified web and news search with LLM-ready snippets
|
||||
- Live news search from authoritative sources
|
||||
- Content extraction from URLs (HTML/Markdown)
|
||||
- Image search
|
||||
- Advanced search operators support
|
||||
- Geographic and language targeting
|
||||
- Freshness controls
|
||||
|
||||
Requires:
|
||||
- youdotcom package: pip install youdotcom
|
||||
- YDC_API_KEY environment variable
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""Initialize YouTools.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key. If not provided, uses YDC_API_KEY env var.
|
||||
"""
|
||||
self._api_key = api_key
|
||||
self._client = None
|
||||
|
||||
def _get_api_key(self) -> Optional[str]:
|
||||
"""Get API key from instance or environment."""
|
||||
return self._api_key or os.environ.get("YDC_API_KEY")
|
||||
|
||||
def _get_client(self):
|
||||
"""Get or create You.com client."""
|
||||
if self._client is None:
|
||||
is_available, error = _check_youdotcom_available()
|
||||
if not is_available:
|
||||
return None, error
|
||||
|
||||
from youdotcom import You
|
||||
api_key = self._get_api_key()
|
||||
if not api_key:
|
||||
return None, "YDC_API_KEY not set"
|
||||
self._client = You(api_key_auth=api_key)
|
||||
return self._client, None
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
count: int = 10,
|
||||
freshness: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
offset: int = 0,
|
||||
safesearch: str = "moderate",
|
||||
livecrawl: Optional[str] = None,
|
||||
livecrawl_formats: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search the web using You.com's unified search API.
|
||||
|
||||
Returns both web and news results in a single call with LLM-ready snippets.
|
||||
|
||||
Args:
|
||||
query: Search query (supports operators like site:, filetype:, +term, -term)
|
||||
count: Max results per section (default 10, max 100)
|
||||
freshness: Filter by recency - "day", "week", "month", "year" or "YYYY-MM-DDtoYYYY-MM-DD"
|
||||
country: ISO country code (e.g., "US", "GB", "FR")
|
||||
language: BCP 47 language code (e.g., "EN", "JP", "DE")
|
||||
offset: Pagination offset (0-9), results = offset * count
|
||||
safesearch: Content filter - "off", "moderate", "strict"
|
||||
livecrawl: Fetch full content - "web", "news", or "all"
|
||||
livecrawl_formats: Content format - "html" or "markdown"
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- results: {web: [...], news: [...]}
|
||||
- metadata: {query, search_uuid, latency}
|
||||
|
||||
Search Operators:
|
||||
- site:domain.com - Search within specific domain
|
||||
- filetype:pdf - Filter by file type
|
||||
- +term - Must include term
|
||||
- -term - Exclude term
|
||||
- AND, OR, NOT - Boolean operators
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {"query": query, "count": count, "offset": offset}
|
||||
|
||||
if freshness:
|
||||
kwargs["freshness"] = freshness
|
||||
if country:
|
||||
kwargs["country"] = country
|
||||
if language:
|
||||
kwargs["language"] = language
|
||||
if safesearch:
|
||||
kwargs["safesearch"] = safesearch
|
||||
if livecrawl:
|
||||
kwargs["livecrawl"] = livecrawl
|
||||
if livecrawl_formats:
|
||||
kwargs["livecrawl_formats"] = livecrawl_formats
|
||||
|
||||
response = client.search.unified(**kwargs)
|
||||
|
||||
# Convert response to dict if it's a model object
|
||||
if hasattr(response, 'model_dump'):
|
||||
return response.model_dump()
|
||||
elif hasattr(response, 'to_dict'):
|
||||
return response.to_dict()
|
||||
elif hasattr(response, '__dict__'):
|
||||
return self._convert_response(response)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"You.com search error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def _convert_response(self, response) -> Dict[str, Any]:
|
||||
"""Convert SDK response object to dictionary."""
|
||||
result = {}
|
||||
|
||||
if hasattr(response, 'results'):
|
||||
results = response.results
|
||||
result['results'] = {}
|
||||
if hasattr(results, 'web') and results.web:
|
||||
result['results']['web'] = [
|
||||
{
|
||||
'url': getattr(r, 'url', ''),
|
||||
'title': getattr(r, 'title', ''),
|
||||
'description': getattr(r, 'description', ''),
|
||||
'snippets': getattr(r, 'snippets', []),
|
||||
'thumbnail_url': getattr(r, 'thumbnail_url', None),
|
||||
'page_age': getattr(r, 'page_age', None),
|
||||
'favicon_url': getattr(r, 'favicon_url', None),
|
||||
'authors': getattr(r, 'authors', []),
|
||||
}
|
||||
for r in results.web
|
||||
]
|
||||
if hasattr(results, 'news') and results.news:
|
||||
result['results']['news'] = [
|
||||
{
|
||||
'url': getattr(r, 'url', ''),
|
||||
'title': getattr(r, 'title', ''),
|
||||
'description': getattr(r, 'description', ''),
|
||||
'thumbnail_url': getattr(r, 'thumbnail_url', None),
|
||||
'page_age': getattr(r, 'page_age', None),
|
||||
}
|
||||
for r in results.news
|
||||
]
|
||||
|
||||
if hasattr(response, 'metadata'):
|
||||
meta = response.metadata
|
||||
result['metadata'] = {
|
||||
'query': getattr(meta, 'query', ''),
|
||||
'search_uuid': getattr(meta, 'search_uuid', ''),
|
||||
'latency': getattr(meta, 'latency', 0),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def get_contents(
|
||||
self,
|
||||
urls: Union[str, List[str]],
|
||||
format: str = "markdown",
|
||||
) -> Dict[str, Any]:
|
||||
"""Fetch content from URLs using You.com's content extraction API.
|
||||
|
||||
Args:
|
||||
urls: Single URL or list of URLs to fetch content from
|
||||
format: Content format - "html" or "markdown" (default: markdown)
|
||||
|
||||
Returns:
|
||||
Dict containing array of results with:
|
||||
- url: The fetched URL
|
||||
- title: Page title
|
||||
- html: HTML content (if format="html")
|
||||
- markdown: Markdown content (if format="markdown")
|
||||
- metadata: {site_name, favicon_url}
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
if isinstance(urls, str):
|
||||
urls = [urls]
|
||||
|
||||
# Use the contents endpoint
|
||||
response = client.search.contents(urls=urls, format=format)
|
||||
|
||||
# Convert response
|
||||
if hasattr(response, 'model_dump'):
|
||||
return {"results": response.model_dump()}
|
||||
elif isinstance(response, list):
|
||||
return {"results": [
|
||||
{
|
||||
'url': getattr(r, 'url', ''),
|
||||
'title': getattr(r, 'title', ''),
|
||||
'html': getattr(r, 'html', None),
|
||||
'markdown': getattr(r, 'markdown', None),
|
||||
'metadata': {
|
||||
'site_name': getattr(getattr(r, 'metadata', None), 'site_name', None) if hasattr(r, 'metadata') else None,
|
||||
'favicon_url': getattr(getattr(r, 'metadata', None), 'favicon_url', None) if hasattr(r, 'metadata') else None,
|
||||
}
|
||||
}
|
||||
for r in response
|
||||
]}
|
||||
return {"results": response}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"You.com contents error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def live_news(
|
||||
self,
|
||||
query: str,
|
||||
count: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search for live news using You.com's news API.
|
||||
|
||||
Note: This API may require early access. Contact api@you.com for access.
|
||||
|
||||
Args:
|
||||
query: News search query
|
||||
count: Maximum number of news results to return
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- news: {query, results: [...], metadata}
|
||||
- Each result has: title, description, url, page_age, source_name, thumbnail
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
kwargs = {"q": query}
|
||||
if count:
|
||||
kwargs["count"] = count
|
||||
|
||||
# Try to access live news endpoint
|
||||
if hasattr(client, 'livenews'):
|
||||
response = client.livenews.get(**kwargs)
|
||||
else:
|
||||
# Fallback: use unified search with news focus
|
||||
return self.search(query=query, count=count or 10)
|
||||
|
||||
if hasattr(response, 'model_dump'):
|
||||
return response.model_dump()
|
||||
elif hasattr(response, 'news'):
|
||||
news = response.news
|
||||
return {
|
||||
'news': {
|
||||
'query': getattr(getattr(news, 'query', None), 'original', query) if hasattr(news, 'query') else query,
|
||||
'results': [
|
||||
{
|
||||
'title': getattr(r, 'title', ''),
|
||||
'description': getattr(r, 'description', ''),
|
||||
'url': getattr(r, 'url', ''),
|
||||
'page_age': getattr(r, 'page_age', None),
|
||||
'age': getattr(r, 'age', None),
|
||||
'source_name': getattr(r, 'source_name', None),
|
||||
'thumbnail': getattr(getattr(r, 'thumbnail', None), 'src', None) if hasattr(r, 'thumbnail') else None,
|
||||
}
|
||||
for r in getattr(news, 'results', [])
|
||||
],
|
||||
'metadata': {
|
||||
'request_uuid': getattr(getattr(news, 'metadata', None), 'request_uuid', None) if hasattr(news, 'metadata') else None,
|
||||
}
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"You.com live news error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def images(
|
||||
self,
|
||||
query: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search for images using You.com's image search API.
|
||||
|
||||
Note: This API may require early access. Contact api@you.com for access.
|
||||
|
||||
Args:
|
||||
query: Image search query
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- images: {results: [...]}
|
||||
- metadata: {query, search_uuid}
|
||||
- Each result has: title, page_url, image_url
|
||||
"""
|
||||
client, error = self._get_client()
|
||||
if error:
|
||||
logging.error(error)
|
||||
return {"error": error}
|
||||
|
||||
try:
|
||||
if hasattr(client, 'images'):
|
||||
response = client.images.get(q=query)
|
||||
else:
|
||||
return {"error": "Image search not available in this SDK version"}
|
||||
|
||||
if hasattr(response, 'model_dump'):
|
||||
return response.model_dump()
|
||||
elif hasattr(response, 'images'):
|
||||
images = response.images
|
||||
return {
|
||||
'images': {
|
||||
'results': [
|
||||
{
|
||||
'title': getattr(r, 'title', ''),
|
||||
'page_url': getattr(r, 'page_url', ''),
|
||||
'image_url': getattr(r, 'image_url', ''),
|
||||
}
|
||||
for r in getattr(images, 'results', [])
|
||||
]
|
||||
},
|
||||
'metadata': {
|
||||
'query': getattr(getattr(response, 'metadata', None), 'query', query) if hasattr(response, 'metadata') else query,
|
||||
'search_uuid': getattr(getattr(response, 'metadata', None), 'search_uuid', None) if hasattr(response, 'metadata') else None,
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"You.com images error: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit."""
|
||||
if self._client is not None:
|
||||
# Close client if it has a close method
|
||||
if hasattr(self._client, 'close'):
|
||||
self._client.close()
|
||||
elif hasattr(self._client, '__exit__'):
|
||||
self._client.__exit__(exc_type, exc_val, exc_tb)
|
||||
return False
|
||||
|
||||
|
||||
# Standalone functions for direct import
|
||||
def ydc_search(
|
||||
query: str,
|
||||
count: int = 10,
|
||||
freshness: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
offset: int = 0,
|
||||
safesearch: str = "moderate",
|
||||
livecrawl: Optional[str] = None,
|
||||
livecrawl_formats: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search the web using You.com's unified search API.
|
||||
|
||||
This is a convenience function that creates a YouTools instance.
|
||||
For repeated calls, consider using YouTools class directly.
|
||||
|
||||
Args:
|
||||
query: Search query (supports operators like site:, filetype:, +term, -term)
|
||||
count: Max results per section (default 10, max 100)
|
||||
freshness: Filter by recency - "day", "week", "month", "year"
|
||||
country: ISO country code (e.g., "US", "GB")
|
||||
language: BCP 47 language code (e.g., "EN", "JP")
|
||||
offset: Pagination offset (0-9)
|
||||
safesearch: Content filter - "off", "moderate", "strict"
|
||||
livecrawl: Fetch full content - "web", "news", or "all"
|
||||
livecrawl_formats: Content format - "html" or "markdown"
|
||||
|
||||
Returns:
|
||||
Dict containing search results with web and news sections
|
||||
"""
|
||||
tools = YouTools()
|
||||
return tools.search(
|
||||
query=query,
|
||||
count=count,
|
||||
freshness=freshness,
|
||||
country=country,
|
||||
language=language,
|
||||
offset=offset,
|
||||
safesearch=safesearch,
|
||||
livecrawl=livecrawl,
|
||||
livecrawl_formats=livecrawl_formats,
|
||||
)
|
||||
|
||||
|
||||
def ydc_contents(
|
||||
urls: Union[str, List[str]],
|
||||
format: str = "markdown",
|
||||
) -> Dict[str, Any]:
|
||||
"""Fetch content from URLs using You.com's content extraction API.
|
||||
|
||||
Args:
|
||||
urls: Single URL or list of URLs to fetch content from
|
||||
format: Content format - "html" or "markdown"
|
||||
|
||||
Returns:
|
||||
Dict containing extracted content for each URL
|
||||
"""
|
||||
tools = YouTools()
|
||||
return tools.get_contents(urls=urls, format=format)
|
||||
|
||||
|
||||
def ydc_news(
|
||||
query: str,
|
||||
count: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search for live news using You.com's news API.
|
||||
|
||||
Args:
|
||||
query: News search query
|
||||
count: Maximum number of results
|
||||
|
||||
Returns:
|
||||
Dict containing news results
|
||||
"""
|
||||
tools = YouTools()
|
||||
return tools.live_news(query=query, count=count)
|
||||
|
||||
|
||||
def ydc_images(
|
||||
query: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Search for images using You.com's image search API.
|
||||
|
||||
Args:
|
||||
query: Image search query
|
||||
|
||||
Returns:
|
||||
Dict containing image results
|
||||
"""
|
||||
tools = YouTools()
|
||||
return tools.images(query=query)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
print("\n" + "=" * 60)
|
||||
print("You.com Tools Demonstration")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# Check if API key is available
|
||||
is_available, error = _check_youdotcom_available()
|
||||
if not is_available:
|
||||
print(f"Error: {error}")
|
||||
print("\nTo use You.com tools:")
|
||||
print("1. Install: pip install youdotcom")
|
||||
print("2. Set environment variable: export YDC_API_KEY=your_api_key")
|
||||
else:
|
||||
print("You.com SDK is available!")
|
||||
|
||||
# Example search
|
||||
print("\n1. Basic Search")
|
||||
print("-" * 30)
|
||||
results = ydc_search("Latest AI developments 2024", count=3)
|
||||
if "error" not in results:
|
||||
web_results = results.get('results', {}).get('web', [])
|
||||
print(f"Found {len(web_results)} web results")
|
||||
for r in web_results[:3]:
|
||||
print(f" - {r.get('title', 'No title')[:50]}...")
|
||||
print(f" URL: {r.get('url', 'No URL')}")
|
||||
else:
|
||||
print(f"Error: {results['error']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Demonstration Complete")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# Alias for simple usage: from praisonaiagents.tools import ydc
|
||||
ydc = ydc_search
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.158"
|
||||
version = "0.0.187"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Query Rewriter Agent Example
|
||||
|
||||
Demonstrates various query rewriting strategies for improving RAG retrieval.
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
def main():
|
||||
# Initialize the agent
|
||||
agent = QueryRewriterAgent(
|
||||
model="gpt-4o-mini",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("Query Rewriter Agent Examples")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. Basic Rewriting - Short keyword queries
|
||||
print("\n1. BASIC REWRITING (Short Queries)")
|
||||
print("-" * 40)
|
||||
|
||||
short_queries = [
|
||||
"AI trends",
|
||||
"python performance",
|
||||
"RAG best practices"
|
||||
]
|
||||
|
||||
for query in short_queries:
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.BASIC)
|
||||
print(f"Original: {query}")
|
||||
print(f"Rewritten: {result.primary_query}")
|
||||
print()
|
||||
|
||||
# 2. HyDE - Hypothetical Document Embeddings
|
||||
print("\n2. HYDE (Hypothetical Document)")
|
||||
print("-" * 40)
|
||||
|
||||
query = "What is quantum computing?"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.HYDE)
|
||||
print(f"Original: {query}")
|
||||
print("Hypothetical Document (truncated):")
|
||||
print(f"{result.hypothetical_document[:300]}...")
|
||||
print()
|
||||
|
||||
# 3. Step-Back - Higher-level concept questions
|
||||
print("\n3. STEP-BACK (Broader Context)")
|
||||
print("-" * 40)
|
||||
|
||||
query = "What is the difference between GPT-4 and Claude 3?"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.STEP_BACK)
|
||||
print(f"Original: {query}")
|
||||
print(f"Rewritten: {result.primary_query}")
|
||||
print(f"Step-back Question: {result.step_back_question}")
|
||||
print()
|
||||
|
||||
# 4. Sub-Queries - Decompose complex questions
|
||||
print("\n4. SUB-QUERIES (Decomposition)")
|
||||
print("-" * 40)
|
||||
|
||||
query = "How do I set up a RAG pipeline with vector search and what are the best embedding models to use?"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.SUB_QUERIES)
|
||||
print(f"Original: {query}")
|
||||
print("Sub-queries:")
|
||||
for i, sq in enumerate(result.sub_queries, 1):
|
||||
print(f" {i}. {sq}")
|
||||
print()
|
||||
|
||||
# 5. Multi-Query - Multiple paraphrased versions
|
||||
print("\n5. MULTI-QUERY (Ensemble Retrieval)")
|
||||
print("-" * 40)
|
||||
|
||||
query = "How to improve LLM response quality?"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.MULTI_QUERY, num_queries=3)
|
||||
print(f"Original: {query}")
|
||||
print("Alternative queries:")
|
||||
for i, q in enumerate(result.rewritten_queries, 1):
|
||||
print(f" {i}. {q}")
|
||||
print()
|
||||
|
||||
# 6. Contextual - Using conversation history
|
||||
print("\n6. CONTEXTUAL (With Chat History)")
|
||||
print("-" * 40)
|
||||
|
||||
chat_history = [
|
||||
{"role": "user", "content": "Tell me about Python programming"},
|
||||
{"role": "assistant", "content": "Python is a high-level programming language known for its simplicity and readability..."},
|
||||
{"role": "user", "content": "What frameworks are popular?"},
|
||||
{"role": "assistant", "content": "Popular Python frameworks include Django for web development, FastAPI for APIs, and PyTorch for ML..."}
|
||||
]
|
||||
|
||||
query = "What about its performance compared to others?"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.CONTEXTUAL, chat_history=chat_history)
|
||||
print(f"Original: {query}")
|
||||
print(f"Standalone: {result.primary_query}")
|
||||
print()
|
||||
|
||||
# 7. AUTO - Automatic strategy detection
|
||||
print("\n7. AUTO (Automatic Strategy Detection)")
|
||||
print("-" * 40)
|
||||
|
||||
test_queries = [
|
||||
("ML", None), # Short query -> BASIC
|
||||
("What about the cost?", chat_history), # Follow-up -> CONTEXTUAL
|
||||
("Compare transformers vs RNNs and explain their use cases", None), # Complex -> SUB_QUERIES
|
||||
]
|
||||
|
||||
for query, history in test_queries:
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.AUTO, chat_history=history)
|
||||
print(f"Query: {query}")
|
||||
print(f"Detected Strategy: {result.strategy_used.value}")
|
||||
print(f"Result: {result.primary_query}")
|
||||
print()
|
||||
|
||||
# 8. Custom Abbreviations
|
||||
print("\n8. CUSTOM ABBREVIATIONS")
|
||||
print("-" * 40)
|
||||
|
||||
agent.add_abbreviations({
|
||||
"K8s": "Kubernetes",
|
||||
"TF": "TensorFlow",
|
||||
"PT": "PyTorch"
|
||||
})
|
||||
|
||||
query = "K8s deployment for TF models"
|
||||
result = agent.rewrite(query, strategy=RewriteStrategy.BASIC)
|
||||
print(f"Original: {query}")
|
||||
print(f"Rewritten: {result.primary_query}")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("Examples Complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return "AI trends: LLMs, multimodal AI, autonomous agents"
|
||||
|
||||
agent = Agent(
|
||||
name="AI Assistant",
|
||||
role="Research and Writing Specialist",
|
||||
llm="gpt-4o-mini",
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
planning_reasoning=True
|
||||
)
|
||||
|
||||
result = agent.start("Research AI trends in 2025 and write a 2 sentence summary")
|
||||
print(result)
|
||||
@@ -0,0 +1,18 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return "AI trends: LLMs, multimodal AI, autonomous agents"
|
||||
|
||||
agent = Agent(
|
||||
name="AI Assistant",
|
||||
role="Research and Writing Specialist",
|
||||
llm="gpt-4o-mini",
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
planning_reasoning=True,
|
||||
verbose=False # Quiet mode
|
||||
)
|
||||
|
||||
result = agent.start("Research AI trends in 2025 and write a 2 sentence summary")
|
||||
print("\n=== FINAL RESULT ===")
|
||||
print(result)
|
||||
@@ -51,7 +51,7 @@ def test_guardrail_return_types():
|
||||
role="Tester",
|
||||
goal="Test guardrails",
|
||||
backstory="Testing guardrail functionality",
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
# Test creating tasks with each guardrail type
|
||||
@@ -105,7 +105,7 @@ def test_guardrail_result_handling():
|
||||
role="Tester",
|
||||
goal="Test guardrails",
|
||||
backstory="Testing guardrail functionality",
|
||||
llm="gpt-4o"
|
||||
llm="gpt-5-nano"
|
||||
)
|
||||
|
||||
test_output = TaskOutput(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return "AI trends: LLMs, multimodal AI, autonomous agents, edge AI"
|
||||
|
||||
print("=" * 60)
|
||||
print("TEST 1: Planning ONLY (no tools, no reasoning)")
|
||||
print("=" * 60)
|
||||
|
||||
agent1 = Agent(
|
||||
name="Assistant",
|
||||
llm="gpt-4o-mini",
|
||||
planning=True,
|
||||
verbose=False
|
||||
)
|
||||
result1 = agent1.start("Write 2 sentences about AI in healthcare")
|
||||
print(f"\nResult: {result1[:200]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 2: Planning + Tools (no reasoning)")
|
||||
print("=" * 60)
|
||||
|
||||
agent2 = Agent(
|
||||
name="Assistant",
|
||||
llm="gpt-4o-mini",
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
verbose=False
|
||||
)
|
||||
result2 = agent2.start("Write 2 sentences about AI trends")
|
||||
print(f"\nResult: {result2[:200]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 3: Planning + Tools + Reasoning")
|
||||
print("=" * 60)
|
||||
|
||||
agent3 = Agent(
|
||||
name="Assistant",
|
||||
llm="gpt-4o-mini",
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
planning_reasoning=True,
|
||||
verbose=False
|
||||
)
|
||||
result3 = agent3.start("Write 2 sentences about AI trends")
|
||||
print(f"\nResult: {result3[:200]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 4: No Planning (direct execution)")
|
||||
print("=" * 60)
|
||||
|
||||
agent4 = Agent(
|
||||
name="Assistant",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
result4 = agent4.start("Write 2 sentences about AI trends")
|
||||
print(f"\nResult: {result4[:200]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("COMPARISON COMPLETE")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Complete Planning Mode Demo: Plan First, Then Execute Todo Items One by One.
|
||||
|
||||
This demonstrates the full workflow:
|
||||
1. Create a plan with the PlanningAgent
|
||||
2. Generate todo list from the plan
|
||||
3. Execute each todo item sequentially with the appropriate agent
|
||||
4. Update progress as items are completed
|
||||
|
||||
Run with: python test_planning_execution.py
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task
|
||||
from praisonaiagents.planning import PlanningAgent, TodoList
|
||||
|
||||
|
||||
def print_separator(title: str):
|
||||
print("\n" + "=" * 70)
|
||||
print(f" {title}")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
|
||||
def print_progress(todo_list: TodoList):
|
||||
"""Print current progress."""
|
||||
completed = len(todo_list.completed)
|
||||
total = len(todo_list.items)
|
||||
progress = todo_list.progress * 100
|
||||
bar_length = 30
|
||||
filled = int(bar_length * todo_list.progress)
|
||||
bar = "█" * filled + "░" * (bar_length - filled)
|
||||
print(f"\n📊 Progress: [{bar}] {progress:.0f}% ({completed}/{total} completed)\n")
|
||||
|
||||
|
||||
def main():
|
||||
print_separator("🎯 PLANNING MODE: PLAN FIRST, EXECUTE TODO ITEMS")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 1: Create Agents
|
||||
# ==========================================================================
|
||||
print("📦 Creating agents...")
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Analyst",
|
||||
goal="Research topics thoroughly and provide accurate information",
|
||||
backstory="Expert researcher with access to vast knowledge bases",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Write clear, engaging, and well-structured content",
|
||||
backstory="Professional writer skilled in creating compelling narratives",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print("✅ Agents created: Researcher, Writer\n")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 2: Create Plan using PlanningAgent
|
||||
# ==========================================================================
|
||||
print_separator("📋 PHASE 1: CREATING PLAN")
|
||||
|
||||
planner = PlanningAgent(
|
||||
llm="gpt-4o-mini",
|
||||
read_only=True,
|
||||
verbose=1
|
||||
)
|
||||
|
||||
request = "Write a short article about the top 3 benefits of meditation for mental health"
|
||||
|
||||
print(f"📝 Request: {request}\n")
|
||||
print("🤔 Planning agent is analyzing and creating a plan...\n")
|
||||
|
||||
plan = planner.create_plan_sync(
|
||||
request=request,
|
||||
agents=[researcher, writer],
|
||||
context="Keep it concise - each section should be 2-3 sentences max"
|
||||
)
|
||||
|
||||
print("\n✅ Plan created successfully!")
|
||||
print(f" Plan Name: {plan.name}")
|
||||
print(f" Total Steps: {len(plan.steps)}")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 3: Display the Plan
|
||||
# ==========================================================================
|
||||
print_separator("📄 GENERATED PLAN")
|
||||
print(plan.to_markdown())
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 4: Create Todo List from Plan
|
||||
# ==========================================================================
|
||||
print_separator("📝 PHASE 2: TODO LIST CREATED")
|
||||
|
||||
todo_list = TodoList.from_plan(plan)
|
||||
print(todo_list.to_markdown())
|
||||
print_progress(todo_list)
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 5: Execute Each Todo Item One by One
|
||||
# ==========================================================================
|
||||
print_separator("🚀 PHASE 3: EXECUTING TODO ITEMS")
|
||||
|
||||
# Map agent names to agent instances
|
||||
agent_map = {
|
||||
"Researcher": researcher,
|
||||
"Writer": writer
|
||||
}
|
||||
|
||||
results = {}
|
||||
|
||||
for i, item in enumerate(todo_list.items):
|
||||
print(f"\n{'─' * 70}")
|
||||
print(f"📌 TODO ITEM {i + 1}/{len(todo_list.items)}")
|
||||
print(f"{'─' * 70}")
|
||||
print(f" Description: {item.description}")
|
||||
print(f" Agent: {item.agent or 'Auto-assign'}")
|
||||
print(f" Status: {item.status}")
|
||||
|
||||
if item.dependencies:
|
||||
print(f" Dependencies: {', '.join(item.dependencies)}")
|
||||
|
||||
# Mark as in progress
|
||||
todo_list.start(item.id)
|
||||
print(f"\n ⏳ Starting execution...")
|
||||
|
||||
# Get the appropriate agent
|
||||
agent_name = item.agent or "Researcher"
|
||||
agent = agent_map.get(agent_name, researcher)
|
||||
|
||||
# Build context from previous results
|
||||
context = ""
|
||||
for dep_id in item.dependencies:
|
||||
if dep_id in results:
|
||||
context += f"\nPrevious result ({dep_id}):\n{results[dep_id]}\n"
|
||||
|
||||
# Create task description
|
||||
task_description = item.description
|
||||
if context:
|
||||
task_description += f"\n\nContext from previous steps:{context}"
|
||||
|
||||
# Execute with the agent
|
||||
try:
|
||||
result = agent.chat(task_description)
|
||||
results[item.id] = result
|
||||
|
||||
# Mark as completed
|
||||
todo_list.complete(item.id)
|
||||
|
||||
print(f"\n ✅ Completed!")
|
||||
print(f"\n 📄 Result Preview:")
|
||||
print(" " + "-" * 50)
|
||||
preview = str(result)[:500] + "..." if len(str(result)) > 500 else str(result)
|
||||
for line in preview.split('\n'):
|
||||
print(f" {line}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n ❌ Error: {e}")
|
||||
results[item.id] = f"Error: {e}"
|
||||
|
||||
print_progress(todo_list)
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 6: Final Summary
|
||||
# ==========================================================================
|
||||
print_separator("🎉 EXECUTION COMPLETE")
|
||||
|
||||
print("📋 Final Todo List Status:")
|
||||
print(todo_list.to_markdown())
|
||||
|
||||
print("\n📊 Final Results Summary:")
|
||||
print("-" * 50)
|
||||
for item in todo_list.items:
|
||||
status_icon = "✅" if item.status == "completed" else "❌"
|
||||
print(f"{status_icon} {item.description[:60]}...")
|
||||
|
||||
print(f"\n🏆 All {len(todo_list.completed)} tasks completed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return f"Results: AI trends include LLMs, multimodal AI, autonomous agents"
|
||||
|
||||
researcher = Agent(name="Researcher", role="Research Analyst", llm="gpt-4o-mini")
|
||||
writer = Agent(name="Writer", role="Content Writer", llm="gpt-4o-mini")
|
||||
|
||||
task1 = Task(description="Research AI trends in 2025", agent=researcher)
|
||||
task2 = Task(description="Write a 2 sentence summary", agent=writer)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
planning_reasoning=True,
|
||||
auto_approve_plan=True
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print(result)
|
||||
@@ -0,0 +1,28 @@
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return "AI trends: LLMs, multimodal AI, autonomous agents"
|
||||
|
||||
agent = Agent(
|
||||
name="AI Assistant",
|
||||
role="Research and Writing Specialist",
|
||||
llm="gpt-4o-mini",
|
||||
instructions="Research AI trends in 2025 and write a 2 sentence summary"
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Research AI trends in 2025 and write a 2 sentence summary",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
planning=True,
|
||||
planning_tools=[search_web],
|
||||
planning_reasoning=True,
|
||||
auto_approve_plan=True
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print(result)
|
||||
@@ -0,0 +1,34 @@
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.planning import PlanningAgent
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
return f"Results for '{query}': LLMs, multimodal AI, autonomous agents"
|
||||
|
||||
researcher = Agent(name="Researcher", role="Research Analyst", llm="gpt-4o-mini")
|
||||
writer = Agent(name="Writer", role="Content Writer", llm="gpt-4o-mini")
|
||||
|
||||
print("\n=== TEST 1: Basic Planning (NO tools) ===")
|
||||
planner1 = PlanningAgent(llm="gpt-4o-mini")
|
||||
plan1 = planner1.create_plan_sync(
|
||||
request="Write about AI trends 2025",
|
||||
agents=[researcher, writer]
|
||||
)
|
||||
print(f"Plan: {plan1.name}, Steps: {len(plan1.steps)}")
|
||||
|
||||
print("\n=== TEST 2: Planning WITH tools ===")
|
||||
planner2 = PlanningAgent(llm="gpt-4o-mini", tools=[search_web])
|
||||
plan2 = planner2.create_plan_sync(
|
||||
request="Write about AI trends 2025",
|
||||
agents=[researcher, writer]
|
||||
)
|
||||
print(f"Plan: {plan2.name}, Steps: {len(plan2.steps)}")
|
||||
|
||||
print("\n=== TEST 3: Planning WITH tools + reasoning ===")
|
||||
planner3 = PlanningAgent(llm="gpt-4o-mini", tools=[search_web], reasoning=True)
|
||||
plan3 = planner3.create_plan_sync(
|
||||
request="Write about AI trends 2025",
|
||||
agents=[researcher, writer]
|
||||
)
|
||||
print(f"Plan: {plan3.name}, Steps: {len(plan3.steps)}")
|
||||
|
||||
print("\n✅ Done!")
|
||||
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
Deep Research Agent Examples
|
||||
|
||||
This file demonstrates how to use the DeepResearchAgent for automated
|
||||
complex research workflows using multiple providers:
|
||||
|
||||
- **OpenAI**: o3-deep-research, o4-mini-deep-research (via Responses API)
|
||||
- **Gemini**: deep-research-pro (via Interactions API)
|
||||
- **LiteLLM**: Unified interface for OpenAI models
|
||||
|
||||
Requirements:
|
||||
- OPENAI_API_KEY for OpenAI/LiteLLM providers
|
||||
- GEMINI_API_KEY for Gemini provider
|
||||
- OpenAI SDK >= 1.0.0
|
||||
- google-genai for Gemini (pip install google-genai)
|
||||
- litellm for LiteLLM provider (pip install litellm)
|
||||
|
||||
Available models:
|
||||
OpenAI:
|
||||
- o3-deep-research: Higher quality, in-depth synthesis
|
||||
- o4-mini-deep-research: Faster, lightweight
|
||||
|
||||
Gemini:
|
||||
- deep-research-pro: Gemini 3 Pro powered research agent
|
||||
"""
|
||||
|
||||
from praisonaiagents import DeepResearchAgent, Provider
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 1: Basic Research Query
|
||||
# =============================================================================
|
||||
def basic_research_example():
|
||||
"""Simple research query with default settings."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Research Assistant",
|
||||
instructions="""
|
||||
You are a professional researcher. Focus on:
|
||||
- Data-rich insights with specific figures and trends
|
||||
- Reliable sources: peer-reviewed research, official reports
|
||||
- Include inline citations
|
||||
""",
|
||||
model="o4-mini-deep-research", # Use faster model
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research(
|
||||
"What are the key trends in renewable energy adoption in 2024?"
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("RESEARCH REPORT")
|
||||
print("=" * 60)
|
||||
print(result.report)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("CITATIONS")
|
||||
print("=" * 60)
|
||||
for i, citation in enumerate(result.citations, 1):
|
||||
print(f"{i}. {citation.title}")
|
||||
print(f" URL: {citation.url}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 2: Gemini Deep Research
|
||||
# =============================================================================
|
||||
def gemini_research_example():
|
||||
"""Research using Gemini Deep Research Agent."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Gemini Researcher",
|
||||
instructions="""
|
||||
You are a professional researcher. Focus on:
|
||||
- Comprehensive analysis with multiple perspectives
|
||||
- Data-backed insights and trends
|
||||
- Clear structure with executive summary
|
||||
""",
|
||||
model="deep-research-pro", # Gemini model - auto-detected
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Using provider: {agent.provider}") # Should be Provider.GEMINI
|
||||
|
||||
result = agent.research(
|
||||
"What are the latest breakthroughs in fusion energy research?"
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("GEMINI RESEARCH REPORT")
|
||||
print("=" * 60)
|
||||
print(result.report[:1000] + "..." if len(result.report) > 1000 else result.report)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 3: Research with Code Interpreter (OpenAI)
|
||||
# =============================================================================
|
||||
def research_with_code_example():
|
||||
"""Research with code execution for data analysis."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Data Analyst",
|
||||
instructions="""
|
||||
You are a data analyst. When appropriate:
|
||||
- Use code to analyze data and create visualizations
|
||||
- Summarize data in tables
|
||||
- Include specific statistics and metrics
|
||||
""",
|
||||
model="o3-deep-research",
|
||||
enable_code_interpreter=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research(
|
||||
"Analyze the global smartphone market share trends over the past 5 years.",
|
||||
summary_mode="detailed"
|
||||
)
|
||||
|
||||
# Check if any code was executed
|
||||
if result.code_executions:
|
||||
print("\nCode executed during research:")
|
||||
for code in result.code_executions:
|
||||
print(f" Input: {code.input_code[:100]}...")
|
||||
if code.output:
|
||||
print(f" Output: {code.output[:100]}...")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 4: Using LiteLLM Provider
|
||||
# =============================================================================
|
||||
def litellm_research_example():
|
||||
"""Research using LiteLLM for unified interface."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="LiteLLM Researcher",
|
||||
instructions="You are a professional researcher.",
|
||||
model="o3-deep-research",
|
||||
use_litellm=True, # Use LiteLLM instead of direct OpenAI
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Using provider: {agent.provider}") # Should be Provider.LITELLM
|
||||
|
||||
result = agent.research(
|
||||
"What are the key trends in sustainable agriculture?"
|
||||
)
|
||||
|
||||
print(f"Report length: {len(result.report)} characters")
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: Research with MCP Integration (OpenAI)
|
||||
# =============================================================================
|
||||
def research_with_mcp_example():
|
||||
"""Research with custom MCP server for internal documents."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Internal Researcher",
|
||||
instructions="""
|
||||
You are a researcher with access to internal documents.
|
||||
- Use the internal file lookup tool to retrieve company data
|
||||
- Combine internal data with public research
|
||||
- Prioritize internal sources when available
|
||||
""",
|
||||
model="o3-deep-research",
|
||||
mcp_servers=[
|
||||
{
|
||||
"label": "internal_docs",
|
||||
"url": "https://your-mcp-server.com/sse/",
|
||||
"require_approval": "never"
|
||||
}
|
||||
],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research(
|
||||
"What is our company's market position compared to competitors?"
|
||||
)
|
||||
|
||||
# Check MCP calls
|
||||
if result.mcp_calls:
|
||||
print("\nMCP tools used:")
|
||||
for call in result.mcp_calls:
|
||||
print(f" - {call.name}@{call.server_label}: {call.arguments}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 6: Gemini with File Search
|
||||
# =============================================================================
|
||||
def gemini_file_search_example():
|
||||
"""Research using Gemini with your own data via File Search."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Document Researcher",
|
||||
instructions="""
|
||||
Compare our internal documents against current public information.
|
||||
Prioritize insights from our internal data.
|
||||
""",
|
||||
model="deep-research-pro",
|
||||
enable_file_search=True,
|
||||
file_search_stores=["fileSearchStores/my-store-name"],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research(
|
||||
"Compare our 2025 fiscal year report against current market trends."
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 7: Clarifying Questions Workflow
|
||||
# =============================================================================
|
||||
def clarifying_workflow_example():
|
||||
"""Use clarifying questions before research."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Research Assistant",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Step 1: Get clarifying questions
|
||||
initial_query = "Help me research AI trends"
|
||||
questions = agent.clarify(initial_query)
|
||||
|
||||
print("Clarifying Questions:")
|
||||
print(questions)
|
||||
|
||||
# Step 2: User provides more details (simulated)
|
||||
detailed_query = """
|
||||
I'd like to research AI trends in healthcare specifically.
|
||||
Focus on:
|
||||
- Diagnostic AI applications
|
||||
- FDA-approved AI tools
|
||||
- Cost savings for hospitals
|
||||
Time period: 2023-2024
|
||||
"""
|
||||
|
||||
# Step 3: Rewrite the query for optimal research
|
||||
rewritten = agent.rewrite_query(detailed_query)
|
||||
print("\nRewritten Query:")
|
||||
print(rewritten)
|
||||
|
||||
# Step 4: Perform the research
|
||||
result = agent.research(rewritten)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: Async Research
|
||||
# =============================================================================
|
||||
async def async_research_example():
|
||||
"""Perform research asynchronously."""
|
||||
import asyncio
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Async Researcher",
|
||||
model="o4-mini-deep-research",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Run multiple research queries concurrently
|
||||
queries = [
|
||||
"What are the latest developments in quantum computing?",
|
||||
"What are the trends in electric vehicle adoption?",
|
||||
]
|
||||
|
||||
tasks = [agent.aresearch(q) for q in queries]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
for query, result in zip(queries, results):
|
||||
print(f"\nQuery: {query}")
|
||||
print(f"Report length: {len(result.report)} chars")
|
||||
print(f"Citations: {len(result.citations)}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 6: Inspecting Intermediate Steps
|
||||
# =============================================================================
|
||||
def inspect_steps_example():
|
||||
"""Inspect the research process steps."""
|
||||
|
||||
agent = DeepResearchAgent(
|
||||
name="Detailed Researcher",
|
||||
model="o4-mini-deep-research",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.research(
|
||||
"What are the environmental impacts of cryptocurrency mining?"
|
||||
)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("RESEARCH PROCESS ANALYSIS")
|
||||
print("=" * 60)
|
||||
|
||||
# Reasoning steps
|
||||
print(f"\nReasoning Steps ({len(result.reasoning_steps)}):")
|
||||
for i, step in enumerate(result.reasoning_steps, 1):
|
||||
print(f" {i}. {step.text[:100]}...")
|
||||
|
||||
# Web searches
|
||||
print(f"\nWeb Searches ({len(result.web_searches)}):")
|
||||
for search in result.web_searches:
|
||||
print(f" - Query: {search.query}")
|
||||
print(f" Status: {search.status}")
|
||||
|
||||
# Sources
|
||||
print(f"\nUnique Sources ({len(result.get_all_sources())}):")
|
||||
for source in result.get_all_sources()[:10]:
|
||||
print(f" - {source['title']}")
|
||||
print(f" {source['url']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
print("Please set it before running examples:")
|
||||
print(" export OPENAI_API_KEY='your-api-key'")
|
||||
exit(1)
|
||||
|
||||
print("Running basic research example...")
|
||||
result = basic_research_example()
|
||||
print(f"\nCompleted! Report has {len(result.report)} characters and {len(result.citations)} citations.")
|
||||
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user