Compare commits

...
16 Commits
Author SHA1 Message Date
Mervin Praison a3bfc6f795 Merge pull request #563 from MervinPraison/claude/issue-459-20250531_155633
fix: resolve MCP tools not working in Streamlit applications (issue #459)
2025-05-31 17:05:57 +01:00
Mervin Praison ebc130854d Merge pull request #562 from MervinPraison/claude/issue-5-20250531_154718
feat: add deployment scheduling functionality
2025-05-31 17:04:49 +01:00
Mervin Praison f40a5c1359 Rename schedule_config.yaml.example to src/praisonai/schedule_config.yaml.example 2025-05-31 17:04:06 +01:00
Mervin Praison 3284f7bbc6 Rename test_scheduler.py to src/praisonai/tests/unit/test_scheduler.py 2025-05-31 17:03:34 +01:00
claude[bot]andMervinPraison 87928611f7 fix: resolve MCP tools not working in Streamlit applications (issue #459)
This commit provides working examples and comprehensive documentation for using MCP tools with Streamlit, without modifying core MCP implementation.

Key fixes:
- Proper session state management to prevent agent re-initialization
- Use standard LLM format instead of provider/model format
- Comprehensive error handling and user feedback
- Working examples with troubleshooting guidance

Files added:
- examples/python/ui/mcp-streamlit-airbnb.py - Complete working Streamlit app
- examples/python/ui/mcp-streamlit-simple.py - Simple fixed version of original code
- docs/ui/streamlit/mcp-streamlit.mdx - Comprehensive documentation and troubleshooting

The root issue was improper usage patterns in Streamlit, not the core MCP implementation.

Co-authored-by: MervinPraison <[email protected]>

🤖 Generated with [Claude Code](https://claude.ai/code)
2025-05-31 15:59:21 +00:00
Mervin Praison 2727bf5992 Merge pull request #561 from MervinPraison/claude/issue-170-20250531_154607
fix: resolve crawling NoneType concatenation error in realtime tools
2025-05-31 16:58:03 +01:00
claude[bot]andMervinPraison 45d04163c3 feat: add deployment scheduling functionality
- Add DeploymentScheduler with provider-agnostic design
- Support interval-based scheduling (daily, hourly, */6h, 3600s)
- CLI integration with --schedule and --schedule-config options
- Backward compatible with existing --deploy behavior
- Thread-safe scheduler with retry mechanism
- Example configuration file for scheduled deployments
- Test suite for validation

Addresses issue #5: --deploy schedule runs

Co-authored-by: MervinPraison <[email protected]>
2025-05-31 15:53:57 +00:00
claude[bot]andMervinPraison 477d0d2e26 fix: resolve crawling NoneType concatenation error in realtime tools
- Fix "can only concatenate str (not "NoneType") to str" error in web crawling
- Update process_tavily_results and fallback_to_duckduckgo to use proper async patterns
- Add null checks for crawl_result.markdown to prevent NoneType errors
- Use async context manager with AsyncWebCrawler and await crawler.arun()
- Match the working async pattern used in chat.py

Fixes #170

Co-authored-by: MervinPraison <[email protected]>
2025-05-31 15:48:02 +00:00
Mervin Praison 77394f5e5f Merge pull request #560 from MervinPraison/develop
Develop
2025-05-31 16:44:03 +01:00
MervinPraison 29e533c18c chore: remove unsloth training dependency from pyproject.toml
- Cleared the train dependencies list by removing unsloth[colab-new]>=2024.11.7.
- Commented out the unsloth dependency to maintain a record without affecting functionality.
2025-05-31 16:43:46 +01:00
MervinPraison 48dbea0566 Merge branch 'main' of https://github.com/MervinPraison/PraisonAI into develop 2025-05-31 12:42:45 +01:00
Mervin Praison 969d6697e1 Merge pull request #559 from MervinPraison/claude/issue-232-20250531_060542
fix: add dependency check for train command to prevent confusing errors
2025-05-31 12:42:22 +01:00
Mervin Praisonandcoderabbitai[bot] 787d2508b1 Update src/praisonai/pyproject.toml
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-31 12:41:18 +01:00
claude[bot]andMervinPraison ec42dc92e4 feat: add unsloth training dependencies to pyproject.toml
- Add train optional dependencies with unsloth[colab-new]>=2024.11.7
- Update Poetry extras to include unsloth for train command
- Enables pip install "praisonai[train]" for training dependencies
- Maintains backward compatibility

Co-authored-by: MervinPraison <[email protected]>
2025-05-31 10:45:40 +00:00
claude[bot]andMervinPraison 5b7bae2f43 fix: add dependency check for train command to prevent confusing errors
- Add TRAIN_AVAILABLE flag to track unsloth dependency availability
- Add dependency validation in both parse_args() and main() methods
- Show clear error message with installation instructions when dependencies missing
- Follow same pattern as other commands (chat, code, call, realtime) for consistency
- Fixes issue where train command would show "train not available" without helpful context

Resolves #232

Co-authored-by: MervinPraison <[email protected]>
2025-05-31 06:07:31 +00:00
MervinPraison 69a1efac47 fix(tests): update expected name in tool call parsing test for malformed JSON
- Changed the expected function name in the test for malformed JSON from 'hello_world' to 'unknown_function' to reflect correct error handling behaviour.
- Adjusted the Python path in the test file to point to the correct source directory for the praisonai-agents module.
2025-05-31 06:51:09 +01:00
10 changed files with 1098 additions and 52 deletions

No files matched your search

+253
View File
@@ -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.
+252
View File
@@ -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`
""")
+82 -3
View File
@@ -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')
@@ -416,6 +486,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")
@@ -509,6 +583,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')
+215
View File
@@ -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
@@ -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 = AsyncWebCrawler()
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 = AsyncWebCrawler()
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:
+3 -1
View File
@@ -89,6 +89,7 @@ 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"]
@@ -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"
+2 -2
View File
@@ -6,7 +6,7 @@ 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'))
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
@@ -69,7 +69,7 @@ def test_tool_call_parsing():
}
}
name, args, id = llm._parse_tool_call_arguments(tool_call_bad, is_ollama=False)
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
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")
+161
View File
@@ -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())