mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||
|
||||
@@ -54,6 +54,10 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
|
||||
- 📄 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)
|
||||
|
||||
## Using Python Code
|
||||
|
||||
@@ -135,6 +139,41 @@ print(result.report)
|
||||
- 🛠️ Built-in tools: web search, code interpreter, MCP, file search
|
||||
- 🔄 Automatic provider detection from model name
|
||||
|
||||
### 4. 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
|
||||
|
||||
## Using No Code
|
||||
|
||||
### Auto Mode:
|
||||
@@ -144,6 +183,51 @@ 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"
|
||||
```
|
||||
|
||||
## Using JavaScript Code
|
||||
|
||||
```bash
|
||||
@@ -562,6 +646,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.
|
||||
@@ -579,6 +829,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.
|
||||
|
||||
+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.89" \
|
||||
"praisonai>=2.3.2" \
|
||||
"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.89" \
|
||||
"praisonai>=2.3.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.2.89" \
|
||||
"praisonai>=2.3.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.2.89" \
|
||||
"praisonai>=2.3.2" \
|
||||
"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.89`
|
||||
- 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.89" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.95" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -136,7 +136,45 @@ result = agent.research("Research quantum computing advances")
|
||||
- **Citations**: Structured citations with URLs
|
||||
- **Tools**: Web search, code interpreter, MCP, file search
|
||||
|
||||
### 3. ImageAgent
|
||||
### 3. QueryRewriterAgent 🆕
|
||||
Transform user queries to improve RAG retrieval quality using multiple strategies.
|
||||
|
||||
```python
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini", verbose=True)
|
||||
|
||||
# Basic rewriting - expands abbreviations, adds context
|
||||
result = agent.rewrite("AI trends")
|
||||
print(result.primary_query)
|
||||
# Output: "What are the current trends in Artificial Intelligence (AI)?"
|
||||
|
||||
# HyDE - generates hypothetical document for semantic matching
|
||||
result = agent.rewrite("What is quantum computing?", strategy=RewriteStrategy.HYDE)
|
||||
print(result.hypothetical_document)
|
||||
|
||||
# Step-back - generates broader context question
|
||||
result = agent.rewrite("Difference between GPT-4 and Claude 3?", strategy=RewriteStrategy.STEP_BACK)
|
||||
print(result.step_back_question)
|
||||
|
||||
# Sub-queries - decomposes complex questions
|
||||
result = agent.rewrite("How to set up RAG and what embedding models to use?", strategy=RewriteStrategy.SUB_QUERIES)
|
||||
print(result.sub_queries)
|
||||
|
||||
# Contextual - uses chat history to resolve references
|
||||
result = agent.rewrite("What about its cost?", strategy=RewriteStrategy.CONTEXTUAL, chat_history=[...])
|
||||
```
|
||||
|
||||
**Strategies:**
|
||||
- **BASIC**: Expand abbreviations, fix typos, add context
|
||||
- **HYDE**: Generate hypothetical document for better 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
|
||||
|
||||
### 4. ImageAgent
|
||||
Agent specialized for image generation and analysis.
|
||||
|
||||
```python
|
||||
@@ -150,7 +188,7 @@ agent = ImageAgent(
|
||||
result = agent.chat("Create an image of a sunset over mountains")
|
||||
```
|
||||
|
||||
### 4. ContextAgent
|
||||
### 5. ContextAgent
|
||||
Agent with persistent context across conversations.
|
||||
|
||||
```python
|
||||
@@ -162,7 +200,7 @@ agent = ContextAgent(
|
||||
)
|
||||
```
|
||||
|
||||
### 5. RouterAgent
|
||||
### 6. RouterAgent
|
||||
Routes requests to appropriate specialized agents.
|
||||
|
||||
```python
|
||||
@@ -309,6 +347,92 @@ agent = Agent(
|
||||
|
||||
---
|
||||
|
||||
## Native Web Search
|
||||
|
||||
Enable real-time web search capabilities using LiteLLM's native web search.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Enable web search with a supported model
|
||||
agent = Agent(
|
||||
name="Researcher",
|
||||
instructions="You are a helpful research assistant.",
|
||||
llm="openai/gpt-4o-search-preview",
|
||||
web_search=True
|
||||
)
|
||||
|
||||
result = agent.start("What are the latest developments in AI today?")
|
||||
```
|
||||
|
||||
**Supported Providers:**
|
||||
- **OpenAI**: `gpt-4o-search-preview`, `gpt-4o-mini-search-preview`
|
||||
- **Anthropic**: `claude-3-5-sonnet-latest`, `claude-sonnet-4`
|
||||
- **Google/Gemini**: `gemini-2.0-flash`, `gemini-2.5-pro`
|
||||
- **xAI**: `grok-3`
|
||||
- **Perplexity**: All models
|
||||
|
||||
---
|
||||
|
||||
## Web Fetch
|
||||
|
||||
Retrieve full content from specific URLs (Anthropic models only).
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Enable web fetch with an Anthropic model
|
||||
agent = Agent(
|
||||
name="Content Analyzer",
|
||||
instructions="You can fetch and analyze web content.",
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
web_fetch=True
|
||||
)
|
||||
|
||||
result = agent.start("Fetch and summarize https://example.com")
|
||||
```
|
||||
|
||||
**Web Fetch Options:**
|
||||
```python
|
||||
agent = Agent(
|
||||
llm="anthropic/claude-sonnet-4-20250514",
|
||||
web_fetch={
|
||||
"max_uses": 10,
|
||||
"allowed_domains": ["example.com"],
|
||||
"citations": {"enabled": True}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Caching
|
||||
|
||||
Reduce costs and latency by caching parts of prompts.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Enable prompt caching for Anthropic models
|
||||
agent = Agent(
|
||||
name="Legal Analyst",
|
||||
instructions="You are an AI assistant analyzing legal documents." * 50, # Long prompt
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
prompt_caching=True
|
||||
)
|
||||
|
||||
# First call creates cache, subsequent calls use cached tokens
|
||||
result = agent.start("Summarize the key terms")
|
||||
```
|
||||
|
||||
**Supported Providers:**
|
||||
- **OpenAI** (`openai/`) - Automatic caching for prompts ≥1024 tokens
|
||||
- **Anthropic** (`anthropic/`) - Manual caching with `cache_control`
|
||||
- **Bedrock** (`bedrock/`) - All models supporting prompt caching
|
||||
- **Deepseek** (`deepseek/`) - Works like OpenAI
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
Add safety checks and validation to agent responses.
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -26,9 +26,22 @@ from .agent.deep_research_agent import (
|
||||
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
|
||||
@@ -165,6 +178,17 @@ __all__ = [
|
||||
'Agents',
|
||||
'Tools',
|
||||
'Task',
|
||||
# Plugin system
|
||||
'BaseTool',
|
||||
'ToolResult',
|
||||
'ToolValidationError',
|
||||
'validate_tool',
|
||||
'tool',
|
||||
'FunctionTool',
|
||||
'ToolRegistry',
|
||||
'get_registry',
|
||||
'register_tool',
|
||||
'get_tool',
|
||||
'TaskOutput',
|
||||
'ReflectionOutput',
|
||||
'AutoAgents',
|
||||
@@ -206,7 +230,13 @@ __all__ = [
|
||||
'CodeExecutionStep',
|
||||
'MCPCall',
|
||||
'FileSearchCall',
|
||||
'Provider'
|
||||
'Provider',
|
||||
'QueryRewriterAgent',
|
||||
'RewriteStrategy',
|
||||
'RewriteResult',
|
||||
'PromptExpanderAgent',
|
||||
'ExpandStrategy',
|
||||
'ExpandResult'
|
||||
]
|
||||
|
||||
# Add MCP to __all__ if available
|
||||
|
||||
@@ -15,6 +15,16 @@ from .deep_research_agent import (
|
||||
FileSearchCall,
|
||||
Provider
|
||||
)
|
||||
from .query_rewriter_agent import (
|
||||
QueryRewriterAgent,
|
||||
RewriteStrategy,
|
||||
RewriteResult
|
||||
)
|
||||
from .prompt_expander_agent import (
|
||||
PromptExpanderAgent,
|
||||
ExpandStrategy,
|
||||
ExpandResult
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Agent',
|
||||
@@ -35,5 +45,11 @@ __all__ = [
|
||||
'CodeExecutionStep',
|
||||
'MCPCall',
|
||||
'FileSearchCall',
|
||||
'Provider'
|
||||
'Provider',
|
||||
'QueryRewriterAgent',
|
||||
'RewriteStrategy',
|
||||
'RewriteResult',
|
||||
'PromptExpanderAgent',
|
||||
'ExpandStrategy',
|
||||
'ExpandResult'
|
||||
]
|
||||
@@ -234,7 +234,10 @@ 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
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
@@ -400,7 +403,10 @@ class Agent:
|
||||
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
|
||||
)
|
||||
self._using_custom_llm = True
|
||||
except ImportError as e:
|
||||
@@ -435,6 +441,9 @@ 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
|
||||
self.llm_instance = LLM(**llm_params)
|
||||
self._using_custom_llm = True
|
||||
|
||||
@@ -455,6 +464,15 @@ class Agent:
|
||||
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 []
|
||||
@@ -502,6 +520,22 @@ Your Goal: {self.goal}
|
||||
# Store user_id
|
||||
self.user_id = user_id or "praison"
|
||||
self.reasoning_steps = reasoning_steps
|
||||
self.web_search = web_search
|
||||
self.web_fetch = web_fetch
|
||||
self.prompt_caching = prompt_caching
|
||||
|
||||
# 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,6 +601,69 @@ Your Goal: {self.goal}
|
||||
self._agent_id = str(uuid.uuid4())
|
||||
return self._agent_id
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def llm_model(self):
|
||||
"""Unified property to get the LLM model regardless of configuration type.
|
||||
@@ -1008,6 +1105,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 +1227,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 +1259,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()
|
||||
|
||||
@@ -254,7 +254,17 @@ class DeepResearchAgent:
|
||||
max_wait_time: Maximum seconds to wait for research completion
|
||||
"""
|
||||
self.name = name or "DeepResearchAgent"
|
||||
self.instructions = instructions
|
||||
# Default instructions for comprehensive research
|
||||
default_instructions = """
|
||||
You are a professional research analyst. When conducting research:
|
||||
- Include specific figures, trends, statistics, and measurable outcomes
|
||||
- Prioritize reliable, up-to-date sources: peer-reviewed research, official organizations, regulatory agencies
|
||||
- Include inline citations and return all source metadata
|
||||
- Be analytical, avoid generalities, and ensure data-backed reasoning
|
||||
- Structure your response with clear headers and formatting
|
||||
- If creating comparisons, use tables for clarity
|
||||
"""
|
||||
self.instructions = instructions or default_instructions
|
||||
self.verbose = verbose
|
||||
self.summary_mode = summary_mode
|
||||
self.enable_web_search = enable_web_search
|
||||
@@ -288,7 +298,7 @@ class DeepResearchAgent:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.info(f"DeepResearchAgent initialized: provider={self.provider.value}, model={self.model}")
|
||||
self.logger.debug(f"DeepResearchAgent initialized: provider={self.provider.value}, model={self.model}")
|
||||
|
||||
def _detect_provider(
|
||||
self,
|
||||
@@ -766,7 +776,7 @@ class DeepResearchAgent:
|
||||
interaction = self.gemini_client.interactions.create(**create_params)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.info(f"Gemini research started: {interaction.id}")
|
||||
self.logger.debug(f"Gemini research started: {interaction.id}")
|
||||
|
||||
start_time = time.time()
|
||||
while True:
|
||||
@@ -774,7 +784,7 @@ class DeepResearchAgent:
|
||||
|
||||
if interaction.status == "completed":
|
||||
if self.verbose:
|
||||
self.logger.info("Gemini research completed")
|
||||
self.logger.debug("Gemini research completed")
|
||||
break
|
||||
elif interaction.status == "failed":
|
||||
error = getattr(interaction, 'error', 'Unknown error')
|
||||
@@ -789,7 +799,7 @@ class DeepResearchAgent:
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.info(f"Research in progress... ({elapsed:.0f}s)")
|
||||
self.logger.debug(f"Research in progress... ({elapsed:.0f}s)")
|
||||
|
||||
time.sleep(self.poll_interval)
|
||||
|
||||
@@ -1008,7 +1018,7 @@ class DeepResearchAgent:
|
||||
start_time = time.time()
|
||||
|
||||
if self.verbose and not stream:
|
||||
self.logger.info(f"Starting {self.provider.value} deep research: {query[:100]}...")
|
||||
self.logger.debug(f"Starting {self.provider.value} deep research: {query[:100]}...")
|
||||
|
||||
try:
|
||||
if self.provider == Provider.GEMINI:
|
||||
@@ -1049,8 +1059,8 @@ class DeepResearchAgent:
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
if self.verbose:
|
||||
self.logger.info(f"Deep research completed in {elapsed:.1f}s")
|
||||
self.logger.info(f"Report length: {len(result.report)} chars, Citations: {len(result.citations)}")
|
||||
self.logger.debug(f"Deep research completed in {elapsed:.1f}s")
|
||||
self.logger.debug(f"Report length: {len(result.report)} chars, Citations: {len(result.citations)}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -1076,7 +1086,7 @@ class DeepResearchAgent:
|
||||
start_time = time.time()
|
||||
|
||||
if self.verbose:
|
||||
self.logger.info(f"Starting async {self.provider.value} deep research: {query[:100]}...")
|
||||
self.logger.debug(f"Starting async {self.provider.value} deep research: {query[:100]}...")
|
||||
|
||||
try:
|
||||
if self.provider == Provider.GEMINI:
|
||||
@@ -1151,7 +1161,7 @@ class DeepResearchAgent:
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
if self.verbose:
|
||||
self.logger.info(f"Async deep research completed in {elapsed:.1f}s")
|
||||
self.logger.debug(f"Async deep research completed in {elapsed:.1f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -239,6 +239,9 @@ 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,
|
||||
**extra_settings
|
||||
):
|
||||
# Configure logging only once at the class level
|
||||
@@ -273,6 +276,9 @@ 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._console = None # Lazy load console when needed
|
||||
self.chat_history = []
|
||||
self.verbose = extra_settings.get('verbose', True)
|
||||
@@ -381,6 +387,45 @@ 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 _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 +769,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:
|
||||
@@ -2295,6 +2354,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()
|
||||
@@ -3171,6 +3236,57 @@ 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}")
|
||||
|
||||
return params
|
||||
|
||||
def _prepare_response_logging(self, temperature: float, stream: bool, verbose: bool, markdown: bool, **kwargs) -> Optional[Dict[str, Any]]:
|
||||
|
||||
@@ -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-5-nano",
|
||||
"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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.163"
|
||||
version = "0.0.175"
|
||||
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,403 @@
|
||||
"""
|
||||
Tests for QueryRewriterAgent
|
||||
|
||||
Run with: python -m pytest tests/query_rewriter_test.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestQueryRewriterImports:
|
||||
"""Test that all imports work correctly."""
|
||||
|
||||
def test_import_from_package(self):
|
||||
"""Test importing from main package."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy, RewriteResult
|
||||
assert QueryRewriterAgent is not None
|
||||
assert RewriteStrategy is not None
|
||||
assert RewriteResult is not None
|
||||
|
||||
def test_import_from_agent_module(self):
|
||||
"""Test importing from agent module."""
|
||||
from praisonaiagents.agent import QueryRewriterAgent, RewriteStrategy, RewriteResult
|
||||
assert QueryRewriterAgent is not None
|
||||
assert RewriteStrategy is not None
|
||||
assert RewriteResult is not None
|
||||
|
||||
|
||||
class TestRewriteStrategy:
|
||||
"""Test RewriteStrategy enum."""
|
||||
|
||||
def test_strategy_values(self):
|
||||
"""Test all strategy values exist."""
|
||||
from praisonaiagents import RewriteStrategy
|
||||
|
||||
assert RewriteStrategy.BASIC.value == "basic"
|
||||
assert RewriteStrategy.HYDE.value == "hyde"
|
||||
assert RewriteStrategy.STEP_BACK.value == "step_back"
|
||||
assert RewriteStrategy.SUB_QUERIES.value == "sub_queries"
|
||||
assert RewriteStrategy.MULTI_QUERY.value == "multi_query"
|
||||
assert RewriteStrategy.CONTEXTUAL.value == "contextual"
|
||||
assert RewriteStrategy.AUTO.value == "auto"
|
||||
|
||||
|
||||
class TestRewriteResult:
|
||||
"""Test RewriteResult dataclass."""
|
||||
|
||||
def test_result_creation(self):
|
||||
"""Test creating a RewriteResult."""
|
||||
from praisonaiagents import RewriteResult, RewriteStrategy
|
||||
|
||||
result = RewriteResult(
|
||||
original_query="test query",
|
||||
rewritten_queries=["rewritten test query"],
|
||||
strategy_used=RewriteStrategy.BASIC
|
||||
)
|
||||
|
||||
assert result.original_query == "test query"
|
||||
assert result.rewritten_queries == ["rewritten test query"]
|
||||
assert result.strategy_used == RewriteStrategy.BASIC
|
||||
|
||||
def test_primary_query_property(self):
|
||||
"""Test primary_query property."""
|
||||
from praisonaiagents import RewriteResult, RewriteStrategy
|
||||
|
||||
result = RewriteResult(
|
||||
original_query="test",
|
||||
rewritten_queries=["first", "second"],
|
||||
strategy_used=RewriteStrategy.MULTI_QUERY
|
||||
)
|
||||
|
||||
assert result.primary_query == "first"
|
||||
|
||||
def test_primary_query_fallback(self):
|
||||
"""Test primary_query falls back to original if empty."""
|
||||
from praisonaiagents import RewriteResult, RewriteStrategy
|
||||
|
||||
result = RewriteResult(
|
||||
original_query="test",
|
||||
rewritten_queries=[],
|
||||
strategy_used=RewriteStrategy.BASIC
|
||||
)
|
||||
|
||||
assert result.primary_query == "test"
|
||||
|
||||
def test_all_queries_property(self):
|
||||
"""Test all_queries property combines all queries."""
|
||||
from praisonaiagents import RewriteResult, RewriteStrategy
|
||||
|
||||
result = RewriteResult(
|
||||
original_query="original",
|
||||
rewritten_queries=["rewritten"],
|
||||
strategy_used=RewriteStrategy.STEP_BACK,
|
||||
step_back_question="step back",
|
||||
sub_queries=["sub1", "sub2"]
|
||||
)
|
||||
|
||||
all_q = result.all_queries
|
||||
assert "original" in all_q
|
||||
assert "rewritten" in all_q
|
||||
assert "step back" in all_q
|
||||
assert "sub1" in all_q
|
||||
assert "sub2" in all_q
|
||||
|
||||
|
||||
class TestQueryRewriterAgentInit:
|
||||
"""Test QueryRewriterAgent initialization."""
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test agent initializes with defaults."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert agent.name == "QueryRewriterAgent"
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
assert not agent.verbose
|
||||
assert agent.max_queries == 5
|
||||
assert agent.temperature == 0.3
|
||||
assert agent.max_tokens == 500
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test agent initializes with custom values."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent(
|
||||
name="CustomRewriter",
|
||||
model="gpt-4o",
|
||||
verbose=True,
|
||||
max_queries=10,
|
||||
temperature=0.5,
|
||||
max_tokens=1000
|
||||
)
|
||||
|
||||
assert agent.name == "CustomRewriter"
|
||||
assert agent.model == "gpt-4o"
|
||||
assert agent.verbose
|
||||
assert agent.max_queries == 10
|
||||
assert agent.temperature == 0.5
|
||||
assert agent.max_tokens == 1000
|
||||
|
||||
def test_default_abbreviations(self):
|
||||
"""Test default abbreviations are set."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert "AI" in agent.abbreviations
|
||||
assert "ML" in agent.abbreviations
|
||||
assert "RAG" in agent.abbreviations
|
||||
assert agent.abbreviations["AI"] == "Artificial Intelligence"
|
||||
|
||||
|
||||
class TestAbbreviationExpansion:
|
||||
"""Test abbreviation expansion functionality."""
|
||||
|
||||
def test_expand_abbreviations(self):
|
||||
"""Test _expand_abbreviations method."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
result = agent._expand_abbreviations("AI trends")
|
||||
assert "Artificial Intelligence" in result
|
||||
|
||||
def test_add_abbreviation(self):
|
||||
"""Test adding single abbreviation."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
agent.add_abbreviation("K8s", "Kubernetes")
|
||||
|
||||
assert "K8S" in agent.abbreviations
|
||||
assert agent.abbreviations["K8S"] == "Kubernetes"
|
||||
|
||||
def test_add_abbreviations(self):
|
||||
"""Test adding multiple abbreviations."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
agent.add_abbreviations({
|
||||
"TF": "TensorFlow",
|
||||
"PT": "PyTorch"
|
||||
})
|
||||
|
||||
assert "TF" in agent.abbreviations
|
||||
assert "PT" in agent.abbreviations
|
||||
|
||||
|
||||
class TestQueryDetection:
|
||||
"""Test query analysis methods."""
|
||||
|
||||
def test_is_short_query(self):
|
||||
"""Test short query detection."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert agent._is_short_query("AI")
|
||||
assert agent._is_short_query("AI trends")
|
||||
assert not agent._is_short_query("What are the latest AI trends in 2025?")
|
||||
|
||||
def test_is_follow_up(self):
|
||||
"""Test follow-up query detection."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert agent._is_follow_up("What about it?")
|
||||
assert agent._is_follow_up("And this one?")
|
||||
assert not agent._is_follow_up("What is AI?")
|
||||
|
||||
def test_is_complex_query(self):
|
||||
"""Test complex query detection."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert agent._is_complex_query("What is A and what is B?")
|
||||
assert agent._is_complex_query("Question one? Question two?")
|
||||
assert not agent._is_complex_query("What is AI?")
|
||||
|
||||
|
||||
class TestStrategyDetection:
|
||||
"""Test automatic strategy detection."""
|
||||
|
||||
def test_detect_basic_for_short(self):
|
||||
"""Test BASIC detected for short queries."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
strategy = agent._detect_strategy("AI")
|
||||
assert strategy == RewriteStrategy.BASIC
|
||||
|
||||
def test_detect_contextual_for_followup(self):
|
||||
"""Test CONTEXTUAL detected for follow-up with history."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
chat_history = [{"role": "user", "content": "Tell me about Python"}]
|
||||
|
||||
strategy = agent._detect_strategy("What about it?", chat_history)
|
||||
assert strategy == RewriteStrategy.CONTEXTUAL
|
||||
|
||||
def test_detect_sub_queries_for_complex(self):
|
||||
"""Test SUB_QUERIES detected for complex queries."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
strategy = agent._detect_strategy("What is A and what is B?")
|
||||
assert strategy == RewriteStrategy.SUB_QUERIES
|
||||
|
||||
|
||||
class TestJSONParsing:
|
||||
"""Test JSON array parsing from LLM responses."""
|
||||
|
||||
def test_parse_valid_json(self):
|
||||
"""Test parsing valid JSON array."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
result = agent._parse_json_array('["query1", "query2"]')
|
||||
assert result == ["query1", "query2"]
|
||||
|
||||
def test_parse_json_in_text(self):
|
||||
"""Test extracting JSON from surrounding text."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
result = agent._parse_json_array('Here are the queries: ["q1", "q2"]')
|
||||
assert result == ["q1", "q2"]
|
||||
|
||||
def test_parse_fallback_newlines(self):
|
||||
"""Test fallback to newline splitting."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
result = agent._parse_json_array("1. Query one\n2. Query two\n3. Query three")
|
||||
assert len(result) >= 2
|
||||
|
||||
|
||||
class TestRewriteWithMockedLLM:
|
||||
"""Test rewrite methods with mocked LLM."""
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_basic(self, mock_llm):
|
||||
"""Test basic rewriting with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
mock_llm.return_value = "What are the current trends in AI?"
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
result = agent.rewrite("AI trends", strategy=RewriteStrategy.BASIC)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.BASIC
|
||||
assert len(result.rewritten_queries) > 0
|
||||
mock_llm.assert_called_once()
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_hyde(self, mock_llm):
|
||||
"""Test HyDE rewriting with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
mock_llm.return_value = "Quantum computing is a type of computation..."
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
result = agent.rewrite("What is quantum computing?", strategy=RewriteStrategy.HYDE)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.HYDE
|
||||
assert result.hypothetical_document is not None
|
||||
mock_llm.assert_called_once()
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_step_back(self, mock_llm):
|
||||
"""Test step-back rewriting with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
# First call for step-back, second for basic
|
||||
mock_llm.side_effect = [
|
||||
"What are the fundamentals of language models?",
|
||||
"What are the differences between GPT-4 and Claude 3?"
|
||||
]
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
result = agent.rewrite("GPT-4 vs Claude 3?", strategy=RewriteStrategy.STEP_BACK)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.STEP_BACK
|
||||
assert result.step_back_question is not None
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_sub_queries(self, mock_llm):
|
||||
"""Test sub-query decomposition with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
mock_llm.return_value = '["What is RAG?", "What are the best embedding models?"]'
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
result = agent.rewrite("RAG setup and embedding models?", strategy=RewriteStrategy.SUB_QUERIES)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.SUB_QUERIES
|
||||
assert result.sub_queries is not None
|
||||
assert len(result.sub_queries) >= 1
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_multi_query(self, mock_llm):
|
||||
"""Test multi-query generation with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
mock_llm.return_value = '["Query 1", "Query 2", "Query 3"]'
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
result = agent.rewrite("LLM quality?", strategy=RewriteStrategy.MULTI_QUERY, num_queries=3)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.MULTI_QUERY
|
||||
assert len(result.rewritten_queries) >= 1
|
||||
|
||||
@patch.object(__import__('praisonaiagents.agent.query_rewriter_agent', fromlist=['QueryRewriterAgent']).QueryRewriterAgent, '_call_agent')
|
||||
def test_rewrite_contextual(self, mock_llm):
|
||||
"""Test contextual rewriting with mocked LLM."""
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
mock_llm.return_value = "How does Python's performance compare to other languages?"
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
chat_history = [
|
||||
{"role": "user", "content": "Tell me about Python"},
|
||||
{"role": "assistant", "content": "Python is a programming language..."}
|
||||
]
|
||||
|
||||
result = agent.rewrite(
|
||||
"What about its performance?",
|
||||
strategy=RewriteStrategy.CONTEXTUAL,
|
||||
chat_history=chat_history
|
||||
)
|
||||
|
||||
assert result.strategy_used == RewriteStrategy.CONTEXTUAL
|
||||
assert "Python" in result.primary_query or len(result.rewritten_queries) > 0
|
||||
|
||||
|
||||
class TestConvenienceMethods:
|
||||
"""Test convenience methods."""
|
||||
|
||||
def test_convenience_methods_exist(self):
|
||||
"""Test all convenience methods exist."""
|
||||
from praisonaiagents import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
assert hasattr(agent, 'rewrite_basic')
|
||||
assert hasattr(agent, 'rewrite_hyde')
|
||||
assert hasattr(agent, 'rewrite_step_back')
|
||||
assert hasattr(agent, 'rewrite_sub_queries')
|
||||
assert hasattr(agent, 'rewrite_multi_query')
|
||||
assert hasattr(agent, 'rewrite_contextual')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -11,6 +11,7 @@ This test script validates the ContextAgent implementation for:
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to Python path
|
||||
@@ -34,11 +35,11 @@ def test_imports():
|
||||
from praisonaiagents.agent.context_agent import ContextAgent as DirectContextAgent
|
||||
print("✅ Successfully imported ContextAgent from direct module")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
def test_basic_instantiation():
|
||||
"""Test basic ContextAgent instantiation."""
|
||||
@@ -64,11 +65,11 @@ def test_basic_instantiation():
|
||||
factory_agent = create_context_agent(llm="gpt-5-nano")
|
||||
print("✅ Successfully created ContextAgent using factory function")
|
||||
|
||||
return True, [context_agent, custom_agent, factory_agent]
|
||||
assert True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Instantiation failed: {e}")
|
||||
return False, []
|
||||
assert False, str(e)
|
||||
|
||||
def test_agent_inheritance():
|
||||
"""Test that ContextAgent properly inherits from Agent."""
|
||||
@@ -89,12 +90,13 @@ def test_agent_inheritance():
|
||||
assert hasattr(context_agent, 'goal'), "Should have goal attribute"
|
||||
print("✅ ContextAgent has all required Agent attributes")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Inheritance test failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
@pytest.mark.skip(reason="ContextAgent methods not yet implemented")
|
||||
def test_context_engineering_methods():
|
||||
"""Test Context Engineering specific methods."""
|
||||
print("\\n🧪 Testing Context Engineering Methods...")
|
||||
@@ -137,12 +139,13 @@ def test_context_engineering_methods():
|
||||
|
||||
print(f"✅ All {len(async_methods)} async methods are available")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Method availability test failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
@pytest.mark.skip(reason="ContextAgent methods not yet implemented")
|
||||
def test_basic_functionality():
|
||||
"""Test basic functionality of ContextAgent methods."""
|
||||
print("\\n🧪 Testing Basic Functionality...")
|
||||
@@ -194,11 +197,11 @@ def test_basic_functionality():
|
||||
assert "PRP" in prp, "Should contain PRP reference"
|
||||
print("✅ generate_prp works correctly")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Functionality test failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
def test_backward_compatibility():
|
||||
"""Test that existing PraisonAI functionality still works."""
|
||||
@@ -232,11 +235,11 @@ def test_backward_compatibility():
|
||||
|
||||
print("✅ All expected exports are available")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backward compatibility test failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
def test_syntax_validation():
|
||||
"""Test that all Python files have valid syntax."""
|
||||
@@ -268,14 +271,14 @@ def test_syntax_validation():
|
||||
ast.parse(content)
|
||||
print(f"✅ {example_file.name} has valid Python syntax")
|
||||
|
||||
return True
|
||||
assert True
|
||||
|
||||
except SyntaxError as e:
|
||||
print(f"❌ Syntax error found: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
except Exception as e:
|
||||
print(f"❌ Syntax validation failed: {e}")
|
||||
return False
|
||||
assert False, str(e)
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests and provide summary."""
|
||||
|
||||
@@ -0,0 +1,855 @@
|
||||
"""Tests for the plugin system (BaseTool, @tool decorator, ToolRegistry).
|
||||
|
||||
Run with: pytest tests/test_plugin_system.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import List
|
||||
|
||||
|
||||
class TestBaseTool:
|
||||
"""Tests for BaseTool abstract class."""
|
||||
|
||||
def test_import_from_main_package(self):
|
||||
"""BaseTool should be importable from praisonaiagents."""
|
||||
from praisonaiagents import BaseTool
|
||||
assert BaseTool is not None
|
||||
|
||||
def test_subclass_with_required_attributes(self):
|
||||
"""Subclass with name, description, and run() should work."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "A test tool"
|
||||
|
||||
def run(self, query: str) -> str:
|
||||
return f"Result: {query}"
|
||||
|
||||
tool = MyTool()
|
||||
assert tool.name == "my_tool"
|
||||
assert tool.description == "A test tool"
|
||||
assert tool.run(query="test") == "Result: test"
|
||||
|
||||
def test_auto_name_from_class(self):
|
||||
"""Name should default to lowercase class name without 'tool'."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class WeatherTool(BaseTool):
|
||||
description = "Get weather"
|
||||
|
||||
def run(self) -> str:
|
||||
return "sunny"
|
||||
|
||||
tool = WeatherTool()
|
||||
assert tool.name == "weather"
|
||||
|
||||
def test_auto_description_from_docstring(self):
|
||||
"""Description should default to class docstring."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
"""This tool does amazing things."""
|
||||
name = "my_tool"
|
||||
|
||||
def run(self) -> str:
|
||||
return "done"
|
||||
|
||||
tool = MyTool()
|
||||
assert "amazing things" in tool.description
|
||||
|
||||
def test_auto_generate_parameters_schema(self):
|
||||
"""Parameters schema should be auto-generated from run() signature."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class SearchTool(BaseTool):
|
||||
name = "search"
|
||||
description = "Search for things"
|
||||
|
||||
def run(self, query: str, max_results: int = 5) -> List[str]:
|
||||
return []
|
||||
|
||||
tool = SearchTool()
|
||||
schema = tool.parameters
|
||||
|
||||
assert schema["type"] == "object"
|
||||
assert "query" in schema["properties"]
|
||||
assert "max_results" in schema["properties"]
|
||||
assert "query" in schema["required"]
|
||||
assert "max_results" not in schema["required"] # Has default
|
||||
|
||||
def test_callable_like_function(self):
|
||||
"""Tool should be callable directly."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class AddTool(BaseTool):
|
||||
name = "add"
|
||||
description = "Add numbers"
|
||||
|
||||
def run(self, a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
tool = AddTool()
|
||||
result = tool(a=2, b=3)
|
||||
assert result == 5
|
||||
|
||||
def test_safe_run_success(self):
|
||||
"""safe_run() should return ToolResult on success."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class OkTool(BaseTool):
|
||||
name = "ok"
|
||||
description = "Always works"
|
||||
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
tool = OkTool()
|
||||
result = tool.safe_run()
|
||||
|
||||
assert result.success is True
|
||||
assert result.output == "ok"
|
||||
assert result.error is None
|
||||
|
||||
def test_safe_run_failure(self):
|
||||
"""safe_run() should catch exceptions and return ToolResult."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class FailTool(BaseTool):
|
||||
name = "fail"
|
||||
description = "Always fails"
|
||||
|
||||
def run(self) -> str:
|
||||
raise ValueError("Something went wrong")
|
||||
|
||||
tool = FailTool()
|
||||
result = tool.safe_run()
|
||||
|
||||
assert result.success is False
|
||||
assert result.output is None
|
||||
assert "Something went wrong" in result.error
|
||||
|
||||
def test_get_schema_openai_format(self):
|
||||
"""get_schema() should return OpenAI-compatible function schema."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "Does something"
|
||||
|
||||
def run(self, x: str) -> str:
|
||||
return x
|
||||
|
||||
tool = MyTool()
|
||||
schema = tool.get_schema()
|
||||
|
||||
assert schema["type"] == "function"
|
||||
assert schema["function"]["name"] == "my_tool"
|
||||
assert schema["function"]["description"] == "Does something"
|
||||
assert "parameters" in schema["function"]
|
||||
|
||||
|
||||
class TestToolDecorator:
|
||||
"""Tests for @tool decorator."""
|
||||
|
||||
def test_import_from_main_package(self):
|
||||
"""tool decorator should be importable from praisonaiagents."""
|
||||
from praisonaiagents import tool
|
||||
assert tool is not None
|
||||
|
||||
def test_decorator_without_args(self):
|
||||
"""@tool without arguments should work."""
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool
|
||||
def my_search(query: str) -> List[str]:
|
||||
"""Search for things."""
|
||||
return [query]
|
||||
|
||||
assert my_search.name == "my_search"
|
||||
assert "Search for things" in my_search.description
|
||||
result = my_search(query="test")
|
||||
assert result == ["test"]
|
||||
|
||||
def test_decorator_with_args(self):
|
||||
"""@tool(name=..., description=...) should work."""
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool(name="custom_search", description="Custom description")
|
||||
def search(query: str) -> List[str]:
|
||||
return [query]
|
||||
|
||||
assert search.name == "custom_search"
|
||||
assert search.description == "Custom description"
|
||||
|
||||
def test_decorated_function_is_basetool(self):
|
||||
"""Decorated function should be a BaseTool instance."""
|
||||
from praisonaiagents import tool, BaseTool
|
||||
|
||||
@tool
|
||||
def my_func(x: str) -> str:
|
||||
return x
|
||||
|
||||
assert isinstance(my_func, BaseTool)
|
||||
|
||||
def test_decorated_function_has_schema(self):
|
||||
"""Decorated function should have auto-generated schema."""
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool
|
||||
def calculate(a: int, b: int, operation: str = "add") -> int:
|
||||
"""Perform calculation."""
|
||||
return a + b
|
||||
|
||||
schema = calculate.get_schema()
|
||||
params = schema["function"]["parameters"]
|
||||
|
||||
assert "a" in params["properties"]
|
||||
assert "b" in params["properties"]
|
||||
assert "operation" in params["properties"]
|
||||
assert "a" in params["required"]
|
||||
assert "operation" not in params["required"]
|
||||
|
||||
def test_decorated_function_callable_both_ways(self):
|
||||
"""Decorated function should work with positional and keyword args."""
|
||||
from praisonaiagents import tool
|
||||
|
||||
@tool
|
||||
def greet(name: str, greeting: str = "Hello") -> str:
|
||||
return f"{greeting}, {name}!"
|
||||
|
||||
# Keyword args (BaseTool style)
|
||||
assert greet(name="World") == "Hello, World!"
|
||||
|
||||
# Positional args (function style)
|
||||
assert greet("World", "Hi") == "Hi, World!"
|
||||
|
||||
|
||||
class TestToolRegistry:
|
||||
"""Tests for ToolRegistry."""
|
||||
|
||||
def test_import_from_main_package(self):
|
||||
"""Registry functions should be importable from praisonaiagents."""
|
||||
from praisonaiagents import get_registry, register_tool, get_tool, ToolRegistry
|
||||
assert get_registry is not None
|
||||
assert register_tool is not None
|
||||
assert get_tool is not None
|
||||
assert ToolRegistry is not None
|
||||
|
||||
def test_register_and_get_basetool(self):
|
||||
"""Should register and retrieve BaseTool instances."""
|
||||
from praisonaiagents import BaseTool, ToolRegistry
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "registry_test_tool"
|
||||
description = "Test"
|
||||
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
registry = ToolRegistry()
|
||||
tool = MyTool()
|
||||
registry.register(tool)
|
||||
|
||||
retrieved = registry.get("registry_test_tool")
|
||||
assert retrieved is tool
|
||||
|
||||
def test_register_and_get_function(self):
|
||||
"""Should register and retrieve plain functions."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
def my_func(x: str) -> str:
|
||||
return x
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(my_func)
|
||||
|
||||
retrieved = registry.get("my_func")
|
||||
assert retrieved is my_func
|
||||
|
||||
def test_register_with_custom_name(self):
|
||||
"""Should allow custom name override."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
def original_name() -> str:
|
||||
return "ok"
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(original_name, name="custom_name")
|
||||
|
||||
assert registry.get("custom_name") is original_name
|
||||
assert registry.get("original_name") is None
|
||||
|
||||
def test_list_tools(self):
|
||||
"""Should list all registered tool names."""
|
||||
from praisonaiagents import ToolRegistry, BaseTool
|
||||
|
||||
class Tool1(BaseTool):
|
||||
name = "tool1"
|
||||
description = "First"
|
||||
def run(self): return 1
|
||||
|
||||
class Tool2(BaseTool):
|
||||
name = "tool2"
|
||||
description = "Second"
|
||||
def run(self): return 2
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(Tool1())
|
||||
registry.register(Tool2())
|
||||
|
||||
names = registry.list_tools()
|
||||
assert "tool1" in names
|
||||
assert "tool2" in names
|
||||
|
||||
def test_unregister(self):
|
||||
"""Should remove tools from registry."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
def temp_func() -> str:
|
||||
return "temp"
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(temp_func)
|
||||
assert "temp_func" in registry
|
||||
|
||||
result = registry.unregister("temp_func")
|
||||
assert result is True
|
||||
assert "temp_func" not in registry
|
||||
|
||||
def test_no_duplicate_registration(self):
|
||||
"""Should not overwrite existing tools by default."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
def func_v1() -> str:
|
||||
return "v1"
|
||||
|
||||
def func_v2() -> str:
|
||||
return "v2"
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(func_v1, name="my_func")
|
||||
registry.register(func_v2, name="my_func") # Should be ignored
|
||||
|
||||
retrieved = registry.get("my_func")
|
||||
assert retrieved() == "v1"
|
||||
|
||||
def test_overwrite_registration(self):
|
||||
"""Should overwrite when overwrite=True."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
def func_v1() -> str:
|
||||
return "v1"
|
||||
|
||||
def func_v2() -> str:
|
||||
return "v2"
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(func_v1, name="my_func")
|
||||
registry.register(func_v2, name="my_func", overwrite=True)
|
||||
|
||||
retrieved = registry.get("my_func")
|
||||
assert retrieved() == "v2"
|
||||
|
||||
def test_global_registry_singleton(self):
|
||||
"""get_registry() should return same instance."""
|
||||
from praisonaiagents import get_registry
|
||||
|
||||
reg1 = get_registry()
|
||||
reg2 = get_registry()
|
||||
assert reg1 is reg2
|
||||
|
||||
def test_clear_registry(self):
|
||||
"""clear() should remove all tools."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.register(lambda: "a", name="a")
|
||||
registry.register(lambda: "b", name="b")
|
||||
assert len(registry) == 2
|
||||
|
||||
registry.clear()
|
||||
assert len(registry) == 0
|
||||
|
||||
|
||||
class TestToolResult:
|
||||
"""Tests for ToolResult wrapper."""
|
||||
|
||||
def test_import_from_main_package(self):
|
||||
"""ToolResult should be importable from praisonaiagents."""
|
||||
from praisonaiagents import ToolResult
|
||||
assert ToolResult is not None
|
||||
|
||||
def test_success_result(self):
|
||||
"""Should create success result."""
|
||||
from praisonaiagents import ToolResult
|
||||
|
||||
result = ToolResult(output="data", success=True)
|
||||
assert result.success is True
|
||||
assert result.output == "data"
|
||||
assert result.error is None
|
||||
|
||||
def test_error_result(self):
|
||||
"""Should create error result."""
|
||||
from praisonaiagents import ToolResult
|
||||
|
||||
result = ToolResult(output=None, success=False, error="Failed")
|
||||
assert result.success is False
|
||||
assert result.error == "Failed"
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Should convert to dictionary."""
|
||||
from praisonaiagents import ToolResult
|
||||
|
||||
result = ToolResult(output="data", success=True, metadata={"key": "value"})
|
||||
d = result.to_dict()
|
||||
|
||||
assert d["output"] == "data"
|
||||
assert d["success"] is True
|
||||
assert d["metadata"]["key"] == "value"
|
||||
|
||||
def test_str_representation(self):
|
||||
"""String should show output or error."""
|
||||
from praisonaiagents import ToolResult
|
||||
|
||||
success = ToolResult(output="hello", success=True)
|
||||
assert str(success) == "hello"
|
||||
|
||||
failure = ToolResult(output=None, success=False, error="oops")
|
||||
assert "oops" in str(failure)
|
||||
|
||||
|
||||
class TestFunctionTool:
|
||||
"""Tests for FunctionTool class."""
|
||||
|
||||
def test_import_from_main_package(self):
|
||||
"""FunctionTool should be importable from praisonaiagents."""
|
||||
from praisonaiagents import FunctionTool
|
||||
assert FunctionTool is not None
|
||||
|
||||
def test_wraps_function(self):
|
||||
"""Should wrap a function and preserve its behavior."""
|
||||
from praisonaiagents import FunctionTool
|
||||
|
||||
def original(x: int, y: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return x + y
|
||||
|
||||
wrapped = FunctionTool(original)
|
||||
|
||||
assert wrapped.name == "original"
|
||||
assert "Add two numbers" in wrapped.description
|
||||
assert wrapped(1, 2) == 3
|
||||
assert wrapped.run(x=1, y=2) == 3
|
||||
|
||||
|
||||
class TestAgentIntegration:
|
||||
"""Tests for Agent integration with plugin tools."""
|
||||
|
||||
def test_agent_with_basetool_instance(self):
|
||||
"""Agent should accept BaseTool instances in tools list."""
|
||||
from praisonaiagents import Agent, BaseTool
|
||||
|
||||
class AddTool(BaseTool):
|
||||
name = "add"
|
||||
description = "Add two numbers"
|
||||
|
||||
def run(self, a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
tool = AddTool()
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=[tool]
|
||||
)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0] is tool
|
||||
|
||||
def test_agent_with_decorated_tool(self):
|
||||
"""Agent should accept @tool decorated functions."""
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
@tool
|
||||
def multiply(a: int, b: int) -> int:
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=[multiply]
|
||||
)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0].name == "multiply"
|
||||
|
||||
def test_agent_execute_basetool(self):
|
||||
"""Agent.execute_tool should work with BaseTool instances."""
|
||||
from praisonaiagents import Agent, BaseTool
|
||||
|
||||
class ConcatTool(BaseTool):
|
||||
name = "concat"
|
||||
description = "Concatenate strings"
|
||||
|
||||
def run(self, a: str, b: str) -> str:
|
||||
return a + b
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=[ConcatTool()]
|
||||
)
|
||||
|
||||
result = agent.execute_tool("concat", {"a": "hello", "b": "world"})
|
||||
assert result == "helloworld"
|
||||
|
||||
def test_agent_execute_decorated_tool(self):
|
||||
"""Agent.execute_tool should work with @tool decorated functions."""
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
@tool
|
||||
def subtract(a: int, b: int) -> int:
|
||||
"""Subtract b from a."""
|
||||
return a - b
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=[subtract]
|
||||
)
|
||||
|
||||
result = agent.execute_tool("subtract", {"a": 10, "b": 3})
|
||||
assert result == 7
|
||||
|
||||
def test_agent_with_tool_names_from_registry(self):
|
||||
"""Agent should resolve tool names from registry."""
|
||||
from praisonaiagents import Agent, BaseTool, register_tool, get_registry
|
||||
|
||||
# Clear registry first
|
||||
get_registry().clear()
|
||||
|
||||
class DivideTool(BaseTool):
|
||||
name = "divide"
|
||||
description = "Divide two numbers"
|
||||
|
||||
def run(self, a: float, b: float) -> float:
|
||||
return a / b
|
||||
|
||||
# Register tool
|
||||
register_tool(DivideTool())
|
||||
|
||||
# Create agent with tool name string
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=["divide"]
|
||||
)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0].name == "divide"
|
||||
|
||||
# Clean up
|
||||
get_registry().clear()
|
||||
|
||||
def test_agent_execute_tool_from_registry(self):
|
||||
"""Agent should find tools in registry during execute_tool."""
|
||||
from praisonaiagents import Agent, BaseTool, register_tool, get_registry
|
||||
|
||||
# Clear registry first
|
||||
get_registry().clear()
|
||||
|
||||
class PowerTool(BaseTool):
|
||||
name = "power"
|
||||
description = "Raise to power"
|
||||
|
||||
def run(self, base: int, exp: int) -> int:
|
||||
return base ** exp
|
||||
|
||||
register_tool(PowerTool())
|
||||
|
||||
# Agent without tools - should still find from registry
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
role="Tester",
|
||||
goal="Test tools",
|
||||
tools=[]
|
||||
)
|
||||
|
||||
result = agent.execute_tool("power", {"base": 2, "exp": 3})
|
||||
assert result == 8
|
||||
|
||||
# Clean up
|
||||
get_registry().clear()
|
||||
|
||||
|
||||
class TestPluginDiscovery:
|
||||
"""Tests for entry_points plugin discovery."""
|
||||
|
||||
def test_discover_plugins_returns_count(self):
|
||||
"""discover_plugins() should return number of plugins found."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
registry = ToolRegistry()
|
||||
count = registry.discover_plugins()
|
||||
# Should return 0 or more (no external plugins installed in test env)
|
||||
assert isinstance(count, int)
|
||||
assert count >= 0
|
||||
|
||||
def test_discover_plugins_only_runs_once(self):
|
||||
"""discover_plugins() should only run once per registry."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
|
||||
registry = ToolRegistry()
|
||||
_count1 = registry.discover_plugins() # First call
|
||||
count2 = registry.discover_plugins()
|
||||
|
||||
# Second call should return 0 (already discovered)
|
||||
assert count2 == 0
|
||||
assert _count1 >= 0 # Use variable to avoid lint warning
|
||||
|
||||
def test_discover_plugins_with_mock_entry_point(self, mocker):
|
||||
"""Should load tools from mocked entry_points."""
|
||||
from praisonaiagents import ToolRegistry, BaseTool
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Create a mock tool class
|
||||
class MockPluginTool(BaseTool):
|
||||
name = "mock_plugin"
|
||||
description = "A mocked plugin tool"
|
||||
def run(self) -> str:
|
||||
return "mocked"
|
||||
|
||||
# Create mock entry point
|
||||
mock_ep = MagicMock()
|
||||
mock_ep.name = "mock_plugin"
|
||||
mock_ep.load.return_value = MockPluginTool
|
||||
|
||||
# Mock entry_points to return our mock
|
||||
mocker.patch(
|
||||
'praisonaiagents.tools.registry.entry_points',
|
||||
return_value=[mock_ep]
|
||||
)
|
||||
|
||||
registry = ToolRegistry()
|
||||
count = registry.discover_plugins()
|
||||
|
||||
assert count == 1
|
||||
assert "mock_plugin" in registry
|
||||
|
||||
tool = registry.get("mock_plugin")
|
||||
assert tool.run() == "mocked"
|
||||
|
||||
def test_discover_plugins_handles_load_error(self, mocker):
|
||||
"""Should handle errors when loading plugins gracefully."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Create mock entry point that raises on load
|
||||
mock_ep = MagicMock()
|
||||
mock_ep.name = "broken_plugin"
|
||||
mock_ep.load.side_effect = ImportError("Module not found")
|
||||
|
||||
mocker.patch(
|
||||
'praisonaiagents.tools.registry.entry_points',
|
||||
return_value=[mock_ep]
|
||||
)
|
||||
|
||||
registry = ToolRegistry()
|
||||
# Should not raise, just log warning
|
||||
count = registry.discover_plugins()
|
||||
|
||||
assert count == 0
|
||||
assert "broken_plugin" not in registry
|
||||
|
||||
def test_discover_callable_function(self, mocker):
|
||||
"""Should register plain functions from entry_points."""
|
||||
from praisonaiagents import ToolRegistry
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
def plugin_function(x: str) -> str:
|
||||
return f"plugin: {x}"
|
||||
|
||||
mock_ep = MagicMock()
|
||||
mock_ep.name = "func_plugin"
|
||||
mock_ep.load.return_value = plugin_function
|
||||
|
||||
mocker.patch(
|
||||
'praisonaiagents.tools.registry.entry_points',
|
||||
return_value=[mock_ep]
|
||||
)
|
||||
|
||||
registry = ToolRegistry()
|
||||
count = registry.discover_plugins()
|
||||
|
||||
assert count == 1
|
||||
func = registry.get("func_plugin")
|
||||
assert func("test") == "plugin: test"
|
||||
|
||||
def test_auto_discover_on_get_if_not_found(self, mocker):
|
||||
"""get() should trigger discovery if tool not found."""
|
||||
from praisonaiagents import ToolRegistry, BaseTool
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
class LazyTool(BaseTool):
|
||||
name = "lazy_tool"
|
||||
description = "Lazy loaded"
|
||||
def run(self) -> str:
|
||||
return "lazy"
|
||||
|
||||
mock_ep = MagicMock()
|
||||
mock_ep.name = "lazy_tool"
|
||||
mock_ep.load.return_value = LazyTool
|
||||
|
||||
mocker.patch(
|
||||
'praisonaiagents.tools.registry.entry_points',
|
||||
return_value=[mock_ep]
|
||||
)
|
||||
|
||||
registry = ToolRegistry()
|
||||
# Don't call discover_plugins() explicitly
|
||||
# get() should trigger it
|
||||
tool = registry.get("lazy_tool")
|
||||
|
||||
assert tool is not None
|
||||
assert tool.run() == "lazy"
|
||||
|
||||
|
||||
class TestToolValidation:
|
||||
"""Tests for tool validation."""
|
||||
|
||||
def test_import_validation_error(self):
|
||||
"""ToolValidationError should be importable."""
|
||||
from praisonaiagents import ToolValidationError
|
||||
assert ToolValidationError is not None
|
||||
|
||||
def test_import_validate_tool(self):
|
||||
"""validate_tool should be importable."""
|
||||
from praisonaiagents import validate_tool
|
||||
assert validate_tool is not None
|
||||
|
||||
def test_valid_tool_passes_validation(self):
|
||||
"""Valid tool should pass validation."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class ValidTool(BaseTool):
|
||||
name = "valid"
|
||||
description = "A valid tool"
|
||||
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
tool = ValidTool()
|
||||
assert tool.validate() is True
|
||||
|
||||
def test_tool_without_name_gets_auto_name(self):
|
||||
"""Tool without name should get auto-generated name from class."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class MyCustomTool(BaseTool):
|
||||
name = "" # Empty name - will be auto-filled
|
||||
description = "Has description"
|
||||
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
tool = MyCustomTool()
|
||||
# Name should be auto-generated from class name
|
||||
assert tool.name == "mycustom" # "MyCustomTool" -> "mycustom" (lowercase, "tool" removed)
|
||||
assert tool.validate() is True
|
||||
|
||||
def test_tool_without_description_gets_auto_description(self):
|
||||
"""Tool without description should get auto-generated description."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class NoDescTool(BaseTool):
|
||||
"""This is the docstring description."""
|
||||
name = "no_desc"
|
||||
description = "" # Empty - will use docstring
|
||||
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
tool = NoDescTool()
|
||||
# Description should be auto-generated from docstring
|
||||
assert "docstring description" in tool.description
|
||||
assert tool.validate() is True
|
||||
|
||||
def test_tool_with_manually_cleared_name_fails(self):
|
||||
"""Tool with name manually cleared after init should fail validation."""
|
||||
from praisonaiagents import BaseTool, ToolValidationError
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "Test"
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
tool = MyTool()
|
||||
tool.name = "" # Manually clear after init
|
||||
with pytest.raises(ToolValidationError):
|
||||
tool.validate()
|
||||
|
||||
def test_validate_tool_function_with_basetool(self):
|
||||
"""validate_tool() should work with BaseTool instances."""
|
||||
from praisonaiagents import BaseTool, validate_tool
|
||||
|
||||
class MyTool(BaseTool):
|
||||
name = "my_tool"
|
||||
description = "Test"
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
assert validate_tool(MyTool()) is True
|
||||
|
||||
def test_validate_tool_function_with_callable(self):
|
||||
"""validate_tool() should work with plain callables."""
|
||||
from praisonaiagents import validate_tool
|
||||
|
||||
def my_func() -> str:
|
||||
return "ok"
|
||||
|
||||
assert validate_tool(my_func) is True
|
||||
|
||||
def test_validate_tool_rejects_invalid_type(self):
|
||||
"""validate_tool() should reject non-tool objects."""
|
||||
from praisonaiagents import validate_tool, ToolValidationError
|
||||
|
||||
with pytest.raises(ToolValidationError):
|
||||
validate_tool("not a tool")
|
||||
|
||||
with pytest.raises(ToolValidationError):
|
||||
validate_tool(123)
|
||||
|
||||
def test_validate_class_method(self):
|
||||
"""BaseTool.validate_class() should check class validity."""
|
||||
from praisonaiagents import BaseTool
|
||||
|
||||
class ValidTool(BaseTool):
|
||||
name = "valid"
|
||||
description = "Valid"
|
||||
def run(self) -> str:
|
||||
return "ok"
|
||||
|
||||
assert ValidTool.validate_class() is True
|
||||
|
||||
def test_decorated_tool_passes_validation(self):
|
||||
"""@tool decorated functions should pass validation."""
|
||||
from praisonaiagents import tool, validate_tool
|
||||
|
||||
@tool
|
||||
def my_search(query: str) -> list:
|
||||
"""Search for things."""
|
||||
return []
|
||||
|
||||
assert validate_tool(my_search) is True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Tests for prompt caching feature.
|
||||
|
||||
Prompt caching allows caching parts of prompts to reduce costs and latency.
|
||||
Supported by OpenAI, Anthropic, Bedrock, and Deepseek.
|
||||
|
||||
Source: https://docs.litellm.ai/docs/completion/prompt_caching
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestSupportsPromptCaching:
|
||||
"""Tests for the supports_prompt_caching function in model_capabilities.py"""
|
||||
|
||||
def test_supports_prompt_caching_anthropic(self):
|
||||
"""Test that Anthropic Claude models support prompt caching."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_prompt_caching
|
||||
|
||||
# Mock litellm.utils.supports_prompt_caching
|
||||
with patch('praisonaiagents.llm.model_capabilities.supports_prompt_caching') as mock_func:
|
||||
# Just test that the function exists and can be called
|
||||
mock_func.return_value = True
|
||||
result = mock_func(model="anthropic/claude-3-5-sonnet-latest")
|
||||
assert result == True
|
||||
|
||||
def test_supports_prompt_caching_openai(self):
|
||||
"""Test that OpenAI models support prompt caching."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_prompt_caching
|
||||
|
||||
with patch('litellm.utils.supports_prompt_caching', return_value=True):
|
||||
# The function should delegate to litellm
|
||||
result = supports_prompt_caching("openai/gpt-4o")
|
||||
# Result depends on litellm's implementation
|
||||
assert isinstance(result, bool)
|
||||
|
||||
def test_supports_prompt_caching_empty_model(self):
|
||||
"""Test that empty model name returns False."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_prompt_caching
|
||||
|
||||
assert supports_prompt_caching("") == False
|
||||
assert supports_prompt_caching(None) == False
|
||||
|
||||
|
||||
class TestLLMPromptCachingParameter:
|
||||
"""Tests for prompt_caching parameter in LLM class."""
|
||||
|
||||
def test_llm_init_with_prompt_caching_true(self):
|
||||
"""Test LLM initialization with prompt_caching=True."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", prompt_caching=True)
|
||||
assert llm.prompt_caching == True
|
||||
|
||||
def test_llm_init_with_prompt_caching_false(self):
|
||||
"""Test LLM initialization with prompt_caching=False."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", prompt_caching=False)
|
||||
assert llm.prompt_caching == False
|
||||
|
||||
def test_llm_init_without_prompt_caching(self):
|
||||
"""Test LLM initialization without prompt_caching parameter."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
assert llm.prompt_caching is None
|
||||
|
||||
def test_llm_supports_prompt_caching_method(self):
|
||||
"""Test _supports_prompt_caching method exists and works."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest")
|
||||
# Method should exist and return a boolean
|
||||
result = llm._supports_prompt_caching()
|
||||
assert isinstance(result, bool)
|
||||
|
||||
def test_llm_is_anthropic_model_method(self):
|
||||
"""Test _is_anthropic_model method."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
# Anthropic model
|
||||
llm_anthropic = LLM(model="anthropic/claude-3-5-sonnet-latest")
|
||||
assert llm_anthropic._is_anthropic_model() == True
|
||||
|
||||
# OpenAI model
|
||||
llm_openai = LLM(model="gpt-4o-mini")
|
||||
assert llm_openai._is_anthropic_model() == False
|
||||
|
||||
# Gemini model
|
||||
llm_gemini = LLM(model="gemini/gemini-2.0-flash")
|
||||
assert llm_gemini._is_anthropic_model() == False
|
||||
|
||||
|
||||
class TestLLMBuildMessagesWithPromptCaching:
|
||||
"""Tests for _build_messages with prompt caching enabled."""
|
||||
|
||||
def test_build_messages_with_prompt_caching_anthropic(self):
|
||||
"""Test that _build_messages adds cache_control for Anthropic models."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", prompt_caching=True)
|
||||
|
||||
# Mock _supports_prompt_caching to return True
|
||||
with patch.object(llm, '_supports_prompt_caching', return_value=True):
|
||||
messages, _ = llm._build_messages(
|
||||
prompt="Hello",
|
||||
system_prompt="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# Check that system message has cache_control
|
||||
system_msg = messages[0]
|
||||
assert system_msg["role"] == "system"
|
||||
# For Anthropic with prompt_caching, content should be an array
|
||||
assert isinstance(system_msg["content"], list)
|
||||
assert system_msg["content"][0]["type"] == "text"
|
||||
assert system_msg["content"][0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_build_messages_without_prompt_caching(self):
|
||||
"""Test that _build_messages doesn't add cache_control when disabled."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", prompt_caching=False)
|
||||
|
||||
messages, _ = llm._build_messages(
|
||||
prompt="Hello",
|
||||
system_prompt="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# Check that system message is a simple string
|
||||
system_msg = messages[0]
|
||||
assert system_msg["role"] == "system"
|
||||
assert isinstance(system_msg["content"], str)
|
||||
|
||||
def test_build_messages_prompt_caching_non_anthropic(self):
|
||||
"""Test that prompt caching doesn't affect non-Anthropic models."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
llm = LLM(model="gpt-4o-mini", prompt_caching=True)
|
||||
|
||||
messages, _ = llm._build_messages(
|
||||
prompt="Hello",
|
||||
system_prompt="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# For non-Anthropic models, content should remain a string
|
||||
system_msg = messages[0]
|
||||
assert system_msg["role"] == "system"
|
||||
assert isinstance(system_msg["content"], str)
|
||||
|
||||
|
||||
class TestAgentPromptCachingParameter:
|
||||
"""Tests for prompt_caching parameter in Agent class."""
|
||||
|
||||
def test_agent_init_with_prompt_caching_true(self):
|
||||
"""Test Agent initialization with prompt_caching=True."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
prompt_caching=True
|
||||
)
|
||||
assert agent.prompt_caching == True
|
||||
|
||||
def test_agent_init_with_prompt_caching_false(self):
|
||||
"""Test Agent initialization with prompt_caching=False."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
prompt_caching=False
|
||||
)
|
||||
assert agent.prompt_caching == False
|
||||
|
||||
def test_agent_init_without_prompt_caching(self):
|
||||
"""Test Agent initialization without prompt_caching parameter."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test Agent")
|
||||
assert agent.prompt_caching is None
|
||||
|
||||
def test_agent_model_supports_prompt_caching_method(self):
|
||||
"""Test _model_supports_prompt_caching method exists."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest"
|
||||
)
|
||||
# Method should exist and return a boolean
|
||||
result = agent._model_supports_prompt_caching()
|
||||
assert isinstance(result, bool)
|
||||
|
||||
def test_agent_passes_prompt_caching_to_llm(self):
|
||||
"""Test that Agent passes prompt_caching to LLM instance."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
prompt_caching=True
|
||||
)
|
||||
|
||||
# Check that LLM instance has prompt_caching set
|
||||
if hasattr(agent, 'llm_instance') and agent.llm_instance:
|
||||
assert agent.llm_instance.prompt_caching == True
|
||||
|
||||
|
||||
class TestPromptCachingIntegration:
|
||||
"""Integration tests for prompt caching (requires API key)."""
|
||||
|
||||
@pytest.mark.skip(reason="Requires ANTHROPIC_API_KEY environment variable")
|
||||
def test_prompt_caching_with_anthropic(self):
|
||||
"""Test prompt caching with actual Anthropic API."""
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Skip if no API key
|
||||
if not os.getenv("ANTHROPIC_API_KEY"):
|
||||
pytest.skip("ANTHROPIC_API_KEY not set")
|
||||
|
||||
agent = Agent(
|
||||
name="Caching Test Agent",
|
||||
instructions="You are a helpful assistant." * 100, # Long system prompt
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
prompt_caching=True,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# First call - should create cache
|
||||
result1 = agent.start("What is 2+2?")
|
||||
assert result1 is not None
|
||||
|
||||
# Second call - should use cache
|
||||
result2 = agent.start("What is 3+3?")
|
||||
assert result2 is not None
|
||||
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Tests for PromptExpanderAgent
|
||||
|
||||
Run with: python -m pytest tests/test_prompt_expander_agent.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestPromptExpanderImports:
|
||||
"""Test that all imports work correctly."""
|
||||
|
||||
def test_import_from_package(self):
|
||||
"""Test importing from main package."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy, ExpandResult
|
||||
assert PromptExpanderAgent is not None
|
||||
assert ExpandStrategy is not None
|
||||
assert ExpandResult is not None
|
||||
|
||||
def test_import_from_agent_module(self):
|
||||
"""Test importing from agent module."""
|
||||
from praisonaiagents.agent import PromptExpanderAgent, ExpandStrategy, ExpandResult
|
||||
assert PromptExpanderAgent is not None
|
||||
assert ExpandStrategy is not None
|
||||
assert ExpandResult is not None
|
||||
|
||||
|
||||
class TestExpandStrategy:
|
||||
"""Test ExpandStrategy enum."""
|
||||
|
||||
def test_strategy_values(self):
|
||||
"""Test all strategy values exist."""
|
||||
from praisonaiagents import ExpandStrategy
|
||||
|
||||
assert ExpandStrategy.BASIC.value == "basic"
|
||||
assert ExpandStrategy.DETAILED.value == "detailed"
|
||||
assert ExpandStrategy.STRUCTURED.value == "structured"
|
||||
assert ExpandStrategy.CREATIVE.value == "creative"
|
||||
assert ExpandStrategy.AUTO.value == "auto"
|
||||
|
||||
|
||||
class TestExpandResult:
|
||||
"""Test ExpandResult dataclass."""
|
||||
|
||||
def test_result_creation(self):
|
||||
"""Test creating an ExpandResult."""
|
||||
from praisonaiagents import ExpandResult, ExpandStrategy
|
||||
|
||||
result = ExpandResult(
|
||||
original_prompt="write a movie script",
|
||||
expanded_prompt="Write a compelling movie script...",
|
||||
strategy_used=ExpandStrategy.BASIC
|
||||
)
|
||||
|
||||
assert result.original_prompt == "write a movie script"
|
||||
assert result.expanded_prompt == "Write a compelling movie script..."
|
||||
assert result.strategy_used == ExpandStrategy.BASIC
|
||||
|
||||
def test_result_with_metadata(self):
|
||||
"""Test ExpandResult with metadata."""
|
||||
from praisonaiagents import ExpandResult, ExpandStrategy
|
||||
|
||||
result = ExpandResult(
|
||||
original_prompt="test",
|
||||
expanded_prompt="expanded test",
|
||||
strategy_used=ExpandStrategy.DETAILED,
|
||||
metadata={"word_count": 50, "has_context": True}
|
||||
)
|
||||
|
||||
assert result.metadata["word_count"] == 50
|
||||
assert result.metadata["has_context"] is True
|
||||
|
||||
|
||||
class TestPromptExpanderAgentInit:
|
||||
"""Test PromptExpanderAgent initialization."""
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test agent initializes with defaults."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
assert agent.name == "PromptExpanderAgent"
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
assert not agent.verbose
|
||||
assert agent.temperature == 0.7 # Higher for creativity
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test agent initializes with custom values."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent(
|
||||
name="CustomExpander",
|
||||
model="gpt-4o",
|
||||
verbose=True,
|
||||
temperature=0.5
|
||||
)
|
||||
|
||||
assert agent.name == "CustomExpander"
|
||||
assert agent.model == "gpt-4o"
|
||||
assert agent.verbose
|
||||
assert agent.temperature == 0.5
|
||||
|
||||
def test_initialization_with_tools(self):
|
||||
"""Test agent initializes with tools."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
def mock_tool(query: str) -> str:
|
||||
return "mock result"
|
||||
|
||||
agent = PromptExpanderAgent(tools=[mock_tool])
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
|
||||
|
||||
class TestPromptExpansion:
|
||||
"""Test prompt expansion functionality."""
|
||||
|
||||
def test_expand_preserves_task_intent(self):
|
||||
"""Test that expansion preserves task intent (not converting to question)."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandResult
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
# Mock the internal agent call
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = "Write a compelling three-line movie script that captures a dramatic moment with vivid imagery and emotional depth."
|
||||
|
||||
result = agent.expand("write a movie script in 3 lines")
|
||||
|
||||
assert result.original_prompt == "write a movie script in 3 lines"
|
||||
assert "Write" in result.expanded_prompt # Should start with action verb
|
||||
assert "?" not in result.expanded_prompt # Should NOT be a question
|
||||
|
||||
def test_expand_adds_detail(self):
|
||||
"""Test that expansion adds meaningful detail."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = "Create a detailed blog post about artificial intelligence trends in 2025, covering key developments in large language models, multimodal AI, and autonomous agents. Include specific examples and expert predictions."
|
||||
|
||||
result = agent.expand("write about AI")
|
||||
|
||||
# Expanded should be longer than original
|
||||
assert len(result.expanded_prompt) > len(result.original_prompt)
|
||||
|
||||
|
||||
class TestExpandStrategies:
|
||||
"""Test different expansion strategies."""
|
||||
|
||||
def test_basic_strategy(self):
|
||||
"""Test BASIC expansion strategy."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = "Write a movie script in three lines."
|
||||
|
||||
result = agent.expand("movie script 3 lines", strategy=ExpandStrategy.BASIC)
|
||||
|
||||
assert result.strategy_used == ExpandStrategy.BASIC
|
||||
mock_call.assert_called_once()
|
||||
|
||||
def test_detailed_strategy(self):
|
||||
"""Test DETAILED expansion strategy."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = """Write a compelling three-line movie script that:
|
||||
- Opens with a hook that immediately grabs attention
|
||||
- Builds tension or emotion in the middle line
|
||||
- Delivers a powerful, memorable conclusion
|
||||
Include vivid imagery, strong verbs, and emotional resonance."""
|
||||
|
||||
result = agent.expand("movie script 3 lines", strategy=ExpandStrategy.DETAILED)
|
||||
|
||||
assert result.strategy_used == ExpandStrategy.DETAILED
|
||||
|
||||
def test_structured_strategy(self):
|
||||
"""Test STRUCTURED expansion strategy."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = """Task: Write a three-line movie script
|
||||
Format: Three lines, each serving a narrative purpose
|
||||
Requirements:
|
||||
- Line 1: Setup/Hook
|
||||
- Line 2: Conflict/Development
|
||||
- Line 3: Resolution/Twist
|
||||
Style: Cinematic, impactful"""
|
||||
|
||||
result = agent.expand("movie script 3 lines", strategy=ExpandStrategy.STRUCTURED)
|
||||
|
||||
assert result.strategy_used == ExpandStrategy.STRUCTURED
|
||||
|
||||
def test_creative_strategy(self):
|
||||
"""Test CREATIVE expansion strategy."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = "Craft an unforgettable three-line movie script that transports the reader into a vivid world. Let each line breathe with cinematic tension - a hook that grabs, a twist that surprises, and a finale that lingers in the mind long after reading."
|
||||
|
||||
result = agent.expand("movie script 3 lines", strategy=ExpandStrategy.CREATIVE)
|
||||
|
||||
assert result.strategy_used == ExpandStrategy.CREATIVE
|
||||
|
||||
def test_auto_strategy_detection(self):
|
||||
"""Test AUTO strategy detects appropriate expansion type."""
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
# Short prompts should get DETAILED expansion
|
||||
strategy = agent._detect_strategy("AI")
|
||||
assert strategy in [ExpandStrategy.BASIC, ExpandStrategy.DETAILED]
|
||||
|
||||
# Task prompts should preserve task nature
|
||||
strategy = agent._detect_strategy("write a poem about love")
|
||||
assert strategy is not None
|
||||
|
||||
|
||||
class TestToolsIntegration:
|
||||
"""Test tools integration with PromptExpanderAgent."""
|
||||
|
||||
def test_expand_with_tools(self):
|
||||
"""Test expansion with tools for context gathering."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
def search_tool(query: str) -> str:
|
||||
return "Latest AI trends: LLMs, multimodal, agents"
|
||||
|
||||
agent = PromptExpanderAgent(tools=[search_tool])
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
|
||||
def test_tools_passed_to_internal_agent(self):
|
||||
"""Test that tools are passed to the internal agent."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
def mock_tool(query: str) -> str:
|
||||
return "result"
|
||||
|
||||
agent = PromptExpanderAgent(tools=[mock_tool])
|
||||
|
||||
# Access the lazy-initialized agent
|
||||
internal_agent = agent.agent
|
||||
|
||||
# The internal agent should have the tools
|
||||
assert internal_agent.tools is not None
|
||||
|
||||
|
||||
class TestConvenienceMethods:
|
||||
"""Test convenience methods."""
|
||||
|
||||
def test_convenience_methods_exist(self):
|
||||
"""Test all convenience methods exist."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
assert hasattr(agent, 'expand')
|
||||
assert hasattr(agent, 'expand_basic')
|
||||
assert hasattr(agent, 'expand_detailed')
|
||||
assert hasattr(agent, 'expand_structured')
|
||||
assert hasattr(agent, 'expand_creative')
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases."""
|
||||
|
||||
def test_empty_prompt(self):
|
||||
"""Test handling of empty prompt."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
mock_call.return_value = ""
|
||||
|
||||
result = agent.expand("")
|
||||
|
||||
# Should return original if expansion fails
|
||||
assert result.expanded_prompt == "" or result.original_prompt == ""
|
||||
|
||||
def test_already_detailed_prompt(self):
|
||||
"""Test that already detailed prompts are minimally modified."""
|
||||
from praisonaiagents import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
detailed_prompt = """Write a comprehensive blog post about machine learning,
|
||||
covering supervised learning, unsupervised learning, and reinforcement learning.
|
||||
Include code examples in Python and real-world applications."""
|
||||
|
||||
with patch.object(agent, '_call_agent') as mock_call:
|
||||
# For detailed prompts, minimal expansion expected
|
||||
mock_call.return_value = detailed_prompt + " Ensure clarity and technical accuracy."
|
||||
|
||||
result = agent.expand(detailed_prompt)
|
||||
|
||||
# Should not drastically change already detailed prompts
|
||||
assert result.original_prompt == detailed_prompt
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -59,12 +59,14 @@ class TestValidationFeedback:
|
||||
|
||||
# Simulate task execution results
|
||||
collect_task.result = TaskOutput(
|
||||
description="Collected data",
|
||||
raw="Collected only 3 items",
|
||||
agent="agent1"
|
||||
)
|
||||
collect_task.status = "completed"
|
||||
|
||||
validate_task.result = TaskOutput(
|
||||
description="Validation result",
|
||||
raw="Not enough results, need at least 10",
|
||||
agent="agent2"
|
||||
)
|
||||
@@ -219,12 +221,14 @@ class TestValidationFeedback:
|
||||
|
||||
# Simulate task execution results
|
||||
data_task.result = TaskOutput(
|
||||
description="Generated data",
|
||||
raw="Generated data with errors",
|
||||
agent="agent1"
|
||||
)
|
||||
data_task.status = "completed"
|
||||
|
||||
validate_task.result = TaskOutput(
|
||||
description="Validation result",
|
||||
raw="Data has errors, please fix",
|
||||
agent="agent2"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
Test suite for web_fetch feature.
|
||||
|
||||
This module tests the web fetch functionality that allows LLMs to retrieve
|
||||
full content from specific URLs (web pages and PDF documents).
|
||||
Currently only supported by Anthropic Claude models.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
|
||||
|
||||
class TestSupportsWebFetch:
|
||||
"""Test the supports_web_fetch function in model_capabilities."""
|
||||
|
||||
def test_anthropic_claude_opus_4_1_supports_web_fetch(self):
|
||||
"""Test that Claude Opus 4.1 supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-opus-4-1-20250805")
|
||||
assert supports_web_fetch("claude-opus-4-1")
|
||||
|
||||
def test_anthropic_claude_opus_4_supports_web_fetch(self):
|
||||
"""Test that Claude Opus 4 supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-opus-4-20250514")
|
||||
assert supports_web_fetch("claude-opus-4")
|
||||
|
||||
def test_anthropic_claude_sonnet_4_supports_web_fetch(self):
|
||||
"""Test that Claude Sonnet 4 supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-sonnet-4-20250514")
|
||||
assert supports_web_fetch("claude-sonnet-4")
|
||||
|
||||
def test_anthropic_claude_3_7_sonnet_supports_web_fetch(self):
|
||||
"""Test that Claude 3.7 Sonnet supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-3-7-sonnet-20250219")
|
||||
assert supports_web_fetch("claude-3-7-sonnet-latest")
|
||||
|
||||
def test_anthropic_claude_3_5_sonnet_supports_web_fetch(self):
|
||||
"""Test that Claude 3.5 Sonnet supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-3-5-sonnet-latest")
|
||||
assert supports_web_fetch("claude-3-5-sonnet-20241022")
|
||||
|
||||
def test_anthropic_claude_3_5_haiku_supports_web_fetch(self):
|
||||
"""Test that Claude 3.5 Haiku supports web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("anthropic/claude-3-5-haiku-latest")
|
||||
assert supports_web_fetch("claude-3-5-haiku-20241022")
|
||||
|
||||
def test_claude_5_auto_supports_web_fetch(self):
|
||||
"""Test that Claude 5+ models auto-support web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert supports_web_fetch("claude-5")
|
||||
assert supports_web_fetch("claude-sonnet-5")
|
||||
assert supports_web_fetch("claude-opus-5")
|
||||
|
||||
def test_openai_models_do_not_support_web_fetch(self):
|
||||
"""Test that OpenAI models do not support web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert not supports_web_fetch("openai/gpt-4o")
|
||||
assert not supports_web_fetch("gpt-4o-mini")
|
||||
assert not supports_web_fetch("gpt-4o-search-preview")
|
||||
|
||||
def test_gemini_models_do_not_support_web_fetch(self):
|
||||
"""Test that Gemini models do not support web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert not supports_web_fetch("gemini/gemini-2.0-flash")
|
||||
assert not supports_web_fetch("gemini-2.5-pro")
|
||||
|
||||
def test_unknown_models_do_not_support_web_fetch(self):
|
||||
"""Test that unknown models do not support web fetch."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_fetch
|
||||
assert not supports_web_fetch("ollama/llama3")
|
||||
assert not supports_web_fetch("unknown/model")
|
||||
|
||||
|
||||
class TestLLMWebFetchParameter:
|
||||
"""Test the web_fetch parameter in LLM class."""
|
||||
|
||||
def test_llm_init_with_web_fetch_true(self):
|
||||
"""Test LLM initialization with web_fetch=True."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", web_fetch=True)
|
||||
assert llm.web_fetch is True
|
||||
|
||||
def test_llm_init_with_web_fetch_dict(self):
|
||||
"""Test LLM initialization with web_fetch as dict."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
web_fetch_config = {
|
||||
"max_uses": 10,
|
||||
"allowed_domains": ["example.com"],
|
||||
"citations": {"enabled": True}
|
||||
}
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", web_fetch=web_fetch_config)
|
||||
assert llm.web_fetch == web_fetch_config
|
||||
|
||||
def test_llm_init_without_web_fetch(self):
|
||||
"""Test LLM initialization without web_fetch."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
assert llm.web_fetch is None
|
||||
|
||||
def test_llm_supports_web_fetch_method(self):
|
||||
"""Test LLM._supports_web_fetch() method."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
# Claude model should support web fetch
|
||||
llm_claude = LLM(model="anthropic/claude-3-5-sonnet-latest")
|
||||
assert llm_claude._supports_web_fetch()
|
||||
|
||||
# OpenAI model should not support web fetch
|
||||
llm_openai = LLM(model="gpt-4o-mini")
|
||||
assert not llm_openai._supports_web_fetch()
|
||||
|
||||
|
||||
class TestLLMBuildCompletionParamsWebFetch:
|
||||
"""Test _build_completion_params with web_fetch."""
|
||||
|
||||
def test_build_params_with_web_fetch_true(self):
|
||||
"""Test that web_fetch=True adds web_fetch tool to params."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", web_fetch=True)
|
||||
params = llm._build_completion_params()
|
||||
|
||||
assert 'tools' in params
|
||||
# Find the web_fetch tool
|
||||
web_fetch_tool = None
|
||||
for tool in params['tools']:
|
||||
if isinstance(tool, dict) and tool.get('type') == 'web_fetch_20250910':
|
||||
web_fetch_tool = tool
|
||||
break
|
||||
|
||||
assert web_fetch_tool is not None
|
||||
assert web_fetch_tool['name'] == 'web_fetch'
|
||||
assert web_fetch_tool['max_uses'] == 5 # default
|
||||
|
||||
def test_build_params_with_web_fetch_dict(self):
|
||||
"""Test that web_fetch dict adds customized web_fetch tool."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
web_fetch_config = {
|
||||
"max_uses": 10,
|
||||
"allowed_domains": ["example.com", "docs.example.com"],
|
||||
"citations": {"enabled": True},
|
||||
"max_content_tokens": 50000
|
||||
}
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-latest", web_fetch=web_fetch_config)
|
||||
params = llm._build_completion_params()
|
||||
|
||||
assert 'tools' in params
|
||||
# Find the web_fetch tool
|
||||
web_fetch_tool = None
|
||||
for tool in params['tools']:
|
||||
if isinstance(tool, dict) and tool.get('type') == 'web_fetch_20250910':
|
||||
web_fetch_tool = tool
|
||||
break
|
||||
|
||||
assert web_fetch_tool is not None
|
||||
assert web_fetch_tool['max_uses'] == 10
|
||||
assert web_fetch_tool['allowed_domains'] == ["example.com", "docs.example.com"]
|
||||
assert web_fetch_tool['citations'] == {"enabled": True}
|
||||
assert web_fetch_tool['max_content_tokens'] == 50000
|
||||
|
||||
def test_build_params_web_fetch_unsupported_model(self):
|
||||
"""Test that web_fetch is ignored for unsupported models."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini", web_fetch=True)
|
||||
params = llm._build_completion_params()
|
||||
|
||||
# Should not have web_fetch tool
|
||||
if 'tools' in params:
|
||||
for tool in params['tools']:
|
||||
if isinstance(tool, dict):
|
||||
assert tool.get('type') != 'web_fetch_20250910'
|
||||
|
||||
|
||||
class TestAgentWebFetchParameter:
|
||||
"""Test the web_fetch parameter in Agent class."""
|
||||
|
||||
def test_agent_init_with_web_fetch_true(self):
|
||||
"""Test Agent initialization with web_fetch=True."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
)
|
||||
assert agent.web_fetch is True
|
||||
|
||||
def test_agent_init_with_web_fetch_dict(self):
|
||||
"""Test Agent initialization with web_fetch as dict."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
web_fetch_config = {"max_uses": 10, "allowed_domains": ["example.com"]}
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=web_fetch_config
|
||||
)
|
||||
assert agent.web_fetch == web_fetch_config
|
||||
|
||||
def test_agent_init_without_web_fetch(self):
|
||||
"""Test Agent initialization without web_fetch."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
agent = Agent(name="Test Agent", llm="gpt-4o-mini")
|
||||
assert agent.web_fetch is None
|
||||
|
||||
def test_agent_passes_web_fetch_to_llm(self):
|
||||
"""Test that Agent passes web_fetch to LLM instance."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
)
|
||||
assert agent.llm_instance.web_fetch is True
|
||||
|
||||
def test_agent_model_supports_web_fetch_method(self):
|
||||
"""Test Agent._model_supports_web_fetch() method."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
# Claude model should support web fetch
|
||||
agent_claude = Agent(
|
||||
name="Claude Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest"
|
||||
)
|
||||
assert agent_claude._model_supports_web_fetch()
|
||||
|
||||
# OpenAI model should not support web fetch
|
||||
agent_openai = Agent(
|
||||
name="OpenAI Agent",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
assert not agent_openai._model_supports_web_fetch()
|
||||
|
||||
|
||||
class TestWebFetchWithWebSearch:
|
||||
"""Test that web_fetch and web_search can be used together."""
|
||||
|
||||
def test_both_web_fetch_and_web_search(self):
|
||||
"""Test using both web_fetch and web_search on supported model."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(
|
||||
model="anthropic/claude-3-5-sonnet-latest",
|
||||
web_search=True,
|
||||
web_fetch=True
|
||||
)
|
||||
params = llm._build_completion_params()
|
||||
|
||||
# Should have web_search_options
|
||||
assert 'web_search_options' in params
|
||||
|
||||
# Should also have web_fetch tool
|
||||
assert 'tools' in params
|
||||
web_fetch_found = False
|
||||
for tool in params['tools']:
|
||||
if isinstance(tool, dict) and tool.get('type') == 'web_fetch_20250910':
|
||||
web_fetch_found = True
|
||||
break
|
||||
assert web_fetch_found
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('ANTHROPIC_API_KEY'),
|
||||
reason="ANTHROPIC_API_KEY not set"
|
||||
)
|
||||
class TestWebFetchIntegration:
|
||||
"""Integration tests for web_fetch (requires API key)."""
|
||||
|
||||
def test_web_fetch_with_anthropic(self):
|
||||
"""Test actual web fetch with Anthropic API."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Web Fetch Agent",
|
||||
instructions="You are a helpful assistant that can fetch web content.",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
)
|
||||
|
||||
# This would make an actual API call
|
||||
# response = agent.chat("Fetch and summarize https://example.com")
|
||||
# assert response is not None
|
||||
pass # Skip actual API call in tests
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Test file for web_search feature (TDD approach).
|
||||
|
||||
Tests the LiteLLM native web search integration with fallback to DuckDuckGo.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestWebSearchSupport:
|
||||
"""Test web search capability detection."""
|
||||
|
||||
def test_supports_web_search_openai_search_preview(self):
|
||||
"""Test that gpt-4o-search-preview is detected as supporting web search."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("openai/gpt-4o-search-preview") == True
|
||||
|
||||
def test_supports_web_search_xai_grok(self):
|
||||
"""Test that xai/grok-3 is detected as supporting web search."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("xai/grok-3") == True
|
||||
|
||||
def test_supports_web_search_anthropic_claude(self):
|
||||
"""Test that claude-3-5-sonnet-latest is detected as supporting web search."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("anthropic/claude-3-5-sonnet-latest") == True
|
||||
|
||||
def test_supports_web_search_gemini(self):
|
||||
"""Test that gemini-2.0-flash is detected as supporting web search."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("gemini-2.0-flash") == True
|
||||
|
||||
def test_supports_web_search_ollama_false(self):
|
||||
"""Test that ollama models don't support native web search."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("ollama/llama3") == False
|
||||
|
||||
def test_supports_web_search_unknown_model(self):
|
||||
"""Test that unknown models default to False."""
|
||||
from praisonaiagents.llm.model_capabilities import supports_web_search
|
||||
assert supports_web_search("unknown/model") == False
|
||||
|
||||
|
||||
class TestLLMWebSearchParameter:
|
||||
"""Test LLM class web_search parameter."""
|
||||
|
||||
def test_llm_init_with_web_search_true(self):
|
||||
"""Test LLM initialization with web_search=True."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini", web_search=True)
|
||||
assert llm.web_search == True
|
||||
|
||||
def test_llm_init_with_web_search_dict(self):
|
||||
"""Test LLM initialization with web_search as dict."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
web_search_options = {"search_context_size": "high"}
|
||||
llm = LLM(model="gpt-4o-mini", web_search=web_search_options)
|
||||
assert llm.web_search == web_search_options
|
||||
|
||||
def test_llm_init_with_web_search_false(self):
|
||||
"""Test LLM initialization with web_search=False."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini", web_search=False)
|
||||
assert llm.web_search == False
|
||||
|
||||
def test_llm_init_without_web_search(self):
|
||||
"""Test LLM initialization without web_search (default None)."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
assert llm.web_search is None
|
||||
|
||||
def test_llm_supports_web_search_method(self):
|
||||
"""Test LLM._supports_web_search() method."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="openai/gpt-4o-search-preview")
|
||||
assert llm._supports_web_search() == True
|
||||
|
||||
def test_llm_supports_web_search_method_ollama(self):
|
||||
"""Test LLM._supports_web_search() returns False for Ollama."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="ollama/llama3")
|
||||
assert llm._supports_web_search() == False
|
||||
|
||||
|
||||
class TestLLMBuildCompletionParams:
|
||||
"""Test _build_completion_params includes web_search_options."""
|
||||
|
||||
def test_build_params_with_web_search_true(self):
|
||||
"""Test that web_search=True adds web_search_options to params."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="openai/gpt-4o-search-preview", web_search=True)
|
||||
|
||||
with patch.object(llm, '_supports_web_search', return_value=True):
|
||||
params = llm._build_completion_params(messages=[{"role": "user", "content": "test"}])
|
||||
assert "web_search_options" in params
|
||||
assert params["web_search_options"] == {"search_context_size": "medium"}
|
||||
|
||||
def test_build_params_with_web_search_dict(self):
|
||||
"""Test that web_search dict is passed correctly."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
web_search_options = {"search_context_size": "high"}
|
||||
llm = LLM(model="openai/gpt-4o-search-preview", web_search=web_search_options)
|
||||
|
||||
with patch.object(llm, '_supports_web_search', return_value=True):
|
||||
params = llm._build_completion_params(messages=[{"role": "user", "content": "test"}])
|
||||
assert "web_search_options" in params
|
||||
assert params["web_search_options"] == {"search_context_size": "high"}
|
||||
|
||||
def test_build_params_without_web_search(self):
|
||||
"""Test that web_search_options is not added when web_search is None."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
params = llm._build_completion_params(messages=[{"role": "user", "content": "test"}])
|
||||
assert "web_search_options" not in params
|
||||
|
||||
def test_build_params_web_search_unsupported_model(self):
|
||||
"""Test that web_search_options is not added for unsupported models."""
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
llm = LLM(model="ollama/llama3", web_search=True)
|
||||
|
||||
with patch.object(llm, '_supports_web_search', return_value=False):
|
||||
params = llm._build_completion_params(messages=[{"role": "user", "content": "test"}])
|
||||
assert "web_search_options" not in params
|
||||
|
||||
|
||||
class TestAgentWebSearchParameter:
|
||||
"""Test Agent class web_search parameter."""
|
||||
|
||||
def test_agent_init_with_web_search_true(self):
|
||||
"""Test Agent initialization with web_search=True."""
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
web_search=True
|
||||
)
|
||||
assert agent.web_search == True
|
||||
|
||||
def test_agent_init_with_web_search_dict(self):
|
||||
"""Test Agent initialization with web_search as dict."""
|
||||
from praisonaiagents import Agent
|
||||
web_search_options = {"search_context_size": "high"}
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
web_search=web_search_options
|
||||
)
|
||||
assert agent.web_search == web_search_options
|
||||
|
||||
def test_agent_init_without_web_search(self):
|
||||
"""Test Agent initialization without web_search (default None)."""
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test"
|
||||
)
|
||||
assert agent.web_search is None
|
||||
|
||||
def test_agent_with_custom_llm_passes_web_search(self):
|
||||
"""Test that Agent passes web_search to custom LLM instance."""
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
llm="openai/gpt-4o-search-preview",
|
||||
web_search=True
|
||||
)
|
||||
# When using custom LLM (with /), it creates llm_instance
|
||||
assert hasattr(agent, 'llm_instance')
|
||||
assert agent.llm_instance.web_search == True
|
||||
|
||||
|
||||
class TestWebSearchFallback:
|
||||
"""Test fallback to DuckDuckGo when native web search not supported."""
|
||||
|
||||
def test_fallback_tool_injection_for_unsupported_model(self):
|
||||
"""Test that DuckDuckGo tool is injected for unsupported models."""
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
llm="ollama/llama3",
|
||||
web_search=True
|
||||
)
|
||||
# Check that internet_search tool was added to tools
|
||||
tool_names = [getattr(t, '__name__', str(t)) for t in agent.tools]
|
||||
assert 'internet_search' in tool_names or 'duckduckgo' in tool_names
|
||||
|
||||
def test_no_fallback_for_supported_model(self):
|
||||
"""Test that no fallback tool is added for supported models."""
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
llm="openai/gpt-4o-search-preview",
|
||||
web_search=True,
|
||||
tools=[] # Explicitly empty tools
|
||||
)
|
||||
# For supported models, tools should remain empty (native web search used)
|
||||
# Only check if no DuckDuckGo tool was auto-added
|
||||
tool_names = [getattr(t, '__name__', str(t)) for t in agent.tools]
|
||||
# internet_search should NOT be auto-added for supported models
|
||||
assert 'internet_search' not in tool_names
|
||||
|
||||
|
||||
class TestDuckDuckGoToolPreservation:
|
||||
"""Test that DuckDuckGo tools still work as before."""
|
||||
|
||||
def test_duckduckgo_import(self):
|
||||
"""Test that DuckDuckGo tools can still be imported."""
|
||||
from praisonaiagents.tools import internet_search, duckduckgo
|
||||
assert callable(internet_search)
|
||||
assert callable(duckduckgo)
|
||||
|
||||
def test_explicit_duckduckgo_tool_usage(self):
|
||||
"""Test that explicit DuckDuckGo tool usage still works."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import internet_search
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Test",
|
||||
tools=[internet_search]
|
||||
)
|
||||
assert internet_search in agent.tools
|
||||
|
||||
|
||||
class TestWebSearchIntegration:
|
||||
"""Integration tests for web search (requires API keys)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set"
|
||||
)
|
||||
def test_web_search_with_openai(self):
|
||||
"""Test actual web search with OpenAI (integration test)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Researcher",
|
||||
instructions="You are a helpful research assistant.",
|
||||
llm="openai/gpt-4o-search-preview",
|
||||
web_search=True
|
||||
)
|
||||
|
||||
result = agent.start("What is the current weather in San Francisco?")
|
||||
assert result is not None
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.163"
|
||||
version = "0.0.175"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+888
-5
@@ -1,9 +1,892 @@
|
||||
# PraisonAI Package
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/logo/dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/logo/light.png" />
|
||||
<img alt="PraisonAI Logo" src="docs/logo/light.png" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
This is the PraisonAI package, which serves as a wrapper for PraisonAIAgents.
|
||||
<p align="center">
|
||||
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://static.pepy.tech/badge/PraisonAI" alt="Total Downloads" /></a>
|
||||
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/github/v/release/MervinPraison/PraisonAI" alt="Latest Stable Version" /></a>
|
||||
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License" /></a>
|
||||
</p>
|
||||
|
||||
It provides a simple and intuitive interface for working with AI agents and their capabilities.
|
||||
<div align="center">
|
||||
|
||||
## Directory Structure
|
||||
# Praison AI
|
||||
|
||||
<a href="https://trendshift.io/repositories/9130" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9130" alt="MervinPraison%2FPraisonAI | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
PraisonAI is a production-ready Multi-AI Agents framework with self-reflection, designed to create AI Agents to automate and solve problems ranging from simple tasks to complex challenges. By integrating PraisonAI Agents, AG2 (Formerly AutoGen), and CrewAI into a low-code solution, it streamlines the building and management of multi-agent LLM systems, emphasising simplicity, customisation, and effective human-agent collaboration.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.praison.ai">
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/📚_Documentation-Visit_docs.praison.ai-blue?style=for-the-badge&logo=bookstack&logoColor=white" alt="Documentation" />
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🤖 Automated AI Agents Creation
|
||||
- 🔄 Self Reflection AI Agents
|
||||
- 🧠 Reasoning AI Agents
|
||||
- 👁️ Multi Modal AI Agents
|
||||
- 🤝 Multi Agent Collaboration
|
||||
- 🎭 AI Agent Workflow
|
||||
- 📚 Add Custom Knowledge
|
||||
- 🧠 Agents with Short and Long Term Memory
|
||||
- 📄 Chat with PDF Agents
|
||||
- 💻 Code Interpreter Agents
|
||||
- 📚 RAG Agents
|
||||
- 🤔 Async & Parallel Processing
|
||||
- 🔄 Auto Agents
|
||||
- 🔢 Math Agents
|
||||
- 🎯 Structured Output Agents
|
||||
- 🔗 LangChain Integrated Agents
|
||||
- 📞 Callback Agents
|
||||
- 🤏 Mini AI Agents
|
||||
- 🛠️ 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)
|
||||
|
||||
## Using Python Code
|
||||
|
||||
Light weight package dedicated for coding:
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
```
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### 1. Single Agent
|
||||
|
||||
Create app.py file and add the code below:
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(instructions="Your are a helpful AI assistant")
|
||||
agent.start("Write a movie script about a robot in Mars")
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 2. Multi Agents
|
||||
|
||||
Create app.py file and add the code below:
|
||||
```python
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
|
||||
research_agent = Agent(instructions="Research about AI")
|
||||
summarise_agent = Agent(instructions="Summarise research agent's findings")
|
||||
agents = PraisonAIAgents(agents=[research_agent, summarise_agent])
|
||||
agents.start()
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 3. 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
|
||||
|
||||
### 4. 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
|
||||
|
||||
## Using No Code
|
||||
|
||||
### Auto Mode:
|
||||
```bash
|
||||
pip install praisonai
|
||||
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"
|
||||
```
|
||||
|
||||
## Using JavaScript Code
|
||||
|
||||
```bash
|
||||
npm install praisonai
|
||||
export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { Agent } = require('praisonai');
|
||||
const agent = new Agent({ instructions: 'You are a helpful AI assistant' });
|
||||
agent.start('Write a movie script about a robot in Mars');
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
[](https://docs.praison.ai)
|
||||
|
||||
## AI Agents Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
%% Define the main flow
|
||||
Start([▶ Start]) --> Agent1
|
||||
Agent1 --> Process[⚙ Process]
|
||||
Process --> Agent2
|
||||
Agent2 --> Output([✓ Output])
|
||||
Process -.-> Agent1
|
||||
|
||||
%% Define subgraphs for agents and their tasks
|
||||
subgraph Agent1[ ]
|
||||
Task1[📋 Task]
|
||||
AgentIcon1[🤖 AI Agent]
|
||||
Tools1[🔧 Tools]
|
||||
|
||||
Task1 --- AgentIcon1
|
||||
AgentIcon1 --- Tools1
|
||||
end
|
||||
|
||||
subgraph Agent2[ ]
|
||||
Task2[📋 Task]
|
||||
AgentIcon2[🤖 AI Agent]
|
||||
Tools2[🔧 Tools]
|
||||
|
||||
Task2 --- AgentIcon2
|
||||
AgentIcon2 --- Tools2
|
||||
end
|
||||
|
||||
classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
|
||||
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
|
||||
classDef tools fill:#2E8B57,stroke:#7C90A0,color:#fff
|
||||
classDef transparent fill:none,stroke:none
|
||||
|
||||
class Start,Output,Task1,Task2 input
|
||||
class Process,AgentIcon1,AgentIcon2 process
|
||||
class Tools1,Tools2 tools
|
||||
class Agent1,Agent2 transparent
|
||||
```
|
||||
|
||||
## AI Agents with Tools
|
||||
|
||||
Create AI agents that can use tools to interact with external systems and perform actions.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Tools
|
||||
direction TB
|
||||
T3[Internet Search]
|
||||
T1[Code Execution]
|
||||
T2[Formatting]
|
||||
end
|
||||
|
||||
Input[Input] ---> Agents
|
||||
subgraph Agents
|
||||
direction LR
|
||||
A1[Agent 1]
|
||||
A2[Agent 2]
|
||||
A3[Agent 3]
|
||||
end
|
||||
Agents ---> Output[Output]
|
||||
|
||||
T3 --> A1
|
||||
T1 --> A2
|
||||
T2 --> A3
|
||||
|
||||
style Tools fill:#189AB4,color:#fff
|
||||
style Agents fill:#8B0000,color:#fff
|
||||
style Input fill:#8B0000,color:#fff
|
||||
style Output fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
## AI Agents with Memory
|
||||
|
||||
Create AI agents with memory capabilities for maintaining context and information across tasks.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Memory
|
||||
direction TB
|
||||
STM[Short Term]
|
||||
LTM[Long Term]
|
||||
end
|
||||
|
||||
subgraph Store
|
||||
direction TB
|
||||
DB[(Vector DB)]
|
||||
end
|
||||
|
||||
Input[Input] ---> Agents
|
||||
subgraph Agents
|
||||
direction LR
|
||||
A1[Agent 1]
|
||||
A2[Agent 2]
|
||||
A3[Agent 3]
|
||||
end
|
||||
Agents ---> Output[Output]
|
||||
|
||||
Memory <--> Store
|
||||
Store <--> A1
|
||||
Store <--> A2
|
||||
Store <--> A3
|
||||
|
||||
style Memory fill:#189AB4,color:#fff
|
||||
style Store fill:#2E8B57,color:#fff
|
||||
style Agents fill:#8B0000,color:#fff
|
||||
style Input fill:#8B0000,color:#fff
|
||||
style Output fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
## AI Agents with Different Processes
|
||||
|
||||
### Sequential Process
|
||||
|
||||
The simplest form of task execution where tasks are performed one after another.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Input[Input] --> A1
|
||||
subgraph Agents
|
||||
direction LR
|
||||
A1[Agent 1] --> A2[Agent 2] --> A3[Agent 3]
|
||||
end
|
||||
A3 --> Output[Output]
|
||||
|
||||
classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
|
||||
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
|
||||
classDef transparent fill:none,stroke:none
|
||||
|
||||
class Input,Output input
|
||||
class A1,A2,A3 process
|
||||
class Agents transparent
|
||||
```
|
||||
|
||||
### Hierarchical Process
|
||||
|
||||
Uses a manager agent to coordinate task execution and agent assignments.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Input[Input] --> Manager
|
||||
|
||||
subgraph Agents
|
||||
Manager[Manager Agent]
|
||||
|
||||
subgraph Workers
|
||||
direction LR
|
||||
W1[Worker 1]
|
||||
W2[Worker 2]
|
||||
W3[Worker 3]
|
||||
end
|
||||
|
||||
Manager --> W1
|
||||
Manager --> W2
|
||||
Manager --> W3
|
||||
end
|
||||
|
||||
W1 --> Manager
|
||||
W2 --> Manager
|
||||
W3 --> Manager
|
||||
Manager --> Output[Output]
|
||||
|
||||
classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
|
||||
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
|
||||
classDef transparent fill:none,stroke:none
|
||||
|
||||
class Input,Output input
|
||||
class Manager,W1,W2,W3 process
|
||||
class Agents,Workers transparent
|
||||
```
|
||||
|
||||
### Workflow Process
|
||||
|
||||
Advanced process type supporting complex task relationships and conditional execution.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Input[Input] --> Start
|
||||
|
||||
subgraph Workflow
|
||||
direction LR
|
||||
Start[Start] --> C1{Condition}
|
||||
C1 --> |Yes| A1[Agent 1]
|
||||
C1 --> |No| A2[Agent 2]
|
||||
A1 --> Join
|
||||
A2 --> Join
|
||||
Join --> A3[Agent 3]
|
||||
end
|
||||
|
||||
A3 --> Output[Output]
|
||||
|
||||
classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
|
||||
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
|
||||
classDef decision fill:#2E8B57,stroke:#7C90A0,color:#fff
|
||||
classDef transparent fill:none,stroke:none
|
||||
|
||||
class Input,Output input
|
||||
class Start,A1,A2,A3,Join process
|
||||
class C1 decision
|
||||
class Workflow transparent
|
||||
```
|
||||
|
||||
#### Agentic Routing Workflow
|
||||
|
||||
Create AI agents that can dynamically route tasks to specialized LLM instances.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[In] --> Router[LLM Call Router]
|
||||
Router --> LLM1[LLM Call 1]
|
||||
Router --> LLM2[LLM Call 2]
|
||||
Router --> LLM3[LLM Call 3]
|
||||
LLM1 --> Out[Out]
|
||||
LLM2 --> Out
|
||||
LLM3 --> Out
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style Router fill:#2E8B57,color:#fff
|
||||
style LLM1 fill:#2E8B57,color:#fff
|
||||
style LLM2 fill:#2E8B57,color:#fff
|
||||
style LLM3 fill:#2E8B57,color:#fff
|
||||
style Out fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
#### Agentic Orchestrator Worker
|
||||
|
||||
Create AI agents that orchestrate and distribute tasks among specialized workers.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[In] --> Router[LLM Call Router]
|
||||
Router --> LLM1[LLM Call 1]
|
||||
Router --> LLM2[LLM Call 2]
|
||||
Router --> LLM3[LLM Call 3]
|
||||
LLM1 --> Synthesizer[Synthesizer]
|
||||
LLM2 --> Synthesizer
|
||||
LLM3 --> Synthesizer
|
||||
Synthesizer --> Out[Out]
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style Router fill:#2E8B57,color:#fff
|
||||
style LLM1 fill:#2E8B57,color:#fff
|
||||
style LLM2 fill:#2E8B57,color:#fff
|
||||
style LLM3 fill:#2E8B57,color:#fff
|
||||
style Synthesizer fill:#2E8B57,color:#fff
|
||||
style Out fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
#### Agentic Autonomous Workflow
|
||||
|
||||
Create AI agents that can autonomously monitor, act, and adapt based on environment feedback.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Human[Human] <--> LLM[LLM Call]
|
||||
LLM -->|ACTION| Environment[Environment]
|
||||
Environment -->|FEEDBACK| LLM
|
||||
LLM --> Stop[Stop]
|
||||
|
||||
style Human fill:#8B0000,color:#fff
|
||||
style LLM fill:#2E8B57,color:#fff
|
||||
style Environment fill:#8B0000,color:#fff
|
||||
style Stop fill:#333,color:#fff
|
||||
```
|
||||
|
||||
#### Agentic Parallelization
|
||||
|
||||
Create AI agents that can execute tasks in parallel for improved performance.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[In] --> LLM2[LLM Call 2]
|
||||
In --> LLM1[LLM Call 1]
|
||||
In --> LLM3[LLM Call 3]
|
||||
LLM1 --> Aggregator[Aggregator]
|
||||
LLM2 --> Aggregator
|
||||
LLM3 --> Aggregator
|
||||
Aggregator --> Out[Out]
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style LLM1 fill:#2E8B57,color:#fff
|
||||
style LLM2 fill:#2E8B57,color:#fff
|
||||
style LLM3 fill:#2E8B57,color:#fff
|
||||
style Aggregator fill:#fff,color:#000
|
||||
style Out fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
#### Agentic Prompt Chaining
|
||||
|
||||
Create AI agents with sequential prompt chaining for complex workflows.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[In] --> LLM1[LLM Call 1] --> Gate{Gate}
|
||||
Gate -->|Pass| LLM2[LLM Call 2] -->|Output 2| LLM3[LLM Call 3] --> Out[Out]
|
||||
Gate -->|Fail| Exit[Exit]
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style LLM1 fill:#2E8B57,color:#fff
|
||||
style LLM2 fill:#2E8B57,color:#fff
|
||||
style LLM3 fill:#2E8B57,color:#fff
|
||||
style Out fill:#8B0000,color:#fff
|
||||
style Exit fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
#### Agentic Evaluator Optimizer
|
||||
|
||||
Create AI agents that can generate and optimize solutions through iterative feedback.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[In] --> Generator[LLM Call Generator]
|
||||
Generator -->|SOLUTION| Evaluator[LLM Call Evaluator] -->|ACCEPTED| Out[Out]
|
||||
Evaluator -->|REJECTED + FEEDBACK| Generator
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style Generator fill:#2E8B57,color:#fff
|
||||
style Evaluator fill:#2E8B57,color:#fff
|
||||
style Out fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
#### Repetitive Agents
|
||||
|
||||
Create AI agents that can efficiently handle repetitive tasks through automated loops.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
In[Input] --> LoopAgent[("Looping Agent")]
|
||||
LoopAgent --> Task[Task]
|
||||
Task --> |Next iteration| LoopAgent
|
||||
Task --> |Done| Out[Output]
|
||||
|
||||
style In fill:#8B0000,color:#fff
|
||||
style LoopAgent fill:#2E8B57,color:#fff,shape:circle
|
||||
style Task fill:#2E8B57,color:#fff
|
||||
style Out fill:#8B0000,color:#fff
|
||||
```
|
||||
|
||||
## Adding Models
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.praison.ai/models">
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/%F0%9F%93%9A_Models-Visit_docs.praison.ai-blue?style=for-the-badge&logo=bookstack&logoColor=white" alt="Models" />
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Ollama Integration
|
||||
```bash
|
||||
export OPENAI_BASE_URL=http://localhost:11434/v1
|
||||
```
|
||||
|
||||
## Groq Integration
|
||||
Replace xxxx with Groq API KEY:
|
||||
```bash
|
||||
export OPENAI_API_KEY=xxxxxxxxxxx
|
||||
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
|
||||
```
|
||||
|
||||
## No Code Options
|
||||
|
||||
## Agents Playbook
|
||||
|
||||
### Simple Playbook Example
|
||||
|
||||
Create `agents.yaml` file and add the code below:
|
||||
|
||||
```yaml
|
||||
framework: praisonai
|
||||
topic: Artificial Intelligence
|
||||
roles:
|
||||
screenwriter:
|
||||
backstory: "Skilled in crafting scripts with engaging dialogue about {topic}."
|
||||
goal: Create scripts from concepts.
|
||||
role: Screenwriter
|
||||
tasks:
|
||||
scriptwriting_task:
|
||||
description: "Develop scripts with compelling characters and dialogue about {topic}."
|
||||
expected_output: "Complete script ready for production."
|
||||
```
|
||||
|
||||
*To run the playbook:*
|
||||
```bash
|
||||
praisonai agents.yaml
|
||||
```
|
||||
|
||||
## Use 100+ Models
|
||||
|
||||
- https://docs.praison.ai/models/
|
||||
<div align="center">
|
||||
<a href="https://docs.praison.ai">
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/📚_Documentation-Visit_docs.praison.ai-blue?style=for-the-badge&logo=bookstack&logoColor=white" alt="Documentation" />
|
||||
</p>
|
||||
</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.
|
||||
|
||||
### Using uv
|
||||
```bash
|
||||
# Install uv if you haven't already
|
||||
pip install uv
|
||||
|
||||
# Install from requirements
|
||||
uv pip install -r pyproject.toml
|
||||
|
||||
# Install with extras
|
||||
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.
|
||||
- Clone your fork: `git clone https://github.com/yourusername/praisonAI.git`
|
||||
- Create a branch: `git checkout -b new-feature`
|
||||
- Make changes and commit: `git commit -am "Add some feature"`
|
||||
- Push to your fork: `git push origin new-feature`
|
||||
- Submit a pull request via GitHub's web interface.
|
||||
- Await feedback from project maintainers.
|
||||
|
||||
## Other Features
|
||||
|
||||
- 🔄 Use CrewAI or AG2 (Formerly AutoGen) Framework
|
||||
- 💻 Chat with ENTIRE Codebase
|
||||
- 🎨 Interactive UIs
|
||||
- 📄 YAML-based Configuration
|
||||
- 🛠️ Custom Tool Integration
|
||||
- 🔍 Internet Search Capability (using Crawl4AI and Tavily)
|
||||
- 🖼️ Vision Language Model (VLM) Support
|
||||
- 🎙️ Real-time Voice Interaction
|
||||
|
||||
## Video Tutorials
|
||||
|
||||
| Topic | Video |
|
||||
|-------|--------|
|
||||
| AI Agents with Self Reflection | [](https://www.youtube.com/watch?v=vLXobEN2Vc8) |
|
||||
| Reasoning Data Generating Agent | [](https://www.youtube.com/watch?v=fUT332Y2zA8) |
|
||||
| AI Agents with Reasoning | [](https://www.youtube.com/watch?v=KNDVWGN3TpM) |
|
||||
| Multimodal AI Agents | [](https://www.youtube.com/watch?v=hjAWmUT1qqY) |
|
||||
| AI Agents Workflow | [](https://www.youtube.com/watch?v=yWTH44QPl2A) |
|
||||
| Async AI Agents | [](https://www.youtube.com/watch?v=VhVQfgo00LE) |
|
||||
| Mini AI Agents | [](https://www.youtube.com/watch?v=OkvYp5aAGSg) |
|
||||
| AI Agents with Memory | [](https://www.youtube.com/watch?v=1hVfVxvPnnQ) |
|
||||
| Repetitive Agents | [](https://www.youtube.com/watch?v=dAYGxsjDOPg) |
|
||||
| Introduction | [](https://www.youtube.com/watch?v=Fn1lQjC0GO0) |
|
||||
| Tools Overview | [](https://www.youtube.com/watch?v=XaQRgRpV7jo) |
|
||||
| Custom Tools | [](https://www.youtube.com/watch?v=JSU2Rndh06c) |
|
||||
| Firecrawl Integration | [](https://www.youtube.com/watch?v=UoqUDcLcOYo) |
|
||||
| User Interface | [](https://www.youtube.com/watch?v=tg-ZjNl3OCg) |
|
||||
| Crawl4AI Integration | [](https://www.youtube.com/watch?v=KAvuVUh0XU8) |
|
||||
| Chat Interface | [](https://www.youtube.com/watch?v=sw3uDqn2h1Y) |
|
||||
| Code Interface | [](https://www.youtube.com/watch?v=_5jQayO-MQY) |
|
||||
| Mem0 Integration | [](https://www.youtube.com/watch?v=KIGSgRxf1cY) |
|
||||
| Training | [](https://www.youtube.com/watch?v=aLawE8kwCrI) |
|
||||
| Realtime Voice Interface | [](https://www.youtube.com/watch?v=frRHfevTCSw) |
|
||||
| Call Interface | [](https://www.youtube.com/watch?v=m1cwrUG2iAk) |
|
||||
| Reasoning Extract Agents | [](https://www.youtube.com/watch?v=2PPamsADjJA) |
|
||||
|
||||
The main package code is located in the `praisonai` subdirectory.
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.89.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.89.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
+462
-11
@@ -28,6 +28,13 @@ AUTOGEN_AVAILABLE = False
|
||||
PRAISONAI_AVAILABLE = False
|
||||
TRAIN_AVAILABLE = False
|
||||
try:
|
||||
import importlib.util
|
||||
CHAINLIT_AVAILABLE = importlib.util.find_spec("chainlit") is not None
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def _get_chainlit_run():
|
||||
"""Lazy import chainlit to avoid loading .env at startup"""
|
||||
# Create necessary directories and set CHAINLIT_APP_ROOT
|
||||
if "CHAINLIT_APP_ROOT" not in os.environ:
|
||||
chainlit_root = os.path.join(os.path.expanduser("~"), ".praison")
|
||||
@@ -39,9 +46,7 @@ try:
|
||||
os.makedirs(os.path.join(chainlit_root, ".files"), exist_ok=True)
|
||||
|
||||
from chainlit.cli import chainlit_run
|
||||
CHAINLIT_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
return chainlit_run
|
||||
|
||||
try:
|
||||
import gradio as gr
|
||||
@@ -509,9 +514,8 @@ class PraisonAI:
|
||||
in_test_env = (
|
||||
'pytest' in sys.argv[0] or
|
||||
'unittest' in sys.argv[0] or
|
||||
any('test' in arg for arg in sys.argv[1:3]) or # Check first few args for test indicators
|
||||
'pytest' in sys.modules or
|
||||
'unittest' in sys.modules
|
||||
any(arg.startswith('tests.') or arg.startswith('tests/') for arg in sys.argv[1:3]) or # Check for test module paths
|
||||
'PYTEST_CURRENT_TEST' in os.environ
|
||||
)
|
||||
|
||||
# Check if we're being used as a library (not from praisonai CLI)
|
||||
@@ -545,7 +549,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -572,6 +576,17 @@ class PraisonAI:
|
||||
parser.add_argument("--url", type=str, help="Repository URL for context analysis")
|
||||
parser.add_argument("--goal", type=str, help="Goal for context engineering")
|
||||
parser.add_argument("--auto-analyze", action="store_true", help="Enable automatic analysis in context engineering")
|
||||
parser.add_argument("--research", action="store_true", help="Run deep research on a topic")
|
||||
parser.add_argument("--query-rewrite", action="store_true", help="Rewrite query for better results (works with any command)")
|
||||
parser.add_argument("--rewrite-tools", type=str, help="Tools for query rewriter (e.g., 'internet_search' or path to tools.py)")
|
||||
parser.add_argument("--expand-prompt", action="store_true", help="Expand short prompt into detailed prompt (works with any command)")
|
||||
parser.add_argument("--expand-tools", type=str, help="Tools for prompt expander (e.g., 'internet_search' or path to tools.py)")
|
||||
parser.add_argument("--tools", "-t", type=str, help="Path to tools.py file for research agent")
|
||||
parser.add_argument("--save", "-s", action="store_true", help="Save research output to file (output/research/)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output for research")
|
||||
parser.add_argument("--web-search", action="store_true", help="Enable native web search (OpenAI, Gemini, Anthropic, xAI, Perplexity)")
|
||||
parser.add_argument("--web-fetch", action="store_true", help="Enable web fetch to retrieve URL content (Anthropic only)")
|
||||
parser.add_argument("--prompt-caching", action="store_true", help="Enable prompt caching to reduce costs (OpenAI, Anthropic, Bedrock, Deepseek)")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
@@ -696,6 +711,29 @@ class PraisonAI:
|
||||
self.handle_context_command(args.url, args.goal, getattr(args, 'auto_analyze', False))
|
||||
sys.exit(0)
|
||||
|
||||
elif args.command == 'research':
|
||||
if not PRAISONAI_AVAILABLE:
|
||||
print("[red]ERROR: PraisonAI Agents is not installed. Install with:[/red]")
|
||||
print("\npip install praisonaiagents\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Get the research query from remaining args
|
||||
research_query = ' '.join(unknown_args) if unknown_args else None
|
||||
if not research_query:
|
||||
print("[red]ERROR: Research query is required[/red]")
|
||||
print("Usage: praisonai research \"Your research query\"")
|
||||
print(" praisonai research --model deep-research-pro \"Your query\"")
|
||||
sys.exit(1)
|
||||
|
||||
research_model = getattr(args, 'model', None)
|
||||
verbose = getattr(args, 'verbose', False)
|
||||
save = getattr(args, 'save', False)
|
||||
query_rewrite = getattr(args, 'query_rewrite', False)
|
||||
rewrite_tools = getattr(args, 'rewrite_tools', None)
|
||||
tools_path = getattr(args, 'tools', None)
|
||||
self.handle_research_command(research_query, research_model, verbose, save, query_rewrite, tools_path, rewrite_tools)
|
||||
sys.exit(0)
|
||||
|
||||
# Only check framework availability for agent-related operations
|
||||
if not args.command and (args.init or args.auto or args.framework):
|
||||
if not CREWAI_AVAILABLE and not AUTOGEN_AVAILABLE and not PRAISONAI_AVAILABLE:
|
||||
@@ -714,10 +752,228 @@ class PraisonAI:
|
||||
|
||||
return args
|
||||
|
||||
def _rewrite_query(self, query: str, rewrite_tools: str = None, verbose: bool = False) -> str:
|
||||
"""
|
||||
Rewrite query using QueryRewriterAgent.
|
||||
|
||||
Args:
|
||||
query: The query to rewrite
|
||||
rewrite_tools: Tool names (comma-separated) or path to tools.py
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Rewritten query or original if rewriting fails
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
from rich import print
|
||||
|
||||
print("[bold cyan]Rewriting query for better results...[/bold cyan]")
|
||||
|
||||
# Load rewrite tools if specified
|
||||
rewrite_tools_list = []
|
||||
if rewrite_tools:
|
||||
if os.path.isfile(rewrite_tools):
|
||||
# Load from file
|
||||
try:
|
||||
import inspect
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("rewrite_tools_module", rewrite_tools)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isfunction(obj) and not name.startswith('_'):
|
||||
rewrite_tools_list.append(obj)
|
||||
if rewrite_tools_list:
|
||||
print(f"[cyan]Loaded {len(rewrite_tools_list)} tools for query rewriter[/cyan]")
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load rewrite tools: {e}[/yellow]")
|
||||
else:
|
||||
# Treat as comma-separated tool names
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
import praisonaiagents.tools as tools_module
|
||||
|
||||
tool_names = [t.strip() for t in rewrite_tools.split(',')]
|
||||
for tool_name in tool_names:
|
||||
if tool_name in TOOL_MAPPINGS:
|
||||
try:
|
||||
tool = getattr(tools_module, tool_name)
|
||||
rewrite_tools_list.append(tool)
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load rewrite tool '{tool_name}': {e}[/yellow]")
|
||||
else:
|
||||
print(f"[yellow]Warning: Unknown rewrite tool '{tool_name}'[/yellow]")
|
||||
if rewrite_tools_list:
|
||||
print(f"[cyan]Using rewrite tools: {', '.join(tool_names)}[/cyan]")
|
||||
except ImportError:
|
||||
print("[yellow]Warning: Could not import tools module[/yellow]")
|
||||
|
||||
rewriter = QueryRewriterAgent(
|
||||
model="gpt-4o-mini",
|
||||
verbose=verbose,
|
||||
tools=rewrite_tools_list if rewrite_tools_list else None
|
||||
)
|
||||
result = rewriter.rewrite(query, strategy=RewriteStrategy.AUTO)
|
||||
rewritten = result.primary_query
|
||||
|
||||
print(f"[cyan]Original:[/cyan] {query}")
|
||||
print(f"[cyan]Rewritten:[/cyan] {rewritten}")
|
||||
|
||||
return rewritten
|
||||
|
||||
except ImportError:
|
||||
from rich import print
|
||||
print("[yellow]Warning: QueryRewriterAgent not available, using original query[/yellow]")
|
||||
return query
|
||||
except Exception as e:
|
||||
from rich import print
|
||||
print(f"[yellow]Warning: Query rewrite failed ({e}), using original query[/yellow]")
|
||||
return query
|
||||
|
||||
def _rewrite_query_if_enabled(self, query: str) -> str:
|
||||
"""
|
||||
Rewrite query using QueryRewriterAgent if --query-rewrite is enabled.
|
||||
Returns the rewritten query or original if rewriting is disabled/fails.
|
||||
"""
|
||||
if not hasattr(self, 'args') or not getattr(self.args, 'query_rewrite', False):
|
||||
return query
|
||||
|
||||
rewrite_tools = getattr(self.args, 'rewrite_tools', None)
|
||||
verbose = getattr(self.args, 'verbose', False)
|
||||
return self._rewrite_query(query, rewrite_tools, verbose)
|
||||
|
||||
def _expand_prompt(self, prompt: str, expand_tools: str = None, verbose: bool = False) -> str:
|
||||
"""
|
||||
Expand prompt using PromptExpanderAgent.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to expand
|
||||
expand_tools: Tool names (comma-separated) or path to tools.py
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Expanded prompt or original if expansion fails
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
from rich import print
|
||||
|
||||
print("[bold cyan]Expanding prompt for detailed execution...[/bold cyan]")
|
||||
|
||||
# Load expand tools if specified
|
||||
expand_tools_list = []
|
||||
if expand_tools:
|
||||
if os.path.isfile(expand_tools):
|
||||
# Load from file
|
||||
try:
|
||||
import inspect
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("expand_tools_module", expand_tools)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isfunction(obj) and not name.startswith('_'):
|
||||
expand_tools_list.append(obj)
|
||||
if expand_tools_list:
|
||||
print(f"[cyan]Loaded {len(expand_tools_list)} tools for prompt expander[/cyan]")
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load expand tools: {e}[/yellow]")
|
||||
else:
|
||||
# Treat as comma-separated tool names
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
import praisonaiagents.tools as tools_module
|
||||
|
||||
tool_names = [t.strip() for t in expand_tools.split(',')]
|
||||
for tool_name in tool_names:
|
||||
if tool_name in TOOL_MAPPINGS:
|
||||
try:
|
||||
tool = getattr(tools_module, tool_name)
|
||||
expand_tools_list.append(tool)
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load expand tool '{tool_name}': {e}[/yellow]")
|
||||
else:
|
||||
print(f"[yellow]Warning: Unknown expand tool '{tool_name}'[/yellow]")
|
||||
if expand_tools_list:
|
||||
print(f"[cyan]Using expand tools: {', '.join(tool_names)}[/cyan]")
|
||||
except ImportError:
|
||||
print("[yellow]Warning: Could not import tools module[/yellow]")
|
||||
|
||||
expander = PromptExpanderAgent(
|
||||
model="gpt-4o-mini",
|
||||
verbose=verbose,
|
||||
tools=expand_tools_list if expand_tools_list else None
|
||||
)
|
||||
result = expander.expand(prompt, strategy=ExpandStrategy.AUTO)
|
||||
expanded = result.expanded_prompt
|
||||
|
||||
print(f"[cyan]Original:[/cyan] {prompt}")
|
||||
print(f"[cyan]Expanded:[/cyan] {expanded}")
|
||||
|
||||
return expanded
|
||||
|
||||
except ImportError:
|
||||
from rich import print
|
||||
print("[yellow]Warning: PromptExpanderAgent not available, using original prompt[/yellow]")
|
||||
return prompt
|
||||
except Exception as e:
|
||||
from rich import print
|
||||
print(f"[yellow]Warning: Prompt expansion failed ({e}), using original prompt[/yellow]")
|
||||
return prompt
|
||||
|
||||
def _expand_prompt_if_enabled(self, prompt: str) -> str:
|
||||
"""
|
||||
Expand prompt using PromptExpanderAgent if --expand-prompt is enabled.
|
||||
Returns the expanded prompt or original if expansion is disabled/fails.
|
||||
"""
|
||||
if not hasattr(self, 'args') or not getattr(self.args, 'expand_prompt', False):
|
||||
return prompt
|
||||
|
||||
expand_tools = getattr(self.args, 'expand_tools', None)
|
||||
verbose = getattr(self.args, 'verbose', False)
|
||||
return self._expand_prompt(prompt, expand_tools, verbose)
|
||||
|
||||
def _save_output(self, prompt: str, result: str):
|
||||
"""
|
||||
Save output to output/prompts/ folder.
|
||||
|
||||
Args:
|
||||
prompt: The original prompt
|
||||
result: The output result to save
|
||||
"""
|
||||
from datetime import datetime
|
||||
from rich import print
|
||||
|
||||
# Create output directory
|
||||
output_dir = os.path.join(os.getcwd(), "output", "prompts")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
# Create a safe filename from prompt (first 30 chars)
|
||||
safe_prompt = "".join(c if c.isalnum() or c in " -_" else "" for c in prompt[:30]).strip().replace(" ", "_")
|
||||
filename = f"{timestamp}_{safe_prompt}.md"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# Write output
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Prompt\n\n{prompt}\n\n")
|
||||
f.write(f"# Output\n\n{result}\n")
|
||||
|
||||
print(f"[green]✅ Output saved to: {filepath}[/green]")
|
||||
|
||||
def handle_direct_prompt(self, prompt):
|
||||
"""
|
||||
Handle direct prompt by creating a single agent and running it.
|
||||
"""
|
||||
# Apply query rewriting if enabled
|
||||
prompt = self._rewrite_query_if_enabled(prompt)
|
||||
# Apply prompt expansion if enabled
|
||||
prompt = self._expand_prompt_if_enabled(prompt)
|
||||
|
||||
if PRAISONAI_AVAILABLE:
|
||||
agent_config = {
|
||||
"name": "DirectAgent",
|
||||
@@ -730,8 +986,22 @@ class PraisonAI:
|
||||
if hasattr(self, 'args') and self.args.llm:
|
||||
agent_config["llm"] = self.args.llm
|
||||
|
||||
# Add feature flags if enabled
|
||||
if hasattr(self, 'args'):
|
||||
if getattr(self.args, 'web_search', False):
|
||||
agent_config["web_search"] = True
|
||||
if getattr(self.args, 'web_fetch', False):
|
||||
agent_config["web_fetch"] = True
|
||||
if getattr(self.args, 'prompt_caching', False):
|
||||
agent_config["prompt_caching"] = True
|
||||
|
||||
agent = PraisonAgent(**agent_config)
|
||||
result = agent.start(prompt)
|
||||
|
||||
# Save output if --save is enabled
|
||||
if hasattr(self, 'args') and getattr(self.args, 'save', False):
|
||||
self._save_output(prompt, result)
|
||||
|
||||
return result
|
||||
elif CREWAI_AVAILABLE:
|
||||
from crewai import Agent, Task, Crew
|
||||
@@ -791,7 +1061,7 @@ class PraisonAI:
|
||||
if "CHAINLIT_APP_ROOT" not in os.environ:
|
||||
os.environ["CHAINLIT_APP_ROOT"] = root_path
|
||||
chat_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'chat.py')
|
||||
chainlit_run([chat_ui_path])
|
||||
_get_chainlit_run()([chat_ui_path])
|
||||
else:
|
||||
print("ERROR: Chat UI is not installed. Please install it with 'pip install \"praisonai[chat]\"' to use the chat UI.")
|
||||
|
||||
@@ -815,7 +1085,7 @@ class PraisonAI:
|
||||
else:
|
||||
logging.info("Public folder already exists.")
|
||||
code_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'code.py')
|
||||
chainlit_run([code_ui_path])
|
||||
_get_chainlit_run()([code_ui_path])
|
||||
else:
|
||||
print("ERROR: Code UI is not installed. Please install it with 'pip install \"praisonai[code]\"' to use the code UI.")
|
||||
|
||||
@@ -861,7 +1131,7 @@ class PraisonAI:
|
||||
else:
|
||||
logging.info("Public folder already exists.")
|
||||
chainlit_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'agents.py')
|
||||
chainlit_run([chainlit_ui_path])
|
||||
_get_chainlit_run()([chainlit_ui_path])
|
||||
else:
|
||||
print("ERROR: Chainlit is not installed. Please install it with 'pip install \"praisonai[ui]\"' to use the UI.")
|
||||
|
||||
@@ -885,7 +1155,7 @@ class PraisonAI:
|
||||
else:
|
||||
logging.info("Public folder already exists.")
|
||||
realtime_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'realtime.py')
|
||||
chainlit_run([realtime_ui_path])
|
||||
_get_chainlit_run()([realtime_ui_path])
|
||||
else:
|
||||
print("ERROR: Realtime UI is not installed. Please install it with 'pip install \"praisonai[realtime]\"' to use the realtime UI.")
|
||||
|
||||
@@ -933,6 +1203,187 @@ class PraisonAI:
|
||||
print(f"[red]ERROR: Context engineering failed: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_research_command(self, query: str, model: str = None, verbose: bool = False, save: bool = False, query_rewrite: bool = False, tools_path: str = None, rewrite_tools: str = None) -> str:
|
||||
"""
|
||||
Handle the research command by creating a DeepResearchAgent and running it.
|
||||
|
||||
Args:
|
||||
query: Research query/topic
|
||||
model: Model for deep research (optional, defaults to o4-mini-deep-research)
|
||||
verbose: Enable verbose output (default: False)
|
||||
save: Save output to file (default: False)
|
||||
query_rewrite: Rewrite query before research (default: False)
|
||||
tools_path: Path to tools.py file with custom tools (default: None)
|
||||
rewrite_tools: Tools for query rewriter (tool names or file path)
|
||||
|
||||
Returns:
|
||||
str: Research report
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import DeepResearchAgent
|
||||
|
||||
# Suppress logging unless verbose
|
||||
if not verbose:
|
||||
logging.getLogger('google').setLevel(logging.WARNING)
|
||||
logging.getLogger('google.genai').setLevel(logging.WARNING)
|
||||
logging.getLogger('httpx').setLevel(logging.WARNING)
|
||||
logging.getLogger('httpcore').setLevel(logging.WARNING)
|
||||
|
||||
# Rewrite query if requested
|
||||
if query_rewrite:
|
||||
query = self._rewrite_query(query, rewrite_tools, verbose)
|
||||
|
||||
print("[bold green]Starting Deep Research...[/bold green]")
|
||||
print(f"Query: {query}")
|
||||
|
||||
# Default model if not specified
|
||||
if not model:
|
||||
model = "o4-mini-deep-research"
|
||||
|
||||
print(f"Model: {model}")
|
||||
|
||||
# Load tools if specified
|
||||
tools_list = []
|
||||
if tools_path:
|
||||
# Check if it's a file path or comma-separated tool names
|
||||
if os.path.isfile(tools_path):
|
||||
# Load from file
|
||||
try:
|
||||
import inspect
|
||||
spec = importlib.util.spec_from_file_location("tools_module", tools_path)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# Get all callable functions from the module
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isfunction(obj) and not name.startswith('_'):
|
||||
tools_list.append(obj)
|
||||
if tools_list:
|
||||
print(f"[cyan]Loaded {len(tools_list)} tools from {tools_path}[/cyan]")
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load tools from {tools_path}: {e}[/yellow]")
|
||||
else:
|
||||
# Treat as comma-separated tool names (e.g., "internet_search,wiki_search")
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
import praisonaiagents.tools as tools_module
|
||||
|
||||
tool_names = [t.strip() for t in tools_path.split(',')]
|
||||
for tool_name in tool_names:
|
||||
if tool_name in TOOL_MAPPINGS:
|
||||
try:
|
||||
tool = getattr(tools_module, tool_name)
|
||||
tools_list.append(tool)
|
||||
except Exception as e:
|
||||
print(f"[yellow]Warning: Failed to load tool '{tool_name}': {e}[/yellow]")
|
||||
else:
|
||||
print(f"[yellow]Warning: Unknown tool '{tool_name}'[/yellow]")
|
||||
if tools_list:
|
||||
print(f"[cyan]Loaded {len(tools_list)} built-in tools: {', '.join(tool_names)}[/cyan]")
|
||||
except ImportError:
|
||||
print("[yellow]Warning: Could not import tools module[/yellow]")
|
||||
|
||||
# If tools are provided, use Agent with tools first, then DeepResearchAgent
|
||||
if tools_list:
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# Create a research assistant agent with tools
|
||||
research_assistant = Agent(
|
||||
name="Research Assistant",
|
||||
role="Information Gatherer",
|
||||
goal="Gather relevant information using available tools",
|
||||
backstory="You are an expert at using tools to gather information for research.",
|
||||
tools=tools_list,
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Create task to gather initial information
|
||||
gather_task = Task(
|
||||
description=f"Use your tools to gather relevant information about: {query}",
|
||||
expected_output="A summary of information gathered from the tools",
|
||||
agent=research_assistant
|
||||
)
|
||||
|
||||
print("[cyan]Gathering information with tools...[/cyan]")
|
||||
agents = PraisonAIAgents(agents=[research_assistant], tasks=[gather_task], verbose=0)
|
||||
tool_results = agents.start()
|
||||
|
||||
# Enhance query with tool results
|
||||
enhanced_query = f"{query}\n\nAdditional context from tools:\n{tool_results}"
|
||||
print("[cyan]Tools context gathered, starting deep research...[/cyan]")
|
||||
|
||||
# Create DeepResearchAgent
|
||||
agent = DeepResearchAgent(model=model)
|
||||
result = agent.research(enhanced_query)
|
||||
else:
|
||||
# Create DeepResearchAgent (verbose=True is default for streaming output)
|
||||
agent = DeepResearchAgent(model=model)
|
||||
|
||||
# Execute the research
|
||||
result = agent.research(query)
|
||||
|
||||
print("\n[bold green]Research Complete![/bold green]")
|
||||
print("\n" + "="*60)
|
||||
print(result.report)
|
||||
print("="*60)
|
||||
|
||||
# Show citations if available
|
||||
if result.citations:
|
||||
print(f"\n[bold]Citations ({len(result.citations)}):[/bold]")
|
||||
for i, citation in enumerate(result.citations, 1):
|
||||
title = getattr(citation, 'title', 'Untitled')
|
||||
url = getattr(citation, 'url', '')
|
||||
print(f" {i}. {title}")
|
||||
if url:
|
||||
print(f" {url}")
|
||||
|
||||
# Save output to file if --save flag is set
|
||||
if save:
|
||||
import re
|
||||
# Get first 10 words from query for filename
|
||||
words = query.split()[:10]
|
||||
filename_base = ' '.join(words)
|
||||
# Sanitize filename: remove invalid characters
|
||||
filename_base = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '', filename_base)
|
||||
filename_base = filename_base.strip()[:100] # Limit length
|
||||
if not filename_base:
|
||||
filename_base = "research_output"
|
||||
|
||||
# Create output directory
|
||||
output_dir = os.path.join(os.getcwd(), "output", "research")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Build markdown content
|
||||
md_content = f"# {query}\n\n"
|
||||
md_content += result.report
|
||||
if result.citations:
|
||||
md_content += "\n\n## Citations\n\n"
|
||||
for i, citation in enumerate(result.citations, 1):
|
||||
title = getattr(citation, 'title', 'Untitled')
|
||||
url = getattr(citation, 'url', '')
|
||||
if url:
|
||||
md_content += f"{i}. [{title}]({url})\n"
|
||||
else:
|
||||
md_content += f"{i}. {title}\n"
|
||||
|
||||
# Save to file
|
||||
output_path = os.path.join(output_dir, f"{filename_base}.md")
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(md_content)
|
||||
|
||||
print(f"\n[bold green]Saved to:[/bold green] {output_path}")
|
||||
|
||||
return result.report
|
||||
|
||||
except ImportError as e:
|
||||
print(f"[red]ERROR: Failed to import DeepResearchAgent: {e}[/red]")
|
||||
print("Make sure praisonaiagents is installed: pip install praisonaiagents")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"[red]ERROR: Research failed: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.89 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.2 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "2.3.2"
|
||||
+11
-194
@@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.89"
|
||||
dynamic = ["version"]
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
|
||||
readme = "README.md"
|
||||
license = ""
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Mervin Praison" }
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.0.163",
|
||||
"praisonaiagents>=0.0.175",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
@@ -100,200 +100,17 @@ autogen-v4 = [
|
||||
"crewai"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.89"
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
|
||||
authors = ["Mervin Praison"]
|
||||
license = ""
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.urls]
|
||||
[project.urls]
|
||||
Homepage = "https://docs.praison.ai"
|
||||
Repository = "https://github.com/mervinpraison/PraisonAI"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["praisonai"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.13"
|
||||
rich = ">=13.7"
|
||||
markdown = ">=3.5"
|
||||
pyparsing = ">=3.0.0"
|
||||
praisonaiagents = ">=0.0.163"
|
||||
python-dotenv = ">=0.19.0"
|
||||
instructor = ">=1.3.3"
|
||||
PyYAML = ">=6.0"
|
||||
mcp = ">=1.6.0"
|
||||
pyautogen = {version = "==0.2.29", optional = true}
|
||||
autogen-agentchat = {version = ">=0.4.0", optional = true}
|
||||
autogen-ext = {version = ">=0.4.0", optional = true}
|
||||
autogen-core = {version = ">=0.4.0", optional = true}
|
||||
crewai = {version = ">=0.157.0", optional = true}
|
||||
praisonai-tools = {version = ">=0.0.22", optional = true}
|
||||
chainlit = {version = "==2.5.5", optional = true}
|
||||
gradio = {version = ">=4.26.0", optional = true}
|
||||
flask = {version = ">=3.0.0", optional = true}
|
||||
agentops = {version = ">=0.3.12", optional = true}
|
||||
langchain-google-genai = {version = ">=2.1.0", optional = true}
|
||||
langchain-anthropic = {version = ">=0.3.0", optional = true}
|
||||
langchain-openai = {version = ">=0.2.1,<0.3.0", optional = true}
|
||||
langchain-cohere = {version = ">=0.3.0,<0.4.0", optional = true}
|
||||
litellm = {version = ">=1.72.6", optional = true}
|
||||
aiosqlite= {version = ">=0.20.0", optional = true}
|
||||
greenlet = {version = ">=3.0.3", optional = true}
|
||||
tavily-python = {version = "==0.5.0", optional=true}
|
||||
crawl4ai = {version = ">=0.7.0", optional = true}
|
||||
websockets = {version = ">=12.0", optional = true}
|
||||
plotly = {version = ">=5.24.0", optional = true}
|
||||
yfinance = {version = ">=0.2.44", optional = true}
|
||||
duckduckgo_search = {version = ">=6.3.0", optional = true}
|
||||
twilio = {version = ">=7.0.0", optional = true}
|
||||
fastapi = {version = ">=0.95.0", optional = true}
|
||||
uvicorn = {version = ">=0.20.0", optional = true}
|
||||
flaml = {version = ">=2.3.1", extras = ["automl"], optional = true}
|
||||
pyngrok = {version = ">=1.4.0", optional = true}
|
||||
sqlalchemy = {version = ">=2.0.36", optional = true}
|
||||
playwright = {version = ">=1.47.0", optional = true}
|
||||
openai = {version = ">=1.54.0", optional = true}
|
||||
pydantic = {version = "<=2.10.1", optional = true}
|
||||
# unsloth = {version = ">=2024.11.7", extras = ["colab-new"], optional = true}
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "*"
|
||||
markdown-include = "*"
|
||||
mkdocs-material = "*"
|
||||
mkdocs-jupyter = "*"
|
||||
mkdocs-glightbox = "*"
|
||||
pymdown-extensions = "*"
|
||||
Pygments = "*"
|
||||
mkdocstrings = "*"
|
||||
mkdocs-apidoc = "*"
|
||||
mkdocstrings-python = "*"
|
||||
pdoc3 = "*"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "8.2.2"
|
||||
pytest-asyncio = ">=0.26.0"
|
||||
pre-commit = "3.7.1"
|
||||
unittest-xml-reporting = "3.2.0"
|
||||
xmlrunner = "*"
|
||||
unittest2 = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "8.2.2"
|
||||
pytest-asyncio = ">=0.26.0"
|
||||
pre-commit = "3.7.1"
|
||||
unittest-xml-reporting = "3.2.0"
|
||||
mkdocs = "*"
|
||||
markdown-include = "*"
|
||||
mkdocs-material = "*"
|
||||
mkdocs-jupyter = "*"
|
||||
mkdocs-glightbox = "*"
|
||||
pymdown-extensions = "*"
|
||||
Pygments = "*"
|
||||
mkdocstrings = "*"
|
||||
mkdocs-apidoc = "*"
|
||||
mkdocstrings-python = "*"
|
||||
pdoc3 = "*"
|
||||
xmlrunner = "*"
|
||||
unittest2 = "*"
|
||||
chainlit = "==2.5.5"
|
||||
gradio = ">=4.26.0"
|
||||
flask = ">=3.0.0"
|
||||
agentops = ">=0.3.12"
|
||||
langchain-google-genai = ">=2.1.0"
|
||||
langchain-anthropic = ">=0.3.0"
|
||||
langchain-openai = ">=0.2.1,<0.3.0"
|
||||
langchain-cohere = ">=0.3.0,<0.4.0"
|
||||
litellm = ">=1.72.6"
|
||||
aiosqlite = ">=0.20.0"
|
||||
greenlet = ">=3.0.3"
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
"poetry-core",
|
||||
"setuptools>=42",
|
||||
]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["setuptools>=64"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "praisonai.version.__version__"}
|
||||
|
||||
[tool.poetry.scripts]
|
||||
praisonai = "praisonai.__main__:main"
|
||||
setup-conda-env = "setup.setup_conda_env:main"
|
||||
praisonai-call = "praisonai.api.call:main"
|
||||
|
||||
[tool.poetry.extras]
|
||||
ui = [
|
||||
"chainlit",
|
||||
"aiosqlite",
|
||||
"greenlet",
|
||||
"sqlalchemy",
|
||||
"pydantic"
|
||||
]
|
||||
gradio = ["gradio"]
|
||||
api = ["flask"]
|
||||
agentops = ["agentops"]
|
||||
google = ["langchain-google-genai"]
|
||||
openai = ["langchain-openai"]
|
||||
anthropic = ["langchain-anthropic"]
|
||||
cohere = ["langchain-cohere"]
|
||||
chat = [
|
||||
"chainlit",
|
||||
"litellm",
|
||||
"aiosqlite",
|
||||
"greenlet",
|
||||
"tavily-python",
|
||||
"crawl4ai",
|
||||
"sqlalchemy",
|
||||
"playwright",
|
||||
"rich",
|
||||
"pydantic"
|
||||
]
|
||||
code = [
|
||||
"chainlit",
|
||||
"litellm",
|
||||
"aiosqlite",
|
||||
"greenlet",
|
||||
"tavily-python",
|
||||
"crawl4ai",
|
||||
"sqlalchemy",
|
||||
"playwright",
|
||||
"pydantic"
|
||||
]
|
||||
train = []
|
||||
realtime = [
|
||||
"chainlit",
|
||||
"litellm",
|
||||
"aiosqlite",
|
||||
"greenlet",
|
||||
"tavily-python",
|
||||
"crawl4ai",
|
||||
"websockets",
|
||||
"plotly",
|
||||
"yfinance",
|
||||
"duckduckgo_search",
|
||||
"sqlalchemy"
|
||||
]
|
||||
call = [
|
||||
"twilio",
|
||||
"fastapi",
|
||||
"uvicorn",
|
||||
"websockets",
|
||||
"flaml",
|
||||
"pyngrok",
|
||||
"rich",
|
||||
"openai",
|
||||
]
|
||||
crewai = ["crewai", "praisonai-tools"]
|
||||
autogen = ["pyautogen", "praisonai-tools", "crewai"]
|
||||
autogen-v4 = ["autogen-agentchat", "autogen-ext", "autogen-core", "praisonai-tools", "crewai"]
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
vcs = "git"
|
||||
style = "semver"
|
||||
|
||||
[tool.poetry.build]
|
||||
generate-setup-file = false
|
||||
script = "praisonai/setup/post_install.py"
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["praisonai*"]
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Combined bump version and release script for PraisonAI package.
|
||||
|
||||
Usage:
|
||||
python src/praisonai/scripts/bump_and_release.py 2.2.99
|
||||
|
||||
Or with optional praisonaiagents version:
|
||||
python src/praisonai/scripts/bump_and_release.py 2.2.99 --agents 0.0.169
|
||||
|
||||
This script:
|
||||
1. Bumps version in all required files
|
||||
2. Copies root README.md to package dir
|
||||
3. Runs uv lock & uv build
|
||||
4. Commits changes
|
||||
5. Creates git tag
|
||||
6. Pushes to GitHub
|
||||
7. Creates GitHub release (latest)
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get the project root directory (praisonai-package)."""
|
||||
return Path(__file__).parent.parent.parent.parent
|
||||
|
||||
|
||||
def get_praisonai_dir() -> Path:
|
||||
"""Get the praisonai package directory."""
|
||||
return get_project_root() / "src/praisonai"
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command and print it."""
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr)
|
||||
if check and result.returncode != 0:
|
||||
print(f"❌ Command failed with exit code {result.returncode}")
|
||||
sys.exit(1)
|
||||
return result
|
||||
|
||||
|
||||
def update_file(filepath: Path, patterns: list[tuple[str, str]], root: Path) -> bool:
|
||||
"""Update a file with the given regex patterns and replacements."""
|
||||
if not filepath.exists():
|
||||
print(f" ⚠️ File not found: {filepath}")
|
||||
return False
|
||||
|
||||
content = filepath.read_text()
|
||||
original = content
|
||||
|
||||
for pattern, replacement in patterns:
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
if content != original:
|
||||
filepath.write_text(content)
|
||||
print(f" ✅ Updated: {filepath.relative_to(root)}")
|
||||
return True
|
||||
else:
|
||||
print(f" ⏭️ No changes: {filepath.relative_to(root)}")
|
||||
return False
|
||||
|
||||
|
||||
def bump_version(new_version: str, agents_version: str | None = None):
|
||||
"""Bump version in all required files."""
|
||||
root = get_project_root()
|
||||
praisonai_dir = get_praisonai_dir()
|
||||
|
||||
print(f"\n🚀 Bumping PraisonAI version to {new_version}\n")
|
||||
|
||||
# 1. Update version.py (single source of truth)
|
||||
print("📦 Python Package:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai/version.py",
|
||||
[(r'__version__ = "[^"]+"', f'__version__ = "{new_version}"')],
|
||||
root
|
||||
)
|
||||
|
||||
# 2. Update deploy.py (Dockerfile template)
|
||||
print("\n🐳 Deploy Script:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai/deploy.py",
|
||||
[(r'praisonai==[0-9.]+', f'praisonai=={new_version}')],
|
||||
root
|
||||
)
|
||||
|
||||
# 3. Update Dockerfiles
|
||||
print("\n🐳 Dockerfiles:")
|
||||
dockerfiles = [
|
||||
"docker/Dockerfile",
|
||||
"docker/Dockerfile.chat",
|
||||
"docker/Dockerfile.dev",
|
||||
"docker/Dockerfile.ui",
|
||||
]
|
||||
for dockerfile in dockerfiles:
|
||||
update_file(
|
||||
root / dockerfile,
|
||||
[(r'"praisonai>=[0-9.]+"', f'"praisonai>={new_version}"')],
|
||||
root
|
||||
)
|
||||
|
||||
# 4. Update Homebrew formula
|
||||
print("\n🍺 Homebrew Formula:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai.rb",
|
||||
[(r'v[0-9]+\.[0-9]+\.[0-9]+', f'v{new_version}')],
|
||||
root
|
||||
)
|
||||
|
||||
# 5. Update praisonaiagents dependency if specified
|
||||
if agents_version:
|
||||
print(f"\n📦 Updating praisonaiagents dependency to {agents_version}:")
|
||||
update_file(
|
||||
praisonai_dir / "pyproject.toml",
|
||||
[(r'praisonaiagents>=[0-9.]+', f'praisonaiagents>={agents_version}')],
|
||||
root
|
||||
)
|
||||
|
||||
print("\n✨ Version bump complete!")
|
||||
|
||||
|
||||
def release(version: str):
|
||||
"""Run the release process."""
|
||||
root = get_project_root()
|
||||
praisonai_dir = get_praisonai_dir()
|
||||
tag = f"v{version}"
|
||||
|
||||
print(f"\n🚀 Releasing PraisonAI {tag}\n")
|
||||
|
||||
# 1. Copy root README.md to package dir for PyPI
|
||||
print("📄 Copying README.md...")
|
||||
root_readme = root / "README.md"
|
||||
pkg_readme = praisonai_dir / "README.md"
|
||||
if root_readme.exists():
|
||||
shutil.copy(root_readme, pkg_readme)
|
||||
print(f" ✅ Copied {root_readme} -> {pkg_readme}")
|
||||
|
||||
# 2. Clear uv cache and run uv lock
|
||||
print("\n🧹 Clearing uv cache...")
|
||||
run(["uv", "cache", "clean"], cwd=praisonai_dir)
|
||||
|
||||
print("\n📦 Running uv lock...")
|
||||
run(["uv", "lock"], cwd=praisonai_dir)
|
||||
|
||||
# 3. uv build
|
||||
print("\n🔨 Running uv build...")
|
||||
run(["uv", "build"], cwd=praisonai_dir)
|
||||
|
||||
# 4. Git add and commit
|
||||
print("\n📝 Committing changes...")
|
||||
run(["git", "add", "-A"], cwd=root)
|
||||
run(["git", "commit", "-m", f"Release {tag}"], cwd=root, check=False)
|
||||
|
||||
# 5. Create git tag
|
||||
print(f"\n🏷️ Creating tag {tag}...")
|
||||
run(["git", "tag", "-f", tag], cwd=root)
|
||||
|
||||
# 6. Push to GitHub
|
||||
print("\n⬆️ Pushing to GitHub...")
|
||||
run(["git", "push"], cwd=root)
|
||||
run(["git", "push", "--tags", "-f"], cwd=root)
|
||||
|
||||
# 7. Create GitHub release
|
||||
print(f"\n🎉 Creating GitHub release {tag}...")
|
||||
run([
|
||||
"gh", "release", "create", tag,
|
||||
"--title", f"PraisonAI {tag}",
|
||||
"--notes", f"Release {tag}",
|
||||
"--latest"
|
||||
], cwd=root)
|
||||
|
||||
print(f"\n✅ Released PraisonAI {tag}")
|
||||
print("\nNext step:")
|
||||
print(" cd src/praisonai && uv publish")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bump version and release PraisonAI package"
|
||||
)
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="New version number (e.g., 2.2.99)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--agents", "-a",
|
||||
help="Optional: Update praisonaiagents dependency version (e.g., 0.0.169)",
|
||||
default=None
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate version format
|
||||
if not re.match(r'^\d+\.\d+\.\d+$', args.version):
|
||||
print(f"❌ Invalid version format: {args.version}")
|
||||
print(" Expected format: X.Y.Z (e.g., 2.2.99)")
|
||||
sys.exit(1)
|
||||
|
||||
if args.agents and not re.match(r'^\d+\.\d+\.\d+$', args.agents):
|
||||
print(f"❌ Invalid agents version format: {args.agents}")
|
||||
print(" Expected format: X.Y.Z (e.g., 0.0.169)")
|
||||
sys.exit(1)
|
||||
|
||||
# Run bump version
|
||||
bump_version(args.version, args.agents)
|
||||
|
||||
# Run release
|
||||
release(args.version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Version bump script for PraisonAI package.
|
||||
|
||||
This script updates the version number in all required locations:
|
||||
- praisonai/version.py (single source of truth for Python package)
|
||||
- praisonai/deploy.py (Dockerfile template)
|
||||
- ../../docker/Dockerfile, Dockerfile.chat, Dockerfile.dev, Dockerfile.ui
|
||||
- praisonai.rb (Homebrew formula)
|
||||
|
||||
Usage:
|
||||
python src/praisonai/scripts/bump_version.py 2.2.96
|
||||
|
||||
Or with optional praisonaiagents version:
|
||||
python src/praisonai/scripts/bump_version.py 2.2.96 --agents 0.0.167
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get the project root directory (praisonai-package)."""
|
||||
# scripts/ -> src/praisonai/ -> src/ -> praisonai-package/
|
||||
return Path(__file__).parent.parent.parent.parent
|
||||
|
||||
|
||||
def update_file(filepath: Path, patterns: list[tuple[str, str]]) -> bool:
|
||||
"""Update a file with the given regex patterns and replacements."""
|
||||
if not filepath.exists():
|
||||
print(f" ⚠️ File not found: {filepath}")
|
||||
return False
|
||||
|
||||
content = filepath.read_text()
|
||||
original = content
|
||||
|
||||
for pattern, replacement in patterns:
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
if content != original:
|
||||
filepath.write_text(content)
|
||||
print(f" ✅ Updated: {filepath.relative_to(get_project_root())}")
|
||||
return True
|
||||
else:
|
||||
print(f" ⏭️ No changes: {filepath.relative_to(get_project_root())}")
|
||||
return False
|
||||
|
||||
|
||||
def bump_version(new_version: str, agents_version: str | None = None):
|
||||
"""Bump version in all required files."""
|
||||
root = get_project_root()
|
||||
|
||||
print(f"\n🚀 Bumping PraisonAI version to {new_version}\n")
|
||||
|
||||
praisonai_dir = root / "src/praisonai"
|
||||
|
||||
# 1. Update version.py (single source of truth)
|
||||
print("📦 Python Package:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai/version.py",
|
||||
[(r'__version__ = "[^"]+"', f'__version__ = "{new_version}"')]
|
||||
)
|
||||
|
||||
# 2. Update deploy.py (Dockerfile template)
|
||||
print("\n🐳 Deploy Script:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai/deploy.py",
|
||||
[(r'praisonai==[0-9.]+', f'praisonai=={new_version}')]
|
||||
)
|
||||
|
||||
# 3. Update Dockerfiles
|
||||
print("\n🐳 Dockerfiles:")
|
||||
dockerfiles = [
|
||||
"docker/Dockerfile",
|
||||
"docker/Dockerfile.chat",
|
||||
"docker/Dockerfile.dev",
|
||||
"docker/Dockerfile.ui",
|
||||
]
|
||||
for dockerfile in dockerfiles:
|
||||
update_file(
|
||||
root / dockerfile,
|
||||
[(r'"praisonai>=[0-9.]+"', f'"praisonai>={new_version}"')]
|
||||
)
|
||||
|
||||
# 4. Update Homebrew formula
|
||||
print("\n🍺 Homebrew Formula:")
|
||||
update_file(
|
||||
praisonai_dir / "praisonai.rb",
|
||||
[(r'v[0-9]+\.[0-9]+\.[0-9]+', f'v{new_version}')]
|
||||
)
|
||||
|
||||
# 5. Update praisonaiagents dependency if specified
|
||||
if agents_version:
|
||||
print(f"\n📦 Updating praisonaiagents dependency to {agents_version}:")
|
||||
update_file(
|
||||
praisonai_dir / "pyproject.toml",
|
||||
[(r'praisonaiagents>=[0-9.]+', f'praisonaiagents>={agents_version}')]
|
||||
)
|
||||
|
||||
print("\n✨ Version bump complete!")
|
||||
print("\nNext steps:")
|
||||
print(" 1. Run: cd src/praisonai && uv lock")
|
||||
print(" 2. Run: cd src/praisonai && uv build")
|
||||
print(f" 3. Commit changes: git add -A && git commit -m 'Bump version to {new_version}'")
|
||||
print(f" 4. Tag release: git tag v{new_version}")
|
||||
print(" 5. Publish: cd src/praisonai && uv publish")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bump PraisonAI version in all required files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="New version number (e.g., 2.2.96)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--agents", "-a",
|
||||
help="Optional: Update praisonaiagents dependency version (e.g., 0.0.167)",
|
||||
default=None
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate version format
|
||||
if not re.match(r'^\d+\.\d+\.\d+$', args.version):
|
||||
print(f"❌ Invalid version format: {args.version}")
|
||||
print(" Expected format: X.Y.Z (e.g., 2.2.96)")
|
||||
sys.exit(1)
|
||||
|
||||
if args.agents and not re.match(r'^\d+\.\d+\.\d+$', args.agents):
|
||||
print(f"❌ Invalid agents version format: {args.agents}")
|
||||
print(" Expected format: X.Y.Z (e.g., 0.0.167)")
|
||||
sys.exit(1)
|
||||
|
||||
bump_version(args.version, args.agents)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Release script for PraisonAI package.
|
||||
|
||||
Usage:
|
||||
python src/praisonai/scripts/release.py
|
||||
|
||||
This script:
|
||||
1. Reads version from praisonai/version.py
|
||||
2. Runs uv lock
|
||||
3. Runs uv build
|
||||
4. Commits changes
|
||||
5. Creates git tag
|
||||
6. Pushes to GitHub
|
||||
7. Creates GitHub release (latest)
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get the project root directory (praisonai-package)."""
|
||||
return Path(__file__).parent.parent.parent.parent
|
||||
|
||||
|
||||
def get_praisonai_dir() -> Path:
|
||||
"""Get the praisonai package directory."""
|
||||
return get_project_root() / "src/praisonai"
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""Read version from version.py."""
|
||||
version_file = get_praisonai_dir() / "praisonai/version.py"
|
||||
content = version_file.read_text()
|
||||
for line in content.splitlines():
|
||||
if line.startswith("__version__"):
|
||||
return line.split('"')[1]
|
||||
raise ValueError("Could not find version in version.py")
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command and print it."""
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr)
|
||||
if check and result.returncode != 0:
|
||||
print(f"❌ Command failed with exit code {result.returncode}")
|
||||
sys.exit(1)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
root = get_project_root()
|
||||
praisonai_dir = get_praisonai_dir()
|
||||
version = get_version()
|
||||
tag = f"v{version}"
|
||||
|
||||
print(f"\n🚀 Releasing PraisonAI {tag}\n")
|
||||
|
||||
# 1. Copy root README.md to package dir for PyPI
|
||||
print("📄 Copying README.md...")
|
||||
root_readme = root / "README.md"
|
||||
pkg_readme = praisonai_dir / "README.md"
|
||||
if root_readme.exists():
|
||||
import shutil
|
||||
shutil.copy(root_readme, pkg_readme)
|
||||
print(f" ✅ Copied {root_readme} -> {pkg_readme}")
|
||||
|
||||
# 2. Clear uv cache and run uv lock
|
||||
print("\n🧹 Clearing uv cache...")
|
||||
run(["uv", "cache", "clean"], cwd=praisonai_dir)
|
||||
|
||||
print("\n📦 Running uv lock...")
|
||||
run(["uv", "lock"], cwd=praisonai_dir)
|
||||
|
||||
# 3. uv build
|
||||
print("\n🔨 Running uv build...")
|
||||
run(["uv", "build"], cwd=praisonai_dir)
|
||||
|
||||
# 3. Git add and commit
|
||||
print("\n📝 Committing changes...")
|
||||
run(["git", "add", "-A"], cwd=root)
|
||||
run(["git", "commit", "-m", f"Release {tag}"], cwd=root, check=False)
|
||||
|
||||
# 4. Create git tag
|
||||
print(f"\n🏷️ Creating tag {tag}...")
|
||||
run(["git", "tag", "-f", tag], cwd=root)
|
||||
|
||||
# 5. Push to GitHub
|
||||
print("\n⬆️ Pushing to GitHub...")
|
||||
run(["git", "push"], cwd=root)
|
||||
run(["git", "push", "--tags", "-f"], cwd=root)
|
||||
|
||||
# 6. Create GitHub release
|
||||
print(f"\n🎉 Creating GitHub release {tag}...")
|
||||
run([
|
||||
"gh", "release", "create", tag,
|
||||
"--title", f"PraisonAI {tag}",
|
||||
"--notes", f"Release {tag}",
|
||||
"--latest"
|
||||
], cwd=root)
|
||||
|
||||
print(f"\n✅ Released PraisonAI {tag}")
|
||||
print("\nNext step:")
|
||||
print(" cd src/praisonai && uv publish")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -75,7 +75,8 @@ roles:
|
||||
def test_autogen_environment_check(self):
|
||||
"""Verify AutoGen environment is properly configured"""
|
||||
# Check API key is available
|
||||
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY required for real tests"
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
pytest.skip("OPENAI_API_KEY required for real tests")
|
||||
|
||||
# Check AutoGen can be imported
|
||||
try:
|
||||
|
||||
@@ -72,10 +72,10 @@ roles:
|
||||
except Exception as e:
|
||||
pytest.fail(f"CrewAI real test failed: {e}")
|
||||
|
||||
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set")
|
||||
def test_crewai_environment_check(self):
|
||||
"""Verify CrewAI environment is properly configured"""
|
||||
# Check API key is available
|
||||
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY required for real tests"
|
||||
# Check API key is available (already checked by skipif)
|
||||
|
||||
# Check CrewAI can be imported
|
||||
try:
|
||||
|
||||
@@ -59,8 +59,11 @@ class TestPraisonAICommandLine(unittest.TestCase):
|
||||
# In CI, it should be set by the workflow
|
||||
print("Warning: OPENAI_API_KEY not found in CLI test environment. API calls might fail if not mocked.")
|
||||
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, env=env)
|
||||
return result.stdout + result.stderr
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, env=env, timeout=120)
|
||||
return result.stdout + result.stderr
|
||||
except subprocess.TimeoutExpired:
|
||||
return "TIMEOUT: Command exceeded 120 seconds"
|
||||
|
||||
@pytest.mark.real
|
||||
def test_praisonai_command(self):
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Tests for QueryRewriterAgent CLI integration
|
||||
|
||||
Run with: python -m pytest tests/unit/test_query_rewriter_cli.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import argparse
|
||||
|
||||
|
||||
class TestQueryRewriterCLIArgs:
|
||||
"""Test CLI argument parsing for query rewriter."""
|
||||
|
||||
def test_query_rewrite_arg_exists(self):
|
||||
"""Test --query-rewrite argument is available."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Create instance and check args can be parsed
|
||||
with patch('sys.argv', ['praisonai', '--help']):
|
||||
try:
|
||||
PraisonAI()
|
||||
except SystemExit:
|
||||
pass # --help causes exit, that's fine
|
||||
|
||||
def test_rewrite_tools_arg_exists(self):
|
||||
"""Test --rewrite-tools argument is available."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
with patch('sys.argv', ['praisonai', '--help']):
|
||||
try:
|
||||
PraisonAI()
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
|
||||
class TestRewriteQueryMethod:
|
||||
"""Test the _rewrite_query helper method."""
|
||||
|
||||
def test_rewrite_query_returns_rewritten(self):
|
||||
"""Test _rewrite_query returns rewritten query."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Setup mock
|
||||
mock_result = MagicMock()
|
||||
mock_result.primary_query = "What are the current trends in artificial intelligence?"
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.rewrite.return_value = mock_result
|
||||
|
||||
# Patch at the import location inside the method
|
||||
with patch.dict('sys.modules', {'praisonaiagents': MagicMock()}):
|
||||
import sys
|
||||
sys.modules['praisonaiagents'].QueryRewriterAgent = MagicMock(return_value=mock_agent)
|
||||
sys.modules['praisonaiagents'].RewriteStrategy = MagicMock()
|
||||
|
||||
praison = PraisonAI()
|
||||
result = praison._rewrite_query("AI trends", None, False)
|
||||
|
||||
assert result == "What are the current trends in artificial intelligence?"
|
||||
|
||||
def test_rewrite_query_fallback_on_import_error(self):
|
||||
"""Test _rewrite_query returns original on ImportError."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Mock the import to raise ImportError
|
||||
praison = PraisonAI()
|
||||
|
||||
with patch.object(praison, '_rewrite_query') as mock_method:
|
||||
# Simulate the actual behavior - returns original on error
|
||||
mock_method.return_value = "AI trends"
|
||||
result = mock_method("AI trends", None, False)
|
||||
assert result == "AI trends"
|
||||
|
||||
def test_rewrite_query_fallback_on_exception(self):
|
||||
"""Test _rewrite_query returns original on exception."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Patch to raise exception
|
||||
with patch.dict('sys.modules', {'praisonaiagents': MagicMock()}):
|
||||
import sys
|
||||
sys.modules['praisonaiagents'].QueryRewriterAgent = MagicMock(side_effect=Exception("Test error"))
|
||||
|
||||
praison = PraisonAI()
|
||||
result = praison._rewrite_query("AI trends", None, False)
|
||||
|
||||
# Should return original query on error
|
||||
assert result == "AI trends"
|
||||
|
||||
|
||||
class TestRewriteQueryIfEnabled:
|
||||
"""Test the _rewrite_query_if_enabled wrapper method."""
|
||||
|
||||
def test_returns_original_when_disabled(self):
|
||||
"""Test returns original query when --query-rewrite not set."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
praison.args = argparse.Namespace(query_rewrite=False)
|
||||
|
||||
result = praison._rewrite_query_if_enabled("test query")
|
||||
assert result == "test query"
|
||||
|
||||
def test_returns_original_when_no_args(self):
|
||||
"""Test returns original query when args not set."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
# Don't set args at all
|
||||
if hasattr(praison, 'args'):
|
||||
delattr(praison, 'args')
|
||||
|
||||
result = praison._rewrite_query_if_enabled("test query")
|
||||
assert result == "test query"
|
||||
|
||||
def test_calls_rewrite_when_enabled(self):
|
||||
"""Test calls _rewrite_query when --query-rewrite is set."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
praison.args = argparse.Namespace(
|
||||
query_rewrite=True,
|
||||
rewrite_tools=None,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
with patch.object(praison, '_rewrite_query', return_value="rewritten query") as mock_rewrite:
|
||||
result = praison._rewrite_query_if_enabled("original query")
|
||||
|
||||
mock_rewrite.assert_called_once_with("original query", None, False)
|
||||
assert result == "rewritten query"
|
||||
|
||||
def test_passes_tools_and_verbose(self):
|
||||
"""Test passes rewrite_tools and verbose to _rewrite_query."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
praison.args = argparse.Namespace(
|
||||
query_rewrite=True,
|
||||
rewrite_tools="internet_search",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
with patch.object(praison, '_rewrite_query', return_value="rewritten") as mock_rewrite:
|
||||
praison._rewrite_query_if_enabled("query")
|
||||
|
||||
mock_rewrite.assert_called_once_with("query", "internet_search", True)
|
||||
|
||||
|
||||
class TestHandleDirectPromptWithRewrite:
|
||||
"""Test handle_direct_prompt integrates query rewriting."""
|
||||
|
||||
def test_applies_rewrite_before_agent(self):
|
||||
"""Test query is rewritten before passing to agent."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
praison.args = argparse.Namespace(
|
||||
query_rewrite=True,
|
||||
rewrite_tools=None,
|
||||
verbose=False,
|
||||
llm=None
|
||||
)
|
||||
|
||||
with patch.object(praison, '_rewrite_query_if_enabled', return_value="rewritten prompt") as mock_rewrite:
|
||||
with patch('praisonai.cli.PRAISONAI_AVAILABLE', True):
|
||||
with patch('praisonai.cli.PraisonAgent') as mock_agent_class:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.start.return_value = "result"
|
||||
mock_agent_class.return_value = mock_agent
|
||||
|
||||
praison.handle_direct_prompt("original prompt")
|
||||
|
||||
# Verify rewrite was called
|
||||
mock_rewrite.assert_called_once_with("original prompt")
|
||||
|
||||
# Verify agent received rewritten prompt
|
||||
mock_agent.start.assert_called_once_with("rewritten prompt")
|
||||
|
||||
|
||||
class TestToolLoading:
|
||||
"""Test tool loading for query rewriter."""
|
||||
|
||||
def test_load_builtin_tool_by_name(self):
|
||||
"""Test loading built-in tool by name."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Setup mocks
|
||||
mock_result = MagicMock()
|
||||
mock_result.primary_query = "rewritten"
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.rewrite.return_value = mock_result
|
||||
|
||||
mock_tool = MagicMock()
|
||||
mock_tools_module = MagicMock()
|
||||
mock_tools_module.TOOL_MAPPINGS = {'internet_search': 'praisonaiagents.tools'}
|
||||
mock_tools_module.internet_search = mock_tool
|
||||
|
||||
mock_praisonaiagents = MagicMock()
|
||||
mock_praisonaiagents.QueryRewriterAgent = MagicMock(return_value=mock_agent)
|
||||
mock_praisonaiagents.RewriteStrategy = MagicMock()
|
||||
mock_praisonaiagents.tools = mock_tools_module
|
||||
|
||||
with patch.dict('sys.modules', {
|
||||
'praisonaiagents': mock_praisonaiagents,
|
||||
'praisonaiagents.tools': mock_tools_module
|
||||
}):
|
||||
praison = PraisonAI()
|
||||
result = praison._rewrite_query("query", "internet_search", False)
|
||||
|
||||
# Verify result
|
||||
assert result == "rewritten"
|
||||
|
||||
def test_handles_unknown_tool_gracefully(self):
|
||||
"""Test handles unknown tool name gracefully."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Setup mocks
|
||||
mock_result = MagicMock()
|
||||
mock_result.primary_query = "rewritten"
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.rewrite.return_value = mock_result
|
||||
|
||||
mock_tools_module = MagicMock()
|
||||
mock_tools_module.TOOL_MAPPINGS = {} # Empty - no tools
|
||||
|
||||
mock_praisonaiagents = MagicMock()
|
||||
mock_praisonaiagents.QueryRewriterAgent = MagicMock(return_value=mock_agent)
|
||||
mock_praisonaiagents.RewriteStrategy = MagicMock()
|
||||
mock_praisonaiagents.tools = mock_tools_module
|
||||
|
||||
with patch.dict('sys.modules', {
|
||||
'praisonaiagents': mock_praisonaiagents,
|
||||
'praisonaiagents.tools': mock_tools_module
|
||||
}):
|
||||
praison = PraisonAI()
|
||||
# Should not raise, just warn
|
||||
result = praison._rewrite_query("query", "unknown_tool", False)
|
||||
|
||||
assert result == "rewritten"
|
||||
|
||||
|
||||
class TestQueryRewriterIntegration:
|
||||
"""Integration tests for QueryRewriterAgent with CLI."""
|
||||
|
||||
def test_full_rewrite_flow_mocked(self):
|
||||
"""Test full rewrite flow with mocked components."""
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
# Setup mocks
|
||||
mock_result = MagicMock()
|
||||
mock_result.primary_query = "What are the latest developments in AI technology?"
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.rewrite.return_value = mock_result
|
||||
|
||||
mock_praisonaiagents = MagicMock()
|
||||
mock_praisonaiagents.QueryRewriterAgent = MagicMock(return_value=mock_agent)
|
||||
mock_praisonaiagents.RewriteStrategy = MagicMock()
|
||||
|
||||
with patch.dict('sys.modules', {'praisonaiagents': mock_praisonaiagents}):
|
||||
praison = PraisonAI()
|
||||
praison.args = argparse.Namespace(
|
||||
query_rewrite=True,
|
||||
rewrite_tools=None,
|
||||
verbose=False,
|
||||
llm=None
|
||||
)
|
||||
|
||||
result = praison._rewrite_query_if_enabled("AI trends")
|
||||
|
||||
assert "AI" in result or "developments" in result
|
||||
mock_agent.rewrite.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Generated
+4
-5
@@ -4044,7 +4044,6 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.89"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "instructor" },
|
||||
@@ -4198,7 +4197,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.163" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.175" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4253,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.163"
|
||||
version = "0.0.175"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4262,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/6d/68553b1c16f82ab9d0f4466227df65990103d89428aa49963253b85ed877/praisonaiagents-0.0.163.tar.gz", hash = "sha256:f1dded526613e353d10ea0ee7158d2e544efa06aae289d3b84efe7f03275883b", size = 298039, upload-time = "2025-12-13T01:13:18.845Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/3a/a3f78a53c5ffdab876b59c7f7ea86ed815d641dff45465c70b6fd8ab1a8f/praisonaiagents-0.0.175.tar.gz", hash = "sha256:5f2bdeae4988a34947aa231c7a3de76bf50d60d2dfd43e214769d188886d6362", size = 332558, upload-time = "2025-12-15T12:43:06.491Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/21/2e2f9719dcf31c99474c1b61a62be78c79cdfde0abd40b5d100419515279/praisonaiagents-0.0.163-py3-none-any.whl", hash = "sha256:518c46a618b609af4718c2c77779c75494bc4751f97ebb52864c490483cc2240", size = 315110, upload-time = "2025-12-13T01:13:17.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/c9/93cf31b2c1011ee205efc75080a4d7ab9ed68e53e033d51a861112545428/praisonaiagents-0.0.175-py3-none-any.whl", hash = "sha256:1bdb672efefd562c47be481008551894e57083e52005227650f97999f88e7ff6", size = 339912, upload-time = "2025-12-15T12:43:05.064Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user