mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb551debfe | ||
|
|
fabd852002 | ||
|
|
99de9f4d4f | ||
|
|
daaf92cbeb | ||
|
|
d8375cb50b | ||
|
|
36ea8d381e | ||
|
|
8a8f96e5e5 | ||
|
|
cd09b2de96 | ||
|
|
5e5b6672eb | ||
|
|
f7ce10b92e | ||
|
|
eead19802d | ||
|
|
208c8aad3b | ||
|
|
fb2b086dd5 | ||
|
|
75972372a6 | ||
|
|
108053bb32 | ||
|
|
fbddda54ba | ||
|
|
83427ca929 | ||
|
|
36e5047e53 | ||
|
|
05d8136433 | ||
|
|
af9b251b59 | ||
|
|
c8a6744469 | ||
|
|
678fd462c3 | ||
|
|
ba03a0d893 | ||
|
|
3be4c4c70e | ||
|
|
709640240f | ||
|
|
93d526eb7b | ||
|
|
624a2cfa17 | ||
|
|
4d2a6a2e2e | ||
|
|
a1bdb056a0 | ||
|
|
b4a6401bae | ||
|
|
772ce7d2e2 | ||
|
|
20c2d28f35 | ||
|
|
f8589e4363 | ||
|
|
97d368d2e2 | ||
|
|
49d9d40199 | ||
|
|
445cc037ce | ||
|
|
c4e7b7dc7f | ||
|
|
9e7c172f55 | ||
|
|
d89570b085 | ||
|
|
ee5dfddccd | ||
|
|
a3bfc6f795 | ||
|
|
ebc130854d | ||
|
|
f40a5c1359 | ||
|
|
3284f7bbc6 | ||
|
|
87928611f7 | ||
|
|
2727bf5992 | ||
|
|
45d04163c3 | ||
|
|
477d0d2e26 | ||
|
|
77394f5e5f | ||
|
|
29e533c18c | ||
|
|
48dbea0566 | ||
|
|
969d6697e1 | ||
|
|
787d2508b1 | ||
|
|
ec42dc92e4 | ||
|
|
5b7bae2f43 | ||
|
|
69a1efac47 | ||
|
|
e6381139ec | ||
|
|
aca2a58971 | ||
|
|
7a07ebd05e | ||
|
|
418d6c810b | ||
|
|
8bcfdfe713 | ||
|
|
57f2fa0471 | ||
|
|
2fdffd2d8d | ||
|
|
471a6a106c | ||
|
|
7c51c3aad1 | ||
|
|
dfa261f5ef | ||
|
|
9a97f0b80f | ||
|
|
eef9a5f7ae | ||
|
|
510e9b1797 | ||
|
|
43da66d129 | ||
|
|
e1a61959ea |
No files matched your search
+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.20" \
|
||||
"praisonai>=2.2.25" \
|
||||
"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.20" \
|
||||
"praisonai>=2.2.25" \
|
||||
"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.20" \
|
||||
"praisonai>=2.2.25" \
|
||||
"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.20" \
|
||||
"praisonai>=2.2.25" \
|
||||
"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.20`
|
||||
- PraisonAI: `>=2.2.25`
|
||||
- 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.20" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.25" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
|
||||
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.20 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.25 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install flask praisonai==2.2.20 watchdog
|
||||
RUN pip install flask praisonai==2.2.25 watchdog
|
||||
|
||||
EXPOSE 5555
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 'Playwright'
|
||||
description: 'Using Playwright MCP with PraisonAI'
|
||||
---
|
||||
|
||||
# Playwright MCP
|
||||
|
||||
[Playwright](https://playwright.dev/) is a powerful browser automation tool that can be used with the Model Context Protocol (MCP) to enable agents to interact with web browsers.
|
||||
|
||||
## Installation
|
||||
|
||||
To use Playwright with MCP, you'll need to install the Playwright MCP package:
|
||||
|
||||
```bash
|
||||
npm install @playwright/mcp
|
||||
```
|
||||
|
||||
## Running the Playwright MCP Server
|
||||
|
||||
You need to run the Playwright MCP server in a separate terminal before your agent can use it:
|
||||
|
||||
```bash
|
||||
npx @playwright/mcp@latest --port 8931
|
||||
```
|
||||
|
||||
This will start a Playwright MCP server on port 8931 that your agent can connect to.
|
||||
|
||||
## Using Playwright MCP with PraisonAI
|
||||
|
||||
Once the Playwright MCP server is running, you can create an agent that uses it as a tool:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
search_agent = Agent(
|
||||
instructions="""You help search the web.""",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("http://localhost:8931/sse")
|
||||
)
|
||||
|
||||
search_agent.start("Find about Praison AI")
|
||||
```
|
||||
|
||||
The agent will be able to use Playwright to automate browser interactions and search the web.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
You can customize the Playwright MCP server by passing additional options:
|
||||
|
||||
```bash
|
||||
npx @playwright/mcp@latest --port 8931 --browser chromium
|
||||
```
|
||||
|
||||
For more information on available options, refer to the [Playwright MCP documentation](https://github.com/microsoft/playwright/tree/main/packages/mcp).
|
||||
@@ -251,6 +251,7 @@
|
||||
"mcp/anthropic",
|
||||
"mcp/gemini",
|
||||
"mcp/mistral",
|
||||
"mcp/playwright",
|
||||
"mcp/whatsapp",
|
||||
"mcp/perplexity",
|
||||
"mcp/ollama-python",
|
||||
|
||||
@@ -31,7 +31,7 @@ export OPENAI_API_KEY=xxxxxxxxx
|
||||
|
||||
```python
|
||||
import os
|
||||
from crawl4ai import WebCrawler
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
from crawl4ai.extraction_strategy import LLMExtractionStrategy
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -41,7 +41,7 @@ class OpenAIModelFee(BaseModel):
|
||||
output_fee: str = Field(..., description="Fee for output token ßfor the OpenAI model.")
|
||||
|
||||
url = 'https://openai.com/api/pricing/'
|
||||
crawler = WebCrawler()
|
||||
crawler = AsyncWebCrawler()
|
||||
crawler.warmup()
|
||||
|
||||
result = crawler.run(
|
||||
|
||||
@@ -120,7 +120,7 @@ from praisonaiagents.tools import scrape_page, extract_links, crawl, extract_tex
|
||||
|
||||
# Create search agent
|
||||
agent = Agent(
|
||||
name="WebCrawler",
|
||||
name="AsyncWebCrawler",
|
||||
role="Web Scraping Specialist",
|
||||
goal="Extract and analyze web content efficiently.",
|
||||
backstory="Expert in web scraping and content extraction.",
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ To facilitate local development with live reload, you can use Docker. Follow the
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install flask praisonai==2.2.20 watchdog
|
||||
RUN pip install flask praisonai==2.2.25 watchdog
|
||||
|
||||
EXPOSE 5555
|
||||
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ To facilitate local development with live reload, you can use Docker. Follow the
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install flask praisonai==2.2.20 watchdog
|
||||
RUN pip install flask praisonai==2.2.25 watchdog
|
||||
|
||||
EXPOSE 5555
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: "MCP with Streamlit"
|
||||
description: "How to properly integrate MCP (Model Context Protocol) tools with PraisonAI Agents in Streamlit applications"
|
||||
---
|
||||
|
||||
# MCP with Streamlit
|
||||
|
||||
This guide explains how to properly integrate MCP (Model Context Protocol) tools with PraisonAI Agents in Streamlit applications, addressing common issues and providing working solutions.
|
||||
|
||||
## Common Issues
|
||||
|
||||
When integrating MCP tools with Streamlit, users often encounter these problems:
|
||||
|
||||
### Issue #1: Agent Re-initialization
|
||||
**Problem**: Agent gets re-initialized on every Streamlit interaction, causing MCP tools to fail.
|
||||
|
||||
**Solution**: Use Streamlit's session state to initialize the agent only once.
|
||||
|
||||
### Issue #2: LLM Provider Format
|
||||
**Problem**: Using provider/model format like `"ollama/llama3.2"` can cause tool calling issues in Streamlit environments.
|
||||
|
||||
**Solution**: Use standard model names like `"gpt-4o-mini"` instead of provider/model format.
|
||||
|
||||
### Issue #3: Missing Error Handling
|
||||
**Problem**: No user feedback when MCP initialization fails.
|
||||
|
||||
**Solution**: Implement comprehensive error handling with user-friendly messages.
|
||||
|
||||
## Working Example
|
||||
|
||||
Here's a complete working example that demonstrates the correct approach:
|
||||
|
||||
```python
|
||||
import streamlit as st
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.mcp import MCP
|
||||
import traceback
|
||||
|
||||
st.title("🏠 AI Airbnb Assistant")
|
||||
|
||||
# Configuration in sidebar
|
||||
with st.sidebar:
|
||||
st.header("⚙️ Configuration")
|
||||
|
||||
# Use standard model names, not provider/model format
|
||||
llm_model = st.selectbox(
|
||||
"Choose LLM Model",
|
||||
options=[
|
||||
"gpt-4o-mini", # ✅ Correct format
|
||||
"gpt-4o",
|
||||
"claude-3-5-sonnet-20241022"
|
||||
],
|
||||
index=0
|
||||
)
|
||||
|
||||
debug_mode = st.checkbox("Enable Debug Mode", value=False)
|
||||
|
||||
# Initialize session state (CRITICAL for Streamlit)
|
||||
if "agent_initialized" not in st.session_state:
|
||||
st.session_state.agent_initialized = False
|
||||
st.session_state.agent = None
|
||||
st.session_state.init_error = None
|
||||
|
||||
# Function to initialize agent with proper error handling
|
||||
def initialize_agent():
|
||||
try:
|
||||
with st.spinner("🔄 Initializing AI agent..."):
|
||||
# Create MCP tools
|
||||
mcp_tools = MCP(
|
||||
"npx -y @openbnb/mcp-server-airbnb --ignore-robots-txt",
|
||||
timeout=60,
|
||||
debug=debug_mode
|
||||
)
|
||||
|
||||
# Create agent - this should only happen ONCE
|
||||
agent = Agent(
|
||||
instructions="You are a helpful Airbnb assistant...",
|
||||
llm=llm_model, # Use standard format
|
||||
tools=mcp_tools,
|
||||
verbose=debug_mode
|
||||
)
|
||||
|
||||
return agent, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to initialize agent: {str(e)}"
|
||||
if debug_mode:
|
||||
error_msg += f"\n\nFull traceback:\n{traceback.format_exc()}"
|
||||
return None, error_msg
|
||||
|
||||
# Agent initialization (only runs once)
|
||||
if not st.session_state.agent_initialized:
|
||||
if st.button("🚀 Initialize AI Assistant", type="primary"):
|
||||
agent, error = initialize_agent()
|
||||
|
||||
if agent:
|
||||
st.session_state.agent = agent
|
||||
st.session_state.agent_initialized = True
|
||||
st.session_state.init_error = None
|
||||
st.success("✅ AI Assistant initialized successfully!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.session_state.init_error = error
|
||||
st.error(f"❌ Initialization failed: {error}")
|
||||
|
||||
# Main interface (only show if agent is ready)
|
||||
if st.session_state.agent_initialized and st.session_state.agent:
|
||||
query = st.text_input("🔍 What are you looking for?")
|
||||
|
||||
if st.button("Search") and query:
|
||||
try:
|
||||
with st.spinner("🔍 Searching..."):
|
||||
# Use the SAME agent instance from session state
|
||||
result = st.session_state.agent.start(query)
|
||||
st.write(result)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ Search failed: {str(e)}")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Session State Management
|
||||
Always use Streamlit's session state to manage agent lifecycle:
|
||||
|
||||
```python
|
||||
# ✅ Correct - Initialize once in session state
|
||||
if "agent" not in st.session_state:
|
||||
st.session_state.agent = Agent(...)
|
||||
|
||||
# ❌ Wrong - Re-initializes on every interaction
|
||||
agent = Agent(...)
|
||||
```
|
||||
|
||||
### 2. LLM Model Selection
|
||||
Use standard model names, avoid provider/model format in Streamlit:
|
||||
|
||||
```python
|
||||
# ✅ Correct - Standard model names
|
||||
llm="gpt-4o-mini"
|
||||
llm="claude-3-5-sonnet-20241022"
|
||||
|
||||
# ❌ Problematic in Streamlit - Provider/model format
|
||||
llm="ollama/llama3.2"
|
||||
llm="anthropic/claude-3-5-sonnet"
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
Implement comprehensive error handling:
|
||||
|
||||
```python
|
||||
try:
|
||||
# MCP initialization
|
||||
mcp_tools = MCP("your-mcp-command")
|
||||
agent = Agent(tools=mcp_tools, ...)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Initialization failed: {str(e)}")
|
||||
# Provide troubleshooting tips
|
||||
```
|
||||
|
||||
### 4. User Feedback
|
||||
Provide clear feedback during initialization:
|
||||
|
||||
```python
|
||||
with st.spinner("🔄 Initializing AI agent and MCP tools..."):
|
||||
# Initialization code here
|
||||
pass
|
||||
|
||||
if initialization_successful:
|
||||
st.success("✅ AI Assistant initialized successfully!")
|
||||
else:
|
||||
st.error("❌ Initialization failed")
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Tools Not Found
|
||||
If you get "MCP tool cannot be found" errors:
|
||||
|
||||
1. **Check MCP Server Command**: Ensure the MCP server command is correct and the server starts successfully
|
||||
2. **Verify Dependencies**: Make sure all required dependencies (Node.js, npm, etc.) are installed
|
||||
3. **Test Manually**: Try running the MCP server command manually first
|
||||
4. **Enable Debug Mode**: Set `debug=True` in MCP constructor for detailed logs
|
||||
|
||||
### Tool Calling Issues
|
||||
If tools aren't being called properly:
|
||||
|
||||
1. **Use Standard LLM Format**: Avoid provider/model format like `"ollama/llama3.2"`
|
||||
2. **Check API Keys**: Ensure proper environment variables are set for your chosen LLM
|
||||
3. **Session State**: Verify agent is properly stored in session state
|
||||
4. **Timeout Settings**: Increase timeout if needed: `MCP(command, timeout=120)`
|
||||
|
||||
### Threading Conflicts
|
||||
If you encounter threading-related errors:
|
||||
|
||||
1. **Single Agent Instance**: Use only one agent instance per session
|
||||
2. **Proper Cleanup**: Let Streamlit handle cleanup naturally
|
||||
3. **Avoid Manual Threading**: Don't create additional threads in Streamlit
|
||||
|
||||
## Complete Working Example
|
||||
|
||||
For a complete, production-ready example, see:
|
||||
- [`examples/python/ui/mcp-streamlit-airbnb.py`](https://github.com/MervinPraison/PraisonAI/blob/main/examples/python/ui/mcp-streamlit-airbnb.py)
|
||||
|
||||
This example includes:
|
||||
- ✅ Proper session state management
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ User-friendly interface
|
||||
- ✅ Debug mode support
|
||||
- ✅ Troubleshooting guidance
|
||||
- ✅ Configuration options
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Make sure your environment has the required dependencies:
|
||||
|
||||
```bash
|
||||
# Install PraisonAI Agents
|
||||
pip install praisonaiagents
|
||||
|
||||
# Install Streamlit
|
||||
pip install streamlit
|
||||
|
||||
# For Airbnb MCP server example
|
||||
npm install -g @openbnb/mcp-server-airbnb
|
||||
|
||||
# Set environment variables for your chosen LLM
|
||||
export OPENAI_API_KEY="your-key"
|
||||
# or
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
```
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# Run the working example
|
||||
streamlit run examples/python/ui/mcp-streamlit-airbnb.py
|
||||
|
||||
# Or create your own based on the patterns above
|
||||
streamlit run your-mcp-app.py
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The key to successfully using MCP with Streamlit is:
|
||||
|
||||
1. **Proper session state management** - Initialize agent only once
|
||||
2. **Standard LLM format** - Avoid provider/model format in Streamlit
|
||||
3. **Comprehensive error handling** - Provide user feedback
|
||||
4. **Correct usage patterns** - Follow Streamlit best practices
|
||||
|
||||
By following these guidelines, MCP tools will work reliably in your Streamlit applications without requiring any modifications to the core MCP implementation.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"source": [
|
||||
"# Article Generation Agents\n",
|
||||
"[](https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/cookbooks/notebooks/article_generation_agents.ipynb)"
|
||||
"[](https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/examples/cookbooks/yaml/article_generation_agents.ipynb)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -609,4 +609,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from crawl4ai import WebCrawler\n",
|
||||
"from crawl4ai import AsyncWebCrawler\n",
|
||||
"from crawl4ai.extraction_strategy import LLMExtractionStrategy\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from praisonai_tools import BaseTool\n",
|
||||
@@ -66,7 +66,7 @@
|
||||
" description: str = \"Extracts model fees for input and output tokens from the given pricing page.\"\n",
|
||||
"\n",
|
||||
" def _run(self, url: str):\n",
|
||||
" crawler = WebCrawler()\n",
|
||||
" crawler = AsyncWebCrawler()\n",
|
||||
" crawler.warmup()\n",
|
||||
"\n",
|
||||
" result = crawler.run(\n",
|
||||
@@ -196,8 +196,8 @@
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[LOG] 🚀 Initializing LocalSeleniumCrawlerStrategy\n",
|
||||
"[LOG] 🌤️ Warming up the WebCrawler\n",
|
||||
"[LOG] 🌞 WebCrawler is ready to crawl\n",
|
||||
"[LOG] 🌤️ Warming up the AsyncWebCrawler\n",
|
||||
"[LOG] 🌞 AsyncWebCrawler is ready to crawl\n",
|
||||
"[LOG] 🚀 Crawling done for https://openai.com/api/pricing/, success: True, time taken: 0.86 seconds\n",
|
||||
"[LOG] 🚀 Content extracted for https://openai.com/api/pricing/, success: True, time taken: 0.01 seconds\n",
|
||||
"[LOG] 🔥 Extracting semantic blocks for https://openai.com/api/pricing/, Strategy: LLMExtractionStrategy\n",
|
||||
@@ -245,8 +245,8 @@
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[LOG] 🚀 Initializing LocalSeleniumCrawlerStrategy\n",
|
||||
"[LOG] 🌤️ Warming up the WebCrawler\n",
|
||||
"[LOG] 🌞 WebCrawler is ready to crawl\n",
|
||||
"[LOG] 🌤️ Warming up the AsyncWebCrawler\n",
|
||||
"[LOG] 🌞 AsyncWebCrawler is ready to crawl\n",
|
||||
"[LOG] 🚀 Crawling done for https://www.anthropic.com/pricing, success: True, time taken: 2.93 seconds\n",
|
||||
"[LOG] 🚀 Content extracted for https://www.anthropic.com/pricing, success: True, time taken: 0.06 seconds\n",
|
||||
"[LOG] 🔥 Extracting semantic blocks for https://www.anthropic.com/pricing, Strategy: LLMExtractionStrategy\n",
|
||||
@@ -323,8 +323,8 @@
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[LOG] 🚀 Initializing LocalSeleniumCrawlerStrategy\n",
|
||||
"[LOG] 🌤️ Warming up the WebCrawler\n",
|
||||
"[LOG] 🌞 WebCrawler is ready to crawl\n",
|
||||
"[LOG] 🌤️ Warming up the AsyncWebCrawler\n",
|
||||
"[LOG] 🌞 AsyncWebCrawler is ready to crawl\n",
|
||||
"[LOG] 🚀 Crawling done for https://cohere.com/pricing, success: True, time taken: 6.34 seconds\n",
|
||||
"[LOG] 🚀 Content extracted for https://cohere.com/pricing, success: True, time taken: 0.14 seconds\n",
|
||||
"[LOG] 🔥 Extracting semantic blocks for https://cohere.com/pricing, Strategy: LLMExtractionStrategy\n",
|
||||
|
||||
@@ -0,0 +1,640 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Secondary Market Research Agents\n",
|
||||
"[](https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/cookbooks/notebooks/secondary_market_research_agents.ipynb)\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to create a comprehensive secondary market research system using PraisonAI agents. The system generates detailed reports with customizable parameters for company, geography, and industry analysis."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Features\n",
|
||||
"\n",
|
||||
"- **Multi-agent Research System**: Specialized agents for different research areas\n",
|
||||
"- **Customizable Parameters**: Company, geography, industry, and research sections\n",
|
||||
"- **Comprehensive Reports**: Market overview, competitive analysis, financial performance, growth opportunities, and risk assessment\n",
|
||||
"- **FastAPI Integration**: Ready for production deployment\n",
|
||||
"- **Structured Output**: Professional report format suitable for business decision-making"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Install required dependencies\n",
|
||||
"%pip install praisonai[crewai] > /dev/null\n",
|
||||
"%pip install fastapi uvicorn > /dev/null\n",
|
||||
"%pip install requests > /dev/null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup API Key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from google.colab import userdata\n",
|
||||
"\n",
|
||||
"# Set up OpenAI API key (add to Google Colab secrets)\n",
|
||||
"os.environ[\"OPENAI_API_KEY\"] = userdata.get('OPENAI_API_KEY') or \"ENTER OPENAI_API_KEY HERE\"\n",
|
||||
"os.environ[\"OPENAI_MODEL_NAME\"] = \"gpt-4o-mini\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Method 1: Using YAML Configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# YAML configuration for market research agents\n",
|
||||
"agent_yaml = \"\"\"\n",
|
||||
"framework: \"crewai\"\n",
|
||||
"topic: \"Secondary Market Research Analysis\"\n",
|
||||
"\n",
|
||||
"# Configuration variables\n",
|
||||
"variables:\n",
|
||||
" company: \"Tesla\"\n",
|
||||
" geography: \"North America\" \n",
|
||||
" industry: \"Electric Vehicles\"\n",
|
||||
"\n",
|
||||
"roles:\n",
|
||||
" market_overview_specialist:\n",
|
||||
" role: \"Market Overview Specialist\" \n",
|
||||
" backstory: |\n",
|
||||
" You are a seasoned market research analyst with 10+ years of experience \n",
|
||||
" in analyzing market dynamics, trends, and growth patterns.\n",
|
||||
" goal: |\n",
|
||||
" Analyze the {industry} market in {geography} to provide comprehensive \n",
|
||||
" market overview including size, trends, and growth drivers.\n",
|
||||
" verbose: true\n",
|
||||
" allow_delegation: false\n",
|
||||
" tools:\n",
|
||||
" - \"InternetSearchTool\"\n",
|
||||
" tasks:\n",
|
||||
" market_overview_research:\n",
|
||||
" description: |\n",
|
||||
" Conduct comprehensive market overview research for {industry} in {geography}:\n",
|
||||
" 1. Market size and valuation analysis\n",
|
||||
" 2. Key growth trends and patterns\n",
|
||||
" 3. Primary market drivers\n",
|
||||
" 4. Market segmentation\n",
|
||||
" expected_output: |\n",
|
||||
" A comprehensive market overview report with quantitative data and insights.\n",
|
||||
"\n",
|
||||
" competitive_intelligence_analyst:\n",
|
||||
" role: \"Competitive Intelligence Analyst\"\n",
|
||||
" backstory: |\n",
|
||||
" You are a competitive intelligence expert specializing in analyzing \n",
|
||||
" competitive landscapes and market positioning.\n",
|
||||
" goal: |\n",
|
||||
" Analyze {company}'s competitive landscape in the {industry} market.\n",
|
||||
" verbose: true\n",
|
||||
" allow_delegation: false\n",
|
||||
" tools:\n",
|
||||
" - \"InternetSearchTool\"\n",
|
||||
" tasks:\n",
|
||||
" competitive_analysis:\n",
|
||||
" description: |\n",
|
||||
" Conduct detailed competitive analysis for {company}:\n",
|
||||
" 1. Identify top 5-7 competitors\n",
|
||||
" 2. Market share analysis\n",
|
||||
" 3. Competitive positioning\n",
|
||||
" 4. Strengths and weaknesses\n",
|
||||
" expected_output: |\n",
|
||||
" A comprehensive competitive intelligence report.\n",
|
||||
"\n",
|
||||
" financial_performance_analyst:\n",
|
||||
" role: \"Financial Performance Analyst\"\n",
|
||||
" backstory: |\n",
|
||||
" You are a financial analyst with expertise in corporate finance \n",
|
||||
" and industry benchmarking.\n",
|
||||
" goal: |\n",
|
||||
" Analyze {company}'s financial performance and industry benchmarks.\n",
|
||||
" verbose: true\n",
|
||||
" allow_delegation: false\n",
|
||||
" tools:\n",
|
||||
" - \"InternetSearchTool\"\n",
|
||||
" tasks:\n",
|
||||
" financial_analysis:\n",
|
||||
" description: |\n",
|
||||
" Conduct financial performance analysis for {company}:\n",
|
||||
" 1. Revenue trends and growth\n",
|
||||
" 2. Profitability metrics\n",
|
||||
" 3. Financial ratios\n",
|
||||
" 4. Industry benchmarking\n",
|
||||
" expected_output: |\n",
|
||||
" A detailed financial performance analysis report.\n",
|
||||
"\n",
|
||||
" research_report_synthesizer:\n",
|
||||
" role: \"Research Report Synthesizer\"\n",
|
||||
" backstory: |\n",
|
||||
" You are an expert business report writer specializing in \n",
|
||||
" synthesizing complex research into actionable insights.\n",
|
||||
" goal: |\n",
|
||||
" Create a comprehensive secondary market research report for {company}.\n",
|
||||
" verbose: true\n",
|
||||
" allow_delegation: false\n",
|
||||
" tasks:\n",
|
||||
" report_synthesis:\n",
|
||||
" description: |\n",
|
||||
" Synthesize all research findings into a comprehensive report:\n",
|
||||
" 1. Executive Summary\n",
|
||||
" 2. Market Overview\n",
|
||||
" 3. Competitive Analysis\n",
|
||||
" 4. Financial Performance\n",
|
||||
" 5. Conclusions and Recommendations\n",
|
||||
" expected_output: |\n",
|
||||
" A complete, professionally formatted secondary market research report.\n",
|
||||
" context:\n",
|
||||
" - \"market_overview_research\"\n",
|
||||
" - \"competitive_analysis\" \n",
|
||||
" - \"financial_analysis\"\n",
|
||||
"\n",
|
||||
"dependencies:\n",
|
||||
" - market_overview_specialist\n",
|
||||
" - competitive_intelligence_analyst\n",
|
||||
" - financial_performance_analyst\n",
|
||||
" - research_report_synthesizer\n",
|
||||
"\"\"\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import required libraries\n",
|
||||
"from praisonai import PraisonAI\n",
|
||||
"from praisonai_tools import BaseTool\n",
|
||||
"from duckduckgo_search import DDGS\n",
|
||||
"import requests\n",
|
||||
"from bs4 import BeautifulSoup\n",
|
||||
"\n",
|
||||
"# Create internet search tool\n",
|
||||
"class InternetSearchTool(BaseTool):\n",
|
||||
" name: str = \"InternetSearchTool\"\n",
|
||||
" description: str = \"Search Internet for relevant information\"\n",
|
||||
"\n",
|
||||
" def _run(self, query: str):\n",
|
||||
" ddgs = DDGS()\n",
|
||||
" results = ddgs.text(keywords=query, region='wt-wt', safesearch='moderate', max_results=5)\n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
"# Create PraisonAI instance\n",
|
||||
"praisonai = PraisonAI(agent_yaml=agent_yaml, tools=[InternetSearchTool])\n",
|
||||
"\n",
|
||||
"# Run the market research\n",
|
||||
"print(\"🔍 Starting Secondary Market Research for Tesla...\")\n",
|
||||
"result = praisonai.run()\n",
|
||||
"\n",
|
||||
"print(\"\\n📋 Market Research Complete!\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "10",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Method 2: Using Python API with Custom Parameters"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "11",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import the market research system\n",
|
||||
"from praisonaiagents import Agent, Task, PraisonAIAgents, Tools\n",
|
||||
"from typing import Dict, List, Any\n",
|
||||
"import asyncio\n",
|
||||
"from datetime import datetime\n",
|
||||
"\n",
|
||||
"class MarketResearchConfig:\n",
|
||||
" \"\"\"Configuration for market research parameters\"\"\"\n",
|
||||
" \n",
|
||||
" def __init__(self, company: str, geography: str, industry: str, sections: List[str]):\n",
|
||||
" self.company = company\n",
|
||||
" self.geography = geography\n",
|
||||
" self.industry = industry\n",
|
||||
" self.sections = sections\n",
|
||||
" self.timestamp = datetime.now().isoformat()\n",
|
||||
"\n",
|
||||
"def create_market_research_agents(config: MarketResearchConfig):\n",
|
||||
" \"\"\"Create specialized market research agents\"\"\"\n",
|
||||
" \n",
|
||||
" agents = {}\n",
|
||||
" \n",
|
||||
" if \"market_overview\" in config.sections:\n",
|
||||
" agents[\"market_overview\"] = Agent(\n",
|
||||
" name=\"Market Overview Specialist\",\n",
|
||||
" role=\"Market Analysis Expert\",\n",
|
||||
" goal=f\"Analyze the {config.industry} market in {config.geography}\",\n",
|
||||
" instructions=f\"\"\"\n",
|
||||
" Analyze market size, trends, growth drivers for {config.industry} in {config.geography}.\n",
|
||||
" Provide quantitative data and cite reliable sources.\n",
|
||||
" \"\"\",\n",
|
||||
" tools=[Tools.internet_search],\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" if \"competitive_analysis\" in config.sections:\n",
|
||||
" agents[\"competitive\"] = Agent(\n",
|
||||
" name=\"Competitive Intelligence Analyst\",\n",
|
||||
" role=\"Competition Research Expert\", \n",
|
||||
" goal=f\"Analyze {config.company}'s competitive landscape\",\n",
|
||||
" instructions=f\"\"\"\n",
|
||||
" Analyze {config.company}'s competitive position in {config.industry}.\n",
|
||||
" Identify key competitors, market share, and competitive advantages.\n",
|
||||
" \"\"\",\n",
|
||||
" tools=[Tools.internet_search],\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" if \"financial_performance\" in config.sections:\n",
|
||||
" agents[\"financial\"] = Agent(\n",
|
||||
" name=\"Financial Performance Analyst\", \n",
|
||||
" role=\"Financial Research Expert\",\n",
|
||||
" goal=f\"Analyze {config.company}'s financial performance\",\n",
|
||||
" instructions=f\"\"\"\n",
|
||||
" Research {config.company}'s financial performance, revenue trends,\n",
|
||||
" profitability, and compare with industry benchmarks.\n",
|
||||
" \"\"\",\n",
|
||||
" tools=[Tools.internet_search],\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" if \"growth_opportunities\" in config.sections:\n",
|
||||
" agents[\"growth\"] = Agent(\n",
|
||||
" name=\"Growth Opportunities Researcher\",\n",
|
||||
" role=\"Strategic Growth Expert\",\n",
|
||||
" goal=f\"Identify growth opportunities for {config.company}\",\n",
|
||||
" instructions=f\"\"\"\n",
|
||||
" Identify emerging opportunities, market gaps, expansion possibilities\n",
|
||||
" for {config.company} in {config.industry}.\n",
|
||||
" \"\"\",\n",
|
||||
" tools=[Tools.internet_search],\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Always include synthesizer\n",
|
||||
" agents[\"synthesizer\"] = Agent(\n",
|
||||
" name=\"Research Report Synthesizer\",\n",
|
||||
" role=\"Report Writing Expert\",\n",
|
||||
" goal=\"Synthesize research into comprehensive report\",\n",
|
||||
" instructions=f\"\"\"\n",
|
||||
" Create a professional secondary market research report for {config.company} \n",
|
||||
" in {config.industry} with clear structure and actionable recommendations.\n",
|
||||
" \"\"\",\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" return agents\n",
|
||||
"\n",
|
||||
"def create_research_tasks(agents: Dict[str, Agent], config: MarketResearchConfig):\n",
|
||||
" \"\"\"Create research tasks for the agents\"\"\"\n",
|
||||
" \n",
|
||||
" tasks = []\n",
|
||||
" \n",
|
||||
" if \"market_overview\" in config.sections and \"market_overview\" in agents:\n",
|
||||
" market_task = Task(\n",
|
||||
" name=\"market_overview_research\",\n",
|
||||
" description=f\"Research {config.industry} market overview in {config.geography}\",\n",
|
||||
" expected_output=\"Market overview analysis with key metrics and trends\",\n",
|
||||
" agent=agents[\"market_overview\"]\n",
|
||||
" )\n",
|
||||
" tasks.append(market_task)\n",
|
||||
" \n",
|
||||
" if \"competitive_analysis\" in config.sections and \"competitive\" in agents:\n",
|
||||
" competitive_task = Task(\n",
|
||||
" name=\"competitive_analysis\",\n",
|
||||
" description=f\"Analyze competitive landscape for {config.company}\",\n",
|
||||
" expected_output=\"Competitive analysis with key competitors and positioning\",\n",
|
||||
" agent=agents[\"competitive\"]\n",
|
||||
" )\n",
|
||||
" tasks.append(competitive_task)\n",
|
||||
" \n",
|
||||
" if \"financial_performance\" in config.sections and \"financial\" in agents:\n",
|
||||
" financial_task = Task(\n",
|
||||
" name=\"financial_analysis\",\n",
|
||||
" description=f\"Analyze {config.company}'s financial performance\",\n",
|
||||
" expected_output=\"Financial performance analysis with key metrics\",\n",
|
||||
" agent=agents[\"financial\"]\n",
|
||||
" )\n",
|
||||
" tasks.append(financial_task)\n",
|
||||
" \n",
|
||||
" if \"growth_opportunities\" in config.sections and \"growth\" in agents:\n",
|
||||
" growth_task = Task(\n",
|
||||
" name=\"growth_opportunities\",\n",
|
||||
" description=f\"Identify growth opportunities for {config.company}\",\n",
|
||||
" expected_output=\"Growth opportunities with strategic recommendations\",\n",
|
||||
" agent=agents[\"growth\"]\n",
|
||||
" )\n",
|
||||
" tasks.append(growth_task)\n",
|
||||
" \n",
|
||||
" # Synthesis task\n",
|
||||
" synthesis_task = Task(\n",
|
||||
" name=\"synthesis\",\n",
|
||||
" description=\"Synthesize all findings into comprehensive report\",\n",
|
||||
" expected_output=\"Complete secondary market research report\",\n",
|
||||
" agent=agents[\"synthesizer\"],\n",
|
||||
" context=tasks\n",
|
||||
" )\n",
|
||||
" tasks.append(synthesis_task)\n",
|
||||
" \n",
|
||||
" return tasks\n",
|
||||
"\n",
|
||||
"async def run_market_research(config: MarketResearchConfig):\n",
|
||||
" \"\"\"Run the complete market research workflow\"\"\"\n",
|
||||
" \n",
|
||||
" print(f\"🔍 Starting Market Research for {config.company}\")\n",
|
||||
" print(f\"📍 Geography: {config.geography}\")\n",
|
||||
" print(f\"🏭 Industry: {config.industry}\")\n",
|
||||
" print(f\"📊 Sections: {', '.join(config.sections)}\")\n",
|
||||
" print(\"=\" * 60)\n",
|
||||
" \n",
|
||||
" # Create agents and tasks\n",
|
||||
" agents = create_market_research_agents(config)\n",
|
||||
" tasks = create_research_tasks(agents, config)\n",
|
||||
" \n",
|
||||
" # Create workflow\n",
|
||||
" workflow = PraisonAIAgents(\n",
|
||||
" agents=list(agents.values()),\n",
|
||||
" tasks=tasks,\n",
|
||||
" process=\"workflow\",\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Execute research\n",
|
||||
" results = await workflow.astart()\n",
|
||||
" \n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
"# Example with custom parameters\n",
|
||||
"custom_config = MarketResearchConfig(\n",
|
||||
" company=\"Apple\",\n",
|
||||
" geography=\"Global\", \n",
|
||||
" industry=\"Consumer Electronics\",\n",
|
||||
" sections=[\"market_overview\", \"competitive_analysis\", \"financial_performance\"]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Run the research\n",
|
||||
"print(\"🚀 Running Custom Market Research...\")\n",
|
||||
"custom_results = await run_market_research(custom_config)\n",
|
||||
"\n",
|
||||
"print(\"\\n📋 Research Complete!\")\n",
|
||||
"print(\"=\" * 60)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "12",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Method 3: FastAPI Integration Example"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "13",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Example of using the FastAPI endpoint\n",
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"import time\n",
|
||||
"\n",
|
||||
"# Note: This requires the FastAPI server to be running\n",
|
||||
"# Run: uvicorn secondary-market-research-api:app --reload --port 8000\n",
|
||||
"\n",
|
||||
"API_BASE_URL = \"http://localhost:8000\" # Change if running elsewhere\n",
|
||||
"\n",
|
||||
"def generate_report_via_api(company, geography, industry, sections):\n",
|
||||
" \"\"\"Generate market research report via API\"\"\"\n",
|
||||
" \n",
|
||||
" # Prepare request\n",
|
||||
" request_data = {\n",
|
||||
" \"company\": company,\n",
|
||||
" \"geography\": geography,\n",
|
||||
" \"industry\": industry,\n",
|
||||
" \"sections\": sections,\n",
|
||||
" \"format\": \"json\"\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" print(f\"📤 Submitting research request for {company}...\")\n",
|
||||
" \n",
|
||||
" # Submit research request\n",
|
||||
" response = requests.post(f\"{API_BASE_URL}/research/generate\", json=request_data)\n",
|
||||
" \n",
|
||||
" if response.status_code == 200:\n",
|
||||
" job_data = response.json()\n",
|
||||
" job_id = job_data[\"job_id\"]\n",
|
||||
" print(f\"✅ Job submitted successfully. Job ID: {job_id}\")\n",
|
||||
" \n",
|
||||
" # Poll for completion\n",
|
||||
" print(\"⏳ Waiting for completion...\")\n",
|
||||
" while True:\n",
|
||||
" status_response = requests.get(f\"{API_BASE_URL}/research/status/{job_id}\")\n",
|
||||
" if status_response.status_code == 200:\n",
|
||||
" status_data = status_response.json()\n",
|
||||
" print(f\"📊 Progress: {status_data['progress']}% - {status_data['message']}\")\n",
|
||||
" \n",
|
||||
" if status_data[\"status\"] == \"completed\":\n",
|
||||
" print(\"🎉 Research completed!\")\n",
|
||||
" \n",
|
||||
" # Download report\n",
|
||||
" report_response = requests.get(f\"{API_BASE_URL}/research/reports/{job_id}\")\n",
|
||||
" if report_response.status_code == 200:\n",
|
||||
" return report_response.json()\n",
|
||||
" else:\n",
|
||||
" print(\"❌ Failed to download report\")\n",
|
||||
" return None\n",
|
||||
" \n",
|
||||
" elif status_data[\"status\"] == \"failed\":\n",
|
||||
" print(f\"❌ Research failed: {status_data.get('error', 'Unknown error')}\")\n",
|
||||
" return None\n",
|
||||
" \n",
|
||||
" time.sleep(5) # Wait 5 seconds before checking again\n",
|
||||
" else:\n",
|
||||
" print(\"❌ Failed to check status\")\n",
|
||||
" return None\n",
|
||||
" else:\n",
|
||||
" print(f\"❌ Failed to submit request: {response.status_code}\")\n",
|
||||
" return None\n",
|
||||
"\n",
|
||||
"# Example API usage (uncomment to run with API server)\n",
|
||||
"# report = generate_report_via_api(\n",
|
||||
"# company=\"Microsoft\",\n",
|
||||
"# geography=\"Global\",\n",
|
||||
"# industry=\"Cloud Computing\",\n",
|
||||
"# sections=[\"market_overview\", \"competitive_analysis\"]\n",
|
||||
"# )\n",
|
||||
"# \n",
|
||||
"# if report:\n",
|
||||
"# print(\"\\n📋 Generated Report:\")\n",
|
||||
"# print(json.dumps(report, indent=2)[:1000] + \"...\")\n",
|
||||
"\n",
|
||||
"print(\"💡 API integration example ready. Start the FastAPI server to test.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "14",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Customization Examples"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "15",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Example configurations for different industries and regions\n",
|
||||
"\n",
|
||||
"research_examples = {\n",
|
||||
" \"tech_startup\": {\n",
|
||||
" \"company\": \"OpenAI\",\n",
|
||||
" \"geography\": \"United States\",\n",
|
||||
" \"industry\": \"Artificial Intelligence\",\n",
|
||||
" \"sections\": [\"market_overview\", \"competitive_analysis\", \"growth_opportunities\"]\n",
|
||||
" },\n",
|
||||
" \"automotive\": {\n",
|
||||
" \"company\": \"BMW\",\n",
|
||||
" \"geography\": \"Europe\",\n",
|
||||
" \"industry\": \"Luxury Automobiles\",\n",
|
||||
" \"sections\": [\"market_overview\", \"competitive_analysis\", \"financial_performance\", \"risk_assessment\"]\n",
|
||||
" },\n",
|
||||
" \"healthcare\": {\n",
|
||||
" \"company\": \"Pfizer\",\n",
|
||||
" \"geography\": \"Global\",\n",
|
||||
" \"industry\": \"Pharmaceuticals\",\n",
|
||||
" \"sections\": [\"market_overview\", \"competitive_analysis\", \"growth_opportunities\", \"risk_assessment\"]\n",
|
||||
" },\n",
|
||||
" \"fintech\": {\n",
|
||||
" \"company\": \"Square\",\n",
|
||||
" \"geography\": \"North America\",\n",
|
||||
" \"industry\": \"Financial Technology\",\n",
|
||||
" \"sections\": [\"market_overview\", \"competitive_analysis\", \"financial_performance\", \"growth_opportunities\"]\n",
|
||||
" },\n",
|
||||
" \"retail\": {\n",
|
||||
" \"company\": \"Alibaba\",\n",
|
||||
" \"geography\": \"Asia Pacific\",\n",
|
||||
" \"industry\": \"E-commerce\",\n",
|
||||
" \"sections\": [\"market_overview\", \"competitive_analysis\", \"financial_performance\", \"growth_opportunities\", \"risk_assessment\"]\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print(\"📚 Research Configuration Examples:\")\n",
|
||||
"print(\"=\" * 50)\n",
|
||||
"\n",
|
||||
"for example_name, config in research_examples.items():\n",
|
||||
" print(f\"\\n🔍 {example_name.upper()}:\")\n",
|
||||
" print(f\" Company: {config['company']}\")\n",
|
||||
" print(f\" Geography: {config['geography']}\")\n",
|
||||
" print(f\" Industry: {config['industry']}\")\n",
|
||||
" print(f\" Sections: {', '.join(config['sections'])}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n💡 Use any of these configurations as templates for your research!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "16",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"This notebook demonstrates three different ways to use the Secondary Market Research Agent system:\n",
|
||||
"\n",
|
||||
"1. **YAML Configuration**: Easy to customize and modify research parameters\n",
|
||||
"2. **Python API**: Programmatic control with custom configurations\n",
|
||||
"3. **FastAPI Integration**: Production-ready REST API for web applications\n",
|
||||
"\n",
|
||||
"### Key Features:\n",
|
||||
"- **Customizable Research Sections**: Choose from market overview, competitive analysis, financial performance, growth opportunities, and risk assessment\n",
|
||||
"- **Geographic Flexibility**: Research any geographic region\n",
|
||||
"- **Industry Agnostic**: Works across different industries and sectors\n",
|
||||
"- **Professional Reports**: Generate business-ready research reports\n",
|
||||
"- **Scalable Architecture**: Multi-agent system that can be extended\n",
|
||||
"\n",
|
||||
"### Use Cases:\n",
|
||||
"- Market entry analysis\n",
|
||||
"- Competitive intelligence\n",
|
||||
"- Investment research\n",
|
||||
"- Strategic planning\n",
|
||||
"- Due diligence\n",
|
||||
"\n",
|
||||
"The system provides a comprehensive foundation for secondary market research that can be customized and extended based on specific business needs."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.8.5"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
framework: "crewai"
|
||||
topic: "Secondary Market Research Analysis with Customizable Parameters"
|
||||
|
||||
# Configuration variables for customization
|
||||
variables:
|
||||
company: "Tesla"
|
||||
geography: "North America"
|
||||
industry: "Electric Vehicles"
|
||||
research_focus: "comprehensive market analysis"
|
||||
|
||||
roles:
|
||||
market_overview_specialist:
|
||||
role: "Market Overview Specialist"
|
||||
backstory: |
|
||||
You are a seasoned market research analyst with 10+ years of experience
|
||||
in analyzing market dynamics, trends, and growth patterns across various
|
||||
industries. You excel at synthesizing complex market data into clear,
|
||||
actionable insights for business decision-makers.
|
||||
goal: |
|
||||
Analyze the {industry} market in {geography} to provide comprehensive
|
||||
market overview including size, trends, growth drivers, and future projections.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.3
|
||||
tools:
|
||||
- "InternetSearchTool"
|
||||
tasks:
|
||||
market_overview_research:
|
||||
description: |
|
||||
Conduct comprehensive market overview research for {industry} in {geography}:
|
||||
|
||||
1. Market Size & Valuation:
|
||||
- Current market size and historical growth
|
||||
- Market valuation and revenue projections
|
||||
- Geographic market distribution
|
||||
|
||||
2. Market Trends:
|
||||
- Key growth trends and patterns
|
||||
- Emerging market developments
|
||||
- Technology adoption rates
|
||||
|
||||
3. Market Drivers:
|
||||
- Primary growth drivers and catalysts
|
||||
- Consumer behavior shifts
|
||||
- Regulatory and policy impacts
|
||||
|
||||
4. Market Segments:
|
||||
- Key market segments and sub-sectors
|
||||
- Segment performance and potential
|
||||
- Customer demographics and preferences
|
||||
|
||||
Research should include quantitative data, statistics, and cite reliable sources.
|
||||
expected_output: |
|
||||
A comprehensive market overview report containing:
|
||||
- Executive summary of market status
|
||||
- Detailed market size and growth analysis
|
||||
- Key trends and drivers identification
|
||||
- Market segmentation analysis
|
||||
- Future market projections and opportunities
|
||||
|
||||
Include relevant charts, statistics, and data sources.
|
||||
|
||||
competitive_intelligence_analyst:
|
||||
role: "Competitive Intelligence Analyst"
|
||||
backstory: |
|
||||
You are a competitive intelligence expert specializing in analyzing
|
||||
competitive landscapes, market positioning, and strategic intelligence.
|
||||
You have deep expertise in competitor analysis, market share assessment,
|
||||
and competitive strategy evaluation.
|
||||
goal: |
|
||||
Analyze {company}'s competitive landscape in the {industry} market
|
||||
within {geography} to identify key competitors, market positioning,
|
||||
and competitive dynamics.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.3
|
||||
tools:
|
||||
- "InternetSearchTool"
|
||||
tasks:
|
||||
competitive_analysis:
|
||||
description: |
|
||||
Conduct detailed competitive analysis for {company} in {industry}:
|
||||
|
||||
1. Competitor Identification:
|
||||
- Identify top 5-7 direct competitors
|
||||
- Include emerging and indirect competitors
|
||||
- Analyze competitor profiles and backgrounds
|
||||
|
||||
2. Market Share Analysis:
|
||||
- Market share distribution among competitors
|
||||
- {company}'s market position and ranking
|
||||
- Market concentration and fragmentation
|
||||
|
||||
3. Competitive Positioning:
|
||||
- Value proposition comparison
|
||||
- Product/service differentiation
|
||||
- Pricing strategies and positioning
|
||||
|
||||
4. Competitor Strengths & Weaknesses:
|
||||
- SWOT analysis for key competitors
|
||||
- Competitive advantages and disadvantages
|
||||
- Performance comparison metrics
|
||||
|
||||
5. Competitive Strategies:
|
||||
- Go-to-market strategies
|
||||
- Innovation and R&D approaches
|
||||
- Partnership and acquisition strategies
|
||||
expected_output: |
|
||||
A comprehensive competitive intelligence report including:
|
||||
- Competitive landscape overview
|
||||
- Detailed competitor profiles (top 5-7)
|
||||
- Market share analysis and positioning
|
||||
- Competitive strengths/weaknesses matrix
|
||||
- Strategic implications and recommendations
|
||||
|
||||
financial_performance_analyst:
|
||||
role: "Financial Performance Analyst"
|
||||
backstory: |
|
||||
You are a financial analyst with expertise in corporate finance,
|
||||
financial statement analysis, and industry benchmarking. You specialize
|
||||
in analyzing financial performance metrics, trends, and comparative
|
||||
analysis within specific industry contexts.
|
||||
goal: |
|
||||
Analyze {company}'s financial performance, key metrics, and compare
|
||||
with industry benchmarks to assess financial health and performance trends.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.3
|
||||
tools:
|
||||
- "InternetSearchTool"
|
||||
tasks:
|
||||
financial_analysis:
|
||||
description: |
|
||||
Conduct comprehensive financial performance analysis for {company}:
|
||||
|
||||
1. Revenue Analysis:
|
||||
- Revenue trends over last 3-5 years
|
||||
- Revenue growth rates and patterns
|
||||
- Revenue by segment/geography
|
||||
|
||||
2. Profitability Metrics:
|
||||
- Gross, operating, and net profit margins
|
||||
- EBITDA and EBITDA margins
|
||||
- Return on assets (ROA) and equity (ROE)
|
||||
|
||||
3. Financial Ratios:
|
||||
- Liquidity ratios (current, quick)
|
||||
- Leverage ratios (debt-to-equity, interest coverage)
|
||||
- Efficiency ratios (asset turnover, inventory turnover)
|
||||
|
||||
4. Industry Benchmarking:
|
||||
- Compare key metrics with industry averages
|
||||
- Position against top competitors
|
||||
- Identify performance gaps and advantages
|
||||
|
||||
5. Financial Health Assessment:
|
||||
- Cash flow analysis
|
||||
- Debt levels and structure
|
||||
- Financial stability indicators
|
||||
expected_output: |
|
||||
A detailed financial performance report containing:
|
||||
- Financial performance summary and trends
|
||||
- Key financial metrics and ratios analysis
|
||||
- Industry benchmark comparison
|
||||
- Financial strengths and areas for improvement
|
||||
- Financial health assessment and outlook
|
||||
|
||||
growth_opportunities_researcher:
|
||||
role: "Growth Opportunities Researcher"
|
||||
backstory: |
|
||||
You are a strategic business analyst specializing in identifying growth
|
||||
opportunities, market expansion strategies, and emerging business models.
|
||||
You excel at spotting trends, gaps, and untapped potential in markets
|
||||
and industries.
|
||||
goal: |
|
||||
Identify and analyze growth opportunities for {company} in {industry}
|
||||
within {geography}, including market expansion, innovation, and
|
||||
strategic development possibilities.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.4
|
||||
tools:
|
||||
- "InternetSearchTool"
|
||||
tasks:
|
||||
growth_opportunities_analysis:
|
||||
description: |
|
||||
Identify comprehensive growth opportunities for {company}:
|
||||
|
||||
1. Market Expansion:
|
||||
- Geographic expansion opportunities
|
||||
- New customer segments and demographics
|
||||
- Untapped market niches
|
||||
|
||||
2. Product/Service Innovation:
|
||||
- New product development opportunities
|
||||
- Service enhancement possibilities
|
||||
- Technology integration opportunities
|
||||
|
||||
3. Strategic Partnerships:
|
||||
- Potential partnership opportunities
|
||||
- Joint venture possibilities
|
||||
- Acquisition targets and synergies
|
||||
|
||||
4. Technology Advancement:
|
||||
- Emerging technology adoption
|
||||
- Digital transformation opportunities
|
||||
- Innovation and R&D investments
|
||||
|
||||
5. Market Trends Alignment:
|
||||
- Emerging industry trends to leverage
|
||||
- Consumer behavior shifts to capitalize on
|
||||
- Regulatory changes creating opportunities
|
||||
expected_output: |
|
||||
A strategic growth opportunities report including:
|
||||
- Prioritized growth opportunity matrix
|
||||
- Market expansion analysis and recommendations
|
||||
- Innovation and development opportunities
|
||||
- Strategic partnership possibilities
|
||||
- Implementation roadmap and timeline
|
||||
|
||||
risk_assessment_specialist:
|
||||
role: "Risk Assessment Specialist"
|
||||
backstory: |
|
||||
You are a risk management expert with extensive experience in identifying,
|
||||
analyzing, and assessing business risks across various industries. You
|
||||
specialize in market risks, competitive threats, regulatory challenges,
|
||||
and operational risk assessment.
|
||||
goal: |
|
||||
Assess and analyze potential risks, challenges, and threats facing
|
||||
{company} in the {industry} market within {geography}.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.3
|
||||
tools:
|
||||
- "InternetSearchTool"
|
||||
tasks:
|
||||
risk_assessment_analysis:
|
||||
description: |
|
||||
Conduct comprehensive risk assessment for {company}:
|
||||
|
||||
1. Competitive Risks:
|
||||
- New entrant threats
|
||||
- Competitor aggressive strategies
|
||||
- Market share erosion risks
|
||||
|
||||
2. Market Risks:
|
||||
- Market volatility and cyclicality
|
||||
- Demand fluctuation risks
|
||||
- Economic downturn impacts
|
||||
|
||||
3. Regulatory Risks:
|
||||
- Regulatory changes and compliance
|
||||
- Policy shifts and government intervention
|
||||
- Environmental and safety regulations
|
||||
|
||||
4. Operational Risks:
|
||||
- Supply chain vulnerabilities
|
||||
- Technology and cybersecurity risks
|
||||
- Talent and human capital risks
|
||||
|
||||
5. Financial Risks:
|
||||
- Currency and exchange rate risks
|
||||
- Interest rate and inflation impacts
|
||||
- Credit and liquidity risks
|
||||
|
||||
6. Technology Disruption:
|
||||
- Emerging technology threats
|
||||
- Digital transformation challenges
|
||||
- Innovation obsolescence risks
|
||||
expected_output: |
|
||||
A comprehensive risk assessment report including:
|
||||
- Risk identification and categorization
|
||||
- Risk impact and probability analysis
|
||||
- Risk mitigation strategies and recommendations
|
||||
- Risk monitoring and management framework
|
||||
- Priority risk action plan
|
||||
|
||||
research_report_synthesizer:
|
||||
role: "Research Report Synthesizer"
|
||||
backstory: |
|
||||
You are an expert business report writer and analyst with extensive
|
||||
experience in synthesizing complex research findings into clear,
|
||||
actionable business intelligence reports. You excel at creating
|
||||
comprehensive, well-structured reports for executive decision-making.
|
||||
goal: |
|
||||
Synthesize all research findings into a comprehensive, professional
|
||||
secondary market research report for {company} in {industry} that
|
||||
provides actionable insights for business decision-making.
|
||||
verbose: true
|
||||
allow_delegation: false
|
||||
llm:
|
||||
model: "gpt-4o-mini"
|
||||
temperature: 0.3
|
||||
tasks:
|
||||
report_synthesis:
|
||||
description: |
|
||||
Create a comprehensive secondary market research report synthesizing
|
||||
all research findings:
|
||||
|
||||
Report Structure:
|
||||
1. Executive Summary
|
||||
- Key findings overview
|
||||
- Critical insights and implications
|
||||
- Primary recommendations
|
||||
|
||||
2. Market Overview
|
||||
- Market size, trends, and dynamics
|
||||
- Growth drivers and opportunities
|
||||
- Market segmentation analysis
|
||||
|
||||
3. Competitive Landscape
|
||||
- Competitive positioning analysis
|
||||
- Key competitor profiles
|
||||
- Market share and dynamics
|
||||
|
||||
4. Financial Performance
|
||||
- Financial health assessment
|
||||
- Performance benchmarking
|
||||
- Financial trends and projections
|
||||
|
||||
5. Growth Opportunities
|
||||
- Strategic growth vectors
|
||||
- Market expansion possibilities
|
||||
- Innovation and development opportunities
|
||||
|
||||
6. Risk Assessment
|
||||
- Key risk factors and mitigation
|
||||
- Challenge identification
|
||||
- Risk management recommendations
|
||||
|
||||
7. Conclusions and Recommendations
|
||||
- Strategic recommendations
|
||||
- Action priorities
|
||||
- Implementation considerations
|
||||
|
||||
Use professional formatting with clear headings, bullet points, and data visualization suggestions.
|
||||
expected_output: |
|
||||
A complete, professionally formatted secondary market research report containing:
|
||||
- Executive summary with key insights
|
||||
- Detailed analysis across all research areas
|
||||
- Data-driven findings and conclusions
|
||||
- Strategic recommendations and action items
|
||||
- Professional formatting suitable for executive presentation
|
||||
|
||||
The report should be 8-12 pages in length with clear structure and actionable intelligence.
|
||||
context:
|
||||
- "market_overview_research"
|
||||
- "competitive_analysis"
|
||||
- "financial_analysis"
|
||||
- "growth_opportunities_analysis"
|
||||
- "risk_assessment_analysis"
|
||||
|
||||
dependencies:
|
||||
- market_overview_specialist
|
||||
- competitive_intelligence_analyst
|
||||
- financial_performance_analyst
|
||||
- growth_opportunities_researcher
|
||||
- risk_assessment_specialist
|
||||
- research_report_synthesizer
|
||||
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
Secondary Market Research FastAPI Application
|
||||
|
||||
This FastAPI application provides a REST API for generating customized
|
||||
secondary market research reports. Users can specify company, geography,
|
||||
industry, and research sections to generate tailored reports.
|
||||
|
||||
Features:
|
||||
- RESTful API endpoints for market research
|
||||
- Customizable research parameters
|
||||
- Async report generation
|
||||
- JSON and PDF report formats
|
||||
- Real-time progress tracking
|
||||
- Report history and caching
|
||||
|
||||
Usage:
|
||||
uvicorn secondary-market-research-api:app --reload --port 8000
|
||||
|
||||
Then visit: http://localhost:8000/docs for API documentation
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Import our market research system
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, Tools
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Secondary Market Research API",
|
||||
description="Generate customized secondary market research reports",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Create directories for storing reports
|
||||
REPORTS_DIR = Path("generated_reports")
|
||||
REPORTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# In-memory storage for job status (in production, use Redis or database)
|
||||
job_status = {}
|
||||
|
||||
# Pydantic models for request/response
|
||||
class MarketResearchRequest(BaseModel):
|
||||
"""Request model for market research generation"""
|
||||
company: str = Field(..., description="Company name to research", example="Tesla")
|
||||
geography: str = Field(..., description="Geographic region", example="North America")
|
||||
industry: str = Field(..., description="Industry sector", example="Electric Vehicles")
|
||||
sections: Optional[List[str]] = Field(
|
||||
default=["market_overview", "competitive_analysis", "financial_performance", "growth_opportunities", "risk_assessment"],
|
||||
description="Research sections to include",
|
||||
example=["market_overview", "competitive_analysis", "financial_performance"]
|
||||
)
|
||||
format: Optional[str] = Field(default="json", description="Output format: json or pdf", example="json")
|
||||
email: Optional[str] = Field(None, description="Email for notification when complete", example="[email protected]")
|
||||
|
||||
class MarketResearchResponse(BaseModel):
|
||||
"""Response model for market research request"""
|
||||
job_id: str = Field(..., description="Unique job identifier")
|
||||
status: str = Field(..., description="Job status: pending, running, completed, failed")
|
||||
message: str = Field(..., description="Status message")
|
||||
estimated_completion: Optional[str] = Field(None, description="Estimated completion time")
|
||||
|
||||
class JobStatusResponse(BaseModel):
|
||||
"""Response model for job status check"""
|
||||
job_id: str
|
||||
status: str
|
||||
progress: int = Field(..., description="Progress percentage (0-100)")
|
||||
message: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
result_url: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
class MarketResearchConfig:
|
||||
"""Configuration class for market research parameters"""
|
||||
|
||||
def __init__(self, company: str, geography: str, industry: str, sections: List[str]):
|
||||
self.company = company
|
||||
self.geography = geography
|
||||
self.industry = industry
|
||||
self.sections = sections
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
def create_market_research_agents(config: MarketResearchConfig):
|
||||
"""Create specialized market research agents"""
|
||||
|
||||
agents = {}
|
||||
|
||||
if "market_overview" in config.sections:
|
||||
agents["market_overview"] = Agent(
|
||||
name="Market Overview Specialist",
|
||||
role="Market Analysis Expert",
|
||||
goal=f"Analyze the {config.industry} market in {config.geography}",
|
||||
instructions=f"""
|
||||
Analyze market size, trends, growth drivers, and overall market dynamics for
|
||||
{config.industry} in {config.geography}. Provide quantitative data and cite sources.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
if "competitive_analysis" in config.sections:
|
||||
agents["competitive"] = Agent(
|
||||
name="Competitive Intelligence Analyst",
|
||||
role="Competition Research Expert",
|
||||
goal=f"Analyze {config.company}'s competitive landscape",
|
||||
instructions=f"""
|
||||
Analyze {config.company}'s competitive position in {config.industry} within {config.geography}.
|
||||
Identify key competitors, market share, and competitive advantages.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
if "financial_performance" in config.sections:
|
||||
agents["financial"] = Agent(
|
||||
name="Financial Performance Analyst",
|
||||
role="Financial Research Expert",
|
||||
goal=f"Analyze {config.company}'s financial performance",
|
||||
instructions=f"""
|
||||
Research {config.company}'s financial performance, revenue trends, profitability,
|
||||
and compare with industry benchmarks.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
if "growth_opportunities" in config.sections:
|
||||
agents["growth"] = Agent(
|
||||
name="Growth Opportunities Researcher",
|
||||
role="Strategic Growth Expert",
|
||||
goal=f"Identify growth opportunities for {config.company}",
|
||||
instructions=f"""
|
||||
Identify emerging opportunities, market gaps, expansion possibilities for {config.company}
|
||||
in {config.industry} within {config.geography}.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
if "risk_assessment" in config.sections:
|
||||
agents["risk"] = Agent(
|
||||
name="Risk Assessment Specialist",
|
||||
role="Risk Analysis Expert",
|
||||
goal=f"Assess risks for {config.company}",
|
||||
instructions=f"""
|
||||
Identify and analyze potential risks, challenges, and threats facing {config.company}
|
||||
in {config.geography}. Consider regulatory, competitive, and market risks.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Always include synthesizer
|
||||
agents["synthesizer"] = Agent(
|
||||
name="Research Report Synthesizer",
|
||||
role="Report Writing Expert",
|
||||
goal="Synthesize research into comprehensive report",
|
||||
instructions=f"""
|
||||
Create a professional secondary market research report for {config.company}
|
||||
in {config.industry} with clear structure, insights, and recommendations.
|
||||
""",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
return agents
|
||||
|
||||
def create_research_tasks(agents: Dict[str, Agent], config: MarketResearchConfig):
|
||||
"""Create research tasks for the agents"""
|
||||
|
||||
tasks = []
|
||||
|
||||
# Create tasks based on available sections
|
||||
if "market_overview" in config.sections and "market_overview" in agents:
|
||||
market_task = Task(
|
||||
name="market_overview_research",
|
||||
description=f"Research {config.industry} market overview in {config.geography}",
|
||||
expected_output="Market overview analysis with key metrics and trends",
|
||||
agent=agents["market_overview"]
|
||||
)
|
||||
tasks.append(market_task)
|
||||
|
||||
if "competitive_analysis" in config.sections and "competitive" in agents:
|
||||
competitive_task = Task(
|
||||
name="competitive_analysis",
|
||||
description=f"Analyze competitive landscape for {config.company}",
|
||||
expected_output="Competitive analysis with key competitors and positioning",
|
||||
agent=agents["competitive"]
|
||||
)
|
||||
tasks.append(competitive_task)
|
||||
|
||||
if "financial_performance" in config.sections and "financial" in agents:
|
||||
financial_task = Task(
|
||||
name="financial_analysis",
|
||||
description=f"Analyze {config.company}'s financial performance",
|
||||
expected_output="Financial performance analysis with key metrics",
|
||||
agent=agents["financial"]
|
||||
)
|
||||
tasks.append(financial_task)
|
||||
|
||||
if "growth_opportunities" in config.sections and "growth" in agents:
|
||||
growth_task = Task(
|
||||
name="growth_opportunities",
|
||||
description=f"Identify growth opportunities for {config.company}",
|
||||
expected_output="Growth opportunities analysis with strategic recommendations",
|
||||
agent=agents["growth"]
|
||||
)
|
||||
tasks.append(growth_task)
|
||||
|
||||
if "risk_assessment" in config.sections and "risk" in agents:
|
||||
risk_task = Task(
|
||||
name="risk_assessment",
|
||||
description=f"Assess risks for {config.company}",
|
||||
expected_output="Risk assessment with mitigation strategies",
|
||||
agent=agents["risk"]
|
||||
)
|
||||
tasks.append(risk_task)
|
||||
|
||||
# Synthesis task
|
||||
synthesis_task = Task(
|
||||
name="synthesis",
|
||||
description=f"Synthesize all findings into comprehensive report",
|
||||
expected_output="Complete secondary market research report",
|
||||
agent=agents["synthesizer"],
|
||||
context=tasks
|
||||
)
|
||||
tasks.append(synthesis_task)
|
||||
|
||||
return tasks
|
||||
|
||||
async def generate_market_research_report(job_id: str, config: MarketResearchConfig):
|
||||
"""Background task to generate market research report"""
|
||||
|
||||
try:
|
||||
# Update job status
|
||||
job_status[job_id]["status"] = "running"
|
||||
job_status[job_id]["progress"] = 10
|
||||
job_status[job_id]["message"] = "Creating research agents..."
|
||||
job_status[job_id]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
# Create agents and tasks
|
||||
agents = create_market_research_agents(config)
|
||||
tasks = create_research_tasks(agents, config)
|
||||
|
||||
job_status[job_id]["progress"] = 20
|
||||
job_status[job_id]["message"] = "Setting up research workflow..."
|
||||
|
||||
# Create workflow
|
||||
workflow = PraisonAIAgents(
|
||||
agents=list(agents.values()),
|
||||
tasks=tasks,
|
||||
process="workflow",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
job_status[job_id]["progress"] = 30
|
||||
job_status[job_id]["message"] = "Executing research workflow..."
|
||||
|
||||
# Execute research
|
||||
results = await workflow.astart()
|
||||
|
||||
job_status[job_id]["progress"] = 90
|
||||
job_status[job_id]["message"] = "Generating final report..."
|
||||
|
||||
# Prepare final report
|
||||
report_data = {
|
||||
"metadata": {
|
||||
"job_id": job_id,
|
||||
"company": config.company,
|
||||
"geography": config.geography,
|
||||
"industry": config.industry,
|
||||
"sections": config.sections,
|
||||
"generated_at": datetime.now().isoformat()
|
||||
},
|
||||
"executive_summary": "Executive summary would be generated here...",
|
||||
"research_findings": {}
|
||||
}
|
||||
|
||||
# Extract results from each task
|
||||
if "task_results" in results:
|
||||
for task_name, result in results["task_results"].items():
|
||||
if result:
|
||||
report_data["research_findings"][task_name] = {
|
||||
"content": result.raw,
|
||||
"agent": result.agent if hasattr(result, 'agent') else "Unknown"
|
||||
}
|
||||
|
||||
# Save report to file
|
||||
report_filename = f"market_research_{job_id}.json"
|
||||
report_path = REPORTS_DIR / report_filename
|
||||
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(report_data, f, indent=2, default=str)
|
||||
|
||||
# Update job status to completed
|
||||
job_status[job_id]["status"] = "completed"
|
||||
job_status[job_id]["progress"] = 100
|
||||
job_status[job_id]["message"] = "Report generation completed successfully"
|
||||
job_status[job_id]["result_url"] = f"/reports/{job_id}"
|
||||
job_status[job_id]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
except Exception as e:
|
||||
# Update job status to failed
|
||||
job_status[job_id]["status"] = "failed"
|
||||
job_status[job_id]["error"] = str(e)
|
||||
job_status[job_id]["message"] = f"Report generation failed: {str(e)}"
|
||||
job_status[job_id]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@app.get("/", summary="API Health Check")
|
||||
async def root():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"service": "Secondary Market Research API",
|
||||
"status": "active",
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@app.post("/research/generate", response_model=MarketResearchResponse, summary="Generate Market Research Report")
|
||||
async def generate_research(
|
||||
request: MarketResearchRequest,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""
|
||||
Generate a secondary market research report with customizable parameters.
|
||||
|
||||
This endpoint starts an async job to generate a comprehensive market research
|
||||
report based on the provided parameters. The report generation runs in the
|
||||
background and can be tracked using the returned job_id.
|
||||
"""
|
||||
|
||||
# Generate unique job ID
|
||||
job_id = str(uuid.uuid4())
|
||||
|
||||
# Initialize job status
|
||||
job_status[job_id] = {
|
||||
"job_id": job_id,
|
||||
"status": "pending",
|
||||
"progress": 0,
|
||||
"message": "Report generation queued",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"config": request.dict()
|
||||
}
|
||||
|
||||
# Create config object
|
||||
config = MarketResearchConfig(
|
||||
company=request.company,
|
||||
geography=request.geography,
|
||||
industry=request.industry,
|
||||
sections=request.sections
|
||||
)
|
||||
|
||||
# Start background task
|
||||
background_tasks.add_task(generate_market_research_report, job_id, config)
|
||||
|
||||
return MarketResearchResponse(
|
||||
job_id=job_id,
|
||||
status="pending",
|
||||
message="Market research report generation started",
|
||||
estimated_completion=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
@app.get("/research/status/{job_id}", response_model=JobStatusResponse, summary="Check Job Status")
|
||||
async def check_job_status(job_id: str):
|
||||
"""
|
||||
Check the status of a market research report generation job.
|
||||
|
||||
Returns the current status, progress percentage, and any results or errors.
|
||||
"""
|
||||
|
||||
if job_id not in job_status:
|
||||
raise HTTPException(status_code=404, detail="Job ID not found")
|
||||
|
||||
return JobStatusResponse(**job_status[job_id])
|
||||
|
||||
@app.get("/research/reports/{job_id}", summary="Download Research Report")
|
||||
async def download_report(job_id: str):
|
||||
"""
|
||||
Download the generated market research report.
|
||||
|
||||
Returns the complete report in JSON format once generation is complete.
|
||||
"""
|
||||
|
||||
if job_id not in job_status:
|
||||
raise HTTPException(status_code=404, detail="Job ID not found")
|
||||
|
||||
if job_status[job_id]["status"] != "completed":
|
||||
raise HTTPException(status_code=400, detail="Report not ready. Check job status first.")
|
||||
|
||||
report_path = REPORTS_DIR / f"market_research_{job_id}.json"
|
||||
|
||||
if not report_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Report file not found")
|
||||
|
||||
return FileResponse(
|
||||
path=report_path,
|
||||
media_type="application/json",
|
||||
filename=f"market_research_{job_id}.json"
|
||||
)
|
||||
|
||||
@app.get("/research/jobs", summary="List All Jobs")
|
||||
async def list_jobs():
|
||||
"""
|
||||
List all market research jobs and their current status.
|
||||
|
||||
Useful for monitoring and administrative purposes.
|
||||
"""
|
||||
|
||||
return {
|
||||
"total_jobs": len(job_status),
|
||||
"jobs": [
|
||||
{
|
||||
"job_id": job_id,
|
||||
"status": status["status"],
|
||||
"progress": status["progress"],
|
||||
"created_at": status["created_at"],
|
||||
"company": status["config"]["company"] if "config" in status else "Unknown"
|
||||
}
|
||||
for job_id, status in job_status.items()
|
||||
]
|
||||
}
|
||||
|
||||
@app.delete("/research/jobs/{job_id}", summary="Cancel Job")
|
||||
async def cancel_job(job_id: str):
|
||||
"""
|
||||
Cancel a pending or running market research job.
|
||||
"""
|
||||
|
||||
if job_id not in job_status:
|
||||
raise HTTPException(status_code=404, detail="Job ID not found")
|
||||
|
||||
if job_status[job_id]["status"] in ["completed", "failed"]:
|
||||
raise HTTPException(status_code=400, detail="Cannot cancel completed or failed job")
|
||||
|
||||
job_status[job_id]["status"] = "cancelled"
|
||||
job_status[job_id]["message"] = "Job cancelled by user"
|
||||
job_status[job_id]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
return {"message": "Job cancelled successfully", "job_id": job_id}
|
||||
|
||||
@app.get("/research/templates", summary="Get Research Templates")
|
||||
async def get_research_templates():
|
||||
"""
|
||||
Get predefined research templates for common use cases.
|
||||
"""
|
||||
|
||||
templates = {
|
||||
"technology_company": {
|
||||
"sections": ["market_overview", "competitive_analysis", "financial_performance", "growth_opportunities"],
|
||||
"example": {
|
||||
"company": "Tesla",
|
||||
"geography": "North America",
|
||||
"industry": "Electric Vehicles"
|
||||
}
|
||||
},
|
||||
"financial_services": {
|
||||
"sections": ["market_overview", "competitive_analysis", "risk_assessment", "financial_performance"],
|
||||
"example": {
|
||||
"company": "Goldman Sachs",
|
||||
"geography": "Global",
|
||||
"industry": "Investment Banking"
|
||||
}
|
||||
},
|
||||
"healthcare": {
|
||||
"sections": ["market_overview", "competitive_analysis", "growth_opportunities", "risk_assessment"],
|
||||
"example": {
|
||||
"company": "Johnson & Johnson",
|
||||
"geography": "North America",
|
||||
"industry": "Pharmaceuticals"
|
||||
}
|
||||
},
|
||||
"retail": {
|
||||
"sections": ["market_overview", "competitive_analysis", "financial_performance", "growth_opportunities"],
|
||||
"example": {
|
||||
"company": "Amazon",
|
||||
"geography": "Global",
|
||||
"industry": "E-commerce"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"templates": templates,
|
||||
"available_sections": [
|
||||
"market_overview",
|
||||
"competitive_analysis",
|
||||
"financial_performance",
|
||||
"growth_opportunities",
|
||||
"risk_assessment"
|
||||
]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("🚀 Starting Secondary Market Research API...")
|
||||
print("📚 API Documentation: http://localhost:8000/docs")
|
||||
print("🔄 ReDoc Documentation: http://localhost:8000/redoc")
|
||||
|
||||
uvicorn.run(
|
||||
"secondary-market-research-api:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True
|
||||
)
|
||||
@@ -0,0 +1,205 @@
|
||||
# CSV Processing with PraisonAI Agents
|
||||
|
||||
This document answers GitHub Issue #23: "How to pass in a CSV file" and provides multiple solutions for processing CSV files with PraisonAI agents.
|
||||
|
||||
## Quick Answer
|
||||
|
||||
**Yes, it's absolutely possible!** PraisonAI provides multiple ways to process CSV files with agents working through the list sequentially.
|
||||
|
||||
## The Simplest Solution
|
||||
|
||||
For the exact use case mentioned in the issue (CSV list of URLs processed sequentially):
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# Create your agent
|
||||
agent = Agent(
|
||||
name="URLProcessor",
|
||||
instructions="Analyze each URL from the CSV file"
|
||||
)
|
||||
|
||||
# Create a loop task
|
||||
task = Task(
|
||||
description="Process each URL from the CSV",
|
||||
agent=agent,
|
||||
task_type="loop", # Enable CSV loop processing
|
||||
input_file="your_urls.csv" # Your CSV file
|
||||
)
|
||||
|
||||
# Run the agents
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="workflow"
|
||||
)
|
||||
|
||||
agents.start()
|
||||
```
|
||||
|
||||
## CSV File Format
|
||||
|
||||
Your CSV file should have a simple structure. For URLs:
|
||||
|
||||
```csv
|
||||
url
|
||||
https://example.com
|
||||
https://github.com
|
||||
https://stackoverflow.com
|
||||
```
|
||||
|
||||
Or with additional columns:
|
||||
|
||||
```csv
|
||||
url,description
|
||||
https://example.com,Example website
|
||||
https://github.com,GitHub platform
|
||||
https://stackoverflow.com,Q&A site
|
||||
```
|
||||
|
||||
## Available Methods
|
||||
|
||||
### Method 1: Simple Loop Processing (Recommended)
|
||||
- **File**: `simple-csv-url-processor.py`
|
||||
- **Best for**: Beginners, simple use cases
|
||||
- **How it works**: Uses `task_type="loop"` to automatically process each CSV row
|
||||
|
||||
### Method 2: Manual CSV Processing
|
||||
- **File**: `csv-processing-agents.py` (Method 2)
|
||||
- **Best for**: When you need more control over validation and processing
|
||||
- **How it works**: Uses CSV tools (`read_csv`, `write_csv`) for manual processing
|
||||
|
||||
### Method 3: URL-Specific Processing
|
||||
- **File**: `csv-processing-agents.py` (Method 3)
|
||||
- **Best for**: Specifically processing URLs with detailed analysis
|
||||
- **How it works**: Combines CSV tools with URL analysis logic
|
||||
|
||||
### Method 4: Advanced Processing with Validation
|
||||
- **File**: `csv-processing-agents.py` (Method 4)
|
||||
- **Best for**: Production use cases requiring data validation
|
||||
- **How it works**: Validates data before processing, handles errors gracefully
|
||||
|
||||
## Built-in CSV Tools
|
||||
|
||||
PraisonAI includes powerful CSV tools:
|
||||
|
||||
```python
|
||||
from praisonaiagents.tools import read_csv, write_csv, merge_csv
|
||||
|
||||
# Read CSV files
|
||||
data = read_csv("input.csv")
|
||||
|
||||
# Write CSV files
|
||||
write_csv("output.csv", data)
|
||||
|
||||
# Merge multiple CSV files
|
||||
merge_csv(["file1.csv", "file2.csv"], "merged.csv")
|
||||
```
|
||||
|
||||
## Examples for Different Use Cases
|
||||
|
||||
### Processing URLs from CSV
|
||||
```python
|
||||
# Your CSV: urls.csv
|
||||
# url,priority
|
||||
# https://example.com,high
|
||||
# https://github.com,medium
|
||||
|
||||
agent = Agent(
|
||||
name="URLAnalyzer",
|
||||
tools=[read_csv, write_csv],
|
||||
instructions="Analyze URLs based on priority"
|
||||
)
|
||||
```
|
||||
|
||||
### Processing Any Text File
|
||||
```python
|
||||
# For text files, you can convert to CSV format first
|
||||
# Or process line by line using file tools
|
||||
```
|
||||
|
||||
### Batch Processing Tasks
|
||||
```python
|
||||
# Your CSV: tasks.csv
|
||||
# task
|
||||
# "Analyze competitor website"
|
||||
# "Research market trends"
|
||||
# "Generate report summary"
|
||||
|
||||
task = Task(
|
||||
description="Complete each task from the CSV",
|
||||
task_type="loop",
|
||||
input_file="tasks.csv"
|
||||
)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install PraisonAI Agents**:
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
```
|
||||
|
||||
2. **Set API Key**:
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
3. **Install pandas** (for advanced CSV operations):
|
||||
```bash
|
||||
pip install pandas
|
||||
```
|
||||
|
||||
## Running the Examples
|
||||
|
||||
1. **Simple URL Processing**:
|
||||
```bash
|
||||
python simple-csv-url-processor.py
|
||||
```
|
||||
|
||||
2. **Comprehensive Examples**:
|
||||
```bash
|
||||
python csv-processing-agents.py
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"CSV file not found"**
|
||||
- Make sure your CSV file is in the same directory as your script
|
||||
- Use absolute paths if needed: `/full/path/to/your/file.csv`
|
||||
|
||||
2. **"pandas not available"**
|
||||
- Install pandas: `pip install pandas`
|
||||
|
||||
3. **"API key not set"**
|
||||
- Set your OpenAI API key: `export OPENAI_API_KEY=your_key`
|
||||
|
||||
4. **Processing stops early**
|
||||
- Increase `max_iter` parameter in PraisonAIAgents
|
||||
- Check for errors in your CSV format
|
||||
|
||||
### CSV Format Tips:
|
||||
|
||||
- Use UTF-8 encoding
|
||||
- Ensure consistent column headers
|
||||
- Handle special characters properly
|
||||
- Keep URLs properly formatted (http:// or https://)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [CSV Tools Documentation](../../../docs/tools/csv_tools.mdx)
|
||||
- [Repetitive Agents](../../../docs/features/repetitive.mdx)
|
||||
- [Agent Examples](../../README.md)
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
1. Check the troubleshooting section above
|
||||
2. Review the example files for working implementations
|
||||
3. Open an issue on the GitHub repository with your specific use case
|
||||
|
||||
---
|
||||
|
||||
*This solution was generated in response to GitHub Issue #23*
|
||||
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
CSV Processing with PraisonAI Agents
|
||||
|
||||
This example demonstrates multiple ways to process CSV files with agents:
|
||||
1. Simple CSV Loop Processing (easiest)
|
||||
2. Manual CSV Processing with read_csv tools
|
||||
3. URL Processing from CSV files
|
||||
4. Custom CSV Processing with validation
|
||||
|
||||
Author: Generated for GitHub Issue #23
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import read_csv, write_csv
|
||||
|
||||
def create_sample_urls_csv():
|
||||
"""Create a sample CSV file with URLs for testing"""
|
||||
urls_data = [
|
||||
{"id": 1, "url": "https://example.com", "description": "Example website"},
|
||||
{"id": 2, "url": "https://github.com", "description": "GitHub platform"},
|
||||
{"id": 3, "url": "https://stackoverflow.com", "description": "Stack Overflow Q&A"},
|
||||
{"id": 4, "url": "https://python.org", "description": "Python official site"},
|
||||
{"id": 5, "url": "https://docs.python.org", "description": "Python documentation"}
|
||||
]
|
||||
|
||||
# Ensure the CSV file exists
|
||||
write_csv("sample_urls.csv", urls_data)
|
||||
print("✅ Created sample_urls.csv with 5 URLs")
|
||||
return "sample_urls.csv"
|
||||
|
||||
def create_sample_tasks_csv():
|
||||
"""Create a sample CSV file with tasks for loop processing"""
|
||||
tasks_data = [
|
||||
{"task": "Analyze the homepage of example.com and summarize its content"},
|
||||
{"task": "Check if github.com is accessible and describe its main features"},
|
||||
{"task": "Research what Stack Overflow is used for"},
|
||||
{"task": "Summarize the key features of Python programming language"},
|
||||
{"task": "Describe what you can find in Python documentation"}
|
||||
]
|
||||
|
||||
write_csv("sample_tasks.csv", tasks_data)
|
||||
print("✅ Created sample_tasks.csv with 5 tasks")
|
||||
return "sample_tasks.csv"
|
||||
|
||||
# ==============================================================================
|
||||
# METHOD 1: SIMPLE CSV LOOP PROCESSING (RECOMMENDED FOR BEGINNERS)
|
||||
# ==============================================================================
|
||||
|
||||
def method_1_simple_loop():
|
||||
"""
|
||||
The easiest way to process CSV files with PraisonAI agents.
|
||||
|
||||
This method automatically loops through each row in your CSV file.
|
||||
Each row becomes input for the agent to process.
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("METHOD 1: Simple CSV Loop Processing")
|
||||
print("="*60)
|
||||
|
||||
# Create sample CSV file
|
||||
csv_file = create_sample_tasks_csv()
|
||||
|
||||
# Create an agent that will process each row
|
||||
loop_agent = Agent(
|
||||
name="CSVLoopProcessor",
|
||||
role="CSV Task Processor",
|
||||
goal="Process each task from the CSV file efficiently",
|
||||
backstory="Expert at handling repetitive tasks from CSV data",
|
||||
instructions="Process each task thoroughly and provide detailed output",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create a loop task that will automatically process each CSV row
|
||||
loop_task = Task(
|
||||
description="Process each task from the CSV file",
|
||||
expected_output="Completed task with detailed results",
|
||||
agent=loop_agent,
|
||||
task_type="loop", # This enables CSV loop processing
|
||||
input_file=csv_file # The CSV file to process
|
||||
)
|
||||
|
||||
# Initialize the agents system
|
||||
agents = PraisonAIAgents(
|
||||
agents=[loop_agent],
|
||||
tasks=[loop_task],
|
||||
process="workflow",
|
||||
max_iter=10 # Maximum iterations to prevent infinite loops
|
||||
)
|
||||
|
||||
print(f"🚀 Starting loop processing of {csv_file}")
|
||||
print("Each row will be processed as a separate task...")
|
||||
|
||||
# Start processing
|
||||
agents.start()
|
||||
|
||||
# ==============================================================================
|
||||
# METHOD 2: MANUAL CSV PROCESSING WITH TOOLS
|
||||
# ==============================================================================
|
||||
|
||||
def method_2_manual_csv():
|
||||
"""
|
||||
Manual CSV processing using built-in CSV tools.
|
||||
|
||||
This gives you more control over how the CSV is processed,
|
||||
including validation, filtering, and custom logic.
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("METHOD 2: Manual CSV Processing with Tools")
|
||||
print("="*60)
|
||||
|
||||
# Create sample CSV file
|
||||
csv_file = create_sample_urls_csv()
|
||||
|
||||
# Create an agent with CSV tools
|
||||
csv_agent = Agent(
|
||||
name="CSVAnalyzer",
|
||||
role="CSV Data Processor",
|
||||
goal="Read, validate, and process CSV data manually",
|
||||
backstory="Specialist in data validation and processing",
|
||||
tools=[read_csv, write_csv],
|
||||
instructions="""
|
||||
You are an expert at processing CSV files. When given a CSV file:
|
||||
1. Read the CSV file using read_csv tool
|
||||
2. Validate the data structure
|
||||
3. Process each row according to the requirements
|
||||
4. Create a summary of the processing results
|
||||
5. Save results to a new CSV file if needed
|
||||
""",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create a task for manual CSV processing
|
||||
csv_task = Task(
|
||||
description=f"""
|
||||
Process the CSV file '{csv_file}' manually:
|
||||
1. Read the CSV file using the read_csv tool
|
||||
2. Validate that it contains 'url' and 'description' columns
|
||||
3. For each URL, describe what type of website it is
|
||||
4. Create a summary report of all URLs processed
|
||||
5. Save the results to 'processed_urls.csv'
|
||||
""",
|
||||
expected_output="A detailed report of CSV processing with validation results",
|
||||
agent=csv_agent
|
||||
)
|
||||
|
||||
# Initialize and run
|
||||
agents = PraisonAIAgents(
|
||||
agents=[csv_agent],
|
||||
tasks=[csv_task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
print(f"🚀 Starting manual processing of {csv_file}")
|
||||
agents.start()
|
||||
|
||||
# ==============================================================================
|
||||
# METHOD 3: URL PROCESSING FROM CSV (SPECIFIC TO ISSUE REQUEST)
|
||||
# ==============================================================================
|
||||
|
||||
def method_3_url_processing():
|
||||
"""
|
||||
Specific example for processing URLs from CSV files.
|
||||
|
||||
This addresses the original question about processing a CSV list of URLs
|
||||
where agents work through the list sequentially.
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("METHOD 3: URL Processing from CSV Files")
|
||||
print("="*60)
|
||||
|
||||
# Create sample CSV file with URLs
|
||||
csv_file = create_sample_urls_csv()
|
||||
|
||||
# Create a URL processing agent
|
||||
url_agent = Agent(
|
||||
name="URLProcessor",
|
||||
role="Website Analyzer",
|
||||
goal="Analyze websites from CSV URL list",
|
||||
backstory="Expert web analyst who can evaluate websites",
|
||||
tools=[read_csv, write_csv],
|
||||
instructions="""
|
||||
You are a website analysis expert. When given a CSV with URLs:
|
||||
1. Read the CSV file to get the list of URLs
|
||||
2. For each URL, analyze what kind of website it is
|
||||
3. Determine the purpose and main features of each site
|
||||
4. Create a detailed analysis report
|
||||
5. Save results with analysis data
|
||||
""",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create the URL processing task
|
||||
url_task = Task(
|
||||
description=f"""
|
||||
Process URLs from '{csv_file}':
|
||||
1. Read the CSV file containing URLs and descriptions
|
||||
2. For each URL in the list, provide an analysis of:
|
||||
- What type of website it is
|
||||
- Its main purpose and features
|
||||
- Target audience
|
||||
- Key characteristics
|
||||
3. Work through the URLs sequentially (one by one)
|
||||
4. Create a comprehensive report with all analyses
|
||||
5. Save the results to 'url_analysis_results.csv'
|
||||
|
||||
Make sure to process each URL individually and thoroughly.
|
||||
""",
|
||||
expected_output="Detailed analysis of each URL with comprehensive results saved to CSV",
|
||||
agent=url_agent
|
||||
)
|
||||
|
||||
# Initialize and run
|
||||
agents = PraisonAIAgents(
|
||||
agents=[url_agent],
|
||||
tasks=[url_task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
print(f"🚀 Starting URL processing from {csv_file}")
|
||||
print("URLs will be processed sequentially...")
|
||||
agents.start()
|
||||
|
||||
# ==============================================================================
|
||||
# METHOD 4: ADVANCED CSV PROCESSING WITH VALIDATION
|
||||
# ==============================================================================
|
||||
|
||||
def method_4_advanced_processing():
|
||||
"""
|
||||
Advanced CSV processing with data validation and error handling.
|
||||
|
||||
This method shows how to handle complex CSV processing scenarios
|
||||
with validation, error handling, and structured outputs.
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("METHOD 4: Advanced CSV Processing with Validation")
|
||||
print("="*60)
|
||||
|
||||
# Create a more complex CSV file
|
||||
complex_data = [
|
||||
{"id": 1, "url": "https://example.com", "priority": "high", "category": "test"},
|
||||
{"id": 2, "url": "https://github.com", "priority": "medium", "category": "development"},
|
||||
{"id": 3, "url": "invalid-url", "priority": "low", "category": "test"}, # Invalid URL for testing
|
||||
{"id": 4, "url": "https://stackoverflow.com", "priority": "high", "category": "development"},
|
||||
{"id": 5, "url": "", "priority": "medium", "category": "empty"} # Empty URL for testing
|
||||
]
|
||||
|
||||
csv_file = "complex_urls.csv"
|
||||
write_csv(csv_file, complex_data)
|
||||
print(f"✅ Created {csv_file} with complex data including invalid entries")
|
||||
|
||||
# Create a validation agent
|
||||
validator_agent = Agent(
|
||||
name="DataValidator",
|
||||
role="Data Quality Specialist",
|
||||
goal="Validate and clean CSV data before processing",
|
||||
backstory="Expert in data validation and quality assurance",
|
||||
tools=[read_csv, write_csv],
|
||||
instructions="""
|
||||
You are a data validation expert. Your job is to:
|
||||
1. Read CSV files and validate data quality
|
||||
2. Identify invalid or problematic entries
|
||||
3. Clean and standardize data where possible
|
||||
4. Create separate files for valid and invalid data
|
||||
5. Provide detailed validation reports
|
||||
""",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create a processing agent
|
||||
processor_agent = Agent(
|
||||
name="ValidatedProcessor",
|
||||
role="Validated Data Processor",
|
||||
goal="Process only validated and clean data",
|
||||
backstory="Specialist in processing pre-validated data efficiently",
|
||||
tools=[read_csv, write_csv],
|
||||
instructions="""
|
||||
You process validated data efficiently:
|
||||
1. Read validated CSV data
|
||||
2. Process each valid entry according to requirements
|
||||
3. Generate comprehensive results
|
||||
4. Create detailed output reports
|
||||
""",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create validation task
|
||||
validation_task = Task(
|
||||
description=f"""
|
||||
Validate the data in '{csv_file}':
|
||||
1. Read the CSV file
|
||||
2. Check each row for:
|
||||
- Valid URL format (must start with http:// or https://)
|
||||
- Non-empty required fields
|
||||
- Valid priority values (high, medium, low)
|
||||
- Valid category values
|
||||
3. Separate valid and invalid entries
|
||||
4. Save valid entries to 'valid_urls.csv'
|
||||
5. Save invalid entries to 'invalid_urls.csv' with error descriptions
|
||||
6. Create a validation summary report
|
||||
""",
|
||||
expected_output="Validation complete with separate files for valid/invalid data and summary report",
|
||||
agent=validator_agent
|
||||
)
|
||||
|
||||
# Create processing task (depends on validation)
|
||||
processing_task = Task(
|
||||
description="""
|
||||
Process the validated data from 'valid_urls.csv':
|
||||
1. Read the validated CSV file
|
||||
2. For each valid URL, provide analysis based on priority and category
|
||||
3. High priority items get detailed analysis
|
||||
4. Medium priority items get standard analysis
|
||||
5. Low priority items get basic analysis
|
||||
6. Save results to 'final_results.csv'
|
||||
""",
|
||||
expected_output="Processed results for all valid entries saved to final CSV",
|
||||
agent=processor_agent
|
||||
)
|
||||
|
||||
# Initialize and run with sequential processing
|
||||
agents = PraisonAIAgents(
|
||||
agents=[validator_agent, processor_agent],
|
||||
tasks=[validation_task, processing_task],
|
||||
process="sequential" # Validation must complete before processing
|
||||
)
|
||||
|
||||
print(f"🚀 Starting advanced processing with validation")
|
||||
agents.start()
|
||||
|
||||
# ==============================================================================
|
||||
# MAIN EXECUTION
|
||||
# ==============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("CSV Processing with PraisonAI Agents - Multiple Methods Demo")
|
||||
print("=" * 70)
|
||||
|
||||
print("\nThis demo shows 4 different ways to process CSV files with agents:")
|
||||
print("1. Simple CSV Loop Processing (easiest)")
|
||||
print("2. Manual CSV Processing with tools")
|
||||
print("3. URL Processing from CSV (addresses the GitHub issue)")
|
||||
print("4. Advanced processing with validation")
|
||||
|
||||
# You can run all methods or comment out ones you don't need
|
||||
try:
|
||||
# Method 1: Simple loop processing
|
||||
method_1_simple_loop()
|
||||
|
||||
# Method 2: Manual CSV processing
|
||||
method_2_manual_csv()
|
||||
|
||||
# Method 3: URL processing (specific to the GitHub issue)
|
||||
method_3_url_processing()
|
||||
|
||||
# Method 4: Advanced processing with validation
|
||||
method_4_advanced_processing()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("✅ All CSV processing methods completed!")
|
||||
print("Check the generated CSV files for results.")
|
||||
print("="*70)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during processing: {e}")
|
||||
print("Make sure you have:")
|
||||
print("- OpenAI API key set (export OPENAI_API_KEY=your_key)")
|
||||
print("- praisonaiagents package installed (pip install praisonaiagents)")
|
||||
print("- pandas package installed (pip install pandas)")
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Knowledge Agent with Reranker Example
|
||||
===================================
|
||||
|
||||
This example demonstrates how to use Mem0's built-in reranking feature
|
||||
with the Knowledge Agent for improved search result relevance.
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
|
||||
def main():
|
||||
"""
|
||||
Demonstrates knowledge search with and without reranking.
|
||||
"""
|
||||
|
||||
# Create sample content for knowledge base
|
||||
sample_documents = [
|
||||
"Artificial Intelligence is a field of computer science that aims to create machines that can perform tasks that typically require human intelligence.",
|
||||
"Machine Learning is a subset of AI that enables computers to learn and improve from experience without being explicitly programmed.",
|
||||
"Deep Learning is a type of machine learning that uses neural networks with multiple layers to model and understand complex patterns.",
|
||||
"Natural Language Processing (NLP) is a branch of AI that helps computers understand, interpret and manipulate human language.",
|
||||
"Computer Vision is an AI field that trains computers to interpret and understand the visual world from digital images and videos.",
|
||||
"Reinforcement Learning is a type of machine learning where an agent learns to make decisions by taking actions in an environment.",
|
||||
"Neural Networks are computing systems inspired by biological neural networks that constitute animal brains.",
|
||||
"Data Science combines domain expertise, programming skills, and knowledge of mathematics and statistics to extract insights from data."
|
||||
]
|
||||
|
||||
print("=== Knowledge Agent Reranker Example ===\n")
|
||||
|
||||
# Example 1: Basic Knowledge without reranking
|
||||
print("1. Creating Knowledge base without reranking...")
|
||||
basic_config = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "knowledge_basic",
|
||||
"path": ".praison_basic"
|
||||
}
|
||||
},
|
||||
"reranker": {
|
||||
"enabled": True,
|
||||
"default_rerank": False # Disabled by default
|
||||
}
|
||||
}
|
||||
|
||||
basic_knowledge = Knowledge(config=basic_config, verbose=1)
|
||||
|
||||
# Add documents to knowledge base
|
||||
for i, doc in enumerate(sample_documents):
|
||||
basic_knowledge.store(doc, metadata={"doc_id": f"doc_{i}", "category": "AI"})
|
||||
|
||||
# Example 2: Knowledge with reranking enabled by default
|
||||
print("\n2. Creating Knowledge base with reranking enabled...")
|
||||
rerank_config = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "knowledge_rerank",
|
||||
"path": ".praison_rerank"
|
||||
}
|
||||
},
|
||||
"reranker": {
|
||||
"enabled": True,
|
||||
"default_rerank": True # Enabled by default
|
||||
}
|
||||
}
|
||||
|
||||
rerank_knowledge = Knowledge(config=rerank_config, verbose=1)
|
||||
|
||||
# Add same documents to reranking knowledge base
|
||||
for i, doc in enumerate(sample_documents):
|
||||
rerank_knowledge.store(doc, metadata={"doc_id": f"doc_{i}", "category": "AI"})
|
||||
|
||||
print("\n=== Search Results Comparison ===\n")
|
||||
|
||||
query = "What is machine learning and how does it work?"
|
||||
|
||||
# Search without reranking
|
||||
print("3. Search WITHOUT reranking:")
|
||||
basic_results = basic_knowledge.search(query, rerank=False)
|
||||
print(f"Query: {query}")
|
||||
print(f"Results (limit=3):")
|
||||
for i, result in enumerate(basic_results[:3]):
|
||||
text = result.get('memory', result.get('text', str(result)))
|
||||
score = result.get('score', 'N/A')
|
||||
print(f" {i+1}. Score: {score}")
|
||||
print(f" Text: {text[:100]}...")
|
||||
print()
|
||||
|
||||
# Search with reranking explicitly enabled
|
||||
print("4. Search WITH reranking:")
|
||||
rerank_results = basic_knowledge.search(query, rerank=True)
|
||||
print(f"Query: {query}")
|
||||
print(f"Results (limit=3):")
|
||||
for i, result in enumerate(rerank_results[:3]):
|
||||
text = result.get('memory', result.get('text', str(result)))
|
||||
score = result.get('score', 'N/A')
|
||||
print(f" {i+1}. Score: {score}")
|
||||
print(f" Text: {text[:100]}...")
|
||||
print()
|
||||
|
||||
# Search using config default (reranking enabled)
|
||||
print("5. Search using config default (reranking enabled):")
|
||||
default_results = rerank_knowledge.search(query) # Will use default_rerank=True
|
||||
print(f"Query: {query}")
|
||||
print(f"Results (limit=3):")
|
||||
for i, result in enumerate(default_results[:3]):
|
||||
text = result.get('memory', result.get('text', str(result)))
|
||||
score = result.get('score', 'N/A')
|
||||
print(f" {i+1}. Score: {score}")
|
||||
print(f" Text: {text[:100]}...")
|
||||
print()
|
||||
|
||||
print("=== Advanced Search Options ===\n")
|
||||
|
||||
# Example with additional Mem0 advanced retrieval options
|
||||
print("6. Search with keyword search + reranking:")
|
||||
advanced_results = basic_knowledge.search(
|
||||
query,
|
||||
rerank=True,
|
||||
keyword_search=True, # Enable keyword search for better recall
|
||||
filter_memories=True # Enable filtering for better precision
|
||||
)
|
||||
print(f"Query: {query}")
|
||||
print(f"Advanced results with keyword_search=True, filter_memories=True:")
|
||||
for i, result in enumerate(advanced_results[:3]):
|
||||
text = result.get('memory', result.get('text', str(result)))
|
||||
score = result.get('score', 'N/A')
|
||||
print(f" {i+1}. Score: {score}")
|
||||
print(f" Text: {text[:100]}...")
|
||||
print()
|
||||
|
||||
# Example with Agent using reranking knowledge
|
||||
print("\n=== Agent with Reranking Knowledge ===\n")
|
||||
|
||||
agent = Agent(
|
||||
name="AI Research Assistant",
|
||||
instructions="You are an AI research assistant. Use your knowledge base to answer questions about artificial intelligence topics.",
|
||||
knowledge=sample_documents, # Will use default knowledge config
|
||||
knowledge_config=rerank_config, # Use reranking config
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("7. Agent with reranking knowledge:")
|
||||
response = agent.run("Explain the difference between machine learning and deep learning, and how they relate to AI.")
|
||||
print(f"Agent response: {response}")
|
||||
|
||||
print("\n=== Performance Notes ===")
|
||||
print("- Basic search: ~10ms latency")
|
||||
print("- Reranking: 150-200ms additional latency")
|
||||
print("- Reranking improves relevance ordering significantly")
|
||||
print("- Use rerank=True for better results when query relevance is critical")
|
||||
print("- Use keyword_search=True for better recall")
|
||||
print("- Use filter_memories=True for better precision")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Simple CSV URL Processor
|
||||
|
||||
This example directly addresses the GitHub issue #23:
|
||||
"Is it possible to pass in a CSV list of URLs and have agents work through the list each in turn?"
|
||||
|
||||
This is the simplest possible implementation for processing URLs from a CSV file.
|
||||
|
||||
Author: Generated for GitHub Issue #23
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import write_csv
|
||||
|
||||
def create_url_csv():
|
||||
"""Create a CSV file with URLs to process"""
|
||||
# Example URLs - replace with your own
|
||||
urls = [
|
||||
{"url": "https://example.com"},
|
||||
{"url": "https://github.com"},
|
||||
{"url": "https://stackoverflow.com"},
|
||||
{"url": "https://python.org"}
|
||||
]
|
||||
|
||||
write_csv("urls_to_process.csv", urls)
|
||||
print("✅ Created urls_to_process.csv")
|
||||
return "urls_to_process.csv"
|
||||
|
||||
def main():
|
||||
"""
|
||||
Simple CSV URL processing example
|
||||
|
||||
This shows the easiest way to process a CSV list of URLs
|
||||
where agents work through the list sequentially.
|
||||
"""
|
||||
print("Simple CSV URL Processor")
|
||||
print("=" * 40)
|
||||
|
||||
# Step 1: Create the CSV file (or use your existing one)
|
||||
csv_file = create_url_csv()
|
||||
|
||||
# Step 2: Create an agent to process URLs
|
||||
url_agent = Agent(
|
||||
name="URLProcessor",
|
||||
role="URL Analyzer",
|
||||
goal="Analyze each URL from the CSV list",
|
||||
backstory="Expert at analyzing websites and URLs",
|
||||
instructions="Analyze each URL and provide insights about the website",
|
||||
llm="gpt-4o-mini" # You can change this to any supported model
|
||||
)
|
||||
|
||||
# Step 3: Create a task that will loop through the CSV
|
||||
url_task = Task(
|
||||
description="Analyze each URL from the CSV file and provide insights",
|
||||
expected_output="Analysis of the website at each URL",
|
||||
agent=url_agent,
|
||||
task_type="loop", # This tells PraisonAI to loop through CSV rows
|
||||
input_file=csv_file # Your CSV file with URLs
|
||||
)
|
||||
|
||||
# Step 4: Run the agents
|
||||
agents = PraisonAIAgents(
|
||||
agents=[url_agent],
|
||||
tasks=[url_task],
|
||||
process="workflow",
|
||||
max_iter=10 # Adjust based on how many URLs you have
|
||||
)
|
||||
|
||||
print(f"🚀 Processing URLs from {csv_file}")
|
||||
print("The agent will work through each URL in turn...")
|
||||
|
||||
agents.start()
|
||||
|
||||
print("✅ Finished processing all URLs!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Human Approval Example for PraisonAI Agents
|
||||
|
||||
This example demonstrates the human-in-the-loop approval system for dangerous operations.
|
||||
When agents attempt to execute high-risk tools (like shell commands or file operations),
|
||||
the system will prompt the user for approval before proceeding.
|
||||
|
||||
Usage:
|
||||
python human_approval_example.py
|
||||
|
||||
Features:
|
||||
- Automatic approval prompts for dangerous tools
|
||||
- Risk level classification (critical, high, medium, low)
|
||||
- User can approve, deny, or modify tool arguments
|
||||
- Console-based approval interface with rich formatting
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the local development path to use the current implementation
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src', 'praisonai-agents'))
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import python_tools, file_tools, shell_tools
|
||||
from praisonaiagents.main import register_approval_callback
|
||||
from praisonaiagents.approval import (
|
||||
console_approval_callback,
|
||||
ApprovalDecision,
|
||||
add_approval_requirement,
|
||||
remove_approval_requirement
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
def custom_approval_callback(function_name: str, arguments: dict, risk_level: str) -> ApprovalDecision:
|
||||
"""
|
||||
Custom approval callback that demonstrates how to implement
|
||||
custom approval logic beyond the default console prompts.
|
||||
"""
|
||||
print(f"\n🔒 CUSTOM APPROVAL REQUIRED")
|
||||
print(f"Function: {function_name}")
|
||||
print(f"Risk Level: {risk_level}")
|
||||
print(f"Arguments: {arguments}")
|
||||
|
||||
# Example: Auto-approve low-risk operations
|
||||
if risk_level == "low":
|
||||
print("✅ Auto-approving low-risk operation")
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved low risk")
|
||||
|
||||
# Example: Always deny certain dangerous commands
|
||||
if function_name == "execute_command" and any(
|
||||
dangerous in str(arguments.get("command", ""))
|
||||
for dangerous in ["rm -rf", "del /f", "format", "shutdown"]
|
||||
):
|
||||
print("❌ Automatically denying dangerous command")
|
||||
return ApprovalDecision(approved=False, reason="Dangerous command blocked")
|
||||
|
||||
# For other cases, use the default console approval
|
||||
return console_approval_callback(function_name, arguments, risk_level)
|
||||
|
||||
def main():
|
||||
"""Demonstrate human approval system with various dangerous operations."""
|
||||
|
||||
print("🤖 PraisonAI Human Approval System Demo")
|
||||
print("=" * 50)
|
||||
print("This demo will show approval prompts for dangerous operations.")
|
||||
print("You can approve or deny each operation as it's requested.")
|
||||
print()
|
||||
|
||||
# Option 1: Use default console approval callback (automatic)
|
||||
# Option 2: Use custom approval callback
|
||||
# Uncomment the line below to use custom approval logic:
|
||||
# register_approval_callback(custom_approval_callback)
|
||||
|
||||
# Create agent with dangerous tools
|
||||
agent = Agent(
|
||||
name="Security Demo Agent",
|
||||
role="System Administrator",
|
||||
goal="Demonstrate human approval for dangerous operations",
|
||||
tools=[python_tools, file_tools, shell_tools],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Define tasks that will trigger approval prompts
|
||||
tasks = [
|
||||
Task(
|
||||
description="Execute a simple Python print statement",
|
||||
agent=agent,
|
||||
expected_output="Python code execution result"
|
||||
),
|
||||
Task(
|
||||
description="Create a test file with some content",
|
||||
agent=agent,
|
||||
expected_output="File creation confirmation"
|
||||
),
|
||||
Task(
|
||||
description="List the current directory contents using shell command",
|
||||
agent=agent,
|
||||
expected_output="Directory listing"
|
||||
),
|
||||
Task(
|
||||
description="Delete the test file that was created",
|
||||
agent=agent,
|
||||
expected_output="File deletion confirmation"
|
||||
)
|
||||
]
|
||||
|
||||
# Create and run process
|
||||
process = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=tasks,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print("\n🚀 Starting process with approval-required operations...")
|
||||
print("You will be prompted to approve each dangerous operation.")
|
||||
print()
|
||||
|
||||
try:
|
||||
result = process.start()
|
||||
print(f"\n✅ Process completed successfully!")
|
||||
print(f"Final result: {result}")
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ Process cancelled by user")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Process failed: {e}")
|
||||
|
||||
def demo_manual_approval_configuration():
|
||||
"""Demonstrate how to manually configure approval requirements."""
|
||||
|
||||
print("\n📋 Manual Approval Configuration Demo")
|
||||
print("=" * 40)
|
||||
|
||||
# Add approval requirement for a normally safe operation
|
||||
add_approval_requirement("list_files", "medium")
|
||||
print("✅ Added approval requirement for 'list_files' (medium risk)")
|
||||
|
||||
# Remove approval requirement for a normally dangerous operation
|
||||
remove_approval_requirement("write_file")
|
||||
print("✅ Removed approval requirement for 'write_file'")
|
||||
|
||||
# Show current approval requirements
|
||||
from praisonaiagents.approval import APPROVAL_REQUIRED_TOOLS, TOOL_RISK_LEVELS
|
||||
print(f"\nCurrent approval-required tools: {list(APPROVAL_REQUIRED_TOOLS)}")
|
||||
print(f"Tool risk levels: {TOOL_RISK_LEVELS}")
|
||||
|
||||
async def async_demo():
|
||||
"""Demonstrate approval system with async operations."""
|
||||
|
||||
print("\n🔄 Async Approval Demo")
|
||||
print("=" * 25)
|
||||
|
||||
# Create async agent
|
||||
agent = Agent(
|
||||
name="Async Demo Agent",
|
||||
role="Async Operations Specialist",
|
||||
goal="Demonstrate async approval workflow",
|
||||
tools=[python_tools],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# This would trigger approval in async context
|
||||
task = Task(
|
||||
description="Execute async Python code that prints 'Hello from async!'",
|
||||
agent=agent,
|
||||
expected_output="Async execution result"
|
||||
)
|
||||
|
||||
process = PraisonAIAgents(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
try:
|
||||
result = await process.astart()
|
||||
print(f"✅ Async process completed: {result}")
|
||||
except Exception as e:
|
||||
print(f"❌ Async process failed: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(__doc__)
|
||||
|
||||
# Run the main demo
|
||||
main()
|
||||
|
||||
# Show manual configuration options
|
||||
demo_manual_approval_configuration()
|
||||
|
||||
# Demonstrate async approval (optional)
|
||||
print("\n" + "=" * 50)
|
||||
print("Would you like to run the async demo as well? (y/n): ", end="")
|
||||
try:
|
||||
if input().lower().startswith('y'):
|
||||
asyncio.run(async_demo())
|
||||
except KeyboardInterrupt:
|
||||
print("\nDemo cancelled.")
|
||||
|
||||
print("\n🎉 Human approval demo completed!")
|
||||
print("\nKey takeaways:")
|
||||
print("- Dangerous operations automatically prompt for approval")
|
||||
print("- Risk levels help prioritize approval decisions")
|
||||
print("- Custom approval callbacks allow for automated policies")
|
||||
print("- Both sync and async operations are supported")
|
||||
print("- Approval requirements can be configured at runtime")
|
||||
@@ -0,0 +1,9 @@
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
search_agent = Agent(
|
||||
instructions="""You help search the web.""",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("http://localhost:8931/sse")
|
||||
)
|
||||
|
||||
search_agent.start("Find about Praison AI")
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Graph Knowledge Example
|
||||
|
||||
This example shows how to use graph memory with the Knowledge class
|
||||
for enhanced document processing and relationship extraction.
|
||||
|
||||
Requirements:
|
||||
pip install "praisonaiagents[graph]"
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
|
||||
def main():
|
||||
print("📚 Graph Knowledge Example")
|
||||
print("=" * 40)
|
||||
|
||||
# Configuration with graph memory support
|
||||
config_with_graph = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "graph_knowledge_test",
|
||||
"path": ".praison/graph_knowledge"
|
||||
}
|
||||
},
|
||||
"graph_store": {
|
||||
"provider": "memgraph", # or "neo4j"
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4o-mini"
|
||||
}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "text-embedding-3-small"
|
||||
}
|
||||
},
|
||||
"reranker": {
|
||||
"enabled": True,
|
||||
"default_rerank": True
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback configuration (without graph store)
|
||||
config_basic = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "basic_knowledge_test",
|
||||
"path": ".praison/basic_knowledge"
|
||||
}
|
||||
},
|
||||
"reranker": {
|
||||
"enabled": True,
|
||||
"default_rerank": True
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
# Try graph-enabled knowledge first
|
||||
knowledge = Knowledge(config=config_with_graph, verbose=1)
|
||||
print("✅ Graph memory enabled!")
|
||||
graph_mode = True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Graph memory not available: {e}")
|
||||
print("📦 Falling back to vector-only memory...")
|
||||
knowledge = Knowledge(config=config_basic, verbose=1)
|
||||
graph_mode = False
|
||||
|
||||
# Sample documents with rich relationship information
|
||||
documents = [
|
||||
"""
|
||||
OpenAI is an artificial intelligence research company founded in 2015.
|
||||
The company was co-founded by Sam Altman, Greg Brockman, and Elon Musk,
|
||||
among others. Sam Altman serves as the CEO, while Greg Brockman is the
|
||||
President and Chairman. Elon Musk was initially involved but left the
|
||||
board in 2018.
|
||||
""",
|
||||
"""
|
||||
GPT-4 is OpenAI's most advanced language model, released in March 2023.
|
||||
It powers ChatGPT Plus and is available through OpenAI's API. GPT-4
|
||||
demonstrates improved reasoning and reduced hallucinations compared to
|
||||
its predecessor GPT-3.5. The model uses transformer architecture and
|
||||
was trained using reinforcement learning from human feedback (RLHF).
|
||||
""",
|
||||
"""
|
||||
Microsoft has a significant partnership with OpenAI, having invested
|
||||
billions of dollars in the company. This partnership allows Microsoft
|
||||
to integrate OpenAI's models into products like Microsoft Copilot,
|
||||
which is built into Office 365 and Windows. The collaboration gives
|
||||
Microsoft exclusive access to GPT models for certain use cases.
|
||||
""",
|
||||
"""
|
||||
Anthropic is a major competitor to OpenAI, founded by former OpenAI
|
||||
researchers including Dario Amodei and Daniela Amodei. Anthropic
|
||||
created Claude, an AI assistant that competes with ChatGPT. The company
|
||||
focuses on AI safety research and constitutional AI training methods.
|
||||
"""
|
||||
]
|
||||
|
||||
print(f"\n📥 Adding {len(documents)} documents to knowledge base...")
|
||||
|
||||
# Add documents to knowledge base
|
||||
for i, doc in enumerate(documents, 1):
|
||||
try:
|
||||
result = knowledge.add(doc, user_id="demo_user")
|
||||
print(f"✓ Document {i} added successfully")
|
||||
if graph_mode:
|
||||
print(f" Graph relationships extracted and stored")
|
||||
except Exception as e:
|
||||
print(f"❌ Error adding document {i}: {e}")
|
||||
|
||||
print("\n🔍 Testing relationship-aware queries...")
|
||||
|
||||
# Test queries that benefit from graph relationships
|
||||
queries = [
|
||||
"Who founded OpenAI?",
|
||||
"What is the relationship between OpenAI and Microsoft?",
|
||||
"How does GPT-4 compare to other models?",
|
||||
"Who are OpenAI's competitors?",
|
||||
"What role does Sam Altman play at OpenAI?",
|
||||
"How is Anthropic related to OpenAI?"
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n❓ Query: {query}")
|
||||
try:
|
||||
results = knowledge.search(query, user_id="demo_user", limit=2)
|
||||
|
||||
if results and hasattr(results, '__iter__'):
|
||||
for i, result in enumerate(results, 1):
|
||||
if hasattr(result, 'get'):
|
||||
content = result.get('memory', result.get('text', str(result)))
|
||||
else:
|
||||
content = str(result)
|
||||
|
||||
# Truncate long responses
|
||||
if len(content) > 200:
|
||||
content = content[:197] + "..."
|
||||
|
||||
print(f" {i}. {content}")
|
||||
else:
|
||||
print(" No results found")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Search error: {e}")
|
||||
|
||||
print(f"\n{'✅ Graph Knowledge Example Complete!' if graph_mode else '✅ Basic Knowledge Example Complete!'}")
|
||||
print("=" * 40)
|
||||
|
||||
if graph_mode:
|
||||
print("🧠 Graph memory successfully captured entity relationships!")
|
||||
print(" Queries benefit from understanding connections between entities.")
|
||||
else:
|
||||
print("📦 Vector memory provided semantic search capabilities.")
|
||||
print("💡 To enable graph memory, set up Neo4j or Memgraph and install:")
|
||||
print(" pip install \"mem0ai[graph]\"")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("⚠️ Warning: OPENAI_API_KEY not set.")
|
||||
print(" Export your key: export OPENAI_API_KEY='your-key-here'")
|
||||
print(" Some features may not work without it.\n")
|
||||
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Example interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
print("💡 Make sure you have the required dependencies installed:")
|
||||
print(" pip install \"praisonaiagents[graph]\"")
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Graph Memory Agent Example
|
||||
|
||||
This example demonstrates how to use Mem0's graph memory capabilities
|
||||
with PraisonAI agents to create and utilize complex relationships
|
||||
between pieces of information.
|
||||
|
||||
Requirements:
|
||||
pip install "praisonaiagents[graph]"
|
||||
|
||||
Setup:
|
||||
- For Neo4j: Set up a Neo4j instance (local or AuraDB)
|
||||
- For Memgraph: Run with Docker:
|
||||
docker run -p 7687:7687 memgraph/memgraph-mage:latest --schema-info-enabled=True
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
def main():
|
||||
# Example with Neo4j (uncomment and configure as needed)
|
||||
neo4j_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"graph_store": {
|
||||
"provider": "neo4j",
|
||||
"config": {
|
||||
"url": "neo4j+s://your-instance.databases.neo4j.io",
|
||||
"username": "neo4j",
|
||||
"password": "your-password"
|
||||
}
|
||||
},
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": 6333
|
||||
}
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4o-mini"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Example with Memgraph (local setup)
|
||||
memgraph_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"graph_store": {
|
||||
"provider": "memgraph",
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": 6333
|
||||
}
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4o-mini"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Simple local configuration (fallback to vector-only memory)
|
||||
local_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"path": ".praison/graph_memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Use local config by default (can be switched to neo4j_config or memgraph_config)
|
||||
memory_config = local_config
|
||||
|
||||
print("🧠 Graph Memory Agent Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Define research agent
|
||||
researcher = Agent(
|
||||
name="Knowledge Researcher",
|
||||
role="AI Research Specialist",
|
||||
goal="Research and build knowledge graphs about AI and technology relationships",
|
||||
backstory="""You are an expert at identifying relationships between concepts,
|
||||
people, technologies, and ideas. You excel at creating structured knowledge
|
||||
that captures complex interconnections.""",
|
||||
verbose=True,
|
||||
memory=True
|
||||
)
|
||||
|
||||
# Task 1: Build initial knowledge graph
|
||||
build_knowledge_task = Task(
|
||||
description="""Research and store information about the relationships between:
|
||||
1. OpenAI and its key products (GPT-4, ChatGPT, DALL-E)
|
||||
2. Key people: Sam Altman (CEO), Greg Brockman (Co-founder)
|
||||
3. Partnerships: Microsoft partnership and investment
|
||||
4. Competitors: Anthropic, Google, Meta
|
||||
5. Technologies: Transformer architecture, reinforcement learning
|
||||
|
||||
Focus on capturing the relationships and connections between these entities.
|
||||
Store comprehensive information about how these elements relate to each other.""",
|
||||
expected_output="Detailed knowledge graph with entities and relationships about OpenAI ecosystem",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Task 2: Query and expand knowledge
|
||||
query_knowledge_task = Task(
|
||||
description="""Based on the previously stored knowledge, answer these questions:
|
||||
1. Who are the key leaders at OpenAI and what are their roles?
|
||||
2. What are the main products and how do they relate to each other?
|
||||
3. Who are OpenAI's main competitors and how do they compare?
|
||||
4. What is the relationship between OpenAI and Microsoft?
|
||||
|
||||
Use the stored memory to provide comprehensive answers that leverage
|
||||
the relationship information.""",
|
||||
expected_output="Comprehensive answers utilizing relationship-aware memory retrieval",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Task 3: Add new connections
|
||||
expand_knowledge_task = Task(
|
||||
description="""Add new information to the knowledge graph:
|
||||
1. Elon Musk was a co-founder of OpenAI but left the board
|
||||
2. OpenAI started as a non-profit but created a capped-profit subsidiary
|
||||
3. GPT-4 powers ChatGPT and is also available via API
|
||||
4. DALL-E uses diffusion models for image generation
|
||||
5. OpenAI's research influences the broader AI field
|
||||
|
||||
Ensure these new facts are connected to the existing knowledge graph.""",
|
||||
expected_output="Updated knowledge graph with new relationships and connections",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Run the multi-agent system with graph memory
|
||||
agents_system = PraisonAIAgents(
|
||||
agents=[researcher],
|
||||
tasks=[build_knowledge_task, query_knowledge_task, expand_knowledge_task],
|
||||
verbose=1,
|
||||
memory=True,
|
||||
memory_config=memory_config
|
||||
)
|
||||
|
||||
print("\n🚀 Starting graph memory demonstration...")
|
||||
result = agents_system.start()
|
||||
|
||||
print("\n✅ Graph Memory Example Complete!")
|
||||
print("=" * 50)
|
||||
print("The agent has built and queried a knowledge graph that captures")
|
||||
print("complex relationships between entities, demonstrating how graph memory")
|
||||
print("enhances traditional vector-based memory with relationship awareness.")
|
||||
|
||||
return result
|
||||
|
||||
def test_direct_memory_api():
|
||||
"""
|
||||
Direct example using the Memory class with graph support
|
||||
"""
|
||||
print("\n🔬 Direct Memory API Test")
|
||||
print("=" * 30)
|
||||
|
||||
from praisonaiagents.memory import Memory
|
||||
|
||||
# Configure memory with graph support (using local fallback)
|
||||
memory_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"path": ".praison/test_graph_memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memory = Memory(config=memory_config, verbose=5)
|
||||
|
||||
# Test storing user memories with relationships
|
||||
user_id = "alice"
|
||||
|
||||
memories = [
|
||||
"I love hiking in the mountains",
|
||||
"My friend John also loves hiking",
|
||||
"John has a dog named Max",
|
||||
"Max loves to go on hikes with us",
|
||||
"We often hike in Yosemite National Park",
|
||||
"Yosemite has beautiful waterfalls"
|
||||
]
|
||||
|
||||
print("Storing memories with potential relationships...")
|
||||
for memory_text in memories:
|
||||
memory.store_user_memory(user_id, memory_text)
|
||||
print(f"✓ Stored: {memory_text}")
|
||||
|
||||
print("\nSearching for relationship-aware memories...")
|
||||
|
||||
# Test relationship-aware searches
|
||||
queries = [
|
||||
"Who likes hiking?",
|
||||
"Tell me about John",
|
||||
"What do we know about dogs?",
|
||||
"Where do they go hiking?"
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n🔍 Query: {query}")
|
||||
results = memory.search_user_memory(user_id, query, limit=3, rerank=True)
|
||||
for i, result in enumerate(results, 1):
|
||||
if isinstance(result, dict):
|
||||
print(f" {i}. {result.get('memory', result.get('text', str(result)))}")
|
||||
else:
|
||||
print(f" {i}. {result}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if required environment variables are set
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("⚠️ Warning: OPENAI_API_KEY not set. Some features may not work.")
|
||||
print(" Set your API key: export OPENAI_API_KEY='your-key-here'")
|
||||
|
||||
try:
|
||||
# Run the main agent example
|
||||
main()
|
||||
|
||||
# Run direct API test
|
||||
test_direct_memory_api()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import Error: {e}")
|
||||
print("💡 Try installing graph memory support:")
|
||||
print(" pip install \"praisonaiagents[graph]\"")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
print("💡 Make sure you have the required dependencies and configuration.")
|
||||
@@ -0,0 +1,252 @@
|
||||
import streamlit as st
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.mcp import MCP
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# Configure page
|
||||
st.set_page_config(
|
||||
page_title="AI Airbnb Assistant",
|
||||
page_icon="🏠",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
st.title("🏠 AI Airbnb Assistant")
|
||||
st.write("Enter your accommodation search query below to get started!")
|
||||
|
||||
# Add configuration options in sidebar
|
||||
with st.sidebar:
|
||||
st.header("⚙️ Configuration")
|
||||
|
||||
# LLM Model Selection
|
||||
llm_model = st.selectbox(
|
||||
"Choose LLM Model",
|
||||
options=[
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o",
|
||||
"gpt-3.5-turbo",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022"
|
||||
],
|
||||
index=0,
|
||||
help="Select the language model to use. Avoid provider/model format like 'ollama/llama3.2' in Streamlit."
|
||||
)
|
||||
|
||||
# Debug Mode
|
||||
debug_mode = st.checkbox(
|
||||
"Enable Debug Mode",
|
||||
value=False,
|
||||
help="Enable detailed logging for troubleshooting"
|
||||
)
|
||||
|
||||
# MCP Server Command
|
||||
mcp_command = st.text_input(
|
||||
"MCP Server Command",
|
||||
value="npx -y @openbnb/mcp-server-airbnb --ignore-robots-txt",
|
||||
help="The command to start the MCP server"
|
||||
)
|
||||
|
||||
# Initialize session state
|
||||
if "agent_initialized" not in st.session_state:
|
||||
st.session_state.agent_initialized = False
|
||||
st.session_state.agent = None
|
||||
st.session_state.init_error = None
|
||||
|
||||
# Function to initialize agent
|
||||
def initialize_agent():
|
||||
"""Initialize the agent with proper error handling."""
|
||||
try:
|
||||
with st.spinner("🔄 Initializing AI agent and MCP tools..."):
|
||||
if debug_mode:
|
||||
st.info("🐛 Debug mode enabled - check console for detailed logs")
|
||||
|
||||
# Create MCP tools
|
||||
mcp_tools = MCP(
|
||||
mcp_command,
|
||||
timeout=60,
|
||||
debug=debug_mode
|
||||
)
|
||||
|
||||
# Create agent with session state management
|
||||
agent = Agent(
|
||||
instructions="""You are a helpful Airbnb assistant. Help users find and book apartments.
|
||||
Use the available tools to search for accommodations, get property details, and provide helpful recommendations.
|
||||
Always be polite and provide comprehensive information about properties.""",
|
||||
llm=llm_model,
|
||||
tools=mcp_tools,
|
||||
verbose=debug_mode
|
||||
)
|
||||
|
||||
return agent, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to initialize agent: {str(e)}"
|
||||
if debug_mode:
|
||||
error_msg += f"\n\nFull traceback:\n{traceback.format_exc()}"
|
||||
return None, error_msg
|
||||
|
||||
# Agent initialization section
|
||||
if not st.session_state.agent_initialized:
|
||||
if st.button("🚀 Initialize AI Assistant", type="primary"):
|
||||
agent, error = initialize_agent()
|
||||
|
||||
if agent:
|
||||
st.session_state.agent = agent
|
||||
st.session_state.agent_initialized = True
|
||||
st.session_state.init_error = None
|
||||
st.success("✅ AI Assistant initialized successfully!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.session_state.init_error = error
|
||||
st.error(f"❌ Initialization failed: {error}")
|
||||
|
||||
# Show initialization status
|
||||
if st.session_state.init_error:
|
||||
st.error(f"❌ Initialization Error: {st.session_state.init_error}")
|
||||
|
||||
with st.expander("🔧 Troubleshooting Tips"):
|
||||
st.markdown("""
|
||||
**Common issues and solutions:**
|
||||
|
||||
1. **MCP Server Issues:**
|
||||
- Ensure Node.js and npm are installed
|
||||
- Check if the MCP server command is correct
|
||||
- Try running the MCP command manually first
|
||||
|
||||
2. **LLM Model Issues:**
|
||||
- Make sure you have API keys set as environment variables
|
||||
- Avoid provider/model formats (e.g., "ollama/llama3.2") in Streamlit
|
||||
- Use standard model names (e.g., "gpt-4o-mini")
|
||||
|
||||
3. **Network/Timeout Issues:**
|
||||
- Check your internet connection
|
||||
- Increase timeout if needed
|
||||
- Enable debug mode for more details
|
||||
|
||||
4. **Environment Issues:**
|
||||
- Make sure all required packages are installed
|
||||
- Check Python version compatibility
|
||||
""")
|
||||
|
||||
# Main chat interface (only show if agent is initialized)
|
||||
if st.session_state.agent_initialized and st.session_state.agent:
|
||||
st.success("✅ AI Assistant is ready!")
|
||||
|
||||
# Create two columns for better layout
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# Query input
|
||||
query = st.text_area(
|
||||
"🔍 What are you looking for?",
|
||||
placeholder="e.g., 'Find a 2-bedroom apartment in Paris for next weekend' or 'Show me luxury stays in Tokyo under $200/night'",
|
||||
height=100
|
||||
)
|
||||
|
||||
# Search button
|
||||
search_col1, search_col2 = st.columns([1, 4])
|
||||
with search_col1:
|
||||
search_button = st.button("🔍 Search", type="primary", use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# Tips and examples
|
||||
with st.container():
|
||||
st.markdown("### 💡 Tips")
|
||||
st.markdown("""
|
||||
- Be specific about location and dates
|
||||
- Mention budget preferences
|
||||
- Include number of guests
|
||||
- Ask about amenities
|
||||
""")
|
||||
|
||||
with st.expander("📝 Example Queries"):
|
||||
st.markdown("""
|
||||
- "Find a beachfront apartment in Barcelona for 4 people"
|
||||
- "Show me pet-friendly stays in London under £150/night"
|
||||
- "I need a place with a kitchen near downtown Tokyo"
|
||||
- "Find luxury accommodations in NYC for a business trip"
|
||||
""")
|
||||
|
||||
# Handle search
|
||||
if search_button and query:
|
||||
if not query.strip():
|
||||
st.warning("⚠️ Please enter a search query")
|
||||
else:
|
||||
try:
|
||||
with st.spinner("🔍 Searching for accommodations..."):
|
||||
# Add query to chat history
|
||||
if "chat_history" not in st.session_state:
|
||||
st.session_state.chat_history = []
|
||||
|
||||
st.session_state.chat_history.append({"role": "user", "content": query})
|
||||
|
||||
# Get response from agent
|
||||
result = st.session_state.agent.start(query)
|
||||
|
||||
st.session_state.chat_history.append({"role": "assistant", "content": result})
|
||||
|
||||
# Display result
|
||||
st.markdown("### 🏠 Search Results")
|
||||
st.markdown(result)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Search failed: {str(e)}"
|
||||
st.error(f"❌ {error_msg}")
|
||||
|
||||
if debug_mode:
|
||||
st.error(f"Full traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# Chat history section
|
||||
if "chat_history" in st.session_state and st.session_state.chat_history:
|
||||
st.markdown("---")
|
||||
st.markdown("### 💬 Conversation History")
|
||||
|
||||
for i, message in enumerate(st.session_state.chat_history):
|
||||
if message["role"] == "user":
|
||||
st.markdown(f"**🙋 You:** {message['content']}")
|
||||
else:
|
||||
st.markdown(f"**🤖 Assistant:** {message['content']}")
|
||||
|
||||
if i < len(st.session_state.chat_history) - 1:
|
||||
st.markdown("---")
|
||||
|
||||
# Clear history button
|
||||
if st.button("🗑️ Clear History"):
|
||||
st.session_state.chat_history = []
|
||||
st.rerun()
|
||||
|
||||
else:
|
||||
# Show instructions for first-time users
|
||||
st.info("👆 Please initialize the AI Assistant first using the button above.")
|
||||
|
||||
with st.expander("ℹ️ About this Application"):
|
||||
st.markdown("""
|
||||
This Streamlit application demonstrates how to properly integrate MCP (Model Context Protocol)
|
||||
tools with PraisonAI Agents in a Streamlit environment.
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Proper session state management for Streamlit
|
||||
- ✅ Comprehensive error handling and user feedback
|
||||
- ✅ Debug mode for troubleshooting
|
||||
- ✅ Flexible LLM model selection
|
||||
- ✅ Clean and intuitive user interface
|
||||
|
||||
**Technical Implementation:**
|
||||
- Uses session state to prevent agent re-initialization
|
||||
- Implements proper error handling for MCP setup
|
||||
- Provides detailed troubleshooting guidance
|
||||
- Demonstrates best practices for Streamlit + MCP integration
|
||||
""")
|
||||
|
||||
# Footer
|
||||
st.markdown("---")
|
||||
st.markdown(
|
||||
"""
|
||||
<div style='text-align: center; color: gray;'>
|
||||
Built with PraisonAI Agents + MCP •
|
||||
<a href='https://github.com/MervinPraison/PraisonAI' target='_blank'>GitHub</a> •
|
||||
<a href='https://docs.praisonai.com' target='_blank'>Documentation</a>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
import streamlit as st
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.mcp import MCP
|
||||
|
||||
st.title("AI Airbnb")
|
||||
st.write("Enter your research query below to get started!")
|
||||
|
||||
# CRITICAL: Initialize agent in session state to prevent re-initialization
|
||||
if "agent" not in st.session_state:
|
||||
try:
|
||||
with st.spinner("Initializing AI agent..."):
|
||||
# Initialize agent only once and store in session state
|
||||
st.session_state.agent = Agent(
|
||||
instructions="You help book apartments on Airbnb.",
|
||||
llm="gpt-4o-mini", # ✅ Use standard format instead of "ollama/llama3.2"
|
||||
tools=MCP("npx -y @openbnb/mcp-server-airbnb --ignore-robots-txt"),
|
||||
verbose=True # Enable debugging
|
||||
)
|
||||
st.success("✅ Agent initialized successfully!")
|
||||
except Exception as e:
|
||||
st.error(f"❌ Failed to initialize agent: {str(e)}")
|
||||
st.stop()
|
||||
|
||||
# Create the input field
|
||||
query = st.text_input("Query")
|
||||
|
||||
# Add a search button
|
||||
if st.button("Search"):
|
||||
if query:
|
||||
try:
|
||||
with st.spinner("Researching..."):
|
||||
# Use the agent from session state (already initialized)
|
||||
result = st.session_state.agent.start(query)
|
||||
st.write(result)
|
||||
except Exception as e:
|
||||
st.error(f"❌ Search failed: {str(e)}")
|
||||
else:
|
||||
st.warning("Please enter a query")
|
||||
|
||||
# Add troubleshooting information
|
||||
with st.expander("🔧 Troubleshooting"):
|
||||
st.markdown("""
|
||||
**Key differences from problematic code:**
|
||||
|
||||
1. **Session State**: Agent is initialized once in `st.session_state.agent`
|
||||
2. **LLM Format**: Using `"gpt-4o-mini"` instead of `"ollama/llama3.2"`
|
||||
3. **Error Handling**: Proper try-catch blocks with user feedback
|
||||
4. **Initialization Check**: Agent only created once, not on every interaction
|
||||
|
||||
**If you still have issues:**
|
||||
- Make sure you have Node.js installed for the MCP server
|
||||
- Set your OpenAI API key: `export OPENAI_API_KEY="your-key"`
|
||||
- Try the debug mode by setting `verbose=True`
|
||||
""")
|
||||
@@ -0,0 +1,81 @@
|
||||
# Domain Context Solution
|
||||
|
||||
This example demonstrates how to solve domain context issues in multi-agent systems using existing PraisonAI features.
|
||||
|
||||
## Problem
|
||||
|
||||
In hierarchical multi-agent workflows, tools often default to generic values like "example.com" instead of using user-specified domains. This happens because domain context doesn't automatically reach the tool execution level.
|
||||
|
||||
## Solution: 4-Layer Context Approach
|
||||
|
||||
This solution uses four existing PraisonAI features to ensure domain context is properly maintained:
|
||||
|
||||
### 1. **Custom Tool Wrappers**
|
||||
```python
|
||||
def create_domain_aware_tools(target_domain: str):
|
||||
def query_fofa(query: str = None) -> dict:
|
||||
if query is None or query == "example.com":
|
||||
query = target_domain
|
||||
# Tool implementation with domain injection
|
||||
```
|
||||
|
||||
### 2. **Agent Instructions**
|
||||
```python
|
||||
agent = Agent(
|
||||
instructions=f"""
|
||||
CRITICAL DOMAIN CONTEXT: You are working exclusively with the domain '{domain}'.
|
||||
All operations, tool calls, and analysis must focus on this specific domain.
|
||||
Never use example.com or generic examples - always use {domain}.
|
||||
""",
|
||||
tools=domain_tools
|
||||
)
|
||||
```
|
||||
|
||||
### 3. **Task Context Parameter**
|
||||
```python
|
||||
task = Task(
|
||||
description=f"Analyze domain {domain}",
|
||||
context=[domain_context.get_context()], # Pass domain context
|
||||
agent=agent
|
||||
)
|
||||
```
|
||||
|
||||
### 4. **Shared Memory**
|
||||
```python
|
||||
agents = PraisonAIAgents(
|
||||
agents=[agent1, agent2],
|
||||
tasks=[task1, task2],
|
||||
memory=True, # Enable shared memory for context
|
||||
memory_config={
|
||||
"provider": "rag",
|
||||
"config": {"collection_name": f"{domain}_analysis"}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from domain_context_solution import main
|
||||
|
||||
# Set your target domain
|
||||
domain = "your-domain.com"
|
||||
|
||||
# Run the hierarchical workflow
|
||||
main()
|
||||
```
|
||||
|
||||
## Key Benefits
|
||||
|
||||
- ✅ **No Core SDK Changes** - Uses only existing framework features
|
||||
- ✅ **Multi-Layer Reliability** - Context maintained through multiple mechanisms
|
||||
- ✅ **Domain-Specific Results** - Tools focus on specified domain
|
||||
- ✅ **Backward Compatible** - Doesn't break existing functionality
|
||||
- ✅ **Scalable Pattern** - Works for any domain or context type
|
||||
|
||||
## Files
|
||||
|
||||
- `domain-context-solution.py` - Complete working solution
|
||||
- `README-domain-context.md` - This documentation
|
||||
|
||||
This pattern can be adapted for any context passing scenario in PraisonAI multi-agent systems.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Secondary Market Research Agent System
|
||||
|
||||
A comprehensive multi-agent system for generating customized secondary market research reports with FastAPI integration.
|
||||
|
||||
## 🎯 Features
|
||||
|
||||
- **Multi-Agent Research System**: Specialized agents for different research areas
|
||||
- **Customizable Parameters**: Company, geography, industry, and research sections
|
||||
- **Comprehensive Reports**: Market overview, competitive analysis, financial performance, growth opportunities, and risk assessment
|
||||
- **FastAPI Integration**: Production-ready REST API
|
||||
- **Professional Output**: Business-ready reports suitable for decision-making
|
||||
|
||||
## 📁 Files
|
||||
|
||||
- `secondary-market-research.py` - Main Python implementation
|
||||
- `secondary-market-research-api.py` - FastAPI web service
|
||||
- `secondary_market_research_agents.yaml` - YAML configuration
|
||||
- `secondary_market_research_agents.ipynb` - Jupyter notebook examples
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Python Implementation
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, Tools
|
||||
import asyncio
|
||||
|
||||
# Configure research parameters
|
||||
config = MarketResearchConfig(
|
||||
company="Tesla",
|
||||
geography="North America",
|
||||
industry="Electric Vehicles",
|
||||
sections=["market_overview", "competitive_analysis", "financial_performance"]
|
||||
)
|
||||
|
||||
# Run research
|
||||
results = await run_market_research(config)
|
||||
```
|
||||
|
||||
### 2. FastAPI Web Service
|
||||
|
||||
```bash
|
||||
# Start the API server
|
||||
uvicorn secondary-market-research-api:app --reload --port 8000
|
||||
|
||||
# Visit API documentation
|
||||
http://localhost:8000/docs
|
||||
```
|
||||
|
||||
### 3. YAML Configuration
|
||||
|
||||
```yaml
|
||||
framework: "crewai"
|
||||
topic: "Secondary Market Research Analysis"
|
||||
|
||||
variables:
|
||||
company: "Tesla"
|
||||
geography: "North America"
|
||||
industry: "Electric Vehicles"
|
||||
|
||||
roles:
|
||||
market_overview_specialist:
|
||||
role: "Market Overview Specialist"
|
||||
# ... configuration details
|
||||
```
|
||||
|
||||
## 📊 Research Sections
|
||||
|
||||
Choose from the following research sections:
|
||||
|
||||
- **market_overview**: Market size, trends, and growth drivers
|
||||
- **competitive_analysis**: Competitor analysis and market positioning
|
||||
- **financial_performance**: Financial metrics and benchmarking
|
||||
- **growth_opportunities**: Strategic growth vectors and opportunities
|
||||
- **risk_assessment**: Risk factors and mitigation strategies
|
||||
|
||||
## 🌍 Use Cases
|
||||
|
||||
### Technology Companies
|
||||
```python
|
||||
config = MarketResearchConfig(
|
||||
company="OpenAI",
|
||||
geography="United States",
|
||||
industry="Artificial Intelligence",
|
||||
sections=["market_overview", "competitive_analysis", "growth_opportunities"]
|
||||
)
|
||||
```
|
||||
|
||||
### Automotive Industry
|
||||
```python
|
||||
config = MarketResearchConfig(
|
||||
company="BMW",
|
||||
geography="Europe",
|
||||
industry="Luxury Automobiles",
|
||||
sections=["market_overview", "competitive_analysis", "financial_performance", "risk_assessment"]
|
||||
)
|
||||
```
|
||||
|
||||
### Healthcare/Pharmaceuticals
|
||||
```python
|
||||
config = MarketResearchConfig(
|
||||
company="Pfizer",
|
||||
geography="Global",
|
||||
industry="Pharmaceuticals",
|
||||
sections=["market_overview", "competitive_analysis", "growth_opportunities", "risk_assessment"]
|
||||
)
|
||||
```
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### Generate Research Report
|
||||
```http
|
||||
POST /research/generate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"company": "Tesla",
|
||||
"geography": "North America",
|
||||
"industry": "Electric Vehicles",
|
||||
"sections": ["market_overview", "competitive_analysis"],
|
||||
"format": "json"
|
||||
}
|
||||
```
|
||||
|
||||
### Check Job Status
|
||||
```http
|
||||
GET /research/status/{job_id}
|
||||
```
|
||||
|
||||
### Download Report
|
||||
```http
|
||||
GET /research/reports/{job_id}
|
||||
```
|
||||
|
||||
### Get Templates
|
||||
```http
|
||||
GET /research/templates
|
||||
```
|
||||
|
||||
## 📈 Output Format
|
||||
|
||||
The system generates comprehensive reports with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"job_id": "uuid",
|
||||
"company": "Tesla",
|
||||
"geography": "North America",
|
||||
"industry": "Electric Vehicles",
|
||||
"generated_at": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"executive_summary": "...",
|
||||
"research_findings": {
|
||||
"market_overview_research": {
|
||||
"content": "Market analysis...",
|
||||
"agent": "Market Overview Specialist"
|
||||
},
|
||||
"competitive_analysis": {
|
||||
"content": "Competitive intelligence...",
|
||||
"agent": "Competitive Intelligence Analyst"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
```bash
|
||||
pip install praisonai[crewai]
|
||||
pip install fastapi uvicorn
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Set your API keys:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-api-key"
|
||||
export OPENAI_MODEL_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
## 📝 Customization
|
||||
|
||||
### Adding New Research Sections
|
||||
|
||||
1. Create a new agent in `create_market_research_agents()`
|
||||
2. Add corresponding task in `create_research_tasks()`
|
||||
3. Update the sections list in configuration
|
||||
|
||||
### Geographic Regions
|
||||
|
||||
Supported regions include:
|
||||
- North America
|
||||
- Europe
|
||||
- Asia Pacific
|
||||
- Global
|
||||
- Custom regions
|
||||
|
||||
### Industries
|
||||
|
||||
The system works across industries:
|
||||
- Technology
|
||||
- Automotive
|
||||
- Healthcare/Pharmaceuticals
|
||||
- Financial Services
|
||||
- Retail/E-commerce
|
||||
- Energy
|
||||
- Manufacturing
|
||||
- And more...
|
||||
|
||||
## 🎓 Examples
|
||||
|
||||
See the Jupyter notebook `secondary_market_research_agents.ipynb` for detailed examples including:
|
||||
|
||||
- YAML-based configuration
|
||||
- Python API usage
|
||||
- FastAPI integration
|
||||
- Multiple industry examples
|
||||
- Customization patterns
|
||||
|
||||
## 🚀 Production Deployment
|
||||
|
||||
For production use:
|
||||
|
||||
1. Deploy FastAPI with proper ASGI server (Gunicorn + Uvicorn)
|
||||
2. Use Redis or database for job status storage
|
||||
3. Implement proper error handling and logging
|
||||
4. Add authentication and rate limiting
|
||||
5. Set up monitoring and alerting
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is part of PraisonAI and follows the same licensing terms.
|
||||
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
SOLUTION: Domain Context Issue Fix using Existing PraisonAI Features
|
||||
|
||||
This solution fixes the issue where agents default to "example.com" instead of using
|
||||
the specified domain "eenadu.net" by leveraging existing PraisonAI context features.
|
||||
|
||||
Key improvements:
|
||||
1. Domain context passed through task context parameter
|
||||
2. Agent instructions include domain-specific guidance
|
||||
3. Custom tool wrappers that inject domain context
|
||||
4. Shared memory for consistent domain context
|
||||
|
||||
No core SDK changes required - uses only existing framework features.
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
from dotenv import load_dotenv
|
||||
from qdrant_client import QdrantClient
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, Memory
|
||||
|
||||
# Load environment variables from .env.local
|
||||
if not load_dotenv(dotenv_path=".env.local"):
|
||||
raise FileNotFoundError(".env.local file not found. Please ensure it exists in the working directory.")
|
||||
|
||||
# API Keys
|
||||
api_keys = {
|
||||
"WHOXY_API_KEY": os.getenv("WHOXY_API_KEY"),
|
||||
"FOFA_API_KEY": os.getenv("FOFA_API_KEY"),
|
||||
"API_NINJAS_KEY": os.getenv("API_NINJAS_KEY"),
|
||||
"QDRANT_API_KEY": os.getenv("QDRANT_API_KEY"),
|
||||
}
|
||||
|
||||
# Debug API Keys
|
||||
for key, value in api_keys.items():
|
||||
if not value:
|
||||
raise ValueError(f"Missing API key: {key}. Ensure it is set in the environment variables or .env.local.")
|
||||
|
||||
# Qdrant Configuration
|
||||
qdrant_url = "https://d32d17dc-6b1d-4e13-9bb5-94a4d9f4b457.europe-west3-0.gcp.cloud.qdrant.io:6333"
|
||||
qdrant_client = QdrantClient(url=qdrant_url, api_key=api_keys["QDRANT_API_KEY"])
|
||||
|
||||
# Domain Configuration - This is the key to solving the context issue
|
||||
class DomainContext:
|
||||
def __init__(self, target_domain: str):
|
||||
self.target_domain = target_domain
|
||||
self.context_description = f"DOMAIN CONTEXT: All tool operations must focus exclusively on the domain '{target_domain}'. Never use example.com or any other domain."
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
"domain": self.target_domain,
|
||||
"instructions": self.context_description,
|
||||
"constraints": [f"Only analyze {self.target_domain}", "No generic examples", "Domain-specific results only"]
|
||||
}
|
||||
|
||||
# Initialize domain context
|
||||
domain = "eenadu.net"
|
||||
domain_context = DomainContext(domain)
|
||||
|
||||
# SOLUTION 1: Custom Tool Wrappers with Domain Context Injection
|
||||
def create_domain_aware_tools(target_domain: str):
|
||||
"""Create domain-aware versions of tools that inject the target domain"""
|
||||
|
||||
def query_fofa(query: str = None) -> dict:
|
||||
# Use target domain if no specific query provided
|
||||
if query is None or query == "example.com":
|
||||
query = target_domain
|
||||
# Ensure domain context is considered
|
||||
enhanced_query = f"{target_domain} {query}" if target_domain not in query else query
|
||||
|
||||
base64_query = base64.b64encode(enhanced_query.encode("utf-8")).decode("utf-8")
|
||||
response = requests.get(
|
||||
f"https://fofa.info/api/v1/search/all?key={api_keys['FOFA_API_KEY']}&qbase64={base64_query}&size=100&fields=jarm,host,domain,title,ip,port,protocol,base_protocol,country,country_name,region,city,longitude,latitude,as_number,as_organization,os,server,icp,header,banner,cert,link,certs_issuer_org,certs_issuer_cn,certs_subject_org,certs_subject_cn,tls_ja3s,tls_version"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def query_crtsh(domain_param: str = None) -> list:
|
||||
# Use target domain if no specific domain provided
|
||||
if domain_param is None or domain_param == "example.com":
|
||||
domain_param = target_domain
|
||||
|
||||
response = requests.get(f"https://crt.sh/?q={domain_param}&output=json")
|
||||
return response.json()
|
||||
|
||||
def query_whoxy(email_or_org: str = None) -> dict:
|
||||
# Use target domain if no specific parameter provided
|
||||
if email_or_org is None or email_or_org == "example.com":
|
||||
email_or_org = target_domain
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.whoxy.com/?key={api_keys['WHOXY_API_KEY']}&reverse=whois&search={email_or_org}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def query_api_ninjas(domain_param: str = None) -> dict:
|
||||
# Use target domain if no specific domain provided
|
||||
if domain_param is None or domain_param == "example.com":
|
||||
domain_param = target_domain
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.api-ninjas.com/v1/dnslookup?domain={domain_param}",
|
||||
headers={"X-Api-Key": api_keys["API_NINJAS_KEY"]},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def query_networkcalc(domain_param: str = None) -> dict:
|
||||
# Use target domain if no specific domain provided
|
||||
if domain_param is None or domain_param == "example.com":
|
||||
domain_param = target_domain
|
||||
|
||||
response = requests.get(f"https://networkcalc.com/api/dns/lookup/{domain_param}")
|
||||
return response.json()
|
||||
|
||||
return [query_fofa, query_crtsh, query_whoxy, query_api_ninjas, query_networkcalc]
|
||||
|
||||
# Create domain-aware tools
|
||||
domain_tools = create_domain_aware_tools(domain)
|
||||
|
||||
def perform_reverse_lookups(domain: str, manager_memory: dict):
|
||||
"""Perform WHOXY lookups and store related domains."""
|
||||
reverse_data = domain_tools[2](domain) # Use domain-aware whoxy tool
|
||||
email = reverse_data.get("email_address", "")
|
||||
organization = reverse_data.get("organization_name", "")
|
||||
owner = reverse_data.get("owner_name", "")
|
||||
|
||||
# Perform additional reverse lookups
|
||||
related_by_email = domain_tools[2](email) if email else {}
|
||||
related_by_org = domain_tools[2](organization) if organization else {}
|
||||
related_by_owner = domain_tools[2](owner) if owner else {}
|
||||
|
||||
# Filter out unrelated domains
|
||||
in_scope_domains = set()
|
||||
for lookup_data in [related_by_email, related_by_org, related_by_owner]:
|
||||
for domain_info in lookup_data.get("searchResult", {}).get("domains", []):
|
||||
if domain in domain_info or is_in_scope(domain_info):
|
||||
in_scope_domains.add(domain_info)
|
||||
|
||||
# Store results in memory
|
||||
manager_memory["related_domains"] = list(in_scope_domains)
|
||||
|
||||
def is_in_scope(domain: str) -> bool:
|
||||
"""Filter domains based on scope criteria."""
|
||||
out_of_scope_keywords = ["example.com", "google.com", "microsoft.com", "cloudflare.com"]
|
||||
return not any(keyword in domain for keyword in out_of_scope_keywords)
|
||||
|
||||
# SOLUTION 2: Agents with Domain-Specific Instructions
|
||||
manager_agent = Agent(
|
||||
name="Manager Agent",
|
||||
role="Coordinate data collection and refinement",
|
||||
goal=f"Collect user input, oversee tasks, and perform WHOXY lookups for related domains specifically for {domain}.",
|
||||
backstory=f"Strategically manages all agents and ensures focused execution on the domain {domain}.",
|
||||
instructions=f"""
|
||||
CRITICAL DOMAIN CONTEXT: You are working exclusively with the domain '{domain}'.
|
||||
|
||||
All operations, tool calls, and analysis must focus on this specific domain.
|
||||
When coordinating with other agents, ensure they understand the domain context.
|
||||
Never use example.com or generic examples - always use {domain}.
|
||||
|
||||
Your primary responsibility is to ensure all agents maintain focus on {domain} throughout the workflow.
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
|
||||
tool_agent = Agent(
|
||||
name="Tool Agent",
|
||||
role="Run initial tool queries",
|
||||
goal=f"Gather subdomains, DNS records, reverse WHOIS data, and FOFA data specifically for {domain}.",
|
||||
backstory=f"Tool specialist focused exclusively on analyzing the domain {domain}. Expert in running information-gathering queries using all available tools.",
|
||||
instructions=f"""
|
||||
CRITICAL DOMAIN CONTEXT: Your exclusive focus is the domain '{domain}'.
|
||||
|
||||
When using any tool:
|
||||
1. Always use '{domain}' as the target domain
|
||||
2. Never use example.com or any other domain
|
||||
3. If a tool asks for a domain parameter, provide '{domain}'
|
||||
4. If a tool asks for a query, include '{domain}' in the query
|
||||
5. All searches and analysis must be domain-specific to '{domain}'
|
||||
|
||||
Remember: You are a specialist for {domain} only. No generic examples or other domains.
|
||||
""",
|
||||
tools=domain_tools, # Use domain-aware tools
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
|
||||
sr_pentester_agent = Agent(
|
||||
name="Senior Pentester",
|
||||
role="Analyze tool results for vulnerabilities",
|
||||
goal=f"Investigate vulnerabilities and organize data for deep dives specifically for {domain}.",
|
||||
backstory=f"An expert in penetration testing and data correlation with deep expertise in analyzing {domain}.",
|
||||
instructions=f"""
|
||||
CRITICAL DOMAIN CONTEXT: You are analyzing security findings for the domain '{domain}' exclusively.
|
||||
|
||||
When reviewing tool results:
|
||||
1. Focus only on findings related to '{domain}'
|
||||
2. Filter out any results from other domains
|
||||
3. Prioritize vulnerabilities specific to '{domain}'
|
||||
4. Organize findings with '{domain}' as the central focus
|
||||
|
||||
Your expertise is domain-specific to {domain}. All analysis must be relevant to this domain only.
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
|
||||
ciso_agent = Agent(
|
||||
name="CISO Agent",
|
||||
role="Executive Security Analysis",
|
||||
goal=f"Review findings and provide high-level recommendations specifically for {domain}.",
|
||||
backstory=f"Provides strategic insights and ensures alignment with organizational goals for {domain} security posture.",
|
||||
instructions=f"""
|
||||
CRITICAL DOMAIN CONTEXT: You are providing executive security analysis for the domain '{domain}' exclusively.
|
||||
|
||||
In your strategic recommendations:
|
||||
1. Focus on security posture of '{domain}' only
|
||||
2. Provide recommendations specific to '{domain}'
|
||||
3. Consider business impact on '{domain}' operations
|
||||
4. Align security strategy with '{domain}' organizational goals
|
||||
|
||||
Your strategic insights must be domain-specific to {domain}. No generic recommendations.
|
||||
""",
|
||||
tools=[],
|
||||
verbose=True,
|
||||
llm="gpt-4o",
|
||||
markdown=True,
|
||||
memory={"short_term": {"target_domain": domain}, "long_term": qdrant_client},
|
||||
)
|
||||
|
||||
# SOLUTION 3: Tasks with Domain Context Parameter
|
||||
tool_query_task = Task(
|
||||
name="tool_query_task",
|
||||
description=f"Run all tools to gather initial domain data for {domain}. Use the domain {domain} exclusively for all tool operations.",
|
||||
expected_output=f"Raw data from all tools for the domain {domain} stored in Qdrant.",
|
||||
agent=tool_agent,
|
||||
context=[domain_context.get_context()] # Pass domain context to task
|
||||
)
|
||||
|
||||
pentester_analysis_task = Task(
|
||||
name="pentester_analysis_task",
|
||||
description=f"Analyze tool output for vulnerabilities and prioritize areas of focus for the domain {domain}.",
|
||||
expected_output=f"Organized and prioritized security findings for {domain} stored in Qdrant.",
|
||||
agent=sr_pentester_agent,
|
||||
context=[domain_context.get_context(), tool_query_task] # Include domain context and previous task
|
||||
)
|
||||
|
||||
ciso_review_task = Task(
|
||||
name="ciso_review_task",
|
||||
description=f"Review pentester findings and provide strategic recommendations for the domain {domain}.",
|
||||
expected_output=f"High-level executive security recommendations for {domain} stored in Qdrant.",
|
||||
agent=ciso_agent,
|
||||
context=[domain_context.get_context(), pentester_analysis_task] # Include domain context and previous task
|
||||
)
|
||||
|
||||
# SOLUTION 4: Agents with Shared Memory Configuration
|
||||
agents = PraisonAIAgents(
|
||||
agents=[manager_agent, tool_agent, sr_pentester_agent, ciso_agent],
|
||||
tasks=[tool_query_task, pentester_analysis_task, ciso_review_task],
|
||||
verbose=True,
|
||||
process="hierarchical",
|
||||
manager_llm="gpt-4o",
|
||||
memory=True, # Enable shared memory for domain context
|
||||
memory_config={
|
||||
"provider": "rag",
|
||||
"config": {
|
||||
"collection_name": f"{domain.replace('.', '_')}_security_analysis"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Main Execution Workflow
|
||||
def main():
|
||||
print(f"Welcome to the Hierarchical Pentesting Workflow for {domain}!")
|
||||
|
||||
# Store domain context in memory for all agents
|
||||
domain_info = f"Target domain for analysis: {domain}. All operations must focus on this domain exclusively."
|
||||
|
||||
# Initialize shared context for all agents
|
||||
for agent in [manager_agent, tool_agent, sr_pentester_agent, ciso_agent]:
|
||||
agent.memory["short_term"]["domain_context"] = domain_context.get_context()
|
||||
agent.memory["short_term"]["target_domain"] = domain
|
||||
|
||||
print(f"Domain context initialized: {domain}")
|
||||
print(f"All agents configured to focus exclusively on: {domain}")
|
||||
|
||||
# Execute the workflow
|
||||
result = agents.start()
|
||||
|
||||
# Serialize TaskOutput to JSON-serializable format
|
||||
serialized_result = []
|
||||
for task in agents.tasks:
|
||||
if hasattr(task, "name") and hasattr(task, "output"):
|
||||
serialized_result.append({
|
||||
"task_name": task.name,
|
||||
"domain": domain,
|
||||
"output": task.output.to_dict() if hasattr(task.output, "to_dict") else str(task.output),
|
||||
})
|
||||
else:
|
||||
print(f"Skipping task: {task} due to missing attributes.")
|
||||
|
||||
# Output results
|
||||
print(f"Workflow Results for {domain}:")
|
||||
print(json.dumps(serialized_result, indent=4))
|
||||
|
||||
# Verify domain context was maintained
|
||||
print(f"\nDomain Context Verification:")
|
||||
print(f"Target Domain: {domain}")
|
||||
print(f"Context properly maintained: {'YES' if all('target_domain' in agent.memory['short_term'] for agent in agents.agents) else 'NO'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
Secondary Market Research Agent System
|
||||
|
||||
This system creates a comprehensive market research solution with customizable
|
||||
inputs for company, geography, and industry sections. It generates detailed
|
||||
reports suitable for secondary market research.
|
||||
|
||||
Features:
|
||||
- Multi-agent system with specialized research roles
|
||||
- Customizable geography and company focus
|
||||
- Structured report generation
|
||||
- Market analysis and competitive intelligence
|
||||
- FastAPI integration ready
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, Tools
|
||||
from typing import Dict, List, Any, Optional
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
class MarketResearchConfig:
|
||||
"""Configuration class for market research parameters"""
|
||||
|
||||
def __init__(self,
|
||||
company: str = "Tesla",
|
||||
geography: str = "North America",
|
||||
industry: str = "Electric Vehicles",
|
||||
sections: List[str] = None):
|
||||
self.company = company
|
||||
self.geography = geography
|
||||
self.industry = industry
|
||||
self.sections = sections or [
|
||||
"market_overview",
|
||||
"competitive_analysis",
|
||||
"financial_performance",
|
||||
"growth_opportunities",
|
||||
"risk_assessment"
|
||||
]
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
def create_market_research_agents(config: MarketResearchConfig):
|
||||
"""Create specialized market research agents"""
|
||||
|
||||
# Market Overview Agent
|
||||
market_overview_agent = Agent(
|
||||
name="Market Overview Specialist",
|
||||
role="Market Analysis Expert",
|
||||
goal=f"Analyze the {config.industry} market in {config.geography}",
|
||||
instructions=f"""
|
||||
You are a market research specialist focused on {config.industry} in {config.geography}.
|
||||
Analyze market size, trends, growth drivers, and overall market dynamics.
|
||||
Focus on current market conditions and future projections.
|
||||
Provide quantitative data where possible and cite reliable sources.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Competitive Intelligence Agent
|
||||
competitive_agent = Agent(
|
||||
name="Competitive Intelligence Analyst",
|
||||
role="Competition Research Expert",
|
||||
goal=f"Analyze {config.company}'s competitive landscape",
|
||||
instructions=f"""
|
||||
You are a competitive intelligence specialist analyzing {config.company}'s position
|
||||
in the {config.industry} market within {config.geography}.
|
||||
Identify key competitors, market share, competitive advantages, and positioning.
|
||||
Analyze competitor strategies, strengths, weaknesses, and market positioning.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Financial Performance Agent
|
||||
financial_agent = Agent(
|
||||
name="Financial Performance Analyst",
|
||||
role="Financial Research Expert",
|
||||
goal=f"Analyze {config.company}'s financial performance and metrics",
|
||||
instructions=f"""
|
||||
You are a financial analyst specializing in {config.industry} companies.
|
||||
Research {config.company}'s financial performance, revenue trends, profitability,
|
||||
key financial metrics, and compare with industry benchmarks.
|
||||
Focus on recent financial data and performance indicators.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Growth Opportunities Agent
|
||||
growth_agent = Agent(
|
||||
name="Growth Opportunities Researcher",
|
||||
role="Strategic Growth Expert",
|
||||
goal=f"Identify growth opportunities for {config.company}",
|
||||
instructions=f"""
|
||||
You are a strategic growth analyst focusing on {config.industry} in {config.geography}.
|
||||
Identify emerging opportunities, market gaps, expansion possibilities,
|
||||
new product/service opportunities, and strategic growth vectors for {config.company}.
|
||||
Consider technological trends, regulatory changes, and market evolution.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Risk Assessment Agent
|
||||
risk_agent = Agent(
|
||||
name="Risk Assessment Specialist",
|
||||
role="Risk Analysis Expert",
|
||||
goal=f"Assess risks and challenges for {config.company}",
|
||||
instructions=f"""
|
||||
You are a risk assessment specialist for {config.industry} companies.
|
||||
Identify and analyze potential risks, challenges, and threats facing {config.company}
|
||||
in {config.geography}. Consider regulatory, competitive, technological,
|
||||
economic, and operational risks.
|
||||
""",
|
||||
tools=[Tools.internet_search],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Report Synthesizer Agent
|
||||
synthesizer_agent = Agent(
|
||||
name="Research Report Synthesizer",
|
||||
role="Report Writing Expert",
|
||||
goal="Synthesize research into comprehensive market research report",
|
||||
instructions=f"""
|
||||
You are an expert report writer specializing in market research reports.
|
||||
Synthesize all research findings into a comprehensive, well-structured
|
||||
secondary market research report for {config.company} in {config.industry}.
|
||||
|
||||
Create a professional report with:
|
||||
- Executive Summary
|
||||
- Market Overview
|
||||
- Competitive Analysis
|
||||
- Financial Performance Analysis
|
||||
- Growth Opportunities
|
||||
- Risk Assessment
|
||||
- Conclusions and Recommendations
|
||||
|
||||
Use clear headings, bullet points, and structured formatting.
|
||||
Include key insights and actionable recommendations.
|
||||
""",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
return {
|
||||
"market_overview": market_overview_agent,
|
||||
"competitive": competitive_agent,
|
||||
"financial": financial_agent,
|
||||
"growth": growth_agent,
|
||||
"risk": risk_agent,
|
||||
"synthesizer": synthesizer_agent
|
||||
}
|
||||
|
||||
def create_research_tasks(agents: Dict[str, Agent], config: MarketResearchConfig):
|
||||
"""Create research tasks for the agents"""
|
||||
|
||||
tasks = []
|
||||
|
||||
# Market Overview Task
|
||||
if "market_overview" in config.sections:
|
||||
market_task = Task(
|
||||
name="market_overview_research",
|
||||
description=f"""
|
||||
Conduct comprehensive market overview research for {config.industry}
|
||||
in {config.geography}. Include:
|
||||
- Market size and valuation
|
||||
- Growth trends and projections
|
||||
- Key market drivers
|
||||
- Market segments
|
||||
- Regulatory environment
|
||||
""",
|
||||
expected_output="Detailed market overview analysis with quantitative data",
|
||||
agent=agents["market_overview"],
|
||||
is_start=True,
|
||||
next_tasks=["competitive_analysis"] if "competitive_analysis" in config.sections else ["synthesis"]
|
||||
)
|
||||
tasks.append(market_task)
|
||||
|
||||
# Competitive Analysis Task
|
||||
if "competitive_analysis" in config.sections:
|
||||
competitive_task = Task(
|
||||
name="competitive_analysis",
|
||||
description=f"""
|
||||
Analyze competitive landscape for {config.company} in {config.industry}:
|
||||
- Identify top 5-7 competitors
|
||||
- Market share analysis
|
||||
- Competitive positioning
|
||||
- Competitor strengths and weaknesses
|
||||
- Competitive strategies
|
||||
""",
|
||||
expected_output="Comprehensive competitive intelligence report",
|
||||
agent=agents["competitive"],
|
||||
next_tasks=["financial_analysis"] if "financial_performance" in config.sections else ["synthesis"]
|
||||
)
|
||||
tasks.append(competitive_task)
|
||||
|
||||
# Financial Performance Task
|
||||
if "financial_performance" in config.sections:
|
||||
financial_task = Task(
|
||||
name="financial_analysis",
|
||||
description=f"""
|
||||
Analyze {config.company}'s financial performance:
|
||||
- Revenue trends (last 3-5 years)
|
||||
- Profitability metrics
|
||||
- Key financial ratios
|
||||
- Industry benchmark comparison
|
||||
- Financial health assessment
|
||||
""",
|
||||
expected_output="Detailed financial performance analysis",
|
||||
agent=agents["financial"],
|
||||
next_tasks=["growth_opportunities"] if "growth_opportunities" in config.sections else ["synthesis"]
|
||||
)
|
||||
tasks.append(financial_task)
|
||||
|
||||
# Growth Opportunities Task
|
||||
if "growth_opportunities" in config.sections:
|
||||
growth_task = Task(
|
||||
name="growth_opportunities",
|
||||
description=f"""
|
||||
Identify growth opportunities for {config.company}:
|
||||
- Market expansion opportunities
|
||||
- New product/service opportunities
|
||||
- Strategic partnerships potential
|
||||
- Technology advancement opportunities
|
||||
- Emerging market trends
|
||||
""",
|
||||
expected_output="Strategic growth opportunities analysis",
|
||||
agent=agents["growth"],
|
||||
next_tasks=["risk_assessment"] if "risk_assessment" in config.sections else ["synthesis"]
|
||||
)
|
||||
tasks.append(growth_task)
|
||||
|
||||
# Risk Assessment Task
|
||||
if "risk_assessment" in config.sections:
|
||||
risk_task = Task(
|
||||
name="risk_assessment",
|
||||
description=f"""
|
||||
Assess risks and challenges for {config.company}:
|
||||
- Competitive threats
|
||||
- Regulatory risks
|
||||
- Market risks
|
||||
- Operational challenges
|
||||
- Technology disruption risks
|
||||
""",
|
||||
expected_output="Comprehensive risk assessment report",
|
||||
agent=agents["risk"],
|
||||
next_tasks=["synthesis"]
|
||||
)
|
||||
tasks.append(risk_task)
|
||||
|
||||
# Synthesis Task
|
||||
synthesis_task = Task(
|
||||
name="synthesis",
|
||||
description=f"""
|
||||
Synthesize all research findings into a comprehensive secondary market
|
||||
research report for {config.company} in {config.industry} ({config.geography}).
|
||||
|
||||
Create a professional report with clear sections, insights, and recommendations.
|
||||
Focus on actionable intelligence for business decision-making.
|
||||
""",
|
||||
expected_output="Complete secondary market research report",
|
||||
agent=agents["synthesizer"],
|
||||
context=tasks # Use all previous tasks as context
|
||||
)
|
||||
tasks.append(synthesis_task)
|
||||
|
||||
return tasks
|
||||
|
||||
async def run_market_research(config: MarketResearchConfig) -> Dict[str, Any]:
|
||||
"""Run the complete market research workflow"""
|
||||
|
||||
print(f"\n🔍 Starting Secondary Market Research for {config.company}")
|
||||
print(f"📍 Geography: {config.geography}")
|
||||
print(f"🏭 Industry: {config.industry}")
|
||||
print(f"📊 Sections: {', '.join(config.sections)}")
|
||||
print("=" * 60)
|
||||
|
||||
# Create agents and tasks
|
||||
agents = create_market_research_agents(config)
|
||||
tasks = create_research_tasks(agents, config)
|
||||
|
||||
# Create workflow
|
||||
workflow = PraisonAIAgents(
|
||||
agents=list(agents.values()),
|
||||
tasks=tasks,
|
||||
process="workflow",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Execute research
|
||||
results = await workflow.astart()
|
||||
|
||||
return {
|
||||
"config": config.__dict__,
|
||||
"results": results,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Example usage
|
||||
async def main():
|
||||
"""Main function demonstrating the market research system"""
|
||||
|
||||
# Example 1: Tesla Electric Vehicle Research
|
||||
tesla_config = MarketResearchConfig(
|
||||
company="Tesla",
|
||||
geography="North America",
|
||||
industry="Electric Vehicles",
|
||||
sections=["market_overview", "competitive_analysis", "financial_performance", "growth_opportunities"]
|
||||
)
|
||||
|
||||
tesla_results = await run_market_research(tesla_config)
|
||||
|
||||
print("\n📋 Research Complete!")
|
||||
print("=" * 60)
|
||||
|
||||
# Display results
|
||||
if "task_results" in tesla_results["results"]:
|
||||
for task_name, result in tesla_results["results"]["task_results"].items():
|
||||
if result:
|
||||
print(f"\n📌 {task_name.upper()}")
|
||||
print("-" * 40)
|
||||
print(result.raw[:500] + "..." if len(result.raw) > 500 else result.raw)
|
||||
|
||||
return tesla_results
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the market research
|
||||
results = asyncio.run(main())
|
||||
|
||||
# Save results to file
|
||||
with open("market_research_results.json", "w") as f:
|
||||
json.dump(results, f, indent=2, default=str)
|
||||
|
||||
print(f"\n💾 Results saved to market_research_results.json")
|
||||
@@ -16,7 +16,8 @@ from ..main import (
|
||||
display_self_reflection,
|
||||
ReflectionOutput,
|
||||
client,
|
||||
adisplay_instruction
|
||||
adisplay_instruction,
|
||||
approval_callback
|
||||
)
|
||||
import inspect
|
||||
import uuid
|
||||
@@ -570,6 +571,35 @@ Your Goal: {self.goal}
|
||||
"""
|
||||
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
|
||||
|
||||
# Check if approval is required for this tool
|
||||
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, ApprovalDecision
|
||||
if is_approval_required(function_name):
|
||||
risk_level = get_risk_level(function_name)
|
||||
logging.info(f"Tool {function_name} requires approval (risk level: {risk_level})")
|
||||
|
||||
# Use global approval callback or default console callback
|
||||
callback = approval_callback or console_approval_callback
|
||||
|
||||
try:
|
||||
decision = callback(function_name, arguments, risk_level)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
# Mark as approved in context to prevent double approval in decorator
|
||||
mark_approved(function_name)
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error during approval process: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg, "approval_error": True}
|
||||
|
||||
# Special handling for MCP tools
|
||||
# Check if tools is an MCP instance with the requested function name
|
||||
from ..mcp.mcp import MCP
|
||||
@@ -864,7 +894,11 @@ Your Goal: {self.goal}
|
||||
if self._using_custom_llm:
|
||||
try:
|
||||
# Special handling for MCP tools when using provider/model format
|
||||
tool_param = self.tools if tools is None else tools
|
||||
# Fix: Handle empty tools list properly - use self.tools if tools is None or empty
|
||||
if tools is None or (isinstance(tools, list) and len(tools) == 0):
|
||||
tool_param = self.tools
|
||||
else:
|
||||
tool_param = tools
|
||||
|
||||
# Convert MCP tool objects to OpenAI format if needed
|
||||
if tool_param is not None:
|
||||
@@ -1414,6 +1448,21 @@ Your Goal: {self.goal}
|
||||
"""Async version of execute_tool"""
|
||||
try:
|
||||
logging.info(f"Executing async tool: {function_name} with arguments: {arguments}")
|
||||
|
||||
# Check if approval is required for this tool
|
||||
from ..approval import is_approval_required, request_approval
|
||||
if is_approval_required(function_name):
|
||||
decision = await request_approval(function_name, arguments)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
|
||||
# Try to find the function in the agent's tools list first
|
||||
func = None
|
||||
for tool in self.tools:
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Human Approval Framework for PraisonAI Agents
|
||||
|
||||
This module provides a minimal human-in-the-loop approval system for dangerous tool operations.
|
||||
It extends the existing callback system to require human approval before executing high-risk tools.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Set, Optional, Callable, Any, Literal
|
||||
from functools import wraps
|
||||
from contextvars import ContextVar
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich.prompt import Confirm
|
||||
|
||||
# Global registries for approval requirements
|
||||
APPROVAL_REQUIRED_TOOLS: Set[str] = set()
|
||||
TOOL_RISK_LEVELS: Dict[str, str] = {}
|
||||
|
||||
# Risk levels
|
||||
RiskLevel = Literal["critical", "high", "medium", "low"]
|
||||
|
||||
# Global approval callback
|
||||
approval_callback: Optional[Callable] = None
|
||||
|
||||
# Context variable to track if we're in an approved execution context
|
||||
_approved_context: ContextVar[Set[str]] = ContextVar('approved_context', default=set())
|
||||
|
||||
class ApprovalDecision:
|
||||
"""Result of an approval request"""
|
||||
def __init__(self, approved: bool, modified_args: Optional[Dict[str, Any]] = None, reason: str = ""):
|
||||
self.approved = approved
|
||||
self.modified_args = modified_args or {}
|
||||
self.reason = reason
|
||||
|
||||
def set_approval_callback(callback_fn: Callable):
|
||||
"""Set a custom approval callback function.
|
||||
|
||||
The callback should accept (function_name, arguments, risk_level) and return ApprovalDecision.
|
||||
"""
|
||||
global approval_callback
|
||||
approval_callback = callback_fn
|
||||
|
||||
def mark_approved(tool_name: str):
|
||||
"""Mark a tool as approved in the current context."""
|
||||
approved = _approved_context.get(set())
|
||||
approved.add(tool_name)
|
||||
_approved_context.set(approved)
|
||||
|
||||
def is_already_approved(tool_name: str) -> bool:
|
||||
"""Check if a tool is already approved in the current context."""
|
||||
approved = _approved_context.get(set())
|
||||
return tool_name in approved
|
||||
|
||||
def clear_approval_context():
|
||||
"""Clear the approval context."""
|
||||
_approved_context.set(set())
|
||||
|
||||
def require_approval(risk_level: RiskLevel = "high"):
|
||||
"""Decorator to mark a tool as requiring human approval.
|
||||
|
||||
Args:
|
||||
risk_level: The risk level of the tool ("critical", "high", "medium", "low")
|
||||
"""
|
||||
def decorator(func):
|
||||
tool_name = getattr(func, '__name__', str(func))
|
||||
APPROVAL_REQUIRED_TOOLS.add(tool_name)
|
||||
TOOL_RISK_LEVELS[tool_name] = risk_level
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Skip approval if already approved in current context
|
||||
if is_already_approved(tool_name):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Request approval before executing the function
|
||||
try:
|
||||
# Try to check if we're in an async context
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
# We're in an async context, but this is a sync function
|
||||
# Fall back to sync approval to avoid loop conflicts
|
||||
raise RuntimeError("Use sync fallback in async context")
|
||||
except RuntimeError:
|
||||
# Either no running loop or we want sync fallback
|
||||
# Use asyncio.run for clean async execution
|
||||
decision = asyncio.run(request_approval(tool_name, kwargs))
|
||||
except Exception as e:
|
||||
# Fallback to sync approval if async fails
|
||||
logging.warning(f"Async approval failed, using sync fallback: {e}")
|
||||
callback = approval_callback or console_approval_callback
|
||||
decision = callback(tool_name, kwargs, risk_level)
|
||||
|
||||
if not decision.approved:
|
||||
raise PermissionError(f"Execution of {tool_name} denied: {decision.reason}")
|
||||
|
||||
# Mark as approved and merge modified args
|
||||
mark_approved(tool_name)
|
||||
kwargs.update(decision.modified_args)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
# Skip approval if already approved in current context
|
||||
if is_already_approved(tool_name):
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
# Request approval before executing the function
|
||||
decision = await request_approval(tool_name, kwargs)
|
||||
if not decision.approved:
|
||||
raise PermissionError(f"Execution of {tool_name} denied: {decision.reason}")
|
||||
|
||||
# Mark as approved and merge modified args
|
||||
mark_approved(tool_name)
|
||||
kwargs.update(decision.modified_args)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
# Return the appropriate wrapper based on function type
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
else:
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
def console_approval_callback(function_name: str, arguments: Dict[str, Any], risk_level: str) -> ApprovalDecision:
|
||||
"""Default console-based approval callback.
|
||||
|
||||
Displays tool information and prompts user for approval via console.
|
||||
"""
|
||||
console = Console()
|
||||
|
||||
# Create risk level styling
|
||||
risk_colors = {
|
||||
"critical": "bold red",
|
||||
"high": "red",
|
||||
"medium": "yellow",
|
||||
"low": "blue"
|
||||
}
|
||||
risk_color = risk_colors.get(risk_level, "white")
|
||||
|
||||
# Display tool information
|
||||
tool_info = f"[bold]Function:[/] {function_name}\n"
|
||||
tool_info += f"[bold]Risk Level:[/] [{risk_color}]{risk_level.upper()}[/{risk_color}]\n"
|
||||
tool_info += f"[bold]Arguments:[/]\n"
|
||||
|
||||
for key, value in arguments.items():
|
||||
# Truncate long values for display
|
||||
str_value = str(value)
|
||||
if len(str_value) > 100:
|
||||
str_value = str_value[:97] + "..."
|
||||
tool_info += f" {key}: {str_value}\n"
|
||||
|
||||
console.print(Panel(
|
||||
tool_info.strip(),
|
||||
title="🔒 Tool Approval Required",
|
||||
border_style=risk_color,
|
||||
title_align="left"
|
||||
))
|
||||
|
||||
# Get user approval
|
||||
try:
|
||||
approved = Confirm.ask(
|
||||
f"[{risk_color}]Do you want to execute this {risk_level} risk tool?[/{risk_color}]",
|
||||
default=False
|
||||
)
|
||||
|
||||
if approved:
|
||||
console.print("[green]✅ Tool execution approved[/green]")
|
||||
return ApprovalDecision(approved=True, reason="User approved")
|
||||
else:
|
||||
console.print("[red]❌ Tool execution denied[/red]")
|
||||
return ApprovalDecision(approved=False, reason="User denied")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[red]❌ Tool execution cancelled by user[/red]")
|
||||
return ApprovalDecision(approved=False, reason="User cancelled")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error during approval: {e}[/red]")
|
||||
return ApprovalDecision(approved=False, reason=f"Approval error: {e}")
|
||||
|
||||
async def request_approval(function_name: str, arguments: Dict[str, Any]) -> ApprovalDecision:
|
||||
"""Request approval for a tool execution.
|
||||
|
||||
Args:
|
||||
function_name: Name of the function to execute
|
||||
arguments: Arguments to pass to the function
|
||||
|
||||
Returns:
|
||||
ApprovalDecision with approval status and any modifications
|
||||
"""
|
||||
# Check if approval is required
|
||||
if function_name not in APPROVAL_REQUIRED_TOOLS:
|
||||
return ApprovalDecision(approved=True, reason="No approval required")
|
||||
|
||||
risk_level = TOOL_RISK_LEVELS.get(function_name, "medium")
|
||||
|
||||
# Use custom callback if set, otherwise use console callback
|
||||
callback = approval_callback or console_approval_callback
|
||||
|
||||
try:
|
||||
# Handle async callbacks
|
||||
if asyncio.iscoroutinefunction(callback):
|
||||
decision = await callback(function_name, arguments, risk_level)
|
||||
else:
|
||||
# Run sync callback in executor to avoid blocking
|
||||
loop = asyncio.get_event_loop()
|
||||
decision = await loop.run_in_executor(None, callback, function_name, arguments, risk_level)
|
||||
|
||||
return decision
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in approval callback: {e}")
|
||||
return ApprovalDecision(approved=False, reason=f"Approval callback error: {e}")
|
||||
|
||||
# Default dangerous tools - can be configured at runtime
|
||||
DEFAULT_DANGEROUS_TOOLS = {
|
||||
# Critical risk tools
|
||||
"execute_command": "critical",
|
||||
"kill_process": "critical",
|
||||
"execute_code": "critical",
|
||||
|
||||
# High risk tools
|
||||
"write_file": "high",
|
||||
"delete_file": "high",
|
||||
"move_file": "high",
|
||||
"copy_file": "high",
|
||||
"execute_query": "high",
|
||||
|
||||
# Medium risk tools
|
||||
"evaluate": "medium",
|
||||
"crawl": "medium",
|
||||
"scrape_page": "medium",
|
||||
}
|
||||
|
||||
def configure_default_approvals():
|
||||
"""Configure default dangerous tools to require approval."""
|
||||
for tool_name, risk_level in DEFAULT_DANGEROUS_TOOLS.items():
|
||||
APPROVAL_REQUIRED_TOOLS.add(tool_name)
|
||||
TOOL_RISK_LEVELS[tool_name] = risk_level
|
||||
|
||||
def add_approval_requirement(tool_name: str, risk_level: RiskLevel = "high"):
|
||||
"""Dynamically add approval requirement for a tool."""
|
||||
APPROVAL_REQUIRED_TOOLS.add(tool_name)
|
||||
TOOL_RISK_LEVELS[tool_name] = risk_level
|
||||
|
||||
def remove_approval_requirement(tool_name: str):
|
||||
"""Remove approval requirement for a tool."""
|
||||
APPROVAL_REQUIRED_TOOLS.discard(tool_name)
|
||||
TOOL_RISK_LEVELS.pop(tool_name, None)
|
||||
|
||||
def is_approval_required(tool_name: str) -> bool:
|
||||
"""Check if a tool requires approval."""
|
||||
return tool_name in APPROVAL_REQUIRED_TOOLS
|
||||
|
||||
def get_risk_level(tool_name: str) -> Optional[str]:
|
||||
"""Get the risk level of a tool."""
|
||||
return TOOL_RISK_LEVELS.get(tool_name)
|
||||
|
||||
# Initialize with defaults
|
||||
configure_default_approvals()
|
||||
@@ -17,9 +17,13 @@ class CustomMemory:
|
||||
}).from_config(config)
|
||||
|
||||
@staticmethod
|
||||
def _add_to_vector_store(self, messages, metadata, filters, infer):
|
||||
def _add_to_vector_store(self, messages, metadata=None, filters=None, infer=None):
|
||||
# Custom implementation that doesn't use LLM
|
||||
parsed_messages = "\n".join([msg["content"] for msg in messages])
|
||||
# Handle different message formats for backward compatibility
|
||||
if isinstance(messages, list):
|
||||
parsed_messages = "\n".join([msg.get("content", str(msg)) if isinstance(msg, dict) else str(msg) for msg in messages])
|
||||
else:
|
||||
parsed_messages = str(messages)
|
||||
|
||||
# Create a simple fact without using LLM
|
||||
new_retrieved_facts = [parsed_messages]
|
||||
@@ -34,7 +38,7 @@ class CustomMemory:
|
||||
memory_id = self._create_memory(
|
||||
data=parsed_messages,
|
||||
existing_embeddings=new_message_embeddings,
|
||||
metadata=metadata
|
||||
metadata=metadata or {}
|
||||
)
|
||||
|
||||
return [{
|
||||
@@ -101,7 +105,11 @@ class Knowledge:
|
||||
}
|
||||
},
|
||||
"version": "v1.1",
|
||||
"custom_prompt": "Return {{\"facts\": [text]}} where text is the exact input provided and json response"
|
||||
"custom_prompt": "Return {{\"facts\": [text]}} where text is the exact input provided and json response",
|
||||
"reranker": {
|
||||
"enabled": False,
|
||||
"default_rerank": False
|
||||
}
|
||||
}
|
||||
|
||||
# If config is provided, merge it with base config
|
||||
@@ -129,6 +137,14 @@ class Knowledge:
|
||||
# Merge llm config if provided
|
||||
if "llm" in self._config:
|
||||
base_config["llm"] = self._config["llm"]
|
||||
|
||||
# Merge reranker config if provided
|
||||
if "reranker" in self._config:
|
||||
base_config["reranker"].update(self._config["reranker"])
|
||||
|
||||
# Merge graph_store config if provided (for graph memory support)
|
||||
if "graph_store" in self._config:
|
||||
base_config["graph_store"] = self._config["graph_store"]
|
||||
return base_config
|
||||
|
||||
@cached_property
|
||||
@@ -176,7 +192,22 @@ class Knowledge:
|
||||
if not content:
|
||||
return []
|
||||
|
||||
result = self.memory.add(content, user_id=user_id, agent_id=agent_id, run_id=run_id, metadata=metadata)
|
||||
# Try new API format first, fall back to old format for backward compatibility
|
||||
try:
|
||||
# Convert content to messages format for mem0 API compatibility
|
||||
if isinstance(content, str):
|
||||
messages = [{"role": "user", "content": content}]
|
||||
else:
|
||||
messages = content if isinstance(content, list) else [{"role": "user", "content": str(content)}]
|
||||
|
||||
result = self.memory.add(messages=messages, user_id=user_id, agent_id=agent_id, run_id=run_id, metadata=metadata)
|
||||
except TypeError as e:
|
||||
# Fallback to old API format if messages parameter is not supported
|
||||
if "unexpected keyword argument" in str(e) or "positional argument" in str(e):
|
||||
self._log(f"Falling back to legacy API format due to: {e}")
|
||||
result = self.memory.add(content, user_id=user_id, agent_id=agent_id, run_id=run_id, metadata=metadata)
|
||||
else:
|
||||
raise
|
||||
self._log(f"Store operation result: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -191,9 +222,25 @@ class Knowledge:
|
||||
"""Retrieve a specific memory by ID."""
|
||||
return self.memory.get(memory_id)
|
||||
|
||||
def search(self, query, user_id=None, agent_id=None, run_id=None):
|
||||
"""Search for memories related to a query."""
|
||||
return self.memory.search(query, user_id=user_id, agent_id=agent_id, run_id=run_id)
|
||||
def search(self, query, user_id=None, agent_id=None, run_id=None, rerank=None, **kwargs):
|
||||
"""Search for memories related to a query.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
user_id: Optional user ID for user-specific search
|
||||
agent_id: Optional agent ID for agent-specific search
|
||||
run_id: Optional run ID for run-specific search
|
||||
rerank: Whether to use Mem0's advanced reranking. If None, uses config default
|
||||
**kwargs: Additional search parameters to pass to Mem0 (keyword_search, filter_memories, etc.)
|
||||
|
||||
Returns:
|
||||
List of search results, reranked if rerank=True
|
||||
"""
|
||||
# Use config default if rerank not explicitly specified
|
||||
if rerank is None:
|
||||
rerank = self.config.get("reranker", {}).get("default_rerank", False)
|
||||
|
||||
return self.memory.search(query, user_id=user_id, agent_id=agent_id, run_id=run_id, rerank=rerank, **kwargs)
|
||||
|
||||
def update(self, memory_id, data):
|
||||
"""Update a memory."""
|
||||
|
||||
@@ -205,6 +205,58 @@ class LLM:
|
||||
}
|
||||
logging.debug(f"LLM instance initialized with: {json.dumps(debug_info, indent=2, default=str)}")
|
||||
|
||||
def _is_ollama_provider(self) -> bool:
|
||||
"""Detect if this is an Ollama provider regardless of naming convention"""
|
||||
if not self.model:
|
||||
return False
|
||||
|
||||
# Direct ollama/ prefix
|
||||
if self.model.startswith("ollama/"):
|
||||
return True
|
||||
|
||||
# Check environment variables for Ollama base URL
|
||||
base_url = os.getenv("OPENAI_BASE_URL", "")
|
||||
api_base = os.getenv("OPENAI_API_BASE", "")
|
||||
|
||||
# Common Ollama endpoints
|
||||
ollama_endpoints = ["localhost:11434", "127.0.0.1:11434", ":11434"]
|
||||
|
||||
return any(endpoint in base_url or endpoint in api_base for endpoint in ollama_endpoints)
|
||||
|
||||
def _parse_tool_call_arguments(self, tool_call: Dict, is_ollama: bool = False) -> tuple:
|
||||
"""
|
||||
Safely parse tool call arguments with proper error handling
|
||||
|
||||
Returns:
|
||||
tuple: (function_name, arguments, tool_call_id)
|
||||
"""
|
||||
try:
|
||||
if is_ollama:
|
||||
# Special handling for Ollama provider which may have different structure
|
||||
if "function" in tool_call and isinstance(tool_call["function"], dict):
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
else:
|
||||
# Try alternative format that Ollama might return
|
||||
function_name = tool_call.get("name", "unknown_function")
|
||||
arguments_str = tool_call.get("arguments", "{}")
|
||||
arguments = json.loads(arguments_str) if arguments_str else {}
|
||||
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
|
||||
else:
|
||||
# Standard format for other providers with error handling
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments_str = tool_call["function"]["arguments"]
|
||||
arguments = json.loads(arguments_str) if arguments_str else {}
|
||||
tool_call_id = tool_call["id"]
|
||||
|
||||
except (KeyError, json.JSONDecodeError, TypeError) as e:
|
||||
logging.error(f"Error parsing tool call arguments: {e}")
|
||||
function_name = tool_call.get("name", "unknown_function")
|
||||
arguments = {}
|
||||
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
|
||||
|
||||
return function_name, arguments, tool_call_id
|
||||
|
||||
def _needs_system_message_skip(self) -> bool:
|
||||
"""Check if this model requires skipping system messages"""
|
||||
if not self.model:
|
||||
@@ -486,32 +538,19 @@ class LLM:
|
||||
for tool_call in tool_calls:
|
||||
# Handle both object and dict access patterns
|
||||
if isinstance(tool_call, dict):
|
||||
# Special handling for Ollama provider which may have a different structure
|
||||
if self.model and self.model.startswith("ollama/"):
|
||||
try:
|
||||
# Try standard format first
|
||||
if "function" in tool_call and isinstance(tool_call["function"], dict):
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
else:
|
||||
# Try alternative format that Ollama might return
|
||||
function_name = tool_call.get("name", "unknown_function")
|
||||
arguments = json.loads(tool_call.get("arguments", "{}"))
|
||||
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing Ollama tool call: {e}")
|
||||
function_name = "unknown_function"
|
||||
arguments = {}
|
||||
tool_call_id = f"tool_{id(tool_call)}"
|
||||
else:
|
||||
# Standard format for other providers
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
tool_call_id = tool_call["id"]
|
||||
is_ollama = self._is_ollama_provider()
|
||||
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
|
||||
else:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
tool_call_id = tool_call.id
|
||||
# Handle object-style tool calls
|
||||
try:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
|
||||
tool_call_id = tool_call.id
|
||||
except (json.JSONDecodeError, AttributeError) as e:
|
||||
logging.error(f"Error parsing object-style tool call: {e}")
|
||||
function_name = "unknown_function"
|
||||
arguments = {}
|
||||
tool_call_id = f"tool_{id(tool_call)}"
|
||||
|
||||
logging.debug(f"[TOOL_EXEC_DEBUG] About to execute tool {function_name} with args: {arguments}")
|
||||
tool_result = execute_tool_fn(function_name, arguments)
|
||||
@@ -1083,32 +1122,19 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
for tool_call in tool_calls:
|
||||
# Handle both object and dict access patterns
|
||||
if isinstance(tool_call, dict):
|
||||
# Special handling for Ollama provider which may have a different structure
|
||||
if self.model and self.model.startswith("ollama/"):
|
||||
try:
|
||||
# Try standard format first
|
||||
if "function" in tool_call and isinstance(tool_call["function"], dict):
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
else:
|
||||
# Try alternative format that Ollama might return
|
||||
function_name = tool_call.get("name", "unknown_function")
|
||||
arguments = json.loads(tool_call.get("arguments", "{}"))
|
||||
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing Ollama tool call: {e}")
|
||||
function_name = "unknown_function"
|
||||
arguments = {}
|
||||
tool_call_id = f"tool_{id(tool_call)}"
|
||||
else:
|
||||
# Standard format for other providers
|
||||
function_name = tool_call["function"]["name"]
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
tool_call_id = tool_call["id"]
|
||||
is_ollama = self._is_ollama_provider()
|
||||
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
|
||||
else:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
tool_call_id = tool_call.id
|
||||
# Handle object-style tool calls
|
||||
try:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
|
||||
tool_call_id = tool_call.id
|
||||
except (json.JSONDecodeError, AttributeError) as e:
|
||||
logging.error(f"Error parsing object-style tool call: {e}")
|
||||
function_name = "unknown_function"
|
||||
arguments = {}
|
||||
tool_call_id = f"tool_{id(tool_call)}"
|
||||
|
||||
tool_result = await execute_tool_fn(function_name, arguments)
|
||||
|
||||
@@ -1129,7 +1155,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
response_text = ""
|
||||
|
||||
# Special handling for Ollama models that don't automatically process tool results
|
||||
if self.model and self.model.startswith("ollama/") and tool_result:
|
||||
if self._is_ollama_provider() and tool_result:
|
||||
# For Ollama models, we need to explicitly ask the model to process the tool results
|
||||
# First, check if the response is just a JSON tool call
|
||||
try:
|
||||
@@ -1486,6 +1512,10 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
if self.stop_phrases:
|
||||
params["stop"] = self.stop_phrases
|
||||
|
||||
# Add extra settings for provider-specific parameters (e.g., num_ctx for Ollama)
|
||||
if self.extra_settings:
|
||||
params.update(self.extra_settings)
|
||||
|
||||
# Override with any provided parameters
|
||||
params.update(override_params)
|
||||
|
||||
|
||||
@@ -43,12 +43,18 @@ error_logs = []
|
||||
sync_display_callbacks = {}
|
||||
async_display_callbacks = {}
|
||||
|
||||
# Global approval callback registry
|
||||
approval_callback = None
|
||||
|
||||
# At the top of the file, add display_callbacks to __all__
|
||||
__all__ = [
|
||||
'error_logs',
|
||||
'register_display_callback',
|
||||
'register_approval_callback',
|
||||
'sync_display_callbacks',
|
||||
'async_display_callbacks',
|
||||
'execute_callback',
|
||||
'approval_callback',
|
||||
# ... other exports
|
||||
]
|
||||
|
||||
@@ -65,6 +71,15 @@ def register_display_callback(display_type: str, callback_fn, is_async: bool = F
|
||||
else:
|
||||
sync_display_callbacks[display_type] = callback_fn
|
||||
|
||||
def register_approval_callback(callback_fn):
|
||||
"""Register a global approval callback function for dangerous tool operations.
|
||||
|
||||
Args:
|
||||
callback_fn: Function that takes (function_name, arguments, risk_level) and returns ApprovalDecision
|
||||
"""
|
||||
global approval_callback
|
||||
approval_callback = callback_fn
|
||||
|
||||
async def execute_callback(display_type: str, **kwargs):
|
||||
"""Execute both sync and async callbacks for a given display type.
|
||||
|
||||
|
||||
@@ -299,7 +299,11 @@ class MCP:
|
||||
required_params.append(name)
|
||||
|
||||
# Create the function signature
|
||||
params = []
|
||||
# Separate required and optional parameters to ensure proper ordering
|
||||
# (required parameters must come before optional parameters)
|
||||
required_param_objects = []
|
||||
optional_param_objects = []
|
||||
|
||||
for name in param_names:
|
||||
is_required = name in required_params
|
||||
param = inspect.Parameter(
|
||||
@@ -308,7 +312,14 @@ class MCP:
|
||||
default=inspect.Parameter.empty if is_required else None,
|
||||
annotation=param_annotations.get(name, Any)
|
||||
)
|
||||
params.append(param)
|
||||
|
||||
if is_required:
|
||||
required_param_objects.append(param)
|
||||
else:
|
||||
optional_param_objects.append(param)
|
||||
|
||||
# Combine parameters with required first, then optional
|
||||
params = required_param_objects + optional_param_objects
|
||||
|
||||
# Create function template to be properly decorated
|
||||
def template_function(*args, **kwargs):
|
||||
|
||||
@@ -41,6 +41,7 @@ class Memory:
|
||||
- User memory (preferences/history for each user)
|
||||
- Quality score logic for deciding which data to store in LTM
|
||||
- Context building from multiple memory sources
|
||||
- Graph memory support for complex relationship storage (via Mem0)
|
||||
|
||||
Config example:
|
||||
{
|
||||
@@ -53,9 +54,35 @@ class Memory:
|
||||
"api_key": "...", # if mem0 usage
|
||||
"org_id": "...",
|
||||
"project_id": "...",
|
||||
...
|
||||
|
||||
# Graph memory configuration (optional)
|
||||
"graph_store": {
|
||||
"provider": "neo4j" or "memgraph",
|
||||
"config": {
|
||||
"url": "neo4j+s://xxx" or "bolt://localhost:7687",
|
||||
"username": "neo4j" or "memgraph",
|
||||
"password": "xxx"
|
||||
}
|
||||
},
|
||||
|
||||
# Optional additional configurations for graph memory
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
"config": {"host": "localhost", "port": 6333}
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {"model": "gpt-4o", "api_key": "..."}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
"config": {"model": "text-embedding-3-small", "api_key": "..."}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Note: Graph memory requires "mem0ai[graph]" installation and works alongside
|
||||
vector-based memory for enhanced relationship-aware retrieval.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], verbose: int = 0):
|
||||
@@ -78,6 +105,7 @@ class Memory:
|
||||
self.provider = self.cfg.get("provider", "rag")
|
||||
self.use_mem0 = (self.provider.lower() == "mem0") and MEM0_AVAILABLE
|
||||
self.use_rag = (self.provider.lower() == "rag") and CHROMADB_AVAILABLE and self.cfg.get("use_embedding", False)
|
||||
self.graph_enabled = False # Initialize graph support flag
|
||||
|
||||
# Create .praison directory if it doesn't exist
|
||||
os.makedirs(".praison", exist_ok=True)
|
||||
@@ -137,16 +165,49 @@ class Memory:
|
||||
conn.close()
|
||||
|
||||
def _init_mem0(self):
|
||||
"""Initialize Mem0 client for agent or user memory."""
|
||||
from mem0 import MemoryClient
|
||||
"""Initialize Mem0 client for agent or user memory with optional graph support."""
|
||||
mem_cfg = self.cfg.get("config", {})
|
||||
api_key = mem_cfg.get("api_key", os.getenv("MEM0_API_KEY"))
|
||||
org_id = mem_cfg.get("org_id")
|
||||
proj_id = mem_cfg.get("project_id")
|
||||
if org_id and proj_id:
|
||||
self.mem0_client = MemoryClient(api_key=api_key, org_id=org_id, project_id=proj_id)
|
||||
|
||||
# Check if graph memory is enabled
|
||||
graph_config = mem_cfg.get("graph_store")
|
||||
use_graph = graph_config is not None
|
||||
|
||||
if use_graph:
|
||||
# Initialize with graph memory support
|
||||
from mem0 import Memory
|
||||
self._log_verbose("Initializing Mem0 with graph memory support")
|
||||
|
||||
# Build Mem0 config with graph store
|
||||
mem0_config = {}
|
||||
|
||||
# Add graph store configuration
|
||||
mem0_config["graph_store"] = graph_config
|
||||
|
||||
# Add other configurations if provided
|
||||
if "vector_store" in mem_cfg:
|
||||
mem0_config["vector_store"] = mem_cfg["vector_store"]
|
||||
if "llm" in mem_cfg:
|
||||
mem0_config["llm"] = mem_cfg["llm"]
|
||||
if "embedder" in mem_cfg:
|
||||
mem0_config["embedder"] = mem_cfg["embedder"]
|
||||
|
||||
# Initialize Memory with graph support
|
||||
self.mem0_client = Memory.from_config(config_dict=mem0_config)
|
||||
self.graph_enabled = True
|
||||
self._log_verbose("Graph memory initialized successfully")
|
||||
else:
|
||||
self.mem0_client = MemoryClient(api_key=api_key)
|
||||
# Use traditional MemoryClient
|
||||
from mem0 import MemoryClient
|
||||
self._log_verbose("Initializing Mem0 with traditional memory client")
|
||||
|
||||
if org_id and proj_id:
|
||||
self.mem0_client = MemoryClient(api_key=api_key, org_id=org_id, project_id=proj_id)
|
||||
else:
|
||||
self.mem0_client = MemoryClient(api_key=api_key)
|
||||
self.graph_enabled = False
|
||||
|
||||
def _init_chroma(self):
|
||||
"""Initialize a local Chroma client for embedding-based search."""
|
||||
@@ -262,13 +323,18 @@ class Memory:
|
||||
query: str,
|
||||
limit: int = 5,
|
||||
min_quality: float = 0.0,
|
||||
relevance_cutoff: float = 0.0
|
||||
relevance_cutoff: float = 0.0,
|
||||
rerank: bool = False,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Search short-term memory with optional quality filter"""
|
||||
self._log_verbose(f"Searching short memory for: {query}")
|
||||
|
||||
if self.use_mem0 and hasattr(self, "mem0_client"):
|
||||
results = self.mem0_client.search(query=query, limit=limit)
|
||||
# Pass rerank and other kwargs to Mem0 search
|
||||
search_params = {"query": query, "limit": limit, "rerank": rerank}
|
||||
search_params.update(kwargs)
|
||||
results = self.mem0_client.search(**search_params)
|
||||
filtered = [r for r in results if r.get("score", 1.0) >= relevance_cutoff]
|
||||
return filtered
|
||||
|
||||
@@ -439,7 +505,9 @@ class Memory:
|
||||
query: str,
|
||||
limit: int = 5,
|
||||
relevance_cutoff: float = 0.0,
|
||||
min_quality: float = 0.0
|
||||
min_quality: float = 0.0,
|
||||
rerank: bool = False,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Search long-term memory with optional quality filter"""
|
||||
self._log_verbose(f"Searching long memory for: {query}")
|
||||
@@ -448,7 +516,10 @@ class Memory:
|
||||
found = []
|
||||
|
||||
if self.use_mem0 and hasattr(self, "mem0_client"):
|
||||
results = self.mem0_client.search(query=query, limit=limit)
|
||||
# Pass rerank and other kwargs to Mem0 search
|
||||
search_params = {"query": query, "limit": limit, "rerank": rerank}
|
||||
search_params.update(kwargs)
|
||||
results = self.mem0_client.search(**search_params)
|
||||
# Filter by quality
|
||||
filtered = [r for r in results if r.get("metadata", {}).get("quality", 0.0) >= min_quality]
|
||||
logger.info(f"Found {len(filtered)} results in Mem0")
|
||||
@@ -595,12 +666,15 @@ class Memory:
|
||||
else:
|
||||
self.store_long_term(text, metadata=meta)
|
||||
|
||||
def search_user_memory(self, user_id: str, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
def search_user_memory(self, user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
If mem0 is used, pass user_id in. Otherwise fallback to local filter on user in metadata.
|
||||
"""
|
||||
if self.use_mem0 and hasattr(self, "mem0_client"):
|
||||
return self.mem0_client.search(query=query, limit=limit, user_id=user_id)
|
||||
# Pass rerank and other kwargs to Mem0 search
|
||||
search_params = {"query": query, "limit": limit, "user_id": user_id, "rerank": rerank}
|
||||
search_params.update(kwargs)
|
||||
return self.mem0_client.search(**search_params)
|
||||
else:
|
||||
hits = self.search_long_term(query, limit=20)
|
||||
filtered = []
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import List, Dict, Union, Optional
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import logging
|
||||
from ..approval import require_approval
|
||||
|
||||
class FileTools:
|
||||
"""Tools for file operations including read, write, list, and information."""
|
||||
@@ -41,6 +42,7 @@ class FileTools:
|
||||
return error_msg
|
||||
|
||||
@staticmethod
|
||||
@require_approval(risk_level="high")
|
||||
def write_file(filepath: str, content: str, encoding: str = 'utf-8') -> bool:
|
||||
"""
|
||||
Write content to a file.
|
||||
@@ -134,6 +136,7 @@ class FileTools:
|
||||
return {'error': error_msg}
|
||||
|
||||
@staticmethod
|
||||
@require_approval(risk_level="high")
|
||||
def copy_file(src: str, dst: str) -> bool:
|
||||
"""
|
||||
Copy a file from source to destination.
|
||||
@@ -156,6 +159,7 @@ class FileTools:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@require_approval(risk_level="high")
|
||||
def move_file(src: str, dst: str) -> bool:
|
||||
"""
|
||||
Move a file from source to destination.
|
||||
@@ -178,6 +182,7 @@ class FileTools:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@require_approval(risk_level="high")
|
||||
def delete_file(filepath: str) -> bool:
|
||||
"""
|
||||
Delete a file.
|
||||
|
||||
@@ -15,6 +15,7 @@ from importlib import util
|
||||
import io
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
import traceback
|
||||
from ..approval import require_approval
|
||||
|
||||
class PythonTools:
|
||||
"""Tools for Python code execution and analysis."""
|
||||
@@ -36,6 +37,7 @@ class PythonTools:
|
||||
f"Run: pip install {' '.join(missing)}"
|
||||
)
|
||||
|
||||
@require_approval(risk_level="critical")
|
||||
def execute_code(
|
||||
self,
|
||||
code: str,
|
||||
|
||||
@@ -13,6 +13,7 @@ import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, List, Optional, Union
|
||||
from ..approval import require_approval
|
||||
|
||||
class ShellTools:
|
||||
"""Tools for executing shell commands safely."""
|
||||
@@ -31,6 +32,7 @@ class ShellTools:
|
||||
"Run: pip install psutil"
|
||||
)
|
||||
|
||||
@require_approval(risk_level="critical")
|
||||
def execute_command(
|
||||
self,
|
||||
command: str,
|
||||
@@ -146,6 +148,7 @@ class ShellTools:
|
||||
logging.error(error_msg)
|
||||
return []
|
||||
|
||||
@require_approval(risk_level="critical")
|
||||
def kill_process(
|
||||
self,
|
||||
pid: int,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.92"
|
||||
version = "0.0.96"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
@@ -35,6 +35,12 @@ knowledge = [
|
||||
"chonkie>=1.0.2"
|
||||
]
|
||||
|
||||
# Graph memory support (includes Mem0 with graph capabilities)
|
||||
graph = [
|
||||
"mem0ai[graph]>=0.1.0",
|
||||
"chromadb>=1.0.0"
|
||||
]
|
||||
|
||||
# Add LLM dependencies
|
||||
llm = [
|
||||
"litellm>=1.50.0",
|
||||
@@ -51,6 +57,7 @@ api = [
|
||||
all = [
|
||||
"praisonaiagents[memory]",
|
||||
"praisonaiagents[knowledge]",
|
||||
"praisonaiagents[graph]",
|
||||
"praisonaiagents[llm]",
|
||||
"praisonaiagents[mcp]",
|
||||
"praisonaiagents[api]"
|
||||
|
||||
@@ -11,5 +11,9 @@ mem0ai>=0.1.0
|
||||
markitdown
|
||||
chonkie>=1.0.2
|
||||
|
||||
# Graph memory dependencies (optional)
|
||||
# Uncomment for graph memory support:
|
||||
# mem0ai[graph]>=0.1.0
|
||||
|
||||
# Development dependencies
|
||||
python-dotenv>=0.19.0
|
||||
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for graph memory functionality
|
||||
"""
|
||||
|
||||
from praisonaiagents.memory import Memory
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
|
||||
def test_memory_graph_config():
|
||||
"""Test memory with graph configuration"""
|
||||
print("Testing Memory with graph configuration...")
|
||||
|
||||
# Configuration with graph store
|
||||
config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"graph_store": {
|
||||
"provider": "memgraph",
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {"path": ".test_graph_memory"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
memory = Memory(config=config, verbose=1)
|
||||
print(f"✅ Memory initialized. Graph enabled: {getattr(memory, 'graph_enabled', False)}")
|
||||
|
||||
# Test basic memory operations
|
||||
memory.store_short_term("Alice loves hiking", {"category": "preference"})
|
||||
memory.store_short_term("Alice's friend John also loves hiking", {"category": "social"})
|
||||
|
||||
results = memory.search_short_term("hiking", limit=2)
|
||||
print(f"✅ Short-term search returned {len(results)} results")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Graph memory test failed (expected if dependencies missing): {e}")
|
||||
return False
|
||||
|
||||
def test_knowledge_graph_config():
|
||||
"""Test Knowledge with graph configuration"""
|
||||
print("\nTesting Knowledge with graph configuration...")
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "test_graph_knowledge",
|
||||
"path": ".test_graph_knowledge"
|
||||
}
|
||||
},
|
||||
"graph_store": {
|
||||
"provider": "memgraph",
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
knowledge = Knowledge(config=config, verbose=0)
|
||||
final_config = knowledge.config
|
||||
|
||||
if "graph_store" in final_config:
|
||||
print("✅ Graph store configuration preserved in Knowledge")
|
||||
else:
|
||||
print("❌ Graph store configuration missing")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Knowledge graph test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_backward_compatibility():
|
||||
"""Test that existing configurations still work"""
|
||||
print("\nTesting backward compatibility...")
|
||||
|
||||
# Old-style configuration without graph
|
||||
old_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {"path": ".test_backward_compat"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
memory = Memory(config=old_config, verbose=0)
|
||||
print("✅ Backward compatibility maintained")
|
||||
print(f" Graph enabled: {getattr(memory, 'graph_enabled', False)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backward compatibility broken: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🧪 Graph Memory Integration Tests")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
test_memory_graph_config,
|
||||
test_knowledge_graph_config,
|
||||
test_backward_compatibility
|
||||
]
|
||||
|
||||
passed = 0
|
||||
for test in tests:
|
||||
if test():
|
||||
passed += 1
|
||||
|
||||
print(f"\n📊 Results: {passed}/{len(tests)} tests passed")
|
||||
|
||||
if passed == len(tests):
|
||||
print("🎉 All tests passed!")
|
||||
else:
|
||||
print("⚠️ Some tests failed. This is expected if graph dependencies are not installed.")
|
||||
|
||||
print("\n💡 To enable full graph memory functionality:")
|
||||
print(" pip install \"mem0ai[graph]\"")
|
||||
print(" docker run -p 7687:7687 memgraph/memgraph-mage:latest")
|
||||
Generated
+1
-1
@@ -2233,7 +2233,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.92"
|
||||
version = "0.0.96"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
|
||||
@@ -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.20.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.20.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.25.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.25.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -121,12 +121,12 @@ Tools are not available for {framework}. To use tools, install:
|
||||
mode=instructor.Mode.JSON,
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
def generate(self, merge=False):
|
||||
"""
|
||||
Generates a team structure for the specified topic.
|
||||
|
||||
Args:
|
||||
None
|
||||
merge (bool): Whether to merge with existing agents.yaml file instead of overwriting.
|
||||
|
||||
Returns:
|
||||
str: The full path of the YAML file containing the generated team structure.
|
||||
@@ -149,46 +149,119 @@ Tools are not available for {framework}. To use tools, install:
|
||||
]
|
||||
)
|
||||
json_data = json.loads(response.model_dump_json())
|
||||
self.convert_and_save(json_data)
|
||||
self.convert_and_save(json_data, merge=merge)
|
||||
full_path = os.path.abspath(self.agent_file)
|
||||
return full_path
|
||||
|
||||
def convert_and_save(self, json_data):
|
||||
def convert_and_save(self, json_data, merge=False):
|
||||
"""Converts the provided JSON data into the desired YAML format and saves it to a file.
|
||||
|
||||
Args:
|
||||
json_data (dict): The JSON data representing the team structure.
|
||||
topic (str, optional): The topic to be inserted into the YAML. Defaults to "Artificial Intelligence".
|
||||
agent_file (str, optional): The name of the YAML file to save. Defaults to "test.yaml".
|
||||
merge (bool): Whether to merge with existing agents.yaml file instead of overwriting.
|
||||
"""
|
||||
|
||||
yaml_data = {
|
||||
"framework": self.framework,
|
||||
"topic": self.topic,
|
||||
"roles": {},
|
||||
"dependencies": []
|
||||
}
|
||||
|
||||
for role_id, role_details in json_data['roles'].items():
|
||||
yaml_data['roles'][role_id] = {
|
||||
"backstory": "" + role_details['backstory'],
|
||||
"goal": role_details['goal'],
|
||||
"role": role_details['role'],
|
||||
"tasks": {},
|
||||
# "tools": role_details.get('tools', []),
|
||||
"tools": ['']
|
||||
# Handle merge functionality
|
||||
if merge and os.path.exists(self.agent_file):
|
||||
yaml_data = self.merge_with_existing_agents(json_data)
|
||||
else:
|
||||
# Original behavior: create new yaml_data structure
|
||||
yaml_data = {
|
||||
"framework": self.framework,
|
||||
"topic": self.topic,
|
||||
"roles": {},
|
||||
"dependencies": []
|
||||
}
|
||||
|
||||
for task_id, task_details in role_details['tasks'].items():
|
||||
yaml_data['roles'][role_id]['tasks'][task_id] = {
|
||||
"description": "" + task_details['description'],
|
||||
"expected_output": "" + task_details['expected_output']
|
||||
for role_id, role_details in json_data['roles'].items():
|
||||
yaml_data['roles'][role_id] = {
|
||||
"backstory": "" + role_details['backstory'],
|
||||
"goal": role_details['goal'],
|
||||
"role": role_details['role'],
|
||||
"tasks": {},
|
||||
# "tools": role_details.get('tools', []),
|
||||
"tools": ['']
|
||||
}
|
||||
|
||||
for task_id, task_details in role_details['tasks'].items():
|
||||
yaml_data['roles'][role_id]['tasks'][task_id] = {
|
||||
"description": "" + task_details['description'],
|
||||
"expected_output": "" + task_details['expected_output']
|
||||
}
|
||||
|
||||
# Save to YAML file, maintaining the order
|
||||
with open(self.agent_file, 'w') as f:
|
||||
yaml.dump(yaml_data, f, allow_unicode=True, sort_keys=False)
|
||||
|
||||
def merge_with_existing_agents(self, new_json_data):
|
||||
"""
|
||||
Merge existing agents.yaml with new auto-generated agents.
|
||||
|
||||
Args:
|
||||
new_json_data (dict): The JSON data representing the new team structure.
|
||||
|
||||
Returns:
|
||||
dict: The merged YAML data structure.
|
||||
"""
|
||||
try:
|
||||
# Load existing agents.yaml
|
||||
with open(self.agent_file, 'r') as f:
|
||||
existing_data = yaml.safe_load(f)
|
||||
|
||||
if not existing_data:
|
||||
# If existing file is empty, treat as new file
|
||||
existing_data = {"roles": {}, "dependencies": []}
|
||||
except (yaml.YAMLError, FileNotFoundError) as e:
|
||||
logging.warning(f"Could not load existing agents file {self.agent_file}: {e}")
|
||||
logging.warning("Creating new file instead of merging")
|
||||
existing_data = {"roles": {}, "dependencies": []}
|
||||
|
||||
# Start with existing data structure
|
||||
merged_data = existing_data.copy()
|
||||
|
||||
# Ensure required fields exist
|
||||
if 'roles' not in merged_data:
|
||||
merged_data['roles'] = {}
|
||||
if 'dependencies' not in merged_data:
|
||||
merged_data['dependencies'] = []
|
||||
if 'framework' not in merged_data:
|
||||
merged_data['framework'] = self.framework
|
||||
|
||||
# Handle topic merging
|
||||
existing_topic = merged_data.get('topic', '')
|
||||
new_topic = self.topic
|
||||
if existing_topic and existing_topic != new_topic:
|
||||
merged_data['topic'] = f"{existing_topic} + {new_topic}"
|
||||
else:
|
||||
merged_data['topic'] = new_topic
|
||||
|
||||
# Merge new roles with existing ones
|
||||
for role_id, role_details in new_json_data['roles'].items():
|
||||
# Check for conflicts and rename if necessary
|
||||
final_role_id = role_id
|
||||
counter = 1
|
||||
while final_role_id in merged_data['roles']:
|
||||
final_role_id = f"{role_id}_auto_{counter}"
|
||||
counter += 1
|
||||
|
||||
# Add the new role
|
||||
merged_data['roles'][final_role_id] = {
|
||||
"backstory": "" + role_details['backstory'],
|
||||
"goal": role_details['goal'],
|
||||
"role": role_details['role'],
|
||||
"tasks": {},
|
||||
"tools": ['']
|
||||
}
|
||||
|
||||
# Add tasks for this role
|
||||
for task_id, task_details in role_details['tasks'].items():
|
||||
merged_data['roles'][final_role_id]['tasks'][task_id] = {
|
||||
"description": "" + task_details['description'],
|
||||
"expected_output": "" + task_details['expected_output']
|
||||
}
|
||||
|
||||
return merged_data
|
||||
|
||||
def get_user_content(self):
|
||||
"""
|
||||
Generates a prompt for the OpenAI API to generate a team structure.
|
||||
|
||||
@@ -5,6 +5,7 @@ import argparse
|
||||
from .version import __version__
|
||||
import yaml
|
||||
import os
|
||||
import time
|
||||
from rich import print
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
@@ -25,6 +26,7 @@ CALL_MODULE_AVAILABLE = False
|
||||
CREWAI_AVAILABLE = False
|
||||
AUTOGEN_AVAILABLE = False
|
||||
PRAISONAI_AVAILABLE = False
|
||||
TRAIN_AVAILABLE = False
|
||||
try:
|
||||
# Create necessary directories and set CHAINLIT_APP_ROOT
|
||||
if "CHAINLIT_APP_ROOT" not in os.environ:
|
||||
@@ -71,6 +73,12 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from unsloth import FastLanguageModel
|
||||
TRAIN_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'), format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.getLogger('alembic').setLevel(logging.ERROR)
|
||||
logging.getLogger('gradio').setLevel(logging.ERROR)
|
||||
@@ -217,9 +225,66 @@ class PraisonAI:
|
||||
# If no command or direct_prompt, preserve agent_file from constructor (don't overwrite)
|
||||
|
||||
if args.deploy:
|
||||
from .deploy import CloudDeployer
|
||||
deployer = CloudDeployer()
|
||||
deployer.run_commands()
|
||||
if args.schedule or args.schedule_config:
|
||||
# Scheduled deployment
|
||||
from .scheduler import create_scheduler
|
||||
|
||||
# Load configuration from file if provided
|
||||
config = {"max_retries": args.max_retries}
|
||||
schedule_expr = args.schedule
|
||||
provider = args.provider
|
||||
|
||||
if args.schedule_config:
|
||||
try:
|
||||
with open(args.schedule_config, 'r') as f:
|
||||
file_config = yaml.safe_load(f)
|
||||
|
||||
# Extract deployment config
|
||||
deploy_config = file_config.get('deployment', {})
|
||||
schedule_expr = schedule_expr or deploy_config.get('schedule')
|
||||
provider = deploy_config.get('provider', provider)
|
||||
config['max_retries'] = deploy_config.get('max_retries', config['max_retries'])
|
||||
|
||||
# Apply environment variables if specified
|
||||
env_vars = file_config.get('environment', {})
|
||||
for key, value in env_vars.items():
|
||||
os.environ[key] = str(value)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Configuration file not found: {args.schedule_config}")
|
||||
sys.exit(1)
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing configuration file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if not schedule_expr:
|
||||
print("Error: Schedule expression required. Use --schedule or specify in config file.")
|
||||
sys.exit(1)
|
||||
|
||||
scheduler = create_scheduler(provider=provider, config=config)
|
||||
|
||||
print(f"Starting scheduled deployment with schedule: {schedule_expr}")
|
||||
print(f"Provider: {provider}")
|
||||
print(f"Max retries: {config['max_retries']}")
|
||||
print("Press Ctrl+C to stop the scheduler")
|
||||
|
||||
if scheduler.start(schedule_expr, config['max_retries']):
|
||||
try:
|
||||
# Keep the main thread alive
|
||||
while scheduler.is_running:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping scheduler...")
|
||||
scheduler.stop()
|
||||
print("Scheduler stopped successfully")
|
||||
else:
|
||||
print("Failed to start scheduler")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# One-time deployment (backward compatible)
|
||||
from .deploy import CloudDeployer
|
||||
deployer = CloudDeployer()
|
||||
deployer.run_commands()
|
||||
return
|
||||
|
||||
if getattr(args, 'chat', False):
|
||||
@@ -242,6 +307,11 @@ class PraisonAI:
|
||||
return
|
||||
|
||||
if args.command == 'train':
|
||||
if not TRAIN_AVAILABLE:
|
||||
print("[red]ERROR: Training dependencies not installed. Install with:[/red]")
|
||||
print("\npip install \"praisonai[train]\"")
|
||||
print("Or run: praisonai train init\n")
|
||||
sys.exit(1)
|
||||
package_root = os.path.dirname(os.path.abspath(__file__))
|
||||
config_yaml_destination = os.path.join(os.getcwd(), 'config.yaml')
|
||||
|
||||
@@ -347,7 +417,7 @@ class PraisonAI:
|
||||
|
||||
self.agent_file = "test.yaml"
|
||||
generator = AutoGenerator(topic=self.topic, framework=self.framework, agent_file=self.agent_file)
|
||||
self.agent_file = generator.generate()
|
||||
self.agent_file = generator.generate(merge=getattr(args, 'merge', False))
|
||||
agents_generator = AgentsGenerator(self.agent_file, self.framework, self.config_list)
|
||||
result = agents_generator.generate_crew_and_kickoff()
|
||||
print(result)
|
||||
@@ -360,7 +430,7 @@ class PraisonAI:
|
||||
|
||||
self.agent_file = "agents.yaml"
|
||||
generator = AutoGenerator(topic=self.topic, framework=self.framework, agent_file=self.agent_file)
|
||||
self.agent_file = generator.generate()
|
||||
self.agent_file = generator.generate(merge=getattr(args, 'merge', False))
|
||||
print(f"File {self.agent_file} created successfully")
|
||||
return f"File {self.agent_file} created successfully"
|
||||
|
||||
@@ -406,6 +476,36 @@ class PraisonAI:
|
||||
'unittest' in sys.modules
|
||||
)
|
||||
|
||||
# Check if we're being used as a library (not from praisonai CLI)
|
||||
# Skip CLI parsing to avoid conflicts with applications like Fabric
|
||||
is_library_usage = (
|
||||
'praisonai' not in sys.argv[0] and
|
||||
not in_test_env
|
||||
)
|
||||
|
||||
if is_library_usage:
|
||||
# Return default args when used as library to prevent CLI conflicts
|
||||
default_args = argparse.Namespace()
|
||||
default_args.framework = None
|
||||
default_args.ui = None
|
||||
default_args.auto = None
|
||||
default_args.init = None
|
||||
default_args.command = None
|
||||
default_args.deploy = False
|
||||
default_args.schedule = None
|
||||
default_args.schedule_config = None
|
||||
default_args.provider = "gcp"
|
||||
default_args.max_retries = 3
|
||||
default_args.model = None
|
||||
default_args.llm = None
|
||||
default_args.hf = None
|
||||
default_args.ollama = None
|
||||
default_args.dataset = "yahma/alpaca-cleaned"
|
||||
default_args.realtime = False
|
||||
default_args.call = False
|
||||
default_args.public = False
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui']
|
||||
|
||||
@@ -416,6 +516,10 @@ class PraisonAI:
|
||||
parser.add_argument("--init", nargs=argparse.REMAINDER, help="Initialize agents with optional topic")
|
||||
parser.add_argument("command", nargs="?", help="Command to run or direct prompt")
|
||||
parser.add_argument("--deploy", action="store_true", help="Deploy the application")
|
||||
parser.add_argument("--schedule", type=str, help="Schedule deployment (e.g., 'daily', 'hourly', '*/6h', '3600')")
|
||||
parser.add_argument("--schedule-config", type=str, help="Path to scheduling configuration file")
|
||||
parser.add_argument("--provider", type=str, default="gcp", help="Deployment provider (gcp, aws, azure)")
|
||||
parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry attempts for scheduled deployments")
|
||||
parser.add_argument("--model", type=str, help="Model name")
|
||||
parser.add_argument("--llm", type=str, help="LLM model to use for direct prompts")
|
||||
parser.add_argument("--hf", type=str, help="Hugging Face model name")
|
||||
@@ -424,6 +528,7 @@ class PraisonAI:
|
||||
parser.add_argument("--realtime", action="store_true", help="Start the realtime voice interaction interface")
|
||||
parser.add_argument("--call", action="store_true", help="Start the PraisonAI Call server")
|
||||
parser.add_argument("--public", action="store_true", help="Use ngrok to expose the server publicly (only with --call)")
|
||||
parser.add_argument("--merge", action="store_true", help="Merge existing agents.yaml with auto-generated agents instead of overwriting")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
@@ -509,6 +614,11 @@ class PraisonAI:
|
||||
sys.exit(0)
|
||||
|
||||
elif args.command == 'train':
|
||||
if not TRAIN_AVAILABLE:
|
||||
print("[red]ERROR: Training dependencies not installed. Install with:[/red]")
|
||||
print("\npip install \"praisonai[train]\"")
|
||||
print("Or run: praisonai train init\n")
|
||||
sys.exit(1)
|
||||
package_root = os.path.dirname(os.path.abspath(__file__))
|
||||
config_yaml_destination = os.path.join(os.getcwd(), 'config.yaml')
|
||||
|
||||
|
||||
@@ -56,7 +56,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.20 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.25 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Union, Optional, Callable, Dict, Any
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DeployerInterface(ABC):
|
||||
"""Abstract interface for deployers to ensure provider compatibility."""
|
||||
|
||||
@abstractmethod
|
||||
def deploy(self) -> bool:
|
||||
"""Execute deployment. Returns True on success, False on failure."""
|
||||
pass
|
||||
|
||||
class CloudDeployerAdapter(DeployerInterface):
|
||||
"""Adapter for existing CloudDeployer to match interface."""
|
||||
|
||||
def __init__(self):
|
||||
from .deploy import CloudDeployer
|
||||
self._deployer = CloudDeployer()
|
||||
|
||||
def deploy(self) -> bool:
|
||||
"""Execute deployment using CloudDeployer."""
|
||||
try:
|
||||
self._deployer.run_commands()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Deployment failed: {e}")
|
||||
return False
|
||||
|
||||
class ScheduleParser:
|
||||
"""Parse schedule expressions into intervals."""
|
||||
|
||||
@staticmethod
|
||||
def parse(schedule_expr: str) -> int:
|
||||
"""
|
||||
Parse schedule expression and return interval in seconds.
|
||||
|
||||
Supported formats:
|
||||
- "daily" -> 86400 seconds
|
||||
- "hourly" -> 3600 seconds
|
||||
- "*/30m" -> 1800 seconds (every 30 minutes)
|
||||
- "*/6h" -> 21600 seconds (every 6 hours)
|
||||
- "60" -> 60 seconds (plain number)
|
||||
"""
|
||||
schedule_expr = schedule_expr.strip().lower()
|
||||
|
||||
if schedule_expr == "daily":
|
||||
return 86400
|
||||
elif schedule_expr == "hourly":
|
||||
return 3600
|
||||
elif schedule_expr.isdigit():
|
||||
return int(schedule_expr)
|
||||
elif schedule_expr.startswith("*/"):
|
||||
# Handle */30m, */6h patterns
|
||||
interval_part = schedule_expr[2:]
|
||||
if interval_part.endswith("m"):
|
||||
minutes = int(interval_part[:-1])
|
||||
return minutes * 60
|
||||
elif interval_part.endswith("h"):
|
||||
hours = int(interval_part[:-1])
|
||||
return hours * 3600
|
||||
elif interval_part.endswith("s"):
|
||||
return int(interval_part[:-1])
|
||||
else:
|
||||
return int(interval_part)
|
||||
else:
|
||||
raise ValueError(f"Unsupported schedule format: {schedule_expr}")
|
||||
|
||||
class DeploymentScheduler:
|
||||
"""
|
||||
Minimal deployment scheduler with provider-agnostic design.
|
||||
|
||||
Features:
|
||||
- Simple interval-based scheduling
|
||||
- Thread-safe operation
|
||||
- Extensible deployer factory pattern
|
||||
- Minimal dependencies
|
||||
"""
|
||||
|
||||
def __init__(self, schedule_config: Optional[Dict[str, Any]] = None):
|
||||
self.config = schedule_config or {}
|
||||
self.is_running = False
|
||||
self._stop_event = threading.Event()
|
||||
self._thread = None
|
||||
self._deployer = None
|
||||
|
||||
def set_deployer(self, deployer: DeployerInterface):
|
||||
"""Set custom deployer implementation."""
|
||||
self._deployer = deployer
|
||||
|
||||
def _get_deployer(self) -> DeployerInterface:
|
||||
"""Get deployer instance using factory pattern."""
|
||||
if self._deployer:
|
||||
return self._deployer
|
||||
|
||||
# Default to CloudDeployer for backward compatibility
|
||||
return CloudDeployerAdapter()
|
||||
|
||||
def start(self, schedule_expr: str, max_retries: int = 3) -> bool:
|
||||
"""
|
||||
Start scheduled deployment.
|
||||
|
||||
Args:
|
||||
schedule_expr: Schedule expression (e.g., "daily", "*/6h", "3600")
|
||||
max_retries: Maximum retry attempts on failure
|
||||
|
||||
Returns:
|
||||
True if scheduler started successfully
|
||||
"""
|
||||
if self.is_running:
|
||||
logger.warning("Scheduler is already running")
|
||||
return False
|
||||
|
||||
try:
|
||||
interval = ScheduleParser.parse(schedule_expr)
|
||||
self.is_running = True
|
||||
self._stop_event.clear()
|
||||
|
||||
self._thread = threading.Thread(
|
||||
target=self._run_schedule,
|
||||
args=(interval, max_retries),
|
||||
daemon=True
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
logger.info(f"Deployment scheduler started with {interval}s interval")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start scheduler: {e}")
|
||||
self.is_running = False
|
||||
return False
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""Stop the scheduler."""
|
||||
if not self.is_running:
|
||||
return True
|
||||
|
||||
self._stop_event.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
self.is_running = False
|
||||
logger.info("Deployment scheduler stopped")
|
||||
return True
|
||||
|
||||
def _run_schedule(self, interval: int, max_retries: int):
|
||||
"""Internal method to run scheduled deployments."""
|
||||
deployer = self._get_deployer()
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
logger.info("Starting scheduled deployment")
|
||||
|
||||
success = False
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
if deployer.deploy():
|
||||
logger.info(f"Deployment successful on attempt {attempt + 1}")
|
||||
success = True
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Deployment failed on attempt {attempt + 1}")
|
||||
except Exception as e:
|
||||
logger.error(f"Deployment error on attempt {attempt + 1}: {e}")
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(30) # Wait before retry
|
||||
|
||||
if not success:
|
||||
logger.error(f"Deployment failed after {max_retries} attempts")
|
||||
|
||||
# Wait for next scheduled time
|
||||
self._stop_event.wait(interval)
|
||||
|
||||
def deploy_once(self) -> bool:
|
||||
"""Execute a single deployment immediately."""
|
||||
deployer = self._get_deployer()
|
||||
try:
|
||||
return deployer.deploy()
|
||||
except Exception as e:
|
||||
logger.error(f"One-time deployment failed: {e}")
|
||||
return False
|
||||
|
||||
def create_scheduler(provider: str = "gcp", config: Optional[Dict[str, Any]] = None) -> DeploymentScheduler:
|
||||
"""
|
||||
Factory function to create scheduler for different providers.
|
||||
|
||||
Args:
|
||||
provider: Deployment provider ("gcp", "aws", "azure", etc.)
|
||||
config: Optional configuration dict
|
||||
|
||||
Returns:
|
||||
Configured DeploymentScheduler instance
|
||||
"""
|
||||
scheduler = DeploymentScheduler(config)
|
||||
|
||||
# Provider-specific deployer setup can be added here
|
||||
if provider == "gcp":
|
||||
# Default CloudDeployer for GCP
|
||||
pass
|
||||
elif provider == "aws":
|
||||
# Future: AWS deployer implementation
|
||||
logger.warning("AWS provider not yet implemented, using default")
|
||||
elif provider == "azure":
|
||||
# Future: Azure deployer implementation
|
||||
logger.warning("Azure provider not yet implemented, using default")
|
||||
else:
|
||||
logger.warning(f"Unknown provider {provider}, using default")
|
||||
|
||||
return scheduler
|
||||
@@ -72,7 +72,7 @@ async def tavily_web_search(query):
|
||||
response = tavily_client.search(query)
|
||||
logger.debug(f"Tavily search response: {response}")
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async with AsyncAsyncWebCrawler() as crawler:
|
||||
results = []
|
||||
for result in response.get('results', []):
|
||||
url = result.get('url')
|
||||
|
||||
@@ -20,6 +20,7 @@ from chainlit.input_widget import TextInput
|
||||
from chainlit.types import ThreadDict
|
||||
import chainlit.data as cl_data
|
||||
from litellm import acompletion
|
||||
import litellm
|
||||
from db import DatabaseManager
|
||||
|
||||
# Load environment variables
|
||||
@@ -40,6 +41,15 @@ logger.addHandler(console_handler)
|
||||
# Set the logging level for the logger
|
||||
logger.setLevel(log_level)
|
||||
|
||||
# Configure litellm same as in llm.py
|
||||
litellm.set_verbose = False
|
||||
litellm.success_callback = []
|
||||
litellm._async_success_callback = []
|
||||
litellm.callbacks = []
|
||||
litellm.drop_params = True
|
||||
litellm.modify_params = True
|
||||
litellm.suppress_debug_messages = True
|
||||
|
||||
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
|
||||
|
||||
if not CHAINLIT_AUTH_SECRET:
|
||||
@@ -55,6 +65,17 @@ db_manager.initialize()
|
||||
|
||||
deleted_thread_ids = [] # type: List[str]
|
||||
|
||||
def _build_completion_params(model_name, **override_params):
|
||||
"""Build parameters for litellm completion calls with proper model handling"""
|
||||
params = {
|
||||
"model": model_name,
|
||||
}
|
||||
|
||||
# Override with any provided parameters
|
||||
params.update(override_params)
|
||||
|
||||
return params
|
||||
|
||||
def save_setting(key: str, value: str):
|
||||
"""Saves a setting to the database.
|
||||
|
||||
@@ -153,8 +174,8 @@ async def tavily_web_search(query):
|
||||
response = tavily_client.search(query)
|
||||
logger.debug(f"Tavily search response: {response}")
|
||||
|
||||
# Create an instance of AsyncWebCrawler
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Create an instance of AsyncAsyncWebCrawler
|
||||
async with AsyncAsyncWebCrawler() as crawler:
|
||||
# Prepare the results
|
||||
results = []
|
||||
for result in response.get('results', []):
|
||||
@@ -237,12 +258,12 @@ Context:
|
||||
msg = cl.Message(content="")
|
||||
await msg.send()
|
||||
|
||||
# Prepare the completion parameters
|
||||
completion_params = {
|
||||
"model": model_name,
|
||||
"messages": message_history,
|
||||
"stream": True,
|
||||
}
|
||||
# Prepare the completion parameters using the helper function
|
||||
completion_params = _build_completion_params(
|
||||
model_name,
|
||||
messages=message_history,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
# If an image is uploaded, include it in the message
|
||||
if image:
|
||||
@@ -344,9 +365,11 @@ Context:
|
||||
logger.error(f"Failed to parse function arguments: {function_args}")
|
||||
|
||||
second_response = await acompletion(
|
||||
model=model_name,
|
||||
stream=True,
|
||||
messages=messages,
|
||||
**_build_completion_params(
|
||||
model_name,
|
||||
stream=True,
|
||||
messages=messages,
|
||||
)
|
||||
)
|
||||
logger.debug(f"Second LLM response: {second_response}")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from litellm import acompletion
|
||||
import json
|
||||
import dotenv
|
||||
from tavily import TavilyClient
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
from crawl4ai import AsyncAsyncWebCrawler
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
@@ -144,7 +144,7 @@ class AICoder:
|
||||
})
|
||||
response = self.tavily_client.search(query)
|
||||
results = []
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async with AsyncAsyncWebCrawler() as crawler:
|
||||
for result in response.get('results', []):
|
||||
url = result.get('url')
|
||||
if url:
|
||||
|
||||
@@ -229,7 +229,7 @@ except Exception as e:
|
||||
@cl.on_chat_start
|
||||
async def start():
|
||||
initialize_db()
|
||||
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini-realtime-preview")
|
||||
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini-realtime-preview-2024-12-17")
|
||||
cl.user_session.set("model_name", model_name)
|
||||
cl.user_session.set("message_history", []) # Initialize message history
|
||||
logger.debug(f"Model name: {model_name}")
|
||||
@@ -238,7 +238,7 @@ async def start():
|
||||
# TextInput(
|
||||
# id="model_name",
|
||||
# label="Enter the Model Name",
|
||||
# placeholder="e.g., gpt-4o-mini-realtime-preview",
|
||||
# placeholder="e.g., gpt-4o-mini-realtime-preview-2024-12-17",
|
||||
# initial=model_name
|
||||
# )
|
||||
# ]
|
||||
@@ -382,7 +382,8 @@ async def on_audio_start():
|
||||
openai_realtime = cl.user_session.get("openai_realtime")
|
||||
|
||||
if not openai_realtime.is_connected():
|
||||
await openai_realtime.connect()
|
||||
model_name = cl.user_session.get("model_name", "gpt-4o-mini-realtime-preview-2024-12-17")
|
||||
await openai_realtime.connect(model_name)
|
||||
|
||||
logger.info("Connected to OpenAI realtime")
|
||||
return True
|
||||
@@ -394,11 +395,22 @@ async def on_audio_start():
|
||||
|
||||
@cl.on_audio_chunk
|
||||
async def on_audio_chunk(chunk: cl.InputAudioChunk):
|
||||
openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime")
|
||||
openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime")
|
||||
|
||||
if not openai_realtime:
|
||||
logger.debug("No realtime client available")
|
||||
return
|
||||
|
||||
if openai_realtime.is_connected():
|
||||
await openai_realtime.append_input_audio(chunk.data)
|
||||
try:
|
||||
success = await openai_realtime.append_input_audio(chunk.data)
|
||||
if not success:
|
||||
logger.debug("Failed to append audio data - connection may be lost")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error processing audio chunk: {e}")
|
||||
# Optionally try to reconnect here if needed
|
||||
else:
|
||||
logger.info("RealtimeClient is not connected")
|
||||
logger.debug("RealtimeClient is not connected - audio chunk ignored")
|
||||
|
||||
@cl.on_audio_end
|
||||
@cl.on_chat_end
|
||||
@@ -423,14 +435,14 @@ def auth_callback(username: str, password: str):
|
||||
@cl.on_chat_resume
|
||||
async def on_chat_resume(thread: ThreadDict):
|
||||
logger.info(f"Resuming chat: {thread['id']}")
|
||||
model_name = load_setting("model_name") or os.getenv("MODEL_NAME") or "gpt-4o-mini-realtime-preview"
|
||||
model_name = os.getenv("MODEL_NAME") or "gpt-4o-mini-realtime-preview-2024-12-17"
|
||||
logger.debug(f"Model name: {model_name}")
|
||||
settings = cl.ChatSettings(
|
||||
[
|
||||
TextInput(
|
||||
id="model_name",
|
||||
label="Enter the Model Name",
|
||||
placeholder="e.g., gpt-4o-mini-realtime-preview",
|
||||
placeholder="e.g., gpt-4o-mini-realtime-preview-2024-12-17",
|
||||
initial=model_name
|
||||
)
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ import inspect
|
||||
import numpy as np
|
||||
import json
|
||||
import websockets
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
import base64
|
||||
@@ -97,29 +98,66 @@ class RealtimeAPI(RealtimeEventHandler):
|
||||
self.ws = None
|
||||
|
||||
def is_connected(self):
|
||||
return self.ws is not None
|
||||
if self.ws is None:
|
||||
return False
|
||||
# Some websockets versions don't have a closed attribute
|
||||
try:
|
||||
return not self.ws.closed
|
||||
except AttributeError:
|
||||
# Fallback: check if websocket is still alive by checking state
|
||||
try:
|
||||
return hasattr(self.ws, 'state') and self.ws.state.name == 'OPEN'
|
||||
except:
|
||||
# Last fallback: assume connected if ws exists
|
||||
return True
|
||||
|
||||
def log(self, *args):
|
||||
logger.debug(f"[Websocket/{datetime.utcnow().isoformat()}]", *args)
|
||||
|
||||
async def connect(self, model='gpt-4o-realtime-preview-2024-10-01'):
|
||||
async def connect(self, model='gpt-4o-mini-realtime-preview-2024-12-17'):
|
||||
if self.is_connected():
|
||||
raise Exception("Already connected")
|
||||
self.ws = await websockets.connect(f"{self.url}?model={model}", extra_headers={
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'OpenAI-Beta': 'realtime=v1'
|
||||
})
|
||||
}
|
||||
|
||||
# Try different header parameter names for compatibility
|
||||
try:
|
||||
self.ws = await websockets.connect(f"{self.url}?model={model}", additional_headers=headers)
|
||||
except TypeError:
|
||||
# Fallback to older websockets versions
|
||||
try:
|
||||
self.ws = await websockets.connect(f"{self.url}?model={model}", extra_headers=headers)
|
||||
except TypeError:
|
||||
# Last fallback - some versions might not support headers parameter
|
||||
raise Exception("Websockets library version incompatible. Please update websockets to version 11.0 or higher.")
|
||||
|
||||
self.log(f"Connected to {self.url}")
|
||||
asyncio.create_task(self._receive_messages())
|
||||
|
||||
async def _receive_messages(self):
|
||||
async for message in self.ws:
|
||||
event = json.loads(message)
|
||||
if event['type'] == "error":
|
||||
logger.error("ERROR", event)
|
||||
self.log("received:", event)
|
||||
self.dispatch(f"server.{event['type']}", event)
|
||||
self.dispatch("server.*", event)
|
||||
try:
|
||||
async for message in self.ws:
|
||||
event = json.loads(message)
|
||||
if event['type'] == "error":
|
||||
logger.error(f"OpenAI Realtime API Error: {event}")
|
||||
self.log("received:", event)
|
||||
self.dispatch(f"server.{event['type']}", event)
|
||||
self.dispatch("server.*", event)
|
||||
except ConnectionClosed as e:
|
||||
logger.info(f"WebSocket connection closed normally: {e}")
|
||||
# Mark connection as closed
|
||||
self.ws = None
|
||||
# Dispatch disconnection event
|
||||
self.dispatch("disconnected", {"reason": str(e)})
|
||||
except Exception as e:
|
||||
logger.warning(f"WebSocket receive loop ended: {e}")
|
||||
# Mark connection as closed
|
||||
self.ws = None
|
||||
# Dispatch disconnection event
|
||||
self.dispatch("disconnected", {"reason": str(e)})
|
||||
|
||||
async def send(self, event_name, data=None):
|
||||
if not self.is_connected():
|
||||
@@ -135,16 +173,33 @@ class RealtimeAPI(RealtimeEventHandler):
|
||||
self.dispatch(f"client.{event_name}", event)
|
||||
self.dispatch("client.*", event)
|
||||
self.log("sent:", event)
|
||||
await self.ws.send(json.dumps(event))
|
||||
|
||||
try:
|
||||
await self.ws.send(json.dumps(event))
|
||||
except ConnectionClosed as e:
|
||||
logger.info(f"WebSocket connection closed during send: {e}")
|
||||
# Mark connection as closed if send fails
|
||||
self.ws = None
|
||||
raise Exception(f"WebSocket connection lost: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send WebSocket message: {e}")
|
||||
# Mark connection as closed if send fails
|
||||
self.ws = None
|
||||
raise Exception(f"WebSocket connection lost: {e}")
|
||||
|
||||
def _generate_id(self, prefix):
|
||||
return f"{prefix}{int(datetime.utcnow().timestamp() * 1000)}"
|
||||
|
||||
async def disconnect(self):
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
self.ws = None
|
||||
self.log(f"Disconnected from {self.url}")
|
||||
try:
|
||||
await self.ws.close()
|
||||
logger.info(f"Disconnected from {self.url}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during WebSocket close: {e}")
|
||||
finally:
|
||||
self.ws = None
|
||||
self.log(f"WebSocket connection cleaned up")
|
||||
|
||||
class RealtimeConversation:
|
||||
default_frequency = config.features.audio.sample_rate
|
||||
@@ -341,8 +396,7 @@ class RealtimeConversation:
|
||||
return None, None
|
||||
array_buffer = base64_to_array_buffer(delta)
|
||||
append_values = array_buffer.tobytes()
|
||||
# TODO: make it work
|
||||
# item['formatted']['audio'] = merge_int16_arrays(item['formatted']['audio'], append_values)
|
||||
item['formatted']['audio'].append(append_values)
|
||||
return item, {'audio': append_values}
|
||||
|
||||
def _process_text_delta(self, event):
|
||||
@@ -381,7 +435,6 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
"tools": [],
|
||||
"tool_choice": "auto",
|
||||
"temperature": 0.8,
|
||||
"max_response_output_tokens": 4096,
|
||||
}
|
||||
self.session_config = {}
|
||||
self.transcription_models = [{"model": "whisper-1"}]
|
||||
@@ -431,8 +484,13 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
self.dispatch("realtime.event", realtime_event)
|
||||
|
||||
def _on_session_created(self, event):
|
||||
print(f"Session created: {event}")
|
||||
logger.debug(f"Session created: {event}")
|
||||
try:
|
||||
session_id = event.get('session', {}).get('id', 'unknown')
|
||||
model = event.get('session', {}).get('model', 'unknown')
|
||||
logger.info(f"OpenAI Realtime session created - ID: {session_id}, Model: {model}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing session created event: {e}")
|
||||
logger.debug(f"Session event details: {event}")
|
||||
self.session_created = True
|
||||
|
||||
def _process_event(self, event, *args):
|
||||
@@ -497,10 +555,15 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
self._add_api_event_handlers()
|
||||
return True
|
||||
|
||||
async def connect(self):
|
||||
async def connect(self, model=None):
|
||||
if self.is_connected():
|
||||
raise Exception("Already connected, use .disconnect() first")
|
||||
await self.realtime.connect()
|
||||
|
||||
# Use provided model or default
|
||||
if model is None:
|
||||
model = 'gpt-4o-mini-realtime-preview-2024-12-17'
|
||||
|
||||
await self.realtime.connect(model)
|
||||
await self.update_session()
|
||||
return True
|
||||
|
||||
@@ -516,6 +579,7 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
self.conversation.clear()
|
||||
if self.realtime.is_connected():
|
||||
await self.realtime.disconnect()
|
||||
logger.info("RealtimeClient disconnected")
|
||||
|
||||
def get_turn_detection_type(self):
|
||||
return self.session_config.get("turn_detection", {}).get("type")
|
||||
@@ -579,11 +643,22 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
return True
|
||||
|
||||
async def append_input_audio(self, array_buffer):
|
||||
if not self.is_connected():
|
||||
logger.warning("Cannot append audio: RealtimeClient is not connected")
|
||||
return False
|
||||
|
||||
if len(array_buffer) > 0:
|
||||
await self.realtime.send("input_audio_buffer.append", {
|
||||
"audio": array_buffer_to_base64(np.array(array_buffer)),
|
||||
})
|
||||
self.input_audio_buffer.extend(array_buffer)
|
||||
try:
|
||||
await self.realtime.send("input_audio_buffer.append", {
|
||||
"audio": array_buffer_to_base64(np.array(array_buffer)),
|
||||
})
|
||||
self.input_audio_buffer.extend(array_buffer)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to append input audio: {e}")
|
||||
# Connection might be lost, mark as disconnected
|
||||
if "connection" in str(e).lower() or "closed" in str(e).lower():
|
||||
logger.warning("WebSocket connection appears to be lost. Audio input will be queued until reconnection.")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def create_response(self):
|
||||
@@ -650,4 +725,17 @@ class RealtimeClient(RealtimeEventHandler):
|
||||
logger.debug(f"Unhandled item type:\n{json.dumps(item, indent=2)}")
|
||||
|
||||
# Additional debug logging
|
||||
logger.debug(f"Processed Chainlit message for item: {item.get('id', 'unknown')}")
|
||||
logger.debug(f"Processed Chainlit message for item: {item.get('id', 'unknown')}")
|
||||
|
||||
async def ensure_connected(self):
|
||||
"""Check connection health and attempt reconnection if needed"""
|
||||
if not self.is_connected():
|
||||
try:
|
||||
logger.info("Attempting to reconnect to OpenAI Realtime API...")
|
||||
model = 'gpt-4o-mini-realtime-preview-2024-12-17'
|
||||
await self.connect(model)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reconnect: {e}")
|
||||
return False
|
||||
return True
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@ import chainlit as cl
|
||||
import plotly
|
||||
import json
|
||||
from tavily import TavilyClient
|
||||
from crawl4ai import WebCrawler
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
import os
|
||||
import logging
|
||||
import asyncio
|
||||
@@ -108,7 +108,7 @@ async def tavily_web_search_handler(query):
|
||||
try:
|
||||
response = tavily_client.search(query_with_date)
|
||||
logger.debug(f"Tavily search response: {response}")
|
||||
results = process_tavily_results(response)
|
||||
results = await process_tavily_results(response)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Tavily search: {str(e)}")
|
||||
results = await fallback_to_duckduckgo(query_with_date)
|
||||
@@ -121,27 +121,27 @@ async def tavily_web_search_handler(query):
|
||||
"results": results
|
||||
})
|
||||
|
||||
def process_tavily_results(response):
|
||||
crawler = WebCrawler()
|
||||
crawler.warmup()
|
||||
results = []
|
||||
for result in response.get('results', []):
|
||||
url = result.get('url')
|
||||
if url:
|
||||
try:
|
||||
crawl_result = crawler.run(url=url)
|
||||
results.append({
|
||||
"content": result.get('content'),
|
||||
"url": url,
|
||||
"full_content": crawl_result.markdown
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling {url}: {str(e)}")
|
||||
results.append({
|
||||
"content": result.get('content'),
|
||||
"url": url,
|
||||
"full_content": "Error: Unable to crawl this URL"
|
||||
})
|
||||
async def process_tavily_results(response):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = []
|
||||
for result in response.get('results', []):
|
||||
url = result.get('url')
|
||||
if url:
|
||||
try:
|
||||
crawl_result = await crawler.arun(url=url)
|
||||
full_content = crawl_result.markdown if crawl_result and hasattr(crawl_result, 'markdown') and crawl_result.markdown else "No content available"
|
||||
results.append({
|
||||
"content": result.get('content'),
|
||||
"url": url,
|
||||
"full_content": full_content
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling {url}: {str(e)}")
|
||||
results.append({
|
||||
"content": result.get('content'),
|
||||
"url": url,
|
||||
"full_content": "Error: Unable to crawl this URL"
|
||||
})
|
||||
return results
|
||||
|
||||
async def fallback_to_duckduckgo(query):
|
||||
@@ -151,33 +151,33 @@ async def fallback_to_duckduckgo(query):
|
||||
|
||||
logger.debug(f"DuckDuckGo search results: {ddg_results}")
|
||||
|
||||
crawler = WebCrawler()
|
||||
crawler.warmup()
|
||||
results = []
|
||||
|
||||
for result in ddg_results:
|
||||
url = result.get('href')
|
||||
if url:
|
||||
try:
|
||||
crawl_result = crawler.run(url=url)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = []
|
||||
|
||||
for result in ddg_results:
|
||||
url = result.get('href')
|
||||
if url:
|
||||
try:
|
||||
crawl_result = await crawler.arun(url=url)
|
||||
full_content = crawl_result.markdown if crawl_result and hasattr(crawl_result, 'markdown') and crawl_result.markdown else "No content available"
|
||||
results.append({
|
||||
"content": result.get('body'),
|
||||
"url": url,
|
||||
"full_content": full_content
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling {url}: {str(e)}")
|
||||
results.append({
|
||||
"content": result.get('body'),
|
||||
"url": url,
|
||||
"full_content": "Error: Unable to crawl this URL"
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"content": result.get('body'),
|
||||
"url": url,
|
||||
"full_content": crawl_result.markdown
|
||||
"url": "N/A",
|
||||
"full_content": "No URL provided for crawling"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling {url}: {str(e)}")
|
||||
results.append({
|
||||
"content": result.get('body'),
|
||||
"url": url,
|
||||
"full_content": "Error: Unable to crawl this URL"
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"content": result.get('body'),
|
||||
"url": "N/A",
|
||||
"full_content": "No URL provided for crawling"
|
||||
})
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.20"
|
||||
version = "2.2.25"
|
||||
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.91",
|
||||
"praisonaiagents>=0.0.96",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
@@ -89,12 +89,13 @@ call = [
|
||||
"rich",
|
||||
"openai>=1.54.0",
|
||||
]
|
||||
train = []
|
||||
crewai = ["crewai>=0.32.0", "praisonai-tools>=0.0.15"]
|
||||
autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.20"
|
||||
version = "2.2.25"
|
||||
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 = ""
|
||||
@@ -112,7 +113,7 @@ python = ">=3.10,<3.13"
|
||||
rich = ">=13.7"
|
||||
markdown = ">=3.5"
|
||||
pyparsing = ">=3.0.0"
|
||||
praisonaiagents = ">=0.0.91"
|
||||
praisonaiagents = ">=0.0.96"
|
||||
python-dotenv = ">=0.19.0"
|
||||
instructor = ">=1.3.3"
|
||||
PyYAML = ">=6.0"
|
||||
@@ -146,6 +147,7 @@ sqlalchemy = {version = ">=2.0.36", optional = true}
|
||||
playwright = {version = ">=1.47.0", optional = true}
|
||||
openai = {version = ">=1.54.0", optional = true}
|
||||
pydantic = {version = "<=2.10.1", optional = true}
|
||||
# unsloth = {version = ">=2024.11.7", extras = ["colab-new"], optional = true}
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "*"
|
||||
@@ -249,7 +251,7 @@ code = [
|
||||
"playwright",
|
||||
"pydantic"
|
||||
]
|
||||
train = ["setup-conda-env"]
|
||||
train = []
|
||||
realtime = [
|
||||
"chainlit",
|
||||
"litellm",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# PraisonAI Deployment Scheduler Configuration
|
||||
# This is an example configuration file for scheduled deployments
|
||||
|
||||
deployment:
|
||||
# Schedule expression (e.g., "daily", "hourly", "*/6h", "3600")
|
||||
schedule: "daily"
|
||||
|
||||
# Deployment provider (gcp, aws, azure)
|
||||
provider: "gcp"
|
||||
|
||||
# Maximum retry attempts on deployment failure
|
||||
max_retries: 3
|
||||
|
||||
# Optional: timezone for scheduling (future feature)
|
||||
# timezone: "UTC"
|
||||
|
||||
# Optional: retry delay in seconds
|
||||
# retry_delay: 30
|
||||
|
||||
# Optional: Environment variables for deployment
|
||||
# environment:
|
||||
# OPENAI_MODEL_NAME: "gpt-4o"
|
||||
# OPENAI_API_KEY: "your-api-key"
|
||||
# OPENAI_API_BASE: "https://api.openai.com/v1"
|
||||
|
||||
# Optional: Notification settings (future feature)
|
||||
# notifications:
|
||||
# on_success: true
|
||||
# on_failure: true
|
||||
# webhook_url: "https://your-webhook-url.com"
|
||||
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Integration Test for Human Approval System
|
||||
|
||||
This test demonstrates the approval system working through the agent's execute_tool method,
|
||||
which is where the approval checks are actually implemented.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
# Add the praisonai-agents module to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'praisonai-agents'))
|
||||
|
||||
# Run interactively only when ASK_USER=1 is set
|
||||
@pytest.mark.skipif(os.getenv("ASK_USER") != "1", reason="interactive approval requires user input")
|
||||
def test_agent_tool_execution_with_approval():
|
||||
"""Test that agent tool execution triggers approval prompts."""
|
||||
print("\n🤖 Testing Agent Tool Execution with Approval")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, console_approval_callback, ApprovalDecision
|
||||
|
||||
# Use auto-approval when running non-interactive
|
||||
if os.getenv("ASK_USER") == "1":
|
||||
set_approval_callback(console_approval_callback)
|
||||
else:
|
||||
# Auto-approve for CI
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for CI")
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
# Create agent with dangerous tools
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Security Tester",
|
||||
goal="Test the human approval system",
|
||||
tools=[ShellTools()],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print("About to execute a shell command through the agent...")
|
||||
print("This should trigger an approval prompt.")
|
||||
|
||||
# Execute tool through agent - this should trigger approval
|
||||
result = agent.execute_tool("execute_command", {"command": "echo 'Hello from agent-executed command!'"})
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Command executed successfully: {result['stdout']}")
|
||||
elif result.get('approval_denied'):
|
||||
print(f"❌ Command was denied by approval system: {result['error']}")
|
||||
else:
|
||||
print(f"⚠️ Command failed for other reasons: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Agent tool execution test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_agent_with_auto_approval():
|
||||
"""Test agent tool execution with auto-approval callback."""
|
||||
print("\n🤖 Testing Agent with Auto-Approval")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create auto-approval callback
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
print(f"🤖 Auto-approving {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for testing")
|
||||
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="Auto-Approve Agent",
|
||||
role="Automated Tester",
|
||||
goal="Test auto-approval",
|
||||
tools=[ShellTools()],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print("Executing command with auto-approval...")
|
||||
result = agent.execute_tool("execute_command", {"command": "echo 'Auto-approved command executed!'"})
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Auto-approved command executed: {result['stdout']}")
|
||||
else:
|
||||
print(f"❌ Auto-approved command failed: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Auto-approval test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_agent_with_auto_denial():
|
||||
"""Test agent tool execution with auto-denial callback."""
|
||||
print("\n🚫 Testing Agent with Auto-Denial")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create auto-denial callback
|
||||
def auto_deny_callback(function_name, arguments, risk_level):
|
||||
print(f"🚫 Auto-denying {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=False, reason="Auto-denied for testing")
|
||||
|
||||
set_approval_callback(auto_deny_callback)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="Auto-Deny Agent",
|
||||
role="Security Tester",
|
||||
goal="Test auto-denial",
|
||||
tools=[ShellTools()],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print("Executing command with auto-denial...")
|
||||
result = agent.execute_tool("execute_command", {"command": "echo 'This should be denied'"})
|
||||
|
||||
if result.get('approval_denied'):
|
||||
print("✅ Command was correctly denied by approval system")
|
||||
elif result.get('success'):
|
||||
print("❌ Command executed when it should have been denied")
|
||||
else:
|
||||
print(f"⚠️ Command failed for other reasons: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Auto-denial test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_agent_python_code_execution():
|
||||
"""Test Python code execution through agent with approval."""
|
||||
print("\n🐍 Testing Agent Python Code Execution")
|
||||
print("=" * 45)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.python_tools import PythonTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create auto-approval for this test
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
print(f"🤖 Auto-approving {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for testing")
|
||||
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="Python Agent",
|
||||
role="Code Executor",
|
||||
goal="Test Python code execution",
|
||||
tools=[PythonTools()],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
code = """
|
||||
print("Hello from agent-executed Python code!")
|
||||
result = 3 * 7
|
||||
print(f"3 * 7 = {result}")
|
||||
"""
|
||||
|
||||
print("Executing Python code through agent...")
|
||||
result = agent.execute_tool("execute_code", {"code": code})
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Python code executed successfully: {result['output']}")
|
||||
else:
|
||||
print(f"❌ Python code execution failed: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Python code execution test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_agent_file_operations():
|
||||
"""Test file operations through agent with approval."""
|
||||
print("\n📁 Testing Agent File Operations")
|
||||
print("=" * 35)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.file_tools import FileTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create auto-approval for this test
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
print(f"🤖 Auto-approving {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for testing")
|
||||
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="File Agent",
|
||||
role="File Manager",
|
||||
goal="Test file operations",
|
||||
tools=[FileTools()],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Test file creation
|
||||
print("Creating file through agent...")
|
||||
result = agent.execute_tool("write_file", {
|
||||
"file_path": "test_agent_file.txt",
|
||||
"content": "This file was created through agent with approval!"
|
||||
})
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ File created successfully: {result['message']}")
|
||||
|
||||
# Test file deletion
|
||||
print("Deleting file through agent...")
|
||||
delete_result = agent.execute_tool("delete_file", {"file_path": "test_agent_file.txt"})
|
||||
|
||||
if delete_result.get('success'):
|
||||
print(f"✅ File deleted successfully: {delete_result['message']}")
|
||||
else:
|
||||
print(f"❌ File deletion failed: {delete_result}")
|
||||
else:
|
||||
print(f"❌ File creation failed: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ File operations test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run agent integration tests for the approval system."""
|
||||
print("🧪 PraisonAI Human Approval System - Agent Integration Tests")
|
||||
print("=" * 65)
|
||||
print("These tests demonstrate the approval system working through agent tool execution.")
|
||||
print()
|
||||
|
||||
# Ask user which tests to run
|
||||
print("Available tests:")
|
||||
print("1. Agent Tool Execution with Interactive Approval")
|
||||
print("2. Agent with Auto-Approval")
|
||||
print("3. Agent with Auto-Denial")
|
||||
print("4. Agent Python Code Execution")
|
||||
print("5. Agent File Operations")
|
||||
print("6. Run all tests")
|
||||
print()
|
||||
|
||||
try:
|
||||
choice = input("Enter your choice (1-6): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
test_agent_tool_execution_with_approval()
|
||||
elif choice == "2":
|
||||
test_agent_with_auto_approval()
|
||||
elif choice == "3":
|
||||
test_agent_with_auto_denial()
|
||||
elif choice == "4":
|
||||
test_agent_python_code_execution()
|
||||
elif choice == "5":
|
||||
test_agent_file_operations()
|
||||
elif choice == "6":
|
||||
print("\n🚀 Running all tests...")
|
||||
|
||||
# Run non-interactive tests first
|
||||
print("\n" + "=" * 65)
|
||||
print("PART 1: NON-INTERACTIVE TESTS")
|
||||
print("=" * 65)
|
||||
test_agent_with_auto_approval()
|
||||
test_agent_with_auto_denial()
|
||||
test_agent_python_code_execution()
|
||||
test_agent_file_operations()
|
||||
|
||||
# Ask if user wants to run interactive test
|
||||
print("\n" + "=" * 65)
|
||||
print("PART 2: INTERACTIVE TEST")
|
||||
print("=" * 65)
|
||||
print("The following test requires human interaction.")
|
||||
run_interactive = input("Run interactive approval test? (y/n): ").strip().lower()
|
||||
|
||||
if run_interactive.startswith('y'):
|
||||
test_agent_tool_execution_with_approval()
|
||||
else:
|
||||
print("Skipping interactive test.")
|
||||
else:
|
||||
print("Invalid choice. Exiting.")
|
||||
return
|
||||
|
||||
print("\n🎉 Test completed!")
|
||||
print("\nKey observations:")
|
||||
print("- Approval system works when tools are executed through agent.execute_tool()")
|
||||
print("- Direct tool calls bypass the approval system")
|
||||
print("- Risk levels are correctly identified and displayed")
|
||||
print("- Auto-approval and auto-denial callbacks work as expected")
|
||||
print("- The approval system integrates properly with the agent architecture")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n❌ Test cancelled by user.")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed with error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic test to verify the human approval system implementation.
|
||||
|
||||
This test focuses on ensuring the approval decorators and callback system work correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# Add the praisonai-agents module to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'praisonai-agents'))
|
||||
|
||||
def test_imports():
|
||||
"""Test that all the new approval imports work correctly."""
|
||||
try:
|
||||
from praisonaiagents.approval import (
|
||||
require_approval,
|
||||
ApprovalDecision,
|
||||
console_approval_callback,
|
||||
request_approval,
|
||||
add_approval_requirement,
|
||||
remove_approval_requirement,
|
||||
is_approval_required,
|
||||
get_risk_level,
|
||||
APPROVAL_REQUIRED_TOOLS,
|
||||
TOOL_RISK_LEVELS
|
||||
)
|
||||
print("✅ All approval imports successful")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f"❌ Import failed: {e}")
|
||||
return False
|
||||
|
||||
def test_approval_configuration():
|
||||
"""Test approval requirement configuration."""
|
||||
from praisonaiagents.approval import (
|
||||
add_approval_requirement,
|
||||
remove_approval_requirement,
|
||||
is_approval_required,
|
||||
get_risk_level,
|
||||
APPROVAL_REQUIRED_TOOLS,
|
||||
TOOL_RISK_LEVELS
|
||||
)
|
||||
|
||||
print("\n🔧 Testing approval configuration...")
|
||||
|
||||
# Test adding requirement
|
||||
add_approval_requirement("test_tool", "medium")
|
||||
assert is_approval_required("test_tool"), "Tool should require approval after adding"
|
||||
assert get_risk_level("test_tool") == "medium", "Risk level should be medium"
|
||||
print("✅ Add approval requirement works")
|
||||
|
||||
# Test removing requirement
|
||||
remove_approval_requirement("test_tool")
|
||||
assert not is_approval_required("test_tool"), "Tool should not require approval after removing"
|
||||
assert get_risk_level("test_tool") is None, "Risk level should be None after removal"
|
||||
print("✅ Remove approval requirement works")
|
||||
|
||||
# Test default dangerous tools are configured
|
||||
assert is_approval_required("execute_command"), "execute_command should require approval by default"
|
||||
assert get_risk_level("execute_command") == "critical", "execute_command should be critical risk"
|
||||
print("✅ Default dangerous tools are configured")
|
||||
|
||||
print(f"✅ Current approval-required tools: {len(APPROVAL_REQUIRED_TOOLS)} configured")
|
||||
return True
|
||||
|
||||
def test_approval_decorator():
|
||||
"""Test the require_approval decorator."""
|
||||
from praisonaiagents.approval import require_approval, is_approval_required, get_risk_level
|
||||
|
||||
print("\n🎯 Testing approval decorator...")
|
||||
|
||||
# Test decorator on a test function
|
||||
@require_approval(risk_level="high")
|
||||
def test_dangerous_function(param1, param2="default"):
|
||||
"""A test function that requires approval."""
|
||||
return f"Executed with {param1} and {param2}"
|
||||
|
||||
# Check if the function is marked as requiring approval
|
||||
assert is_approval_required("test_dangerous_function"), "Decorated function should require approval"
|
||||
assert get_risk_level("test_dangerous_function") == "high", "Risk level should match decorator"
|
||||
print("✅ Approval decorator works correctly")
|
||||
|
||||
# Test that the function still executes normally (approval will be checked at agent level)
|
||||
result = test_dangerous_function("test", param2="value")
|
||||
assert "Executed with test and value" in result, "Function should execute normally"
|
||||
print("✅ Decorated function executes correctly")
|
||||
|
||||
return True
|
||||
|
||||
def test_tool_integration():
|
||||
"""Test that dangerous tools have approval decorators."""
|
||||
print("\n🛠️ Testing tool integration...")
|
||||
|
||||
# Test shell tools
|
||||
try:
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
shell_tools = ShellTools()
|
||||
|
||||
# Check if execute_command requires approval
|
||||
from praisonaiagents.approval import is_approval_required
|
||||
assert is_approval_required("execute_command"), "execute_command should require approval"
|
||||
print("✅ ShellTools.execute_command requires approval")
|
||||
|
||||
# Check if kill_process requires approval
|
||||
assert is_approval_required("kill_process"), "kill_process should require approval"
|
||||
print("✅ ShellTools.kill_process requires approval")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Shell tools test failed: {e}")
|
||||
|
||||
# Test python tools
|
||||
try:
|
||||
from praisonaiagents.tools.python_tools import PythonTools
|
||||
python_tools = PythonTools()
|
||||
|
||||
# Check if execute_code requires approval
|
||||
assert is_approval_required("execute_code"), "execute_code should require approval"
|
||||
print("✅ PythonTools.execute_code requires approval")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Python tools test failed: {e}")
|
||||
|
||||
# Test file tools
|
||||
try:
|
||||
from praisonaiagents.tools.file_tools import FileTools
|
||||
file_tools = FileTools()
|
||||
|
||||
# Check if write_file requires approval
|
||||
assert is_approval_required("write_file"), "write_file should require approval"
|
||||
print("✅ FileTools.write_file requires approval")
|
||||
|
||||
# Check if delete_file requires approval
|
||||
assert is_approval_required("delete_file"), "delete_file should require approval"
|
||||
print("✅ FileTools.delete_file requires approval")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ File tools test failed: {e}")
|
||||
|
||||
return True
|
||||
|
||||
async def test_approval_callback():
|
||||
"""Test the approval callback system."""
|
||||
print("\n📞 Testing approval callback system...")
|
||||
|
||||
from praisonaiagents.approval import request_approval, ApprovalDecision
|
||||
|
||||
# Mock approval that auto-denies
|
||||
def mock_denial_callback(function_name, arguments, risk_level):
|
||||
print(f"🔒 Mock callback: Denying {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=False, reason="Test denial")
|
||||
|
||||
# Mock approval that auto-approves
|
||||
def mock_approval_callback(function_name, arguments, risk_level):
|
||||
print(f"✅ Mock callback: Approving {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=True, reason="Test approval")
|
||||
|
||||
# Test with denial callback
|
||||
from praisonaiagents.approval import set_approval_callback
|
||||
set_approval_callback(mock_denial_callback)
|
||||
|
||||
decision = await request_approval("execute_command", {"command": "ls"})
|
||||
assert not decision.approved, "Should be denied by mock callback"
|
||||
assert "Test denial" in decision.reason, "Should have denial reason"
|
||||
print("✅ Denial callback works")
|
||||
|
||||
# Test with approval callback
|
||||
set_approval_callback(mock_approval_callback)
|
||||
|
||||
decision = await request_approval("execute_command", {"command": "ls"})
|
||||
assert decision.approved, "Should be approved by mock callback"
|
||||
assert "Test approval" in decision.reason, "Should have approval reason"
|
||||
print("✅ Approval callback works")
|
||||
|
||||
# Test non-dangerous tool (should auto-approve)
|
||||
decision = await request_approval("safe_function", {})
|
||||
assert decision.approved, "Non-dangerous tools should auto-approve"
|
||||
assert "No approval required" in decision.reason, "Should indicate no approval needed"
|
||||
print("✅ Non-dangerous tools auto-approve")
|
||||
|
||||
return True
|
||||
|
||||
def test_agent_integration():
|
||||
"""Test that agents properly integrate with the approval system."""
|
||||
print("\n🤖 Testing agent integration...")
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
|
||||
# Create agent with dangerous tools
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Tester",
|
||||
goal="Test approval integration",
|
||||
tools=[ShellTools()],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print("✅ Agent created with dangerous tools")
|
||||
|
||||
# Check that agent has the approval callback configured
|
||||
from praisonaiagents.main import approval_callback
|
||||
print(f"✅ Global approval callback configured: {approval_callback is not None}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Agent integration test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all approval system tests."""
|
||||
print("🧪 PraisonAI Human Approval System Test Suite")
|
||||
print("=" * 50)
|
||||
|
||||
test_results = []
|
||||
|
||||
# Run synchronous tests
|
||||
test_results.append(("Imports", test_imports()))
|
||||
test_results.append(("Configuration", test_approval_configuration()))
|
||||
test_results.append(("Decorator", test_approval_decorator()))
|
||||
test_results.append(("Tool Integration", test_tool_integration()))
|
||||
test_results.append(("Agent Integration", test_agent_integration()))
|
||||
|
||||
# Run async tests
|
||||
try:
|
||||
async_result = asyncio.run(test_approval_callback())
|
||||
test_results.append(("Approval Callback", async_result))
|
||||
except Exception as e:
|
||||
print(f"❌ Async test failed: {e}")
|
||||
test_results.append(("Approval Callback", False))
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 TEST SUMMARY")
|
||||
print("=" * 50)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_name, result in test_results:
|
||||
status = "✅ PASSED" if result else "❌ FAILED"
|
||||
print(f"{test_name:<20} {status}")
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\nTotal: {passed + failed} tests")
|
||||
print(f"Passed: {passed}")
|
||||
print(f"Failed: {failed}")
|
||||
|
||||
if failed == 0:
|
||||
print("\n🎉 All tests passed! Human approval system is working correctly.")
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ {failed} test(s) failed. Please check the implementation.")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive test for the human approval system.
|
||||
|
||||
This test actually calls dangerous tools to trigger approval prompts,
|
||||
allowing you to test the human-in-the-loop functionality.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
# Add the praisonai-agents module to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'praisonai-agents'))
|
||||
|
||||
@pytest.mark.skipif(os.getenv("ASK_USER") != "1", reason="interactive approval requires user input")
|
||||
def test_shell_command_approval():
|
||||
"""Test shell command execution with approval prompts."""
|
||||
print("\n🐚 Testing Shell Command Approval")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, console_approval_callback, ApprovalDecision
|
||||
|
||||
# Use auto-approval when running non-interactive
|
||||
if os.getenv("ASK_USER") == "1":
|
||||
set_approval_callback(console_approval_callback)
|
||||
else:
|
||||
# Auto-approve for CI
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for CI")
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
shell_tools = ShellTools()
|
||||
|
||||
print("About to execute a shell command that requires approval...")
|
||||
print("You should see an approval prompt.")
|
||||
|
||||
# This should trigger an approval prompt
|
||||
result = shell_tools.execute_command("echo 'Hello from approved shell command!'")
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Command executed successfully: {result['stdout']}")
|
||||
else:
|
||||
print(f"❌ Command failed or was denied: {result.get('stderr', 'Unknown error')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Shell command test failed: {e}")
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(os.getenv("ASK_USER") != "1", reason="interactive approval requires user input")
|
||||
def test_python_code_approval():
|
||||
"""Test Python code execution with approval prompts."""
|
||||
print("\n🐍 Testing Python Code Approval")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.python_tools import PythonTools
|
||||
from praisonaiagents.approval import set_approval_callback, console_approval_callback, ApprovalDecision
|
||||
|
||||
# Use auto-approval when running non-interactive
|
||||
if os.getenv("ASK_USER") == "1":
|
||||
set_approval_callback(console_approval_callback)
|
||||
else:
|
||||
# Auto-approve for CI
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for CI")
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
python_tools = PythonTools()
|
||||
|
||||
print("About to execute Python code that requires approval...")
|
||||
print("You should see an approval prompt.")
|
||||
|
||||
# This should trigger an approval prompt
|
||||
code = """
|
||||
print("Hello from approved Python code!")
|
||||
result = 2 + 2
|
||||
print(f"2 + 2 = {result}")
|
||||
"""
|
||||
|
||||
result = python_tools.execute_code(code)
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Code executed successfully: {result['output']}")
|
||||
else:
|
||||
print(f"❌ Code failed or was denied: {result.get('error', 'Unknown error')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Python code test failed: {e}")
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(os.getenv("ASK_USER") != "1", reason="interactive approval requires user input")
|
||||
def test_file_operation_approval():
|
||||
"""Test file operations with approval prompts."""
|
||||
print("\n📁 Testing File Operation Approval")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.file_tools import FileTools
|
||||
from praisonaiagents.approval import set_approval_callback, console_approval_callback, ApprovalDecision
|
||||
|
||||
# Use auto-approval when running non-interactive
|
||||
if os.getenv("ASK_USER") == "1":
|
||||
set_approval_callback(console_approval_callback)
|
||||
else:
|
||||
# Auto-approve for CI
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for CI")
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
file_tools = FileTools()
|
||||
|
||||
print("About to create a file that requires approval...")
|
||||
print("You should see an approval prompt.")
|
||||
|
||||
# This should trigger an approval prompt
|
||||
result = file_tools.write_file(
|
||||
file_path="test_approval_file.txt",
|
||||
content="This file was created with human approval!"
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ File created successfully: {result['message']}")
|
||||
|
||||
# Now test deletion (also requires approval)
|
||||
print("\nAbout to delete the file (also requires approval)...")
|
||||
delete_result = file_tools.delete_file("test_approval_file.txt")
|
||||
|
||||
if delete_result.get('success'):
|
||||
print(f"✅ File deleted successfully: {delete_result['message']}")
|
||||
else:
|
||||
print(f"❌ File deletion failed or was denied: {delete_result.get('error', 'Unknown error')}")
|
||||
else:
|
||||
print(f"❌ File creation failed or was denied: {result.get('error', 'Unknown error')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ File operation test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_auto_approval_callback():
|
||||
"""Test with an auto-approval callback for non-interactive testing."""
|
||||
print("\n🤖 Testing Auto-Approval Callback")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create an auto-approval callback
|
||||
def auto_approve_callback(function_name, arguments, risk_level):
|
||||
print(f"🤖 Auto-approving {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved for testing")
|
||||
|
||||
set_approval_callback(auto_approve_callback)
|
||||
|
||||
shell_tools = ShellTools()
|
||||
|
||||
print("Executing command with auto-approval...")
|
||||
result = shell_tools.execute_command("echo 'Auto-approved command executed!'")
|
||||
|
||||
if result.get('success'):
|
||||
print(f"✅ Auto-approved command executed: {result['stdout']}")
|
||||
else:
|
||||
print(f"❌ Auto-approved command failed: {result.get('stderr', 'Unknown error')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Auto-approval test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_auto_denial_callback():
|
||||
"""Test with an auto-denial callback."""
|
||||
print("\n🚫 Testing Auto-Denial Callback")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Create an auto-denial callback
|
||||
def auto_deny_callback(function_name, arguments, risk_level):
|
||||
print(f"🚫 Auto-denying {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=False, reason="Auto-denied for testing")
|
||||
|
||||
set_approval_callback(auto_deny_callback)
|
||||
|
||||
shell_tools = ShellTools()
|
||||
|
||||
print("Executing command with auto-denial...")
|
||||
result = shell_tools.execute_command("echo 'This should be denied'")
|
||||
|
||||
if result.get('approval_denied'):
|
||||
print("✅ Command was correctly denied by approval system")
|
||||
elif result.get('success'):
|
||||
print("❌ Command executed when it should have been denied")
|
||||
else:
|
||||
print(f"⚠️ Command failed for other reasons: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Auto-denial test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run interactive approval tests."""
|
||||
print("🧪 PraisonAI Human Approval System - Interactive Tests")
|
||||
print("=" * 60)
|
||||
print("This test will demonstrate the human approval system in action.")
|
||||
print("You will be prompted to approve or deny dangerous operations.")
|
||||
print()
|
||||
|
||||
# Ask user which tests to run
|
||||
print("Available tests:")
|
||||
print("1. Shell Command Approval (interactive)")
|
||||
print("2. Python Code Approval (interactive)")
|
||||
print("3. File Operation Approval (interactive)")
|
||||
print("4. Auto-Approval Test (non-interactive)")
|
||||
print("5. Auto-Denial Test (non-interactive)")
|
||||
print("6. Run all tests")
|
||||
print()
|
||||
|
||||
try:
|
||||
choice = input("Enter your choice (1-6): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
test_shell_command_approval()
|
||||
elif choice == "2":
|
||||
test_python_code_approval()
|
||||
elif choice == "3":
|
||||
test_file_operation_approval()
|
||||
elif choice == "4":
|
||||
test_auto_approval_callback()
|
||||
elif choice == "5":
|
||||
test_auto_denial_callback()
|
||||
elif choice == "6":
|
||||
print("\n🚀 Running all tests...")
|
||||
|
||||
# Run non-interactive tests first
|
||||
print("\n" + "=" * 60)
|
||||
print("PART 1: NON-INTERACTIVE TESTS")
|
||||
print("=" * 60)
|
||||
test_auto_approval_callback()
|
||||
test_auto_denial_callback()
|
||||
|
||||
# Ask if user wants to run interactive tests
|
||||
print("\n" + "=" * 60)
|
||||
print("PART 2: INTERACTIVE TESTS")
|
||||
print("=" * 60)
|
||||
print("The following tests require human interaction.")
|
||||
run_interactive = input("Run interactive tests? (y/n): ").strip().lower()
|
||||
|
||||
if run_interactive.startswith('y'):
|
||||
test_shell_command_approval()
|
||||
test_python_code_approval()
|
||||
test_file_operation_approval()
|
||||
else:
|
||||
print("Skipping interactive tests.")
|
||||
else:
|
||||
print("Invalid choice. Exiting.")
|
||||
return
|
||||
|
||||
print("\n🎉 Test completed!")
|
||||
print("\nKey observations:")
|
||||
print("- Dangerous operations should trigger approval prompts")
|
||||
print("- Risk levels should be displayed correctly")
|
||||
print("- Approved operations should execute normally")
|
||||
print("- Denied operations should be blocked")
|
||||
print("- Auto-approval/denial callbacks should work as expected")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n❌ Test cancelled by user.")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed with error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify that the require_approval decorator now enforces approval
|
||||
even when tools are called directly (not through agent.execute_tool).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the praisonai-agents module to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
|
||||
|
||||
def test_decorator_enforcement():
|
||||
"""Test that the decorator actually enforces approval."""
|
||||
print("🧪 Testing Decorator Enforcement")
|
||||
print("=" * 35)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
# Set auto-denial to test enforcement
|
||||
def auto_deny(function_name, arguments, risk_level):
|
||||
print(f"🚫 Denying {function_name} (risk: {risk_level})")
|
||||
return ApprovalDecision(approved=False, reason='Test denial')
|
||||
|
||||
set_approval_callback(auto_deny)
|
||||
|
||||
shell_tools = ShellTools()
|
||||
|
||||
print("Attempting to execute command directly (should be blocked)...")
|
||||
|
||||
try:
|
||||
# This should now be blocked by the decorator
|
||||
result = shell_tools.execute_command('echo "This should be denied"')
|
||||
print('❌ Command executed when it should have been denied!')
|
||||
return False
|
||||
except PermissionError as e:
|
||||
print(f'✅ Decorator enforcement working: {e}')
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'❌ Unexpected error: {e}')
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test setup failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_decorator_enforcement()
|
||||
if success:
|
||||
print("\n🎉 Decorator enforcement is working correctly!")
|
||||
else:
|
||||
print("\n❌ Decorator enforcement test failed!")
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to verify the improved require_approval decorator.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the praisonai-agents module to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
|
||||
|
||||
def test_improved_decorator():
|
||||
"""Test the improved decorator with context management."""
|
||||
print("🧪 Testing Improved Decorator with Context Management")
|
||||
print("=" * 55)
|
||||
|
||||
try:
|
||||
from praisonaiagents.approval import (
|
||||
require_approval, set_approval_callback, ApprovalDecision,
|
||||
mark_approved, is_already_approved, clear_approval_context
|
||||
)
|
||||
|
||||
# Clear any previous context
|
||||
clear_approval_context()
|
||||
|
||||
# Create a test function
|
||||
@require_approval(risk_level="critical")
|
||||
def test_function(message="test"):
|
||||
return f"Function executed: {message}"
|
||||
|
||||
print("✅ Test function decorated successfully")
|
||||
|
||||
# Test 1: Direct call without approval (should fail)
|
||||
print("\n1. Testing direct call without approval (should fail)...")
|
||||
|
||||
def auto_deny(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=False, reason='Test denial')
|
||||
|
||||
set_approval_callback(auto_deny)
|
||||
|
||||
try:
|
||||
result = test_function("direct call")
|
||||
print(f"❌ Function executed when it should have been denied: {result}")
|
||||
except PermissionError as e:
|
||||
print(f"✅ Correctly denied: {e}")
|
||||
|
||||
# Test 2: Mark as approved and call (should succeed)
|
||||
print("\n2. Testing with approval context (should succeed)...")
|
||||
|
||||
mark_approved("test_function")
|
||||
|
||||
try:
|
||||
result = test_function("approved context")
|
||||
print(f"✅ Function executed with approved context: {result}")
|
||||
except Exception as e:
|
||||
print(f"❌ Function failed in approved context: {e}")
|
||||
|
||||
# Test 3: Clear context and test auto-approval
|
||||
print("\n3. Testing auto-approval callback...")
|
||||
|
||||
clear_approval_context()
|
||||
|
||||
def auto_approve(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason='Test approval')
|
||||
|
||||
set_approval_callback(auto_approve)
|
||||
|
||||
try:
|
||||
result = test_function("auto approved")
|
||||
print(f"✅ Function executed with auto-approval: {result}")
|
||||
except Exception as e:
|
||||
print(f"❌ Function failed with auto-approval: {e}")
|
||||
|
||||
# Test 4: Verify context is working
|
||||
print("\n4. Testing context persistence...")
|
||||
|
||||
if is_already_approved("test_function"):
|
||||
print("✅ Context correctly shows function as approved")
|
||||
else:
|
||||
print("❌ Context not working correctly")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_improved_decorator()
|
||||
if success:
|
||||
print("\n🎉 Improved decorator approach is working correctly!")
|
||||
print("\nKey improvements:")
|
||||
print("- ✅ Context management prevents double approval")
|
||||
print("- ✅ Proper async handling")
|
||||
print("- ✅ Decorator actually enforces approval")
|
||||
print("- ✅ Agent integration marks tools as approved")
|
||||
else:
|
||||
print("\n❌ Improved decorator test failed!")
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick test script to verify graph memory implementation
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the source directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
|
||||
|
||||
def test_memory_import():
|
||||
"""Test that Memory class can be imported and initialized"""
|
||||
try:
|
||||
from praisonaiagents.memory import Memory
|
||||
print("✅ Memory class imported successfully")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f"❌ Failed to import Memory: {e}")
|
||||
return False
|
||||
|
||||
def test_knowledge_import():
|
||||
"""Test that Knowledge class can be imported"""
|
||||
try:
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
print("✅ Knowledge class imported successfully")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f"❌ Failed to import Knowledge: {e}")
|
||||
return False
|
||||
|
||||
def test_memory_config():
|
||||
"""Test memory configuration with graph support"""
|
||||
try:
|
||||
from praisonaiagents.memory import Memory
|
||||
|
||||
# Test basic configuration
|
||||
basic_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {"path": ".test_memory"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memory = Memory(config=basic_config, verbose=1)
|
||||
print("✅ Basic memory configuration works")
|
||||
|
||||
# Test graph configuration (will fallback gracefully if dependencies missing)
|
||||
graph_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"graph_store": {
|
||||
"provider": "memgraph",
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
memory_graph = Memory(config=graph_config, verbose=1)
|
||||
print("✅ Graph memory configuration initialized")
|
||||
print(f" Graph enabled: {getattr(memory_graph, 'graph_enabled', False)}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Graph memory not available (expected): {e}")
|
||||
print(" This is normal if graph dependencies are not installed")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Memory configuration test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_knowledge_config():
|
||||
"""Test knowledge configuration with graph support"""
|
||||
try:
|
||||
from praisonaiagents.knowledge import Knowledge
|
||||
|
||||
# Test basic knowledge config
|
||||
basic_config = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "test_collection",
|
||||
"path": ".test_knowledge"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
knowledge = Knowledge(config=basic_config, verbose=0)
|
||||
print("✅ Basic knowledge configuration works")
|
||||
|
||||
# Test knowledge with graph config
|
||||
graph_config = {
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
"collection_name": "test_graph_collection",
|
||||
"path": ".test_graph_knowledge"
|
||||
}
|
||||
},
|
||||
"graph_store": {
|
||||
"provider": "memgraph",
|
||||
"config": {
|
||||
"url": "bolt://localhost:7687",
|
||||
"username": "memgraph",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
knowledge_graph = Knowledge(config=graph_config, verbose=0)
|
||||
print("✅ Graph knowledge configuration created")
|
||||
|
||||
# Check if config contains graph_store
|
||||
final_config = knowledge_graph.config
|
||||
if "graph_store" in final_config:
|
||||
print("✅ Graph store configuration preserved in knowledge config")
|
||||
else:
|
||||
print("❌ Graph store configuration not found in knowledge config")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Knowledge configuration test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("🧪 Testing Graph Memory Implementation")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
("Memory Import", test_memory_import),
|
||||
("Knowledge Import", test_knowledge_import),
|
||||
("Memory Configuration", test_memory_config),
|
||||
("Knowledge Configuration", test_knowledge_config)
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test_name, test_func in tests:
|
||||
print(f"\n🔬 Testing {test_name}...")
|
||||
try:
|
||||
if test_func():
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Test {test_name} crashed: {e}")
|
||||
|
||||
print(f"\n📊 Test Results: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed! Graph memory implementation is working correctly.")
|
||||
else:
|
||||
print("⚠️ Some tests failed. This might be expected if dependencies are missing.")
|
||||
|
||||
print("\n💡 To fully test graph memory, install dependencies:")
|
||||
print(" pip install \"mem0ai[graph]\"")
|
||||
print(" docker run -p 7687:7687 memgraph/memgraph-mage:latest")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Ollama tool-call fixes
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the source directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'praisonai-agents'))
|
||||
|
||||
from praisonaiagents.llm.llm import LLM
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
def test_ollama_provider_detection():
|
||||
"""Test the new Ollama provider detection logic"""
|
||||
print("Testing Ollama provider detection...")
|
||||
|
||||
# Test 1: Direct ollama/ prefix
|
||||
llm1 = LLM(model="ollama/qwen3:32b")
|
||||
assert llm1._is_ollama_provider() == True, "Should detect ollama/ prefix"
|
||||
print("✓ Direct ollama/ prefix detection works")
|
||||
|
||||
# Test 2: Environment variable detection
|
||||
os.environ["OPENAI_BASE_URL"] = "http://localhost:11434/v1"
|
||||
llm2 = LLM(model="qwen3:32b")
|
||||
assert llm2._is_ollama_provider() == True, "Should detect via env var"
|
||||
print("✓ Environment variable detection works")
|
||||
|
||||
# Test 3: Non-Ollama model
|
||||
os.environ.pop("OPENAI_BASE_URL", None)
|
||||
llm3 = LLM(model="gpt-4o-mini")
|
||||
assert llm3._is_ollama_provider() == False, "Should not detect non-Ollama"
|
||||
print("✓ Non-Ollama detection works")
|
||||
|
||||
def test_tool_call_parsing():
|
||||
"""Test the new tool call parsing logic"""
|
||||
print("\nTesting tool call parsing...")
|
||||
|
||||
llm = LLM(model="ollama/qwen3:32b")
|
||||
|
||||
# Test 1: Standard format
|
||||
tool_call_std = {
|
||||
"id": "call_123",
|
||||
"function": {
|
||||
"name": "hello_world",
|
||||
"arguments": '{"name": "test"}'
|
||||
}
|
||||
}
|
||||
name, args, id = llm._parse_tool_call_arguments(tool_call_std, is_ollama=True)
|
||||
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
|
||||
assert args == {"name": "test"}, f"Expected {{'name': 'test'}}, got {args}"
|
||||
print("✓ Standard format parsing works")
|
||||
|
||||
# Test 2: Ollama alternative format
|
||||
tool_call_alt = {
|
||||
"name": "hello_world",
|
||||
"arguments": '{"name": "test"}'
|
||||
}
|
||||
name, args, id = llm._parse_tool_call_arguments(tool_call_alt, is_ollama=True)
|
||||
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
|
||||
assert args == {"name": "test"}, f"Expected {{'name': 'test'}}, got {args}"
|
||||
print("✓ Alternative format parsing works")
|
||||
|
||||
# Test 3: Error handling - malformed JSON
|
||||
tool_call_bad = {
|
||||
"function": {
|
||||
"name": "hello_world",
|
||||
"arguments": 'invalid json'
|
||||
}
|
||||
}
|
||||
name, args, id = llm._parse_tool_call_arguments(tool_call_bad, is_ollama=False)
|
||||
assert name == "unknown_function", f"Expected 'unknown_function' for malformed JSON, got '{name}'"
|
||||
assert args == {}, f"Expected empty dict, got {args}"
|
||||
print("✓ Error handling works")
|
||||
|
||||
def test_agent_tool_parameter_logic():
|
||||
"""Test the fixed tool parameter logic in Agent"""
|
||||
print("\nTesting agent tool parameter logic...")
|
||||
|
||||
def dummy_tool():
|
||||
"""Dummy tool for testing"""
|
||||
return "test"
|
||||
|
||||
# Test 1: None tools should use agent tools
|
||||
agent = Agent(name="test", tools=[dummy_tool])
|
||||
# Simulate the fixed logic
|
||||
tools = None
|
||||
if tools is None or (isinstance(tools, list) and len(tools) == 0):
|
||||
tool_param = agent.tools
|
||||
else:
|
||||
tool_param = tools
|
||||
|
||||
assert tool_param == [dummy_tool], "Should use agent tools when tools=None"
|
||||
print("✓ None tools handling works")
|
||||
|
||||
# Test 2: Empty list should use agent tools
|
||||
tools = []
|
||||
if tools is None or (isinstance(tools, list) and len(tools) == 0):
|
||||
tool_param = agent.tools
|
||||
else:
|
||||
tool_param = tools
|
||||
|
||||
assert tool_param == [dummy_tool], "Should use agent tools when tools=[]"
|
||||
print("✓ Empty list handling works")
|
||||
|
||||
# Test 3: Non-empty list should use provided tools
|
||||
custom_tools = [lambda: "custom"]
|
||||
tools = custom_tools
|
||||
if tools is None or (isinstance(tools, list) and len(tools) == 0):
|
||||
tool_param = agent.tools
|
||||
else:
|
||||
tool_param = tools
|
||||
|
||||
assert tool_param == custom_tools, "Should use provided tools when not empty"
|
||||
print("✓ Non-empty list handling works")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running Ollama tool-call fix tests...\n")
|
||||
|
||||
try:
|
||||
test_ollama_provider_detection()
|
||||
test_tool_call_parsing()
|
||||
test_agent_tool_parameter_logic()
|
||||
|
||||
print("\n🎉 All tests passed! The Ollama tool-call fixes are working correctly.")
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the deployment scheduler functionality.
|
||||
Tests the core components without actually deploying.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
# Add the src directory to the path so we can import modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai'))
|
||||
|
||||
def test_schedule_parser():
|
||||
"""Test the schedule parser functionality."""
|
||||
try:
|
||||
from praisonai.scheduler import ScheduleParser
|
||||
|
||||
# Test various schedule expressions
|
||||
test_cases = [
|
||||
("daily", 86400),
|
||||
("hourly", 3600),
|
||||
("*/30m", 1800),
|
||||
("*/6h", 21600),
|
||||
("60", 60),
|
||||
("3600", 3600)
|
||||
]
|
||||
|
||||
print("Testing ScheduleParser...")
|
||||
for expr, expected in test_cases:
|
||||
result = ScheduleParser.parse(expr)
|
||||
assert result == expected, f"Expected {expected}, got {result} for '{expr}'"
|
||||
print(f" ✓ '{expr}' -> {result} seconds")
|
||||
|
||||
print("ScheduleParser tests passed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"ScheduleParser test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_scheduler_creation():
|
||||
"""Test scheduler creation and basic functionality."""
|
||||
try:
|
||||
from praisonai.scheduler import create_scheduler, DeploymentScheduler
|
||||
|
||||
print("Testing scheduler creation...")
|
||||
|
||||
# Test default scheduler
|
||||
scheduler = create_scheduler()
|
||||
assert isinstance(scheduler, DeploymentScheduler)
|
||||
print(" ✓ Default scheduler created")
|
||||
|
||||
# Test with different providers
|
||||
for provider in ["gcp", "aws", "azure"]:
|
||||
scheduler = create_scheduler(provider=provider)
|
||||
assert isinstance(scheduler, DeploymentScheduler)
|
||||
print(f" ✓ {provider} scheduler created")
|
||||
|
||||
print("Scheduler creation tests passed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Scheduler creation test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_config_file_parsing():
|
||||
"""Test configuration file parsing."""
|
||||
try:
|
||||
# Create a temporary config file
|
||||
config_data = {
|
||||
'deployment': {
|
||||
'schedule': 'daily',
|
||||
'provider': 'gcp',
|
||||
'max_retries': 5
|
||||
},
|
||||
'environment': {
|
||||
'TEST_VAR': 'test_value'
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
yaml.dump(config_data, f)
|
||||
config_file = f.name
|
||||
|
||||
print("Testing config file parsing...")
|
||||
|
||||
# Test loading the config
|
||||
with open(config_file, 'r') as f:
|
||||
loaded_config = yaml.safe_load(f)
|
||||
|
||||
assert loaded_config['deployment']['schedule'] == 'daily'
|
||||
assert loaded_config['deployment']['provider'] == 'gcp'
|
||||
assert loaded_config['deployment']['max_retries'] == 5
|
||||
print(" ✓ Config file parsed correctly")
|
||||
|
||||
# Clean up
|
||||
os.unlink(config_file)
|
||||
|
||||
print("Config file parsing tests passed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Config file parsing test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_cli_argument_parsing():
|
||||
"""Test CLI argument parsing for scheduling options."""
|
||||
try:
|
||||
from praisonai.cli import PraisonAI
|
||||
|
||||
print("Testing CLI argument parsing...")
|
||||
|
||||
# Test basic CLI instantiation
|
||||
praison = PraisonAI()
|
||||
assert praison is not None
|
||||
print(" ✓ PraisonAI CLI instance created")
|
||||
|
||||
print("CLI argument parsing tests passed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"CLI argument parsing test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Running PraisonAI Scheduler Tests")
|
||||
print("=" * 40)
|
||||
|
||||
tests = [
|
||||
test_schedule_parser,
|
||||
test_scheduler_creation,
|
||||
test_config_file_parsing,
|
||||
test_cli_argument_parsing
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test in tests:
|
||||
print()
|
||||
if test():
|
||||
passed += 1
|
||||
else:
|
||||
print("❌ Test failed")
|
||||
|
||||
print()
|
||||
print("=" * 40)
|
||||
print(f"Tests passed: {passed}/{total}")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed!")
|
||||
return 0
|
||||
else:
|
||||
print("❌ Some tests failed")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Generated
+5
-5
@@ -3931,7 +3931,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.20"
|
||||
version = "2.2.25"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "instructor" },
|
||||
@@ -4073,7 +4073,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.91" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.96" },
|
||||
{ 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" },
|
||||
@@ -4130,7 +4130,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.91"
|
||||
version = "0.0.96"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
@@ -4138,9 +4138,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/63/a26fe7ae31191ed5998fabe6cb13f779728caf3123ac8c8e8f4707a99e58/praisonaiagents-0.0.91.tar.gz", hash = "sha256:ec0409591f9278b766c4c6b3876ed142cfc217b1545c1b9809494f8a826b55fa", size = 125710 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/53/2e1facaed976a6c428b67756108d2370b5ca9249356c7c71b7d6e1a28421/praisonaiagents-0.0.96.tar.gz", hash = "sha256:96ff46fbdb54162cf721c94fb04e95eae3f0d9305fd1939bad31fa1aca7b4231", size = 127366 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/2b/2e2d29ceb21a8e3af15beaf60955b85a9373ee6164d2d76f649a3e533786/praisonaiagents-0.0.91-py3-none-any.whl", hash = "sha256:aaaa51370a81aae29500168678d4b7208845890485f0a13da5367f1210dfaa3c", size = 143898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d1/89935abb42bd1c1cda1981583e4b6a2058b5c79e157a062489f581dba9b2/praisonaiagents-0.0.96-py3-none-any.whl", hash = "sha256:6d70884a7033e124510c13c80eb1e19dbad6c04d58eb8f0cfceb30fb9c91f85f", size = 145812 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user