mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02449dfdb | ||
|
|
c4c0c1c146 | ||
|
|
aa2d7e4c31 | ||
|
|
b339c5b3aa | ||
|
|
e535f98f4e | ||
|
|
bc349ad09f | ||
|
|
5b8da1180e | ||
|
|
8f6d8f0a48 | ||
|
|
7f3810ed98 | ||
|
|
c84a59ab4d | ||
|
|
c6a6bfa827 | ||
|
|
a604cd9584 | ||
|
|
cb043da19c | ||
|
|
7b41444aad | ||
|
|
7c8b77063f | ||
|
|
59eb8d6356 | ||
|
|
89bddc31c6 | ||
|
|
e6e908298b | ||
|
|
2b3509c1d1 | ||
|
|
6e8029055a | ||
|
|
d2c9e8825a | ||
|
|
cd9816e218 | ||
|
|
d400149075 | ||
|
|
73b08e6b76 | ||
|
|
19c3a1f6a7 | ||
|
|
71a7968318 | ||
|
|
1b258229c1 | ||
|
|
d0e09f51f3 | ||
|
|
5adc862ead | ||
|
|
856db54334 | ||
|
|
f94e5bb3d6 | ||
|
|
1cece603ec | ||
|
|
9e2bbb9bbb | ||
|
|
35b4cff7f7 | ||
|
|
a094d1ff30 | ||
|
|
826ff56fc5 | ||
|
|
4bd0195d5d | ||
|
|
75c4d8432c | ||
|
|
32537a227e | ||
|
|
e252f86ac9 |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
name: Auto Issue Comment
|
||||
name: Auto Issue Comment with PR Creation
|
||||
|
||||
on:
|
||||
issues:
|
||||
@@ -9,6 +9,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Add automated comment
|
||||
uses: actions/github-script@v7
|
||||
@@ -19,5 +22,5 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review this issue and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes.'
|
||||
})
|
||||
body: '@claude review this issue and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. **Please create a PR using gh tool with your changes**.'
|
||||
})
|
||||
@@ -36,6 +36,25 @@ jobs:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trigger_phrase: "@claude"
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
Bash(python:*)
|
||||
Bash(pip:*)
|
||||
Bash(conda:*)
|
||||
Bash(pytest:*)
|
||||
Bash(gh:*)
|
||||
Bash(python -m pytest:*)
|
||||
Bash(python -m pip:*)
|
||||
Bash(poetry:*)
|
||||
View
|
||||
GlobTool
|
||||
GrepTool
|
||||
BatchTool
|
||||
Edit
|
||||
Replace
|
||||
mcp__github__get_issue
|
||||
mcp__github__get_issue_comments
|
||||
mcp__github__update_issue
|
||||
timeout_minutes: 30
|
||||
|
||||
post-review-comment:
|
||||
@@ -111,23 +130,32 @@ jobs:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
let issueNumber;
|
||||
let isPR = false;
|
||||
|
||||
// Determine issue/PR number based on event type
|
||||
// Determine issue/PR number and type based on event type
|
||||
if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') {
|
||||
issueNumber = context.payload.pull_request?.number || context.issue.number;
|
||||
isPR = true;
|
||||
} else if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) {
|
||||
// This is a comment on a PR (not an issue)
|
||||
issueNumber = context.payload.issue.number;
|
||||
isPR = true;
|
||||
} else {
|
||||
// This is a regular issue
|
||||
// This is a regular issue - don't post review comment
|
||||
issueNumber = context.issue.number;
|
||||
isPR = false;
|
||||
}
|
||||
|
||||
console.log(`Posting review comment on issue/PR #${issueNumber}`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review if the changes made are as per the requirements'
|
||||
});
|
||||
// Only post review comment on PRs, not regular issues
|
||||
if (isPR) {
|
||||
console.log(`Posting review comment on PR #${issueNumber}`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review if the changes made in the above branch are as per the requirements and make changes to the above branch if issues still exist.'
|
||||
});
|
||||
} else {
|
||||
console.log(`Skipping review comment for regular issue #${issueNumber}`);
|
||||
}
|
||||
+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.57" \
|
||||
"praisonai>=2.2.60" \
|
||||
"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.57" \
|
||||
"praisonai>=2.2.60" \
|
||||
"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.57" \
|
||||
"praisonai>=2.2.60" \
|
||||
"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.57" \
|
||||
"praisonai>=2.2.60" \
|
||||
"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.57`
|
||||
- PraisonAI: `>=2.2.60`
|
||||
- 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.57" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.60" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Gemini Internal Tools Examples
|
||||
|
||||
This directory contains examples demonstrating how to use Google Gemini's built-in internal tools through PraisonAI. These tools are natively supported by Gemini models and provide powerful capabilities without requiring external tool implementations.
|
||||
|
||||
## Available Internal Tools
|
||||
|
||||
### 1. Google Search (`googleSearch`)
|
||||
- **Purpose**: Real-time web search with automatic result grounding
|
||||
- **Use Cases**: Current events, latest information, fact-checking
|
||||
- **Example**: `gemini-google-search.py`
|
||||
|
||||
### 2. URL Context (`urlContext`)
|
||||
- **Purpose**: Fetch and analyze content from specific URLs
|
||||
- **Use Cases**: Document analysis, web content summarization, research
|
||||
- **Example**: `gemini-url-context.py`
|
||||
|
||||
### 3. Code Execution (`codeExecution`)
|
||||
- **Purpose**: Execute Python code snippets within the conversation
|
||||
- **Use Cases**: Calculations, data analysis, code validation
|
||||
- **Example**: `gemini-code-execution.py`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Gemini API Key**: Set your Google AI Studio API key
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
2. **Compatible Model**: Use Gemini models that support internal tools:
|
||||
- `gemini/gemini-2.0-flash` (recommended)
|
||||
- `gemini/gemini-2.0-flash-thinking-exp`
|
||||
- Other Gemini 2.0+ models
|
||||
|
||||
3. **PraisonAI Installation**: Ensure you have the latest version
|
||||
```bash
|
||||
pip install "praisonaiagents[llm]"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Individual Tool Examples
|
||||
|
||||
1. **Google Search Example** (`gemini-google-search.py`)
|
||||
```python
|
||||
agent = Agent(
|
||||
instructions="Research assistant with web search",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[{"googleSearch": {}}]
|
||||
)
|
||||
|
||||
response = agent.start("What's the latest news about AI?")
|
||||
```
|
||||
|
||||
2. **URL Context Example** (`gemini-url-context.py`)
|
||||
```python
|
||||
agent = Agent(
|
||||
instructions="Content analyzer",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[{"urlContext": {}}]
|
||||
)
|
||||
|
||||
response = agent.start("Summarize this: https://example.com/article")
|
||||
```
|
||||
|
||||
3. **Code Execution Example** (`gemini-code-execution.py`)
|
||||
```python
|
||||
agent = Agent(
|
||||
instructions="Data analyst with code execution",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[{"codeExecution": {}}]
|
||||
)
|
||||
|
||||
response = agent.start("Calculate compound interest for $10,000 at 5% for 10 years")
|
||||
```
|
||||
|
||||
### Combined Tools Example
|
||||
|
||||
**All Tools Together** (`gemini-internal-tools-combined.py`)
|
||||
```python
|
||||
agent = Agent(
|
||||
instructions="Advanced research assistant",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[
|
||||
{"googleSearch": {}},
|
||||
{"urlContext": {}},
|
||||
{"codeExecution": {}}
|
||||
]
|
||||
)
|
||||
|
||||
# Now the agent can search, analyze URLs, and execute code as needed
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### PraisonAI Integration
|
||||
|
||||
PraisonAI's tool system is designed to pass tools directly to LiteLLM without modification:
|
||||
|
||||
1. **Tool Definition**: Define tools using the special internal tool syntax
|
||||
```python
|
||||
tools=[{"googleSearch": {}}] # Special format for internal tools
|
||||
```
|
||||
|
||||
2. **Pass-Through**: PraisonAI passes these tools directly to LiteLLM
|
||||
3. **Execution**: LiteLLM sends them to Gemini as internal tool configurations
|
||||
4. **Results**: Gemini executes the tools natively and returns integrated responses
|
||||
|
||||
### Mixing with External Tools
|
||||
|
||||
You can combine internal tools with custom external tools:
|
||||
|
||||
```python
|
||||
def custom_calculator(expression: str) -> str:
|
||||
"""Custom calculator function - safely evaluates basic math expressions"""
|
||||
import ast
|
||||
import operator
|
||||
|
||||
# Define safe operations
|
||||
ops = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Pow: operator.pow,
|
||||
ast.USub: operator.neg,
|
||||
}
|
||||
|
||||
def eval_expr(node):
|
||||
if isinstance(node, ast.Constant): # Numbers
|
||||
return node.value
|
||||
elif isinstance(node, ast.BinOp): # Binary operations
|
||||
return ops[type(node.op)](eval_expr(node.left), eval_expr(node.right))
|
||||
elif isinstance(node, ast.UnaryOp): # Unary operations
|
||||
return ops[type(node.op)](eval_expr(node.operand))
|
||||
else:
|
||||
raise TypeError(f"Unsupported operation: {type(node)}")
|
||||
|
||||
try:
|
||||
return str(eval_expr(ast.parse(expression, mode='eval').body))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
agent = Agent(
|
||||
tools=[
|
||||
{"googleSearch": {}}, # Internal tool
|
||||
{"codeExecution": {}}, # Internal tool
|
||||
custom_calculator # External tool
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Running the Examples
|
||||
|
||||
1. **Set API Key**:
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
2. **Run Individual Examples**:
|
||||
```bash
|
||||
python gemini-google-search.py
|
||||
python gemini-url-context.py
|
||||
python gemini-code-execution.py
|
||||
```
|
||||
|
||||
3. **Run Combined Example**:
|
||||
```bash
|
||||
python gemini-internal-tools-combined.py
|
||||
```
|
||||
|
||||
## Benefits Over External Tools
|
||||
|
||||
### Advantages of Internal Tools
|
||||
|
||||
1. **Native Integration**: No need to implement or maintain external tool code
|
||||
2. **Optimized Performance**: Tools are executed within Gemini's environment
|
||||
3. **Automatic Grounding**: Search results are automatically integrated into responses
|
||||
4. **No Rate Limits**: No separate API calls or rate limiting concerns
|
||||
5. **Security**: Code execution is sandboxed within Gemini's environment
|
||||
|
||||
### When to Use External Tools
|
||||
|
||||
- When you need specific custom logic
|
||||
- When you require integration with private APIs
|
||||
- When you need persistent data storage
|
||||
- When you want full control over tool behavior
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **API Key Error**: Ensure `GEMINI_API_KEY` is set correctly
|
||||
2. **Model Not Supported**: Use Gemini 2.0+ models for internal tools
|
||||
3. **Tool Not Available**: Some tools may have regional or quota restrictions
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose logging to see tool usage:
|
||||
```python
|
||||
agent = Agent(
|
||||
# ... config
|
||||
verbose=True # Shows detailed tool usage
|
||||
)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Gemini API: Google Search Grounding](https://ai.google.dev/gemini-api/docs/google-search)
|
||||
- [Gemini API: URL Context](https://ai.google.dev/gemini-api/docs/url-context)
|
||||
- [Gemini API: Code Execution](https://ai.google.dev/gemini-api/docs/code-execution)
|
||||
- [LiteLLM Gemini Provider Documentation](https://docs.litellm.ai/docs/providers/gemini)
|
||||
|
||||
## Contributing
|
||||
|
||||
If you create additional examples or find improvements, please contribute back to the PraisonAI project!
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Gemini Code Execution Tool Example
|
||||
|
||||
This example demonstrates how to use Gemini's built-in Code Execution functionality
|
||||
through PraisonAI. The Code Execution tool allows the model to run Python code
|
||||
snippets directly within the conversation for dynamic computation and validation.
|
||||
|
||||
Prerequisites:
|
||||
- Set GEMINI_API_KEY environment variable
|
||||
- Use a Gemini model that supports internal tools (gemini-2.0-flash, etc.)
|
||||
|
||||
Features:
|
||||
- Direct Python code execution within the model
|
||||
- Real-time calculations and data processing
|
||||
- Code validation and testing
|
||||
- Mathematical computations and visualizations
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Ensure you have your Gemini API key set
|
||||
# import os; os.environ["GEMINI_API_KEY"] = "your-api-key-here"
|
||||
|
||||
def main():
|
||||
# Create agent with Code Execution internal tool
|
||||
agent = Agent(
|
||||
instructions="""You are a data analyst and programmer that can execute Python code.
|
||||
Use the Code Execution tool to perform calculations, analyze data, create visualizations,
|
||||
and validate logic. Always show your work by executing relevant code snippets.""",
|
||||
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
|
||||
# Enable Code Execution internal tool
|
||||
tools=[{"codeExecution": {}}],
|
||||
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Example tasks that benefit from code execution
|
||||
tasks = [
|
||||
"Calculate the compound interest on $10,000 invested at 5% annual rate for 10 years, compounded monthly. Show the calculation step by step.",
|
||||
|
||||
"Generate the first 20 Fibonacci numbers and find their sum. Also calculate the ratio of consecutive Fibonacci numbers.",
|
||||
|
||||
"Create a simple linear regression model for the data points: (1,2), (2,4), (3,6), (4,8), (5,10). Calculate the R-squared value.",
|
||||
|
||||
"Solve the quadratic equation 2x² + 5x - 3 = 0 using the quadratic formula. Verify the solutions by substituting back.",
|
||||
|
||||
"Calculate the statistical summary (mean, median, mode, standard deviation) for this dataset: [12, 15, 18, 12, 20, 25, 18, 12, 22, 30, 15, 18]",
|
||||
|
||||
"Write a function to check if a number is prime, then find all prime numbers between 1 and 100.",
|
||||
|
||||
"Calculate the area under the curve y = x² from x = 0 to x = 5 using numerical integration (trapezoidal rule)."
|
||||
]
|
||||
|
||||
print("=== Gemini Code Execution Tool Demo ===\n")
|
||||
|
||||
for i, task in enumerate(tasks, 1):
|
||||
print(f"Task {i}: {task}")
|
||||
print("-" * 80)
|
||||
|
||||
try:
|
||||
response = agent.start(task)
|
||||
print(f"Response: {response}")
|
||||
print("\n" + "="*100 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("\n" + "="*100 + "\n")
|
||||
|
||||
def demonstrate_direct_usage():
|
||||
"""
|
||||
Example of how the Code Execution tool works at the LiteLLM level
|
||||
(for reference - this requires direct LiteLLM usage)
|
||||
"""
|
||||
print("=== Direct LiteLLM Code Execution Usage (Reference) ===")
|
||||
print("""
|
||||
# This is how it works at the LiteLLM level:
|
||||
|
||||
import litellm
|
||||
import os
|
||||
|
||||
os.environ["GEMINI_API_KEY"] = "your-api-key"
|
||||
|
||||
response = litellm.completion(
|
||||
model="gemini/gemini-2.0-flash",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": "Calculate the factorial of 10 and show the code"
|
||||
}],
|
||||
tools=[{"codeExecution": {}}],
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
demonstrate_direct_usage()
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Gemini Google Search Tool Example
|
||||
|
||||
This example demonstrates how to use Gemini's built-in Google Search functionality
|
||||
through PraisonAI. The Google Search tool allows the model to perform real-time
|
||||
web searches and ground responses in current information.
|
||||
|
||||
Prerequisites:
|
||||
- Set GEMINI_API_KEY environment variable
|
||||
- Use a Gemini model that supports internal tools (gemini-2.0-flash, etc.)
|
||||
|
||||
Features:
|
||||
- Real-time web search capabilities
|
||||
- Automatic grounding of responses in search results
|
||||
- No need to implement external search tools
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Ensure you have your Gemini API key set
|
||||
# import os; os.environ["GEMINI_API_KEY"] = "your-api-key-here"
|
||||
|
||||
def main():
|
||||
# Create agent with Google Search internal tool
|
||||
agent = Agent(
|
||||
instructions="""You are a research assistant that can search the web for current information.
|
||||
Use the Google Search tool to find up-to-date information when answering questions.
|
||||
Always cite your sources and indicate when information comes from search results.""",
|
||||
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
|
||||
# Enable Google Search internal tool
|
||||
tools=[{"googleSearch": {}}],
|
||||
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Example queries that benefit from real-time search
|
||||
queries = [
|
||||
"What's the latest news about AI developments this week?",
|
||||
"What's the current weather in San Francisco?",
|
||||
"What are the latest stock prices for major tech companies?",
|
||||
"What are the most recent breakthroughs in quantum computing?",
|
||||
"Who won the latest major sports championships?"
|
||||
]
|
||||
|
||||
print("=== Gemini Google Search Tool Demo ===\n")
|
||||
|
||||
for i, query in enumerate(queries, 1):
|
||||
print(f"Query {i}: {query}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
response = agent.start(query)
|
||||
print(f"Response: {response}")
|
||||
print("\n" + "="*80 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("\n" + "="*80 + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Gemini Internal Tools Combined Example
|
||||
|
||||
This example demonstrates how to use all three of Gemini's built-in internal tools
|
||||
together through PraisonAI:
|
||||
1. Google Search - for real-time web information
|
||||
2. URL Context - for analyzing web content
|
||||
3. Code Execution - for computations and analysis
|
||||
|
||||
Prerequisites:
|
||||
- Set GEMINI_API_KEY environment variable
|
||||
- Use a Gemini model that supports internal tools (gemini-2.0-flash, etc.)
|
||||
|
||||
Features:
|
||||
- Multi-tool agent combining search, content analysis, and computation
|
||||
- Real-world research and analysis workflows
|
||||
- Comprehensive data gathering and processing capabilities
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Ensure you have your Gemini API key set
|
||||
# import os; os.environ["GEMINI_API_KEY"] = "your-api-key-here"
|
||||
|
||||
def main():
|
||||
# Create agent with all Gemini internal tools enabled
|
||||
agent = Agent(
|
||||
instructions="""You are an advanced research and analysis assistant with access to:
|
||||
1. Google Search - for finding current information
|
||||
2. URL Context - for analyzing web content
|
||||
3. Code Execution - for calculations and data processing
|
||||
|
||||
Use these tools strategically to provide comprehensive, well-researched, and
|
||||
analytically sound responses. Always show your work and cite sources.""",
|
||||
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
|
||||
# Enable all three Gemini internal tools
|
||||
tools=[
|
||||
{"googleSearch": {}},
|
||||
{"urlContext": {}},
|
||||
{"codeExecution": {}}
|
||||
],
|
||||
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Complex tasks that benefit from multiple internal tools
|
||||
complex_tasks = [
|
||||
"""Research the current market cap of the top 5 technology companies,
|
||||
then calculate their combined market value and determine what percentage
|
||||
each company represents of the total. Present the results in a formatted table.""",
|
||||
|
||||
"""Find recent research papers about large language models from arXiv,
|
||||
analyze the abstracts of 2-3 papers, and calculate the average number
|
||||
of authors across these papers.""",
|
||||
|
||||
"""Look up the current population of the 10 largest cities in the world,
|
||||
then calculate statistical measures (mean, median, standard deviation)
|
||||
and create a simple visualization of the data distribution.""",
|
||||
|
||||
"""Research the latest Python release notes, analyze the key new features,
|
||||
and write example code demonstrating 2-3 of the most important new features.""",
|
||||
|
||||
"""Find information about recent climate data, analyze a specific dataset
|
||||
or report, and perform calculations to show trends or comparisons over time."""
|
||||
]
|
||||
|
||||
print("=== Gemini Combined Internal Tools Demo ===\n")
|
||||
|
||||
for i, task in enumerate(complex_tasks, 1):
|
||||
print(f"Complex Task {i}:")
|
||||
print(task)
|
||||
print("-" * 100)
|
||||
|
||||
try:
|
||||
response = agent.start(task)
|
||||
print(f"Response: {response}")
|
||||
print("\n" + "="*120 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("\n" + "="*120 + "\n")
|
||||
|
||||
def demonstrate_tool_synergy():
|
||||
"""
|
||||
Example showing how the tools work together for a research workflow
|
||||
"""
|
||||
print("=== Tool Synergy Example ===")
|
||||
|
||||
agent = Agent(
|
||||
instructions="""You are a market research analyst. Use all available tools
|
||||
to gather information, analyze content, and perform calculations as needed.""",
|
||||
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[
|
||||
{"googleSearch": {}},
|
||||
{"urlContext": {}},
|
||||
{"codeExecution": {}}
|
||||
],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Multi-step research task
|
||||
research_query = """
|
||||
Conduct a mini market analysis on electric vehicle adoption:
|
||||
1. Search for recent statistics on global EV sales
|
||||
2. Find and analyze a recent report or article about EV market trends
|
||||
3. Calculate the year-over-year growth rate if you can find data for multiple years
|
||||
4. Provide a summary with key insights and numerical analysis
|
||||
"""
|
||||
|
||||
print("Research Query:", research_query)
|
||||
print("-" * 80)
|
||||
|
||||
try:
|
||||
response = agent.start(research_query)
|
||||
print(f"Research Results: {response}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def show_configuration_options():
|
||||
"""
|
||||
Show different ways to configure the internal tools
|
||||
"""
|
||||
print("\n=== Configuration Examples ===")
|
||||
print("""
|
||||
# Basic usage (all tools with default settings)
|
||||
tools=[
|
||||
{"googleSearch": {}},
|
||||
{"urlContext": {}},
|
||||
{"codeExecution": {}}
|
||||
]
|
||||
|
||||
# Can also be used individually
|
||||
tools=[{"googleSearch": {}}] # Only Google Search
|
||||
tools=[{"urlContext": {}}] # Only URL Context
|
||||
tools=[{"codeExecution": {}}] # Only Code Execution
|
||||
|
||||
# Mix with custom tools
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def custom_calculator(expression: str) -> str:
|
||||
'''Custom calculator function - safely evaluates basic math expressions'''
|
||||
import ast
|
||||
import operator
|
||||
|
||||
# Define safe operations
|
||||
ops = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Pow: operator.pow,
|
||||
ast.USub: operator.neg,
|
||||
}
|
||||
|
||||
def eval_expr(node):
|
||||
if isinstance(node, ast.Constant): # Numbers
|
||||
return node.value
|
||||
elif isinstance(node, ast.BinOp): # Binary operations
|
||||
return ops[type(node.op)](eval_expr(node.left), eval_expr(node.right))
|
||||
elif isinstance(node, ast.UnaryOp): # Unary operations
|
||||
return ops[type(node.op)](eval_expr(node.operand))
|
||||
else:
|
||||
raise TypeError(f"Unsupported operation: {type(node)}")
|
||||
|
||||
try:
|
||||
return str(eval_expr(ast.parse(expression, mode='eval').body))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
agent = Agent(
|
||||
tools=[
|
||||
{"googleSearch": {}}, # Gemini internal tool
|
||||
{"codeExecution": {}}, # Gemini internal tool
|
||||
custom_calculator # Custom external tool
|
||||
]
|
||||
)
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
print("\n" + "="*50)
|
||||
demonstrate_tool_synergy()
|
||||
show_configuration_options()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Gemini URL Context Tool Example
|
||||
|
||||
This example demonstrates how to use Gemini's built-in URL Context functionality
|
||||
through PraisonAI. The URL Context tool allows the model to fetch and analyze
|
||||
content from specific URLs in real-time.
|
||||
|
||||
Prerequisites:
|
||||
- Set GEMINI_API_KEY environment variable
|
||||
- Use a Gemini model that supports internal tools (gemini-2.0-flash, etc.)
|
||||
|
||||
Features:
|
||||
- Real-time URL content fetching
|
||||
- Automatic content analysis and summarization
|
||||
- Support for various web content types
|
||||
- Access to URL metadata through response.model_extra
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Ensure you have your Gemini API key set
|
||||
# import os; os.environ["GEMINI_API_KEY"] = "your-api-key-here"
|
||||
|
||||
def main():
|
||||
# Create agent with URL Context internal tool
|
||||
agent = Agent(
|
||||
instructions="""You are a content analyst that can fetch and analyze web content.
|
||||
Use the URL Context tool to access and analyze content from provided URLs.
|
||||
Provide detailed summaries, extract key information, and answer questions about the content.""",
|
||||
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
|
||||
# Enable URL Context internal tool
|
||||
tools=[{"urlContext": {}}],
|
||||
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Example URLs for analysis
|
||||
url_tasks = [
|
||||
{
|
||||
"url": "https://ai.google.dev/gemini-api/docs/models",
|
||||
"task": "Summarize the key information about Gemini models and their capabilities"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/MervinPraison/PraisonAI",
|
||||
"task": "Analyze this GitHub repository and explain what PraisonAI does"
|
||||
},
|
||||
{
|
||||
"url": "https://docs.python.org/3/tutorial/introduction.html",
|
||||
"task": "Extract the main learning objectives from this Python tutorial"
|
||||
},
|
||||
{
|
||||
"url": "https://arxiv.org/abs/2301.00234",
|
||||
"task": "Provide a summary of this research paper's abstract and key contributions"
|
||||
}
|
||||
]
|
||||
|
||||
print("=== Gemini URL Context Tool Demo ===\n")
|
||||
|
||||
for i, item in enumerate(url_tasks, 1):
|
||||
print(f"Analysis {i}: {item['url']}")
|
||||
print(f"Task: {item['task']}")
|
||||
print("-" * 50)
|
||||
|
||||
# Combine URL and task in the query
|
||||
query = f"{item['task']}: {item['url']}"
|
||||
|
||||
try:
|
||||
response = agent.start(query)
|
||||
print(f"Analysis: {response}")
|
||||
|
||||
# Note: URL metadata access requires direct LiteLLM response
|
||||
# In PraisonAI, this would need to be accessed through the LLM response
|
||||
print(f"\nAnalyzed URL: {item['url']}")
|
||||
print("\n" + "="*80 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("\n" + "="*80 + "\n")
|
||||
|
||||
def demonstrate_direct_usage():
|
||||
"""
|
||||
Example of how the URL Context tool works at the LiteLLM level
|
||||
(for reference - this requires direct LiteLLM usage)
|
||||
"""
|
||||
print("=== Direct LiteLLM URL Context Usage (Reference) ===")
|
||||
print("""
|
||||
# This is how it works at the LiteLLM level:
|
||||
|
||||
import litellm
|
||||
import os
|
||||
|
||||
os.environ["GEMINI_API_KEY"] = "your-api-key"
|
||||
|
||||
response = litellm.completion(
|
||||
model="gemini/gemini-2.0-flash",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": "Summarize this document: https://ai.google.dev/gemini-api/docs/models"
|
||||
}],
|
||||
tools=[{"urlContext": {}}],
|
||||
)
|
||||
|
||||
# Access URL metadata
|
||||
if hasattr(response, 'model_extra') and 'vertex_ai_url_context_metadata' in response.model_extra:
|
||||
url_context_metadata = response.model_extra['vertex_ai_url_context_metadata']
|
||||
urlMetadata = url_context_metadata[0]['urlMetadata'][0]
|
||||
print(f"Retrieved URL: {urlMetadata['retrievedUrl']}")
|
||||
print(f"Retrieval Status: {urlMetadata['urlRetrievalStatus']}")
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
demonstrate_direct_usage()
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are an audio tour creation AI agent. "
|
||||
"Help users create engaging audio tours, guided experiences, and interactive storytelling content. "
|
||||
"Provide guidance on script writing, audio production, location-based content, and immersive experiences.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your audio tour creation assistant. "
|
||||
"How can I help you create captivating audio experiences today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a chess AI agent. "
|
||||
"Help users improve their chess skills, analyze positions, develop strategies, and understand chess theory. "
|
||||
"Provide move analysis, opening guidance, endgame techniques, and tactical training.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your chess assistant. "
|
||||
"How can I help you improve your chess game today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a creative writing AI agent. "
|
||||
"Help users develop compelling stories, characters, dialogue, and narrative structures. "
|
||||
"Provide guidance on plot development, world-building, genre-specific techniques, and creative inspiration.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your creative writing assistant. "
|
||||
"How can I help you bring your stories and ideas to life today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a cybersecurity AI agent. "
|
||||
"Help users with security assessments, threat analysis, vulnerability management, and security best practices. "
|
||||
"Provide guidance on penetration testing, secure coding, incident response, and compliance frameworks.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your cybersecurity assistant. "
|
||||
"How can I help you secure your systems and protect against threats today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a data visualization AI agent. "
|
||||
"Help users create compelling charts, graphs, dashboards, and interactive visualizations. "
|
||||
"Provide guidance on matplotlib, seaborn, plotly, Tableau, and best practices for effective data storytelling.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your data visualization assistant. "
|
||||
"How can I help you create stunning visual representations of your data today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a 3D PyGame development AI agent. "
|
||||
"Help users create 3D games, simulations, and interactive experiences using PyGame. "
|
||||
"Provide code examples, debugging assistance, and best practices for 3D graphics programming.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your 3D PyGame development assistant. "
|
||||
"How can I help you create amazing 3D games today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a self-evolving AI agent. "
|
||||
"Continuously learn from interactions, adapt your responses, and improve your capabilities. "
|
||||
"Help users with complex problem-solving, creative tasks, and provide insights that evolve based on context and user needs.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your self-evolving AI assistant. "
|
||||
"How can I help you today, and how can I learn and improve from our interaction?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a startup insight AI agent. "
|
||||
"Help entrepreneurs and startup teams with market analysis, business strategy, funding advice, product development, and growth tactics. "
|
||||
"Provide actionable insights for building successful startups.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your startup insight assistant. "
|
||||
"How can I help you build and grow your startup today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a system architecture AI agent. "
|
||||
"Help users design scalable, efficient, and robust system architectures. "
|
||||
"Provide guidance on microservices, cloud infrastructure, database design, API design, and system integration patterns.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your system architecture assistant. "
|
||||
"How can I help you design and optimize your system architecture today?")
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a web scraping AI agent. "
|
||||
"Help users create efficient and ethical web scrapers, data extraction tools, and automation scripts. "
|
||||
"Provide guidance on BeautifulSoup, Selenium, Scrapy, and best practices for responsible web scraping.",
|
||||
llm="openrouter/moonshotai/kimi-k2"
|
||||
)
|
||||
|
||||
response = agent.start("Hello! I'm your web scraping assistant. "
|
||||
"How can I help you extract and analyze web data today?")
|
||||
@@ -0,0 +1,9 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Research assistant with web search",
|
||||
llm="gemini/gemini-2.5-flash",
|
||||
tools=[{"googleSearch": {}}, ]
|
||||
)
|
||||
|
||||
response = agent.start("Who is Mervin Praison?")
|
||||
@@ -1926,6 +1926,26 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
return self.chat(prompt, **kwargs)
|
||||
|
||||
def execute(self, task, context=None):
|
||||
"""Execute a task synchronously - backward compatibility method"""
|
||||
if hasattr(task, 'description'):
|
||||
prompt = task.description
|
||||
elif isinstance(task, str):
|
||||
prompt = task
|
||||
else:
|
||||
prompt = str(task)
|
||||
return self.chat(prompt)
|
||||
|
||||
async def aexecute(self, task, context=None):
|
||||
"""Execute a task asynchronously - backward compatibility method"""
|
||||
if hasattr(task, 'description'):
|
||||
prompt = task.description
|
||||
elif isinstance(task, str):
|
||||
prompt = task
|
||||
else:
|
||||
prompt = str(task)
|
||||
return await self.achat(prompt)
|
||||
|
||||
async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any]) -> Any:
|
||||
"""Async version of execute_tool"""
|
||||
try:
|
||||
|
||||
@@ -19,12 +19,54 @@ class LLMGuardrail:
|
||||
|
||||
Args:
|
||||
description: Natural language description of what to validate
|
||||
llm: The LLM instance to use for validation
|
||||
llm: The LLM instance to use for validation (can be string or LLM instance)
|
||||
"""
|
||||
self.description = description
|
||||
self.llm = llm
|
||||
self.llm = self._initialize_llm(llm)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _initialize_llm(self, llm: Any) -> Any:
|
||||
"""Initialize the LLM instance from string identifier or existing instance.
|
||||
|
||||
Args:
|
||||
llm: String identifier, LLM instance, or None
|
||||
|
||||
Returns:
|
||||
LLM instance or None
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
def _get_llm_class():
|
||||
from ..llm.llm import LLM
|
||||
return LLM
|
||||
|
||||
if llm is None:
|
||||
return None
|
||||
|
||||
# If it's already an LLM instance, return as-is
|
||||
if hasattr(llm, 'chat') or hasattr(llm, 'get_response') or callable(llm):
|
||||
return llm
|
||||
|
||||
# If it's a string, convert to LLM instance
|
||||
if isinstance(llm, str):
|
||||
try:
|
||||
# Handle string identifiers (both provider/model and simple names)
|
||||
return _get_llm_class()(model=llm)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize LLM from string '{llm}': {str(e)}")
|
||||
return None
|
||||
|
||||
# If it's a dict, pass parameters to LLM
|
||||
if isinstance(llm, dict) and "model" in llm:
|
||||
try:
|
||||
return _get_llm_class()(**llm)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize LLM from dict: {str(e)}")
|
||||
return None
|
||||
|
||||
# Unknown type
|
||||
self.logger.warning(f"Unknown LLM type: {type(llm)}, treating as-is")
|
||||
return llm
|
||||
|
||||
def __call__(self, task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]:
|
||||
"""Validate the task output using the LLM.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..main import (
|
||||
ReflectionOutput,
|
||||
execute_sync_callback,
|
||||
)
|
||||
from .model_capabilities import is_gemini_internal_tool
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
|
||||
@@ -542,6 +543,7 @@ class LLM:
|
||||
- Lists of pre-formatted tools
|
||||
- Callable functions
|
||||
- String function names
|
||||
- Gemini internal tools ({"googleSearch": {}}, {"urlContext": {}}, {"codeExecution": {}})
|
||||
|
||||
Args:
|
||||
tools: List of tools in various formats
|
||||
@@ -588,6 +590,11 @@ class LLM:
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
# Handle Gemini internal tools (e.g., {"googleSearch": {}}, {"urlContext": {}}, {"codeExecution": {}})
|
||||
elif is_gemini_internal_tool(tool):
|
||||
tool_name = next(iter(tool.keys()))
|
||||
logging.debug(f"Using Gemini internal tool: {tool_name}")
|
||||
formatted_tools.append(tool)
|
||||
else:
|
||||
logging.debug(f"Skipping tool of unsupported type: {type(tool)}")
|
||||
|
||||
@@ -744,18 +751,9 @@ class LLM:
|
||||
response_text = resp["choices"][0]["message"]["content"]
|
||||
final_response = resp
|
||||
|
||||
# Always execute callbacks regardless of verbose setting
|
||||
# Execute callbacks and display based on verbose setting
|
||||
generation_time_val = time.time() - current_time
|
||||
interaction_displayed = False
|
||||
|
||||
response_content = f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}" if reasoning_content else response_text
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_content,
|
||||
markdown=markdown,
|
||||
generation_time=generation_time_val
|
||||
)
|
||||
|
||||
# Optionally display reasoning if present
|
||||
if verbose and reasoning_content and not interaction_displayed:
|
||||
@@ -763,7 +761,7 @@ class LLM:
|
||||
original_prompt,
|
||||
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time,
|
||||
generation_time=generation_time_val,
|
||||
console=console,
|
||||
agent_name=agent_name,
|
||||
agent_role=agent_role,
|
||||
@@ -773,12 +771,13 @@ class LLM:
|
||||
task_id=task_id
|
||||
)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif verbose and not interaction_displayed:
|
||||
display_interaction(
|
||||
original_prompt,
|
||||
response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time,
|
||||
generation_time=generation_time_val,
|
||||
console=console,
|
||||
agent_name=agent_name,
|
||||
agent_role=agent_role,
|
||||
@@ -788,6 +787,17 @@ class LLM:
|
||||
task_id=task_id
|
||||
)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif not callback_executed:
|
||||
# Only execute callback if display_interaction hasn't been called (which would trigger callbacks internally)
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_content,
|
||||
markdown=markdown,
|
||||
generation_time=generation_time_val
|
||||
)
|
||||
callback_executed = True
|
||||
|
||||
# Otherwise do the existing streaming approach
|
||||
else:
|
||||
@@ -847,14 +857,16 @@ class LLM:
|
||||
|
||||
response_text = response_text.strip() if response_text else ""
|
||||
|
||||
# Always execute callbacks after streaming completes
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time
|
||||
)
|
||||
# Execute callbacks after streaming completes (only if not verbose, since verbose will call display_interaction later)
|
||||
if not verbose and not callback_executed:
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time
|
||||
)
|
||||
callback_executed = True
|
||||
|
||||
|
||||
# Create a mock final_response with the captured data
|
||||
@@ -881,18 +893,9 @@ class LLM:
|
||||
)
|
||||
response_text = final_response["choices"][0]["message"]["content"]
|
||||
|
||||
# Always execute callbacks regardless of verbose setting
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time
|
||||
)
|
||||
|
||||
|
||||
# Execute callbacks and display based on verbose setting
|
||||
if verbose and not interaction_displayed:
|
||||
# Display the complete response at once
|
||||
# Display the complete response at once (this will trigger callbacks internally)
|
||||
display_interaction(
|
||||
original_prompt,
|
||||
response_text,
|
||||
@@ -907,6 +910,17 @@ class LLM:
|
||||
task_id=task_id
|
||||
)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif not callback_executed:
|
||||
# Only execute callback if display_interaction hasn't been called
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - current_time
|
||||
)
|
||||
callback_executed = True
|
||||
|
||||
tool_calls = final_response["choices"][0]["message"].get("tool_calls")
|
||||
|
||||
@@ -1010,16 +1024,8 @@ class LLM:
|
||||
return final_response_text
|
||||
|
||||
# No tool calls were made in this iteration, return the response
|
||||
# Always execute callbacks regardless of verbose setting
|
||||
generation_time_val = time.time() - start_time
|
||||
response_content = f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}" if stored_reasoning_content else response_text
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_content,
|
||||
markdown=markdown,
|
||||
generation_time=generation_time_val
|
||||
)
|
||||
|
||||
if verbose and not interaction_displayed:
|
||||
# If we have stored reasoning content from tool execution, display it
|
||||
@@ -1028,7 +1034,7 @@ class LLM:
|
||||
original_prompt,
|
||||
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}",
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - start_time,
|
||||
generation_time=generation_time_val,
|
||||
console=console,
|
||||
agent_name=agent_name,
|
||||
agent_role=agent_role,
|
||||
@@ -1042,7 +1048,7 @@ class LLM:
|
||||
original_prompt,
|
||||
response_text,
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - start_time,
|
||||
generation_time=generation_time_val,
|
||||
console=console,
|
||||
agent_name=agent_name,
|
||||
agent_role=agent_role,
|
||||
@@ -1052,6 +1058,17 @@ class LLM:
|
||||
task_id=task_id
|
||||
)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif not callback_executed:
|
||||
# Only execute callback if display_interaction hasn't been called
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
response=response_content,
|
||||
markdown=markdown,
|
||||
generation_time=generation_time_val
|
||||
)
|
||||
callback_executed = True
|
||||
|
||||
response_text = response_text.strip() if response_text else ""
|
||||
|
||||
@@ -1063,8 +1080,16 @@ class LLM:
|
||||
if output_json or output_pydantic:
|
||||
self.chat_history.append({"role": "user", "content": original_prompt})
|
||||
self.chat_history.append({"role": "assistant", "content": response_text})
|
||||
# Always execute callbacks regardless of verbose setting
|
||||
if not interaction_displayed:
|
||||
|
||||
if verbose and not interaction_displayed:
|
||||
display_interaction(original_prompt, response_text, markdown=markdown,
|
||||
generation_time=time.time() - start_time, console=console,
|
||||
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
|
||||
task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif not callback_executed:
|
||||
# Only execute callback if display_interaction hasn't been called
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
@@ -1072,17 +1097,19 @@ class LLM:
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - start_time
|
||||
)
|
||||
if verbose and not interaction_displayed:
|
||||
display_interaction(original_prompt, response_text, markdown=markdown,
|
||||
generation_time=time.time() - start_time, console=console,
|
||||
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
|
||||
task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
return response_text
|
||||
|
||||
if not self_reflect:
|
||||
# Always execute callbacks regardless of verbose setting
|
||||
if not interaction_displayed:
|
||||
if verbose and not interaction_displayed:
|
||||
display_interaction(original_prompt, response_text, markdown=markdown,
|
||||
generation_time=time.time() - start_time, console=console,
|
||||
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
|
||||
task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
elif not callback_executed:
|
||||
# Only execute callback if display_interaction hasn't been called
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=original_prompt,
|
||||
@@ -1090,13 +1117,8 @@ class LLM:
|
||||
markdown=markdown,
|
||||
generation_time=time.time() - start_time
|
||||
)
|
||||
|
||||
if verbose and not interaction_displayed:
|
||||
display_interaction(original_prompt, response_text, markdown=markdown,
|
||||
generation_time=time.time() - start_time, console=console,
|
||||
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
|
||||
task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
interaction_displayed = True
|
||||
callback_executed = True
|
||||
|
||||
# Return reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and stored_reasoning_content:
|
||||
return stored_reasoning_content
|
||||
@@ -1401,6 +1423,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
start_time = time.time()
|
||||
reflection_count = 0
|
||||
callback_executed = False # Track if callback has been executed for this interaction
|
||||
interaction_displayed = False # Track if interaction has been displayed
|
||||
|
||||
# Format tools for LiteLLM using the shared helper
|
||||
@@ -2031,10 +2054,11 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
output_json = override_params.get('output_json')
|
||||
output_pydantic = override_params.get('output_pydantic')
|
||||
|
||||
# Always remove these from params as they're not native litellm parameters
|
||||
params.pop('output_json', None)
|
||||
params.pop('output_pydantic', None)
|
||||
|
||||
if output_json or output_pydantic:
|
||||
# Always remove these from params as they're not native litellm parameters
|
||||
params.pop('output_json', None)
|
||||
params.pop('output_pydantic', None)
|
||||
|
||||
# Check if this is a Gemini model that supports native structured outputs
|
||||
if self._is_gemini_model():
|
||||
|
||||
@@ -104,4 +104,27 @@ def supports_streaming_with_tools(model_name: str) -> bool:
|
||||
"""
|
||||
# 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)
|
||||
return supports_structured_outputs(model_name)
|
||||
|
||||
|
||||
# Supported Gemini internal tools
|
||||
GEMINI_INTERNAL_TOOLS = {'googleSearch', 'urlContext', 'codeExecution'}
|
||||
|
||||
|
||||
def is_gemini_internal_tool(tool) -> bool:
|
||||
"""
|
||||
Check if a tool is a Gemini internal tool and should be included in formatted tools.
|
||||
|
||||
Gemini internal tools are single-key dictionaries with specific tool names.
|
||||
Examples: {"googleSearch": {}}, {"urlContext": {}}, {"codeExecution": {}}
|
||||
|
||||
Args:
|
||||
tool: The tool to check
|
||||
|
||||
Returns:
|
||||
bool: True if the tool is a recognized Gemini internal tool, False otherwise
|
||||
"""
|
||||
if isinstance(tool, dict) and len(tool) == 1:
|
||||
tool_name = next(iter(tool.keys()))
|
||||
return tool_name in GEMINI_INTERNAL_TOOLS
|
||||
return False
|
||||
@@ -18,6 +18,7 @@ from dataclasses import dataclass
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
import inspect
|
||||
from .model_capabilities import is_gemini_internal_tool
|
||||
|
||||
# Constants
|
||||
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
|
||||
@@ -360,6 +361,7 @@ class OpenAIClient:
|
||||
- Callable functions
|
||||
- String function names
|
||||
- MCP tools
|
||||
- Gemini internal tools ({"googleSearch": {}}, {"urlContext": {}}, {"codeExecution": {}})
|
||||
|
||||
Args:
|
||||
tools: List of tools in various formats
|
||||
@@ -404,6 +406,11 @@ class OpenAIClient:
|
||||
tool_def = self._generate_tool_definition_from_name(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
# Handle Gemini internal tools (e.g., {"googleSearch": {}}, {"urlContext": {}}, {"codeExecution": {}})
|
||||
elif is_gemini_internal_tool(tool):
|
||||
tool_name = next(iter(tool.keys()))
|
||||
logging.debug(f"Using Gemini internal tool: {tool_name}")
|
||||
formatted_tools.append(tool)
|
||||
else:
|
||||
logging.debug(f"Skipping tool of unsupported type: {type(tool)}")
|
||||
|
||||
|
||||
@@ -159,35 +159,20 @@ def display_interaction(message, response, markdown=True, generation_time=None,
|
||||
message = _clean_display_content(str(message))
|
||||
response = _clean_display_content(str(response))
|
||||
|
||||
|
||||
# Execute synchronous callback if registered
|
||||
if 'interaction' in sync_display_callbacks:
|
||||
callback = sync_display_callbacks['interaction']
|
||||
import inspect
|
||||
sig = inspect.signature(callback)
|
||||
|
||||
all_kwargs = {
|
||||
'message': message,
|
||||
'response': response,
|
||||
'markdown': markdown,
|
||||
'generation_time': generation_time,
|
||||
'agent_name': agent_name,
|
||||
'agent_role': agent_role,
|
||||
'agent_tools': agent_tools,
|
||||
'task_name': task_name,
|
||||
'task_description': task_description,
|
||||
'task_id': task_id
|
||||
}
|
||||
|
||||
# Filter kwargs to what the callback accepts to maintain backward compatibility
|
||||
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
|
||||
# Callback accepts **kwargs, so pass all arguments
|
||||
supported_kwargs = all_kwargs
|
||||
else:
|
||||
# Only pass arguments that the callback signature supports
|
||||
supported_kwargs = {k: v for k, v in all_kwargs.items() if k in sig.parameters}
|
||||
|
||||
callback(**supported_kwargs)
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback(
|
||||
'interaction',
|
||||
message=message,
|
||||
response=response,
|
||||
markdown=markdown,
|
||||
generation_time=generation_time,
|
||||
agent_name=agent_name,
|
||||
agent_role=agent_role,
|
||||
agent_tools=agent_tools,
|
||||
task_name=task_name,
|
||||
task_description=task_description,
|
||||
task_id=task_id
|
||||
)
|
||||
# Rest of the display logic...
|
||||
if generation_time:
|
||||
console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
|
||||
@@ -206,6 +191,9 @@ def display_self_reflection(message: str, console=None):
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback('self_reflection', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
|
||||
|
||||
def display_instruction(message: str, console=None, agent_name: str = None, agent_role: str = None, agent_tools: List[str] = None):
|
||||
@@ -215,6 +203,9 @@ def display_instruction(message: str, console=None, agent_name: str = None, agen
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback('instruction', message=message, agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools)
|
||||
|
||||
# Display agent info if available
|
||||
if agent_name:
|
||||
agent_info = f"[bold #FF9B9B]👤 Agent:[/] [#FFE5E5]{agent_name}[/]"
|
||||
@@ -239,6 +230,9 @@ def display_tool_call(message: str, console=None):
|
||||
message = _clean_display_content(str(message))
|
||||
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
|
||||
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback('tool_call', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
|
||||
def display_error(message: str, console=None):
|
||||
@@ -248,6 +242,9 @@ def display_error(message: str, console=None):
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback('error', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
|
||||
error_logs.append(message)
|
||||
|
||||
@@ -263,6 +260,9 @@ def display_generating(content: str = "", start_time: Optional[float] = None):
|
||||
|
||||
content = _clean_display_content(str(content))
|
||||
|
||||
# Execute synchronous callbacks
|
||||
execute_sync_callback('generating', content=content, elapsed_time=elapsed_str.strip() if elapsed_str else None)
|
||||
|
||||
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
|
||||
|
||||
# Async versions with 'a' prefix
|
||||
@@ -312,8 +312,8 @@ async def adisplay_self_reflection(message: str, console=None):
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
if 'self_reflection' in async_display_callbacks:
|
||||
await async_display_callbacks['self_reflection'](message=message)
|
||||
# Execute callbacks
|
||||
await execute_callback('self_reflection', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
|
||||
|
||||
@@ -325,8 +325,8 @@ async def adisplay_instruction(message: str, console=None, agent_name: str = Non
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
if 'instruction' in async_display_callbacks:
|
||||
await async_display_callbacks['instruction'](message=message)
|
||||
# Execute callbacks
|
||||
await execute_callback('instruction', message=message, agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools)
|
||||
|
||||
# Display agent info if available
|
||||
if agent_name:
|
||||
@@ -353,8 +353,8 @@ async def adisplay_tool_call(message: str, console=None):
|
||||
message = _clean_display_content(str(message))
|
||||
logging.debug(f"Cleaned message in adisplay_tool_call: {repr(message)}")
|
||||
|
||||
if 'tool_call' in async_display_callbacks:
|
||||
await async_display_callbacks['tool_call'](message=message)
|
||||
# Execute callbacks
|
||||
await execute_callback('tool_call', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
|
||||
@@ -366,8 +366,8 @@ async def adisplay_error(message: str, console=None):
|
||||
console = Console()
|
||||
message = _clean_display_content(str(message))
|
||||
|
||||
if 'error' in async_display_callbacks:
|
||||
await async_display_callbacks['error'](message=message)
|
||||
# Execute callbacks
|
||||
await execute_callback('error', message=message)
|
||||
|
||||
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
|
||||
error_logs.append(message)
|
||||
@@ -385,11 +385,8 @@ async def adisplay_generating(content: str = "", start_time: Optional[float] = N
|
||||
|
||||
content = _clean_display_content(str(content))
|
||||
|
||||
if 'generating' in async_display_callbacks:
|
||||
await async_display_callbacks['generating'](
|
||||
content=content,
|
||||
elapsed_time=elapsed_str.strip() if elapsed_str else None
|
||||
)
|
||||
# Execute callbacks
|
||||
await execute_callback('generating', content=content, elapsed_time=elapsed_str.strip() if elapsed_str else None)
|
||||
|
||||
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
|
||||
|
||||
|
||||
@@ -59,10 +59,90 @@ class Process:
|
||||
|
||||
def _create_loop_subtasks(self, loop_task: Task):
|
||||
"""Create subtasks for a loop task from input file."""
|
||||
logging.warning(f"_create_loop_subtasks called for {loop_task.name} but method not fully implemented")
|
||||
# TODO: Implement loop subtask creation from input file
|
||||
# This should read loop_task.input_file and create subtasks
|
||||
pass
|
||||
if not loop_task.input_file:
|
||||
logging.warning(f"_create_loop_subtasks called for {loop_task.name} but no input_file specified")
|
||||
return
|
||||
|
||||
try:
|
||||
file_ext = os.path.splitext(loop_task.input_file)[1].lower()
|
||||
new_tasks = []
|
||||
|
||||
if file_ext == ".csv":
|
||||
with open(loop_task.input_file, "r", encoding="utf-8") as f:
|
||||
reader = csv.reader(f, quotechar='"', escapechar='\\')
|
||||
previous_task = None
|
||||
task_count = 0
|
||||
|
||||
for i, row in enumerate(reader):
|
||||
if not row: # Skip empty rows
|
||||
continue
|
||||
|
||||
# Handle Q&A pairs with potential commas
|
||||
task_desc = row[0].strip() if row else ""
|
||||
if len(row) > 1:
|
||||
question = row[0].strip()
|
||||
answer = ",".join(field.strip() for field in row[1:])
|
||||
task_desc = f"Question: {question}\nAnswer: {answer}"
|
||||
|
||||
if not task_desc: # Skip rows with empty content
|
||||
continue
|
||||
|
||||
task_count += 1
|
||||
logging.debug(f"Creating subtask from CSV row {i+1}: {task_desc}")
|
||||
|
||||
row_task = Task(
|
||||
description=f"{loop_task.description}\n{task_desc}" if loop_task.description else task_desc,
|
||||
agent=loop_task.agent,
|
||||
name=f"{loop_task.name}_{task_count}" if loop_task.name else task_desc,
|
||||
expected_output=getattr(loop_task, 'expected_output', None),
|
||||
callback=loop_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(task_count == 1),
|
||||
task_type="task"
|
||||
)
|
||||
self.tasks[row_task.id] = row_task
|
||||
new_tasks.append(row_task)
|
||||
|
||||
if previous_task:
|
||||
previous_task.next_tasks = [row_task.name]
|
||||
previous_task = row_task
|
||||
|
||||
logging.info(f"Created {task_count} subtasks from CSV file for {loop_task.name}")
|
||||
else:
|
||||
# Handle text files
|
||||
with open(loop_task.input_file, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
previous_task = None
|
||||
for i, line in enumerate(lines):
|
||||
if not line.strip(): # Skip empty lines
|
||||
continue
|
||||
|
||||
row_task = Task(
|
||||
description=f"{loop_task.description}\n{line.strip()}" if loop_task.description else line.strip(),
|
||||
agent=loop_task.agent,
|
||||
name=f"{loop_task.name}_{i+1}" if loop_task.name else line.strip(),
|
||||
expected_output=getattr(loop_task, 'expected_output', None),
|
||||
callback=loop_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(i == 0),
|
||||
task_type="task"
|
||||
)
|
||||
self.tasks[row_task.id] = row_task
|
||||
new_tasks.append(row_task)
|
||||
|
||||
if previous_task:
|
||||
previous_task.next_tasks = [row_task.name]
|
||||
previous_task = row_task
|
||||
|
||||
logging.info(f"Created {len(new_tasks)} subtasks from text file for {loop_task.name}")
|
||||
|
||||
if new_tasks and loop_task.next_tasks:
|
||||
# Connect last subtask to loop task's next tasks
|
||||
last_task = new_tasks[-1]
|
||||
last_task.next_tasks = loop_task.next_tasks
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to create subtasks for loop task {loop_task.name}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _build_task_context(self, current_task: Task) -> str:
|
||||
"""Build context for a task based on its retain_full_context setting"""
|
||||
@@ -809,6 +889,7 @@ Provide a JSON with the structure:
|
||||
agent=start_task.agent,
|
||||
name=f"{start_task.name}_{task_count}" if start_task.name else task_desc,
|
||||
expected_output=getattr(start_task, 'expected_output', None),
|
||||
callback=start_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(task_count == 1),
|
||||
task_type="decision", # Change to decision type
|
||||
next_tasks=inherited_next_tasks, # Inherit parent's next tasks
|
||||
@@ -842,6 +923,7 @@ Provide a JSON with the structure:
|
||||
agent=start_task.agent,
|
||||
name=f"{start_task.name}_{i+1}" if start_task.name else line.strip(),
|
||||
expected_output=getattr(start_task, 'expected_output', None),
|
||||
callback=start_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(i == 0),
|
||||
task_type="task",
|
||||
condition={
|
||||
@@ -925,6 +1007,7 @@ Tasks by type:
|
||||
agent=current_task.agent,
|
||||
name=f"{current_task.name}_{i+1}" if current_task.name else task_desc,
|
||||
expected_output=getattr(current_task, 'expected_output', None),
|
||||
callback=current_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(i == 0),
|
||||
task_type="task",
|
||||
condition={
|
||||
@@ -949,6 +1032,7 @@ Tasks by type:
|
||||
agent=current_task.agent,
|
||||
name=f"{current_task.name}_{i+1}" if current_task.name else line.strip(),
|
||||
expected_output=getattr(current_task, 'expected_output', None),
|
||||
callback=current_task.callback, # Inherit callback from parent loop task
|
||||
is_start=(i == 0),
|
||||
task_type="task",
|
||||
condition={
|
||||
|
||||
@@ -381,13 +381,10 @@ class Task:
|
||||
logger.exception(e) # Print full stack trace
|
||||
# Continue execution even if memory operations fail
|
||||
|
||||
# Execute original callback
|
||||
# Execute original callback with metadata support
|
||||
if self.callback:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback(task_output)
|
||||
else:
|
||||
self.callback(task_output)
|
||||
await self._execute_callback_with_metadata(task_output)
|
||||
except Exception as e:
|
||||
logger.error(f"Task {self.id}: Failed to execute callback: {e}")
|
||||
logger.exception(e)
|
||||
@@ -470,4 +467,63 @@ Context:
|
||||
success=False,
|
||||
result=None,
|
||||
error=f"Guardrail validation error: {str(e)}"
|
||||
)
|
||||
)
|
||||
|
||||
async def _execute_callback_with_metadata(self, task_output):
|
||||
"""Execute callback with metadata support while maintaining backward compatibility.
|
||||
|
||||
This method automatically detects the callback signature:
|
||||
- Single parameter callbacks receive only TaskOutput (backward compatible)
|
||||
- Two parameter callbacks receive TaskOutput and metadata dict (enhanced)
|
||||
|
||||
Args:
|
||||
task_output: The TaskOutput object to pass to the callback
|
||||
"""
|
||||
if not self.callback:
|
||||
return
|
||||
|
||||
try:
|
||||
# Inspect the callback signature to determine parameter count
|
||||
sig = inspect.signature(self.callback)
|
||||
param_count = len(sig.parameters)
|
||||
|
||||
if param_count == 1:
|
||||
# Backward compatible: single parameter callback
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback(task_output)
|
||||
else:
|
||||
self.callback(task_output)
|
||||
elif param_count >= 2:
|
||||
# Enhanced: two parameter callback with metadata
|
||||
metadata = {
|
||||
'task_id': self.id,
|
||||
'task_name': self.name,
|
||||
'agent_name': self.agent.name if self.agent else None,
|
||||
'task_type': self.task_type,
|
||||
'task_status': self.status,
|
||||
'task_description': self.description,
|
||||
'expected_output': self.expected_output,
|
||||
'input_file': getattr(self, 'input_file', None),
|
||||
'loop_state': self.loop_state,
|
||||
'retry_count': getattr(self, 'retry_count', 0),
|
||||
'async_execution': self.async_execution
|
||||
}
|
||||
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback(task_output, metadata)
|
||||
else:
|
||||
self.callback(task_output, metadata)
|
||||
else:
|
||||
# No parameter callback - unusual but handle gracefully
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback()
|
||||
else:
|
||||
self.callback()
|
||||
|
||||
except TypeError as e:
|
||||
# Fallback for signature inspection issues
|
||||
logger.warning(f"Task {self.id}: Callback signature inspection failed, falling back to single parameter: {e}")
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback(task_output)
|
||||
else:
|
||||
self.callback(task_output)
|
||||
@@ -116,8 +116,17 @@ class MinimalTelemetry:
|
||||
|
||||
self._metrics["agent_executions"] += 1
|
||||
|
||||
# In a real implementation, this would send to a backend
|
||||
# For now, just log at debug level
|
||||
# Send event to PostHog
|
||||
if self._posthog:
|
||||
self._posthog.capture(
|
||||
distinct_id=self.session_id,
|
||||
event='agent_execution',
|
||||
properties={
|
||||
'success': success,
|
||||
'session_id': self.session_id
|
||||
}
|
||||
)
|
||||
|
||||
self.logger.debug(f"Agent execution tracked: success={success}")
|
||||
|
||||
def track_task_completion(self, task_name: str = None, success: bool = True):
|
||||
@@ -133,6 +142,17 @@ class MinimalTelemetry:
|
||||
|
||||
self._metrics["task_completions"] += 1
|
||||
|
||||
# Send event to PostHog
|
||||
if self._posthog:
|
||||
self._posthog.capture(
|
||||
distinct_id=self.session_id,
|
||||
event='task_completion',
|
||||
properties={
|
||||
'success': success,
|
||||
'session_id': self.session_id
|
||||
}
|
||||
)
|
||||
|
||||
self.logger.debug(f"Task completion tracked: success={success}")
|
||||
|
||||
def track_tool_usage(self, tool_name: str, success: bool = True):
|
||||
@@ -148,6 +168,18 @@ class MinimalTelemetry:
|
||||
|
||||
self._metrics["tool_calls"] += 1
|
||||
|
||||
# Send event to PostHog
|
||||
if self._posthog:
|
||||
self._posthog.capture(
|
||||
distinct_id=self.session_id,
|
||||
event='tool_usage',
|
||||
properties={
|
||||
'tool_name': tool_name,
|
||||
'success': success,
|
||||
'session_id': self.session_id
|
||||
}
|
||||
)
|
||||
|
||||
# Only track tool name, not arguments or results
|
||||
self.logger.debug(f"Tool usage tracked: {tool_name}, success={success}")
|
||||
|
||||
@@ -163,6 +195,17 @@ class MinimalTelemetry:
|
||||
|
||||
self._metrics["errors"] += 1
|
||||
|
||||
# Send event to PostHog
|
||||
if self._posthog:
|
||||
self._posthog.capture(
|
||||
distinct_id=self.session_id,
|
||||
event='error',
|
||||
properties={
|
||||
'error_type': error_type or 'unknown',
|
||||
'session_id': self.session_id
|
||||
}
|
||||
)
|
||||
|
||||
# Only track error type, not full error messages
|
||||
self.logger.debug(f"Error tracked: type={error_type or 'unknown'}")
|
||||
|
||||
@@ -176,6 +219,17 @@ class MinimalTelemetry:
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
# Send event to PostHog
|
||||
if self._posthog:
|
||||
self._posthog.capture(
|
||||
distinct_id=self.session_id,
|
||||
event='feature_usage',
|
||||
properties={
|
||||
'feature_name': feature_name,
|
||||
'session_id': self.session_id
|
||||
}
|
||||
)
|
||||
|
||||
# Track which features are being used
|
||||
self.logger.debug(f"Feature usage tracked: {feature_name}")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.131"
|
||||
version = "0.0.134"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to isolate the duplicate callback issue #878.
|
||||
This script directly tests the LLM class to determine where the duplicate callbacks originate.
|
||||
|
||||
ROOT CAUSE IDENTIFIED:
|
||||
In llm.py, there are two separate callback execution paths:
|
||||
1. execute_sync_callback() called directly (lines 851-857, 885-891, etc.)
|
||||
2. display_interaction() called when verbose=True (line 896, etc.)
|
||||
|
||||
Since display_interaction() ALSO executes sync callbacks internally (main.py lines 164-190),
|
||||
this results in duplicate callback execution for the same LLM response.
|
||||
|
||||
The issue occurs specifically when verbose=True because both callback paths are active.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the path to the praisonaiagents module
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
|
||||
|
||||
from praisonaiagents import register_display_callback
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
# Global counter and log
|
||||
callback_invocations = []
|
||||
|
||||
def track_callback(message=None, response=None, **kwargs):
|
||||
"""
|
||||
Callback function that tracks each invocation with detailed information.
|
||||
"""
|
||||
invocation = {
|
||||
'count': len(callback_invocations) + 1,
|
||||
'message': str(message)[:100] if message else None,
|
||||
'response': str(response)[:100] if response else None,
|
||||
'kwargs_keys': list(kwargs.keys()) if kwargs else [],
|
||||
'stack_trace': get_simplified_stack_trace()
|
||||
}
|
||||
callback_invocations.append(invocation)
|
||||
|
||||
print(f"🔔 CALLBACK #{invocation['count']}")
|
||||
print(f" Message: {invocation['message']}")
|
||||
print(f" Response: {invocation['response']}")
|
||||
print(f" Called from: {invocation['stack_trace']}")
|
||||
print()
|
||||
|
||||
def get_simplified_stack_trace():
|
||||
"""Get a simplified stack trace to see where the callback is called from."""
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
# Get the current stack
|
||||
stack = inspect.stack()
|
||||
|
||||
# Find relevant frames (skip our own callback function)
|
||||
relevant_frames = []
|
||||
for frame in stack[2:8]: # Skip track_callback and get_simplified_stack_trace
|
||||
filename = os.path.basename(frame.filename)
|
||||
if 'praisonaiagents' in frame.filename:
|
||||
relevant_frames.append(f"{filename}:{frame.lineno} in {frame.function}")
|
||||
|
||||
return " -> ".join(relevant_frames) if relevant_frames else "Unknown"
|
||||
|
||||
def test_llm_streaming():
|
||||
"""Test LLM with streaming enabled."""
|
||||
print("🧪 TEST 1: LLM with streaming")
|
||||
print("-" * 40)
|
||||
|
||||
global callback_invocations
|
||||
callback_invocations = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Hello World",
|
||||
verbose=True, # This should trigger the callback
|
||||
stream=True # Test with streaming
|
||||
)
|
||||
|
||||
print(f"✅ Response: {response}")
|
||||
print(f"📊 Total callback invocations: {len(callback_invocations)}")
|
||||
|
||||
return len(callback_invocations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return -1
|
||||
|
||||
def test_llm_non_streaming():
|
||||
"""Test LLM without streaming."""
|
||||
print("🧪 TEST 2: LLM without streaming")
|
||||
print("-" * 40)
|
||||
|
||||
global callback_invocations
|
||||
callback_invocations = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Hello World",
|
||||
verbose=True, # This should trigger the callback
|
||||
stream=False # Test without streaming
|
||||
)
|
||||
|
||||
print(f"✅ Response: {response}")
|
||||
print(f"📊 Total callback invocations: {len(callback_invocations)}")
|
||||
|
||||
return len(callback_invocations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return -1
|
||||
|
||||
def test_llm_with_self_reflection():
|
||||
"""Test LLM with self-reflection enabled."""
|
||||
print("🧪 TEST 3: LLM with self-reflection")
|
||||
print("-" * 40)
|
||||
|
||||
global callback_invocations
|
||||
callback_invocations = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False, self_reflect=True)
|
||||
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Hello World",
|
||||
verbose=True, # This should trigger the callback
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=1
|
||||
)
|
||||
|
||||
print(f"✅ Response: {response}")
|
||||
print(f"📊 Total callback invocations: {len(callback_invocations)}")
|
||||
|
||||
return len(callback_invocations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return -1
|
||||
|
||||
def test_verbose_vs_non_verbose():
|
||||
"""Test the key difference: verbose=True vs verbose=False to isolate the duplicate issue."""
|
||||
print("🧪 TEST 4: Verbose vs Non-Verbose (Key Test for Duplicate Issue)")
|
||||
print("-" * 60)
|
||||
|
||||
global callback_invocations
|
||||
|
||||
# Test with verbose=False (should trigger callback once via execute_sync_callback)
|
||||
print(" 🔹 Testing with verbose=False:")
|
||||
callback_invocations = []
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Test Non-Verbose",
|
||||
verbose=False # This should only trigger execute_sync_callback
|
||||
)
|
||||
print(f" Response: {response}")
|
||||
print(f" Callbacks with verbose=False: {len(callback_invocations)}")
|
||||
non_verbose_count = len(callback_invocations)
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
non_verbose_count = -1
|
||||
|
||||
print()
|
||||
|
||||
# Test with verbose=True (should trigger callback twice: execute_sync_callback + display_interaction)
|
||||
print(" 🔹 Testing with verbose=True:")
|
||||
callback_invocations = []
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Test Verbose",
|
||||
verbose=True # This should trigger BOTH execute_sync_callback AND display_interaction
|
||||
)
|
||||
print(f" Response: {response}")
|
||||
print(f" Callbacks with verbose=True: {len(callback_invocations)}")
|
||||
verbose_count = len(callback_invocations)
|
||||
|
||||
# Show the call stack differences
|
||||
if len(callback_invocations) > 1:
|
||||
print(f"\n 📋 Call stack analysis:")
|
||||
for i, inv in enumerate(callback_invocations):
|
||||
print(f" Callback #{i+1}: {inv['stack_trace']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
verbose_count = -1
|
||||
|
||||
print(f"\n 📊 RESULT:")
|
||||
print(f" verbose=False: {non_verbose_count} callback(s)")
|
||||
print(f" verbose=True: {verbose_count} callback(s)")
|
||||
|
||||
if verbose_count > non_verbose_count and verbose_count > 1:
|
||||
print(f" 🚨 DUPLICATE ISSUE CONFIRMED!")
|
||||
print(f" verbose=True triggers {verbose_count - non_verbose_count} extra callback(s)")
|
||||
|
||||
return verbose_count
|
||||
|
||||
def test_multiple_calls():
|
||||
"""Test multiple consecutive calls to see pattern."""
|
||||
print("🧪 TEST 5: Multiple consecutive calls")
|
||||
print("-" * 40)
|
||||
|
||||
global callback_invocations
|
||||
callback_invocations = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('interaction', track_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
|
||||
for i in range(3):
|
||||
print(f" Call {i+1}:")
|
||||
response = llm.get_response(
|
||||
prompt=f"Say exactly: Call {i+1}",
|
||||
verbose=True
|
||||
)
|
||||
print(f" Response: {response}")
|
||||
|
||||
print(f"📊 Total callback invocations across 3 calls: {len(callback_invocations)}")
|
||||
|
||||
return len(callback_invocations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return -1
|
||||
|
||||
def analyze_callback_patterns():
|
||||
"""Analyze the patterns in callback invocations."""
|
||||
print("\n📊 CALLBACK PATTERN ANALYSIS")
|
||||
print("=" * 50)
|
||||
|
||||
if not callback_invocations:
|
||||
print("❌ No callback invocations recorded")
|
||||
return
|
||||
|
||||
print(f"Total invocations: {len(callback_invocations)}")
|
||||
print("\nDetailed breakdown:")
|
||||
|
||||
for inv in callback_invocations:
|
||||
print(f" #{inv['count']}: {inv['stack_trace']}")
|
||||
if inv['message']:
|
||||
print(f" Message: {inv['message']}")
|
||||
if inv['response']:
|
||||
print(f" Response: {inv['response']}")
|
||||
print()
|
||||
|
||||
# Look for duplicate patterns
|
||||
unique_traces = set(inv['stack_trace'] for inv in callback_invocations)
|
||||
if len(unique_traces) < len(callback_invocations):
|
||||
print("⚠️ POTENTIAL DUPLICATE PATTERN DETECTED!")
|
||||
print("Some callbacks are coming from the same call stack.")
|
||||
else:
|
||||
print("✅ All callbacks appear to come from different call paths.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔬 ISOLATING DUPLICATE CALLBACK ISSUE #878")
|
||||
print("=" * 60)
|
||||
print("This script tests the LLM class directly to isolate where duplicate callbacks occur.")
|
||||
print()
|
||||
|
||||
# Run different test scenarios
|
||||
results = {}
|
||||
|
||||
# Test 1: Streaming
|
||||
results['streaming'] = test_llm_streaming()
|
||||
print()
|
||||
|
||||
# Test 2: Non-streaming
|
||||
results['non_streaming'] = test_llm_non_streaming()
|
||||
print()
|
||||
|
||||
# Test 3: Self-reflection
|
||||
results['self_reflection'] = test_llm_with_self_reflection()
|
||||
print()
|
||||
|
||||
# Test 4: Verbose vs Non-Verbose (KEY TEST)
|
||||
results['verbose_vs_nonverbose'] = test_verbose_vs_non_verbose()
|
||||
print()
|
||||
|
||||
# Test 5: Multiple calls
|
||||
results['multiple_calls'] = test_multiple_calls()
|
||||
print()
|
||||
|
||||
# Analyze patterns
|
||||
analyze_callback_patterns()
|
||||
|
||||
# Summary
|
||||
print("\n🏁 SUMMARY OF RESULTS")
|
||||
print("=" * 40)
|
||||
|
||||
expected_single_call_tests = ['streaming', 'non_streaming', 'self_reflection']
|
||||
issues_detected = []
|
||||
|
||||
for test_name, count in results.items():
|
||||
if count == -1:
|
||||
print(f"{test_name}: ERROR")
|
||||
elif test_name == 'multiple_calls':
|
||||
expected = 3
|
||||
print(f"{test_name}: {count} invocations (expected ~{expected})")
|
||||
if count > expected * 1.5: # Allow some tolerance
|
||||
issues_detected.append(f"{test_name}: {count} > expected {expected}")
|
||||
elif test_name == 'verbose_vs_nonverbose':
|
||||
# This is the key test - verbose=True should show the duplicate issue
|
||||
print(f"{test_name}: {count} invocations (this test shows verbose=True duplicates)")
|
||||
if count > 1:
|
||||
issues_detected.append(f"{test_name}: verbose=True triggered {count} callbacks (duplicate issue)")
|
||||
else:
|
||||
expected = 1
|
||||
print(f"{test_name}: {count} invocations (expected {expected})")
|
||||
if count > expected:
|
||||
issues_detected.append(f"{test_name}: {count} > expected {expected}")
|
||||
|
||||
if issues_detected:
|
||||
print(f"\n❌ DUPLICATE CALLBACK ISSUES DETECTED:")
|
||||
for issue in issues_detected:
|
||||
print(f" - {issue}")
|
||||
print("\nThe callback system is triggering multiple times when it should trigger once.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"\n✅ NO DUPLICATE CALLBACK ISSUES DETECTED")
|
||||
print("All callback invocations appear to be working as expected.")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to verify that the callback fix is working
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
||||
|
||||
from praisonaiagents import register_display_callback
|
||||
from praisonaiagents.main import display_interaction, display_self_reflection
|
||||
|
||||
# Track callback calls
|
||||
callback_calls = []
|
||||
|
||||
def test_callback(message, response, **kwargs):
|
||||
"""Test callback function"""
|
||||
callback_calls.append({
|
||||
'type': 'interaction',
|
||||
'message': message,
|
||||
'response': response
|
||||
})
|
||||
print(f"[CALLBACK] Interaction: {message[:30]}... -> {response[:30]}...")
|
||||
|
||||
def test_self_reflection_callback(message, **kwargs):
|
||||
"""Test self-reflection callback function"""
|
||||
callback_calls.append({
|
||||
'type': 'self_reflection',
|
||||
'message': message
|
||||
})
|
||||
print(f"[CALLBACK] Self-reflection: {message[:50]}...")
|
||||
|
||||
def test_display_interaction_callback():
|
||||
"""Test that display_interaction calls the callback"""
|
||||
global callback_calls
|
||||
callback_calls = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('interaction', test_callback)
|
||||
|
||||
try:
|
||||
# Call display_interaction
|
||||
display_interaction(
|
||||
message="Test message",
|
||||
response="Test response",
|
||||
markdown=True,
|
||||
generation_time=1.5
|
||||
)
|
||||
|
||||
print(f"Callback calls: {len(callback_calls)}")
|
||||
|
||||
# Verify callback was called
|
||||
assert len(callback_calls) == 1, f"Expected 1 callback call, got {len(callback_calls)}"
|
||||
assert callback_calls[0]['type'] == 'interaction'
|
||||
assert callback_calls[0]['message'] == "Test message"
|
||||
assert callback_calls[0]['response'] == "Test response"
|
||||
|
||||
print("✓ display_interaction callback test PASSED")
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('interaction', None)
|
||||
|
||||
def test_display_self_reflection_callback():
|
||||
"""Test that display_self_reflection calls the callback"""
|
||||
global callback_calls
|
||||
callback_calls = []
|
||||
|
||||
# Register callback
|
||||
register_display_callback('self_reflection', test_self_reflection_callback)
|
||||
|
||||
try:
|
||||
# Call display_self_reflection
|
||||
display_self_reflection("This is a self-reflection message")
|
||||
|
||||
print(f"Self-reflection callback calls: {len(callback_calls)}")
|
||||
|
||||
# Verify callback was called
|
||||
assert len(callback_calls) == 1, f"Expected 1 callback call, got {len(callback_calls)}"
|
||||
assert callback_calls[0]['type'] == 'self_reflection'
|
||||
assert callback_calls[0]['message'] == "This is a self-reflection message"
|
||||
|
||||
print("✓ display_self_reflection callback test PASSED")
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('self_reflection', None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing callback fix for display functions...\n")
|
||||
|
||||
try:
|
||||
print("1. Testing display_interaction callback...")
|
||||
test_display_interaction_callback()
|
||||
print()
|
||||
|
||||
print("2. Testing display_self_reflection callback...")
|
||||
test_display_self_reflection_callback()
|
||||
print()
|
||||
|
||||
print("All callback tests passed! The fix is working correctly.")
|
||||
|
||||
except AssertionError as e:
|
||||
print(f"✗ FAILED: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -9,28 +9,32 @@ import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai-agents'))
|
||||
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
from praisonaiagents import register_display_callback
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
|
||||
# Track display_interaction calls
|
||||
display_calls = []
|
||||
|
||||
def mock_display_interaction(prompt, response, markdown=True, generation_time=0, console=None):
|
||||
def mock_display_interaction(message, response, markdown=True, generation_time=0, **kwargs):
|
||||
"""Mock display_interaction to track calls"""
|
||||
display_calls.append({
|
||||
'prompt': prompt,
|
||||
'message': message,
|
||||
'response': response,
|
||||
'markdown': markdown,
|
||||
'generation_time': generation_time
|
||||
})
|
||||
print(f"[DISPLAY] {prompt[:50]}... -> {response[:50]}...")
|
||||
print(f"[DISPLAY] {message[:50]}... -> {response[:50]}...")
|
||||
|
||||
def test_single_display_no_tools():
|
||||
"""Test that display_interaction is called only once without tools"""
|
||||
global display_calls
|
||||
display_calls = []
|
||||
|
||||
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
|
||||
# Register callback instead of patching
|
||||
register_display_callback('interaction', mock_display_interaction)
|
||||
|
||||
try:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
# Mock streaming response
|
||||
mock_completion.return_value = [
|
||||
@@ -51,13 +55,20 @@ def test_single_display_no_tools():
|
||||
|
||||
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
|
||||
assert response == "Hello world!"
|
||||
finally:
|
||||
# Clean up callback to prevent interference with other tests
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('interaction', None)
|
||||
|
||||
def test_single_display_with_reasoning():
|
||||
"""Test that display_interaction is called only once with reasoning steps"""
|
||||
global display_calls
|
||||
display_calls = []
|
||||
|
||||
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
|
||||
# Register callback instead of patching
|
||||
register_display_callback('interaction', mock_display_interaction)
|
||||
|
||||
try:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
# Mock non-streaming response with reasoning
|
||||
mock_completion.return_value = {
|
||||
@@ -83,13 +94,20 @@ def test_single_display_with_reasoning():
|
||||
print(f"Display calls: {len(display_calls)}")
|
||||
|
||||
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
|
||||
finally:
|
||||
# Clean up callback to prevent interference with other tests
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('interaction', None)
|
||||
|
||||
def test_single_display_with_self_reflection():
|
||||
"""Test that display_interaction is called appropriately with self-reflection"""
|
||||
global display_calls
|
||||
display_calls = []
|
||||
|
||||
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
|
||||
# Register callback instead of patching
|
||||
register_display_callback('interaction', mock_display_interaction)
|
||||
|
||||
try:
|
||||
with patch('praisonaiagents.main.display_self_reflection'):
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
# First call - initial response
|
||||
@@ -137,6 +155,10 @@ def test_single_display_with_self_reflection():
|
||||
# Should display only the final response
|
||||
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
|
||||
assert response == "Better response"
|
||||
finally:
|
||||
# Clean up callback to prevent interference with other tests
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('interaction', None)
|
||||
|
||||
def test_async_single_display():
|
||||
"""Test async version also prevents duplicate displays"""
|
||||
@@ -145,30 +167,37 @@ def test_async_single_display():
|
||||
|
||||
import asyncio
|
||||
|
||||
async def run_test():
|
||||
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
|
||||
with patch('litellm.acompletion') as mock_acompletion:
|
||||
# Mock async streaming response
|
||||
async def async_generator():
|
||||
yield MagicMock(choices=[MagicMock(delta=MagicMock(content="Async"))])
|
||||
yield MagicMock(choices=[MagicMock(delta=MagicMock(content=" response"))])
|
||||
|
||||
mock_acompletion.return_value = async_generator()
|
||||
|
||||
llm = LLM(model="gpt-4o-mini", verbose=False)
|
||||
response = await llm.get_response_async(
|
||||
prompt="Test async",
|
||||
verbose=True,
|
||||
stream=True
|
||||
)
|
||||
|
||||
print(f"\nAsync Response: {response}")
|
||||
print(f"Display calls: {len(display_calls)}")
|
||||
|
||||
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
|
||||
assert response == "Async response"
|
||||
# Register callback instead of patching
|
||||
register_display_callback('interaction', mock_display_interaction)
|
||||
|
||||
asyncio.run(run_test())
|
||||
async def run_test():
|
||||
with patch('litellm.acompletion') as mock_acompletion:
|
||||
# Mock async streaming response
|
||||
async def async_generator():
|
||||
yield MagicMock(choices=[MagicMock(delta=MagicMock(content="Async"))])
|
||||
yield MagicMock(choices=[MagicMock(delta=MagicMock(content=" response"))])
|
||||
|
||||
mock_acompletion.return_value = async_generator()
|
||||
|
||||
llm = LLM(model="gpt-4o-mini", verbose=False)
|
||||
response = await llm.get_response_async(
|
||||
prompt="Test async",
|
||||
verbose=True,
|
||||
stream=True
|
||||
)
|
||||
|
||||
print(f"\nAsync Response: {response}")
|
||||
print(f"Display calls: {len(display_calls)}")
|
||||
|
||||
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
|
||||
assert response == "Async response"
|
||||
|
||||
try:
|
||||
asyncio.run(run_test())
|
||||
finally:
|
||||
# Clean up callback to prevent interference with other tests
|
||||
from praisonaiagents.main import sync_display_callbacks
|
||||
sync_display_callbacks.pop('interaction', None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing duplicate callback fix for issue #878...\n")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Simple test to verify the self-reflection fix works"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import calculator
|
||||
from praisonaiagents.tools import evaluate
|
||||
|
||||
def test_self_reflection_fix():
|
||||
"""Test that self-reflection works with tools after the fix"""
|
||||
@@ -16,7 +16,7 @@ def test_self_reflection_fix():
|
||||
self_reflect=True,
|
||||
llm="gpt-4o-mini", # Use a more widely available model
|
||||
verbose=True,
|
||||
tools=[calculator],
|
||||
tools=[evaluate],
|
||||
min_reflect=1,
|
||||
max_reflect=2
|
||||
)
|
||||
|
||||
@@ -1,60 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test to verify the self-reflection fix works with tools"""
|
||||
"""
|
||||
Test script to verify the duplicate callback fix for issue #878.
|
||||
This tests that callbacks are triggered exactly once per LLM response.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import calculator
|
||||
import os
|
||||
import sys
|
||||
|
||||
def test_self_reflection_fix_verification():
|
||||
"""Test that self-reflection now works with tools"""
|
||||
print("=== Testing Self-Reflection Fix Verification ===")
|
||||
# Add the path to the praisonaiagents module
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
|
||||
|
||||
from praisonaiagents import register_display_callback
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
|
||||
# Track callback invocations
|
||||
callback_count = 0
|
||||
callback_details = []
|
||||
|
||||
def test_callback(message=None, response=None, **kwargs):
|
||||
"""Callback function to track invocations."""
|
||||
global callback_count, callback_details
|
||||
callback_count += 1
|
||||
callback_details.append({
|
||||
'count': callback_count,
|
||||
'message': str(message)[:50] if message else None,
|
||||
'response': str(response)[:50] if response else None,
|
||||
'kwargs': list(kwargs.keys()) if kwargs else []
|
||||
})
|
||||
print(f"🔔 CALLBACK #{callback_count}: {response[:50] if response else 'No response'}")
|
||||
|
||||
def test_verbose_true():
|
||||
"""Test with verbose=True (this was causing duplicates before the fix)."""
|
||||
global callback_count, callback_details
|
||||
callback_count = 0
|
||||
callback_details = []
|
||||
|
||||
# Create an agent with self-reflection enabled and tools
|
||||
agent = Agent(
|
||||
name="MathAgent",
|
||||
role="Math Assistant",
|
||||
goal="Solve mathematical problems accurately",
|
||||
backstory="You are a helpful math assistant who double-checks calculations",
|
||||
tools=[calculator],
|
||||
self_reflect=True, # This should now work with tools
|
||||
min_reflect=1,
|
||||
max_reflect=2,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Define a task that would benefit from self-reflection
|
||||
task = Task(
|
||||
description="Calculate 123 * 456 and verify the result is correct",
|
||||
expected_output="The calculation result with verification",
|
||||
agent=agent,
|
||||
name="math_calculation"
|
||||
)
|
||||
|
||||
# Create and run the agents
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="sequential"
|
||||
)
|
||||
print("🧪 Testing with verbose=True...")
|
||||
register_display_callback('interaction', test_callback, is_async=False)
|
||||
|
||||
try:
|
||||
result = agents.start()
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Hello World",
|
||||
verbose=True # This should NOT cause duplicate callbacks after the fix
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"\n✅ SUCCESS: Self-reflection with tools is working!")
|
||||
print(f"Result: {result}")
|
||||
print(f"✅ Response: {response}")
|
||||
print(f"📊 Callback count: {callback_count}")
|
||||
|
||||
if callback_count == 1:
|
||||
print("✅ SUCCESS: Exactly 1 callback triggered (no duplicates)")
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ FAILED: No result returned")
|
||||
print(f"❌ FAIL: Expected 1 callback, got {callback_count}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR: {str(e)}")
|
||||
print(f"❌ ERROR: {e}")
|
||||
return False
|
||||
|
||||
def test_verbose_false():
|
||||
"""Test with verbose=False (should still work)."""
|
||||
global callback_count, callback_details
|
||||
callback_count = 0
|
||||
callback_details = []
|
||||
|
||||
print("\n🧪 Testing with verbose=False...")
|
||||
register_display_callback('interaction', test_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
response = llm.get_response(
|
||||
prompt="Say exactly: Hello Non-Verbose",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print(f"✅ Response: {response}")
|
||||
print(f"📊 Callback count: {callback_count}")
|
||||
|
||||
if callback_count == 1:
|
||||
print("✅ SUCCESS: Exactly 1 callback triggered")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ FAIL: Expected 1 callback, got {callback_count}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: {e}")
|
||||
return False
|
||||
|
||||
def test_multiple_calls():
|
||||
"""Test multiple consecutive calls."""
|
||||
global callback_count, callback_details
|
||||
callback_count = 0
|
||||
callback_details = []
|
||||
|
||||
print("\n🧪 Testing multiple consecutive calls...")
|
||||
register_display_callback('interaction', test_callback, is_async=False)
|
||||
|
||||
try:
|
||||
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
|
||||
|
||||
for i in range(3):
|
||||
response = llm.get_response(
|
||||
prompt=f"Say exactly: Call {i+1}",
|
||||
verbose=True
|
||||
)
|
||||
print(f" Call {i+1}: {response}")
|
||||
|
||||
print(f"📊 Total callback count: {callback_count}")
|
||||
|
||||
if callback_count == 3:
|
||||
print("✅ SUCCESS: Exactly 3 callbacks for 3 calls (1 each)")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ FAIL: Expected 3 callbacks, got {callback_count}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_self_reflection_fix_verification()
|
||||
print("🔧 TESTING DUPLICATE CALLBACK FIX FOR ISSUE #878")
|
||||
print("=" * 60)
|
||||
|
||||
if success:
|
||||
print("\n🎉 Fix verification successful! Self-reflection now works with tools.")
|
||||
results = []
|
||||
|
||||
# Test 1: verbose=True (the main issue)
|
||||
results.append(test_verbose_true())
|
||||
|
||||
# Test 2: verbose=False (should still work)
|
||||
results.append(test_verbose_false())
|
||||
|
||||
# Test 3: multiple calls
|
||||
results.append(test_multiple_calls())
|
||||
|
||||
# Summary
|
||||
print(f"\n🏁 SUMMARY")
|
||||
print("=" * 30)
|
||||
|
||||
success_count = sum(results)
|
||||
total_tests = len(results)
|
||||
|
||||
print(f"Tests passed: {success_count}/{total_tests}")
|
||||
|
||||
if success_count == total_tests:
|
||||
print("✅ ALL TESTS PASSED! The duplicate callback issue is FIXED.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n💥 Fix verification failed. Self-reflection with tools is still not working.")
|
||||
print("❌ SOME TESTS FAILED! The duplicate callback issue still exists.")
|
||||
sys.exit(1)
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Test LLM class directly to verify self-reflection fix"""
|
||||
|
||||
from praisonaiagents.llm import LLM
|
||||
from praisonaiagents.tools import calculator
|
||||
from praisonaiagents.tools import evaluate
|
||||
|
||||
def test_llm_direct():
|
||||
"""Test LLM class directly with self-reflection and tools"""
|
||||
@@ -15,7 +15,7 @@ def test_llm_direct():
|
||||
response = llm.get_response(
|
||||
prompt="Calculate 15 * 23 and verify your answer",
|
||||
system_prompt="You are a helpful math assistant. Use tools when needed.",
|
||||
tools=[calculator],
|
||||
tools=[evaluate],
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify self-reflection fix for issue #901
|
||||
Tests both with and without tools to ensure fix works correctly
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
def create_mock_tool():
|
||||
"""Create a mock tool that mimics the user's custom search tool"""
|
||||
def mock_search_tool(query: str) -> str:
|
||||
"""Mock tool that returns a fake search result"""
|
||||
return f"Mock search result for: {query}"
|
||||
|
||||
# Set the function name as expected by the tool system
|
||||
mock_search_tool.__name__ = "mock_search_tool"
|
||||
|
||||
return mock_search_tool
|
||||
|
||||
def test_self_reflection_with_tools():
|
||||
"""Test that self-reflection works when tools are present"""
|
||||
print("Testing self-reflection WITH tools...")
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = create_mock_tool()
|
||||
|
||||
# Create an agent with self-reflection and tools
|
||||
agent = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Analyze and provide insights on given topics",
|
||||
backstory="You are an expert analyst with strong critical thinking skills",
|
||||
self_reflect=True,
|
||||
llm="gpt-4o-mini", # Using OpenAI model for testing
|
||||
verbose=True,
|
||||
tools=[mock_tool],
|
||||
max_reflect=2, # Keep it small for testing
|
||||
min_reflect=1
|
||||
)
|
||||
|
||||
# Create a task
|
||||
task = Task(
|
||||
description="Analyze the importance of AI in modern business",
|
||||
expected_output="A brief analysis report",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create and start the agents
|
||||
try:
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print("✅ Self-reflection with tools completed successfully!")
|
||||
print(f"Result: {result}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Self-reflection with tools failed: {e}")
|
||||
return False
|
||||
|
||||
def test_self_reflection_without_tools():
|
||||
"""Test that self-reflection still works without tools (baseline)"""
|
||||
print("\nTesting self-reflection WITHOUT tools...")
|
||||
|
||||
# Create an agent with self-reflection but no tools
|
||||
agent = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Analyze and provide insights on given topics",
|
||||
backstory="You are an expert analyst with strong critical thinking skills",
|
||||
self_reflect=True,
|
||||
llm="gpt-4o-mini", # Using OpenAI model for testing
|
||||
verbose=True,
|
||||
max_reflect=2, # Keep it small for testing
|
||||
min_reflect=1
|
||||
)
|
||||
|
||||
# Create a task
|
||||
task = Task(
|
||||
description="Analyze the importance of AI in modern business",
|
||||
expected_output="A brief analysis report",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create and start the agents
|
||||
try:
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print("✅ Self-reflection without tools completed successfully!")
|
||||
print(f"Result: {result}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Self-reflection without tools failed: {e}")
|
||||
return False
|
||||
|
||||
def test_llm_directly():
|
||||
"""Test LLM class directly to isolate the issue"""
|
||||
print("\nTesting LLM class directly...")
|
||||
|
||||
from praisonaiagents.llm import LLM
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = create_mock_tool()
|
||||
|
||||
# Create LLM instance
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
def mock_tool_executor(function_name, arguments):
|
||||
"""Mock tool executor that simulates tool execution"""
|
||||
if function_name == "mock_search_tool":
|
||||
return mock_tool(arguments.get("query", "test"))
|
||||
return None
|
||||
|
||||
try:
|
||||
# Test with tools and self-reflection
|
||||
response = llm.get_response(
|
||||
prompt="Analyze the importance of AI in modern business",
|
||||
tools=[mock_tool],
|
||||
self_reflect=True,
|
||||
max_reflect=2,
|
||||
min_reflect=1,
|
||||
verbose=True,
|
||||
execute_tool_fn=mock_tool_executor
|
||||
)
|
||||
|
||||
print("✅ LLM direct test with tools completed successfully!")
|
||||
print(f"Response: {response}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ LLM direct test with tools failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if we have required environment variables
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("❌ OPENAI_API_KEY environment variable is required for testing")
|
||||
sys.exit(1)
|
||||
|
||||
print("Testing self-reflection fix for issue #901")
|
||||
print("=" * 50)
|
||||
|
||||
# Run tests
|
||||
test_results = []
|
||||
|
||||
# Test 1: Without tools (baseline)
|
||||
test_results.append(test_self_reflection_without_tools())
|
||||
|
||||
# Test 2: With tools (the fixed case)
|
||||
test_results.append(test_self_reflection_with_tools())
|
||||
|
||||
# Test 3: Direct LLM test
|
||||
test_results.append(test_llm_directly())
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 50)
|
||||
print("Test Summary:")
|
||||
print(f"✅ Tests passed: {sum(test_results)}")
|
||||
print(f"❌ Tests failed: {len(test_results) - sum(test_results)}")
|
||||
|
||||
if all(test_results):
|
||||
print("🎉 All tests passed! Self-reflection fix is working correctly.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("💥 Some tests failed. Please check the output above.")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test self-reflection functionality with tools to verify the fix for issue #901"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.llm import LLM
|
||||
|
||||
def simple_calculator(operation: str, a: int, b: int) -> int:
|
||||
"""A simple calculator tool"""
|
||||
if operation == "add":
|
||||
return a + b
|
||||
elif operation == "subtract":
|
||||
return a - b
|
||||
elif operation == "multiply":
|
||||
return a * b
|
||||
elif operation == "divide":
|
||||
return a / b if b != 0 else "Cannot divide by zero"
|
||||
else:
|
||||
return "Unknown operation"
|
||||
|
||||
def test_llm_self_reflection_with_tools():
|
||||
"""Test self-reflection in LLM class directly with tools"""
|
||||
print("=== Testing LLM Self-Reflection WITH Tools ===")
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
def mock_tool_executor(function_name, arguments):
|
||||
"""Mock tool executor for testing"""
|
||||
if function_name == "simple_calculator":
|
||||
return simple_calculator(
|
||||
arguments.get("operation", "add"),
|
||||
arguments.get("a", 0),
|
||||
arguments.get("b", 0)
|
||||
)
|
||||
return None
|
||||
|
||||
response = llm.get_response(
|
||||
prompt="Calculate 5 + 3 and then reflect on your answer",
|
||||
system_prompt="You are a helpful assistant with access to a calculator tool.",
|
||||
tools=[simple_calculator],
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2,
|
||||
verbose=True,
|
||||
execute_tool_fn=mock_tool_executor
|
||||
)
|
||||
print(f"LLM Response with tools: {response}")
|
||||
print()
|
||||
|
||||
def test_llm_self_reflection_without_tools():
|
||||
"""Test self-reflection in LLM class directly without tools"""
|
||||
print("=== Testing LLM Self-Reflection WITHOUT Tools ===")
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
response = llm.get_response(
|
||||
prompt="Calculate 5 + 3 and then reflect on your answer",
|
||||
system_prompt="You are a helpful assistant.",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2,
|
||||
verbose=True
|
||||
)
|
||||
print(f"LLM Response without tools: {response}")
|
||||
print()
|
||||
|
||||
def test_agent_self_reflection_with_tools():
|
||||
"""Test self-reflection in Agent class with tools"""
|
||||
print("=== Testing Agent Self-Reflection WITH Tools ===")
|
||||
agent = Agent(
|
||||
name="CalculatorAgent",
|
||||
instructions="You are a helpful assistant with access to a calculator tool.",
|
||||
llm="gpt-4o-mini",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2,
|
||||
tools=[simple_calculator]
|
||||
)
|
||||
|
||||
response = agent.start("Calculate 5 + 3 and then reflect on your answer")
|
||||
print(f"Agent Response with tools: {response}")
|
||||
print()
|
||||
|
||||
def test_agent_self_reflection_without_tools():
|
||||
"""Test self-reflection in Agent class without tools"""
|
||||
print("=== Testing Agent Self-Reflection WITHOUT Tools ===")
|
||||
agent = Agent(
|
||||
name="BasicAgent",
|
||||
instructions="You are a helpful assistant.",
|
||||
llm="gpt-4o-mini",
|
||||
self_reflect=True,
|
||||
min_reflect=1,
|
||||
max_reflect=2
|
||||
)
|
||||
|
||||
response = agent.start("Calculate 5 + 3 and then reflect on your answer")
|
||||
print(f"Agent Response without tools: {response}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
# Check if we have the required API key
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("❌ OPENAI_API_KEY environment variable is required for testing")
|
||||
print("Set it with: export OPENAI_API_KEY='your-api-key-here'")
|
||||
exit(1)
|
||||
|
||||
print("Testing self-reflection with tools fix for issue #901")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Test baseline (without tools)
|
||||
test_llm_self_reflection_without_tools()
|
||||
test_agent_self_reflection_without_tools()
|
||||
|
||||
# Test the fix (with tools)
|
||||
test_llm_self_reflection_with_tools()
|
||||
test_agent_self_reflection_with_tools()
|
||||
|
||||
print("✅ All tests completed successfully!")
|
||||
print("🎉 Self-reflection with tools is now working correctly!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script that replicates the exact user code pattern from issue #901
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
def google_web_search_llm(query: str) -> str:
|
||||
"""Mock implementation of the user's custom search tool"""
|
||||
return f"Mock Google search result for: {query}"
|
||||
|
||||
def test_user_code_pattern_with_tools():
|
||||
"""Test the exact user code pattern WITH tools"""
|
||||
print("Testing user code pattern WITH tools...")
|
||||
|
||||
# Create an agent with self-reflection (exact user code pattern)
|
||||
agent = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Analyze and provide insights on given topics",
|
||||
backstory="You are an expert analyst with strong critical thinking skills",
|
||||
self_reflect=True,
|
||||
llm="gpt-4o-mini", # Using OpenAI instead of Gemini for testing
|
||||
verbose=True,
|
||||
tools=[google_web_search_llm]
|
||||
)
|
||||
|
||||
# Create a task
|
||||
task = Task(
|
||||
description="Analyze recent developments in AI",
|
||||
expected_output="A detailed analysis report",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create and start the agents
|
||||
try:
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
# Start execution
|
||||
result = agents.start()
|
||||
print("✅ User code pattern WITH tools completed successfully!")
|
||||
print(f"Result type: {type(result)}")
|
||||
print(f"Result: {result}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ User code pattern WITH tools failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_user_code_pattern_without_tools():
|
||||
"""Test the exact user code pattern WITHOUT tools"""
|
||||
print("\nTesting user code pattern WITHOUT tools...")
|
||||
|
||||
# Create an agent with self-reflection (exact user code pattern, no tools)
|
||||
agent = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Analyze and provide insights on given topics",
|
||||
backstory="You are an expert analyst with strong critical thinking skills",
|
||||
self_reflect=True,
|
||||
llm="gpt-4o-mini", # Using OpenAI instead of Gemini for testing
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Create a task
|
||||
task = Task(
|
||||
description="Analyze recent developments in AI",
|
||||
expected_output="A detailed analysis report",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create and start the agents
|
||||
try:
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
# Start execution
|
||||
result = agents.start()
|
||||
print("✅ User code pattern WITHOUT tools completed successfully!")
|
||||
print(f"Result type: {type(result)}")
|
||||
print(f"Result: {result}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ User code pattern WITHOUT tools failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if we have required environment variables
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("❌ OPENAI_API_KEY environment variable is required for testing")
|
||||
print("Please set it with: export OPENAI_API_KEY='your-api-key-here'")
|
||||
sys.exit(1)
|
||||
|
||||
print("Testing exact user code pattern from issue #901")
|
||||
print("=" * 60)
|
||||
|
||||
# Run tests
|
||||
test_results = []
|
||||
|
||||
# Test 1: Without tools (baseline - should work)
|
||||
test_results.append(test_user_code_pattern_without_tools())
|
||||
|
||||
# Test 2: With tools (the problematic case that should now work)
|
||||
test_results.append(test_user_code_pattern_with_tools())
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Summary:")
|
||||
print(f"✅ Tests passed: {sum(test_results)}")
|
||||
print(f"❌ Tests failed: {len(test_results) - sum(test_results)}")
|
||||
|
||||
if all(test_results):
|
||||
print("🎉 All tests passed! The user's exact code pattern now works with self-reflection and tools.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("💥 Some tests failed. The fix may not be complete.")
|
||||
sys.exit(1)
|
||||
Generated
+1
-1
@@ -2375,7 +2375,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.131"
|
||||
version = "0.0.134"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.57.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.57.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.60.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.60.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -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.57 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.60 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.57"
|
||||
version = "2.2.60"
|
||||
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 = ""
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.0.131",
|
||||
"praisonaiagents>=0.0.134",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
@@ -95,7 +95,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.57"
|
||||
version = "2.2.60"
|
||||
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 = ""
|
||||
@@ -113,7 +113,7 @@ python = ">=3.10,<3.13"
|
||||
rich = ">=13.7"
|
||||
markdown = ">=3.5"
|
||||
pyparsing = ">=3.0.0"
|
||||
praisonaiagents = ">=0.0.131"
|
||||
praisonaiagents = ">=0.0.134"
|
||||
python-dotenv = ">=0.19.0"
|
||||
instructor = ">=1.3.3"
|
||||
PyYAML = ">=6.0"
|
||||
|
||||
Generated
+5
-5
@@ -3932,7 +3932,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.57"
|
||||
version = "2.2.60"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "instructor" },
|
||||
@@ -4074,7 +4074,7 @@ requires-dist = [
|
||||
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.15" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.15" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.131" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.134" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = ">=0.2.19" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4132,7 +4132,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.131"
|
||||
version = "0.0.134"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4142,9 +4142,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/a8/6c8b10f2254a5914a0c282e8691bea7f3fff60ea99d975db11e8d3382c9f/praisonaiagents-0.0.131.tar.gz", hash = "sha256:8b6ceef477f2c1d4e33af1242c52cf1956826066ef4d7ed3bc2dcd50ce1c68c8", size = 198480, upload-time = "2025-07-14T17:35:51.53Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/66/82e37c0c6d74a8abae69f243ab4adaf038943716d5f94894c77031a974a0/praisonaiagents-0.0.134.tar.gz", hash = "sha256:8d408b761012bc8c66b6d567036c054d29d4caaa7d8160e513f2eec3274d0f72", size = 200773, upload-time = "2025-07-15T08:54:22.249Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/d7/c443377ac2eaf68154b4ae6f87e5bc681f80774f8af9fcb368454ea9a7d8/praisonaiagents-0.0.131-py3-none-any.whl", hash = "sha256:fac2cdfe78da0fea89a74d54aab2fa9e920d61ca8d08ccebeb58132fc3770dc8", size = 218389, upload-time = "2025-07-14T17:35:49.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0e/b2ec13f4d3e6f53cbfbe1b26138882b792d5441bd7eefa122a7edb64339c/praisonaiagents-0.0.134-py3-none-any.whl", hash = "sha256:b956e79411eace9c4005ae241d06b40ae07c1680f1d1c9fb5c3f009192a13900", size = 220808, upload-time = "2025-07-15T08:54:20.773Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user