mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 17:29:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa81094ac3 | ||
|
|
c74fd7e925 | ||
|
|
249acf7965 | ||
|
|
ab68b2d296 | ||
|
|
abcc5b3b04 | ||
|
|
e584f56099 | ||
|
|
b6b01af066 | ||
|
|
9aae3eff86 | ||
|
|
f90582c66b | ||
|
|
67a8d65d8e | ||
|
|
d60c31a86d | ||
|
|
fcb7b2a0d0 | ||
|
|
81ad7db679 | ||
|
|
0482c3ebb0 | ||
|
|
3107458016 | ||
|
|
44c8ceebae | ||
|
|
20c0b02528 | ||
|
|
cfc886b591 | ||
|
|
77ac4b7a09 | ||
|
|
5897cedc45 | ||
|
|
a4d1079813 | ||
|
|
4eed985c4d | ||
|
|
0658491072 | ||
|
|
9f82368b4c | ||
|
|
c267cdda87 | ||
|
|
b88bb04090 | ||
|
|
709a35fbdc | ||
|
|
8b792424b6 | ||
|
|
ef2a8cd658 | ||
|
|
d41d2dcba2 | ||
|
|
3bc1786b49 | ||
|
|
2eec3e2359 | ||
|
|
6168401a69 | ||
|
|
473fda653d | ||
|
|
30f1a440b6 | ||
|
|
f991f37cfe | ||
|
|
a43b3ac72c | ||
|
|
06f82249e1 | ||
|
|
d3aa78241a | ||
|
|
dfb77c39f9 | ||
|
|
7e8f9b0574 | ||
|
|
19bcdcd000 | ||
|
|
8f5160f827 | ||
|
|
3574a4d2c0 | ||
|
|
b552843fa9 | ||
|
|
0461a133df | ||
|
|
b33857de33 | ||
|
|
433c643f6c | ||
|
|
214119a063 |
No files matched your search
@@ -1,45 +1,54 @@
|
||||
name: Daily Publish Docker
|
||||
name: Publish Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag (default: latest)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
jobs:
|
||||
check-and-build:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check last commit time
|
||||
id: check_commit
|
||||
run: |
|
||||
last_commit_time=$(git log -1 --format=%ct)
|
||||
current_time=$(date +%s)
|
||||
time_diff=$((current_time - last_commit_time))
|
||||
if [ $time_diff -ge 120 ] && [ $time_diff -lt 240 ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the GitHub Container Registry
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/mervinpraison/praisonai
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' }}
|
||||
type=raw,value=${{ github.event.inputs.tag || 'latest' }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/mervinpraison/praisonai:latest
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
+2
-1
@@ -14,9 +14,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN mkdir -p /root/.praison
|
||||
|
||||
# Install Python packages (using latest versions)
|
||||
# Note: praisonai is installed from PyPI - remove version pin to use latest available
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
"praisonai>=2.3.14" \
|
||||
praisonai \
|
||||
"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.3.14" \
|
||||
"praisonai>=2.3.34" \
|
||||
"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.3.14" \
|
||||
"praisonai>=2.3.34" \
|
||||
"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.3.14" \
|
||||
"praisonai>=2.3.34" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
@@ -71,7 +71,6 @@
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"print(\"📦 Dependencies installed!\")\n",
|
||||
"print(\"🔄 Converting from AGNO → PraisonAI\")\n",
|
||||
"print(\"🎯 Building Job Hunting Agent System\")"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
framework: crewai
|
||||
topic: "Technology Research and Content Creation using AWS Bedrock"
|
||||
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
research_specialist:
|
||||
role: "Research Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are an experienced technology researcher with deep expertise in analyzing
|
||||
emerging technologies and market trends. You excel at gathering comprehensive
|
||||
information from various sources and synthesizing it into actionable insights.
|
||||
@@ -35,7 +35,7 @@ roles:
|
||||
|
||||
content_creator:
|
||||
role: "Technical Content Creator"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a skilled technical writer and content creator who specializes in
|
||||
translating complex technology concepts into engaging, accessible content
|
||||
for various audiences including developers, business leaders, and end users.
|
||||
@@ -64,7 +64,7 @@ roles:
|
||||
|
||||
quality_reviewer:
|
||||
role: "Content Quality Reviewer"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a meticulous editor and quality assurance specialist with extensive
|
||||
experience in technical content review. You ensure accuracy, clarity, and
|
||||
consistency while maintaining the intended tone and message.
|
||||
|
||||
@@ -8,10 +8,10 @@ variables:
|
||||
industry: "Electric Vehicles"
|
||||
research_focus: "comprehensive market analysis"
|
||||
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
market_overview_specialist:
|
||||
role: "Market Overview Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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,
|
||||
@@ -64,7 +64,7 @@ roles:
|
||||
|
||||
competitive_intelligence_analyst:
|
||||
role: "Competitive Intelligence Analyst"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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,
|
||||
@@ -119,7 +119,7 @@ roles:
|
||||
|
||||
financial_performance_analyst:
|
||||
role: "Financial Performance Analyst"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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
|
||||
@@ -173,7 +173,7 @@ roles:
|
||||
|
||||
growth_opportunities_researcher:
|
||||
role: "Growth Opportunities Researcher"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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
|
||||
@@ -228,7 +228,7 @@ roles:
|
||||
|
||||
risk_assessment_specialist:
|
||||
role: "Risk Assessment Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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,
|
||||
@@ -287,7 +287,7 @@ roles:
|
||||
|
||||
research_report_synthesizer:
|
||||
role: "Research Report Synthesizer"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
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
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Multiple MCP Servers Example
|
||||
|
||||
This example demonstrates how to use multiple MCP servers with a single agent.
|
||||
You can combine different MCP servers (time, memory, filesystem, etc.) along
|
||||
with regular Python functions as tools.
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents
|
||||
- uvx (for mcp-server-time)
|
||||
- npx (for @modelcontextprotocol servers)
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.mcp import MCP
|
||||
|
||||
|
||||
def my_custom_tool(query: str) -> str:
|
||||
"""
|
||||
A custom Python function that can be used alongside MCP tools.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
|
||||
Returns:
|
||||
A formatted result string
|
||||
"""
|
||||
return f"Custom tool result for: {query}"
|
||||
|
||||
|
||||
def main():
|
||||
# Example 1: Single MCP server (basic usage)
|
||||
print("=" * 60)
|
||||
print("Example 1: Single MCP Server")
|
||||
print("=" * 60)
|
||||
|
||||
agent_single = Agent(
|
||||
name="TimeAgent",
|
||||
instructions="You are a helpful assistant that can tell time.",
|
||||
tools=MCP("uvx mcp-server-time")
|
||||
)
|
||||
|
||||
response = agent_single.start("What time is it in Tokyo?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 2: Multiple MCP servers
|
||||
print("=" * 60)
|
||||
print("Example 2: Multiple MCP Servers")
|
||||
print("=" * 60)
|
||||
|
||||
agent_multi = Agent(
|
||||
name="MultiMCPAgent",
|
||||
instructions="""You are a helpful assistant with access to multiple tools:
|
||||
- Time tools for getting current time and converting timezones
|
||||
- Memory tools for storing and retrieving information
|
||||
Use the appropriate tool based on the user's request.""",
|
||||
tools=[
|
||||
MCP("uvx mcp-server-time"), # Time tools
|
||||
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
|
||||
]
|
||||
)
|
||||
|
||||
response = agent_multi.start("What time is it in New York and London?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 3: Multiple MCP servers + custom functions
|
||||
print("=" * 60)
|
||||
print("Example 3: MCP Servers + Custom Functions")
|
||||
print("=" * 60)
|
||||
|
||||
agent_mixed = Agent(
|
||||
name="MixedToolsAgent",
|
||||
instructions="""You are a helpful assistant with access to:
|
||||
- Time tools for timezone operations
|
||||
- A custom search tool for general queries
|
||||
Choose the right tool for each task.""",
|
||||
tools=[
|
||||
MCP("uvx mcp-server-time"), # MCP server
|
||||
my_custom_tool # Regular Python function
|
||||
]
|
||||
)
|
||||
|
||||
response = agent_mixed.start("What time is it in Paris?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 4: MCP with environment variables
|
||||
print("=" * 60)
|
||||
print("Example 4: MCP with Environment Variables")
|
||||
print("=" * 60)
|
||||
|
||||
# Note: Replace with your actual API key
|
||||
# agent_with_env = Agent(
|
||||
# name="SearchAgent",
|
||||
# instructions="You can search the web for information.",
|
||||
# tools=[
|
||||
# MCP(
|
||||
# command="npx",
|
||||
# args=["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
# env={"BRAVE_API_KEY": "your-api-key-here"}
|
||||
# ),
|
||||
# MCP("uvx mcp-server-time")
|
||||
# ]
|
||||
# )
|
||||
print("(Skipped - requires BRAVE_API_KEY)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,8 @@
|
||||
framework: praisonai
|
||||
topic: crawl and extract all data from each url from this page https://news.ycombinator.com/
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
web_scraper:
|
||||
backstory: An expert in data extraction from websites, adept at navigating and
|
||||
instructions: # Canonical An expert in data extraction from websites, adept at navigating and
|
||||
retrieving detailed information.
|
||||
goal: Extract URLs from the source page
|
||||
role: Web Scraper
|
||||
@@ -15,7 +15,7 @@ roles:
|
||||
tools:
|
||||
- 'code_interpret'
|
||||
url_data_extractor:
|
||||
backstory: Specializes in crawling websites to gather comprehensive data, ensuring
|
||||
instructions: # Canonical Specializes in crawling websites to gather comprehensive data, ensuring
|
||||
nothing is missed from each link.
|
||||
goal: Crawl each URL for data extraction
|
||||
role: URL Data Extractor
|
||||
@@ -29,7 +29,7 @@ roles:
|
||||
tools:
|
||||
- 'code_interpret'
|
||||
data_organizer:
|
||||
backstory: Known for keen attention to detail, capable of structuring vast amounts
|
||||
instructions: # Canonical Known for keen attention to detail, capable of structuring vast amounts
|
||||
of information into usable formats efficiently.
|
||||
goal: Organize extracted data into a structured format
|
||||
role: Data Organizer
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Simple Agentic Workflow Example
|
||||
|
||||
Demonstrates using Agent instances directly as workflow steps.
|
||||
Each agent processes the input and passes output to the next agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
|
||||
# Create agents with specific roles
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Analyst",
|
||||
goal="Research and provide information about topics",
|
||||
instructions="You are a research analyst. Provide concise, factual information."
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Write engaging content based on research",
|
||||
instructions="You are a content writer. Write clear, engaging content based on the research provided."
|
||||
)
|
||||
|
||||
# Create workflow with agents as steps
|
||||
workflow = Workflow(
|
||||
name="Simple Agentic Pipeline",
|
||||
steps=[researcher, writer]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run workflow - agents process sequentially
|
||||
result = workflow.start(
|
||||
"What are the key benefits of AI agents?",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"\nFinal output:\n{result['output']}")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Simple Workflow Example
|
||||
|
||||
The easiest way to create and run workflows in PraisonAI.
|
||||
Just pass functions or agents as steps - no complex configuration needed!
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
|
||||
# Define simple handler functions
|
||||
def validate(ctx: WorkflowContext) -> StepResult:
|
||||
"""Validate the input data."""
|
||||
if not ctx.input:
|
||||
return StepResult(output="No input provided", stop_workflow=True)
|
||||
return StepResult(output=f"Validated: {ctx.input}")
|
||||
|
||||
def process(ctx: WorkflowContext) -> StepResult:
|
||||
"""Process the validated data."""
|
||||
return StepResult(output=f"Processed: {ctx.previous_result}")
|
||||
|
||||
def format_output(ctx: WorkflowContext) -> StepResult:
|
||||
"""Format the final output."""
|
||||
return StepResult(output=f"✅ Final: {ctx.previous_result}")
|
||||
|
||||
# Create workflow - just list your functions!
|
||||
workflow = Workflow(
|
||||
name="Simple Pipeline",
|
||||
steps=[validate, process, format_output]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the workflow - PraisonAI style uses .start()
|
||||
result = workflow.start("Hello, World!", verbose=True)
|
||||
|
||||
print(f"\nFinal output: {result['output']}")
|
||||
print(f"Steps completed: {len(result['steps'])}")
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Task Callbacks Example
|
||||
|
||||
Demonstrates using on_task_start and on_task_complete callbacks
|
||||
with PraisonAIAgents for workflow monitoring and logging.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# Create an agent
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Assistant",
|
||||
goal="Find and summarize information",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create tasks with variables
|
||||
task1 = Task(
|
||||
name="research",
|
||||
description="Research the topic: {{topic}}",
|
||||
expected_output="A brief summary of the topic",
|
||||
agent=researcher,
|
||||
variables={"topic": "artificial intelligence"}
|
||||
)
|
||||
|
||||
task2 = Task(
|
||||
name="summarize",
|
||||
description="Create a one-paragraph summary of the research.",
|
||||
expected_output="A concise summary",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Define callbacks
|
||||
def on_start(task, task_id):
|
||||
"""Called before each task starts."""
|
||||
print(f"🚀 Starting task: {task.name} (ID: {task_id})")
|
||||
|
||||
def on_complete(task, output):
|
||||
"""Called after each task completes."""
|
||||
print(f"✅ Completed task: {task.name}")
|
||||
print(f" Output preview: {str(output)[:100]}...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create agents with callbacks
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher],
|
||||
tasks=[task1, task2],
|
||||
process="workflow",
|
||||
on_task_start=on_start,
|
||||
on_task_complete=on_complete,
|
||||
variables={"global_var": "shared_value"} # Global variables
|
||||
)
|
||||
|
||||
# Run the workflow
|
||||
result = agents.start()
|
||||
print("\n🎉 Workflow completed!")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Workflow Branching Example
|
||||
|
||||
Demonstrates conditional branching in workflows where steps can route
|
||||
to different next steps based on output content.
|
||||
"""
|
||||
|
||||
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
|
||||
|
||||
# Create a workflow with branching
|
||||
workflow = Workflow(
|
||||
name="Decision Workflow",
|
||||
description="A workflow that branches based on validation result",
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="validate",
|
||||
action="Check if the number 42 is positive. Reply with 'valid' or 'invalid'.",
|
||||
next_steps=["success_handler", "error_handler"],
|
||||
branch_condition={
|
||||
"valid": ["success_handler"],
|
||||
"invalid": ["error_handler"]
|
||||
}
|
||||
),
|
||||
WorkflowStep(
|
||||
name="success_handler",
|
||||
action="The validation passed! Generate a success message."
|
||||
),
|
||||
WorkflowStep(
|
||||
name="error_handler",
|
||||
action="The validation failed. Generate an error message."
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Decision Workflow"] = workflow
|
||||
|
||||
# Execute - will branch to success_handler since 42 is positive
|
||||
result = manager.execute(
|
||||
"Decision Workflow",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("Workflow completed!")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Workflow Checkpoints Example
|
||||
|
||||
Demonstrates saving and resuming workflow execution using checkpoints.
|
||||
Useful for long-running workflows that may be interrupted.
|
||||
"""
|
||||
|
||||
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
|
||||
|
||||
# Create a multi-step workflow
|
||||
workflow = Workflow(
|
||||
name="Long Process",
|
||||
description="A workflow with checkpoints for resumability",
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="step1",
|
||||
action="Initialize the process and prepare data."
|
||||
),
|
||||
WorkflowStep(
|
||||
name="step2",
|
||||
action="Process the first batch of data."
|
||||
),
|
||||
WorkflowStep(
|
||||
name="step3",
|
||||
action="Process the second batch of data."
|
||||
),
|
||||
WorkflowStep(
|
||||
name="step4",
|
||||
action="Finalize and generate report."
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Long Process"] = workflow
|
||||
|
||||
# Execute with checkpoint - saves after each step
|
||||
print("=== Starting workflow with checkpoint ===")
|
||||
result = manager.execute(
|
||||
"Long Process",
|
||||
default_llm="gpt-4o-mini",
|
||||
checkpoint="my-checkpoint" # Saves progress after each step
|
||||
)
|
||||
|
||||
print(f"Completed {len(result['results'])} steps")
|
||||
|
||||
# List available checkpoints
|
||||
print("\n=== Available checkpoints ===")
|
||||
checkpoints = manager.list_checkpoints()
|
||||
for cp in checkpoints:
|
||||
print(f" {cp['name']}: {cp['completed_steps']} steps completed")
|
||||
|
||||
# Resume from checkpoint (if workflow was interrupted)
|
||||
# result = manager.execute(
|
||||
# "Long Process",
|
||||
# default_llm="gpt-4o-mini",
|
||||
# resume="my-checkpoint" # Resumes from last saved state
|
||||
# )
|
||||
|
||||
# Clean up checkpoint
|
||||
manager.delete_checkpoint("my-checkpoint")
|
||||
print("\n✅ Checkpoint deleted")
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Agentic Conditional Workflow Example
|
||||
|
||||
Demonstrates using should_run to conditionally execute agent steps
|
||||
based on input or previous results.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow, WorkflowStep
|
||||
|
||||
# Condition functions
|
||||
def is_technical(ctx) -> bool:
|
||||
"""Only run technical review for technical content."""
|
||||
keywords = ["code", "programming", "algorithm", "database", "api"]
|
||||
return any(keyword in ctx.input.lower() for keyword in keywords)
|
||||
|
||||
def needs_creative(ctx) -> bool:
|
||||
"""Only run creative enhancement for creative content."""
|
||||
keywords = ["story", "poem", "creative", "write", "narrative"]
|
||||
return any(keyword in ctx.input.lower() for keyword in keywords)
|
||||
|
||||
# Create agents
|
||||
analyzer = Agent(
|
||||
name="Analyzer",
|
||||
role="Content Analyzer",
|
||||
goal="Analyze and process content",
|
||||
instructions="Analyze the given content and provide insights."
|
||||
)
|
||||
|
||||
tech_reviewer = Agent(
|
||||
name="TechReviewer",
|
||||
role="Technical Reviewer",
|
||||
goal="Review technical content",
|
||||
instructions="Review the technical aspects and provide technical feedback."
|
||||
)
|
||||
|
||||
creative_enhancer = Agent(
|
||||
name="CreativeEnhancer",
|
||||
role="Creative Enhancer",
|
||||
goal="Enhance creative content",
|
||||
instructions="Enhance the creative aspects and make it more engaging."
|
||||
)
|
||||
|
||||
finalizer = Agent(
|
||||
name="Finalizer",
|
||||
role="Content Finalizer",
|
||||
goal="Finalize the content",
|
||||
instructions="Provide the final polished version of the content."
|
||||
)
|
||||
|
||||
# Create workflow with conditional steps
|
||||
workflow = Workflow(
|
||||
name="Conditional Agentic Pipeline",
|
||||
steps=[
|
||||
analyzer, # Always runs
|
||||
WorkflowStep(
|
||||
name="tech_review",
|
||||
agent=tech_reviewer,
|
||||
should_run=is_technical # Only runs for technical content
|
||||
),
|
||||
WorkflowStep(
|
||||
name="creative_enhance",
|
||||
agent=creative_enhancer,
|
||||
should_run=needs_creative # Only runs for creative content
|
||||
),
|
||||
finalizer # Always runs
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test 1: Technical content
|
||||
print("=== Test 1: Technical Content ===")
|
||||
result = workflow.start("Review this Python code for best practices", verbose=True)
|
||||
print(f"Output: {result['output'][:200]}...\n")
|
||||
|
||||
# Test 2: Creative content
|
||||
print("=== Test 2: Creative Content ===")
|
||||
result = workflow.start("Write a short story about a robot", verbose=True)
|
||||
print(f"Output: {result['output'][:200]}...\n")
|
||||
|
||||
# Test 3: General content (neither technical nor creative)
|
||||
print("=== Test 3: General Content ===")
|
||||
result = workflow.start("What is the weather like today?", verbose=True)
|
||||
print(f"Output: {result['output'][:200]}...")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Workflow Conditional Execution Example
|
||||
|
||||
Demonstrates using should_run to conditionally execute steps
|
||||
based on input or previous results.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowStep, WorkflowContext, StepResult
|
||||
|
||||
# Condition functions - return True to run the step, False to skip
|
||||
def is_sensitive_content(ctx: WorkflowContext) -> bool:
|
||||
"""Only run compliance check for sensitive topics."""
|
||||
sensitive_keywords = ["legal", "medical", "financial", "security"]
|
||||
return any(keyword in ctx.input.lower() for keyword in sensitive_keywords)
|
||||
|
||||
def needs_translation(ctx: WorkflowContext) -> bool:
|
||||
"""Only translate if content is not in English."""
|
||||
return ctx.variables.get("language", "en") != "en"
|
||||
|
||||
# Handler functions
|
||||
def process_content(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"Processed: {ctx.input}")
|
||||
|
||||
def compliance_check(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"✅ Compliance verified for: {ctx.previous_result}")
|
||||
|
||||
def translate(ctx: WorkflowContext) -> StepResult:
|
||||
lang = ctx.variables.get("language", "unknown")
|
||||
return StepResult(output=f"🌐 Translated to {lang}: {ctx.previous_result}")
|
||||
|
||||
def finalize(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"📄 Final: {ctx.previous_result}")
|
||||
|
||||
# Create workflow with conditional steps
|
||||
workflow = Workflow(
|
||||
name="Conditional Pipeline",
|
||||
steps=[
|
||||
WorkflowStep(name="process", handler=process_content),
|
||||
WorkflowStep(
|
||||
name="compliance",
|
||||
handler=compliance_check,
|
||||
should_run=is_sensitive_content # Only runs for sensitive content
|
||||
),
|
||||
WorkflowStep(
|
||||
name="translate",
|
||||
handler=translate,
|
||||
should_run=needs_translation # Only runs if language != "en"
|
||||
),
|
||||
WorkflowStep(name="finalize", handler=finalize)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test 1: Normal content (skips compliance)
|
||||
print("=== Test 1: Normal content ===")
|
||||
result = workflow.start("Hello world", verbose=True)
|
||||
print(f"Output: {result['output']}\n")
|
||||
|
||||
# Test 2: Sensitive content (runs compliance)
|
||||
print("=== Test 2: Legal content ===")
|
||||
result = workflow.start("Review this legal document", verbose=True)
|
||||
print(f"Output: {result['output']}\n")
|
||||
|
||||
# Test 3: Non-English content (runs translation)
|
||||
print("=== Test 3: Non-English content ===")
|
||||
workflow.variables = {"language": "es"}
|
||||
result = workflow.start("Hola mundo", verbose=True)
|
||||
print(f"Output: {result['output']}")
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Workflow Early Stop Example
|
||||
|
||||
Demonstrates how to stop a workflow early using custom handler
|
||||
functions that return StepResult with stop_workflow=True.
|
||||
"""
|
||||
|
||||
from praisonaiagents.memory.workflows import (
|
||||
WorkflowManager, WorkflowStep, Workflow,
|
||||
WorkflowContext, StepResult
|
||||
)
|
||||
|
||||
# Custom validator that can stop the workflow
|
||||
def validate_data(context: WorkflowContext) -> StepResult:
|
||||
"""Check if data is valid and stop workflow if not."""
|
||||
data = context.variables.get("data", {})
|
||||
|
||||
if data.get("value", 0) < 0:
|
||||
return StepResult(
|
||||
output="❌ Validation failed: negative value detected. Stopping workflow.",
|
||||
stop_workflow=True # This stops the entire workflow
|
||||
)
|
||||
|
||||
return StepResult(
|
||||
output="✅ Validation passed: data is valid.",
|
||||
stop_workflow=False # Continue to next step
|
||||
)
|
||||
|
||||
# Create workflow with early stop capability
|
||||
workflow = Workflow(
|
||||
name="Data Processing",
|
||||
description="Process data with validation gate",
|
||||
variables={
|
||||
"data": {"value": -5} # Invalid data - will trigger early stop
|
||||
},
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="validate",
|
||||
handler=validate_data # Custom function
|
||||
),
|
||||
WorkflowStep(
|
||||
name="process",
|
||||
action="Process the validated data." # Won't run if validation fails
|
||||
),
|
||||
WorkflowStep(
|
||||
name="report",
|
||||
action="Generate final report." # Won't run if validation fails
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Data Processing"] = workflow
|
||||
|
||||
# Test with invalid data (will stop early)
|
||||
print("=== Testing with invalid data ===")
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['output']}")
|
||||
|
||||
# Test with valid data (will complete)
|
||||
print("\n=== Testing with valid data ===")
|
||||
workflow.variables["data"] = {"value": 42}
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Agentic Loop with CSV Workflow Example
|
||||
|
||||
Demonstrates iterating over a CSV file with an agent processing each row.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import loop
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# Create sample CSV file content
|
||||
csv_content = """topic,description
|
||||
AI Ethics,Ethical considerations in artificial intelligence
|
||||
Machine Learning,Algorithms that learn from data
|
||||
Neural Networks,Computing systems inspired by biological neural networks
|
||||
Natural Language Processing,AI understanding human language"""
|
||||
|
||||
# Create processor agent
|
||||
processor = Agent(
|
||||
name="TopicProcessor",
|
||||
role="Topic Analyst",
|
||||
goal="Analyze and explain topics",
|
||||
instructions="Provide a brief, insightful analysis of the given topic."
|
||||
)
|
||||
|
||||
# Create summarizer agent
|
||||
summarizer = Agent(
|
||||
name="Summarizer",
|
||||
role="Results Summarizer",
|
||||
goal="Summarize all processed results",
|
||||
instructions="Create a comprehensive summary of all the analyzed topics."
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create temp CSV file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
|
||||
f.write(csv_content)
|
||||
csv_path = f.name
|
||||
|
||||
try:
|
||||
print("=== Testing Agentic Loop with CSV ===\n")
|
||||
print(f"Processing CSV: {csv_path}\n")
|
||||
|
||||
# Create workflow with loop over CSV
|
||||
workflow = Workflow(
|
||||
name="CSV Topic Processor",
|
||||
steps=[
|
||||
loop(processor, from_csv=csv_path),
|
||||
summarizer
|
||||
]
|
||||
)
|
||||
|
||||
result = workflow.start("Analyze these AI topics", verbose=True)
|
||||
|
||||
print(f"\nFinal Summary:\n{result['output']}")
|
||||
|
||||
finally:
|
||||
os.unlink(csv_path)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Workflow Loop with CSV Example
|
||||
|
||||
Demonstrates iterating over a CSV file, processing each row.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents.workflows import loop
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# Create sample CSV file
|
||||
csv_content = """name,email,task
|
||||
Alice,[email protected],Write documentation
|
||||
Bob,[email protected],Review pull requests
|
||||
Charlie,[email protected],Fix critical bug
|
||||
Diana,[email protected],Deploy to production"""
|
||||
|
||||
# Processor for each CSV row
|
||||
def process_task(ctx: WorkflowContext) -> StepResult:
|
||||
"""Process a single task from CSV."""
|
||||
item = ctx.variables.get("item", {})
|
||||
name = item.get("name", "Unknown")
|
||||
task = item.get("task", "No task")
|
||||
index = ctx.variables.get("loop_index", 0)
|
||||
|
||||
return StepResult(
|
||||
output=f"[{index+1}] {name}: {task} ✅",
|
||||
variables={"last_processed": name}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create temp CSV file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
|
||||
f.write(csv_content)
|
||||
csv_path = f.name
|
||||
|
||||
try:
|
||||
print("=== Testing Loop with CSV ===\n")
|
||||
print(f"CSV file: {csv_path}\n")
|
||||
|
||||
# Create workflow
|
||||
workflow = Workflow(
|
||||
name="CSV Task Processor",
|
||||
steps=[loop(process_task, from_csv=csv_path)]
|
||||
)
|
||||
|
||||
result = workflow.start("Process all tasks", verbose=True)
|
||||
|
||||
print(f"\nAll outputs:\n{result['output']}")
|
||||
print(f"\nProcessed {len(result['variables']['loop_outputs'])} items")
|
||||
print(f"Last processed: {result['variables'].get('last_processed')}")
|
||||
|
||||
finally:
|
||||
os.unlink(csv_path)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Workflow Loop with List Example
|
||||
|
||||
Demonstrates iterating over a list of items, processing each one.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents.workflows import loop
|
||||
|
||||
# Sample data
|
||||
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
|
||||
|
||||
# Processor for each item
|
||||
def process_fruit(ctx: WorkflowContext) -> StepResult:
|
||||
"""Process a single fruit from the list."""
|
||||
fruit = ctx.variables.get("item", "unknown")
|
||||
index = ctx.variables.get("loop_index", 0)
|
||||
|
||||
# Simulate processing
|
||||
result = f"🍎 [{index + 1}] {fruit.upper()} - processed successfully"
|
||||
|
||||
return StepResult(
|
||||
output=result,
|
||||
variables={"last_fruit": fruit}
|
||||
)
|
||||
|
||||
# Aggregator to summarize results
|
||||
def summarize(ctx: WorkflowContext) -> StepResult:
|
||||
"""Summarize all processed fruits."""
|
||||
outputs = ctx.variables.get("loop_outputs", [])
|
||||
summary = f"📊 Processed {len(outputs)} fruits:\n"
|
||||
summary += "\n".join(f" {o}" for o in outputs)
|
||||
return StepResult(output=summary)
|
||||
|
||||
# Create workflow
|
||||
workflow = Workflow(
|
||||
name="Fruit Processor",
|
||||
steps=[
|
||||
loop(process_fruit, over="fruits"),
|
||||
summarize
|
||||
],
|
||||
variables={"fruits": fruits}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Loop with List ===\n")
|
||||
|
||||
result = workflow.start("Process all fruits", verbose=True)
|
||||
|
||||
print(f"\nFinal Output:\n{result['output']}")
|
||||
print(f"\nLoop outputs count: {len(result['variables']['loop_outputs'])}")
|
||||
print(f"Last processed: {result['variables'].get('last_fruit')}")
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Workflow Loops Example
|
||||
|
||||
Demonstrates iterating over a list of items in a workflow step.
|
||||
Each item is processed individually with the loop variable.
|
||||
"""
|
||||
|
||||
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
|
||||
|
||||
# Create a workflow with loop
|
||||
workflow = Workflow(
|
||||
name="Process Items",
|
||||
description="Process a list of items using loop",
|
||||
variables={
|
||||
"items": ["apple", "banana", "cherry"] # List to iterate over
|
||||
},
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="process_each",
|
||||
action="Describe the fruit: {{item}}",
|
||||
loop_over="items", # Variable name containing the list
|
||||
loop_var="item" # Variable name for current item
|
||||
),
|
||||
WorkflowStep(
|
||||
name="summarize",
|
||||
action="Summarize all the fruits that were processed."
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Process Items"] = workflow
|
||||
|
||||
# Execute - will process each fruit in the list
|
||||
result = manager.execute(
|
||||
"Process Items",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("Workflow completed!")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
if isinstance(step_result.get("output"), list):
|
||||
print(f" Loop processed {len(step_result['output'])} items")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Mixed Steps Workflow Example
|
||||
|
||||
Demonstrates combining different step types in a single workflow:
|
||||
- Agent instances
|
||||
- Handler functions
|
||||
- WorkflowStep with action strings
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowStep, WorkflowContext, StepResult, Agent
|
||||
|
||||
# Create an agent
|
||||
analyzer = Agent(
|
||||
name="Analyzer",
|
||||
role="Data Analyst",
|
||||
goal="Analyze and summarize data",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Custom handler functions
|
||||
def validate_input(ctx: WorkflowContext) -> StepResult:
|
||||
"""Validate the input before processing."""
|
||||
if len(ctx.input) < 5:
|
||||
return StepResult(
|
||||
output="Input too short. Minimum 5 characters required.",
|
||||
stop_workflow=True
|
||||
)
|
||||
return StepResult(output=f"✅ Valid input: {ctx.input}")
|
||||
|
||||
def format_output(ctx: WorkflowContext) -> StepResult:
|
||||
"""Format the final output."""
|
||||
return StepResult(
|
||||
output=f"📊 Report:\n{ctx.previous_result}",
|
||||
variables={"report_generated": True}
|
||||
)
|
||||
|
||||
# Create workflow with mixed step types
|
||||
workflow = Workflow(
|
||||
name="Mixed Pipeline",
|
||||
steps=[
|
||||
# Step 1: Function - validates input
|
||||
validate_input,
|
||||
|
||||
# Step 2: Agent - analyzes content
|
||||
analyzer,
|
||||
|
||||
# Step 3: WorkflowStep with action string
|
||||
WorkflowStep(
|
||||
name="enhance",
|
||||
action="Add a conclusion to this analysis: {{previous_output}}"
|
||||
),
|
||||
|
||||
# Step 4: Function - formats output
|
||||
format_output
|
||||
],
|
||||
default_llm="gpt-4o-mini" # Used for action-based steps
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test with valid input
|
||||
print("=== Running mixed workflow ===")
|
||||
result = workflow.start(
|
||||
"Analyze the trends in AI adoption for 2024",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"\nFinal output:\n{result['output']}")
|
||||
print(f"\nVariables: {result['variables']}")
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Agentic Parallel Workflow Example
|
||||
|
||||
Demonstrates running multiple agents concurrently and
|
||||
combining their results with an aggregator agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import parallel
|
||||
|
||||
# Create parallel research agents
|
||||
market_researcher = Agent(
|
||||
name="MarketResearcher",
|
||||
role="Market Research Analyst",
|
||||
goal="Research market trends and opportunities",
|
||||
instructions="Analyze market trends. Provide concise market insights."
|
||||
)
|
||||
|
||||
competitor_researcher = Agent(
|
||||
name="CompetitorResearcher",
|
||||
role="Competitive Intelligence Analyst",
|
||||
goal="Research competitor strategies",
|
||||
instructions="Analyze competitors. Provide key competitive insights."
|
||||
)
|
||||
|
||||
customer_researcher = Agent(
|
||||
name="CustomerResearcher",
|
||||
role="Customer Research Analyst",
|
||||
goal="Research customer needs and behaviors",
|
||||
instructions="Analyze customer segments. Provide customer insights."
|
||||
)
|
||||
|
||||
# Create aggregator agent
|
||||
aggregator = Agent(
|
||||
name="Aggregator",
|
||||
role="Research Synthesizer",
|
||||
goal="Synthesize research findings",
|
||||
instructions="Combine all research findings into a comprehensive summary."
|
||||
)
|
||||
|
||||
# Create workflow with parallel execution
|
||||
workflow = Workflow(
|
||||
name="Parallel Research Pipeline",
|
||||
steps=[
|
||||
parallel([market_researcher, competitor_researcher, customer_researcher]),
|
||||
aggregator
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Agentic Parallel Workflow ===\n")
|
||||
|
||||
# Run workflow - all researchers work in parallel, then aggregator summarizes
|
||||
result = workflow.start("Research the AI industry", verbose=True)
|
||||
|
||||
print(f"\nFinal Summary:\n{result['output']}")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Workflow Parallel Execution Example
|
||||
|
||||
Demonstrates running multiple steps concurrently and
|
||||
combining their results.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents.workflows import parallel
|
||||
import time
|
||||
|
||||
# Parallel workers - each does independent work
|
||||
def research_market(ctx: WorkflowContext) -> StepResult:
|
||||
"""Research market trends."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return StepResult(output="📊 Market: Growth expected 15% YoY")
|
||||
|
||||
def research_competitors(ctx: WorkflowContext) -> StepResult:
|
||||
"""Research competitor analysis."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return StepResult(output="🏢 Competitors: 3 major players identified")
|
||||
|
||||
def research_customers(ctx: WorkflowContext) -> StepResult:
|
||||
"""Research customer feedback."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return StepResult(output="👥 Customers: 85% satisfaction rate")
|
||||
|
||||
# Aggregator - combines parallel results
|
||||
def summarize_research(ctx: WorkflowContext) -> StepResult:
|
||||
"""Summarize all research findings."""
|
||||
outputs = ctx.variables.get("parallel_outputs", [])
|
||||
summary = "📋 RESEARCH SUMMARY:\n" + "\n".join(f" • {o}" for o in outputs)
|
||||
return StepResult(output=summary)
|
||||
|
||||
# Create workflow with parallel execution
|
||||
workflow = Workflow(
|
||||
name="Parallel Research",
|
||||
steps=[
|
||||
parallel([research_market, research_competitors, research_customers]),
|
||||
summarize_research
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Parallel Workflow ===\n")
|
||||
|
||||
start_time = time.time()
|
||||
result = workflow.start("Analyze the business landscape", verbose=True)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
print(f"\nFinal Output:\n{result['output']}")
|
||||
print(f"\nCompleted in {elapsed:.2f}s (parallel execution)")
|
||||
print(f"Individual outputs: {len(result['variables']['parallel_outputs'])}")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Agentic Evaluator-Optimizer (Repeat) Workflow Example
|
||||
|
||||
Demonstrates the evaluator-optimizer pattern where an agent
|
||||
generates content and another evaluates it, repeating until approved.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import repeat
|
||||
|
||||
# Create generator agent
|
||||
generator = Agent(
|
||||
name="Generator",
|
||||
role="Content Generator",
|
||||
goal="Generate high-quality content",
|
||||
instructions="Generate content based on the topic. Improve based on feedback if provided."
|
||||
)
|
||||
|
||||
# Create evaluator agent
|
||||
evaluator = Agent(
|
||||
name="Evaluator",
|
||||
role="Content Evaluator",
|
||||
goal="Evaluate and provide feedback on content",
|
||||
instructions="Evaluate the content quality. If good, respond with 'APPROVED'. Otherwise provide specific feedback for improvement."
|
||||
)
|
||||
|
||||
# Evaluation function to check if content is approved
|
||||
def is_approved(ctx) -> bool:
|
||||
return "approved" in ctx.previous_result.lower()
|
||||
|
||||
# Create workflow with evaluator-optimizer pattern
|
||||
workflow = Workflow(
|
||||
name="Evaluator-Optimizer Pipeline",
|
||||
steps=[
|
||||
generator, # First, generate content
|
||||
repeat(
|
||||
evaluator, # Evaluate and provide feedback
|
||||
until=is_approved,
|
||||
max_iterations=3
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Agentic Evaluator-Optimizer Workflow ===\n")
|
||||
|
||||
# Run optimization workflow
|
||||
result = workflow.start(
|
||||
"Write a compelling product description for an AI assistant",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"\nFinal Result:\n{result['output']}")
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Workflow Repeat (Evaluator-Optimizer) Example
|
||||
|
||||
Demonstrates repeating a step until a condition is met.
|
||||
This is useful for iterative improvement patterns.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents.workflows import repeat
|
||||
|
||||
# Simulated content generator that improves each iteration
|
||||
class ContentGenerator:
|
||||
def __init__(self):
|
||||
self.iteration = 0
|
||||
self.content = []
|
||||
|
||||
def generate(self, ctx: WorkflowContext) -> StepResult:
|
||||
"""Generate content, adding more each iteration."""
|
||||
self.iteration += 1
|
||||
|
||||
# Add new content each iteration
|
||||
new_items = [
|
||||
f"Point {self.iteration}.1: Key insight about the topic",
|
||||
f"Point {self.iteration}.2: Supporting evidence"
|
||||
]
|
||||
self.content.extend(new_items)
|
||||
|
||||
output = f"Iteration {self.iteration}: Generated {len(self.content)} points total"
|
||||
|
||||
return StepResult(
|
||||
output=output,
|
||||
variables={
|
||||
"total_points": len(self.content),
|
||||
"content": self.content.copy()
|
||||
}
|
||||
)
|
||||
|
||||
# Condition: stop when we have enough content
|
||||
def has_enough_content(ctx: WorkflowContext) -> bool:
|
||||
"""Check if we have generated enough content."""
|
||||
total = ctx.variables.get("total_points", 0)
|
||||
return total >= 6 # Stop when we have 6+ points
|
||||
|
||||
# Create generator instance
|
||||
generator = ContentGenerator()
|
||||
|
||||
# Create workflow with repeat pattern
|
||||
workflow = Workflow(
|
||||
name="Content Generator",
|
||||
steps=[
|
||||
repeat(
|
||||
generator.generate,
|
||||
until=has_enough_content,
|
||||
max_iterations=10
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Repeat (Evaluator-Optimizer) Pattern ===\n")
|
||||
|
||||
result = workflow.start("Generate comprehensive content", verbose=True)
|
||||
|
||||
print(f"\nFinal output: {result['output']}")
|
||||
print(f"Total iterations: {result['variables']['repeat_iterations']}")
|
||||
print(f"Total points generated: {result['variables']['total_points']}")
|
||||
print("\nGenerated content:")
|
||||
for point in result['variables']['content']:
|
||||
print(f" • {point}")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Agentic Routing Workflow Example
|
||||
|
||||
Demonstrates decision-based routing where an agent classifier
|
||||
routes to specialized handler agents based on the request type.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import route
|
||||
|
||||
# Create classifier agent
|
||||
classifier = Agent(
|
||||
name="Classifier",
|
||||
role="Request Classifier",
|
||||
goal="Classify incoming requests",
|
||||
instructions="Classify the request. Respond with ONLY 'technical', 'creative', or 'general'."
|
||||
)
|
||||
|
||||
# Create specialized handler agents
|
||||
tech_agent = Agent(
|
||||
name="TechExpert",
|
||||
role="Technical Expert",
|
||||
goal="Handle technical questions",
|
||||
instructions="You are a technical expert. Provide detailed technical answers."
|
||||
)
|
||||
|
||||
creative_agent = Agent(
|
||||
name="CreativeWriter",
|
||||
role="Creative Writer",
|
||||
goal="Handle creative requests",
|
||||
instructions="You are a creative writer. Write engaging, creative content."
|
||||
)
|
||||
|
||||
general_agent = Agent(
|
||||
name="GeneralAssistant",
|
||||
role="General Assistant",
|
||||
goal="Handle general requests",
|
||||
instructions="You are a helpful assistant. Provide clear, helpful responses."
|
||||
)
|
||||
|
||||
# Create workflow with routing
|
||||
workflow = Workflow(
|
||||
name="Agentic Router",
|
||||
steps=[
|
||||
classifier,
|
||||
route({
|
||||
"technical": [tech_agent],
|
||||
"creative": [creative_agent],
|
||||
"default": [general_agent]
|
||||
})
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Agentic Routing Workflow ===\n")
|
||||
|
||||
# Test 1: Technical question
|
||||
print("--- Technical Request ---")
|
||||
result = workflow.start("How does machine learning work?", verbose=True)
|
||||
print(f"Result: {result['output'][:200]}...\n")
|
||||
|
||||
# Test 2: Creative request
|
||||
print("--- Creative Request ---")
|
||||
result = workflow.start("Write a poem about the ocean", verbose=True)
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Workflow Routing Example
|
||||
|
||||
Demonstrates decision-based routing where the workflow
|
||||
takes different paths based on the output of a decision step.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents.workflows import route
|
||||
|
||||
# Decision maker - determines which route to take
|
||||
def classify_request(ctx: WorkflowContext) -> StepResult:
|
||||
"""Classify the input and decide the route."""
|
||||
input_lower = ctx.input.lower()
|
||||
|
||||
if "urgent" in input_lower or "emergency" in input_lower:
|
||||
return StepResult(output="priority: high")
|
||||
elif "question" in input_lower or "help" in input_lower:
|
||||
return StepResult(output="priority: support")
|
||||
else:
|
||||
return StepResult(output="priority: normal")
|
||||
|
||||
# Route handlers
|
||||
def handle_high_priority(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="🚨 HIGH PRIORITY: Escalating to senior team immediately!")
|
||||
|
||||
def handle_support(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="💬 SUPPORT: Routing to help desk for assistance.")
|
||||
|
||||
def handle_normal(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="📋 NORMAL: Added to standard processing queue.")
|
||||
|
||||
# Create workflow with routing
|
||||
workflow = Workflow(
|
||||
name="Request Router",
|
||||
steps=[
|
||||
classify_request,
|
||||
route({
|
||||
"high": [handle_high_priority],
|
||||
"support": [handle_support],
|
||||
"normal": [handle_normal],
|
||||
"default": [handle_normal]
|
||||
})
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test different inputs
|
||||
print("=== Testing Routing Workflow ===\n")
|
||||
|
||||
# Test 1: Urgent request
|
||||
result = workflow.start("This is an URGENT matter!", verbose=True)
|
||||
print(f"Result: {result['output']}\n")
|
||||
|
||||
# Test 2: Support request
|
||||
result = workflow.start("I have a question about my account", verbose=True)
|
||||
print(f"Result: {result['output']}\n")
|
||||
|
||||
# Test 3: Normal request
|
||||
result = workflow.start("Please process my order", verbose=True)
|
||||
print(f"Result: {result['output']}")
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Workflow with Agents Example
|
||||
|
||||
Demonstrates using Agent instances directly as workflow steps.
|
||||
Agents are automatically wrapped and executed in sequence.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, Agent
|
||||
|
||||
# Create agents
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Specialist",
|
||||
goal="Find accurate information",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Write engaging content",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
name="Editor",
|
||||
role="Content Editor",
|
||||
goal="Polish and improve content",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create workflow with agents as steps
|
||||
workflow = Workflow(
|
||||
name="Content Pipeline",
|
||||
steps=[researcher, writer, editor]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the workflow
|
||||
result = workflow.start(
|
||||
"Write a short paragraph about artificial intelligence",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"\nFinal output:\n{result['output']}")
|
||||
@@ -0,0 +1,117 @@
|
||||
# Extended agents.yaml with Workflow Patterns
|
||||
# This file demonstrates the extended agents.yaml format that supports
|
||||
# advanced workflow patterns while maintaining backward compatibility.
|
||||
#
|
||||
# Usage: praisonai agents_workflow.yaml
|
||||
#
|
||||
# Key differences from standard agents.yaml:
|
||||
# - process: workflow (triggers workflow mode)
|
||||
# - workflow: section for planning, reasoning, memory
|
||||
# - steps: section for workflow patterns (route, parallel, loop, repeat)
|
||||
|
||||
framework: praisonai
|
||||
process: workflow
|
||||
input: "Research AI trends and create a comprehensive report" # The input passed to {{input}} in steps
|
||||
|
||||
# Workflow configuration (new)
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
# Variables for template substitution
|
||||
variables:
|
||||
topic: AI trends
|
||||
topics:
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
# Agent definitions using familiar 'roles' format
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
classifier:
|
||||
role: Request Classifier
|
||||
instructions: # Canonical |
|
||||
You are an expert at classifying requests into categories.
|
||||
Respond with ONLY one word: 'technical', 'creative', or 'research'.
|
||||
goal: Classify incoming requests accurately
|
||||
|
||||
researcher:
|
||||
role: Research Analyst
|
||||
instructions: # Canonical |
|
||||
You are a skilled research analyst with expertise in technology trends.
|
||||
You provide concise, factual research findings with citations.
|
||||
goal: Research topics thoroughly and provide insights
|
||||
tools:
|
||||
- tavily_search
|
||||
llm:
|
||||
model: gpt-4o-mini
|
||||
|
||||
tech_expert:
|
||||
role: Technical Expert
|
||||
instructions: # Canonical |
|
||||
You are a technical expert with deep knowledge in AI and ML.
|
||||
You provide detailed technical explanations.
|
||||
goal: Handle technical questions with expertise
|
||||
|
||||
creative_writer:
|
||||
role: Creative Writer
|
||||
instructions: # Canonical |
|
||||
You are a creative writer who can make complex topics engaging.
|
||||
You write in a clear, accessible style.
|
||||
goal: Create engaging content from research
|
||||
|
||||
market_analyst:
|
||||
role: Market Analyst
|
||||
instructions: # Canonical |
|
||||
You analyze market trends and competitive landscapes.
|
||||
You provide actionable business insights.
|
||||
goal: Analyze market trends and opportunities
|
||||
|
||||
aggregator:
|
||||
role: Synthesizer
|
||||
instructions: # Canonical |
|
||||
You combine findings from multiple sources into coherent reports.
|
||||
You identify patterns and key insights.
|
||||
goal: Synthesize all findings into comprehensive reports
|
||||
|
||||
# Workflow steps with patterns (new)
|
||||
steps:
|
||||
# Step 1: Classify the request
|
||||
- agent: classifier
|
||||
action: "Classify this request: {{topic}}"
|
||||
|
||||
# Step 2: Route to appropriate handler based on classification
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [researcher]
|
||||
|
||||
# Step 3: Parallel research on multiple aspects
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: researcher
|
||||
action: "Research technology trends for {{topic}}"
|
||||
- agent: market_analyst
|
||||
action: "Analyze market opportunities for {{topic}}"
|
||||
|
||||
# Step 4: Loop over specific topics
|
||||
- agent: researcher
|
||||
action: "Deep dive into {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
# Step 5: Aggregate all findings
|
||||
- agent: aggregator
|
||||
action: "Synthesize all research findings into a comprehensive report"
|
||||
repeat:
|
||||
until: "comprehensive"
|
||||
max_iterations: 2
|
||||
|
||||
# Optional callbacks
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
@@ -0,0 +1,116 @@
|
||||
# Complete Workflow Example
|
||||
# Demonstrates all workflow patterns in a single comprehensive workflow
|
||||
|
||||
name: Complete Research Workflow
|
||||
description: A comprehensive workflow demonstrating all patterns - route, parallel, loop, repeat
|
||||
framework: praisonai
|
||||
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
variables:
|
||||
topic: AI trends
|
||||
topics:
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
agents:
|
||||
classifier:
|
||||
name: Classifier
|
||||
role: Request Classifier
|
||||
goal: Classify incoming requests
|
||||
instructions: |
|
||||
Classify the request. Respond with ONLY one word:
|
||||
- 'technical' for technical questions
|
||||
- 'creative' for creative requests
|
||||
- 'research' for research tasks
|
||||
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics thoroughly
|
||||
instructions: "You are a research analyst. Provide concise, factual research findings."
|
||||
|
||||
tech_expert:
|
||||
name: TechExpert
|
||||
role: Technical Expert
|
||||
goal: Handle technical questions
|
||||
instructions: "You are a technical expert. Provide detailed technical answers."
|
||||
|
||||
creative_writer:
|
||||
name: CreativeWriter
|
||||
role: Creative Writer
|
||||
goal: Handle creative requests
|
||||
instructions: "You are a creative writer. Write engaging, creative content."
|
||||
|
||||
market_analyst:
|
||||
name: MarketAnalyst
|
||||
role: Market Analyst
|
||||
goal: Analyze market trends
|
||||
instructions: "You are a market analyst. Provide market insights."
|
||||
|
||||
competitor_analyst:
|
||||
name: CompetitorAnalyst
|
||||
role: Competitor Analyst
|
||||
goal: Analyze competitors
|
||||
instructions: "You are a competitor analyst. Provide competitive insights."
|
||||
|
||||
aggregator:
|
||||
name: Aggregator
|
||||
role: Synthesizer
|
||||
goal: Combine and synthesize findings
|
||||
instructions: "You are a synthesizer. Combine all findings into a comprehensive report."
|
||||
|
||||
evaluator:
|
||||
name: Evaluator
|
||||
role: Quality Evaluator
|
||||
goal: Evaluate content quality
|
||||
instructions: |
|
||||
Evaluate the content. If it's comprehensive and well-structured, respond with "APPROVED".
|
||||
Otherwise, provide specific feedback for improvement.
|
||||
|
||||
steps:
|
||||
# Step 1: Classify the request
|
||||
- agent: classifier
|
||||
action: "Classify: {{input}}"
|
||||
|
||||
# Step 2: Route to appropriate handler
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [researcher]
|
||||
|
||||
# Step 3: Parallel research
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: market_analyst
|
||||
action: "Analyze market for {{topic}}"
|
||||
- agent: competitor_analyst
|
||||
action: "Analyze competitors for {{topic}}"
|
||||
|
||||
# Step 4: Loop over topics
|
||||
- agent: researcher
|
||||
action: "Research {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
# Step 5: Aggregate findings
|
||||
- agent: aggregator
|
||||
action: "Synthesize all findings into a comprehensive report"
|
||||
|
||||
# Step 6: Evaluator-optimizer
|
||||
- agent: evaluator
|
||||
action: "Evaluate the report quality"
|
||||
repeat:
|
||||
until: "approved"
|
||||
max_iterations: 2
|
||||
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
@@ -0,0 +1,43 @@
|
||||
# Evaluator-Optimizer Workflow
|
||||
# Demonstrates the repeat() pattern for iterative improvement
|
||||
|
||||
name: Evaluator Optimizer Workflow
|
||||
description: A generator creates content, an evaluator reviews it repeatedly until quality standards are met
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
generator:
|
||||
name: Generator
|
||||
role: Content Generator
|
||||
goal: Generate high-quality content
|
||||
instructions: |
|
||||
You are a content generator. Generate content based on the request.
|
||||
If feedback is provided, improve your content based on that feedback.
|
||||
Always strive for clarity, accuracy, and engagement.
|
||||
|
||||
evaluator:
|
||||
name: Evaluator
|
||||
role: Content Evaluator
|
||||
goal: Evaluate content quality and provide feedback
|
||||
instructions: |
|
||||
You are a content evaluator. Review the content provided.
|
||||
|
||||
If the content is high quality (clear, accurate, engaging), respond with:
|
||||
"APPROVED: [brief reason]"
|
||||
|
||||
If the content needs improvement, respond with:
|
||||
"NEEDS IMPROVEMENT: [specific feedback for improvement]"
|
||||
|
||||
Be constructive and specific in your feedback.
|
||||
|
||||
steps:
|
||||
- agent: generator
|
||||
action: "Generate content for: {{input}}"
|
||||
|
||||
- agent: evaluator
|
||||
action: "Evaluate this content and provide feedback: {{previous_output}}"
|
||||
repeat:
|
||||
until: "approved"
|
||||
max_iterations: 3
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Example usage of YAML Workflow Parser.
|
||||
|
||||
This script demonstrates how to load and execute YAML workflow files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure OPENAI_API_KEY is set
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
print("export OPENAI_API_KEY='your-api-key'")
|
||||
exit(1)
|
||||
|
||||
from praisonaiagents.workflows import YAMLWorkflowParser, WorkflowManager
|
||||
|
||||
|
||||
def example_1_parse_and_run():
|
||||
"""Example 1: Parse YAML and run workflow directly."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Parse YAML and run workflow")
|
||||
print("=" * 60)
|
||||
|
||||
yaml_content = """
|
||||
name: Quick Research
|
||||
description: A quick research workflow
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics
|
||||
instructions: "Provide brief, factual information."
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research: {{input}}"
|
||||
"""
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
print(f"Workflow Name: {workflow.name}")
|
||||
print(f"Steps: {len(workflow.steps)}")
|
||||
|
||||
# Run the workflow
|
||||
result = workflow.start("What is Python?")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_2_load_yaml_file():
|
||||
"""Example 2: Load workflow from YAML file using WorkflowManager."""
|
||||
print("=" * 60)
|
||||
print("Example 2: Load YAML file with WorkflowManager")
|
||||
print("=" * 60)
|
||||
|
||||
# Get the directory of this script
|
||||
script_dir = Path(__file__).parent
|
||||
yaml_file = script_dir / "simple_workflow.yaml"
|
||||
|
||||
if not yaml_file.exists():
|
||||
print(f"YAML file not found: {yaml_file}")
|
||||
return
|
||||
|
||||
manager = WorkflowManager()
|
||||
workflow = manager.load_yaml(yaml_file)
|
||||
|
||||
print(f"Loaded workflow: {workflow.name}")
|
||||
print(f"Steps: {len(workflow.steps)}")
|
||||
|
||||
# Run the workflow
|
||||
result = workflow.start("Benefits of renewable energy")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_3_execute_yaml_directly():
|
||||
"""Example 3: Execute YAML file directly with WorkflowManager."""
|
||||
print("=" * 60)
|
||||
print("Example 3: Execute YAML directly")
|
||||
print("=" * 60)
|
||||
|
||||
script_dir = Path(__file__).parent
|
||||
yaml_file = script_dir / "simple_workflow.yaml"
|
||||
|
||||
if not yaml_file.exists():
|
||||
print(f"YAML file not found: {yaml_file}")
|
||||
return
|
||||
|
||||
manager = WorkflowManager()
|
||||
result = manager.execute_yaml(
|
||||
yaml_file,
|
||||
input_data="The future of AI",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Status: {result['status']}")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_4_with_variables():
|
||||
"""Example 4: YAML workflow with variables."""
|
||||
print("=" * 60)
|
||||
print("Example 4: Workflow with variables")
|
||||
print("=" * 60)
|
||||
|
||||
yaml_content = """
|
||||
name: Variable Workflow
|
||||
variables:
|
||||
topic: Machine Learning
|
||||
style: concise
|
||||
|
||||
agents:
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Write content
|
||||
instructions: "Write {{style}} content about the topic."
|
||||
|
||||
steps:
|
||||
- agent: writer
|
||||
action: "Write about {{topic}}"
|
||||
"""
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
print(f"Variables: {workflow.variables}")
|
||||
|
||||
# Run with default variables
|
||||
result = workflow.start("")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("YAML Workflow Examples")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Run examples
|
||||
example_1_parse_and_run()
|
||||
example_2_load_yaml_file()
|
||||
example_3_execute_yaml_directly()
|
||||
example_4_with_variables()
|
||||
|
||||
print("=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Loop Workflow
|
||||
# Demonstrates the loop() pattern for iterating over items
|
||||
|
||||
name: Repetitive Processing Workflow
|
||||
description: An agent processes multiple items in a loop, then a summarizer combines results
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
variables:
|
||||
topics:
|
||||
- Artificial Intelligence
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
agents:
|
||||
processor:
|
||||
name: Processor
|
||||
role: Topic Processor
|
||||
goal: Process and analyze each topic thoroughly
|
||||
instructions: |
|
||||
You are a topic processor. For each topic given, provide:
|
||||
1. A brief definition
|
||||
2. Key concepts
|
||||
3. Real-world applications
|
||||
Keep your response concise but informative.
|
||||
|
||||
summarizer:
|
||||
name: Summarizer
|
||||
role: Results Summarizer
|
||||
goal: Summarize all processed results into a final report
|
||||
instructions: "You are a summarizer. Combine all the processed topics into a cohesive final report."
|
||||
|
||||
steps:
|
||||
- agent: processor
|
||||
action: "Analyze this topic: {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
- agent: summarizer
|
||||
action: "Create a comprehensive summary of all the analyzed topics based on these results: {{previous_output}}"
|
||||
@@ -0,0 +1,47 @@
|
||||
# Parallel Workflow
|
||||
# Demonstrates the parallel() pattern for concurrent execution
|
||||
|
||||
name: Parallel Research Workflow
|
||||
description: Multiple agents research different aspects concurrently, then an aggregator combines results
|
||||
input: "AI-powered customer service solutions" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
market_researcher:
|
||||
name: MarketResearcher
|
||||
role: Market Research Analyst
|
||||
goal: Research market trends and opportunities
|
||||
instructions: "You are a market research analyst. Analyze market trends and provide concise insights."
|
||||
|
||||
competitor_researcher:
|
||||
name: CompetitorResearcher
|
||||
role: Competitive Intelligence Analyst
|
||||
goal: Research competitor strategies and positioning
|
||||
instructions: "You are a competitive intelligence analyst. Analyze competitors and provide key insights."
|
||||
|
||||
customer_researcher:
|
||||
name: CustomerResearcher
|
||||
role: Customer Research Analyst
|
||||
goal: Research customer needs and behaviors
|
||||
instructions: "You are a customer research analyst. Analyze customer segments and provide insights."
|
||||
|
||||
aggregator:
|
||||
name: Aggregator
|
||||
role: Research Synthesizer
|
||||
goal: Synthesize all research findings into a comprehensive summary
|
||||
instructions: "You are a research synthesizer. Combine all research findings into a clear, comprehensive summary."
|
||||
|
||||
steps:
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: market_researcher
|
||||
action: "Research market trends for: {{input}}"
|
||||
- agent: competitor_researcher
|
||||
action: "Research competitors for: {{input}}"
|
||||
- agent: customer_researcher
|
||||
action: "Research customer needs for: {{input}}"
|
||||
|
||||
- agent: aggregator
|
||||
action: "Synthesize all the research findings into a comprehensive report: {{previous_output}}"
|
||||
@@ -0,0 +1,50 @@
|
||||
# Routing Workflow
|
||||
# Demonstrates the route() pattern for decision-based branching
|
||||
|
||||
name: Agentic Routing Workflow
|
||||
description: A classifier agent routes requests to specialized handler agents
|
||||
input: "Write a poem about the ocean" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
classifier:
|
||||
name: Classifier
|
||||
role: Request Classifier
|
||||
goal: Classify incoming requests by type
|
||||
instructions: |
|
||||
You are a request classifier. Analyze the request and respond with ONLY one word:
|
||||
- 'technical' for technical/coding questions
|
||||
- 'creative' for creative writing requests
|
||||
- 'general' for everything else
|
||||
|
||||
Respond with ONLY the classification word, nothing else.
|
||||
|
||||
tech_expert:
|
||||
name: TechExpert
|
||||
role: Technical Expert
|
||||
goal: Answer technical questions with expertise
|
||||
instructions: "You are a technical expert. Provide detailed, accurate technical answers."
|
||||
|
||||
creative_writer:
|
||||
name: CreativeWriter
|
||||
role: Creative Writer
|
||||
goal: Create engaging creative content
|
||||
instructions: "You are a creative writer. Write engaging, imaginative content."
|
||||
|
||||
general_assistant:
|
||||
name: GeneralAssistant
|
||||
role: General Assistant
|
||||
goal: Help with general requests
|
||||
instructions: "You are a helpful assistant. Provide clear, helpful responses."
|
||||
|
||||
steps:
|
||||
- agent: classifier
|
||||
action: "Classify this request: {{input}}"
|
||||
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [general_assistant]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Simple Sequential Workflow
|
||||
# Demonstrates basic agent-to-agent workflow
|
||||
|
||||
name: Simple Research Workflow
|
||||
description: A simple workflow where a researcher gathers info and a writer summarizes it
|
||||
input: "The benefits of renewable energy" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research and gather information about topics
|
||||
instructions: "You are a research analyst. Provide concise, factual information about the given topic."
|
||||
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Write clear and engaging summaries
|
||||
instructions: "You are a content writer. Take the research provided and write a clear, engaging summary."
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research the following topic: {{input}}"
|
||||
|
||||
- agent: writer
|
||||
action: "Write a summary based on this research: {{previous_output}}"
|
||||
@@ -87,3 +87,4 @@ graph.py
|
||||
chroma_db/
|
||||
.qodo
|
||||
*_COMPARISON_ANALYSIS.md
|
||||
*_REPORT.md
|
||||
File diff suppressed because it is too large.
Load diff
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../README.md
|
||||
@@ -0,0 +1,35 @@
|
||||
# PraisonAI Agents - Benchmark Results
|
||||
|
||||
**Generated:** 2025-12-18 14:40:22
|
||||
**Iterations:** 100
|
||||
**Test:** Agent instantiation (without tools)
|
||||
|
||||
## Results
|
||||
|
||||
| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
| **PraisonAI** | **3.77** | **1.00x (fastest)** |
|
||||
| OpenAI Agents SDK | 5.26 | 1.39x |
|
||||
| Agno | 5.64 | 1.49x |
|
||||
| PraisonAI (LiteLLM) | 7.56 | 2.00x |
|
||||
| PydanticAI | 226.94 | 60.16x |
|
||||
| LangGraph | 4,558.71 | 1,209x |
|
||||
| CrewAI | 15,607.92 | 4,138x |
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
| PydanticAI | 1.35.0 |
|
||||
| OpenAI Agents SDK | 0.6.3 |
|
||||
| LangGraph | 1.0.5 |
|
||||
| CrewAI | 1.6.1 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
python benchmarks/simple_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
# PraisonAI Agents - Real Execution Benchmark
|
||||
|
||||
**Generated:** 2025-12-18 14:43:52
|
||||
**Model:** gpt-4o-mini
|
||||
**Iterations:** 3
|
||||
**Prompt:** "What is 2+2? Reply with just the number."
|
||||
|
||||
## Results
|
||||
|
||||
| Framework | Method | Avg Time | Relative |
|
||||
|-----------|--------|----------|----------|
|
||||
| **PraisonAI** | `agent.start()` | **0.45s** | **1.00x (fastest)** |
|
||||
| PraisonAI (LiteLLM) | `agent.start()` | 0.55s | 1.22x |
|
||||
| CrewAI | `crew.kickoff()` | 0.58s | 1.28x |
|
||||
| Agno | `agent.run()` | 0.92s | 2.05x |
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
| CrewAI | 1.6.1 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_key
|
||||
cd praisonai-agents
|
||||
python benchmarks/execution_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# PraisonAI Agents - Real-World Execution Benchmark
|
||||
|
||||
**Generated:** 2025-12-18 14:33:49
|
||||
**Model:** gpt-4o-mini
|
||||
**Iterations:** 3 per test
|
||||
|
||||
## Results Summary
|
||||
|
||||
| Test | PraisonAI (avg) | Agno (avg) | Difference |
|
||||
|------|-----------------|------------|------------|
|
||||
| Single Agent | 2.04s | 3.18s | PraisonAI 1.56x faster |
|
||||
| 2 Agents | 2.49s | 20.49s | PraisonAI 8.21x faster |
|
||||
| 3 Agents | 6.52s | 13.97s | PraisonAI 2.14x faster |
|
||||
|
||||
## Detailed Results
|
||||
|
||||
### praisonai_single
|
||||
- Average: 2.04s
|
||||
- Min: 1.95s
|
||||
- Max: 2.19s
|
||||
- Std Dev: 0.13s
|
||||
- Times: ['2.19s', '1.95s', '2.00s']
|
||||
|
||||
### agno_single
|
||||
- Average: 3.18s
|
||||
- Min: 3.00s
|
||||
- Max: 3.51s
|
||||
- Std Dev: 0.28s
|
||||
- Times: ['3.51s', '3.00s', '3.04s']
|
||||
|
||||
### praisonai_two_agents
|
||||
- Average: 2.49s
|
||||
- Min: 2.22s
|
||||
- Max: 2.65s
|
||||
- Std Dev: 0.24s
|
||||
- Times: ['2.65s', '2.22s', '2.62s']
|
||||
|
||||
### agno_two_agents
|
||||
- Average: 20.49s
|
||||
- Min: 9.96s
|
||||
- Max: 35.05s
|
||||
- Std Dev: 13.02s
|
||||
- Times: ['16.46s', '9.96s', '35.05s']
|
||||
|
||||
### praisonai_three_agents
|
||||
- Average: 6.52s
|
||||
- Min: 5.62s
|
||||
- Max: 7.39s
|
||||
- Std Dev: 0.88s
|
||||
- Times: ['5.62s', '6.54s', '7.39s']
|
||||
|
||||
### agno_three_agents
|
||||
- Average: 13.97s
|
||||
- Min: 11.69s
|
||||
- Max: 15.32s
|
||||
- Std Dev: 1.99s
|
||||
- Times: ['14.91s', '11.69s', '15.32s']
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_key
|
||||
cd praisonai-agents
|
||||
python benchmarks/real_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# PraisonAI Agents - Benchmark Results
|
||||
|
||||
**Generated:** 2025-12-18 13:23:43
|
||||
|
||||
## Agent Instantiation Time
|
||||
|
||||
| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
| **PraisonAI** | **4.16** | **1.00x (fastest)** |
|
||||
| OpenAI Agents SDK | 5.55 | 1.33x |
|
||||
| Agno | 5.58 | 1.34x |
|
||||
| PraisonAI (LiteLLM) | 8.52 | 2.05x |
|
||||
| PydanticAI | 225.87 | 54.25x |
|
||||
| LangGraph | 4,349.54 | 1,045x |
|
||||
| CrewAI | 15,911.11 | 3,821x |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
python benchmarks/simple_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# PraisonAI Agents - Tools Benchmark Results
|
||||
|
||||
**Generated:** 2025-12-18 14:40:35
|
||||
**Iterations:** 100
|
||||
**Test:** Agent instantiation WITH TOOLS
|
||||
|
||||
## Results
|
||||
|
||||
| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
| **PraisonAI** | **3.24** | **1.00x (fastest)** |
|
||||
| Agno | 5.12 | 1.58x |
|
||||
| PraisonAI (LiteLLM) | 8.59 | 2.65x |
|
||||
| OpenAI Agents SDK | 279.95 | 86.44x |
|
||||
| LangGraph | 2,310.82 | 713x |
|
||||
| CrewAI | 15,773.44 | 4,870x |
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
| OpenAI Agents SDK | 0.6.3 |
|
||||
| LangGraph | 1.0.5 |
|
||||
| CrewAI | 1.6.1 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
python benchmarks/tools_benchmark.py
|
||||
```
|
||||
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Deep Profiling Benchmark
|
||||
|
||||
Profiles each step of agent initialization to identify performance bottlenecks.
|
||||
|
||||
Key Optimizations Applied:
|
||||
1. Lazy RulesManager - filesystem operations deferred until first access
|
||||
2. Lazy os.getcwd() - fast_context_path resolved only when needed
|
||||
3. Lazy console initialization - Rich Console created on demand
|
||||
|
||||
Usage:
|
||||
python benchmarks/deep_profiling.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import tracemalloc
|
||||
import cProfile
|
||||
import pstats
|
||||
import io
|
||||
from typing import Literal, Callable, Dict, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
# ============================================================================
|
||||
# Timing Utilities
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class StepTiming:
|
||||
name: str
|
||||
duration_us: float # microseconds
|
||||
memory_kb: float # kilobytes
|
||||
|
||||
|
||||
class StepProfiler:
|
||||
"""Profile individual steps during initialization"""
|
||||
|
||||
def __init__(self):
|
||||
self.steps: List[StepTiming] = []
|
||||
self._start_time = None
|
||||
self._start_memory = None
|
||||
|
||||
def start_step(self, name: str):
|
||||
tracemalloc.start()
|
||||
self._start_time = time.perf_counter()
|
||||
self._step_name = name
|
||||
|
||||
def end_step(self):
|
||||
end_time = time.perf_counter()
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
duration_us = (end_time - self._start_time) * 1_000_000
|
||||
memory_kb = peak / 1024
|
||||
|
||||
self.steps.append(StepTiming(
|
||||
name=self._step_name,
|
||||
duration_us=duration_us,
|
||||
memory_kb=memory_kb
|
||||
))
|
||||
|
||||
def print_report(self, title: str):
|
||||
print(f"\n{'='*70}")
|
||||
print(f"STEP-BY-STEP PROFILING: {title}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
total_time = sum(s.duration_us for s in self.steps)
|
||||
total_memory = sum(s.memory_kb for s in self.steps)
|
||||
|
||||
print(f"\n{'Step':<40} {'Time (μs)':<15} {'Memory (KB)':<15} {'% Time':<10}")
|
||||
print("-" * 80)
|
||||
|
||||
for step in sorted(self.steps, key=lambda x: x.duration_us, reverse=True):
|
||||
pct = (step.duration_us / total_time * 100) if total_time > 0 else 0
|
||||
print(f"{step.name:<40} {step.duration_us:<15.2f} {step.memory_kb:<15.2f} {pct:<10.1f}%")
|
||||
|
||||
print("-" * 80)
|
||||
print(f"{'TOTAL':<40} {total_time:<15.2f} {total_memory:<15.2f} {'100.0':<10}%")
|
||||
|
||||
|
||||
def time_function(func: Callable, iterations: int = 100) -> Dict:
|
||||
"""Time a function with detailed statistics"""
|
||||
times = []
|
||||
|
||||
# Warmup
|
||||
for _ in range(10):
|
||||
func()
|
||||
|
||||
# Measure
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
func()
|
||||
end = time.perf_counter()
|
||||
times.append((end - start) * 1_000_000) # Convert to microseconds
|
||||
|
||||
return {
|
||||
'avg': sum(times) / len(times),
|
||||
'min': min(times),
|
||||
'max': max(times),
|
||||
'median': sorted(times)[len(times)//2]
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile PraisonAI Agent Initialization - Step by Step
|
||||
# ============================================================================
|
||||
|
||||
def profile_praisonai_detailed():
|
||||
"""Profile each step of PraisonAI Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("DETAILED PROFILING: PraisonAI Agent")
|
||||
print("="*70)
|
||||
|
||||
profiler = StepProfiler()
|
||||
|
||||
# Step 1: Import
|
||||
profiler.start_step("1. Import praisonaiagents")
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
profiler.end_step()
|
||||
|
||||
# Step 2: Define tool
|
||||
profiler.start_step("2. Define tool function")
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
profiler.end_step()
|
||||
|
||||
# Step 3: Full instantiation
|
||||
profiler.start_step("3. Full Agent instantiation")
|
||||
agent = PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
profiler.end_step()
|
||||
|
||||
profiler.print_report("PraisonAI Agent")
|
||||
|
||||
# Now profile the __init__ internals using cProfile
|
||||
print("\n" + "="*70)
|
||||
print("cProfile ANALYSIS: PraisonAI Agent.__init__")
|
||||
print("="*70)
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(100):
|
||||
agent = PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
pr.disable()
|
||||
|
||||
# Print top 30 functions by cumulative time
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile Agno Agent Initialization - Step by Step
|
||||
# ============================================================================
|
||||
|
||||
def profile_agno_detailed():
|
||||
"""Profile each step of Agno Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("DETAILED PROFILING: Agno Agent")
|
||||
print("="*70)
|
||||
|
||||
profiler = StepProfiler()
|
||||
|
||||
# Step 1: Import
|
||||
profiler.start_step("1. Import agno")
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
profiler.end_step()
|
||||
|
||||
# Step 2: Define tool
|
||||
profiler.start_step("2. Define tool function")
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
profiler.end_step()
|
||||
|
||||
# Step 3: Full instantiation
|
||||
profiler.start_step("3. Full Agent instantiation")
|
||||
agent = AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
profiler.end_step()
|
||||
|
||||
profiler.print_report("Agno Agent")
|
||||
|
||||
# Now profile the __init__ internals using cProfile
|
||||
print("\n" + "="*70)
|
||||
print("cProfile ANALYSIS: Agno Agent.__init__")
|
||||
print("="*70)
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(100):
|
||||
agent = AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
pr.disable()
|
||||
|
||||
# Print top 30 functions by cumulative time
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile Individual Components of PraisonAI
|
||||
# ============================================================================
|
||||
|
||||
def profile_praisonai_components():
|
||||
"""Profile individual components of PraisonAI Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("COMPONENT-LEVEL PROFILING: PraisonAI Agent")
|
||||
print("="*70)
|
||||
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
|
||||
# Test different configurations
|
||||
configs = [
|
||||
("Minimal (no tools, no llm)", {"name": "Test", "verbose": False}),
|
||||
("With name only", {"name": "Test Agent", "verbose": False}),
|
||||
("With instructions", {"name": "Test", "instructions": "Be concise", "verbose": False}),
|
||||
("With llm (gpt-4o-mini)", {"name": "Test", "llm": "gpt-4o-mini", "verbose": False}),
|
||||
("With llm (openai/gpt-4o-mini)", {"name": "Test", "llm": "openai/gpt-4o-mini", "verbose": False}),
|
||||
("With tools", {"name": "Test", "tools": tools, "verbose": False}),
|
||||
("Full config (OpenAI SDK)", {"name": "Test", "instructions": "Be concise", "llm": "gpt-4o-mini", "tools": tools, "verbose": False}),
|
||||
("Full config (LiteLLM)", {"name": "Test", "instructions": "Be concise", "llm": "openai/gpt-4o-mini", "tools": tools, "verbose": False}),
|
||||
]
|
||||
|
||||
print(f"\n{'Configuration':<40} {'Avg Time (μs)':<15} {'Min (μs)':<15} {'Max (μs)':<15}")
|
||||
print("-" * 85)
|
||||
|
||||
for name, config in configs:
|
||||
def create_agent():
|
||||
return PraisonAgent(**config)
|
||||
|
||||
stats = time_function(create_agent, iterations=100)
|
||||
print(f"{name:<40} {stats['avg']:<15.2f} {stats['min']:<15.2f} {stats['max']:<15.2f}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Compare Specific Operations
|
||||
# ============================================================================
|
||||
|
||||
def compare_specific_operations():
|
||||
"""Compare specific operations between PraisonAI and Agno"""
|
||||
print("\n" + "="*70)
|
||||
print("OPERATION COMPARISON: PraisonAI vs Agno")
|
||||
print("="*70)
|
||||
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
|
||||
operations = []
|
||||
|
||||
# Operation 1: Bare minimum instantiation
|
||||
def praisonai_minimal():
|
||||
return PraisonAgent(name="Test", verbose=False)
|
||||
|
||||
def agno_minimal():
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"))
|
||||
|
||||
praison_stats = time_function(praisonai_minimal, 100)
|
||||
agno_stats = time_function(agno_minimal, 100)
|
||||
operations.append(("Minimal instantiation", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
# Operation 2: With tools
|
||||
def praisonai_with_tools():
|
||||
return PraisonAgent(name="Test", tools=tools, verbose=False)
|
||||
|
||||
def agno_with_tools():
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
praison_stats = time_function(praisonai_with_tools, 100)
|
||||
agno_stats = time_function(agno_with_tools, 100)
|
||||
operations.append(("With tools", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
# Operation 3: Full config
|
||||
def praisonai_full():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
def agno_full():
|
||||
return AgnoAgent(
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
tools=tools,
|
||||
instructions=["Be concise"]
|
||||
)
|
||||
|
||||
praison_stats = time_function(praisonai_full, 100)
|
||||
agno_stats = time_function(agno_full, 100)
|
||||
operations.append(("Full config", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
print(f"\n{'Operation':<30} {'PraisonAI (μs)':<18} {'Agno (μs)':<18} {'Ratio':<10}")
|
||||
print("-" * 76)
|
||||
|
||||
for op_name, praison_time, agno_time in operations:
|
||||
ratio = praison_time / agno_time if agno_time > 0 else 0
|
||||
print(f"{op_name:<30} {praison_time:<18.2f} {agno_time:<18.2f} {ratio:<10.1f}x")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
os.environ.setdefault("OPENAI_API_KEY", "sk-test")
|
||||
|
||||
print("="*70)
|
||||
print("PraisonAI Agents - Deep Profiling Benchmark")
|
||||
print("="*70)
|
||||
print("\nThis analysis identifies performance bottlenecks in agent initialization.\n")
|
||||
|
||||
# Run all profiling
|
||||
profile_praisonai_detailed()
|
||||
profile_agno_detailed()
|
||||
profile_praisonai_components()
|
||||
compare_specific_operations()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("PROFILING COMPLETE")
|
||||
print("="*70)
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Real Execution Benchmark
|
||||
|
||||
Benchmarks ACTUAL agent execution with LLM API calls.
|
||||
Tests single agent running a simple task across frameworks.
|
||||
|
||||
Usage:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python benchmarks/execution_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
# Check API key
|
||||
if not os.environ.get('OPENAI_API_KEY'):
|
||||
print("ERROR: Set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
MODEL = "gpt-4o-mini"
|
||||
ITERATIONS = 3
|
||||
PROMPT = "What is 2+2? Reply with just the number."
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
"""Get version numbers for benchmarked packages."""
|
||||
packages = {
|
||||
'PraisonAI': 'praisonaiagents',
|
||||
'Agno': 'agno',
|
||||
'CrewAI': 'crewai',
|
||||
}
|
||||
versions = {}
|
||||
for name, pkg in packages.items():
|
||||
try:
|
||||
versions[name] = get_version(pkg)
|
||||
except:
|
||||
versions[name] = 'not installed'
|
||||
return versions
|
||||
|
||||
|
||||
def benchmark_praisonai():
|
||||
"""Benchmark PraisonAI agent execution using agent.start()"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("\n--- PraisonAI ---")
|
||||
print(f"Method: agent.start()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
agent = Agent(
|
||||
name="Calculator",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
llm=MODEL,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start(PROMPT)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
|
||||
|
||||
avg = sum(times) / len(times)
|
||||
print(f" Average: {avg:.2f}s")
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_praisonai_litellm():
|
||||
"""Benchmark PraisonAI with LiteLLM backend using agent.start()"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("\n--- PraisonAI (LiteLLM) ---")
|
||||
print(f"Method: agent.start()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
agent = Agent(
|
||||
name="Calculator",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
llm=f"openai/{MODEL}",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start(PROMPT)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
|
||||
|
||||
avg = sum(times) / len(times)
|
||||
print(f" Average: {avg:.2f}s")
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_agno():
|
||||
"""Benchmark Agno agent execution using agent.run()"""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
print("\n--- Agno ---")
|
||||
print(f"Method: agent.run()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id=MODEL),
|
||||
instructions=["You are a helpful assistant. Be very brief."],
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.run(PROMPT)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
# Extract content from response
|
||||
content = str(response.content)[:50] if hasattr(response, 'content') else str(response)[:50]
|
||||
print(f" Run {i+1}: {elapsed:.2f}s - Response: {content}")
|
||||
|
||||
avg = sum(times) / len(times)
|
||||
print(f" Average: {avg:.2f}s")
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_crewai():
|
||||
"""Benchmark CrewAI agent execution using crew.kickoff()"""
|
||||
from crewai import Agent, Task, Crew
|
||||
|
||||
print("\n--- CrewAI ---")
|
||||
print(f"Method: crew.kickoff()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
agent = Agent(
|
||||
role="Calculator",
|
||||
goal="Answer math questions",
|
||||
backstory="You are a helpful assistant.",
|
||||
llm=MODEL,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description=PROMPT,
|
||||
expected_output="A number",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = crew.kickoff()
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
|
||||
|
||||
avg = sum(times) / len(times)
|
||||
print(f" Average: {avg:.2f}s")
|
||||
return avg
|
||||
|
||||
|
||||
def save_results(results: dict):
|
||||
"""Save results to markdown file."""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
filepath = os.path.join(os.path.dirname(__file__), 'EXECUTION_BENCHMARK_RESULTS.md')
|
||||
versions = get_package_versions()
|
||||
|
||||
# Sort by time
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1])
|
||||
fastest = sorted_results[0][1]
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Real Execution Benchmark\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write(f'**Model:** {MODEL}\n')
|
||||
f.write(f'**Iterations:** {ITERATIONS}\n')
|
||||
f.write(f'**Prompt:** "{PROMPT}"\n\n')
|
||||
f.write('## Results\n\n')
|
||||
f.write('| Framework | Method | Avg Time | Relative |\n')
|
||||
f.write('|-----------|--------|----------|----------|\n')
|
||||
|
||||
methods = {
|
||||
'PraisonAI': 'agent.start()',
|
||||
'PraisonAI (LiteLLM)': 'agent.start()',
|
||||
'Agno': 'agent.run()',
|
||||
'CrewAI': 'crew.kickoff()',
|
||||
}
|
||||
|
||||
for name, avg in sorted_results:
|
||||
ratio = avg / fastest
|
||||
method = methods.get(name, 'unknown')
|
||||
if ratio == 1.0:
|
||||
f.write(f'| **{name}** | `{method}` | **{avg:.2f}s** | **1.00x (fastest)** |\n')
|
||||
else:
|
||||
f.write(f'| {name} | `{method}` | {avg:.2f}s | {ratio:.2f}x |\n')
|
||||
|
||||
f.write('\n## Package Versions\n\n')
|
||||
f.write('| Package | Version |\n')
|
||||
f.write('|---------|--------|\n')
|
||||
for pkg, ver in versions.items():
|
||||
f.write(f'| {pkg} | {ver} |\n')
|
||||
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('export OPENAI_API_KEY=your_key\n')
|
||||
f.write('cd praisonai-agents\n')
|
||||
f.write('python benchmarks/execution_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('=' * 60)
|
||||
print('PraisonAI Agents - Real Execution Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nModel: {MODEL}')
|
||||
print(f'Iterations: {ITERATIONS}')
|
||||
print(f'Prompt: "{PROMPT}"')
|
||||
|
||||
versions = get_package_versions()
|
||||
print(f'\nPackage versions:')
|
||||
for pkg, ver in versions.items():
|
||||
print(f' {pkg}: {ver}')
|
||||
|
||||
results = {}
|
||||
|
||||
# Run benchmarks
|
||||
try:
|
||||
results['PraisonAI'] = benchmark_praisonai()
|
||||
except Exception as e:
|
||||
print(f"PraisonAI error: {e}")
|
||||
|
||||
try:
|
||||
results['PraisonAI (LiteLLM)'] = benchmark_praisonai_litellm()
|
||||
except Exception as e:
|
||||
print(f"PraisonAI (LiteLLM) error: {e}")
|
||||
|
||||
try:
|
||||
results['Agno'] = benchmark_agno()
|
||||
except Exception as e:
|
||||
print(f"Agno error: {e}")
|
||||
|
||||
try:
|
||||
results['CrewAI'] = benchmark_crewai()
|
||||
except Exception as e:
|
||||
print(f"CrewAI error: {e}")
|
||||
|
||||
# Summary
|
||||
print('\n' + '=' * 60)
|
||||
print('SUMMARY')
|
||||
print('=' * 60)
|
||||
|
||||
if results:
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1])
|
||||
fastest = sorted_results[0][1]
|
||||
|
||||
print(f"\n{'Framework':<25} {'Avg Time':<12} {'Relative':<10}")
|
||||
print('-' * 47)
|
||||
|
||||
for name, avg in sorted_results:
|
||||
ratio = avg / fastest
|
||||
print(f'{name:<25} {avg:<12.2f}s {ratio:.2f}x')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
|
||||
# Save results
|
||||
if results:
|
||||
save_results(results)
|
||||
@@ -0,0 +1,523 @@
|
||||
"""
|
||||
PraisonAI Agents - Comprehensive Performance Benchmark
|
||||
|
||||
Measures agent instantiation time and memory footprint across
|
||||
multiple AI agent frameworks using industry-standard methodology.
|
||||
|
||||
Usage:
|
||||
python benchmarks/performance_benchmark.py
|
||||
"""
|
||||
|
||||
import gc
|
||||
import time
|
||||
import tracemalloc
|
||||
import statistics
|
||||
from typing import Literal, List, Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceResult:
|
||||
"""Performance measurement results"""
|
||||
run_times: List[float] = field(default_factory=list)
|
||||
memory_usages: List[float] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def avg_run_time(self) -> float:
|
||||
return statistics.mean(self.run_times) if self.run_times else 0
|
||||
|
||||
@property
|
||||
def avg_memory_usage(self) -> float:
|
||||
return statistics.mean(self.memory_usages) if self.memory_usages else 0
|
||||
|
||||
@property
|
||||
def min_run_time(self) -> float:
|
||||
return min(self.run_times) if self.run_times else 0
|
||||
|
||||
@property
|
||||
def max_run_time(self) -> float:
|
||||
return max(self.run_times) if self.run_times else 0
|
||||
|
||||
@property
|
||||
def std_dev_run_time(self) -> float:
|
||||
return statistics.stdev(self.run_times) if len(self.run_times) > 1 else 0
|
||||
|
||||
@property
|
||||
def median_run_time(self) -> float:
|
||||
return statistics.median(self.run_times) if self.run_times else 0
|
||||
|
||||
|
||||
def compute_tracemalloc_baseline(samples: int = 3) -> float:
|
||||
"""Compute baseline memory usage for tracemalloc"""
|
||||
def empty_func():
|
||||
return
|
||||
|
||||
results = []
|
||||
for _ in range(samples):
|
||||
gc.collect()
|
||||
tracemalloc.start()
|
||||
empty_func()
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
results.append(peak / 1024 / 1024) # Convert to MiB
|
||||
|
||||
return sum(results) / len(results) if results else 0
|
||||
|
||||
|
||||
def measure_time(func: Callable, iterations: int = 1000, warmup: int = 10) -> List[float]:
|
||||
"""Measure execution time for a function"""
|
||||
# Warmup runs
|
||||
for _ in range(warmup):
|
||||
func()
|
||||
|
||||
# Measured runs
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
func()
|
||||
end = time.perf_counter()
|
||||
times.append(end - start)
|
||||
|
||||
return times
|
||||
|
||||
|
||||
def measure_memory(func: Callable, iterations: int = 1000, warmup: int = 10) -> List[float]:
|
||||
"""Measure memory usage for a function"""
|
||||
baseline = compute_tracemalloc_baseline()
|
||||
|
||||
# Warmup runs
|
||||
for _ in range(warmup):
|
||||
func()
|
||||
|
||||
# Measured runs
|
||||
memory_usages = []
|
||||
for _ in range(iterations):
|
||||
gc.collect()
|
||||
tracemalloc.start()
|
||||
func()
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
peak_mib = peak / 1024 / 1024
|
||||
adjusted = max(0, peak_mib - baseline)
|
||||
memory_usages.append(adjusted)
|
||||
|
||||
return memory_usages
|
||||
|
||||
|
||||
def run_benchmark(func: Callable, name: str, iterations: int = 1000, warmup: int = 10) -> PerformanceResult:
|
||||
"""Run full benchmark for a function"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Benchmarking: {name}")
|
||||
print(f"{'='*60}")
|
||||
print(f"Warmup runs: {warmup}")
|
||||
print(f"Measured iterations: {iterations}")
|
||||
|
||||
print("\nMeasuring runtime...")
|
||||
run_times = measure_time(func, iterations, warmup)
|
||||
|
||||
print("Measuring memory...")
|
||||
memory_usages = measure_memory(func, iterations, warmup)
|
||||
|
||||
result = PerformanceResult(run_times=run_times, memory_usages=memory_usages)
|
||||
|
||||
print(f"\nResults for {name}:")
|
||||
print(f" Average Time: {result.avg_run_time:.6f} seconds ({result.avg_run_time * 1_000_000:.2f} μs)")
|
||||
print(f" Min Time: {result.min_run_time:.6f} seconds")
|
||||
print(f" Max Time: {result.max_run_time:.6f} seconds")
|
||||
print(f" Std Dev: {result.std_dev_run_time:.6f} seconds")
|
||||
print(f" Median Time: {result.median_run_time:.6f} seconds")
|
||||
print(f" Avg Memory: {result.avg_memory_usage:.6f} MiB ({result.avg_memory_usage * 1024:.2f} KiB)")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Framework Imports
|
||||
# ============================================================================
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
# Additional frameworks for comparison
|
||||
try:
|
||||
from pydantic_ai import Agent as PydanticAgent
|
||||
PYDANTIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYDANTIC_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from crewai.agent import Agent as CrewAgent
|
||||
from crewai.tools import tool as crewai_tool
|
||||
CREWAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
CREWAI_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from langchain_core.tools import tool as langchain_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
LANGGRAPH_AVAILABLE = True
|
||||
except ImportError:
|
||||
LANGGRAPH_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from agents import Agent as OpenAIAgent, function_tool
|
||||
OPENAI_AGENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
OPENAI_AGENTS_AVAILABLE = False
|
||||
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
|
||||
|
||||
tools = [get_weather]
|
||||
|
||||
|
||||
def instantiate_praisonai_agent():
|
||||
"""Instantiate a PraisonAI agent with one tool."""
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
|
||||
def instantiate_praisonai_litellm_agent():
|
||||
"""Instantiate a PraisonAI agent with LiteLLM backend."""
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="openai/gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
|
||||
def instantiate_agno_agent():
|
||||
"""Instantiate an Agno agent with one tool."""
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
|
||||
def instantiate_pydantic_agent():
|
||||
"""Instantiate a PydanticAI agent with one tool."""
|
||||
agent = PydanticAgent("openai:gpt-4o-mini", system_prompt="Be concise, reply with one sentence.")
|
||||
@agent.tool_plain
|
||||
def get_weather_pydantic(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
return agent
|
||||
|
||||
|
||||
def instantiate_openai_agents():
|
||||
"""Instantiate an OpenAI Agents SDK agent with one tool."""
|
||||
return OpenAIAgent(
|
||||
name="Test agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
model="gpt-4o-mini",
|
||||
tools=[function_tool(get_weather)],
|
||||
)
|
||||
|
||||
|
||||
def instantiate_langgraph_agent():
|
||||
"""Instantiate a LangGraph agent with one tool."""
|
||||
@langchain_tool
|
||||
def get_weather_lg(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
return create_react_agent(model=ChatOpenAI(model="gpt-4o-mini"), tools=[get_weather_lg])
|
||||
|
||||
|
||||
def instantiate_crewai_agent():
|
||||
"""Instantiate a CrewAI agent with one tool."""
|
||||
@crewai_tool("Weather Tool")
|
||||
def get_weather_crew(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
return CrewAgent(
|
||||
llm="gpt-4o-mini",
|
||||
role="Test Agent",
|
||||
goal="Be concise, reply with one sentence.",
|
||||
tools=[get_weather_crew],
|
||||
backstory="Test",
|
||||
)
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
"""Get version numbers for all benchmarked packages."""
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
packages = {
|
||||
'PraisonAI': 'praisonaiagents',
|
||||
'Agno': 'agno',
|
||||
'PydanticAI': 'pydantic-ai',
|
||||
'OpenAI Agents SDK': 'openai-agents',
|
||||
'LangGraph': 'langgraph',
|
||||
'CrewAI': 'crewai'
|
||||
}
|
||||
|
||||
versions = {}
|
||||
for display_name, pkg_name in packages.items():
|
||||
try:
|
||||
versions[display_name] = get_version(pkg_name)
|
||||
except Exception:
|
||||
versions[display_name] = 'not installed'
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def save_benchmark_results(results: dict, baseline):
|
||||
"""Save benchmark results to BENCHMARK_RESULTS.md and update README.md"""
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
benchmarks_dir = os.path.dirname(__file__)
|
||||
filepath = os.path.join(benchmarks_dir, 'BENCHMARK_RESULTS.md')
|
||||
|
||||
# Get package versions
|
||||
versions = get_package_versions()
|
||||
|
||||
display_names = {
|
||||
'agno': 'Agno',
|
||||
'praisonai': 'PraisonAI',
|
||||
'praisonai_litellm': 'PraisonAI (LiteLLM)',
|
||||
'pydantic': 'PydanticAI',
|
||||
'openai_agents': 'OpenAI Agents SDK',
|
||||
'langgraph': 'LangGraph',
|
||||
'crewai': 'CrewAI'
|
||||
}
|
||||
|
||||
# Build table rows
|
||||
table_rows = []
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1].avg_run_time)
|
||||
|
||||
for name, result in sorted_results:
|
||||
display_name = display_names.get(name, name)
|
||||
avg_us = result.avg_run_time * 1e6
|
||||
ratio = result.avg_run_time / baseline.avg_run_time if baseline.avg_run_time > 0 else 0
|
||||
|
||||
if name == 'praisonai':
|
||||
table_rows.append(f'| **{display_name}** | **{avg_us:.2f}** | **1.00x (fastest)** |')
|
||||
else:
|
||||
avg_str = f'{avg_us:,.2f}' if avg_us >= 1000 else f'{avg_us:.2f}'
|
||||
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
|
||||
table_rows.append(f'| {display_name} | {avg_str} | {ratio_str} |')
|
||||
|
||||
# Save to BENCHMARK_RESULTS.md
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Benchmark Results\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write('**Methodology:** 10 warmup runs + 1000 measured iterations\n\n')
|
||||
f.write('## Agent Instantiation Time\n\n')
|
||||
f.write('| Framework | Avg Time (μs) | Relative |\n')
|
||||
f.write('|-----------|---------------|----------|\n')
|
||||
f.write('\n'.join(table_rows) + '\n')
|
||||
f.write('\n## Package Versions\n\n')
|
||||
f.write('| Package | Version |\n')
|
||||
f.write('|---------|--------|\n')
|
||||
for pkg, ver in versions.items():
|
||||
f.write(f'| {pkg} | {ver} |\n')
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('cd praisonai-agents\n')
|
||||
f.write('python benchmarks/performance_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
|
||||
# Update README.md
|
||||
readme_path = os.path.join(benchmarks_dir, '..', '..', '..', 'README.md')
|
||||
if os.path.exists(readme_path):
|
||||
with open(readme_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
new_table = '''| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
''' + '\n'.join(table_rows)
|
||||
|
||||
pattern = r'(\| Framework \| Avg Time \(μs\) \| Relative \|\n\|[-|]+\|\n)(\|[^\n]+\|\n)+'
|
||||
|
||||
if re.search(pattern, content):
|
||||
content = re.sub(pattern, new_table + '\n', content)
|
||||
|
||||
with open(readme_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'README.md updated: {readme_path}')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*70)
|
||||
print("PraisonAI Agents - Comprehensive Performance Benchmark")
|
||||
print("="*70)
|
||||
print("\nThis benchmark measures:")
|
||||
print(" 1. Agent instantiation time (how fast an agent object is created)")
|
||||
print(" 2. Memory footprint (how much memory an agent uses)")
|
||||
print("\nMethodology:")
|
||||
print(" - 10 warmup runs (not measured)")
|
||||
print(" - 1000 measured iterations")
|
||||
print(" - tracemalloc for memory measurement")
|
||||
print(" - Baseline subtraction for accuracy")
|
||||
print("\nFrameworks available:")
|
||||
print(" - PraisonAI: ✅")
|
||||
print(f" - Agno: ✅")
|
||||
print(f" - PydanticAI: {'✅' if PYDANTIC_AVAILABLE else '❌'}")
|
||||
print(f" - OpenAI Agents SDK: {'✅' if OPENAI_AGENTS_AVAILABLE else '❌'}")
|
||||
print(f" - LangGraph: {'✅' if LANGGRAPH_AVAILABLE else '❌'}")
|
||||
print(f" - CrewAI: {'✅' if CREWAI_AVAILABLE else '❌'}")
|
||||
|
||||
results = {}
|
||||
|
||||
# Test PraisonAI first
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing PraisonAI Agents")
|
||||
print("="*70)
|
||||
results['praisonai'] = run_benchmark(
|
||||
instantiate_praisonai_agent,
|
||||
"PraisonAI",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing PraisonAI: {e}")
|
||||
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing PraisonAI Agents (LiteLLM)")
|
||||
print("="*70)
|
||||
results['praisonai_litellm'] = run_benchmark(
|
||||
instantiate_praisonai_litellm_agent,
|
||||
"PraisonAI (LiteLLM)",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing PraisonAI (LiteLLM): {e}")
|
||||
|
||||
# Test Agno
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing Agno")
|
||||
print("="*70)
|
||||
results['agno'] = run_benchmark(
|
||||
instantiate_agno_agent,
|
||||
"Agno",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing Agno: {e}")
|
||||
|
||||
# Test PydanticAI
|
||||
if PYDANTIC_AVAILABLE:
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing PydanticAI")
|
||||
print("="*70)
|
||||
results['pydantic'] = run_benchmark(
|
||||
instantiate_pydantic_agent,
|
||||
"PydanticAI Agent Instantiation",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing PydanticAI: {e}")
|
||||
|
||||
# Test OpenAI Agents SDK
|
||||
if OPENAI_AGENTS_AVAILABLE:
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing OpenAI Agents SDK")
|
||||
print("="*70)
|
||||
results['openai_agents'] = run_benchmark(
|
||||
instantiate_openai_agents,
|
||||
"OpenAI Agents SDK Instantiation",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing OpenAI Agents SDK: {e}")
|
||||
|
||||
# Test LangGraph
|
||||
if LANGGRAPH_AVAILABLE:
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing LangGraph")
|
||||
print("="*70)
|
||||
results['langgraph'] = run_benchmark(
|
||||
instantiate_langgraph_agent,
|
||||
"LangGraph Agent Instantiation",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing LangGraph: {e}")
|
||||
|
||||
# Test CrewAI
|
||||
if CREWAI_AVAILABLE:
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing CrewAI")
|
||||
print("="*70)
|
||||
results['crewai'] = run_benchmark(
|
||||
instantiate_crewai_agent,
|
||||
"CrewAI Agent Instantiation",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing CrewAI: {e}")
|
||||
|
||||
# Final Comparison Table
|
||||
print("\n" + "="*70)
|
||||
print("RESULTS SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
if 'praisonai' in results:
|
||||
baseline = results['praisonai']
|
||||
|
||||
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Avg Memory (KiB)':<18} {'Relative':<15}")
|
||||
print("-" * 73)
|
||||
|
||||
# Sort by time
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1].avg_run_time)
|
||||
|
||||
for name, result in sorted_results:
|
||||
time_ratio = result.avg_run_time / baseline.avg_run_time if baseline.avg_run_time > 0 else 0
|
||||
|
||||
time_str = f"{time_ratio:.2f}x"
|
||||
|
||||
display_name = {
|
||||
'agno': 'Agno',
|
||||
'praisonai': 'PraisonAI',
|
||||
'praisonai_litellm': 'PraisonAI (LiteLLM)',
|
||||
'pydantic': 'PydanticAI',
|
||||
'openai_agents': 'OpenAI Agents SDK',
|
||||
'langgraph': 'LangGraph',
|
||||
'crewai': 'CrewAI'
|
||||
}.get(name, name)
|
||||
|
||||
print(f"{display_name:<25} {result.avg_run_time*1e6:<15.2f} {result.avg_memory_usage*1024:<18.2f} {time_str:<15}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
|
||||
# Save results to files
|
||||
save_benchmark_results(results, baseline)
|
||||
@@ -0,0 +1,481 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Real-World Execution Benchmark
|
||||
|
||||
Benchmarks actual agent execution with LLM calls across frameworks.
|
||||
Tests single agent, 2-agent, and 3-agent workflows.
|
||||
|
||||
Usage:
|
||||
python benchmarks/real_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import statistics
|
||||
import os
|
||||
from typing import Literal, List
|
||||
from dataclasses import dataclass, field
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get('OPENAI_API_KEY'):
|
||||
print("ERROR: OPENAI_API_KEY environment variable not set")
|
||||
exit(1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""Execution benchmark results"""
|
||||
times: List[float] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def avg_time(self) -> float:
|
||||
return statistics.mean(self.times) if self.times else 0
|
||||
|
||||
@property
|
||||
def min_time(self) -> float:
|
||||
return min(self.times) if self.times else 0
|
||||
|
||||
@property
|
||||
def max_time(self) -> float:
|
||||
return max(self.times) if self.times else 0
|
||||
|
||||
@property
|
||||
def std_dev(self) -> float:
|
||||
return statistics.stdev(self.times) if len(self.times) > 1 else 0
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
"""Get version numbers for benchmarked packages."""
|
||||
packages = {
|
||||
'PraisonAI': 'praisonaiagents',
|
||||
'Agno': 'agno',
|
||||
}
|
||||
versions = {}
|
||||
for display_name, pkg_name in packages.items():
|
||||
try:
|
||||
versions[display_name] = get_version(pkg_name)
|
||||
except Exception:
|
||||
versions[display_name] = 'not installed'
|
||||
return versions
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tool Definitions
|
||||
# ============================================================================
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]) -> str:
|
||||
"""Get weather information for a city."""
|
||||
if city == "nyc":
|
||||
return "New York: Cloudy, 45°F, 60% humidity"
|
||||
return "San Francisco: Sunny, 72°F, 40% humidity"
|
||||
|
||||
|
||||
def get_time(timezone: Literal["est", "pst"]) -> str:
|
||||
"""Get current time in a timezone."""
|
||||
if timezone == "est":
|
||||
return "Eastern Time: 2:30 PM"
|
||||
return "Pacific Time: 11:30 AM"
|
||||
|
||||
|
||||
def calculate(expression: str) -> str:
|
||||
"""Calculate a math expression."""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return f"Result: {result}"
|
||||
except Exception:
|
||||
return "Error: Invalid expression"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PraisonAI Benchmarks
|
||||
# ============================================================================
|
||||
|
||||
def benchmark_praisonai_single(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark single PraisonAI agent execution."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" PraisonAI single agent iteration {i+1}/{iterations}...")
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_weather],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start("What's the weather in NYC? Reply in one sentence.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_praisonai_two_agents(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark 2 PraisonAI agents working together."""
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" PraisonAI 2-agent iteration {i+1}/{iterations}...")
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Assistant",
|
||||
goal="Gather weather information",
|
||||
instructions="Get weather data and pass to reporter. Be brief.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_weather],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
reporter = Agent(
|
||||
name="Reporter",
|
||||
role="News Reporter",
|
||||
goal="Create weather report",
|
||||
instructions="Create a one-sentence weather summary.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, reporter],
|
||||
process="sequential",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agents.start("Get NYC weather and create a brief report.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_praisonai_three_agents(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark 3 PraisonAI agents working together."""
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" PraisonAI 3-agent iteration {i+1}/{iterations}...")
|
||||
|
||||
weather_agent = Agent(
|
||||
name="WeatherAgent",
|
||||
role="Weather Specialist",
|
||||
goal="Get weather data",
|
||||
instructions="Fetch weather information. Be brief.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_weather],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
time_agent = Agent(
|
||||
name="TimeAgent",
|
||||
role="Time Specialist",
|
||||
goal="Get time information",
|
||||
instructions="Fetch timezone information. Be brief.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_time],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
summarizer = Agent(
|
||||
name="Summarizer",
|
||||
role="Summary Writer",
|
||||
goal="Combine information",
|
||||
instructions="Create a one-sentence summary combining weather and time.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[weather_agent, time_agent, summarizer],
|
||||
process="sequential",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agents.start("Get NYC weather and EST time, then summarize.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Agno Benchmarks
|
||||
# ============================================================================
|
||||
|
||||
def benchmark_agno_single(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark single Agno agent execution."""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" Agno single agent iteration {i+1}/{iterations}...")
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["You are a helpful assistant. Be very brief."],
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.run("What's the weather in NYC? Reply in one sentence.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_agno_two_agents(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark 2 Agno agents working together (using Team)."""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
from agno.team import Team
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" Agno 2-agent iteration {i+1}/{iterations}...")
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["Get weather data. Be brief."],
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
reporter = Agent(
|
||||
name="Reporter",
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["Create a one-sentence weather summary."],
|
||||
)
|
||||
|
||||
team = Team(
|
||||
members=[researcher, reporter],
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = team.run("Get NYC weather and create a brief report.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_agno_three_agents(iterations: int = 3) -> ExecutionResult:
|
||||
"""Benchmark 3 Agno agents working together."""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
from agno.team import Team
|
||||
|
||||
result = ExecutionResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" Agno 3-agent iteration {i+1}/{iterations}...")
|
||||
|
||||
weather_agent = Agent(
|
||||
name="WeatherAgent",
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["Fetch weather information. Be brief."],
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
time_agent = Agent(
|
||||
name="TimeAgent",
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["Fetch timezone information. Be brief."],
|
||||
tools=[get_time],
|
||||
)
|
||||
|
||||
summarizer = Agent(
|
||||
name="Summarizer",
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
instructions=["Create a one-sentence summary combining weather and time."],
|
||||
)
|
||||
|
||||
team = Team(
|
||||
members=[weather_agent, time_agent, summarizer],
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = team.run("Get NYC weather and EST time, then summarize.")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result.times.append(elapsed)
|
||||
print(f" Time: {elapsed:.2f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Save Results
|
||||
# ============================================================================
|
||||
|
||||
def save_results(results: dict, versions: dict):
|
||||
"""Save benchmark results to file."""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
filepath = os.path.join(os.path.dirname(__file__), 'REAL_BENCHMARK_RESULTS.md')
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Real-World Execution Benchmark\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write('**Model:** gpt-4o-mini\n')
|
||||
f.write('**Iterations:** 3 per test\n\n')
|
||||
|
||||
f.write('## Results Summary\n\n')
|
||||
f.write('| Test | PraisonAI (avg) | Agno (avg) | Difference |\n')
|
||||
f.write('|------|-----------------|------------|------------|\n')
|
||||
|
||||
tests = ['single', 'two_agents', 'three_agents']
|
||||
test_names = ['Single Agent', '2 Agents', '3 Agents']
|
||||
|
||||
for test, name in zip(tests, test_names):
|
||||
praison_key = f'praisonai_{test}'
|
||||
agno_key = f'agno_{test}'
|
||||
|
||||
if praison_key in results and agno_key in results:
|
||||
praison_avg = results[praison_key].avg_time
|
||||
agno_avg = results[agno_key].avg_time
|
||||
|
||||
if praison_avg < agno_avg:
|
||||
diff = f'PraisonAI {agno_avg/praison_avg:.2f}x faster'
|
||||
else:
|
||||
diff = f'Agno {praison_avg/agno_avg:.2f}x faster'
|
||||
|
||||
f.write(f'| {name} | {praison_avg:.2f}s | {agno_avg:.2f}s | {diff} |\n')
|
||||
|
||||
f.write('\n## Detailed Results\n\n')
|
||||
|
||||
for key, result in results.items():
|
||||
f.write(f'### {key}\n')
|
||||
f.write(f'- Average: {result.avg_time:.2f}s\n')
|
||||
f.write(f'- Min: {result.min_time:.2f}s\n')
|
||||
f.write(f'- Max: {result.max_time:.2f}s\n')
|
||||
f.write(f'- Std Dev: {result.std_dev:.2f}s\n')
|
||||
f.write(f'- Times: {[f"{t:.2f}s" for t in result.times]}\n\n')
|
||||
|
||||
f.write('## Package Versions\n\n')
|
||||
f.write('| Package | Version |\n')
|
||||
f.write('|---------|--------|\n')
|
||||
for pkg, ver in versions.items():
|
||||
f.write(f'| {pkg} | {ver} |\n')
|
||||
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('export OPENAI_API_KEY=your_key\n')
|
||||
f.write('cd praisonai-agents\n')
|
||||
f.write('python benchmarks/real_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*70)
|
||||
print("PraisonAI Agents - Real-World Execution Benchmark")
|
||||
print("="*70)
|
||||
print("\nThis benchmark measures actual agent execution time including:")
|
||||
print(" - LLM API calls")
|
||||
print(" - Tool execution")
|
||||
print(" - Multi-agent coordination")
|
||||
print("\nModel: gpt-4o-mini")
|
||||
print("Iterations: 3 per test")
|
||||
|
||||
versions = get_package_versions()
|
||||
print(f"\nPackage versions:")
|
||||
for pkg, ver in versions.items():
|
||||
print(f" {pkg}: {ver}")
|
||||
|
||||
results = {}
|
||||
|
||||
# Single Agent Tests
|
||||
print("\n" + "="*70)
|
||||
print("SINGLE AGENT BENCHMARK")
|
||||
print("="*70)
|
||||
|
||||
print("\nTesting PraisonAI single agent...")
|
||||
results['praisonai_single'] = benchmark_praisonai_single()
|
||||
|
||||
print("\nTesting Agno single agent...")
|
||||
results['agno_single'] = benchmark_agno_single()
|
||||
|
||||
# 2-Agent Tests
|
||||
print("\n" + "="*70)
|
||||
print("2-AGENT BENCHMARK")
|
||||
print("="*70)
|
||||
|
||||
print("\nTesting PraisonAI 2 agents...")
|
||||
results['praisonai_two_agents'] = benchmark_praisonai_two_agents()
|
||||
|
||||
print("\nTesting Agno 2 agents (Team)...")
|
||||
results['agno_two_agents'] = benchmark_agno_two_agents()
|
||||
|
||||
# 3-Agent Tests
|
||||
print("\n" + "="*70)
|
||||
print("3-AGENT BENCHMARK")
|
||||
print("="*70)
|
||||
|
||||
print("\nTesting PraisonAI 3 agents...")
|
||||
results['praisonai_three_agents'] = benchmark_praisonai_three_agents()
|
||||
|
||||
print("\nTesting Agno 3 agents (Team)...")
|
||||
results['agno_three_agents'] = benchmark_agno_three_agents()
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("RESULTS SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
print(f"\n{'Test':<20} {'PraisonAI (avg)':<18} {'Agno (avg)':<15} {'Difference':<20}")
|
||||
print("-" * 73)
|
||||
|
||||
tests = [
|
||||
('Single Agent', 'praisonai_single', 'agno_single'),
|
||||
('2 Agents', 'praisonai_two_agents', 'agno_two_agents'),
|
||||
('3 Agents', 'praisonai_three_agents', 'agno_three_agents'),
|
||||
]
|
||||
|
||||
for name, praison_key, agno_key in tests:
|
||||
praison_avg = results[praison_key].avg_time
|
||||
agno_avg = results[agno_key].avg_time
|
||||
|
||||
if praison_avg < agno_avg:
|
||||
diff = f'PraisonAI {agno_avg/praison_avg:.2f}x faster'
|
||||
else:
|
||||
diff = f'Agno {praison_avg/agno_avg:.2f}x faster'
|
||||
|
||||
print(f"{name:<20} {praison_avg:<18.2f}s {agno_avg:<15.2f}s {diff:<20}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
|
||||
# Save results
|
||||
save_results(results, versions)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
PraisonAI Agents - Response Time Benchmark
|
||||
|
||||
Measures actual LLM response time with gpt-4o-mini across frameworks.
|
||||
|
||||
Usage:
|
||||
python benchmarks/response_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import statistics
|
||||
import os
|
||||
from typing import List, Literal
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Ensure we use gpt-4o-mini
|
||||
os.environ['OPENAI_MODEL_NAME'] = 'gpt-4o-mini'
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResponseResult:
|
||||
"""Response time measurement results"""
|
||||
response_times: List[float] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def avg_time(self) -> float:
|
||||
return statistics.mean(self.response_times) if self.response_times else 0
|
||||
|
||||
@property
|
||||
def min_time(self) -> float:
|
||||
return min(self.response_times) if self.response_times else 0
|
||||
|
||||
@property
|
||||
def max_time(self) -> float:
|
||||
return max(self.response_times) if self.response_times else 0
|
||||
|
||||
@property
|
||||
def std_dev(self) -> float:
|
||||
return statistics.stdev(self.response_times) if len(self.response_times) > 1 else 0
|
||||
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
|
||||
|
||||
def benchmark_praisonai_response(iterations: int = 5) -> ResponseResult:
|
||||
"""Benchmark PraisonAI agent response time"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
result = ResponseResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" PraisonAI iteration {i+1}/{iterations}...")
|
||||
|
||||
# Create fresh agent each time
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start("What is the capital of France?")
|
||||
end = time.perf_counter()
|
||||
|
||||
result.response_times.append(end - start)
|
||||
print(f" Response time: {end - start:.3f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_agno_response(iterations: int = 5) -> ResponseResult:
|
||||
"""Benchmark Agno agent response time"""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
result = ResponseResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" Agno iteration {i+1}/{iterations}...")
|
||||
|
||||
# Create fresh agent each time
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
system_message="Be concise, reply with one sentence.",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.run("What is the capital of France?")
|
||||
end = time.perf_counter()
|
||||
|
||||
result.response_times.append(end - start)
|
||||
print(f" Response time: {end - start:.3f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_praisonai_with_tool(iterations: int = 5) -> ResponseResult:
|
||||
"""Benchmark PraisonAI agent response time with tool calling"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
result = ResponseResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" PraisonAI (with tool) iteration {i+1}/{iterations}...")
|
||||
|
||||
agent = Agent(
|
||||
name="Weather Agent",
|
||||
instructions="You are a weather assistant. Use the get_weather tool to answer questions.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_weather],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start("What's the weather in NYC?")
|
||||
end = time.perf_counter()
|
||||
|
||||
result.response_times.append(end - start)
|
||||
print(f" Response time: {end - start:.3f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def benchmark_agno_with_tool(iterations: int = 5) -> ResponseResult:
|
||||
"""Benchmark Agno agent response time with tool calling"""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
result = ResponseResult()
|
||||
|
||||
for i in range(iterations):
|
||||
print(f" Agno (with tool) iteration {i+1}/{iterations}...")
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
system_message="You are a weather assistant. Use the get_weather tool to answer questions.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.run("What's the weather in NYC?")
|
||||
end = time.perf_counter()
|
||||
|
||||
result.response_times.append(end - start)
|
||||
print(f" Response time: {end - start:.3f}s")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*70)
|
||||
print("PraisonAI Agents - Response Time Benchmark")
|
||||
print("="*70)
|
||||
print("\nModel: gpt-4o-mini")
|
||||
print("Measures end-to-end response time including:")
|
||||
print(" - Agent instantiation")
|
||||
print(" - API call to OpenAI")
|
||||
print(" - Response processing")
|
||||
|
||||
iterations = 5
|
||||
results = {}
|
||||
|
||||
# Test 1: Simple response (no tools)
|
||||
print("\n" + "="*70)
|
||||
print("TEST 1: Simple Response (No Tools)")
|
||||
print("="*70)
|
||||
|
||||
print("\nBenchmarking PraisonAI...")
|
||||
try:
|
||||
results['praisonai_simple'] = benchmark_praisonai_response(iterations)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\nBenchmarking Agno...")
|
||||
try:
|
||||
results['agno_simple'] = benchmark_agno_response(iterations)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Test 2: Response with tool calling
|
||||
print("\n" + "="*70)
|
||||
print("TEST 2: Response with Tool Calling")
|
||||
print("="*70)
|
||||
|
||||
print("\nBenchmarking PraisonAI with tool...")
|
||||
try:
|
||||
results['praisonai_tool'] = benchmark_praisonai_with_tool(iterations)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\nBenchmarking Agno with tool...")
|
||||
try:
|
||||
results['agno_tool'] = benchmark_agno_with_tool(iterations)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("SUMMARY RESULTS")
|
||||
print("="*70)
|
||||
|
||||
print(f"\n{'Test':<30} {'PraisonAI (avg)':<20} {'Agno (avg)':<20} {'Ratio':<15}")
|
||||
print("-" * 85)
|
||||
|
||||
if 'praisonai_simple' in results and 'agno_simple' in results:
|
||||
p_time = results['praisonai_simple'].avg_time
|
||||
a_time = results['agno_simple'].avg_time
|
||||
ratio = p_time / a_time if a_time > 0 else float('inf')
|
||||
print(f"{'Simple Response':<30} {p_time:<20.3f}s {a_time:<20.3f}s {ratio:.2f}x")
|
||||
|
||||
if 'praisonai_tool' in results and 'agno_tool' in results:
|
||||
p_time = results['praisonai_tool'].avg_time
|
||||
a_time = results['agno_tool'].avg_time
|
||||
ratio = p_time / a_time if a_time > 0 else float('inf')
|
||||
print(f"{'With Tool Calling':<30} {p_time:<20.3f}s {a_time:<20.3f}s {ratio:.2f}x")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("NOTE")
|
||||
print("="*70)
|
||||
print("\nResponse time is dominated by OpenAI API latency.")
|
||||
print("Agent instantiation overhead is negligible in real-world usage.")
|
||||
@@ -0,0 +1,194 @@
|
||||
❯ python benchmarks/performance_benchmark.py
|
||||
/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/_warning_patch.py:47: UserWarning: Mixing V1 models and V2 models (or constructs, like `TypeAdapter`) is not supported. Please upgrade `Settings` to V2.
|
||||
_original_warn(message, category, stacklevel, source)
|
||||
[13:04:15] INFO [13:04:15] config.py:50
|
||||
config.py:50 INFO
|
||||
Using config path:
|
||||
/Users/praison/.con
|
||||
fig/crewai/settings
|
||||
.json
|
||||
======================================================================
|
||||
PraisonAI Agents - Comprehensive Performance Benchmark
|
||||
======================================================================
|
||||
|
||||
This benchmark measures:
|
||||
1. Agent instantiation time (how fast an agent object is created)
|
||||
2. Memory footprint (how much memory an agent uses)
|
||||
|
||||
Methodology:
|
||||
- 10 warmup runs (not measured)
|
||||
- 1000 measured iterations
|
||||
- tracemalloc for memory measurement
|
||||
- Baseline subtraction for accuracy
|
||||
|
||||
Frameworks available:
|
||||
- PraisonAI: ✅
|
||||
- Agno: ✅
|
||||
- PydanticAI: ✅
|
||||
- OpenAI Agents SDK: ✅
|
||||
- LangGraph: ✅
|
||||
- CrewAI: ✅
|
||||
|
||||
======================================================================
|
||||
Testing PraisonAI Agents
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: PraisonAI
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for PraisonAI:
|
||||
Average Time: 0.000003 seconds (3.20 μs)
|
||||
Min Time: 0.000003 seconds
|
||||
Max Time: 0.000025 seconds
|
||||
Std Dev: 0.000001 seconds
|
||||
Median Time: 0.000003 seconds
|
||||
Avg Memory: 0.003678 MiB (3.77 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing PraisonAI Agents (LiteLLM)
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: PraisonAI (LiteLLM)
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for PraisonAI (LiteLLM):
|
||||
Average Time: 0.000007 seconds (7.31 μs)
|
||||
Min Time: 0.000007 seconds
|
||||
Max Time: 0.000060 seconds
|
||||
Std Dev: 0.000002 seconds
|
||||
Median Time: 0.000007 seconds
|
||||
Avg Memory: 0.006419 MiB (6.57 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing Agno
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: Agno
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for Agno:
|
||||
Average Time: 0.000005 seconds (5.30 μs)
|
||||
Min Time: 0.000005 seconds
|
||||
Max Time: 0.000039 seconds
|
||||
Std Dev: 0.000002 seconds
|
||||
Median Time: 0.000005 seconds
|
||||
Avg Memory: 0.006599 MiB (6.76 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing PydanticAI
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: PydanticAI Agent Instantiation
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for PydanticAI Agent Instantiation:
|
||||
Average Time: 0.000247 seconds (247.34 μs)
|
||||
Min Time: 0.000218 seconds
|
||||
Max Time: 0.000552 seconds
|
||||
Std Dev: 0.000031 seconds
|
||||
Median Time: 0.000237 seconds
|
||||
Avg Memory: 0.028486 MiB (29.17 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing OpenAI Agents SDK
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: OpenAI Agents SDK Instantiation
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for OpenAI Agents SDK Instantiation:
|
||||
Average Time: 0.000282 seconds (282.25 μs)
|
||||
Min Time: 0.000247 seconds
|
||||
Max Time: 0.001110 seconds
|
||||
Std Dev: 0.000055 seconds
|
||||
Median Time: 0.000263 seconds
|
||||
Avg Memory: 0.029032 MiB (29.73 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing LangGraph
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: LangGraph Agent Instantiation
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for LangGraph Agent Instantiation:
|
||||
Average Time: 0.003200 seconds (3200.15 μs)
|
||||
Min Time: 0.002808 seconds
|
||||
Max Time: 0.116363 seconds
|
||||
Std Dev: 0.003628 seconds
|
||||
Median Time: 0.002972 seconds
|
||||
Avg Memory: 0.207509 MiB (212.49 KiB)
|
||||
|
||||
======================================================================
|
||||
Testing CrewAI
|
||||
======================================================================
|
||||
|
||||
============================================================
|
||||
Benchmarking: CrewAI Agent Instantiation
|
||||
============================================================
|
||||
Warmup runs: 10
|
||||
Measured iterations: 1000
|
||||
|
||||
Measuring runtime...
|
||||
Measuring memory...
|
||||
|
||||
Results for CrewAI Agent Instantiation:
|
||||
Average Time: 0.016772 seconds (16771.69 μs)
|
||||
Min Time: 0.014404 seconds
|
||||
Max Time: 0.091841 seconds
|
||||
Std Dev: 0.003445 seconds
|
||||
Median Time: 0.015816 seconds
|
||||
Avg Memory: 0.065050 MiB (66.61 KiB)
|
||||
|
||||
======================================================================
|
||||
RESULTS SUMMARY
|
||||
======================================================================
|
||||
|
||||
Framework Avg Time (μs) Avg Memory (KiB) Relative
|
||||
-------------------------------------------------------------------------
|
||||
PraisonAI 3.20 3.77 1.00x
|
||||
Agno 5.30 6.76 1.66x
|
||||
PraisonAI (LiteLLM) 7.31 6.57 2.28x
|
||||
PydanticAI 247.34 29.17 77.25x
|
||||
OpenAI Agents SDK 282.25 29.73 88.15x
|
||||
LangGraph 3200.15 212.49 999.46x
|
||||
CrewAI 16771.69 66.61 5238.06x
|
||||
|
||||
======================================================================
|
||||
❯
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Performance Benchmark
|
||||
|
||||
Compares agent instantiation times across popular AI agent frameworks.
|
||||
|
||||
Usage:
|
||||
python benchmarks/simple_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
|
||||
|
||||
def sample_tool(city: Literal['nyc', 'sf']):
|
||||
"""Sample tool for benchmark testing."""
|
||||
if city == 'nyc':
|
||||
return 'cloudy'
|
||||
return 'sunny'
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS):
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
create_fn()
|
||||
times.append((time.perf_counter() - start) * 1_000_000)
|
||||
return sum(times) / len(times)
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
"""Run the benchmark across all available frameworks (without tools)."""
|
||||
results = {}
|
||||
|
||||
print('=' * 60)
|
||||
print('PraisonAI Agents - Performance Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nIterations: {ITERATIONS}')
|
||||
print('Metric: Agent instantiation time (microseconds)\n')
|
||||
|
||||
# PraisonAI (without tools)
|
||||
print("Testing PraisonAI...")
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
results['PraisonAI'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', verbose=False)
|
||||
)
|
||||
|
||||
results['PraisonAI (LiteLLM)'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', verbose=False)
|
||||
)
|
||||
|
||||
# Other frameworks for comparison
|
||||
print("Testing other frameworks...")
|
||||
|
||||
try:
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
results['Agno'] = measure_instantiation(
|
||||
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from pydantic_ai import Agent as PydanticAgent
|
||||
results['PydanticAI'] = measure_instantiation(
|
||||
lambda: PydanticAgent('openai:gpt-4o-mini')
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from agents import Agent as OpenAIAgent
|
||||
results['OpenAI Agents SDK'] = measure_instantiation(
|
||||
lambda: OpenAIAgent(name='Test', model='gpt-4o-mini')
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from langchain_core.tools import tool as langchain_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
@langchain_tool
|
||||
def get_weather_lg(city: Literal['nyc', 'sf']):
|
||||
"""Get weather info."""
|
||||
return 'sunny' if city == 'sf' else 'cloudy'
|
||||
|
||||
results['LangGraph'] = measure_instantiation(
|
||||
lambda: create_react_agent(model=ChatOpenAI(model='gpt-4o-mini'), tools=[get_weather_lg])
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from crewai.agent import Agent as CrewAgent
|
||||
from crewai.tools import tool as crewai_tool
|
||||
|
||||
@crewai_tool("Weather Tool")
|
||||
def get_weather_crew(city: Literal['nyc', 'sf']):
|
||||
"""Get weather info."""
|
||||
return 'sunny' if city == 'sf' else 'cloudy'
|
||||
|
||||
results['CrewAI'] = measure_instantiation(
|
||||
lambda: CrewAgent(
|
||||
role='Weather Agent',
|
||||
goal='Provide weather info',
|
||||
backstory='A weather expert',
|
||||
tools=[get_weather_crew],
|
||||
verbose=False
|
||||
)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Print results
|
||||
print('\n' + '=' * 60)
|
||||
print('RESULTS')
|
||||
print('=' * 60)
|
||||
|
||||
baseline = results.get('PraisonAI', 1)
|
||||
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
|
||||
print('-' * 50)
|
||||
|
||||
for name, avg in sorted(results.items(), key=lambda x: x[1]):
|
||||
ratio = avg / baseline
|
||||
print(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
return results
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
"""Get version numbers for benchmarked packages."""
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
packages = {
|
||||
'PraisonAI': 'praisonaiagents',
|
||||
'Agno': 'agno',
|
||||
'PydanticAI': 'pydantic-ai',
|
||||
'OpenAI Agents SDK': 'openai-agents',
|
||||
'LangGraph': 'langgraph',
|
||||
'CrewAI': 'crewai'
|
||||
}
|
||||
|
||||
versions = {}
|
||||
for display_name, pkg_name in packages.items():
|
||||
try:
|
||||
versions[display_name] = get_version(pkg_name)
|
||||
except Exception:
|
||||
versions[display_name] = 'not installed'
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md'):
|
||||
"""Save benchmark results to a markdown file and update README."""
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
filepath = os.path.join(os.path.dirname(__file__), filename)
|
||||
versions = get_package_versions()
|
||||
|
||||
# Find the fastest result (baseline)
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1])
|
||||
fastest_time = sorted_results[0][1] if sorted_results else 1
|
||||
|
||||
# Build the table rows
|
||||
table_rows = []
|
||||
for name, avg in sorted_results:
|
||||
ratio = avg / fastest_time
|
||||
if ratio == 1.0:
|
||||
table_rows.append(f'| **{name}** | **{avg:.2f}** | **1.00x (fastest)** |')
|
||||
else:
|
||||
avg_str = f'{avg:,.2f}' if avg >= 1000 else f'{avg:.2f}'
|
||||
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
|
||||
table_rows.append(f'| {name} | {avg_str} | {ratio_str} |')
|
||||
|
||||
# Save to BENCHMARK_RESULTS.md
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Benchmark Results\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write(f'**Iterations:** {ITERATIONS}\n')
|
||||
f.write('**Test:** Agent instantiation (without tools)\n\n')
|
||||
f.write('## Results\n\n')
|
||||
f.write('| Framework | Avg Time (μs) | Relative |\n')
|
||||
f.write('|-----------|---------------|----------|\n')
|
||||
f.write('\n'.join(table_rows) + '\n')
|
||||
f.write('\n## Package Versions\n\n')
|
||||
f.write('| Package | Version |\n')
|
||||
f.write('|---------|--------|\n')
|
||||
for pkg, ver in versions.items():
|
||||
f.write(f'| {pkg} | {ver} |\n')
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('cd praisonai-agents\n')
|
||||
f.write('python benchmarks/simple_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
|
||||
# Also update the main README.md
|
||||
readme_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'README.md')
|
||||
if os.path.exists(readme_path):
|
||||
update_readme(readme_path, table_rows)
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def update_readme(readme_path: str, table_rows: list):
|
||||
"""Update the performance section in README.md with latest results."""
|
||||
import re
|
||||
|
||||
with open(readme_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Build the new table
|
||||
new_table = '''| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
''' + '\n'.join(table_rows)
|
||||
|
||||
# Pattern to match the performance table
|
||||
pattern = r'(\| Framework \| Avg Time \(μs\) \| Relative \|\n\|[-|]+\|\n)(\|[^\n]+\|\n)+'
|
||||
|
||||
if re.search(pattern, content):
|
||||
content = re.sub(pattern, new_table + '\n', content)
|
||||
|
||||
with open(readme_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'README.md updated: {readme_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
results = run_benchmark()
|
||||
save_results(results)
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Tools Benchmark
|
||||
|
||||
Compares agent instantiation times WITH TOOLS across frameworks.
|
||||
|
||||
Usage:
|
||||
python benchmarks/tools_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Literal
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
|
||||
|
||||
def sample_tool(city: Literal['nyc', 'sf']):
|
||||
"""Sample tool for benchmark testing."""
|
||||
if city == 'nyc':
|
||||
return 'cloudy'
|
||||
return 'sunny'
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS):
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
times = []
|
||||
|
||||
# Warmup
|
||||
for _ in range(10):
|
||||
create_fn()
|
||||
|
||||
# Measure
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
create_fn()
|
||||
elapsed = time.perf_counter() - start
|
||||
times.append(elapsed * 1_000_000) # Convert to microseconds
|
||||
|
||||
return sum(times) / len(times)
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
"""Run the benchmark across all available frameworks WITH TOOLS."""
|
||||
tools = [sample_tool]
|
||||
results = {}
|
||||
|
||||
print('=' * 60)
|
||||
print('PraisonAI Agents - Tools Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nIterations: {ITERATIONS}')
|
||||
print('Metric: Agent instantiation time WITH TOOLS (microseconds)\n')
|
||||
|
||||
# PraisonAI with tools
|
||||
print("Testing PraisonAI...")
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
results['PraisonAI'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', tools=tools, verbose=False)
|
||||
)
|
||||
|
||||
results['PraisonAI (LiteLLM)'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', tools=tools, verbose=False)
|
||||
)
|
||||
|
||||
# Other frameworks with tools
|
||||
print("Testing other frameworks...")
|
||||
|
||||
try:
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
results['Agno'] = measure_instantiation(
|
||||
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'), tools=tools)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from agents import Agent as OpenAIAgent, function_tool
|
||||
results['OpenAI Agents SDK'] = measure_instantiation(
|
||||
lambda: OpenAIAgent(name='Test', model='gpt-4o-mini', tools=[function_tool(sample_tool)])
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from langchain_core.tools import tool as langchain_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
@langchain_tool
|
||||
def get_weather_lg(city: Literal['nyc', 'sf']):
|
||||
"""Get weather info."""
|
||||
return 'sunny' if city == 'sf' else 'cloudy'
|
||||
|
||||
results['LangGraph'] = measure_instantiation(
|
||||
lambda: create_react_agent(model=ChatOpenAI(model='gpt-4o-mini'), tools=[get_weather_lg])
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from crewai.agent import Agent as CrewAgent
|
||||
from crewai.tools import tool as crewai_tool
|
||||
|
||||
@crewai_tool("Weather Tool")
|
||||
def get_weather_crew(city: Literal['nyc', 'sf']):
|
||||
"""Get weather info."""
|
||||
return 'sunny' if city == 'sf' else 'cloudy'
|
||||
|
||||
results['CrewAI'] = measure_instantiation(
|
||||
lambda: CrewAgent(
|
||||
role='Weather Agent',
|
||||
goal='Provide weather info',
|
||||
backstory='A weather expert',
|
||||
tools=[get_weather_crew],
|
||||
verbose=False
|
||||
)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Print results
|
||||
print('\n' + '=' * 60)
|
||||
print('RESULTS (WITH TOOLS)')
|
||||
print('=' * 60)
|
||||
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1])
|
||||
baseline = sorted_results[0][1] if sorted_results else 1
|
||||
|
||||
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
|
||||
print('-' * 50)
|
||||
|
||||
for name, avg in sorted_results:
|
||||
ratio = avg / baseline
|
||||
print(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
return results
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
"""Get version numbers for benchmarked packages."""
|
||||
packages = {
|
||||
'PraisonAI': 'praisonaiagents',
|
||||
'Agno': 'agno',
|
||||
'OpenAI Agents SDK': 'openai-agents',
|
||||
'LangGraph': 'langgraph',
|
||||
'CrewAI': 'crewai'
|
||||
}
|
||||
|
||||
versions = {}
|
||||
for display_name, pkg_name in packages.items():
|
||||
try:
|
||||
versions[display_name] = get_version(pkg_name)
|
||||
except Exception:
|
||||
versions[display_name] = 'not installed'
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def save_results(results: dict, filename: str = 'TOOLS_BENCHMARK_RESULTS.md'):
|
||||
"""Save benchmark results to a markdown file."""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
filepath = os.path.join(os.path.dirname(__file__), filename)
|
||||
versions = get_package_versions()
|
||||
|
||||
# Find the fastest result (baseline)
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1])
|
||||
fastest_time = sorted_results[0][1] if sorted_results else 1
|
||||
|
||||
# Build the table rows
|
||||
table_rows = []
|
||||
for name, avg in sorted_results:
|
||||
ratio = avg / fastest_time
|
||||
if ratio == 1.0:
|
||||
table_rows.append(f'| **{name}** | **{avg:.2f}** | **1.00x (fastest)** |')
|
||||
else:
|
||||
avg_str = f'{avg:,.2f}' if avg >= 1000 else f'{avg:.2f}'
|
||||
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
|
||||
table_rows.append(f'| {name} | {avg_str} | {ratio_str} |')
|
||||
|
||||
# Save to file
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Tools Benchmark Results\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write(f'**Iterations:** {ITERATIONS}\n')
|
||||
f.write('**Test:** Agent instantiation WITH TOOLS\n\n')
|
||||
f.write('## Results\n\n')
|
||||
f.write('| Framework | Avg Time (μs) | Relative |\n')
|
||||
f.write('|-----------|---------------|----------|\n')
|
||||
f.write('\n'.join(table_rows) + '\n')
|
||||
f.write('\n## Package Versions\n\n')
|
||||
f.write('| Package | Version |\n')
|
||||
f.write('|---------|--------|\n')
|
||||
for pkg, ver in versions.items():
|
||||
f.write(f'| {pkg} | {ver} |\n')
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
f.write('```bash\n')
|
||||
f.write('cd praisonai-agents\n')
|
||||
f.write('python benchmarks/tools_benchmark.py\n')
|
||||
f.write('```\n')
|
||||
|
||||
print(f'\nResults saved to: {filepath}')
|
||||
return filepath
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
results = run_benchmark()
|
||||
save_results(results)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
Starting test with verbose=False
|
||||
CALLBACK EXECUTED!
|
||||
Message:
|
||||
You need to do the following task: Say hello world.
|
||||
Expected Output: A greeting.
|
||||
Please provide only the final result of your work. Do not add any conversation or extra explanation.
|
||||
Response: Hello, World!
|
||||
Other kwargs: {'markdown': True, 'generation_time': 2.685833215713501, 'agent_name': 'TestAgent', 'agent_role': 'Assistant', 'agent_tools': None, 'task_name': 'simple_task', 'task_description': 'Say hello world', 'task_id': 0}
|
||||
--------------------------------------------------
|
||||
@@ -54,6 +54,13 @@ except ImportError:
|
||||
MCP = None
|
||||
from .session import Session
|
||||
from .memory.memory import Memory
|
||||
# Workflows - import from dedicated workflows module
|
||||
from .workflows import (
|
||||
Workflow, WorkflowStep, WorkflowContext, StepResult,
|
||||
Route, Parallel, Loop, Repeat,
|
||||
route, parallel, loop, repeat,
|
||||
Pipeline # Alias for Workflow
|
||||
)
|
||||
from .guardrails import GuardrailResult, LLMGuardrail
|
||||
|
||||
# Fast Context support (lazy loaded to avoid performance impact)
|
||||
|
||||
@@ -248,7 +248,9 @@ class Agent:
|
||||
fast_context_model: str = "gpt-4o-mini",
|
||||
fast_context_max_turns: int = 4,
|
||||
fast_context_parallelism: int = 8,
|
||||
fast_context_timeout: float = 30.0
|
||||
fast_context_timeout: float = 30.0,
|
||||
history_in_context: Optional[int] = None,
|
||||
auto_save: Optional[str] = None
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
@@ -544,9 +546,14 @@ Your Goal: {self.goal}
|
||||
self.prompt_caching = prompt_caching
|
||||
self.claude_memory = claude_memory
|
||||
|
||||
# Session management
|
||||
self.history_in_context = history_in_context # Number of past sessions to include
|
||||
self.auto_save = auto_save # Session name for auto-saving
|
||||
|
||||
# Initialize rules manager for persistent context (like Cursor/Windsurf)
|
||||
# NOTE: Lazy initialization - rules are loaded only when accessed (performance optimization)
|
||||
self._rules_manager = None
|
||||
self._init_rules_manager()
|
||||
self._rules_manager_initialized = False
|
||||
|
||||
# Handle web_search fallback: inject DuckDuckGo tool for unsupported models
|
||||
if web_search and not self._model_supports_web_search():
|
||||
@@ -593,7 +600,7 @@ Your Goal: {self.goal}
|
||||
|
||||
# Fast Context configuration (lazy loaded)
|
||||
self.fast_context_enabled = fast_context
|
||||
self.fast_context_path = fast_context_path or os.getcwd()
|
||||
self._fast_context_path = fast_context_path # Store raw, resolve lazily
|
||||
self.fast_context_model = fast_context_model
|
||||
self.fast_context_max_turns = fast_context_max_turns
|
||||
self.fast_context_parallelism = fast_context_parallelism
|
||||
@@ -608,6 +615,17 @@ Your Goal: {self.goal}
|
||||
self._console = Console()
|
||||
return self._console
|
||||
|
||||
@property
|
||||
def fast_context_path(self):
|
||||
"""Lazily resolve fast_context_path - avoids os.getcwd() on every init."""
|
||||
if self._fast_context_path is None:
|
||||
self._fast_context_path = os.getcwd()
|
||||
return self._fast_context_path
|
||||
|
||||
@fast_context_path.setter
|
||||
def fast_context_path(self, value):
|
||||
self._fast_context_path = value
|
||||
|
||||
@property
|
||||
def _openai_client(self):
|
||||
"""Lazily initialize OpenAI client only when needed."""
|
||||
@@ -779,6 +797,21 @@ Your Goal: {self.goal}
|
||||
logging.warning(f"FastContext search failed: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def rules_manager(self):
|
||||
"""
|
||||
Lazy-initialized RulesManager for persistent rules/instructions.
|
||||
|
||||
This property initializes the RulesManager only when first accessed,
|
||||
avoiding expensive filesystem operations during agent instantiation.
|
||||
|
||||
Returns:
|
||||
RulesManager instance or None if not available
|
||||
"""
|
||||
if not self._rules_manager_initialized:
|
||||
self._init_rules_manager()
|
||||
return self._rules_manager
|
||||
|
||||
def _init_rules_manager(self):
|
||||
"""
|
||||
Initialize RulesManager for persistent rules/instructions.
|
||||
@@ -787,7 +820,10 @@ Your Goal: {self.goal}
|
||||
- ~/.praison/rules/ (global)
|
||||
- .praison/rules/ (workspace)
|
||||
- Subdirectory rules
|
||||
|
||||
NOTE: This is called lazily via the rules_manager property for performance.
|
||||
"""
|
||||
self._rules_manager_initialized = True
|
||||
try:
|
||||
from ..memory.rules_manager import RulesManager
|
||||
import os
|
||||
@@ -822,10 +858,10 @@ Your Goal: {self.goal}
|
||||
Returns:
|
||||
Formatted rules context string
|
||||
"""
|
||||
if not self._rules_manager:
|
||||
if not self.rules_manager:
|
||||
return ""
|
||||
|
||||
return self._rules_manager.build_rules_context(
|
||||
return self.rules_manager.build_rules_context(
|
||||
file_path=file_path,
|
||||
include_manual=include_manual
|
||||
)
|
||||
@@ -1233,8 +1269,8 @@ Your Goal: {self.goal}
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}"""
|
||||
|
||||
# Add rules context if rules manager is enabled
|
||||
if self._rules_manager:
|
||||
# Add rules context if rules manager is enabled (lazy initialization)
|
||||
if self._rules_manager_initialized and self._rules_manager:
|
||||
rules_context = self.get_rules_context()
|
||||
if rules_context:
|
||||
system_prompt += f"\n\n## Rules (Guidelines you must follow)\n{rules_context}"
|
||||
@@ -1533,23 +1569,54 @@ Your Goal: {self.goal}"""
|
||||
# Special handling for MCP tools
|
||||
# Check if tools is an MCP instance with the requested function name
|
||||
from ..mcp.mcp import MCP
|
||||
|
||||
# Helper function to execute MCP tool
|
||||
def _execute_mcp_tool(mcp_instance, func_name, args):
|
||||
"""Execute a tool from an MCP instance."""
|
||||
# Handle SSE MCP client
|
||||
if hasattr(mcp_instance, 'is_sse') and mcp_instance.is_sse:
|
||||
if hasattr(mcp_instance, 'sse_client'):
|
||||
for tool in mcp_instance.sse_client.tools:
|
||||
if tool.name == func_name:
|
||||
logging.debug(f"Found matching SSE MCP tool: {func_name}")
|
||||
return True, tool(**args)
|
||||
# Handle HTTP Stream MCP client
|
||||
if hasattr(mcp_instance, 'is_http_stream') and mcp_instance.is_http_stream:
|
||||
if hasattr(mcp_instance, 'http_stream_client'):
|
||||
for tool in mcp_instance.http_stream_client.tools:
|
||||
if tool.name == func_name:
|
||||
logging.debug(f"Found matching HTTP Stream MCP tool: {func_name}")
|
||||
return True, tool(**args)
|
||||
# Handle WebSocket MCP client
|
||||
if hasattr(mcp_instance, 'is_websocket') and mcp_instance.is_websocket:
|
||||
if hasattr(mcp_instance, 'websocket_client'):
|
||||
for tool in mcp_instance.websocket_client.tools:
|
||||
if tool.name == func_name:
|
||||
logging.debug(f"Found matching WebSocket MCP tool: {func_name}")
|
||||
return True, tool(**args)
|
||||
# Handle stdio MCP client
|
||||
if hasattr(mcp_instance, 'runner'):
|
||||
for mcp_tool in mcp_instance.runner.tools:
|
||||
if hasattr(mcp_tool, 'name') and mcp_tool.name == func_name:
|
||||
logging.debug(f"Found matching MCP tool: {func_name}")
|
||||
return True, mcp_instance.runner.call_tool(func_name, args)
|
||||
return False, None
|
||||
|
||||
# Check if tools is a single MCP instance
|
||||
if isinstance(self.tools, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name}")
|
||||
|
||||
# Handle SSE MCP client
|
||||
if hasattr(self.tools, 'is_sse') and self.tools.is_sse:
|
||||
if hasattr(self.tools, 'sse_client'):
|
||||
for tool in self.tools.sse_client.tools:
|
||||
if tool.name == function_name:
|
||||
logging.debug(f"Found matching SSE MCP tool: {function_name}")
|
||||
return tool(**arguments)
|
||||
# Handle stdio MCP client
|
||||
elif hasattr(self.tools, 'runner'):
|
||||
# Check if any of the MCP tools match the function name
|
||||
for mcp_tool in self.tools.runner.tools:
|
||||
if hasattr(mcp_tool, 'name') and mcp_tool.name == function_name:
|
||||
logging.debug(f"Found matching MCP tool: {function_name}")
|
||||
return self.tools.runner.call_tool(function_name, arguments)
|
||||
found, result = _execute_mcp_tool(self.tools, function_name, arguments)
|
||||
if found:
|
||||
return result
|
||||
|
||||
# Check if tools is a list that may contain MCP instances
|
||||
if isinstance(self.tools, (list, tuple)):
|
||||
for tool in self.tools:
|
||||
if isinstance(tool, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name} in MCP instance")
|
||||
found, result = _execute_mcp_tool(tool, function_name, arguments)
|
||||
if found:
|
||||
return result
|
||||
|
||||
# Try to find the function in the agent's tools list first
|
||||
func = None
|
||||
@@ -1864,7 +1931,8 @@ Your Goal: {self.goal}"""
|
||||
if tool_param is not None:
|
||||
from ..mcp.mcp import MCP
|
||||
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
logging.debug("Converting MCP tool to OpenAI format")
|
||||
# Single MCP instance
|
||||
logging.debug("Converting single MCP tool to OpenAI format")
|
||||
openai_tool = tool_param.to_openai_tool()
|
||||
if openai_tool:
|
||||
# Handle both single tool and list of tools
|
||||
@@ -1873,6 +1941,22 @@ Your Goal: {self.goal}"""
|
||||
else:
|
||||
tool_param = [openai_tool]
|
||||
logging.debug(f"Converted MCP tool: {tool_param}")
|
||||
elif isinstance(tool_param, (list, tuple)):
|
||||
# List that may contain MCP instances - convert each MCP to OpenAI format
|
||||
converted_tools = []
|
||||
for t in tool_param:
|
||||
if isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
|
||||
logging.debug("Converting MCP instance in list to OpenAI format")
|
||||
openai_tools = t.to_openai_tool()
|
||||
if isinstance(openai_tools, list):
|
||||
converted_tools.extend(openai_tools)
|
||||
elif openai_tools:
|
||||
converted_tools.append(openai_tools)
|
||||
else:
|
||||
# Keep non-MCP tools as-is
|
||||
converted_tools.append(t)
|
||||
tool_param = converted_tools
|
||||
logging.debug(f"Converted {len(converted_tools)} tools from list")
|
||||
|
||||
# Store chat history length for potential rollback
|
||||
chat_history_length = len(self.chat_history)
|
||||
@@ -2676,21 +2760,78 @@ Write the complete compiled report:"""
|
||||
|
||||
def start(self, prompt: str, **kwargs):
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
# Load history from past sessions if history_in_context is set
|
||||
self._load_history_context()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
return self._start_with_planning(prompt, **kwargs)
|
||||
|
||||
# Check if streaming is enabled (either from kwargs or agent's stream attribute)
|
||||
stream_enabled = kwargs.get('stream', getattr(self, 'stream', False))
|
||||
|
||||
if stream_enabled:
|
||||
result = self._start_with_planning(prompt, **kwargs)
|
||||
elif kwargs.get('stream', getattr(self, 'stream', False)):
|
||||
# Return a generator for streaming response
|
||||
return self._start_stream(prompt, **kwargs)
|
||||
result = self._start_stream(prompt, **kwargs)
|
||||
else:
|
||||
# Return regular chat response for backward compatibility
|
||||
# Explicitly pass the resolved stream parameter to avoid chat() method default
|
||||
kwargs['stream'] = stream_enabled
|
||||
return self.chat(prompt, **kwargs)
|
||||
kwargs['stream'] = False
|
||||
result = self.chat(prompt, **kwargs)
|
||||
|
||||
# Auto-save session if enabled
|
||||
self._auto_save_session()
|
||||
|
||||
return result
|
||||
|
||||
def _load_history_context(self):
|
||||
"""Load history from past sessions into context if history_in_context is set."""
|
||||
if not self.history_in_context or not self._memory_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
sessions = self._memory_instance.list_sessions()
|
||||
if not sessions:
|
||||
return
|
||||
|
||||
# Get the last N sessions
|
||||
sessions_to_load = sessions[:self.history_in_context]
|
||||
|
||||
for session_info in reversed(sessions_to_load):
|
||||
try:
|
||||
session_data = self._memory_instance.resume_session(session_info["name"])
|
||||
history = session_data.get("conversation_history", [])
|
||||
|
||||
# Add history to chat_history with a marker
|
||||
for msg in history:
|
||||
if msg not in self.chat_history:
|
||||
# Mark as from history to avoid duplication
|
||||
msg_copy = msg.copy()
|
||||
msg_copy["_from_history"] = True
|
||||
self.chat_history.append(msg_copy)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if sessions_to_load:
|
||||
logging.debug(f"Loaded history from {len(sessions_to_load)} past session(s)")
|
||||
except Exception as e:
|
||||
logging.debug(f"Error loading history context: {e}")
|
||||
|
||||
def _auto_save_session(self):
|
||||
"""Auto-save session if auto_save is enabled."""
|
||||
if not self.auto_save or not self._memory_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
# Filter out history markers before saving
|
||||
clean_history = [
|
||||
{k: v for k, v in msg.items() if k != "_from_history"}
|
||||
for msg in self.chat_history
|
||||
]
|
||||
|
||||
self._memory_instance.save_session(
|
||||
name=self.auto_save,
|
||||
conversation_history=clean_history,
|
||||
metadata={"agent_name": self.name, "user_id": self.user_id}
|
||||
)
|
||||
logging.debug(f"Auto-saved session: {self.auto_save}")
|
||||
except Exception as e:
|
||||
logging.debug(f"Error auto-saving session: {e}")
|
||||
|
||||
def _start_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
|
||||
"""Stream generator for real-time response chunks."""
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, List
|
||||
from typing import Any, Dict, Optional, List, Callable
|
||||
from pydantic import BaseModel
|
||||
from rich.text import Text
|
||||
from rich.panel import Panel
|
||||
@@ -127,7 +127,7 @@ def process_task_context(context_item, verbose=0, user_id=None):
|
||||
return str(context_item) # Fallback for unknown types
|
||||
|
||||
class PraisonAIAgents:
|
||||
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None, memory=False, memory_config=None, embedder=None, user_id=None, max_iter=10, stream=True, name: Optional[str] = None, planning: bool = False, planning_llm: Optional[str] = None, auto_approve_plan: bool = False, planning_tools: Optional[List] = None, planning_reasoning: bool = False):
|
||||
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None, memory=False, memory_config=None, embedder=None, user_id=None, max_iter=10, stream=True, name: Optional[str] = None, planning: bool = False, planning_llm: Optional[str] = None, auto_approve_plan: bool = False, planning_tools: Optional[List] = None, planning_reasoning: bool = False, on_task_start: Optional[Callable] = None, on_task_complete: Optional[Callable] = None, variables: Optional[Dict[str, Any]] = None):
|
||||
# Add check at the start if memory is requested
|
||||
if memory:
|
||||
try:
|
||||
@@ -162,6 +162,11 @@ class PraisonAIAgents:
|
||||
self.stream = stream
|
||||
self.name = name # Store the name for the Agents collection
|
||||
|
||||
# Callbacks for workflow execution
|
||||
self.on_task_start = on_task_start # Called before each task
|
||||
self.on_task_complete = on_task_complete # Called after each task
|
||||
self.variables = variables if variables else {} # Global variables for substitution
|
||||
|
||||
# Check for manager_llm in environment variable if not provided
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
|
||||
@@ -661,13 +666,24 @@ Context:
|
||||
|
||||
executor_agent = task.agent
|
||||
|
||||
# Create agent from agent_config if provided and no agent assigned
|
||||
if executor_agent is None and getattr(task, 'agent_config', None):
|
||||
executor_agent = self._create_agent_from_config(task.agent_config)
|
||||
task.agent = executor_agent
|
||||
|
||||
# Set current agent for token tracking
|
||||
llm = getattr(executor_agent, 'llm', None) or getattr(executor_agent, 'llm_instance', None)
|
||||
if llm and hasattr(llm, 'set_current_agent'):
|
||||
llm.set_current_agent(executor_agent.name)
|
||||
|
||||
# Substitute variables in task description if provided
|
||||
task_description = task.description
|
||||
if getattr(task, 'variables', None):
|
||||
for key, value in task.variables.items():
|
||||
task_description = task_description.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
task_prompt = f"""
|
||||
You need to do the following task: {task.description}.
|
||||
You need to do the following task: {task_description}.
|
||||
Expected Output: {task.expected_output}.
|
||||
"""
|
||||
if task.context:
|
||||
@@ -825,6 +841,17 @@ Context:
|
||||
logger.info(f"Task with ID {task_id} is already completed")
|
||||
return
|
||||
|
||||
# Call on_task_start callback if provided
|
||||
if self.on_task_start:
|
||||
try:
|
||||
self.on_task_start(task, task_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_start callback: {e}")
|
||||
|
||||
# Apply global variables to task if not already set
|
||||
if self.variables and not getattr(task, 'variables', None):
|
||||
task.variables = self.variables
|
||||
|
||||
retries = 0
|
||||
while task.status != "completed" and retries < self.max_retries:
|
||||
logger.debug(f"Attempt {retries+1} for task {task_id}")
|
||||
@@ -857,6 +884,14 @@ Context:
|
||||
logger.exception(e)
|
||||
|
||||
self.save_output_to_file(task, task_output)
|
||||
|
||||
# Call on_task_complete callback if provided
|
||||
if self.on_task_complete:
|
||||
try:
|
||||
self.on_task_complete(task, task_output)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_complete callback: {e}")
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Task {task_id} completed successfully.")
|
||||
else:
|
||||
@@ -1335,6 +1370,61 @@ Context:
|
||||
agent_names = ", ".join([agent.name for agent in self.agents])
|
||||
print(f"📊 Available agents for this endpoint ({len(self.agents)}): {agent_names}")
|
||||
|
||||
# Create per-agent endpoints for individual agent access
|
||||
# This allows n8n and other tools to call specific agents
|
||||
agents_dict = {agent.name.lower().replace(' ', '_'): agent for agent in self.agents}
|
||||
|
||||
# Add GET endpoint to list available agents
|
||||
@_agents_shared_apps[port].get(f"{path}/list")
|
||||
async def list_agents():
|
||||
return {
|
||||
"agents": [
|
||||
{"name": agent.name, "id": agent.name.lower().replace(' ', '_')}
|
||||
for agent in self.agents
|
||||
]
|
||||
}
|
||||
|
||||
# Add per-agent POST endpoints
|
||||
for agent_id, agent_instance in agents_dict.items():
|
||||
agent_path = f"{path}/{agent_id}"
|
||||
|
||||
# Create a closure to capture the agent instance
|
||||
def create_agent_handler(agent):
|
||||
async def handle_single_agent(request: Request):
|
||||
try:
|
||||
request_data = await request.json()
|
||||
query = request_data.get("query", "")
|
||||
if not query:
|
||||
raise HTTPException(status_code=400, detail="Missing 'query' field")
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
||||
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(agent.chat):
|
||||
response = await agent.achat(query)
|
||||
else:
|
||||
loop = asyncio.get_running_loop()
|
||||
response = await loop.run_in_executor(None, lambda q=query: agent.chat(q))
|
||||
|
||||
return {
|
||||
"agent": agent.name,
|
||||
"query": query,
|
||||
"response": response
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"Error with agent {agent.name}: {str(e)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": f"Agent error: {str(e)}"}
|
||||
)
|
||||
return handle_single_agent
|
||||
|
||||
# Register the endpoint
|
||||
_agents_shared_apps[port].post(agent_path)(create_agent_handler(agent_instance))
|
||||
_agents_registered_endpoints[port][agent_path] = f"{endpoint_id}_{agent_id}"
|
||||
|
||||
print(f"🔗 Per-agent endpoints: {', '.join([f'{path}/{aid}' for aid in agents_dict.keys()])}")
|
||||
|
||||
# Start the server if it's not already running for this port
|
||||
if not _agents_server_started.get(port, False):
|
||||
# Mark the server as started first to prevent duplicate starts
|
||||
@@ -1561,6 +1651,29 @@ Context:
|
||||
"""Get the current todo list."""
|
||||
return self._todo_list
|
||||
|
||||
def _create_agent_from_config(self, agent_config: dict) -> 'Agent':
|
||||
"""
|
||||
Create an Agent from a configuration dictionary.
|
||||
|
||||
Args:
|
||||
agent_config: Dict with keys like 'name', 'role', 'goal', 'backstory', 'llm', 'tools'
|
||||
|
||||
Returns:
|
||||
Agent instance
|
||||
"""
|
||||
from ..agent.agent import Agent
|
||||
|
||||
return Agent(
|
||||
name=agent_config.get('name', 'TaskAgent'),
|
||||
role=agent_config.get('role'),
|
||||
goal=agent_config.get('goal'),
|
||||
backstory=agent_config.get('backstory'),
|
||||
llm=agent_config.get('llm'),
|
||||
tools=agent_config.get('tools'),
|
||||
verbose=self.verbose >= 1,
|
||||
memory=self.memory
|
||||
)
|
||||
|
||||
def _get_planning_agent(self):
|
||||
"""Lazy load PlanningAgent."""
|
||||
if self._planning_agent is None and self.planning:
|
||||
|
||||
@@ -15,6 +15,8 @@ This module provides memory management capabilities including:
|
||||
- Auto-generated memories (like Windsurf Cascade)
|
||||
- Workflows (like Windsurf)
|
||||
- Hooks (like Windsurf Cascade Hooks)
|
||||
- Docs management (like Cursor docs)
|
||||
- MCP config management (like Cursor .cursor/mcp/)
|
||||
|
||||
Memory Providers:
|
||||
- FileMemory: Zero-dependency JSON file-based storage (default)
|
||||
@@ -23,10 +25,14 @@ Memory Providers:
|
||||
- AutoMemory: Automatic memory extraction from conversations
|
||||
- WorkflowManager: Multi-step workflow execution
|
||||
- HooksManager: Pre/post operation hooks
|
||||
- DocsManager: Documentation context management
|
||||
- MCPConfigManager: MCP server configuration management
|
||||
"""
|
||||
|
||||
from .file_memory import FileMemory, create_memory
|
||||
from .rules_manager import RulesManager, Rule, create_rules_manager
|
||||
from .docs_manager import DocsManager, Doc
|
||||
from .mcp_config import MCPConfigManager, MCPConfig
|
||||
|
||||
# Lazy imports for optional modules to avoid dependency issues and improve startup time
|
||||
def __getattr__(name):
|
||||
@@ -42,17 +48,58 @@ def __getattr__(name):
|
||||
if name == "create_auto_memory":
|
||||
from .auto_memory import create_auto_memory
|
||||
return create_auto_memory
|
||||
# Workflows - now in dedicated module, re-export for backward compatibility
|
||||
if name == "WorkflowManager":
|
||||
from .workflows import WorkflowManager
|
||||
from ..workflows import WorkflowManager
|
||||
return WorkflowManager
|
||||
if name == "Workflow":
|
||||
from .workflows import Workflow
|
||||
from ..workflows import Workflow
|
||||
return Workflow
|
||||
if name == "WorkflowStep":
|
||||
from .workflows import WorkflowStep
|
||||
from ..workflows import WorkflowStep
|
||||
return WorkflowStep
|
||||
if name == "WorkflowContext":
|
||||
from ..workflows import WorkflowContext
|
||||
return WorkflowContext
|
||||
if name == "StepResult":
|
||||
from ..workflows import StepResult
|
||||
return StepResult
|
||||
if name == "Pipeline":
|
||||
from ..workflows import Pipeline
|
||||
return Pipeline
|
||||
if name == "Route":
|
||||
from ..workflows import Route
|
||||
return Route
|
||||
if name == "Parallel":
|
||||
from ..workflows import Parallel
|
||||
return Parallel
|
||||
if name == "Loop":
|
||||
from ..workflows import Loop
|
||||
return Loop
|
||||
if name == "Repeat":
|
||||
from ..workflows import Repeat
|
||||
return Repeat
|
||||
if name == "route":
|
||||
from ..workflows import route
|
||||
return route
|
||||
if name == "parallel":
|
||||
from ..workflows import parallel
|
||||
return parallel
|
||||
if name == "loop":
|
||||
from ..workflows import loop
|
||||
return loop
|
||||
if name == "repeat":
|
||||
from ..workflows import repeat
|
||||
return repeat
|
||||
# Backward compatibility aliases
|
||||
if name == "StepInput":
|
||||
from ..workflows import WorkflowContext
|
||||
return WorkflowContext
|
||||
if name == "StepOutput":
|
||||
from ..workflows import StepResult
|
||||
return StepResult
|
||||
if name == "create_workflow_manager":
|
||||
from .workflows import create_workflow_manager
|
||||
from ..workflows.workflows import create_workflow_manager
|
||||
return create_workflow_manager
|
||||
if name == "HooksManager":
|
||||
from .hooks import HooksManager
|
||||
@@ -63,6 +110,7 @@ def __getattr__(name):
|
||||
if name == "create_hooks_manager":
|
||||
from .hooks import create_hooks_manager
|
||||
return create_hooks_manager
|
||||
# DocsManager and MCPConfigManager are already imported at module level
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
__all__ = [
|
||||
@@ -78,13 +126,31 @@ __all__ = [
|
||||
"AutoMemory",
|
||||
"AutoMemoryExtractor",
|
||||
"create_auto_memory",
|
||||
# Workflows
|
||||
# Workflows (re-exported from praisonaiagents.workflows for backward compatibility)
|
||||
"WorkflowManager",
|
||||
"Workflow",
|
||||
"Pipeline",
|
||||
"WorkflowStep",
|
||||
"WorkflowContext",
|
||||
"StepResult",
|
||||
"create_workflow_manager",
|
||||
# Workflow patterns
|
||||
"Route",
|
||||
"Parallel",
|
||||
"Loop",
|
||||
"Repeat",
|
||||
"route",
|
||||
"parallel",
|
||||
"loop",
|
||||
"repeat",
|
||||
# Hooks
|
||||
"HooksManager",
|
||||
"HookResult",
|
||||
"create_hooks_manager",
|
||||
# Docs management
|
||||
"DocsManager",
|
||||
"Doc",
|
||||
# MCP config management
|
||||
"MCPConfigManager",
|
||||
"MCPConfig",
|
||||
]
|
||||
@@ -0,0 +1,431 @@
|
||||
"""
|
||||
Docs Manager for PraisonAI Agents.
|
||||
|
||||
Provides reusable documentation context support similar to:
|
||||
- Cursor Notepads (deprecated in favor of docs)
|
||||
- Cursor .cursor/docs/
|
||||
- Project documentation for AI context
|
||||
|
||||
Features:
|
||||
- Auto-discovery of docs from .praison/docs/
|
||||
- Support for markdown, text, and code files
|
||||
- @doc mentions for including specific docs
|
||||
- Priority ordering for docs
|
||||
- Character limits for context window management
|
||||
|
||||
Storage Structure:
|
||||
.praison/docs/
|
||||
├── project-overview.md # Project overview
|
||||
├── architecture.md # Architecture decisions
|
||||
├── api-reference.md # API documentation
|
||||
└── coding-standards.md # Coding guidelines
|
||||
|
||||
Doc File Format (YAML frontmatter + Markdown):
|
||||
---
|
||||
description: Project architecture overview
|
||||
priority: 10
|
||||
tags: [architecture, design]
|
||||
---
|
||||
|
||||
# Architecture Overview
|
||||
This project uses...
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Literal
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Doc:
|
||||
"""A single documentation file with metadata and content."""
|
||||
name: str
|
||||
content: str
|
||||
description: str = ""
|
||||
priority: int = 0
|
||||
tags: List[str] = field(default_factory=list)
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def matches_tag(self, tag: str) -> bool:
|
||||
"""Check if this doc has a specific tag."""
|
||||
return tag.lower() in [t.lower() for t in self.tags]
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": self.content,
|
||||
"description": self.description,
|
||||
"priority": self.priority,
|
||||
"tags": self.tags,
|
||||
"file_path": self.file_path
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Doc':
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class DocsManager:
|
||||
"""
|
||||
Manages documentation context for AI agents.
|
||||
|
||||
Provides:
|
||||
- Auto-discovery of docs from .praison/docs/
|
||||
- Global docs from ~/.praison/docs/
|
||||
- @doc mentions for including specific docs
|
||||
- Priority-based ordering
|
||||
"""
|
||||
|
||||
DOCS_DIR_NAME = ".praison/docs"
|
||||
SUPPORTED_EXTENSIONS = [".md", ".txt", ".rst"]
|
||||
|
||||
# Maximum characters per doc (context window management)
|
||||
MAX_DOC_CHARS = 12000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
global_docs_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize DocsManager.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
global_docs_path: Path to global docs (default: ~/.praison/docs)
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.global_docs_path = Path(global_docs_path) if global_docs_path else Path.home() / ".praison" / "docs"
|
||||
self.verbose = verbose
|
||||
|
||||
self._docs: Dict[str, Doc] = {}
|
||||
self._load_all_docs()
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _parse_frontmatter(self, content: str) -> tuple:
|
||||
"""
|
||||
Parse YAML frontmatter from content.
|
||||
|
||||
Returns:
|
||||
Tuple of (frontmatter_dict, remaining_content)
|
||||
"""
|
||||
frontmatter = {}
|
||||
remaining = content
|
||||
|
||||
# Check for YAML frontmatter
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
import yaml
|
||||
frontmatter = yaml.safe_load(parts[1]) or {}
|
||||
remaining = parts[2].strip()
|
||||
except Exception:
|
||||
# If YAML parsing fails, use content as-is
|
||||
pass
|
||||
|
||||
return frontmatter, remaining
|
||||
|
||||
def _load_doc_file(self, file_path: Path, scope: str = "workspace") -> Optional[Doc]:
|
||||
"""Load a single doc file."""
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
frontmatter, doc_content = self._parse_frontmatter(content)
|
||||
|
||||
name = file_path.stem
|
||||
|
||||
# Determine priority based on scope
|
||||
base_priority = 100 if scope == "workspace" else 0
|
||||
|
||||
doc = Doc(
|
||||
name=name,
|
||||
content=doc_content,
|
||||
description=frontmatter.get("description", ""),
|
||||
priority=frontmatter.get("priority", base_priority),
|
||||
tags=frontmatter.get("tags", []),
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
return doc
|
||||
except Exception as e:
|
||||
self._log(f"Failed to load doc {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
|
||||
def _load_docs_from_dir(self, docs_dir: Path, scope: str = "workspace"):
|
||||
"""Load all docs from a directory."""
|
||||
if not docs_dir.exists():
|
||||
return
|
||||
|
||||
for ext in self.SUPPORTED_EXTENSIONS:
|
||||
for file_path in docs_dir.glob(f"*{ext}"):
|
||||
if file_path.is_file():
|
||||
doc = self._load_doc_file(file_path, scope)
|
||||
if doc:
|
||||
key = f"{scope}:{doc.name}"
|
||||
self._docs[key] = doc
|
||||
self._log(f"Loaded doc: {key}")
|
||||
|
||||
def _load_all_docs(self):
|
||||
"""Load all docs from global and workspace directories."""
|
||||
self._docs.clear()
|
||||
|
||||
# 1. Load global docs (lowest priority)
|
||||
if self.global_docs_path.exists():
|
||||
self._load_docs_from_dir(self.global_docs_path, "global")
|
||||
for key, doc in self._docs.items():
|
||||
if key.startswith("global:"):
|
||||
doc.priority = doc.priority - 1000 # Lower priority for global
|
||||
|
||||
# 2. Load workspace docs
|
||||
workspace_docs_dir = self.workspace_path / self.DOCS_DIR_NAME.replace("/", os.sep)
|
||||
if workspace_docs_dir.exists():
|
||||
self._load_docs_from_dir(workspace_docs_dir, "workspace")
|
||||
|
||||
self._log(f"Loaded {len(self._docs)} docs total")
|
||||
|
||||
def reload(self):
|
||||
"""Reload all docs from disk."""
|
||||
self._load_all_docs()
|
||||
|
||||
def get_all_docs(self) -> List[Doc]:
|
||||
"""Get all loaded docs, sorted by priority."""
|
||||
docs = list(self._docs.values())
|
||||
docs.sort(key=lambda d: d.priority, reverse=True)
|
||||
return docs
|
||||
|
||||
def get_doc(self, name: str) -> Optional[Doc]:
|
||||
"""Get a specific doc by name."""
|
||||
# Try workspace first, then global
|
||||
for scope in ["workspace", "global"]:
|
||||
key = f"{scope}:{name}"
|
||||
if key in self._docs:
|
||||
return self._docs[key]
|
||||
return None
|
||||
|
||||
def get_docs_by_tag(self, tag: str) -> List[Doc]:
|
||||
"""Get all docs with a specific tag."""
|
||||
docs = [d for d in self._docs.values() if d.matches_tag(tag)]
|
||||
docs.sort(key=lambda d: d.priority, reverse=True)
|
||||
return docs
|
||||
|
||||
def get_docs_for_context(
|
||||
self,
|
||||
include_docs: Optional[List[str]] = None,
|
||||
include_tags: Optional[List[str]] = None,
|
||||
max_chars: int = 50000
|
||||
) -> List[Doc]:
|
||||
"""
|
||||
Get docs for inclusion in agent context.
|
||||
|
||||
Args:
|
||||
include_docs: Specific doc names to include (via @doc mentions)
|
||||
include_tags: Tags to filter by
|
||||
max_chars: Maximum total characters
|
||||
|
||||
Returns:
|
||||
List of docs sorted by priority
|
||||
"""
|
||||
active = []
|
||||
|
||||
# Include specific docs by name
|
||||
if include_docs:
|
||||
for name in include_docs:
|
||||
doc = self.get_doc(name)
|
||||
if doc and doc not in active:
|
||||
active.append(doc)
|
||||
|
||||
# Include docs by tag
|
||||
if include_tags:
|
||||
for tag in include_tags:
|
||||
for doc in self.get_docs_by_tag(tag):
|
||||
if doc not in active:
|
||||
active.append(doc)
|
||||
|
||||
# If no specific docs requested, include all high-priority docs
|
||||
if not include_docs and not include_tags:
|
||||
for doc in self.get_all_docs():
|
||||
if doc.priority >= 100: # Only high-priority docs by default
|
||||
active.append(doc)
|
||||
|
||||
# Sort by priority
|
||||
active.sort(key=lambda d: d.priority, reverse=True)
|
||||
|
||||
return active
|
||||
|
||||
def format_docs_for_prompt(
|
||||
self,
|
||||
include_docs: Optional[List[str]] = None,
|
||||
include_tags: Optional[List[str]] = None,
|
||||
max_chars: int = 50000
|
||||
) -> str:
|
||||
"""
|
||||
Format docs as a string for inclusion in prompts.
|
||||
|
||||
Args:
|
||||
include_docs: Specific doc names to include
|
||||
include_tags: Tags to filter by
|
||||
max_chars: Maximum total characters
|
||||
|
||||
Returns:
|
||||
Formatted docs string
|
||||
"""
|
||||
active_docs = self.get_docs_for_context(include_docs, include_tags, max_chars)
|
||||
|
||||
if not active_docs:
|
||||
return ""
|
||||
|
||||
parts = ["# Project Documentation\n"]
|
||||
total_chars = len(parts[0])
|
||||
|
||||
for doc in active_docs:
|
||||
if total_chars >= max_chars:
|
||||
break
|
||||
|
||||
doc_text = doc.content.strip()
|
||||
if not doc_text:
|
||||
continue
|
||||
|
||||
# Add doc with header
|
||||
if doc.description:
|
||||
header = f"## {doc.name}: {doc.description}"
|
||||
else:
|
||||
header = f"## {doc.name}"
|
||||
|
||||
section = f"{header}\n{doc_text}\n"
|
||||
|
||||
if total_chars + len(section) <= max_chars:
|
||||
parts.append(section)
|
||||
total_chars += len(section)
|
||||
else:
|
||||
# Truncate last doc
|
||||
remaining = max_chars - total_chars
|
||||
if remaining > 100:
|
||||
parts.append(section[:remaining] + "\n... (truncated)")
|
||||
break
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def create_doc(
|
||||
self,
|
||||
name: str,
|
||||
content: str,
|
||||
description: str = "",
|
||||
priority: int = 0,
|
||||
tags: Optional[List[str]] = None,
|
||||
scope: Literal["global", "workspace"] = "workspace"
|
||||
) -> Doc:
|
||||
"""
|
||||
Create a new doc file.
|
||||
|
||||
Args:
|
||||
name: Doc name (used as filename)
|
||||
content: Doc content (markdown)
|
||||
description: Short description
|
||||
priority: Priority (higher = applied first)
|
||||
tags: Tags for categorization
|
||||
scope: Where to save (global or workspace)
|
||||
|
||||
Returns:
|
||||
Created Doc object
|
||||
"""
|
||||
# Determine save path
|
||||
if scope == "global":
|
||||
docs_dir = self.global_docs_path
|
||||
else:
|
||||
docs_dir = self.workspace_path / self.DOCS_DIR_NAME.replace("/", os.sep)
|
||||
|
||||
docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = docs_dir / f"{name}.md"
|
||||
|
||||
# Build frontmatter
|
||||
frontmatter_lines = ["---"]
|
||||
if description:
|
||||
frontmatter_lines.append(f'description: "{description}"')
|
||||
if priority != 0:
|
||||
frontmatter_lines.append(f"priority: {priority}")
|
||||
if tags:
|
||||
tags_str = ", ".join(f'"{t}"' for t in tags)
|
||||
frontmatter_lines.append(f"tags: [{tags_str}]")
|
||||
frontmatter_lines.append("---")
|
||||
frontmatter_lines.append("")
|
||||
|
||||
# Write file
|
||||
full_content = "\n".join(frontmatter_lines) + content
|
||||
file_path.write_text(full_content, encoding="utf-8")
|
||||
|
||||
# Create and register doc
|
||||
doc = Doc(
|
||||
name=name,
|
||||
content=content,
|
||||
description=description,
|
||||
priority=priority,
|
||||
tags=tags or [],
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
self._docs[f"{scope}:{name}"] = doc
|
||||
self._log(f"Created doc '{name}' at {file_path}")
|
||||
|
||||
return doc
|
||||
|
||||
def delete_doc(self, name: str, scope: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Delete a doc file.
|
||||
|
||||
Args:
|
||||
name: Doc name to delete
|
||||
scope: Scope to delete from (None = try both)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
scopes = [scope] if scope else ["workspace", "global"]
|
||||
|
||||
for s in scopes:
|
||||
key = f"{s}:{name}"
|
||||
if key in self._docs:
|
||||
doc = self._docs[key]
|
||||
if doc.file_path:
|
||||
try:
|
||||
Path(doc.file_path).unlink()
|
||||
del self._docs[key]
|
||||
self._log(f"Deleted doc '{name}' from {s}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._log(f"Failed to delete doc file: {e}", logging.ERROR)
|
||||
|
||||
return False
|
||||
|
||||
def list_docs(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all docs with metadata.
|
||||
|
||||
Returns:
|
||||
List of doc info dicts
|
||||
"""
|
||||
docs = []
|
||||
for key, doc in self._docs.items():
|
||||
scope = key.split(":")[0]
|
||||
docs.append({
|
||||
"name": doc.name,
|
||||
"scope": scope,
|
||||
"description": doc.description,
|
||||
"priority": doc.priority,
|
||||
"tags": doc.tags,
|
||||
"file_path": doc.file_path,
|
||||
"content_length": len(doc.content)
|
||||
})
|
||||
|
||||
# Sort by priority
|
||||
docs.sort(key=lambda d: d["priority"], reverse=True)
|
||||
return docs
|
||||
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
MCP Config Manager for PraisonAI Agents.
|
||||
|
||||
Provides JSON-based MCP server configuration support similar to:
|
||||
- Cursor .cursor/mcp/*.mcpjson
|
||||
- Windsurf MCP configuration
|
||||
|
||||
Features:
|
||||
- Auto-discovery of MCP configs from .praison/mcp/
|
||||
- JSON config files for MCP servers
|
||||
- Environment variable support
|
||||
- Easy server management
|
||||
|
||||
Storage Structure:
|
||||
.praison/mcp/
|
||||
├── filesystem.json # Filesystem MCP server
|
||||
├── brave-search.json # Brave search MCP server
|
||||
└── postgres.json # PostgreSQL MCP server
|
||||
|
||||
Config File Format (JSON):
|
||||
{
|
||||
"name": "filesystem",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||
"env": {
|
||||
"SOME_VAR": "value"
|
||||
},
|
||||
"enabled": true,
|
||||
"description": "Filesystem access for the agent"
|
||||
}
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPConfig:
|
||||
"""A single MCP server configuration."""
|
||||
name: str
|
||||
command: str
|
||||
args: List[str] = field(default_factory=list)
|
||||
env: Dict[str, str] = field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
description: str = ""
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"command": self.command,
|
||||
"args": self.args,
|
||||
"env": self.env,
|
||||
"enabled": self.enabled,
|
||||
"description": self.description
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any], file_path: Optional[str] = None) -> 'MCPConfig':
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
command=data.get("command", ""),
|
||||
args=data.get("args", []),
|
||||
env=data.get("env", {}),
|
||||
enabled=data.get("enabled", True),
|
||||
description=data.get("description", ""),
|
||||
file_path=file_path
|
||||
)
|
||||
|
||||
def to_mcp_instance(self):
|
||||
"""
|
||||
Convert this config to an MCP instance.
|
||||
|
||||
Returns:
|
||||
MCP instance ready for use with agents
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents.tools.mcp import MCP
|
||||
|
||||
# Resolve environment variables
|
||||
resolved_env = {}
|
||||
for key, value in self.env.items():
|
||||
if value.startswith("$"):
|
||||
# Environment variable reference
|
||||
env_var = value[1:]
|
||||
resolved_env[key] = os.environ.get(env_var, value)
|
||||
else:
|
||||
resolved_env[key] = value
|
||||
|
||||
return MCP(
|
||||
command=self.command,
|
||||
args=self.args,
|
||||
env=resolved_env if resolved_env else None
|
||||
)
|
||||
except ImportError:
|
||||
logger.warning("MCP tools not available")
|
||||
return None
|
||||
|
||||
|
||||
class MCPConfigManager:
|
||||
"""
|
||||
Manages MCP server configurations for AI agents.
|
||||
|
||||
Provides:
|
||||
- Auto-discovery of MCP configs from .praison/mcp/
|
||||
- Global configs from ~/.praison/mcp/
|
||||
- Easy server management
|
||||
"""
|
||||
|
||||
MCP_DIR_NAME = ".praison/mcp"
|
||||
SUPPORTED_EXTENSIONS = [".json", ".mcpjson"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
global_mcp_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize MCPConfigManager.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
global_mcp_path: Path to global MCP configs (default: ~/.praison/mcp)
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.global_mcp_path = Path(global_mcp_path) if global_mcp_path else Path.home() / ".praison" / "mcp"
|
||||
self.verbose = verbose
|
||||
|
||||
self._configs: Dict[str, MCPConfig] = {}
|
||||
self._load_all_configs()
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _load_config_file(self, file_path: Path, scope: str = "workspace") -> Optional[MCPConfig]:
|
||||
"""Load a single MCP config file."""
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
data = json.loads(content)
|
||||
|
||||
# Use filename as name if not specified
|
||||
if not data.get("name"):
|
||||
data["name"] = file_path.stem
|
||||
|
||||
config = MCPConfig.from_dict(data, str(file_path))
|
||||
return config
|
||||
except json.JSONDecodeError as e:
|
||||
self._log(f"Invalid JSON in {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
except Exception as e:
|
||||
self._log(f"Failed to load MCP config {file_path}: {e}", logging.WARNING)
|
||||
return None
|
||||
|
||||
def _load_configs_from_dir(self, mcp_dir: Path, scope: str = "workspace"):
|
||||
"""Load all MCP configs from a directory."""
|
||||
if not mcp_dir.exists():
|
||||
return
|
||||
|
||||
for ext in self.SUPPORTED_EXTENSIONS:
|
||||
for file_path in mcp_dir.glob(f"*{ext}"):
|
||||
if file_path.is_file():
|
||||
config = self._load_config_file(file_path, scope)
|
||||
if config:
|
||||
key = f"{scope}:{config.name}"
|
||||
self._configs[key] = config
|
||||
self._log(f"Loaded MCP config: {key}")
|
||||
|
||||
def _load_all_configs(self):
|
||||
"""Load all MCP configs from global and workspace directories."""
|
||||
self._configs.clear()
|
||||
|
||||
# 1. Load global configs
|
||||
if self.global_mcp_path.exists():
|
||||
self._load_configs_from_dir(self.global_mcp_path, "global")
|
||||
|
||||
# 2. Load workspace configs (override global)
|
||||
workspace_mcp_dir = self.workspace_path / self.MCP_DIR_NAME.replace("/", os.sep)
|
||||
if workspace_mcp_dir.exists():
|
||||
self._load_configs_from_dir(workspace_mcp_dir, "workspace")
|
||||
|
||||
self._log(f"Loaded {len(self._configs)} MCP configs total")
|
||||
|
||||
def reload(self):
|
||||
"""Reload all configs from disk."""
|
||||
self._load_all_configs()
|
||||
|
||||
def get_all_configs(self) -> List[MCPConfig]:
|
||||
"""Get all loaded MCP configs."""
|
||||
return list(self._configs.values())
|
||||
|
||||
def get_enabled_configs(self) -> List[MCPConfig]:
|
||||
"""Get all enabled MCP configs."""
|
||||
return [c for c in self._configs.values() if c.enabled]
|
||||
|
||||
def get_config(self, name: str) -> Optional[MCPConfig]:
|
||||
"""Get a specific MCP config by name."""
|
||||
# Try workspace first, then global
|
||||
for scope in ["workspace", "global"]:
|
||||
key = f"{scope}:{name}"
|
||||
if key in self._configs:
|
||||
return self._configs[key]
|
||||
return None
|
||||
|
||||
def get_mcp_tools(self, names: Optional[List[str]] = None) -> List:
|
||||
"""
|
||||
Get MCP instances for use with agents.
|
||||
|
||||
Args:
|
||||
names: Specific config names to load (None = all enabled)
|
||||
|
||||
Returns:
|
||||
List of MCP instances
|
||||
"""
|
||||
tools = []
|
||||
|
||||
if names:
|
||||
configs = [self.get_config(name) for name in names]
|
||||
configs = [c for c in configs if c and c.enabled]
|
||||
else:
|
||||
configs = self.get_enabled_configs()
|
||||
|
||||
for config in configs:
|
||||
mcp = config.to_mcp_instance()
|
||||
if mcp:
|
||||
tools.append(mcp)
|
||||
|
||||
return tools
|
||||
|
||||
def create_config(
|
||||
self,
|
||||
name: str,
|
||||
command: str,
|
||||
args: Optional[List[str]] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
enabled: bool = True,
|
||||
description: str = "",
|
||||
scope: str = "workspace"
|
||||
) -> MCPConfig:
|
||||
"""
|
||||
Create a new MCP config file.
|
||||
|
||||
Args:
|
||||
name: Config name (used as filename)
|
||||
command: Command to run (e.g., "npx")
|
||||
args: Command arguments
|
||||
env: Environment variables
|
||||
enabled: Whether the config is enabled
|
||||
description: Description of the MCP server
|
||||
scope: Where to save (global or workspace)
|
||||
|
||||
Returns:
|
||||
Created MCPConfig object
|
||||
"""
|
||||
# Determine save path
|
||||
if scope == "global":
|
||||
mcp_dir = self.global_mcp_path
|
||||
else:
|
||||
mcp_dir = self.workspace_path / self.MCP_DIR_NAME.replace("/", os.sep)
|
||||
|
||||
mcp_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = mcp_dir / f"{name}.json"
|
||||
|
||||
# Build config
|
||||
config = MCPConfig(
|
||||
name=name,
|
||||
command=command,
|
||||
args=args or [],
|
||||
env=env or {},
|
||||
enabled=enabled,
|
||||
description=description,
|
||||
file_path=str(file_path)
|
||||
)
|
||||
|
||||
# Write file
|
||||
file_path.write_text(
|
||||
json.dumps(config.to_dict(), indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self._configs[f"{scope}:{name}"] = config
|
||||
self._log(f"Created MCP config '{name}' at {file_path}")
|
||||
|
||||
return config
|
||||
|
||||
def delete_config(self, name: str, scope: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Delete an MCP config file.
|
||||
|
||||
Args:
|
||||
name: Config name to delete
|
||||
scope: Scope to delete from (None = try both)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
scopes = [scope] if scope else ["workspace", "global"]
|
||||
|
||||
for s in scopes:
|
||||
key = f"{s}:{name}"
|
||||
if key in self._configs:
|
||||
config = self._configs[key]
|
||||
if config.file_path:
|
||||
try:
|
||||
Path(config.file_path).unlink()
|
||||
del self._configs[key]
|
||||
self._log(f"Deleted MCP config '{name}' from {s}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._log(f"Failed to delete config file: {e}", logging.ERROR)
|
||||
|
||||
return False
|
||||
|
||||
def enable_config(self, name: str) -> bool:
|
||||
"""Enable an MCP config."""
|
||||
config = self.get_config(name)
|
||||
if config:
|
||||
config.enabled = True
|
||||
self._save_config(config)
|
||||
return True
|
||||
return False
|
||||
|
||||
def disable_config(self, name: str) -> bool:
|
||||
"""Disable an MCP config."""
|
||||
config = self.get_config(name)
|
||||
if config:
|
||||
config.enabled = False
|
||||
self._save_config(config)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _save_config(self, config: MCPConfig):
|
||||
"""Save a config back to its file."""
|
||||
if config.file_path:
|
||||
Path(config.file_path).write_text(
|
||||
json.dumps(config.to_dict(), indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
def list_configs(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all MCP configs with metadata.
|
||||
|
||||
Returns:
|
||||
List of config info dicts
|
||||
"""
|
||||
configs = []
|
||||
for key, config in self._configs.items():
|
||||
scope = key.split(":")[0]
|
||||
configs.append({
|
||||
"name": config.name,
|
||||
"scope": scope,
|
||||
"command": config.command,
|
||||
"args": config.args,
|
||||
"enabled": config.enabled,
|
||||
"description": config.description,
|
||||
"file_path": config.file_path
|
||||
})
|
||||
|
||||
return configs
|
||||
File diff suppressed because it is too large.
Load diff
@@ -826,7 +826,34 @@ Provide a JSON with the structure:
|
||||
logging.info("Hierarchical task execution finished")
|
||||
|
||||
def workflow(self):
|
||||
"""Synchronous version of workflow method"""
|
||||
"""Synchronous version of workflow method.
|
||||
|
||||
DEPRECATED: This method is deprecated. Use the Workflow class instead:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Workflow, StepResult
|
||||
from praisonaiagents.workflows import route, parallel, loop, repeat
|
||||
|
||||
workflow = Workflow(steps=[step1, step2])
|
||||
result = workflow.start("input")
|
||||
```
|
||||
|
||||
The Workflow class provides a simpler API with more features:
|
||||
- route() for decision-based branching
|
||||
- parallel() for concurrent execution
|
||||
- loop() for iteration over lists/CSV
|
||||
- repeat() for evaluator-optimizer patterns
|
||||
- Callbacks (on_step_start, on_step_complete, etc.)
|
||||
- Guardrails with validation feedback
|
||||
- Status tracking
|
||||
"""
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"process='workflow' is deprecated. Use the Workflow class instead: "
|
||||
"from praisonaiagents import Workflow; workflow = Workflow(steps=[...]); workflow.start()",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
current_iter = 0 # Track how many times we've looped
|
||||
# Build workflow relationships first
|
||||
for task in self.tasks.values():
|
||||
|
||||
@@ -44,7 +44,9 @@ class Task:
|
||||
retain_full_context: bool = False, # By default, only use previous task output, not all previous tasks
|
||||
guardrail: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None,
|
||||
max_retries: int = 3,
|
||||
retry_count: int = 0
|
||||
retry_count: int = 0,
|
||||
agent_config: Optional[Dict[str, Any]] = None,
|
||||
variables: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
# Add check if memory config is provided
|
||||
if memory is not None or (config and config.get('memory_config')):
|
||||
@@ -89,6 +91,8 @@ class Task:
|
||||
self.retry_count = retry_count
|
||||
self._guardrail_fn = None
|
||||
self.validation_feedback = None # Store validation failure feedback for retry attempts
|
||||
self.agent_config = agent_config # Per-task agent configuration {role, goal, backstory, llm}
|
||||
self.variables = variables if variables else {} # Variables for substitution in description
|
||||
|
||||
# Set logger level based on config verbose level
|
||||
verbose = self.config.get("verbose", 0)
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Mentions Parser for PraisonAI Agents.
|
||||
|
||||
Provides @mention syntax support similar to Cursor IDE:
|
||||
- @file:path/to/file.py - Include file content
|
||||
- @web:query - Search the web
|
||||
- @doc:name - Include doc from .praison/docs/
|
||||
- @rule:name - Include specific rule
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser()
|
||||
context, cleaned_prompt = parser.process("@file:main.py explain this code")
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MentionsParser:
|
||||
"""
|
||||
Parse and process @mentions in prompts.
|
||||
|
||||
Supports:
|
||||
- @file:path - Include file content
|
||||
- @web:query - Web search (requires web search tool)
|
||||
- @doc:name - Include doc from .praison/docs/
|
||||
- @rule:name - Include specific rule
|
||||
- @url:https://... - Fetch URL content
|
||||
"""
|
||||
|
||||
# Patterns for different mention types
|
||||
PATTERNS = {
|
||||
"file": re.compile(r'@file:([^\s]+)'),
|
||||
"web": re.compile(r'@web:([^\s]+(?:\s+[^\s@]+)*)'),
|
||||
"doc": re.compile(r'@doc:([^\s]+)'),
|
||||
"rule": re.compile(r'@rule:([^\s]+)'),
|
||||
"url": re.compile(r'@url:(https?://[^\s]+)'),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize MentionsParser.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
verbose: Verbosity level
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.verbose = verbose
|
||||
|
||||
# Lazy-loaded managers
|
||||
self._docs_manager = None
|
||||
self._rules_manager = None
|
||||
|
||||
def _log(self, msg: str, level: int = logging.INFO):
|
||||
"""Log message if verbose."""
|
||||
if self.verbose >= 1:
|
||||
logger.log(level, msg)
|
||||
|
||||
def _get_docs_manager(self):
|
||||
"""Lazy load DocsManager."""
|
||||
if self._docs_manager is None:
|
||||
try:
|
||||
from praisonaiagents.memory.docs_manager import DocsManager
|
||||
self._docs_manager = DocsManager(workspace_path=str(self.workspace_path))
|
||||
except ImportError:
|
||||
self._log("DocsManager not available", logging.WARNING)
|
||||
return self._docs_manager
|
||||
|
||||
def _get_rules_manager(self):
|
||||
"""Lazy load RulesManager."""
|
||||
if self._rules_manager is None:
|
||||
try:
|
||||
from praisonaiagents.memory.rules_manager import RulesManager
|
||||
self._rules_manager = RulesManager(workspace_path=str(self.workspace_path))
|
||||
except ImportError:
|
||||
self._log("RulesManager not available", logging.WARNING)
|
||||
return self._rules_manager
|
||||
|
||||
def process(self, prompt: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Process a prompt and extract @mentions.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt with @mentions
|
||||
|
||||
Returns:
|
||||
Tuple of (context_string, cleaned_prompt)
|
||||
"""
|
||||
context_parts = []
|
||||
cleaned_prompt = prompt
|
||||
|
||||
# Process each mention type
|
||||
for mention_type, pattern in self.PATTERNS.items():
|
||||
matches = pattern.findall(prompt)
|
||||
for match in matches:
|
||||
context = self._process_mention(mention_type, match)
|
||||
if context:
|
||||
context_parts.append(context)
|
||||
# Remove the mention from the prompt
|
||||
cleaned_prompt = pattern.sub('', cleaned_prompt, count=1)
|
||||
|
||||
# Clean up extra whitespace
|
||||
cleaned_prompt = ' '.join(cleaned_prompt.split())
|
||||
|
||||
# Build context string
|
||||
context_string = ""
|
||||
if context_parts:
|
||||
context_string = "\n\n".join(context_parts) + "\n\n"
|
||||
|
||||
return context_string, cleaned_prompt
|
||||
|
||||
def _process_mention(self, mention_type: str, value: str) -> Optional[str]:
|
||||
"""
|
||||
Process a single mention.
|
||||
|
||||
Args:
|
||||
mention_type: Type of mention (file, web, doc, rule, url)
|
||||
value: The mention value
|
||||
|
||||
Returns:
|
||||
Context string or None
|
||||
"""
|
||||
if mention_type == "file":
|
||||
return self._process_file_mention(value)
|
||||
elif mention_type == "web":
|
||||
return self._process_web_mention(value)
|
||||
elif mention_type == "doc":
|
||||
return self._process_doc_mention(value)
|
||||
elif mention_type == "rule":
|
||||
return self._process_rule_mention(value)
|
||||
elif mention_type == "url":
|
||||
return self._process_url_mention(value)
|
||||
return None
|
||||
|
||||
def _process_file_mention(self, file_path: str) -> Optional[str]:
|
||||
"""Process @file:path mention."""
|
||||
try:
|
||||
# Resolve path relative to workspace
|
||||
full_path = self.workspace_path / file_path
|
||||
if not full_path.exists():
|
||||
# Try as absolute path
|
||||
full_path = Path(file_path)
|
||||
|
||||
if not full_path.exists():
|
||||
self._log(f"File not found: {file_path}", logging.WARNING)
|
||||
return f"# File: {file_path}\n[File not found]"
|
||||
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
|
||||
# Limit content size
|
||||
if len(content) > 50000:
|
||||
content = content[:50000] + "\n... (truncated)"
|
||||
|
||||
# Detect language for code fence
|
||||
ext = full_path.suffix.lower()
|
||||
lang_map = {
|
||||
".py": "python",
|
||||
".js": "javascript",
|
||||
".ts": "typescript",
|
||||
".jsx": "jsx",
|
||||
".tsx": "tsx",
|
||||
".java": "java",
|
||||
".cpp": "cpp",
|
||||
".c": "c",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
".rb": "ruby",
|
||||
".php": "php",
|
||||
".swift": "swift",
|
||||
".kt": "kotlin",
|
||||
".md": "markdown",
|
||||
".json": "json",
|
||||
".yaml": "yaml",
|
||||
".yml": "yaml",
|
||||
".xml": "xml",
|
||||
".html": "html",
|
||||
".css": "css",
|
||||
".sql": "sql",
|
||||
".sh": "bash",
|
||||
}
|
||||
lang = lang_map.get(ext, "")
|
||||
|
||||
return f"# File: {file_path}\n```{lang}\n{content}\n```"
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error reading file {file_path}: {e}", logging.ERROR)
|
||||
return f"# File: {file_path}\n[Error reading file: {e}]"
|
||||
|
||||
def _process_web_mention(self, query: str) -> Optional[str]:
|
||||
"""Process @web:query mention."""
|
||||
try:
|
||||
# Try to use web search
|
||||
from praisonaiagents.tools import duckduckgo_search
|
||||
|
||||
results = duckduckgo_search(query, max_results=3)
|
||||
if results:
|
||||
return f"# Web Search: {query}\n{results}"
|
||||
else:
|
||||
return f"# Web Search: {query}\n[No results found]"
|
||||
|
||||
except ImportError:
|
||||
self._log("Web search not available", logging.WARNING)
|
||||
return f"# Web Search: {query}\n[Web search not available. Install with: pip install duckduckgo-search]"
|
||||
except Exception as e:
|
||||
self._log(f"Error in web search: {e}", logging.ERROR)
|
||||
return f"# Web Search: {query}\n[Error: {e}]"
|
||||
|
||||
def _process_doc_mention(self, doc_name: str) -> Optional[str]:
|
||||
"""Process @doc:name mention."""
|
||||
docs = self._get_docs_manager()
|
||||
if not docs:
|
||||
return f"# Doc: {doc_name}\n[DocsManager not available]"
|
||||
|
||||
doc = docs.get_doc(doc_name)
|
||||
if doc:
|
||||
return f"# Doc: {doc.name}\n{doc.content}"
|
||||
else:
|
||||
return f"# Doc: {doc_name}\n[Doc not found]"
|
||||
|
||||
def _process_rule_mention(self, rule_name: str) -> Optional[str]:
|
||||
"""Process @rule:name mention."""
|
||||
rules = self._get_rules_manager()
|
||||
if not rules:
|
||||
return f"# Rule: {rule_name}\n[RulesManager not available]"
|
||||
|
||||
rule = rules.get_rule(rule_name)
|
||||
if rule:
|
||||
return f"# Rule: {rule.name}\n{rule.content}"
|
||||
else:
|
||||
return f"# Rule: {rule_name}\n[Rule not found]"
|
||||
|
||||
def _process_url_mention(self, url: str) -> Optional[str]:
|
||||
"""Process @url:https://... mention."""
|
||||
try:
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={'User-Agent': 'Mozilla/5.0 (compatible; PraisonAI/1.0)'}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
content = response.read().decode('utf-8', errors='ignore')
|
||||
|
||||
# Limit content size
|
||||
if len(content) > 50000:
|
||||
content = content[:50000] + "\n... (truncated)"
|
||||
|
||||
# Try to extract text from HTML
|
||||
if '<html' in content.lower():
|
||||
# Simple HTML text extraction
|
||||
import re
|
||||
# Remove scripts and styles
|
||||
content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL | re.IGNORECASE)
|
||||
content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL | re.IGNORECASE)
|
||||
# Remove tags
|
||||
content = re.sub(r'<[^>]+>', ' ', content)
|
||||
# Clean whitespace
|
||||
content = ' '.join(content.split())
|
||||
|
||||
return f"# URL: {url}\n{content[:10000]}"
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error fetching URL {url}: {e}", logging.ERROR)
|
||||
return f"# URL: {url}\n[Error fetching URL: {e}]"
|
||||
|
||||
def extract_mentions(self, prompt: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Extract all mentions from a prompt without processing them.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt
|
||||
|
||||
Returns:
|
||||
Dict mapping mention types to lists of values
|
||||
"""
|
||||
mentions = {}
|
||||
for mention_type, pattern in self.PATTERNS.items():
|
||||
matches = pattern.findall(prompt)
|
||||
if matches:
|
||||
mentions[mention_type] = matches
|
||||
return mentions
|
||||
|
||||
def has_mentions(self, prompt: str) -> bool:
|
||||
"""Check if a prompt contains any @mentions."""
|
||||
for pattern in self.PATTERNS.values():
|
||||
if pattern.search(prompt):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def process_mentions(prompt: str, workspace_path: Optional[str] = None) -> Tuple[str, str]:
|
||||
"""
|
||||
Convenience function to process mentions in a prompt.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt with @mentions
|
||||
workspace_path: Optional workspace path
|
||||
|
||||
Returns:
|
||||
Tuple of (context_string, cleaned_prompt)
|
||||
"""
|
||||
parser = MentionsParser(workspace_path=workspace_path)
|
||||
return parser.process(prompt)
|
||||
|
||||
|
||||
# Export
|
||||
__all__ = ["MentionsParser", "process_mentions"]
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Rules Tools for PraisonAI Agents.
|
||||
|
||||
Provides tools for agents to create, read, and manage rules dynamically.
|
||||
This enables Agent-Requested rules similar to Cursor's agent-requested rules feature.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools.rules_tools import (
|
||||
create_rule_tool,
|
||||
list_rules_tool,
|
||||
get_rule_tool,
|
||||
delete_rule_tool
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
tools=[create_rule_tool, list_rules_tool, get_rule_tool]
|
||||
)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Singleton RulesManager instance
|
||||
_rules_manager = None
|
||||
|
||||
|
||||
def _get_rules_manager():
|
||||
"""Get or create the RulesManager singleton."""
|
||||
global _rules_manager
|
||||
if _rules_manager is None:
|
||||
from praisonaiagents.memory.rules_manager import RulesManager
|
||||
_rules_manager = RulesManager()
|
||||
return _rules_manager
|
||||
|
||||
|
||||
def create_rule_tool(
|
||||
name: str,
|
||||
content: str,
|
||||
description: str = "",
|
||||
globs: Optional[str] = None,
|
||||
activation: str = "always",
|
||||
priority: int = 0,
|
||||
scope: str = "workspace"
|
||||
) -> str:
|
||||
"""
|
||||
Create a new rule for the AI agent to follow.
|
||||
|
||||
This tool allows the agent to create persistent rules that will be
|
||||
applied to future conversations and tasks.
|
||||
|
||||
Args:
|
||||
name: Rule name (used as filename, no spaces or special chars)
|
||||
content: The rule content in markdown format
|
||||
description: Short description of what the rule does
|
||||
globs: Comma-separated glob patterns for file matching (e.g., "**/*.py,**/*.js")
|
||||
activation: When to apply the rule:
|
||||
- "always": Always apply this rule
|
||||
- "glob": Apply when working with files matching globs
|
||||
- "manual": Only apply when explicitly mentioned
|
||||
- "ai_decision": Let AI decide when to apply
|
||||
priority: Priority (higher = applied first, default 0)
|
||||
scope: Where to save ("workspace" or "global")
|
||||
|
||||
Returns:
|
||||
Success message with rule details
|
||||
|
||||
Example:
|
||||
create_rule_tool(
|
||||
name="python-style",
|
||||
content="- Use type hints for all functions\\n- Follow PEP 8",
|
||||
description="Python coding standards",
|
||||
globs="**/*.py",
|
||||
activation="glob"
|
||||
)
|
||||
"""
|
||||
try:
|
||||
manager = _get_rules_manager()
|
||||
|
||||
# Parse globs
|
||||
glob_list = None
|
||||
if globs:
|
||||
glob_list = [g.strip() for g in globs.split(",") if g.strip()]
|
||||
|
||||
# Validate activation
|
||||
valid_activations = ["always", "glob", "manual", "ai_decision"]
|
||||
if activation not in valid_activations:
|
||||
return f"Error: activation must be one of {valid_activations}"
|
||||
|
||||
# Validate scope
|
||||
if scope not in ["workspace", "global"]:
|
||||
return "Error: scope must be 'workspace' or 'global'"
|
||||
|
||||
# Create the rule
|
||||
rule = manager.create_rule(
|
||||
name=name,
|
||||
content=content,
|
||||
description=description,
|
||||
globs=glob_list,
|
||||
activation=activation,
|
||||
priority=priority,
|
||||
scope=scope
|
||||
)
|
||||
|
||||
return f"""Successfully created rule '{name}'
|
||||
Location: {rule.file_path}
|
||||
Activation: {activation}
|
||||
Priority: {priority}
|
||||
Description: {description or 'None'}
|
||||
Globs: {glob_list or 'None'}
|
||||
|
||||
The rule will be applied according to its activation mode."""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating rule: {e}")
|
||||
return f"Error creating rule: {str(e)}"
|
||||
|
||||
|
||||
def list_rules_tool() -> str:
|
||||
"""
|
||||
List all available rules.
|
||||
|
||||
Returns a formatted list of all rules with their metadata.
|
||||
|
||||
Returns:
|
||||
Formatted string with rule information
|
||||
"""
|
||||
try:
|
||||
manager = _get_rules_manager()
|
||||
rules = manager.get_all_rules()
|
||||
|
||||
if not rules:
|
||||
return "No rules found. Use create_rule_tool to create new rules."
|
||||
|
||||
lines = ["# Available Rules\n"]
|
||||
|
||||
for rule in rules:
|
||||
scope = "global" if "global:" in (rule.file_path or "") else "workspace"
|
||||
lines.append(f"## {rule.name}")
|
||||
lines.append(f"- **Activation**: {rule.activation}")
|
||||
lines.append(f"- **Priority**: {rule.priority}")
|
||||
if rule.description:
|
||||
lines.append(f"- **Description**: {rule.description}")
|
||||
if rule.globs:
|
||||
lines.append(f"- **Globs**: {', '.join(rule.globs)}")
|
||||
lines.append(f"- **Scope**: {scope}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing rules: {e}")
|
||||
return f"Error listing rules: {str(e)}"
|
||||
|
||||
|
||||
def get_rule_tool(name: str) -> str:
|
||||
"""
|
||||
Get the content of a specific rule.
|
||||
|
||||
Args:
|
||||
name: The name of the rule to retrieve
|
||||
|
||||
Returns:
|
||||
The rule content and metadata
|
||||
"""
|
||||
try:
|
||||
manager = _get_rules_manager()
|
||||
rule = manager.get_rule(name)
|
||||
|
||||
if not rule:
|
||||
return f"Rule '{name}' not found. Use list_rules_tool to see available rules."
|
||||
|
||||
return f"""# Rule: {rule.name}
|
||||
|
||||
**Description**: {rule.description or 'None'}
|
||||
**Activation**: {rule.activation}
|
||||
**Priority**: {rule.priority}
|
||||
**Globs**: {', '.join(rule.globs) if rule.globs else 'None'}
|
||||
**File**: {rule.file_path}
|
||||
|
||||
## Content:
|
||||
{rule.content}
|
||||
"""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting rule: {e}")
|
||||
return f"Error getting rule: {str(e)}"
|
||||
|
||||
|
||||
def delete_rule_tool(name: str, scope: Optional[str] = None) -> str:
|
||||
"""
|
||||
Delete a rule.
|
||||
|
||||
Args:
|
||||
name: The name of the rule to delete
|
||||
scope: Optional scope to delete from ("workspace" or "global")
|
||||
|
||||
Returns:
|
||||
Success or error message
|
||||
"""
|
||||
try:
|
||||
manager = _get_rules_manager()
|
||||
|
||||
if scope and scope not in ["workspace", "global"]:
|
||||
return "Error: scope must be 'workspace' or 'global'"
|
||||
|
||||
success = manager.delete_rule(name, scope)
|
||||
|
||||
if success:
|
||||
return f"Successfully deleted rule '{name}'"
|
||||
else:
|
||||
return f"Rule '{name}' not found or could not be deleted"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting rule: {e}")
|
||||
return f"Error deleting rule: {str(e)}"
|
||||
|
||||
|
||||
def get_active_rules_tool(file_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get all rules that are currently active for a given file or context.
|
||||
|
||||
Args:
|
||||
file_path: Optional file path to check which rules apply
|
||||
|
||||
Returns:
|
||||
Formatted string with active rules
|
||||
"""
|
||||
try:
|
||||
manager = _get_rules_manager()
|
||||
|
||||
if file_path:
|
||||
rules = manager.get_rules_for_file(file_path)
|
||||
else:
|
||||
rules = manager.get_active_rules()
|
||||
|
||||
if not rules:
|
||||
return "No active rules for this context."
|
||||
|
||||
lines = ["# Active Rules\n"]
|
||||
|
||||
for rule in rules:
|
||||
lines.append(f"## {rule.name}")
|
||||
if rule.description:
|
||||
lines.append(f"*{rule.description}*\n")
|
||||
lines.append(rule.content)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting active rules: {e}")
|
||||
return f"Error getting active rules: {str(e)}"
|
||||
|
||||
|
||||
# Export all tools
|
||||
__all__ = [
|
||||
"create_rule_tool",
|
||||
"list_rules_tool",
|
||||
"get_rule_tool",
|
||||
"delete_rule_tool",
|
||||
"get_active_rules_tool"
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Workflows module for PraisonAI Agents.
|
||||
|
||||
Provides workflow/pipeline patterns for orchestrating agents and functions.
|
||||
"""
|
||||
|
||||
from .workflows import (
|
||||
# Core classes
|
||||
Workflow,
|
||||
Pipeline, # Alias for Workflow
|
||||
WorkflowStep,
|
||||
WorkflowContext,
|
||||
StepResult,
|
||||
WorkflowManager,
|
||||
|
||||
# Pattern helpers
|
||||
Route,
|
||||
Parallel,
|
||||
Loop,
|
||||
Repeat,
|
||||
|
||||
# Convenience functions
|
||||
route,
|
||||
parallel,
|
||||
loop,
|
||||
repeat,
|
||||
|
||||
# Backward compatibility aliases
|
||||
StepInput,
|
||||
StepOutput,
|
||||
)
|
||||
|
||||
from .yaml_parser import YAMLWorkflowParser
|
||||
|
||||
__all__ = [
|
||||
# Core
|
||||
"Workflow",
|
||||
"Pipeline",
|
||||
"WorkflowStep",
|
||||
"WorkflowContext",
|
||||
"StepResult",
|
||||
"WorkflowManager",
|
||||
|
||||
# YAML Parser
|
||||
"YAMLWorkflowParser",
|
||||
|
||||
# Patterns
|
||||
"Route",
|
||||
"Parallel",
|
||||
"Loop",
|
||||
"Repeat",
|
||||
"route",
|
||||
"parallel",
|
||||
"loop",
|
||||
"repeat",
|
||||
|
||||
# Aliases
|
||||
"StepInput",
|
||||
"StepOutput",
|
||||
]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,752 @@
|
||||
"""
|
||||
YAML Workflow Parser for PraisonAI Agents.
|
||||
|
||||
Parses YAML workflow files into Workflow objects with support for:
|
||||
- Agent definitions with planning, reasoning, tools
|
||||
- Workflow patterns: route(), parallel(), loop(), repeat()
|
||||
- Variables and template substitution
|
||||
- Callbacks and guardrails
|
||||
- Memory configuration
|
||||
"""
|
||||
|
||||
import yaml
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
from ..agent.agent import Agent
|
||||
from .workflows import Workflow, WorkflowStep, route, parallel, loop, repeat
|
||||
|
||||
|
||||
class YAMLWorkflowParser:
|
||||
"""
|
||||
Parser for YAML workflow files.
|
||||
|
||||
Converts YAML workflow definitions into Workflow objects that can be executed.
|
||||
|
||||
Example YAML format:
|
||||
```yaml
|
||||
name: My Workflow
|
||||
description: A multi-agent workflow
|
||||
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
variables:
|
||||
topic: AI trends
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics
|
||||
instructions: "Provide research findings"
|
||||
tools:
|
||||
- tavily_search
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research {{topic}}"
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_agent]
|
||||
default: [general_agent]
|
||||
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, tool_registry: Optional[Dict[str, Callable]] = None):
|
||||
"""
|
||||
Initialize the YAML workflow parser.
|
||||
|
||||
Args:
|
||||
tool_registry: Optional dictionary mapping tool names to callable functions
|
||||
"""
|
||||
self.tool_registry = tool_registry or {}
|
||||
self._agents: Dict[str, Agent] = {}
|
||||
self._callbacks: Dict[str, Callable] = {}
|
||||
|
||||
def parse_file(self, file_path: Union[str, Path]) -> Workflow:
|
||||
"""
|
||||
Parse a YAML workflow file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the YAML file
|
||||
|
||||
Returns:
|
||||
Workflow object ready for execution
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Workflow file not found: {file_path}")
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
yaml_content = f.read()
|
||||
|
||||
return self.parse_string(yaml_content)
|
||||
|
||||
def parse_string(self, yaml_content: str) -> Workflow:
|
||||
"""
|
||||
Parse a YAML workflow string.
|
||||
|
||||
Args:
|
||||
yaml_content: YAML content as string
|
||||
|
||||
Returns:
|
||||
Workflow object ready for execution
|
||||
"""
|
||||
data = yaml.safe_load(yaml_content)
|
||||
# Normalize to canonical format (accept both old and new field names)
|
||||
normalized_data = self._normalize_yaml_config(data)
|
||||
return self._parse_workflow_data(normalized_data)
|
||||
|
||||
def _normalize_yaml_config(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Normalize YAML config to canonical format.
|
||||
|
||||
Accepts both old (agents.yaml) and new (workflow.yaml) field names.
|
||||
This implements "Accept liberally, suggest canonically" principle.
|
||||
|
||||
Canonical names (preferred):
|
||||
- agents (not roles)
|
||||
- instructions (not backstory)
|
||||
- action (not description)
|
||||
- steps (not tasks)
|
||||
- name (not topic)
|
||||
|
||||
Args:
|
||||
data: Raw parsed YAML data
|
||||
|
||||
Returns:
|
||||
Normalized data with canonical field names
|
||||
"""
|
||||
if not data:
|
||||
return data
|
||||
|
||||
normalized = data.copy()
|
||||
|
||||
# 1. Normalize workflow name: topic -> name (only if topic is used as name, not as input)
|
||||
# Note: 'topic' is legacy, 'input' is canonical for workflow input data
|
||||
if 'topic' in normalized and 'name' not in normalized and 'input' not in normalized:
|
||||
# Legacy behavior: topic was used as both name and input
|
||||
normalized['name'] = normalized.get('topic', 'Unnamed Workflow')
|
||||
|
||||
# 2. Normalize workflow input: 'input' is canonical, 'topic' is alias
|
||||
# Priority: input > topic (input takes precedence if both exist)
|
||||
if 'input' not in normalized and 'topic' in normalized:
|
||||
normalized['input'] = normalized['topic']
|
||||
|
||||
# 3. Normalize container: roles -> agents
|
||||
# Note: _convert_roles_to_agents handles backstory -> instructions
|
||||
if 'roles' in normalized and 'agents' not in normalized:
|
||||
normalized['agents'] = self._convert_roles_to_agents(normalized['roles'])
|
||||
# Extract steps from tasks nested in roles if no steps defined
|
||||
if 'steps' not in normalized:
|
||||
normalized['steps'] = self._extract_steps_from_roles(normalized['roles'])
|
||||
|
||||
# 4. Normalize agent fields: backstory -> instructions (for agents section)
|
||||
if 'agents' in normalized:
|
||||
for agent_id, agent_config in normalized['agents'].items():
|
||||
if isinstance(agent_config, dict):
|
||||
# backstory -> instructions
|
||||
if 'backstory' in agent_config and 'instructions' not in agent_config:
|
||||
agent_config['instructions'] = agent_config['backstory']
|
||||
|
||||
# 5. Normalize step fields: description -> action
|
||||
if 'steps' in normalized:
|
||||
for step in normalized['steps']:
|
||||
if isinstance(step, dict):
|
||||
# description -> action
|
||||
if 'description' in step and 'action' not in step:
|
||||
step['action'] = step['description']
|
||||
|
||||
# Handle parallel steps
|
||||
if 'parallel' in step:
|
||||
for parallel_step in step['parallel']:
|
||||
if isinstance(parallel_step, dict):
|
||||
if 'description' in parallel_step and 'action' not in parallel_step:
|
||||
parallel_step['action'] = parallel_step['description']
|
||||
|
||||
return normalized
|
||||
|
||||
def _extract_steps_from_roles(self, roles: Dict[str, Dict]) -> List[Dict]:
|
||||
"""
|
||||
Extract steps from tasks nested within roles (agents.yaml format).
|
||||
|
||||
Args:
|
||||
roles: The roles section from agents.yaml
|
||||
|
||||
Returns:
|
||||
List of step definitions
|
||||
"""
|
||||
steps = []
|
||||
for role_id, role_config in roles.items():
|
||||
if 'tasks' in role_config:
|
||||
for task_id, task_config in role_config['tasks'].items():
|
||||
step = {
|
||||
'name': task_id,
|
||||
'agent': role_id,
|
||||
}
|
||||
# description -> action
|
||||
if 'description' in task_config:
|
||||
step['action'] = task_config['description']
|
||||
elif 'action' in task_config:
|
||||
step['action'] = task_config['action']
|
||||
|
||||
# Copy other task fields
|
||||
for field in ['expected_output', 'context', 'output_file',
|
||||
'output_json', 'create_directory', 'callback',
|
||||
'async_execution', 'guardrail', 'max_retries']:
|
||||
if field in task_config:
|
||||
step[field] = task_config[field]
|
||||
|
||||
steps.append(step)
|
||||
return steps
|
||||
|
||||
def _parse_workflow_data(self, data: Dict[str, Any]) -> Workflow:
|
||||
"""
|
||||
Parse workflow data dictionary into a Workflow object.
|
||||
|
||||
Args:
|
||||
data: Parsed YAML data dictionary
|
||||
|
||||
Returns:
|
||||
Workflow object
|
||||
"""
|
||||
# Extract workflow metadata
|
||||
name = data.get('name', 'Unnamed Workflow')
|
||||
description = data.get('description', '')
|
||||
|
||||
# Extract framework and process (for feature parity with agents.yaml)
|
||||
framework = data.get('framework', 'praisonai')
|
||||
process = data.get('process', 'sequential')
|
||||
manager_llm = data.get('manager_llm')
|
||||
|
||||
# Parse workflow configuration
|
||||
workflow_config = data.get('workflow', {})
|
||||
planning = workflow_config.get('planning', False)
|
||||
planning_llm = workflow_config.get('planning_llm')
|
||||
default_llm = workflow_config.get('default_llm')
|
||||
reasoning = workflow_config.get('reasoning', False)
|
||||
verbose = workflow_config.get('verbose', False)
|
||||
memory_config = workflow_config.get('memory_config')
|
||||
|
||||
# Parse variables
|
||||
variables = data.get('variables', {})
|
||||
|
||||
# Parse agents - support both 'agents' and 'roles' keys for backward compatibility
|
||||
agents_data = data.get('agents', {})
|
||||
if not agents_data and 'roles' in data:
|
||||
# Convert roles format to agents format
|
||||
agents_data = self._convert_roles_to_agents(data['roles'])
|
||||
self._agents = self._parse_agents(agents_data)
|
||||
|
||||
# Parse callbacks
|
||||
callbacks_data = data.get('callbacks', {})
|
||||
self._parse_callbacks(callbacks_data)
|
||||
|
||||
# Parse steps
|
||||
steps_data = data.get('steps', [])
|
||||
steps = self._parse_steps(steps_data)
|
||||
|
||||
# Create workflow
|
||||
workflow = Workflow(
|
||||
name=name,
|
||||
steps=steps,
|
||||
variables=variables,
|
||||
planning=planning,
|
||||
planning_llm=planning_llm,
|
||||
default_llm=default_llm,
|
||||
reasoning=reasoning,
|
||||
verbose=verbose,
|
||||
memory_config=memory_config,
|
||||
on_workflow_start=self._callbacks.get('on_workflow_start'),
|
||||
on_step_complete=self._callbacks.get('on_step_complete'),
|
||||
on_workflow_complete=self._callbacks.get('on_workflow_complete'),
|
||||
)
|
||||
|
||||
# Store additional attributes for feature parity with agents.yaml
|
||||
workflow.description = description
|
||||
workflow.framework = framework
|
||||
workflow.process = process
|
||||
workflow.manager_llm = manager_llm
|
||||
|
||||
# Store workflow input (from 'input' or 'topic' field)
|
||||
# This is the default input passed to workflow.start() if no input is provided
|
||||
workflow.default_input = data.get('input', data.get('topic', ''))
|
||||
|
||||
return workflow
|
||||
|
||||
def _convert_roles_to_agents(self, roles: Dict[str, Dict]) -> Dict[str, Dict]:
|
||||
"""
|
||||
Convert agents.yaml 'roles' format to workflow 'agents' format.
|
||||
|
||||
This enables backward compatibility with the existing agents.yaml format.
|
||||
|
||||
Mapping:
|
||||
- backstory -> instructions
|
||||
- role -> role
|
||||
- goal -> goal
|
||||
- tools -> tools
|
||||
- llm -> llm
|
||||
- tasks -> (used to auto-generate steps if no steps defined)
|
||||
|
||||
Args:
|
||||
roles: The roles section from agents.yaml
|
||||
|
||||
Returns:
|
||||
Converted agents dictionary in workflow format
|
||||
"""
|
||||
agents = {}
|
||||
for role_id, role_config in roles.items():
|
||||
agent = {
|
||||
'name': role_config.get('role', role_id),
|
||||
'role': role_config.get('role', role_id),
|
||||
'goal': role_config.get('goal', ''),
|
||||
'instructions': role_config.get('backstory', ''),
|
||||
}
|
||||
|
||||
# Copy optional fields
|
||||
if 'llm' in role_config:
|
||||
llm_config = role_config['llm']
|
||||
if isinstance(llm_config, dict):
|
||||
agent['llm'] = llm_config.get('model', 'gpt-4o-mini')
|
||||
else:
|
||||
agent['llm'] = llm_config
|
||||
|
||||
if 'tools' in role_config:
|
||||
agent['tools'] = [t for t in role_config['tools'] if t]
|
||||
|
||||
if 'verbose' in role_config:
|
||||
agent['verbose'] = role_config['verbose']
|
||||
|
||||
if 'max_iter' in role_config:
|
||||
agent['max_iter'] = role_config['max_iter']
|
||||
|
||||
if 'planning' in role_config:
|
||||
agent['planning'] = role_config['planning']
|
||||
|
||||
if 'reasoning' in role_config:
|
||||
agent['reasoning'] = role_config['reasoning']
|
||||
|
||||
agents[role_id] = agent
|
||||
|
||||
return agents
|
||||
|
||||
def _parse_agents(self, agents_data: Dict[str, Dict]) -> Dict[str, Agent]:
|
||||
"""
|
||||
Parse agent definitions from YAML.
|
||||
|
||||
Args:
|
||||
agents_data: Dictionary of agent definitions
|
||||
|
||||
Returns:
|
||||
Dictionary mapping agent names to Agent objects
|
||||
"""
|
||||
agents = {}
|
||||
|
||||
for agent_id, agent_config in agents_data.items():
|
||||
agent = self._create_agent(agent_id, agent_config)
|
||||
agents[agent_id] = agent
|
||||
|
||||
return agents
|
||||
|
||||
def _create_agent(self, agent_id: str, config: Dict[str, Any]) -> Agent:
|
||||
"""
|
||||
Create an Agent from configuration.
|
||||
|
||||
Args:
|
||||
agent_id: Identifier for the agent
|
||||
config: Agent configuration dictionary
|
||||
|
||||
Returns:
|
||||
Agent object
|
||||
"""
|
||||
# Extract agent parameters
|
||||
name = config.get('name', agent_id)
|
||||
role = config.get('role', 'Assistant')
|
||||
goal = config.get('goal', '')
|
||||
instructions = config.get('instructions', '')
|
||||
backstory = config.get('backstory', '')
|
||||
|
||||
# LLM configuration
|
||||
llm = config.get('llm')
|
||||
|
||||
# Tools
|
||||
tools_config = config.get('tools', [])
|
||||
tools = self._resolve_tools(tools_config)
|
||||
|
||||
# Advanced parameters (feature parity with agents.yaml)
|
||||
planning = config.get('planning', False)
|
||||
reasoning = config.get('reasoning', False)
|
||||
verbose = config.get('verbose', False)
|
||||
allow_delegation = config.get('allow_delegation', False)
|
||||
max_iter = config.get('max_iter')
|
||||
cache = config.get('cache', True)
|
||||
|
||||
# Additional agents.yaml fields
|
||||
function_calling_llm = config.get('function_calling_llm')
|
||||
max_rpm = config.get('max_rpm')
|
||||
max_execution_time = config.get('max_execution_time')
|
||||
reflect_llm = config.get('reflect_llm')
|
||||
min_reflect = config.get('min_reflect')
|
||||
max_reflect = config.get('max_reflect')
|
||||
system_template = config.get('system_template')
|
||||
prompt_template = config.get('prompt_template')
|
||||
response_template = config.get('response_template')
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name=name,
|
||||
role=role,
|
||||
goal=goal,
|
||||
instructions=instructions,
|
||||
backstory=backstory,
|
||||
llm=llm,
|
||||
tools=tools if tools else None,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
# Store additional attributes for later use
|
||||
agent._yaml_planning = planning
|
||||
agent._yaml_reasoning = reasoning
|
||||
agent._yaml_allow_delegation = allow_delegation
|
||||
agent._yaml_max_iter = max_iter
|
||||
agent._yaml_cache = cache
|
||||
|
||||
# Store additional agents.yaml fields for feature parity
|
||||
agent._yaml_function_calling_llm = function_calling_llm
|
||||
agent._yaml_max_rpm = max_rpm
|
||||
agent._yaml_max_execution_time = max_execution_time
|
||||
agent._yaml_reflect_llm = reflect_llm
|
||||
agent._yaml_min_reflect = min_reflect
|
||||
agent._yaml_max_reflect = max_reflect
|
||||
agent._yaml_system_template = system_template
|
||||
agent._yaml_prompt_template = prompt_template
|
||||
agent._yaml_response_template = response_template
|
||||
|
||||
return agent
|
||||
|
||||
def _resolve_tools(self, tools_config: List[str]) -> List[Callable]:
|
||||
"""
|
||||
Resolve tool names to callable functions.
|
||||
|
||||
Args:
|
||||
tools_config: List of tool names
|
||||
|
||||
Returns:
|
||||
List of callable tool functions
|
||||
"""
|
||||
tools = []
|
||||
for tool_name in tools_config:
|
||||
if tool_name in self.tool_registry:
|
||||
tools.append(self.tool_registry[tool_name])
|
||||
# If tool not in registry, skip it (will be resolved later or is a string reference)
|
||||
return tools
|
||||
|
||||
def _parse_callbacks(self, callbacks_data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Parse callback definitions.
|
||||
|
||||
Args:
|
||||
callbacks_data: Dictionary of callback name to function name
|
||||
"""
|
||||
# For now, store callback names - they can be resolved later
|
||||
# In a full implementation, these would be resolved to actual functions
|
||||
for callback_name, func_name in callbacks_data.items():
|
||||
# Store as None for now - actual resolution happens at runtime
|
||||
self._callbacks[callback_name] = None
|
||||
|
||||
def _parse_steps(self, steps_data: List[Dict]) -> List:
|
||||
"""
|
||||
Parse workflow steps from YAML.
|
||||
|
||||
Args:
|
||||
steps_data: List of step definitions
|
||||
|
||||
Returns:
|
||||
List of workflow steps (Agent, WorkflowStep, or pattern objects)
|
||||
"""
|
||||
steps = []
|
||||
|
||||
for step_data in steps_data:
|
||||
step = self._parse_single_step(step_data)
|
||||
if step is not None:
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
def _parse_single_step(self, step_data: Dict) -> Any:
|
||||
"""
|
||||
Parse a single step definition.
|
||||
|
||||
Args:
|
||||
step_data: Step definition dictionary
|
||||
|
||||
Returns:
|
||||
Step object (Agent, WorkflowStep, or pattern)
|
||||
"""
|
||||
# Check for pattern types
|
||||
if 'route' in step_data:
|
||||
return self._parse_route_step(step_data)
|
||||
elif 'parallel' in step_data:
|
||||
return self._parse_parallel_step(step_data)
|
||||
elif 'loop' in step_data:
|
||||
return self._parse_loop_step(step_data)
|
||||
elif 'repeat' in step_data:
|
||||
return self._parse_repeat_step(step_data)
|
||||
elif 'agent' in step_data:
|
||||
return self._parse_agent_step(step_data)
|
||||
else:
|
||||
# Generic step
|
||||
return self._parse_generic_step(step_data)
|
||||
|
||||
def _parse_agent_step(self, step_data: Dict) -> Agent:
|
||||
"""
|
||||
Parse an agent step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'agent' key
|
||||
|
||||
Returns:
|
||||
Agent object
|
||||
"""
|
||||
agent_id = step_data['agent']
|
||||
|
||||
if agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store step name if provided
|
||||
step_name = step_data.get('name')
|
||||
if step_name:
|
||||
agent._yaml_step_name = step_name
|
||||
|
||||
# If there's an action, we need to handle it
|
||||
action = step_data.get('action')
|
||||
if action:
|
||||
# Store action as task for the agent
|
||||
agent._yaml_action = action
|
||||
|
||||
# Handle guardrail
|
||||
guardrail = step_data.get('guardrail')
|
||||
if guardrail:
|
||||
agent._yaml_guardrail = guardrail
|
||||
|
||||
# Handle max_retries
|
||||
max_retries = step_data.get('max_retries')
|
||||
if max_retries:
|
||||
agent._yaml_max_retries = max_retries
|
||||
|
||||
# Handle expected_output (feature parity with agents.yaml tasks)
|
||||
expected_output = step_data.get('expected_output')
|
||||
if expected_output:
|
||||
agent._yaml_expected_output = expected_output
|
||||
|
||||
# Handle context/dependencies (feature parity with agents.yaml tasks)
|
||||
context = step_data.get('context')
|
||||
if context:
|
||||
agent._yaml_context = context
|
||||
|
||||
# Handle output_file
|
||||
output_file = step_data.get('output_file')
|
||||
if output_file:
|
||||
agent._yaml_output_file = output_file
|
||||
|
||||
# Handle output_json (structured output)
|
||||
output_json = step_data.get('output_json')
|
||||
if output_json:
|
||||
agent._yaml_output_json = output_json
|
||||
|
||||
# Handle output_pydantic
|
||||
output_pydantic = step_data.get('output_pydantic')
|
||||
if output_pydantic:
|
||||
agent._yaml_output_pydantic = output_pydantic
|
||||
|
||||
# Handle create_directory
|
||||
create_directory = step_data.get('create_directory')
|
||||
if create_directory is not None:
|
||||
agent._yaml_create_directory = create_directory
|
||||
|
||||
# Handle callback
|
||||
callback = step_data.get('callback')
|
||||
if callback:
|
||||
agent._yaml_callback = callback
|
||||
|
||||
# Handle async_execution
|
||||
async_execution = step_data.get('async_execution')
|
||||
if async_execution is not None:
|
||||
agent._yaml_async_execution = async_execution
|
||||
|
||||
return agent
|
||||
else:
|
||||
raise ValueError(f"Agent '{agent_id}' not defined in agents section")
|
||||
|
||||
def _parse_route_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a route pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'route' key
|
||||
|
||||
Returns:
|
||||
Route pattern object
|
||||
"""
|
||||
route_config = step_data['route']
|
||||
|
||||
# Build routes dictionary with resolved agents
|
||||
routes = {}
|
||||
for route_key, agent_ids in route_config.items():
|
||||
if isinstance(agent_ids, list):
|
||||
routes[route_key] = [self._agents[aid] for aid in agent_ids if aid in self._agents]
|
||||
else:
|
||||
if agent_ids in self._agents:
|
||||
routes[route_key] = [self._agents[agent_ids]]
|
||||
|
||||
return route(routes)
|
||||
|
||||
def _parse_parallel_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a parallel pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'parallel' key
|
||||
|
||||
Returns:
|
||||
Parallel pattern object
|
||||
"""
|
||||
parallel_config = step_data['parallel']
|
||||
|
||||
# Build list of parallel steps
|
||||
parallel_steps = []
|
||||
for item in parallel_config:
|
||||
if 'agent' in item:
|
||||
agent_id = item['agent']
|
||||
if agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
# Store action if present
|
||||
if 'action' in item:
|
||||
agent._yaml_action = item['action']
|
||||
parallel_steps.append(agent)
|
||||
|
||||
return parallel(parallel_steps)
|
||||
|
||||
def _parse_loop_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a loop pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'loop' key
|
||||
|
||||
Returns:
|
||||
Loop pattern object
|
||||
"""
|
||||
loop_config = step_data['loop']
|
||||
agent_id = step_data.get('agent')
|
||||
|
||||
if agent_id and agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store action if present
|
||||
if 'action' in step_data:
|
||||
agent._yaml_action = step_data['action']
|
||||
|
||||
# Get loop parameters
|
||||
over = loop_config.get('over')
|
||||
from_csv = loop_config.get('from_csv')
|
||||
|
||||
return loop(agent, over=over, from_csv=from_csv)
|
||||
|
||||
raise ValueError("Loop step requires an agent")
|
||||
|
||||
def _parse_repeat_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a repeat pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'repeat' key
|
||||
|
||||
Returns:
|
||||
Repeat pattern object
|
||||
"""
|
||||
repeat_config = step_data['repeat']
|
||||
agent_id = step_data.get('agent')
|
||||
|
||||
if agent_id and agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store action if present
|
||||
if 'action' in step_data:
|
||||
agent._yaml_action = step_data['action']
|
||||
|
||||
# Get repeat parameters
|
||||
until = repeat_config.get('until')
|
||||
max_iterations = repeat_config.get('max_iterations', 5)
|
||||
|
||||
# Create condition function from 'until' string
|
||||
if isinstance(until, str):
|
||||
condition = self._create_condition_from_string(until)
|
||||
else:
|
||||
condition = until
|
||||
|
||||
return repeat(agent, until=condition, max_iterations=max_iterations)
|
||||
|
||||
raise ValueError("Repeat step requires an agent")
|
||||
|
||||
def _create_condition_from_string(self, condition_str: str) -> Callable:
|
||||
"""
|
||||
Create a condition function from a string.
|
||||
|
||||
Args:
|
||||
condition_str: Condition string (e.g., "approved", "comprehensive")
|
||||
|
||||
Returns:
|
||||
Callable condition function
|
||||
"""
|
||||
def condition(ctx) -> bool:
|
||||
result = str(ctx.previous_result).lower()
|
||||
return condition_str.lower() in result
|
||||
return condition
|
||||
|
||||
def _parse_generic_step(self, step_data: Dict) -> WorkflowStep:
|
||||
"""
|
||||
Parse a generic workflow step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition dictionary
|
||||
|
||||
Returns:
|
||||
WorkflowStep object
|
||||
"""
|
||||
name = step_data.get('name', 'step')
|
||||
action = step_data.get('action', '')
|
||||
|
||||
return WorkflowStep(
|
||||
name=name,
|
||||
action=action,
|
||||
)
|
||||
|
||||
def register_tool(self, name: str, tool: Callable) -> None:
|
||||
"""
|
||||
Register a tool for use in workflows.
|
||||
|
||||
Args:
|
||||
name: Tool name as referenced in YAML
|
||||
tool: Callable tool function
|
||||
"""
|
||||
self.tool_registry[name] = tool
|
||||
|
||||
def register_callback(self, name: str, callback: Callable) -> None:
|
||||
"""
|
||||
Register a callback function.
|
||||
|
||||
Args:
|
||||
name: Callback name as referenced in YAML
|
||||
callback: Callable callback function
|
||||
"""
|
||||
self._callbacks[name] = callback
|
||||
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.192"
|
||||
version = "0.1.14"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name="Mervin Praison" }
|
||||
|
||||
@@ -493,17 +493,22 @@ class TestAgentIntegration:
|
||||
|
||||
def test_agent_store_memory(self):
|
||||
"""Test Agent store_memory method."""
|
||||
import uuid
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Use unique user_id to avoid stale data from previous test runs
|
||||
unique_user_id = f"test_user_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
memory=True
|
||||
memory=True,
|
||||
user_id=unique_user_id
|
||||
)
|
||||
|
||||
agent.store_memory("User prefers dark mode", memory_type="short_term")
|
||||
|
||||
items = agent._memory_instance.get_short_term()
|
||||
assert len(items) == 1
|
||||
assert len(items) == 1, f"Expected 1 item, got {len(items)} items"
|
||||
assert items[0].content == "User prefers dark mode"
|
||||
|
||||
def test_agent_get_memory_context(self):
|
||||
@@ -658,10 +663,12 @@ class TestMultiAgentMemorySharing:
|
||||
|
||||
def test_agents_share_memory_with_same_user_id(self):
|
||||
"""Test that agents with same user_id share memory."""
|
||||
import uuid
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
user_id = "shared_user"
|
||||
# Use unique user_id to avoid stale data from previous test runs
|
||||
user_id = f"shared_user_{uuid.uuid4().hex[:8]}"
|
||||
base_path = f"{tmpdir}/memory"
|
||||
|
||||
# Agent 1 stores memories
|
||||
@@ -676,9 +683,9 @@ class TestMultiAgentMemorySharing:
|
||||
agent1._memory_instance.add_entity("Alice", "person", {"role": "developer"})
|
||||
|
||||
# Verify Agent1 stored memories
|
||||
assert len(agent1._memory_instance.get_short_term()) == 1
|
||||
assert len(agent1._memory_instance.get_long_term()) == 1
|
||||
assert len(agent1._memory_instance.get_all_entities()) == 1
|
||||
assert len(agent1._memory_instance.get_short_term()) == 1, f"Expected 1 short_term, got {len(agent1._memory_instance.get_short_term())}"
|
||||
assert len(agent1._memory_instance.get_long_term()) == 1, f"Expected 1 long_term, got {len(agent1._memory_instance.get_long_term())}"
|
||||
assert len(agent1._memory_instance.get_all_entities()) == 1, f"Expected 1 entity, got {len(agent1._memory_instance.get_all_entities())}"
|
||||
|
||||
# Agent 2 with same user_id should load memories
|
||||
agent2 = Agent(
|
||||
|
||||
@@ -6,6 +6,8 @@ The agent should:
|
||||
3. Return the final result (200)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def get_stock_price(company_name: str) -> str:
|
||||
@@ -35,41 +37,60 @@ def multiply(a: int, b: int) -> int:
|
||||
print(f"[Tool Called] multiply({a}, {b})")
|
||||
return a * b
|
||||
|
||||
# Test with Gemini
|
||||
print("=" * 60)
|
||||
print("Testing with Gemini model")
|
||||
print("=" * 60)
|
||||
|
||||
agent_gemini = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gemini/gemini-2.5-pro",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_sequential_tool_calling_gpt():
|
||||
"""Test sequential tool calling with GPT model"""
|
||||
print("=" * 60)
|
||||
print("Testing with GPT model")
|
||||
print("=" * 60)
|
||||
|
||||
result_gemini = agent_gemini.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (Gemini): {result_gemini}")
|
||||
agent_gpt = Agent(
|
||||
instructions="You are a helpful assistant. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Test with GPT-4
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing with GPT-4 model")
|
||||
print("=" * 60)
|
||||
result = agent_gpt.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (GPT): {result}")
|
||||
|
||||
# Verify result
|
||||
assert result, "GPT returned empty result"
|
||||
print(f"GPT result contains '200': {'200' in str(result)}")
|
||||
|
||||
agent_gpt4 = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gpt-5-nano",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("GOOGLE_API_KEY"),
|
||||
reason="GOOGLE_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_sequential_tool_calling_gemini():
|
||||
"""Test sequential tool calling with Gemini model"""
|
||||
print("=" * 60)
|
||||
print("Testing with Gemini model")
|
||||
print("=" * 60)
|
||||
|
||||
result_gpt4 = agent_gpt4.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (GPT-4): {result_gpt4}")
|
||||
agent_gemini = Agent(
|
||||
instructions="You are a helpful assistant. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Verify results
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Results Summary")
|
||||
print("=" * 60)
|
||||
print(f"Gemini result contains '200': {'200' in str(result_gemini) if result_gemini else False}")
|
||||
print(f"GPT-4 result contains '200': {'200' in str(result_gpt4) if result_gpt4 else False}")
|
||||
print(f"Gemini returned empty: {not result_gemini or result_gemini == ''}")
|
||||
print(f"GPT-4 returned empty: {not result_gpt4 or result_gpt4 == ''}")
|
||||
result = agent_gemini.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (Gemini): {result}")
|
||||
|
||||
# Verify result
|
||||
assert result, "Gemini returned empty result"
|
||||
print(f"Gemini result contains '200': {'200' in str(result)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests manually
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
test_sequential_tool_calling_gpt()
|
||||
if os.environ.get("GOOGLE_API_KEY"):
|
||||
test_sequential_tool_calling_gemini()
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Test LLM class directly to verify self-reflection fix"""
|
||||
|
||||
from praisonaiagents.llm import LLM
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "2 + 2", "15 * 23")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_llm_direct():
|
||||
"""Test LLM class directly with self-reflection and tools"""
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Test-Driven Development tests for Multiple MCP Tools support.
|
||||
|
||||
This test file verifies that:
|
||||
1. Single MCP instance works (baseline)
|
||||
2. Multiple MCP instances in a list work for tool definition conversion
|
||||
3. Multiple MCP instances in a list work for tool execution
|
||||
4. Mixed tools (MCP + regular functions) work together
|
||||
|
||||
Uses mcp-server-time as the test MCP server (simple, no API keys needed).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
# Add the package to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def time_mcp():
|
||||
"""Create a Time MCP server instance."""
|
||||
return MCP("uvx mcp-server-time", timeout=30)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_with_tools():
|
||||
"""Create a mock MCP instance with predefined tools."""
|
||||
mock = Mock(spec=MCP)
|
||||
mock.is_sse = False
|
||||
mock.is_http_stream = False
|
||||
mock.is_websocket = False
|
||||
|
||||
# Create mock runner with tools
|
||||
mock.runner = Mock()
|
||||
mock_tool1 = Mock()
|
||||
mock_tool1.name = "get_current_time"
|
||||
mock_tool1.description = "Get current time in a timezone"
|
||||
mock_tool1.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {"type": "string", "description": "IANA timezone name"}
|
||||
},
|
||||
"required": ["timezone"]
|
||||
}
|
||||
|
||||
mock_tool2 = Mock()
|
||||
mock_tool2.name = "convert_time"
|
||||
mock_tool2.description = "Convert time between timezones"
|
||||
mock_tool2.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source_timezone": {"type": "string"},
|
||||
"time": {"type": "string"},
|
||||
"target_timezone": {"type": "string"}
|
||||
},
|
||||
"required": ["source_timezone", "time", "target_timezone"]
|
||||
}
|
||||
|
||||
mock.runner.tools = [mock_tool1, mock_tool2]
|
||||
mock._tools = []
|
||||
|
||||
# Mock to_openai_tool method
|
||||
def to_openai_tool():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"description": "Get current time in a timezone",
|
||||
"parameters": mock_tool1.inputSchema
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "convert_time",
|
||||
"description": "Convert time between timezones",
|
||||
"parameters": mock_tool2.inputSchema
|
||||
}
|
||||
}
|
||||
]
|
||||
mock.to_openai_tool = to_openai_tool
|
||||
|
||||
# Mock __iter__ to return tool functions
|
||||
def get_current_time(timezone: str) -> str:
|
||||
return f"Time in {timezone}: 12:00"
|
||||
def convert_time(source_timezone: str, time: str, target_timezone: str) -> str:
|
||||
return f"Converted {time} from {source_timezone} to {target_timezone}"
|
||||
get_current_time.__name__ = "get_current_time"
|
||||
convert_time.__name__ = "convert_time"
|
||||
mock._tools = [get_current_time, convert_time]
|
||||
mock.__iter__ = lambda self: iter(self._tools)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_filesystem():
|
||||
"""Create a mock MCP instance simulating filesystem server."""
|
||||
mock = Mock(spec=MCP)
|
||||
mock.is_sse = False
|
||||
mock.is_http_stream = False
|
||||
mock.is_websocket = False
|
||||
|
||||
mock.runner = Mock()
|
||||
mock_tool = Mock()
|
||||
mock_tool.name = "read_file"
|
||||
mock_tool.description = "Read a file"
|
||||
mock_tool.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
mock.runner.tools = [mock_tool]
|
||||
mock._tools = []
|
||||
|
||||
def to_openai_tool():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": mock_tool.inputSchema
|
||||
}
|
||||
}
|
||||
]
|
||||
mock.to_openai_tool = to_openai_tool
|
||||
|
||||
def read_file(path: str) -> str:
|
||||
return f"Contents of {path}"
|
||||
read_file.__name__ = "read_file"
|
||||
mock._tools = [read_file]
|
||||
mock.__iter__ = lambda self: iter(self._tools)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
def sample_function(query: str) -> str:
|
||||
"""A sample regular function tool."""
|
||||
return f"Result for: {query}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 1: Single MCP Instance (Baseline)
|
||||
# ============================================================================
|
||||
|
||||
class TestSingleMCPInstance:
|
||||
"""Tests for single MCP instance - baseline functionality."""
|
||||
|
||||
def test_single_mcp_creates_tools(self, mock_mcp_with_tools):
|
||||
"""Test that a single MCP instance properly exposes tools."""
|
||||
tools = mock_mcp_with_tools.to_openai_tool()
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) == 2
|
||||
assert tools[0]["function"]["name"] == "get_current_time"
|
||||
assert tools[1]["function"]["name"] == "convert_time"
|
||||
|
||||
def test_single_mcp_is_iterable(self, mock_mcp_with_tools):
|
||||
"""Test that MCP instance is iterable."""
|
||||
tools_list = mock_mcp_with_tools._tools
|
||||
assert len(tools_list) == 2
|
||||
assert callable(tools_list[0])
|
||||
assert callable(tools_list[1])
|
||||
|
||||
def test_agent_accepts_single_mcp(self, mock_mcp_with_tools):
|
||||
"""Test that Agent accepts a single MCP instance as tools."""
|
||||
# This should not raise an error
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=mock_mcp_with_tools
|
||||
)
|
||||
# Agent wraps tools in a list if not already a list
|
||||
assert mock_mcp_with_tools in agent.tools or agent.tools == mock_mcp_with_tools
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 2: Multiple MCP Instances - Tool Definition Conversion
|
||||
# ============================================================================
|
||||
|
||||
class TestMultipleMCPToolDefinition:
|
||||
"""Tests for multiple MCP instances - tool definition conversion."""
|
||||
|
||||
def test_agent_accepts_multiple_mcp_in_list(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that Agent accepts multiple MCP instances in a list."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
assert isinstance(agent.tools, list)
|
||||
assert len(agent.tools) == 2
|
||||
|
||||
def test_format_tools_handles_multiple_mcp(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that _format_tools_for_llm handles multiple MCP instances."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Call the internal method to format tools
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
|
||||
# Should have 3 tools total (2 from time + 1 from filesystem)
|
||||
assert isinstance(formatted, list)
|
||||
assert len(formatted) == 3
|
||||
|
||||
# Check tool names
|
||||
tool_names = [t["function"]["name"] for t in formatted]
|
||||
assert "get_current_time" in tool_names
|
||||
assert "convert_time" in tool_names
|
||||
assert "read_file" in tool_names
|
||||
|
||||
def test_system_prompt_includes_all_mcp_tools(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that system prompt includes tools from all MCP instances."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
system_prompt = agent._build_system_prompt(agent.tools)
|
||||
|
||||
# Should mention all tools
|
||||
assert "get_current_time" in system_prompt
|
||||
assert "convert_time" in system_prompt
|
||||
assert "read_file" in system_prompt
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 3: Multiple MCP Instances - Tool Execution
|
||||
# ============================================================================
|
||||
|
||||
class TestMultipleMCPToolExecution:
|
||||
"""Tests for multiple MCP instances - tool execution."""
|
||||
|
||||
def test_execute_tool_finds_mcp_tool_in_list(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that execute_tool_call finds tools from MCP instances in a list."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Mock the runner.call_tool method
|
||||
mock_mcp_with_tools.runner.call_tool = Mock(return_value='{"timezone": "UTC", "datetime": "2024-01-01T12:00:00Z"}')
|
||||
|
||||
# This should find and execute the tool from the first MCP instance
|
||||
result = agent.execute_tool("get_current_time", {"timezone": "UTC"})
|
||||
|
||||
# Verify the tool was called
|
||||
mock_mcp_with_tools.runner.call_tool.assert_called_once_with("get_current_time", {"timezone": "UTC"})
|
||||
|
||||
def test_execute_tool_finds_tool_from_second_mcp(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that execute_tool_call finds tools from the second MCP instance."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Mock the runner.call_tool method
|
||||
mock_mcp_filesystem.runner.call_tool = Mock(return_value='Contents of /test/file.txt')
|
||||
|
||||
# This should find and execute the tool from the second MCP instance
|
||||
result = agent.execute_tool("read_file", {"path": "/test/file.txt"})
|
||||
|
||||
# Verify the tool was called
|
||||
mock_mcp_filesystem.runner.call_tool.assert_called_once_with("read_file", {"path": "/test/file.txt"})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 4: Mixed Tools (MCP + Regular Functions)
|
||||
# ============================================================================
|
||||
|
||||
class TestMixedTools:
|
||||
"""Tests for mixed tools - MCP instances and regular functions."""
|
||||
|
||||
def test_agent_accepts_mixed_tools(self, mock_mcp_with_tools):
|
||||
"""Test that Agent accepts a mix of MCP instances and regular functions."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
assert isinstance(agent.tools, list)
|
||||
assert len(agent.tools) == 2
|
||||
|
||||
def test_format_tools_handles_mixed(self, mock_mcp_with_tools):
|
||||
"""Test that _format_tools_for_llm handles mixed tools."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
|
||||
# Should have 3 tools (2 from MCP + 1 regular function)
|
||||
assert isinstance(formatted, list)
|
||||
assert len(formatted) == 3
|
||||
|
||||
tool_names = [t["function"]["name"] for t in formatted]
|
||||
assert "get_current_time" in tool_names
|
||||
assert "convert_time" in tool_names
|
||||
assert "sample_function" in tool_names
|
||||
|
||||
def test_execute_mixed_tools(self, mock_mcp_with_tools):
|
||||
"""Test executing both MCP tools and regular functions."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
|
||||
# Execute regular function
|
||||
result = agent.execute_tool("sample_function", {"query": "test"})
|
||||
assert "Result for: test" in str(result)
|
||||
|
||||
# Mock and execute MCP tool
|
||||
mock_mcp_with_tools.runner.call_tool = Mock(return_value='{"timezone": "UTC"}')
|
||||
result = agent.execute_tool("get_current_time", {"timezone": "UTC"})
|
||||
mock_mcp_with_tools.runner.call_tool.assert_called_once()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Integration Tests (requires actual MCP server)
|
||||
# ============================================================================
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("SKIP_MCP_INTEGRATION") == "1",
|
||||
reason="MCP integration tests skipped"
|
||||
)
|
||||
class TestMCPIntegration:
|
||||
"""Integration tests using actual MCP servers."""
|
||||
|
||||
def test_real_time_mcp_server(self):
|
||||
"""Test with real mcp-server-time."""
|
||||
try:
|
||||
mcp = MCP("uvx mcp-server-time", timeout=30)
|
||||
tools = mcp.to_openai_tool()
|
||||
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) >= 1
|
||||
|
||||
tool_names = [t["function"]["name"] for t in tools]
|
||||
assert "get_current_time" in tool_names
|
||||
except Exception as e:
|
||||
pytest.skip(f"MCP server not available: {e}")
|
||||
|
||||
def test_multiple_real_mcp_servers(self):
|
||||
"""Test with multiple real MCP servers."""
|
||||
try:
|
||||
time_mcp = MCP("uvx mcp-server-time", timeout=30)
|
||||
|
||||
# Create agent with multiple MCP instances
|
||||
agent = Agent(
|
||||
name="MultiMCPAgent",
|
||||
instructions="Test agent with multiple MCP tools",
|
||||
tools=[time_mcp]
|
||||
)
|
||||
|
||||
# Verify tools are available
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
assert len(formatted) >= 1
|
||||
|
||||
except Exception as e:
|
||||
pytest.skip(f"MCP servers not available: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Run tests
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--tb=short"])
|
||||
@@ -5,7 +5,9 @@ Test script to verify Ollama empty response fix for both sync and async methods.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, TaskOutput
|
||||
import os
|
||||
import pytest
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from typing import Dict, Any
|
||||
|
||||
# Enable debug logging
|
||||
@@ -25,8 +27,8 @@ def search_tool(query: str) -> Dict[str, Any]:
|
||||
logging.debug(f"[TEST] Search tool returning: {results}")
|
||||
return results
|
||||
|
||||
# Test with both sync and async workflows
|
||||
async def test_model_async(model_name: str):
|
||||
# Helper async function to test a model
|
||||
async def _test_model_async_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing ASYNC with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -78,7 +80,7 @@ async def test_model_async(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_model_sync(model_name: str):
|
||||
def _test_model_sync_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing SYNC with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -130,18 +132,60 @@ def test_model_sync(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_ollama_async_openai():
|
||||
"""Test async with OpenAI model as baseline."""
|
||||
result = await _test_model_async_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI async model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_ollama_sync_openai():
|
||||
"""Test sync with OpenAI model as baseline."""
|
||||
result = _test_model_sync_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI sync model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_ollama_async_ollama():
|
||||
"""Test async with Ollama model."""
|
||||
result = await _test_model_async_helper("ollama/llama3.2")
|
||||
assert result, "Ollama async model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
def test_ollama_sync_ollama():
|
||||
"""Test sync with Ollama model."""
|
||||
result = _test_model_sync_helper("ollama/llama3.2")
|
||||
assert result, "Ollama sync model test failed"
|
||||
|
||||
|
||||
async def main():
|
||||
print("Testing Ollama empty response fix for both sync and async...")
|
||||
|
||||
# Test sync methods
|
||||
print("\n1. Testing SYNC methods:")
|
||||
openai_sync_success = test_model_sync("openai/gpt-5-nano")
|
||||
ollama_sync_success = test_model_sync("ollama/llama3.2")
|
||||
openai_sync_success = _test_model_sync_helper("gpt-4o-mini")
|
||||
ollama_sync_success = _test_model_sync_helper("ollama/llama3.2")
|
||||
|
||||
# Test async methods
|
||||
print("\n2. Testing ASYNC methods:")
|
||||
openai_async_success = await test_model_async("openai/gpt-5-nano")
|
||||
ollama_async_success = await test_model_async("ollama/llama3.2")
|
||||
openai_async_success = await _test_model_async_helper("gpt-4o-mini")
|
||||
ollama_async_success = await _test_model_async_helper("ollama/llama3.2")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
|
||||
@@ -4,7 +4,9 @@ Test script to verify Ollama empty response fix.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, TaskOutput
|
||||
import os
|
||||
import pytest
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from typing import Dict, Any
|
||||
|
||||
# Enable debug logging
|
||||
@@ -24,8 +26,8 @@ def search_tool(query: str) -> Dict[str, Any]:
|
||||
logging.debug(f"[TEST] Search tool returning: {results}")
|
||||
return results
|
||||
|
||||
# Test with both OpenAI and Ollama models
|
||||
def test_model(model_name: str):
|
||||
# Helper function to test a model
|
||||
def _test_model_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -77,16 +79,36 @@ def test_model(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_ollama_fix_openai():
|
||||
"""Test with OpenAI model as baseline."""
|
||||
result = _test_model_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
def test_ollama_fix_ollama():
|
||||
"""Test with Ollama model."""
|
||||
result = _test_model_helper("ollama/llama3.2")
|
||||
assert result, "Ollama model test failed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing Ollama empty response fix...")
|
||||
|
||||
# Test with OpenAI first (as baseline)
|
||||
print("\n1. Testing with OpenAI (baseline):")
|
||||
openai_success = test_model("openai/gpt-5-nano")
|
||||
openai_success = _test_model_helper("gpt-4o-mini")
|
||||
|
||||
# Test with Ollama
|
||||
print("\n2. Testing with Ollama:")
|
||||
ollama_success = test_model("ollama/llama3.2")
|
||||
ollama_success = _test_model_helper("ollama/llama3.2")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
|
||||
@@ -53,11 +53,14 @@ def test_ollama_argument_validation():
|
||||
print("\n2. Testing mixed arguments (the main issue):")
|
||||
mixed_args = {"a": "get_stock_price", "company_name": "Google", "b": "2"}
|
||||
filtered_args = llm._validate_and_filter_ollama_arguments("multiply", mixed_args, tools)
|
||||
expected_filtered = {"a": "get_stock_price", "b": "2"} # Should remove 'company_name'
|
||||
# Note: The method may coerce "2" to int 2 based on type hints - both are acceptable
|
||||
expected_filtered_str = {"a": "get_stock_price", "b": "2"} # String version
|
||||
expected_filtered_int = {"a": "get_stock_price", "b": 2} # Int version (coerced)
|
||||
print(f"Original: {mixed_args}")
|
||||
print(f"Filtered: {filtered_args}")
|
||||
print(f"Expected: {expected_filtered}")
|
||||
assert filtered_args == expected_filtered, f"Expected {expected_filtered}, got {filtered_args}"
|
||||
print(f"Expected: {expected_filtered_str} or {expected_filtered_int}")
|
||||
assert filtered_args == expected_filtered_str or filtered_args == expected_filtered_int, \
|
||||
f"Expected {expected_filtered_str} or {expected_filtered_int}, got {filtered_args}"
|
||||
print("✅ Mixed arguments filtering test passed")
|
||||
|
||||
# Test case 3: All invalid arguments
|
||||
|
||||
@@ -704,7 +704,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value=json.dumps({
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value=json.dumps({
|
||||
"name": "Test Plan",
|
||||
"description": "A test plan",
|
||||
"steps": [
|
||||
@@ -733,7 +734,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value=json.dumps({
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value=json.dumps({
|
||||
"name": "Refined Plan",
|
||||
"description": "Updated plan",
|
||||
"steps": [
|
||||
@@ -765,7 +767,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value="Analysis: Python project with FastAPI")
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value="Analysis: Python project with FastAPI")
|
||||
|
||||
planner._llm = mock_llm
|
||||
|
||||
|
||||
@@ -2,7 +2,29 @@
|
||||
"""Comprehensive test script to verify self-reflection works with tools after the fix"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import duckduckgo_search
|
||||
|
||||
# Define duckduckgo_search tool locally to avoid import issues
|
||||
def duckduckgo_search(query: str) -> str:
|
||||
"""
|
||||
Search the web using DuckDuckGo.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
|
||||
Returns:
|
||||
Search results as a string
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
with DDGS() as ddgs:
|
||||
results = list(ddgs.text(query, max_results=3))
|
||||
if results:
|
||||
return "\n".join([f"- {r.get('title', '')}: {r.get('body', '')}" for r in results])
|
||||
return "No results found"
|
||||
except ImportError:
|
||||
return f"Mock search results for: {query}"
|
||||
except Exception as e:
|
||||
return f"Search error: {str(e)}"
|
||||
|
||||
def test_self_reflection_with_tools():
|
||||
"""Test self-reflection with tools - should work after the fix"""
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Simple test to verify the self-reflection fix works"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "2 + 2", "25 * 17")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_self_reflection_fix():
|
||||
"""Test that self-reflection works with tools after the fix"""
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Test to verify the self-reflection fix works with tools"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "123 * 456")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_self_reflection_fix_verification():
|
||||
"""Test that self-reflection now works with tools"""
|
||||
|
||||
@@ -319,7 +319,14 @@ class TestExecuteWithAgents:
|
||||
result = manager.execute("test_workflow")
|
||||
|
||||
assert not result["success"]
|
||||
assert "No executor available" in result.get("error", "")
|
||||
# Error can be at top level or in results array
|
||||
error_found = "No executor available" in result.get("error", "")
|
||||
if not error_found and "results" in result:
|
||||
error_found = any(
|
||||
"No executor available" in r.get("error", "")
|
||||
for r in result.get("results", [])
|
||||
)
|
||||
assert error_found, f"Expected 'No executor available' error, got: {result}"
|
||||
|
||||
|
||||
class TestWorkflowLevelDefaults:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.192"
|
||||
version = "0.1.14"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+915
-331
File diff suppressed because it is too large.
Load diff
+41
-36
@@ -1,44 +1,49 @@
|
||||
# Movie Script Workflow - Cat in Mars
|
||||
# Uses the modern agents + steps format
|
||||
|
||||
name: Create Movie Script About Cat in Mars
|
||||
description: A sequential workflow to research, design narrative, and write a movie script
|
||||
framework: praisonai
|
||||
process: sequential
|
||||
topic: create movie script about cat in mars
|
||||
memory: false
|
||||
roles:
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
backstory: Skilled in research, with a focus on gathering accurate and relevant
|
||||
information.
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Gather information about Mars and cats
|
||||
role: Researcher
|
||||
min_reflect: 1
|
||||
max_reflect: 2
|
||||
tasks:
|
||||
research_task:
|
||||
description: Research about Mars, its environment, and the feasibility of
|
||||
a cat being on Mars. Also, research about cat behavior and characteristics.
|
||||
expected_output: Document with research findings on Mars and cats.
|
||||
backstory: Skilled in research, with a focus on gathering accurate and relevant information.
|
||||
instructions: |
|
||||
Research about Mars, its environment, and the feasibility of a cat being on Mars.
|
||||
Also, research about cat behavior and characteristics.
|
||||
tools:
|
||||
- search_tool
|
||||
- search_tool
|
||||
|
||||
narrative_designer:
|
||||
backstory: Skilled in narrative development, with a focus on creating engaging
|
||||
stories.
|
||||
name: Narrative Designer
|
||||
role: Story Concept Developer
|
||||
goal: Create a story concept for a movie about a cat in Mars
|
||||
role: Narrative Designer
|
||||
tasks:
|
||||
story_concept_development:
|
||||
description: Use the research findings to craft a unique story concept for
|
||||
a movie about a cat in Mars.
|
||||
expected_output: Document with narrative arcs, character bios, and settings.
|
||||
tools:
|
||||
- ''
|
||||
backstory: Skilled in narrative development, with a focus on creating engaging stories.
|
||||
instructions: |
|
||||
Use the research findings to craft a unique story concept for a movie about a cat in Mars.
|
||||
Create narrative arcs, character bios, and settings.
|
||||
|
||||
scriptwriter:
|
||||
backstory: Expert in dialogue and script structure, translating concepts into
|
||||
scripts.
|
||||
name: Scriptwriter
|
||||
role: Script Writer
|
||||
goal: Write a movie script about a cat in Mars
|
||||
role: Scriptwriter
|
||||
tasks:
|
||||
scriptwriting_task:
|
||||
description: Turn the story concept into a production-ready movie script,
|
||||
including dialogue and scene details.
|
||||
expected_output: Final movie script with dialogue and scene details.
|
||||
tools:
|
||||
- ''
|
||||
dependencies: []
|
||||
backstory: Expert in dialogue and script structure, translating concepts into scripts.
|
||||
instructions: |
|
||||
Turn the story concept into a production-ready movie script,
|
||||
including dialogue and scene details.
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research about Mars environment and cat behavior for the movie concept"
|
||||
|
||||
- agent: narrative_designer
|
||||
action: "Create story concept based on research: {{previous_output}}"
|
||||
|
||||
- agent: scriptwriter
|
||||
action: "Write the final movie script based on the story concept: {{previous_output}}"
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"id": "e022a3a1",
|
||||
"name": "Create Movie Script About Cat in Mars",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
250,
|
||||
300
|
||||
],
|
||||
"webhookId": "dd7cf5cb72d19eae",
|
||||
"parameters": {
|
||||
"path": "create-movie-script-about-cat-in-mars",
|
||||
"httpMethod": "POST",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agent_0",
|
||||
"name": "Researcher",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
450,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://127.0.0.1:8005/agents/researcher",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ query: $json.body?.query || $json.query || 'Research about Mars environment and cat behavior for the movie concept' } }) }}",
|
||||
"options": {
|
||||
"timeout": 300000
|
||||
}
|
||||
},
|
||||
"notes": "Agent: Researcher\nTask: Research about Mars environment and cat behavior for the movie concept"
|
||||
},
|
||||
{
|
||||
"id": "agent_1",
|
||||
"name": "Narrative Designer",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
700,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://127.0.0.1:8005/agents/narrative_designer",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ query: $json.response || 'Create story concept based on research: {{previous_output}}' } }) }}",
|
||||
"options": {
|
||||
"timeout": 300000
|
||||
}
|
||||
},
|
||||
"notes": "Agent: Narrative Designer\nTask: Create story concept based on research: {{previous_output}}"
|
||||
},
|
||||
{
|
||||
"id": "agent_2",
|
||||
"name": "Scriptwriter",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
950,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://127.0.0.1:8005/agents/scriptwriter",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ query: $json.response || 'Write the final movie script based on the story concept: {{previous_output}}' } }) }}",
|
||||
"options": {
|
||||
"timeout": 300000
|
||||
}
|
||||
},
|
||||
"notes": "Agent: Scriptwriter\nTask: Write the final movie script based on the story concept: {{previous_output}}"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Researcher",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Researcher": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Narrative Designer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Narrative Designer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Scriptwriter",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "1",
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "praisonai-export",
|
||||
"templateCredsSetupCompleted": true,
|
||||
"description": "A sequential workflow to research, design narrative, and write a movie script"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Advanced Workflow Example
|
||||
# Demonstrates: conditional routing, decision tasks, and multi-agent coordination
|
||||
|
||||
name: Content Creation Pipeline
|
||||
description: A workflow that researches, writes, reviews, and publishes content
|
||||
|
||||
workflow:
|
||||
process: workflow # Use workflow process for conditional execution
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Specialist
|
||||
goal: Gather comprehensive information on topics
|
||||
backstory: Expert at finding and synthesizing information from various sources
|
||||
instructions: |
|
||||
Research the given topic thoroughly.
|
||||
Provide detailed findings with key points.
|
||||
llm: gpt-4o-mini
|
||||
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Create engaging and informative content
|
||||
backstory: Skilled writer who transforms research into readable content
|
||||
instructions: |
|
||||
Write clear, engaging content based on the research.
|
||||
Structure the content with introduction, body, and conclusion.
|
||||
llm: gpt-4o-mini
|
||||
|
||||
reviewer:
|
||||
name: Reviewer
|
||||
role: Quality Reviewer
|
||||
goal: Ensure content meets quality standards
|
||||
backstory: Meticulous editor who ensures accuracy and clarity
|
||||
instructions: |
|
||||
Review the content for accuracy, clarity, and engagement.
|
||||
Decide if it's ready for publication or needs revision.
|
||||
llm: gpt-4o-mini
|
||||
|
||||
publisher:
|
||||
name: Publisher
|
||||
role: Content Publisher
|
||||
goal: Finalize and format content for publication
|
||||
backstory: Expert at preparing content for various platforms
|
||||
instructions: |
|
||||
Format the approved content for publication.
|
||||
Add metadata and prepare final deliverable.
|
||||
llm: gpt-4o-mini
|
||||
|
||||
steps:
|
||||
# Step 1: Research (starting point)
|
||||
- name: research_task
|
||||
agent: researcher
|
||||
action: "Research the topic: AI trends in 2025"
|
||||
expected_output: "Comprehensive research findings with key points and sources"
|
||||
is_start: true
|
||||
next_tasks: [write_task]
|
||||
|
||||
# Step 2: Write content based on research
|
||||
- name: write_task
|
||||
agent: writer
|
||||
action: "Write an article based on the research findings"
|
||||
expected_output: "Well-structured article with introduction, body, and conclusion"
|
||||
context: [research_task]
|
||||
next_tasks: [review_task]
|
||||
|
||||
# Step 3: Review with decision (conditional routing)
|
||||
- name: review_task
|
||||
agent: reviewer
|
||||
action: "Review the article for quality and accuracy"
|
||||
expected_output: "Quality assessment with approval or revision recommendation"
|
||||
context: [write_task]
|
||||
task_type: decision
|
||||
condition:
|
||||
approved: [publish_task]
|
||||
needs_revision: [revise_task]
|
||||
|
||||
# Step 4a: Revision (conditional - only if review fails)
|
||||
- name: revise_task
|
||||
agent: writer
|
||||
action: "Revise the article based on reviewer feedback"
|
||||
expected_output: "Revised article addressing all feedback"
|
||||
context: [write_task, review_task]
|
||||
next_tasks: [review_task] # Loop back to review
|
||||
|
||||
# Step 4b: Publish (conditional - only if review passes)
|
||||
- name: publish_task
|
||||
agent: publisher
|
||||
action: "Prepare the approved article for publication"
|
||||
expected_output: "Final formatted article ready for publication"
|
||||
context: [write_task, review_task]
|
||||
@@ -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.3.14.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.14.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.34.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.34.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -318,7 +318,17 @@ class AgentsGenerator:
|
||||
print(f"File not found: {self.agent_file}")
|
||||
return
|
||||
|
||||
topic = config['topic']
|
||||
# Check if this is a workflow-mode YAML (process: workflow or has steps section)
|
||||
process_type = config.get('process', 'sequential')
|
||||
has_steps = 'steps' in config
|
||||
has_workflow_config = 'workflow' in config
|
||||
|
||||
if process_type == 'workflow' or (has_steps and has_workflow_config):
|
||||
# Route to YAMLWorkflowParser for advanced workflow patterns
|
||||
return self._run_yaml_workflow(config)
|
||||
|
||||
# Get workflow input: 'input' is canonical, 'topic' is alias for backward compatibility
|
||||
topic = config.get('input', config.get('topic', ''))
|
||||
tools_dict = {}
|
||||
|
||||
# Only try to use praisonai_tools if it's available and needed
|
||||
@@ -406,6 +416,62 @@ class AgentsGenerator:
|
||||
agentops.init(os.environ.get("AGENTOPS_API_KEY"), default_tags=["crewai"])
|
||||
return self._run_crewai(config, topic, tools_dict)
|
||||
|
||||
def _run_yaml_workflow(self, config):
|
||||
"""
|
||||
Run a YAML workflow using the YAMLWorkflowParser.
|
||||
|
||||
This method handles agents.yaml files that have:
|
||||
- process: workflow
|
||||
- steps section with workflow patterns (route, parallel, loop, repeat)
|
||||
|
||||
Args:
|
||||
config (dict): The parsed YAML configuration
|
||||
|
||||
Returns:
|
||||
str: Result of the workflow execution
|
||||
"""
|
||||
if not PRAISONAI_AVAILABLE:
|
||||
raise ImportError("PraisonAI is not installed. Please install it with 'pip install praisonaiagents'")
|
||||
|
||||
try:
|
||||
from praisonaiagents.workflows import YAMLWorkflowParser
|
||||
except ImportError:
|
||||
raise ImportError("YAMLWorkflowParser not available. Please update praisonaiagents.")
|
||||
|
||||
# Ensure name is present (YAMLWorkflowParser handles roles->agents conversion)
|
||||
if 'name' not in config:
|
||||
config['name'] = config.get('topic', 'Workflow')
|
||||
|
||||
# Pass model from config_list to workflow as default_llm
|
||||
if self.config_list and self.config_list[0].get('model'):
|
||||
model_from_cli = self.config_list[0]['model']
|
||||
# Set default_llm in workflow config if not already set
|
||||
if 'workflow' not in config:
|
||||
config['workflow'] = {}
|
||||
if 'default_llm' not in config['workflow']:
|
||||
config['workflow']['default_llm'] = model_from_cli
|
||||
|
||||
# Convert config back to YAML string for parser
|
||||
# Note: YAMLWorkflowParser handles 'roles' to 'agents' conversion internally
|
||||
import yaml as yaml_module
|
||||
yaml_content = yaml_module.dump(config, default_flow_style=False)
|
||||
|
||||
# Parse and execute
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
# Get input: 'input' is canonical, 'topic' is alias for backward compatibility
|
||||
input_data = config.get('input', config.get('topic', ''))
|
||||
|
||||
# Execute workflow
|
||||
self.logger.debug(f"Running workflow: {workflow.name}")
|
||||
result = workflow.start(input_data)
|
||||
|
||||
if result.get("status") == "completed":
|
||||
return result.get("output", "Workflow completed successfully")
|
||||
else:
|
||||
return f"Workflow failed: {result.get('error', 'Unknown error')}"
|
||||
|
||||
def _run_autogen(self, config, topic, tools_dict):
|
||||
"""
|
||||
Run agents using the AutoGen framework.
|
||||
|
||||
@@ -335,4 +335,225 @@ The final role you create should satisfy the provided task: """ + self.topic + "
|
||||
|
||||
|
||||
# generator = AutoGenerator(framework="crewai", topic="Create a movie script about Cat in Mars")
|
||||
# print(generator.generate())
|
||||
# print(generator.generate())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Workflow Auto-Generation (Feature Parity)
|
||||
# =============================================================================
|
||||
|
||||
class WorkflowStepDetails(BaseModel):
|
||||
"""Details for a workflow step."""
|
||||
agent: str
|
||||
action: str
|
||||
expected_output: Optional[str] = None
|
||||
|
||||
class WorkflowRouteDetails(BaseModel):
|
||||
"""Details for a route step."""
|
||||
name: str
|
||||
route: Dict[str, List[str]]
|
||||
|
||||
class WorkflowParallelDetails(BaseModel):
|
||||
"""Details for a parallel step."""
|
||||
name: str
|
||||
parallel: List[WorkflowStepDetails]
|
||||
|
||||
class WorkflowAgentDetails(BaseModel):
|
||||
"""Details for a workflow agent."""
|
||||
name: str
|
||||
role: str
|
||||
goal: str
|
||||
instructions: str
|
||||
tools: Optional[List[str]] = None
|
||||
|
||||
class WorkflowStructure(BaseModel):
|
||||
"""Structure for auto-generated workflow."""
|
||||
name: str
|
||||
description: str
|
||||
agents: Dict[str, WorkflowAgentDetails]
|
||||
steps: List[Dict] # Can be agent steps, route, parallel, etc.
|
||||
|
||||
|
||||
class WorkflowAutoGenerator:
|
||||
"""
|
||||
Auto-generates workflow.yaml files from a topic description.
|
||||
|
||||
Uses lazy loading to avoid performance impact when not used.
|
||||
|
||||
Usage:
|
||||
generator = WorkflowAutoGenerator(topic="Research AI trends and write a report")
|
||||
path = generator.generate()
|
||||
"""
|
||||
|
||||
def __init__(self, topic: str = "Research and write about AI",
|
||||
workflow_file: str = "workflow.yaml",
|
||||
config_list: Optional[List[Dict]] = None):
|
||||
"""
|
||||
Initialize the WorkflowAutoGenerator.
|
||||
|
||||
Args:
|
||||
topic: The task/topic for the workflow
|
||||
workflow_file: Output file name
|
||||
config_list: Optional LLM configuration
|
||||
"""
|
||||
# Support multiple environment variable patterns
|
||||
model_name = os.environ.get("MODEL_NAME") or os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini")
|
||||
base_url = (
|
||||
os.environ.get("OPENAI_BASE_URL") or
|
||||
os.environ.get("OPENAI_API_BASE") or
|
||||
os.environ.get("OLLAMA_API_BASE", "https://api.openai.com/v1")
|
||||
)
|
||||
|
||||
self.config_list = config_list or [
|
||||
{
|
||||
'model': model_name,
|
||||
'base_url': base_url,
|
||||
'api_key': os.environ.get("OPENAI_API_KEY")
|
||||
}
|
||||
]
|
||||
self.topic = topic
|
||||
self.workflow_file = workflow_file
|
||||
self._client = None # Lazy loading
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Lazy load the OpenAI client to avoid performance impact."""
|
||||
if self._client is None:
|
||||
self._client = instructor.patch(
|
||||
OpenAI(
|
||||
base_url=self.config_list[0]['base_url'],
|
||||
api_key=self.config_list[0]['api_key'],
|
||||
),
|
||||
mode=instructor.Mode.JSON,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def generate(self, pattern: str = "sequential") -> str:
|
||||
"""
|
||||
Generate a workflow YAML file.
|
||||
|
||||
Args:
|
||||
pattern: Workflow pattern - "sequential", "routing", "parallel", "loop"
|
||||
|
||||
Returns:
|
||||
Path to the generated workflow file
|
||||
"""
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.config_list[0]['model'],
|
||||
response_model=WorkflowStructure,
|
||||
max_retries=5,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant that designs workflow structures."},
|
||||
{"role": "user", "content": self._get_prompt(pattern)}
|
||||
]
|
||||
)
|
||||
|
||||
json_data = json.loads(response.model_dump_json())
|
||||
return self._save_workflow(json_data, pattern)
|
||||
|
||||
def _get_prompt(self, pattern: str) -> str:
|
||||
"""Generate the prompt based on the workflow pattern."""
|
||||
base_prompt = f"""Generate a workflow structure for: "{self.topic}"
|
||||
|
||||
The workflow should use the "{pattern}" pattern.
|
||||
Create 2-4 agents with clear roles and instructions.
|
||||
Each step should have a clear action.
|
||||
|
||||
"""
|
||||
|
||||
if pattern == "routing":
|
||||
base_prompt += """
|
||||
Include a classifier agent that routes to different specialized agents.
|
||||
The route step should have at least 2 routes plus a default.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Routing Workflow",
|
||||
"description": "Routes requests to specialized agents",
|
||||
"agents": {
|
||||
"classifier": {"name": "Classifier", "role": "Request Classifier", "goal": "Classify requests", "instructions": "Respond with ONLY: technical, creative, or general"},
|
||||
"tech_agent": {"name": "TechExpert", "role": "Technical Expert", "goal": "Handle technical questions", "instructions": "Provide technical answers"}
|
||||
},
|
||||
"steps": [
|
||||
{"agent": "classifier", "action": "Classify: {{input}}"},
|
||||
{"name": "routing", "route": {"technical": ["tech_agent"], "default": ["tech_agent"]}}
|
||||
]
|
||||
}
|
||||
"""
|
||||
elif pattern == "parallel":
|
||||
base_prompt += """
|
||||
Include multiple agents that work in parallel, then an aggregator.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Parallel Workflow",
|
||||
"description": "Multiple agents work concurrently",
|
||||
"agents": {
|
||||
"researcher1": {"name": "Researcher1", "role": "Market Analyst", "goal": "Research market", "instructions": "Provide market insights"},
|
||||
"researcher2": {"name": "Researcher2", "role": "Competitor Analyst", "goal": "Research competitors", "instructions": "Provide competitor insights"},
|
||||
"aggregator": {"name": "Aggregator", "role": "Synthesizer", "goal": "Combine findings", "instructions": "Synthesize all research"}
|
||||
},
|
||||
"steps": [
|
||||
{"name": "parallel_research", "parallel": [
|
||||
{"agent": "researcher1", "action": "Research market for {{input}}"},
|
||||
{"agent": "researcher2", "action": "Research competitors for {{input}}"}
|
||||
]},
|
||||
{"agent": "aggregator", "action": "Combine all findings"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
else: # sequential
|
||||
base_prompt += """
|
||||
Create a sequential workflow where agents work one after another.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Sequential Workflow",
|
||||
"description": "Agents work in sequence",
|
||||
"agents": {
|
||||
"researcher": {"name": "Researcher", "role": "Research Analyst", "goal": "Research topics", "instructions": "Provide research findings"},
|
||||
"writer": {"name": "Writer", "role": "Content Writer", "goal": "Write content", "instructions": "Write clear content"}
|
||||
},
|
||||
"steps": [
|
||||
{"agent": "researcher", "action": "Research: {{input}}"},
|
||||
{"agent": "writer", "action": "Write based on: {{previous_output}}"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
base_prompt += f"\nGenerate a workflow for: {self.topic}"
|
||||
return base_prompt
|
||||
|
||||
def _save_workflow(self, data: Dict, pattern: str) -> str:
|
||||
"""Save the workflow to a YAML file."""
|
||||
# Build the workflow YAML structure
|
||||
workflow_yaml = {
|
||||
'name': data.get('name', 'Auto-Generated Workflow'),
|
||||
'description': data.get('description', ''),
|
||||
'framework': 'praisonai',
|
||||
'workflow': {
|
||||
'verbose': True,
|
||||
'planning': False,
|
||||
'reasoning': False
|
||||
},
|
||||
'agents': {},
|
||||
'steps': data.get('steps', [])
|
||||
}
|
||||
|
||||
# Convert agents
|
||||
for agent_id, agent_data in data.get('agents', {}).items():
|
||||
workflow_yaml['agents'][agent_id] = {
|
||||
'name': agent_data.get('name', agent_id),
|
||||
'role': agent_data.get('role', 'Assistant'),
|
||||
'goal': agent_data.get('goal', ''),
|
||||
'instructions': agent_data.get('instructions', '')
|
||||
}
|
||||
if agent_data.get('tools'):
|
||||
workflow_yaml['agents'][agent_id]['tools'] = agent_data['tools']
|
||||
|
||||
# Write to file
|
||||
full_path = os.path.abspath(self.workflow_file)
|
||||
with open(full_path, 'w') as f:
|
||||
yaml.dump(workflow_yaml, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
return full_path
|
||||
@@ -19,6 +19,8 @@ Features:
|
||||
- todo: Todo list management
|
||||
- router: Smart model selection
|
||||
- flow_display: Visual workflow tracking
|
||||
- workflow: YAML workflow management
|
||||
- n8n: Export workflows to n8n visual editor
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -39,6 +41,8 @@ __all__ = [
|
||||
'TodoHandler',
|
||||
'RouterHandler',
|
||||
'FlowDisplayHandler',
|
||||
'WorkflowHandler',
|
||||
'N8nHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -85,4 +89,10 @@ def __getattr__(name):
|
||||
elif name == 'FlowDisplayHandler':
|
||||
from .flow_display import FlowDisplayHandler
|
||||
return FlowDisplayHandler
|
||||
elif name == 'WorkflowHandler':
|
||||
from .workflow import WorkflowHandler
|
||||
return WorkflowHandler
|
||||
elif name == 'N8nHandler':
|
||||
from .n8n import N8nHandler
|
||||
return N8nHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -1,25 +1,31 @@
|
||||
"""
|
||||
Flow Display Handler for CLI.
|
||||
|
||||
Provides visual workflow tracking.
|
||||
Shows a visual flow diagram of the workflow WITHOUT executing it.
|
||||
Usage: praisonai agents.yaml --flow-display
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Tuple, List
|
||||
from typing import Any, Dict, Tuple
|
||||
from .base import FlagHandler
|
||||
import os
|
||||
import yaml
|
||||
|
||||
|
||||
class FlowDisplayHandler(FlagHandler):
|
||||
"""
|
||||
Handler for --flow-display flag.
|
||||
|
||||
Enables visual workflow tracking for multi-agent executions.
|
||||
Shows a visual flow diagram/chart of agents and tasks WITHOUT executing.
|
||||
This allows users to preview the workflow structure before running it.
|
||||
|
||||
Example:
|
||||
praisonai agents.yaml --flow-display
|
||||
praisonai "Complex task" --flow-display
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False):
|
||||
super().__init__(verbose)
|
||||
self._console = None
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "flow_display"
|
||||
@@ -30,160 +36,531 @@ class FlowDisplayHandler(FlagHandler):
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
return "Enable visual workflow tracking"
|
||||
return "Show workflow flow diagram without executing"
|
||||
|
||||
def _get_console(self):
|
||||
"""Get or create Rich Console."""
|
||||
if self._console is None:
|
||||
try:
|
||||
from rich.console import Console
|
||||
self._console = Console()
|
||||
except ImportError:
|
||||
pass
|
||||
return self._console
|
||||
|
||||
def check_dependencies(self) -> Tuple[bool, str]:
|
||||
"""Check if FlowDisplay is available."""
|
||||
"""Check if Rich is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("praisonaiagents") is not None:
|
||||
return True, ""
|
||||
return False, "praisonaiagents not installed"
|
||||
rich_available = importlib.util.find_spec("rich") is not None
|
||||
if not rich_available:
|
||||
return False, "rich not installed. Install with: pip install rich"
|
||||
return True, ""
|
||||
except ImportError:
|
||||
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
|
||||
|
||||
def _get_flow_display(self):
|
||||
"""Get FlowDisplay class lazily."""
|
||||
try:
|
||||
from praisonaiagents import FlowDisplay
|
||||
return FlowDisplay
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
def create_flow_display(self, title: str = "Workflow") -> Any:
|
||||
"""
|
||||
Create a FlowDisplay instance.
|
||||
|
||||
Args:
|
||||
title: Title for the workflow display
|
||||
|
||||
Returns:
|
||||
FlowDisplay instance or None
|
||||
"""
|
||||
FlowDisplay = self._get_flow_display()
|
||||
if not FlowDisplay:
|
||||
self.print_status(
|
||||
"FlowDisplay requires praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return FlowDisplay(title=title)
|
||||
except Exception as e:
|
||||
self.log(f"Failed to create FlowDisplay: {e}", "error")
|
||||
return None
|
||||
return False, "rich not installed. Install with: pip install rich"
|
||||
|
||||
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply flow display configuration.
|
||||
|
||||
Args:
|
||||
config: Agent configuration dictionary
|
||||
flag_value: Boolean indicating flow display
|
||||
|
||||
Returns:
|
||||
Modified configuration
|
||||
"""
|
||||
"""Apply flow display configuration."""
|
||||
if flag_value:
|
||||
config['flow_display'] = True
|
||||
return config
|
||||
|
||||
def display_workflow_start(self, workflow_name: str, agents: List[str] = None):
|
||||
def display_flow_diagram(self, yaml_file: str, show_footer: bool = True) -> bool:
|
||||
"""
|
||||
Display workflow start.
|
||||
Display a visual flow diagram of the workflow from YAML file.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow
|
||||
agents: List of agent names
|
||||
"""
|
||||
self.print_status(f"\n🚀 Starting Workflow: {workflow_name}", "info")
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
if agents:
|
||||
self.print_status("Agents:", "info")
|
||||
for i, agent in enumerate(agents, 1):
|
||||
self.print_status(f" {i}. {agent}", "info")
|
||||
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
def display_step(self, step_num: int, step_name: str, status: str = "running"):
|
||||
"""
|
||||
Display a workflow step.
|
||||
|
||||
Args:
|
||||
step_num: Step number
|
||||
step_name: Name of the step
|
||||
status: Step status (running, completed, failed)
|
||||
"""
|
||||
icons = {
|
||||
'running': '⏳',
|
||||
'completed': '✅',
|
||||
'failed': '❌',
|
||||
'pending': '⬜'
|
||||
}
|
||||
icon = icons.get(status, '•')
|
||||
self.print_status(f" {icon} Step {step_num}: {step_name}", "info")
|
||||
|
||||
def display_workflow_end(self, success: bool = True, duration: float = None):
|
||||
"""
|
||||
Display workflow end.
|
||||
|
||||
Args:
|
||||
success: Whether workflow completed successfully
|
||||
duration: Workflow duration in seconds
|
||||
"""
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
if success:
|
||||
msg = "✅ Workflow completed successfully"
|
||||
else:
|
||||
msg = "❌ Workflow failed"
|
||||
|
||||
if duration:
|
||||
msg += f" ({duration:.2f}s)"
|
||||
|
||||
self.print_status(msg, "success" if success else "error")
|
||||
|
||||
def track_execution(self, func):
|
||||
"""
|
||||
Decorator to track function execution in flow display.
|
||||
|
||||
Args:
|
||||
func: Function to track
|
||||
yaml_file: Path to the agents.yaml file
|
||||
show_footer: Whether to show the "run without --flow-display" footer
|
||||
|
||||
Returns:
|
||||
Wrapped function
|
||||
True if displayed successfully, False otherwise
|
||||
"""
|
||||
import functools
|
||||
console = self._get_console()
|
||||
if not console:
|
||||
print("Rich library not available for flow display")
|
||||
return False
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
func_name = func.__name__
|
||||
# Load YAML config
|
||||
try:
|
||||
if not os.path.exists(yaml_file):
|
||||
console.print(f"[red]Error: File not found: {yaml_file}[/red]")
|
||||
return False
|
||||
|
||||
self.display_step(1, func_name, "running")
|
||||
with open(yaml_file, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error loading YAML: {e}[/red]")
|
||||
return False
|
||||
|
||||
# Detect YAML format: 'roles' (old) or 'agents'+'steps' (new workflow)
|
||||
has_roles = 'roles' in config and config['roles']
|
||||
has_agents = 'agents' in config and config['agents']
|
||||
has_steps = 'steps' in config and config['steps']
|
||||
|
||||
if not has_roles and not has_agents:
|
||||
console.print("[red]Error: Invalid YAML format. No 'roles' or 'agents' found.[/red]")
|
||||
return False
|
||||
|
||||
try:
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
# Determine format type
|
||||
is_workflow_format = has_agents and has_steps
|
||||
|
||||
# Extract workflow info based on format
|
||||
if is_workflow_format:
|
||||
name = config.get('name', 'Workflow')
|
||||
description = config.get('description', '')
|
||||
framework = config.get('framework', 'praisonai')
|
||||
workflow_config = config.get('workflow', {})
|
||||
memory = workflow_config.get('memory_config', {}).get('persist', False)
|
||||
num_agents = len(config['agents'])
|
||||
num_steps = len(config['steps'])
|
||||
else:
|
||||
name = config.get('topic', 'Workflow')
|
||||
description = ''
|
||||
framework = config.get('framework', 'praisonai')
|
||||
process = config.get('process', 'sequential')
|
||||
memory = config.get('memory', False)
|
||||
num_agents = len(config['roles'])
|
||||
num_steps = sum(len(details.get('tasks', {})) for details in config['roles'].values())
|
||||
|
||||
# Build the flow diagram
|
||||
console.print()
|
||||
|
||||
# === HEADER ===
|
||||
header = Text()
|
||||
header.append("📊 WORKFLOW FLOW DIAGRAM\n", style="bold cyan")
|
||||
header.append("─" * 40, style="dim")
|
||||
|
||||
header_panel = Panel(
|
||||
header,
|
||||
border_style="cyan",
|
||||
box=box.DOUBLE_EDGE,
|
||||
padding=(0, 2)
|
||||
)
|
||||
console.print(header_panel)
|
||||
|
||||
# === WORKFLOW INFO ===
|
||||
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
||||
info_table.add_column("Key", style="bold yellow")
|
||||
info_table.add_column("Value", style="white")
|
||||
|
||||
info_table.add_row("📋 Name:", name)
|
||||
if description:
|
||||
info_table.add_row("📝 Description:", description)
|
||||
|
||||
# Show input if present ('input' is canonical, 'topic' is alias)
|
||||
workflow_input = config.get('input', config.get('topic', ''))
|
||||
if workflow_input:
|
||||
info_table.add_row("💬 Input:", workflow_input)
|
||||
|
||||
info_table.add_row("⚙️ Framework:", framework)
|
||||
|
||||
if is_workflow_format:
|
||||
info_table.add_row("🔄 Format:", "WORKFLOW (steps-based)")
|
||||
planning = workflow_config.get('planning', False)
|
||||
info_table.add_row("🧠 Planning:", "✅ Enabled" if planning else "❌ Disabled")
|
||||
else:
|
||||
info_table.add_row("🔄 Process:", process.upper())
|
||||
|
||||
info_table.add_row("💾 Memory:", "✅ Enabled" if memory else "❌ Disabled")
|
||||
info_table.add_row("👥 Agents:", str(num_agents))
|
||||
info_table.add_row("📝 Steps:", str(num_steps))
|
||||
|
||||
console.print(Panel(info_table, title="[bold white]Workflow Configuration[/bold white]",
|
||||
border_style="blue", box=box.ROUNDED))
|
||||
console.print()
|
||||
|
||||
# === FLOW DIAGRAM ===
|
||||
if is_workflow_format:
|
||||
self._display_steps_flow(console, config)
|
||||
else:
|
||||
process = config.get('process', 'sequential')
|
||||
if process == 'sequential':
|
||||
self._display_sequential_flow(console, config)
|
||||
elif process == 'hierarchical':
|
||||
self._display_hierarchical_flow(console, config)
|
||||
else:
|
||||
self._display_sequential_flow(console, config)
|
||||
|
||||
# === DETAILED AGENT INFO ===
|
||||
console.print()
|
||||
if is_workflow_format:
|
||||
self._display_workflow_agent_details(console, config)
|
||||
else:
|
||||
self._display_agent_details(console, config)
|
||||
|
||||
# === FOOTER ===
|
||||
if show_footer:
|
||||
console.print()
|
||||
footer = Text()
|
||||
footer.append("💡 ", style="bold")
|
||||
footer.append("To execute this workflow, run without --flow-display flag", style="dim italic")
|
||||
console.print(Panel(footer, border_style="dim", box=box.ROUNDED))
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Error: Rich components not available: {e}[/red]")
|
||||
return False
|
||||
|
||||
def _display_sequential_flow(self, console, config: Dict):
|
||||
"""Display sequential flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
roles = list(config['roles'].items())
|
||||
|
||||
# Box width (content area)
|
||||
BOX_WIDTH = 36
|
||||
|
||||
def pad_line(content: str, width: int = BOX_WIDTH) -> str:
|
||||
"""Pad content to fixed width."""
|
||||
padding = width - len(content)
|
||||
if padding > 0:
|
||||
return content + " " * padding
|
||||
return content[:width]
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append(" SEQUENTIAL EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
role_name = details.get('role', role_key)
|
||||
tasks = list(details.get('tasks', {}).keys())
|
||||
|
||||
# Agent box
|
||||
if i == 0:
|
||||
flow_text.append(" ┌── START ──┐\n", style="green")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
# Agent node
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="cyan")
|
||||
line = f" Agent {i+1}: {role_name}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="cyan")
|
||||
|
||||
# Tasks
|
||||
for task in tasks:
|
||||
line = f" -> {task}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim white")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="cyan")
|
||||
|
||||
# Arrow to next
|
||||
if i < len(roles) - 1:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" output\n", style="dim italic")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
else:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
flow_text.append(" └── END ────┘\n", style="red")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_hierarchical_flow(self, console, config: Dict):
|
||||
"""Display hierarchical flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
manager_llm = config.get('manager_llm', 'default')
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append("👑 HIERARCHICAL EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
# Manager at top
|
||||
flow_text.append(" ┌──────────────────────┐\n", style="yellow")
|
||||
flow_text.append(" │ 👑 MANAGER AGENT │\n", style="bold yellow")
|
||||
flow_text.append(f" │ LLM: {manager_llm:<14}│\n", style="dim yellow")
|
||||
flow_text.append(" └──────────┬───────────┘\n", style="yellow")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ┌──────────┴───────────┐\n", style="dim")
|
||||
flow_text.append(" │ delegates to │\n", style="dim italic")
|
||||
flow_text.append(" └──────────┬───────────┘\n", style="dim")
|
||||
|
||||
roles = list(config['roles'].items())
|
||||
num_agents = len(roles)
|
||||
|
||||
# Draw branches
|
||||
if num_agents > 0:
|
||||
branch_line = " "
|
||||
for i in range(num_agents):
|
||||
if i == 0:
|
||||
branch_line += "┌" + "─" * 8
|
||||
elif i == num_agents - 1:
|
||||
branch_line += "┬" + "─" * 8 + "┐"
|
||||
else:
|
||||
branch_line += "┬" + "─" * 8
|
||||
flow_text.append(branch_line + "\n", style="dim")
|
||||
|
||||
# Agent boxes
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
flow_text.append("\n ╔═══════════════╗", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
role_name = details.get('role', role_key)[:12]
|
||||
flow_text.append(f" ║ 🤖 {role_name:<10} ║", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
flow_text.append(" ╚═══════════════╝", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_agent_details(self, console, config: Dict):
|
||||
"""Display detailed agent information."""
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
table = Table(
|
||||
title="[bold cyan]📋 Agent & Task Details[/bold cyan]",
|
||||
box=box.ROUNDED,
|
||||
show_header=True,
|
||||
header_style="bold magenta",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
expand=True
|
||||
)
|
||||
|
||||
table.add_column("#", style="dim", width=3, justify="center")
|
||||
table.add_column("Agent", style="bold yellow", min_width=15)
|
||||
table.add_column("Role", style="cyan", min_width=15)
|
||||
table.add_column("Goal", style="white", min_width=25, overflow="fold")
|
||||
table.add_column("Tasks", style="green", min_width=20)
|
||||
table.add_column("Tools", style="magenta", min_width=15)
|
||||
|
||||
for i, (role_key, details) in enumerate(config['roles'].items(), 1):
|
||||
role_name = details.get('role', role_key)
|
||||
goal = details.get('goal', '-')
|
||||
if len(goal) > 50:
|
||||
goal = goal[:47] + "..."
|
||||
|
||||
tasks = list(details.get('tasks', {}).keys())
|
||||
tasks_str = "\n".join(f"• {t}" for t in tasks) if tasks else "-"
|
||||
|
||||
tools = details.get('tools', [])
|
||||
tools_str = ", ".join(t for t in tools if t) if tools else "-"
|
||||
|
||||
table.add_row(
|
||||
str(i),
|
||||
role_name,
|
||||
role_key,
|
||||
goal,
|
||||
tasks_str,
|
||||
tools_str
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def _display_steps_flow(self, console, config: Dict):
|
||||
"""Display steps-based workflow flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
steps = config.get('steps', [])
|
||||
agents = config.get('agents', {})
|
||||
|
||||
# Box width (content area)
|
||||
BOX_WIDTH = 36
|
||||
|
||||
def pad_line(content: str, width: int = BOX_WIDTH) -> str:
|
||||
"""Pad content to fixed width, accounting for emoji width."""
|
||||
# Emojis typically take 2 character widths
|
||||
emoji_count = sum(1 for c in content if ord(c) > 0x1F300)
|
||||
actual_len = len(content) + emoji_count # Add extra for emoji width
|
||||
padding = width - actual_len
|
||||
if padding > 0:
|
||||
return content + " " * padding
|
||||
return content[:width]
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append(" STEPS-BASED EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
flow_text.append(" ┌── START ──┐\n", style="green")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
# Determine step type
|
||||
agent_key = step.get('agent', '')
|
||||
step_name = step.get('name', '')
|
||||
|
||||
# Check for special step types
|
||||
has_route = 'route' in step
|
||||
has_parallel = 'parallel' in step
|
||||
has_loop = 'loop' in step
|
||||
has_repeat = 'repeat' in step
|
||||
|
||||
if has_route:
|
||||
# Route step
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="yellow")
|
||||
line = f" ROUTE: {step_name or 'routing'}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="yellow")
|
||||
|
||||
routes = step['route']
|
||||
for route_name, route_agents in routes.items():
|
||||
agents_str = ", ".join(route_agents) if isinstance(route_agents, list) else str(route_agents)
|
||||
line = f" -> {route_name}: {agents_str}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="yellow")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="yellow")
|
||||
|
||||
elif has_parallel:
|
||||
# Parallel step
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="green")
|
||||
line = f" PARALLEL: {step_name or 'parallel'}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="green")
|
||||
|
||||
for p_step in step['parallel']:
|
||||
p_agent = p_step.get('agent', '?')
|
||||
line = f" |-- {p_agent}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="green")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="green")
|
||||
|
||||
else:
|
||||
# Regular agent step
|
||||
agent_info = agents.get(agent_key, {})
|
||||
agent_name = agent_info.get('name', agent_key)
|
||||
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="cyan")
|
||||
line = f" Step {i+1}: {agent_name}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="cyan")
|
||||
|
||||
# Show modifiers
|
||||
if has_loop:
|
||||
loop_over = step['loop'].get('over', '?')
|
||||
line = f" [loop over: {loop_over}]"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim magenta")
|
||||
|
||||
if has_repeat:
|
||||
until = step['repeat'].get('until', '?')
|
||||
max_iter = step['repeat'].get('max_iterations', '?')
|
||||
line = f" [repeat until: {until} max:{max_iter}]"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim magenta")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="cyan")
|
||||
|
||||
# Arrow to next
|
||||
if i < len(steps) - 1:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
flow_text.append(" └── END ────┘\n", style="red")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_workflow_agent_details(self, console, config: Dict):
|
||||
"""Display detailed agent information for workflow format."""
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
table = Table(
|
||||
title="[bold cyan]📋 Agent Details[/bold cyan]",
|
||||
box=box.ROUNDED,
|
||||
show_header=True,
|
||||
header_style="bold magenta",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
expand=True
|
||||
)
|
||||
|
||||
table.add_column("#", style="dim", width=3, justify="center")
|
||||
table.add_column("Agent Key", style="bold yellow", min_width=15)
|
||||
table.add_column("Name", style="cyan", min_width=15)
|
||||
table.add_column("Role", style="white", min_width=20)
|
||||
table.add_column("Goal", style="green", min_width=25, overflow="fold")
|
||||
|
||||
for i, (agent_key, details) in enumerate(config['agents'].items(), 1):
|
||||
name = details.get('name', agent_key)
|
||||
role = details.get('role', '-')
|
||||
goal = details.get('goal', '-')
|
||||
if len(goal) > 40:
|
||||
goal = goal[:37] + "..."
|
||||
|
||||
table.add_row(
|
||||
str(i),
|
||||
agent_key,
|
||||
name,
|
||||
role,
|
||||
goal
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def display_workflow_start(self, workflow_name: str, agents: list) -> None:
|
||||
"""
|
||||
Display workflow start message.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow being started
|
||||
agents: List of agent names in the workflow
|
||||
"""
|
||||
console = self._get_console()
|
||||
if console:
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
self.display_step(1, func_name, "completed")
|
||||
return result
|
||||
except Exception:
|
||||
self.display_step(1, func_name, "failed")
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
|
||||
agents_str = ", ".join(agents) if agents else "No agents"
|
||||
content = f"🚀 Starting: {workflow_name}\n👥 Agents: {agents_str}"
|
||||
console.print(Panel(content, title="[bold cyan]Workflow Started[/bold cyan]",
|
||||
border_style="cyan", box=box.ROUNDED))
|
||||
except ImportError:
|
||||
print(f"Starting workflow: {workflow_name} with agents: {agents}")
|
||||
|
||||
def execute(self, workflow_name: str = None, **kwargs) -> Any:
|
||||
def display_workflow_end(self, success: bool = True) -> None:
|
||||
"""
|
||||
Execute flow display setup.
|
||||
Display workflow end message.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow
|
||||
success: Whether the workflow completed successfully
|
||||
"""
|
||||
console = self._get_console()
|
||||
if console:
|
||||
try:
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
|
||||
if success:
|
||||
content = "✅ Workflow completed successfully"
|
||||
style = "green"
|
||||
else:
|
||||
content = "❌ Workflow failed"
|
||||
style = "red"
|
||||
|
||||
console.print(Panel(content, title=f"[bold {style}]Workflow Ended[/bold {style}]",
|
||||
border_style=style, box=box.ROUNDED))
|
||||
except ImportError:
|
||||
status = "successfully" if success else "with errors"
|
||||
print(f"Workflow ended {status}")
|
||||
|
||||
def execute(self, yaml_file: str = None, **kwargs) -> bool:
|
||||
"""
|
||||
Execute flow display - shows diagram without running workflow.
|
||||
|
||||
Args:
|
||||
yaml_file: Path to the YAML file
|
||||
|
||||
Returns:
|
||||
FlowDisplay instance or None
|
||||
True if displayed successfully
|
||||
"""
|
||||
if workflow_name:
|
||||
self.display_workflow_start(workflow_name)
|
||||
|
||||
return self.create_flow_display(workflow_name or "Workflow")
|
||||
if yaml_file:
|
||||
return self.display_flow_diagram(yaml_file)
|
||||
return False
|
||||
@@ -0,0 +1,670 @@
|
||||
"""
|
||||
n8n Integration Handler for PraisonAI CLI.
|
||||
|
||||
Converts PraisonAI agents.yaml workflows to n8n workflow JSON format
|
||||
and opens them in the n8n UI.
|
||||
|
||||
Usage:
|
||||
praisonai agents.yaml --n8n
|
||||
praisonai agents.yaml --n8n --n8n-url http://localhost:5678
|
||||
"""
|
||||
|
||||
import json
|
||||
import yaml
|
||||
import webbrowser
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
REQUESTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
REQUESTS_AVAILABLE = False
|
||||
|
||||
from .base import FlagHandler
|
||||
|
||||
|
||||
class N8nHandler(FlagHandler):
|
||||
"""
|
||||
Handler for n8n workflow export and visualization.
|
||||
|
||||
Converts PraisonAI YAML workflows to n8n-compatible JSON format
|
||||
and opens them in the n8n UI for visual editing.
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False, n8n_url: str = "http://localhost:5678",
|
||||
use_execute_command: bool = False):
|
||||
"""
|
||||
Initialize the n8n handler.
|
||||
|
||||
Args:
|
||||
verbose: Enable verbose output
|
||||
n8n_url: Base URL of the n8n instance
|
||||
use_execute_command: Use Execute Command nodes (runs praisonai directly)
|
||||
instead of HTTP Request nodes
|
||||
"""
|
||||
super().__init__(verbose=verbose)
|
||||
self.n8n_url = n8n_url
|
||||
self.praisonai_api_url = "http://127.0.0.1:8005"
|
||||
self.use_execute_command = use_execute_command
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "n8n"
|
||||
|
||||
@property
|
||||
def flag_name(self) -> str:
|
||||
return "n8n"
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
return "Export workflow to n8n and open in browser"
|
||||
|
||||
def execute(self, yaml_path: str, n8n_url: Optional[str] = None,
|
||||
open_browser: bool = True, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute the n8n export workflow.
|
||||
|
||||
Args:
|
||||
yaml_path: Path to the agents.yaml file
|
||||
n8n_url: Optional custom n8n URL
|
||||
open_browser: Whether to open the workflow in browser
|
||||
|
||||
Returns:
|
||||
Dictionary with workflow JSON and URL
|
||||
"""
|
||||
if n8n_url:
|
||||
self.n8n_url = n8n_url
|
||||
|
||||
# Convert YAML to n8n workflow JSON
|
||||
workflow_json = self.convert_yaml_to_n8n(yaml_path)
|
||||
|
||||
# Save workflow JSON to file
|
||||
output_path = self._save_workflow_json(workflow_json, yaml_path)
|
||||
|
||||
self.print_status("✅ Workflow converted successfully!", "success")
|
||||
self.print_status(f"📄 JSON saved to: {output_path}", "info")
|
||||
|
||||
# Try to create workflow via n8n API
|
||||
api_key = os.environ.get('N8N_API_KEY')
|
||||
workflow_id = None
|
||||
|
||||
if api_key and REQUESTS_AVAILABLE:
|
||||
workflow_id = self._create_workflow_via_api(workflow_json, api_key)
|
||||
|
||||
webhook_url = None
|
||||
if workflow_id:
|
||||
# Successfully created via API - open directly
|
||||
url = f"{self.n8n_url}/workflow/{workflow_id}"
|
||||
self.print_status("🚀 Workflow created in n8n!", "success")
|
||||
|
||||
# Activate the workflow so webhook is available
|
||||
if self._activate_workflow(workflow_id, api_key):
|
||||
self.print_status("✅ Workflow activated!", "success")
|
||||
|
||||
# Get webhook path from the workflow
|
||||
trigger_node = next((n for n in workflow_json["nodes"]
|
||||
if n.get("type") == "n8n-nodes-base.webhook"), None)
|
||||
if trigger_node:
|
||||
webhook_path = trigger_node["parameters"].get("path", "praisonai")
|
||||
webhook_url = f"{self.n8n_url}/webhook/{webhook_path}"
|
||||
self.print_status("", "info")
|
||||
self.print_status("🔗 Webhook URL (to trigger workflow):", "info")
|
||||
self.print_status(f" POST {webhook_url}", "info")
|
||||
else:
|
||||
self.print_status("⚠️ Could not activate workflow (activate manually in n8n)", "warning")
|
||||
|
||||
if open_browser:
|
||||
self.open_in_browser(url)
|
||||
self.print_status(f"🌐 Opening: {url}", "info")
|
||||
else:
|
||||
# Fallback to manual import instructions
|
||||
url = self.generate_n8n_url(workflow_json, self.n8n_url)
|
||||
|
||||
if not api_key:
|
||||
self.print_status("", "info")
|
||||
self.print_status("💡 Tip: Set N8N_API_KEY env var for auto-import", "info")
|
||||
self.print_status(" Generate API key in n8n: Settings → API", "info")
|
||||
|
||||
self.print_status("", "info")
|
||||
self.print_status("📋 To import into n8n:", "info")
|
||||
self.print_status(" 1. Open n8n in your browser", "info")
|
||||
self.print_status(" 2. Click the three dots menu (⋮) in the top right", "info")
|
||||
self.print_status(" 3. Select 'Import from File'", "info")
|
||||
self.print_status(f" 4. Choose: {output_path}", "info")
|
||||
|
||||
if open_browser:
|
||||
self.open_in_browser(url)
|
||||
self.print_status(f"🌐 Opening n8n: {url}", "info")
|
||||
|
||||
return {
|
||||
"workflow": workflow_json,
|
||||
"url": url,
|
||||
"output_path": output_path,
|
||||
"workflow_id": workflow_id,
|
||||
"webhook_url": webhook_url
|
||||
}
|
||||
|
||||
def _create_workflow_via_api(self, workflow_json: Dict[str, Any],
|
||||
api_key: str) -> Optional[str]:
|
||||
"""
|
||||
Create workflow in n8n via REST API.
|
||||
|
||||
Args:
|
||||
workflow_json: The n8n workflow JSON
|
||||
api_key: n8n API key
|
||||
|
||||
Returns:
|
||||
Workflow ID if successful, None otherwise
|
||||
"""
|
||||
if not REQUESTS_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Only include fields allowed by n8n API schema
|
||||
# See: n8n/packages/cli/src/public-api/v1/handlers/workflows/spec/schemas/workflow.yml
|
||||
workflow_data = {
|
||||
"name": workflow_json.get("name", "PraisonAI Workflow"),
|
||||
"nodes": workflow_json.get("nodes", []),
|
||||
"connections": workflow_json.get("connections", {}),
|
||||
"settings": workflow_json.get("settings", {"executionOrder": "v1"}),
|
||||
}
|
||||
|
||||
# Optional fields
|
||||
if "staticData" in workflow_json:
|
||||
workflow_data["staticData"] = workflow_json["staticData"]
|
||||
|
||||
response = requests.post(
|
||||
f"{self.n8n_url}/api/v1/workflows",
|
||||
headers={
|
||||
"X-N8N-API-KEY": api_key,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json=workflow_data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result.get('id')
|
||||
else:
|
||||
self.log(f"API error: {response.status_code} - {response.text}", "warning")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Failed to create workflow via API: {e}", "warning")
|
||||
return None
|
||||
|
||||
def _activate_workflow(self, workflow_id: str, api_key: str) -> bool:
|
||||
"""
|
||||
Activate a workflow via n8n API.
|
||||
|
||||
Args:
|
||||
workflow_id: The workflow ID to activate
|
||||
api_key: n8n API key
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not REQUESTS_AVAILABLE:
|
||||
return False
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.n8n_url}/api/v1/workflows/{workflow_id}/activate",
|
||||
headers={
|
||||
"X-N8N-API-KEY": api_key,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
self.log(f"Activation error: {response.status_code} - {response.text}", "warning")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Failed to activate workflow: {e}", "warning")
|
||||
return False
|
||||
|
||||
def convert_yaml_to_n8n(self, yaml_path: str, use_webhook: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert a PraisonAI YAML workflow to n8n JSON format.
|
||||
|
||||
The n8n workflow will have:
|
||||
- A webhook trigger to receive requests
|
||||
- One HTTP Request node per agent (calls /agents/{agent_name})
|
||||
- Each agent node passes its output to the next agent
|
||||
|
||||
Args:
|
||||
yaml_path: Path to the agents.yaml file
|
||||
use_webhook: Use webhook trigger for programmatic execution (default: True)
|
||||
|
||||
Returns:
|
||||
n8n workflow JSON as dictionary
|
||||
"""
|
||||
# Load YAML file
|
||||
with open(yaml_path, 'r') as f:
|
||||
yaml_content = yaml.safe_load(f)
|
||||
|
||||
# Extract workflow metadata
|
||||
workflow_name = yaml_content.get('name', 'PraisonAI Workflow')
|
||||
description = yaml_content.get('description', '')
|
||||
|
||||
# Generate a unique webhook path from workflow name
|
||||
webhook_path = workflow_name.lower().replace(' ', '-').replace('/', '-')[:50]
|
||||
|
||||
# Get agents and steps
|
||||
agents = yaml_content.get('agents', yaml_content.get('roles', {}))
|
||||
steps = yaml_content.get('steps', [])
|
||||
|
||||
# If no steps defined, create steps from agents
|
||||
if not steps and agents:
|
||||
steps = self._create_steps_from_agents(agents)
|
||||
|
||||
# Build n8n nodes and connections
|
||||
nodes = []
|
||||
connections = {}
|
||||
|
||||
# Add trigger node (webhook for programmatic execution, manual otherwise)
|
||||
trigger_node = self._create_trigger_node(use_webhook=use_webhook, webhook_path=webhook_path)
|
||||
nodes.append(trigger_node)
|
||||
|
||||
# Track previous node for connections
|
||||
prev_node_name = trigger_node["name"]
|
||||
x_position = 450
|
||||
|
||||
# Create one HTTP node per agent step
|
||||
for i, step in enumerate(steps):
|
||||
if isinstance(step, dict):
|
||||
agent_id = step.get('agent', '')
|
||||
action = step.get('action', step.get('description', ''))
|
||||
agent_config = agents.get(agent_id, {})
|
||||
|
||||
# Create HTTP node that calls the specific agent endpoint
|
||||
agent_node = self._create_per_agent_node(
|
||||
agent_id=agent_id,
|
||||
agent_config=agent_config,
|
||||
action=action,
|
||||
position=[x_position, 300],
|
||||
index=i,
|
||||
is_first=(i == 0)
|
||||
)
|
||||
nodes.append(agent_node)
|
||||
|
||||
# Connect to previous node
|
||||
if prev_node_name not in connections:
|
||||
connections[prev_node_name] = {"main": [[]]}
|
||||
connections[prev_node_name]["main"][0].append({
|
||||
"node": agent_node["name"],
|
||||
"type": "main",
|
||||
"index": 0
|
||||
})
|
||||
|
||||
prev_node_name = agent_node["name"]
|
||||
x_position += 250
|
||||
|
||||
# Build the complete workflow
|
||||
# Generate a unique ID based on workflow name
|
||||
workflow_id = hashlib.md5(workflow_name.encode()).hexdigest()[:8]
|
||||
|
||||
workflow = {
|
||||
"id": workflow_id,
|
||||
"name": workflow_name,
|
||||
"nodes": nodes,
|
||||
"connections": connections,
|
||||
"active": False,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "1",
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "praisonai-export",
|
||||
"templateCredsSetupCompleted": True
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
|
||||
if description:
|
||||
workflow["meta"]["description"] = description
|
||||
|
||||
return workflow
|
||||
|
||||
def _create_steps_from_agents(self, agents: Dict[str, Any]) -> List[Dict]:
|
||||
"""Create steps from agents when no steps are defined."""
|
||||
steps = []
|
||||
for agent_id, agent_config in agents.items():
|
||||
if isinstance(agent_config, dict):
|
||||
# Check for tasks nested in agent
|
||||
tasks = agent_config.get('tasks', {})
|
||||
if tasks:
|
||||
for task_id, task_config in tasks.items():
|
||||
steps.append({
|
||||
'agent': agent_id,
|
||||
'action': task_config.get('description', task_config.get('action', f'Execute {task_id}'))
|
||||
})
|
||||
else:
|
||||
# Create a default step for the agent
|
||||
steps.append({
|
||||
'agent': agent_id,
|
||||
'action': agent_config.get('instructions', agent_config.get('goal', f'Execute {agent_id}'))
|
||||
})
|
||||
return steps
|
||||
|
||||
def _create_trigger_node(self, use_webhook: bool = False,
|
||||
webhook_path: str = "praisonai") -> Dict[str, Any]:
|
||||
"""Create a trigger node (webhook or manual).
|
||||
|
||||
Args:
|
||||
use_webhook: If True, creates a webhook trigger for programmatic execution
|
||||
webhook_path: Path for the webhook URL (only used if use_webhook=True)
|
||||
"""
|
||||
if use_webhook:
|
||||
# Generate a unique webhook ID
|
||||
webhook_id = hashlib.md5(webhook_path.encode()).hexdigest()[:16]
|
||||
return {
|
||||
"id": "trigger",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300],
|
||||
"webhookId": webhook_id,
|
||||
"parameters": {
|
||||
"path": webhook_path,
|
||||
"httpMethod": "POST",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
return {
|
||||
"id": "trigger",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300],
|
||||
"parameters": {}
|
||||
}
|
||||
|
||||
def _create_per_agent_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int, is_first: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Create an HTTP Request node that calls a specific agent endpoint.
|
||||
|
||||
Args:
|
||||
agent_id: Agent identifier (used in URL path)
|
||||
agent_config: Agent configuration from YAML
|
||||
action: The action/task description for this agent
|
||||
position: Node position [x, y]
|
||||
index: Node index
|
||||
is_first: Whether this is the first agent (uses webhook input)
|
||||
|
||||
Returns:
|
||||
n8n HTTP Request node configuration
|
||||
"""
|
||||
agent_name = agent_config.get('name', agent_id.title())
|
||||
# Convert agent_id to URL-safe format (lowercase, underscores)
|
||||
agent_url_id = agent_id.lower().replace(' ', '_')
|
||||
|
||||
# Build the query - first agent uses webhook input, others use previous response
|
||||
if is_first:
|
||||
# First agent: use query from webhook body
|
||||
query_expr = f"={{ $json.body?.query || $json.query || '{action}' }}"
|
||||
else:
|
||||
# Subsequent agents: use previous agent's response + original action context
|
||||
query_expr = f"={{ $json.response || '{action}' }}"
|
||||
|
||||
return {
|
||||
"id": f"agent_{index}",
|
||||
"name": agent_name,
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": position,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": f"{self.praisonai_api_url}/agents/{agent_url_id}",
|
||||
"sendBody": True,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": f"={{{{ JSON.stringify({{ query: {query_expr[2:]} }}) }}}}",
|
||||
"options": {
|
||||
"timeout": 300000 # 5 minute timeout per agent
|
||||
}
|
||||
},
|
||||
"notes": f"Agent: {agent_name}\nTask: {action[:100]}"
|
||||
}
|
||||
|
||||
def _create_praisonai_workflow_node(self, workflow_name: str,
|
||||
agent_names: List[str],
|
||||
position: List[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a single HTTP Request node that triggers the entire PraisonAI workflow.
|
||||
|
||||
The PraisonAI API will run all agents sequentially when called.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow
|
||||
agent_names: List of agent names (for documentation)
|
||||
position: Node position [x, y]
|
||||
|
||||
Returns:
|
||||
n8n HTTP Request node configuration
|
||||
"""
|
||||
# The query will be passed from the webhook trigger
|
||||
# Using n8n expression to get the body from the webhook
|
||||
return {
|
||||
"id": "praisonai_workflow",
|
||||
"name": f"PraisonAI: {workflow_name}",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": position,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": f"{self.praisonai_api_url}/agents",
|
||||
"sendBody": True,
|
||||
"specifyBody": "json",
|
||||
# Pass the query from webhook body, or use a default
|
||||
"jsonBody": "={{ JSON.stringify({ query: $json.body?.query || $json.query || 'Start workflow' }) }}",
|
||||
"options": {
|
||||
"timeout": 600000 # 10 minute timeout for full workflow
|
||||
}
|
||||
},
|
||||
"notes": f"Runs agents: {', '.join(agent_names)}"
|
||||
}
|
||||
|
||||
def _create_agent_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create a node for an agent - either Execute Command or HTTP Request."""
|
||||
if self.use_execute_command:
|
||||
return self._create_execute_command_node(
|
||||
agent_id, agent_config, action, position, index
|
||||
)
|
||||
else:
|
||||
return self._create_http_request_node(
|
||||
agent_id, agent_config, action, position, index
|
||||
)
|
||||
|
||||
def _create_execute_command_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create an Execute Command node that runs praisonai directly."""
|
||||
agent_name = agent_config.get('name', agent_id.title())
|
||||
|
||||
# Escape quotes in the action for shell command
|
||||
escaped_action = action.replace('"', '\\"').replace("'", "\\'")
|
||||
|
||||
# Build the praisonai command
|
||||
# Uses the input from previous node if available
|
||||
command = f'praisonai "{escaped_action}"'
|
||||
|
||||
return {
|
||||
"id": f"agent_{index}",
|
||||
"name": agent_name,
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": position,
|
||||
"parameters": {
|
||||
"command": command,
|
||||
"executeOnce": True
|
||||
}
|
||||
}
|
||||
|
||||
def _create_http_request_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create an HTTP Request node for an agent."""
|
||||
agent_name = agent_config.get('name', agent_id.title())
|
||||
|
||||
# Build the request body
|
||||
request_body = {
|
||||
"query": action,
|
||||
"agent": agent_id
|
||||
}
|
||||
|
||||
return {
|
||||
"id": f"agent_{index}",
|
||||
"name": agent_name,
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": position,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": f"{self.praisonai_api_url}/agents",
|
||||
"sendBody": True,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": json.dumps(request_body),
|
||||
"options": {
|
||||
"timeout": 300000 # 5 minute timeout for LLM calls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def _create_parallel_nodes(self, parallel_steps: List[Dict],
|
||||
x_position: int,
|
||||
agents: Dict[str, Any]) -> tuple:
|
||||
"""Create nodes for parallel execution."""
|
||||
nodes = []
|
||||
connections = {}
|
||||
|
||||
for i, step in enumerate(parallel_steps):
|
||||
agent_id = step.get('agent', '')
|
||||
action = step.get('action', step.get('description', ''))
|
||||
agent_config = agents.get(agent_id, {})
|
||||
|
||||
node = self._create_agent_node(
|
||||
agent_id=agent_id,
|
||||
agent_config=agent_config,
|
||||
action=action,
|
||||
position=[x_position + (i * 200), 300 + (i * 100)],
|
||||
index=100 + i # Offset index for parallel nodes
|
||||
)
|
||||
nodes.append(node)
|
||||
|
||||
return nodes, connections
|
||||
|
||||
def _create_route_node(self, step: Dict, x_position: int,
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create an IF node for routing decisions."""
|
||||
route_config = step.get('route', {})
|
||||
conditions = list(route_config.keys())
|
||||
|
||||
return {
|
||||
"id": f"route_{index}",
|
||||
"name": f"Route Decision {index}",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [x_position, 300],
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": True,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": f"condition_{i}",
|
||||
"leftValue": "={{ $json.result }}",
|
||||
"rightValue": condition,
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "contains"
|
||||
}
|
||||
}
|
||||
for i, condition in enumerate(conditions) if condition != 'default'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generate_n8n_url(self, workflow_json: Dict[str, Any],
|
||||
base_url: str) -> str:
|
||||
"""
|
||||
Generate a URL to open n8n workflow editor.
|
||||
|
||||
Note: n8n doesn't support URL hash-based import. Users need to:
|
||||
1. Open n8n
|
||||
2. Use "Import from File" to load the saved JSON
|
||||
|
||||
Args:
|
||||
workflow_json: The n8n workflow JSON (unused, kept for API compatibility)
|
||||
base_url: Base URL of the n8n instance
|
||||
|
||||
Returns:
|
||||
URL to open n8n workflow editor
|
||||
"""
|
||||
# n8n doesn't support URL-based workflow import via hash
|
||||
# Return the new workflow URL - user will need to import manually
|
||||
return f"{base_url}/workflow/new"
|
||||
|
||||
def open_in_browser(self, url: str) -> None:
|
||||
"""
|
||||
Open a URL in the default web browser.
|
||||
|
||||
Args:
|
||||
url: URL to open
|
||||
"""
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception as e:
|
||||
self.print_status(f"Could not open browser: {e}", "warning")
|
||||
self.print_status(f"Please open this URL manually: {url}", "info")
|
||||
|
||||
def _save_workflow_json(self, workflow_json: Dict[str, Any],
|
||||
yaml_path: str) -> str:
|
||||
"""
|
||||
Save the workflow JSON to a file.
|
||||
|
||||
Args:
|
||||
workflow_json: The n8n workflow JSON
|
||||
yaml_path: Original YAML file path
|
||||
|
||||
Returns:
|
||||
Path to the saved JSON file
|
||||
"""
|
||||
# Create output path based on input YAML
|
||||
yaml_file = Path(yaml_path)
|
||||
output_path = yaml_file.parent / f"{yaml_file.stem}_n8n.json"
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(workflow_json, f, indent=2)
|
||||
|
||||
return str(output_path)
|
||||
|
||||
def get_workflow_json_for_clipboard(self, yaml_path: str) -> str:
|
||||
"""
|
||||
Get the workflow JSON as a string for clipboard copy.
|
||||
|
||||
Args:
|
||||
yaml_path: Path to the agents.yaml file
|
||||
|
||||
Returns:
|
||||
JSON string ready for clipboard
|
||||
"""
|
||||
workflow_json = self.convert_yaml_to_n8n(yaml_path)
|
||||
return json.dumps(workflow_json, indent=2)
|
||||
@@ -2,12 +2,12 @@
|
||||
Session Handler for CLI.
|
||||
|
||||
Provides session management for multi-turn conversations.
|
||||
Usage: praisonai session start "project-x"
|
||||
praisonai session list
|
||||
praisonai session resume "project-x"
|
||||
Usage: praisonai session list
|
||||
praisonai session show <name>
|
||||
praisonai session resume <name>
|
||||
praisonai session delete <name>
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
from .base import CommandHandler
|
||||
|
||||
@@ -16,151 +16,175 @@ class SessionHandler(CommandHandler):
|
||||
"""
|
||||
Handler for session command.
|
||||
|
||||
Manages conversation sessions for multi-turn interactions.
|
||||
Manages conversation sessions using FileMemory.
|
||||
|
||||
Example:
|
||||
praisonai session start my-project
|
||||
praisonai session list
|
||||
praisonai session show my-project
|
||||
praisonai session resume my-project
|
||||
praisonai session delete my-project
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False, workspace: str = None):
|
||||
def __init__(self, verbose: bool = False, user_id: str = "praison"):
|
||||
super().__init__(verbose)
|
||||
self.workspace = workspace or os.path.join(os.path.expanduser("~"), ".praison", "sessions")
|
||||
os.makedirs(self.workspace, exist_ok=True)
|
||||
self._session = None
|
||||
self.user_id = user_id
|
||||
self._memory = None
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "session"
|
||||
|
||||
def get_actions(self) -> List[str]:
|
||||
return ["start", "list", "resume", "delete", "info", "help"]
|
||||
return ["list", "show", "resume", "delete", "help"]
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
return """
|
||||
Session Commands:
|
||||
praisonai session start <name> - Start a new session
|
||||
praisonai session list - List all sessions
|
||||
praisonai session resume <name> - Resume an existing session
|
||||
praisonai session list - List all saved sessions
|
||||
praisonai session show <name> - Show session details
|
||||
praisonai session resume <name> - Resume a session (load into memory)
|
||||
praisonai session delete <name> - Delete a session
|
||||
praisonai session info <name> - Show session details
|
||||
|
||||
Sessions enable multi-turn conversations with context preservation.
|
||||
Sessions store conversation history for multi-turn interactions.
|
||||
Use --auto-save <name> flag when running agents to auto-save sessions.
|
||||
"""
|
||||
|
||||
def _get_session_class(self):
|
||||
"""Get Session class lazily."""
|
||||
try:
|
||||
from praisonaiagents import Session
|
||||
return Session
|
||||
except ImportError:
|
||||
self.print_status(
|
||||
"Session requires praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
def _get_memory(self):
|
||||
"""Get FileMemory instance lazily."""
|
||||
if self._memory is None:
|
||||
try:
|
||||
from praisonaiagents.memory.file_memory import FileMemory
|
||||
self._memory = FileMemory(user_id=self.user_id)
|
||||
except ImportError:
|
||||
self.print_status(
|
||||
"Session requires praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
return self._memory
|
||||
|
||||
def _get_session_path(self, name: str) -> str:
|
||||
"""Get path for a session file."""
|
||||
return os.path.join(self.workspace, f"{name}.json")
|
||||
|
||||
def action_start(self, args: List[str], **kwargs) -> Any:
|
||||
def action_list(self, args: List[str], **kwargs) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Start a new session.
|
||||
|
||||
Args:
|
||||
args: List containing session name
|
||||
|
||||
Returns:
|
||||
Session instance or None
|
||||
"""
|
||||
if not args:
|
||||
self.print_status("Usage: praisonai session start <name>", "error")
|
||||
return None
|
||||
|
||||
Session = self._get_session_class()
|
||||
if not Session:
|
||||
return None
|
||||
|
||||
name = args[0]
|
||||
session_path = self._get_session_path(name)
|
||||
|
||||
if os.path.exists(session_path):
|
||||
self.print_status(f"Session '{name}' already exists. Use 'resume' to continue.", "warning")
|
||||
return None
|
||||
|
||||
try:
|
||||
session = Session(session_id=name)
|
||||
self._session = session
|
||||
self.print_status(f"✅ Session '{name}' started", "success")
|
||||
return session
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to start session: {e}", "error")
|
||||
return None
|
||||
|
||||
def action_list(self, args: List[str], **kwargs) -> List[str]:
|
||||
"""
|
||||
List all sessions.
|
||||
List all saved sessions.
|
||||
|
||||
Returns:
|
||||
List of session names
|
||||
List of session info dicts
|
||||
"""
|
||||
sessions = []
|
||||
memory = self._get_memory()
|
||||
if not memory:
|
||||
return []
|
||||
|
||||
try:
|
||||
for file in os.listdir(self.workspace):
|
||||
if file.endswith('.json'):
|
||||
sessions.append(file[:-5]) # Remove .json extension
|
||||
sessions = memory.list_sessions()
|
||||
|
||||
if sessions:
|
||||
self.print_status("\n📋 Available Sessions:", "info")
|
||||
for name in sessions:
|
||||
self.print_status(f" - {name}", "info")
|
||||
from rich.table import Table
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Saved Sessions")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Saved At", style="green")
|
||||
table.add_column("Messages", style="yellow")
|
||||
table.add_column("Has History", style="magenta")
|
||||
|
||||
for s in sessions:
|
||||
table.add_row(
|
||||
s.get("name", ""),
|
||||
s.get("saved_at", "")[:19] if s.get("saved_at") else "",
|
||||
str(s.get("short_term_count", 0)),
|
||||
"✅" if s.get("has_conversation") else "❌"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
else:
|
||||
self.print_status("No sessions found", "warning")
|
||||
self.print_status("No sessions found. Use --auto-save <name> when running agents.", "warning")
|
||||
|
||||
return sessions
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to list sessions: {e}", "error")
|
||||
return []
|
||||
|
||||
def action_resume(self, args: List[str], **kwargs) -> Any:
|
||||
def action_show(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Resume an existing session.
|
||||
Show session details.
|
||||
|
||||
Args:
|
||||
args: List containing session name
|
||||
|
||||
Returns:
|
||||
Session instance or None
|
||||
Session data dict
|
||||
"""
|
||||
if not args:
|
||||
self.print_status("Usage: praisonai session show <name>", "error")
|
||||
return {}
|
||||
|
||||
memory = self._get_memory()
|
||||
if not memory:
|
||||
return {}
|
||||
|
||||
name = args[0]
|
||||
|
||||
try:
|
||||
session_data = memory.resume_session(name)
|
||||
|
||||
print(f"\n📊 Session: {name}")
|
||||
print(f"Saved at: {session_data.get('saved_at_iso', 'Unknown')}")
|
||||
print(f"User ID: {session_data.get('user_id', 'Unknown')}")
|
||||
print(f"Short-term items: {len(session_data.get('short_term', []))}")
|
||||
print(f"Conversation messages: {len(session_data.get('conversation_history', []))}")
|
||||
|
||||
# Show last few messages
|
||||
history = session_data.get('conversation_history', [])
|
||||
if history:
|
||||
print(f"\nLast {min(5, len(history))} messages:")
|
||||
for msg in history[-5:]:
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')[:100]
|
||||
if len(msg.get('content', '')) > 100:
|
||||
content += "..."
|
||||
print(f" [{role}]: {content}")
|
||||
|
||||
return session_data
|
||||
except FileNotFoundError:
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
return {}
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to show session: {e}", "error")
|
||||
return {}
|
||||
|
||||
def action_resume(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Resume a session (load into memory).
|
||||
|
||||
Args:
|
||||
args: List containing session name
|
||||
|
||||
Returns:
|
||||
Session data dict
|
||||
"""
|
||||
if not args:
|
||||
self.print_status("Usage: praisonai session resume <name>", "error")
|
||||
return None
|
||||
return {}
|
||||
|
||||
Session = self._get_session_class()
|
||||
if not Session:
|
||||
return None
|
||||
memory = self._get_memory()
|
||||
if not memory:
|
||||
return {}
|
||||
|
||||
name = args[0]
|
||||
session_path = self._get_session_path(name)
|
||||
|
||||
if not os.path.exists(session_path):
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
return None
|
||||
|
||||
try:
|
||||
session = Session(session_id=name)
|
||||
if hasattr(session, 'load'):
|
||||
session.load(session_path)
|
||||
self._session = session
|
||||
session_data = memory.resume_session(name)
|
||||
self.print_status(f"✅ Session '{name}' resumed", "success")
|
||||
return session
|
||||
print(f" Loaded {len(session_data.get('short_term', []))} memory items")
|
||||
print(f" Loaded {len(session_data.get('conversation_history', []))} conversation messages")
|
||||
return session_data
|
||||
except FileNotFoundError:
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
return {}
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to resume session: {e}", "error")
|
||||
return None
|
||||
return {}
|
||||
|
||||
def action_delete(self, args: List[str], **kwargs) -> bool:
|
||||
"""
|
||||
@@ -176,63 +200,23 @@ Sessions enable multi-turn conversations with context preservation.
|
||||
self.print_status("Usage: praisonai session delete <name>", "error")
|
||||
return False
|
||||
|
||||
name = args[0]
|
||||
session_path = self._get_session_path(name)
|
||||
|
||||
if not os.path.exists(session_path):
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
memory = self._get_memory()
|
||||
if not memory:
|
||||
return False
|
||||
|
||||
name = args[0]
|
||||
|
||||
try:
|
||||
os.remove(session_path)
|
||||
self.print_status(f"✅ Session '{name}' deleted", "success")
|
||||
return True
|
||||
if memory.delete_session(name):
|
||||
self.print_status(f"✅ Session '{name}' deleted", "success")
|
||||
return True
|
||||
else:
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to delete session: {e}", "error")
|
||||
return False
|
||||
|
||||
def action_info(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Show session info.
|
||||
|
||||
Args:
|
||||
args: List containing session name
|
||||
|
||||
Returns:
|
||||
Dictionary of session info
|
||||
"""
|
||||
if not args:
|
||||
self.print_status("Usage: praisonai session info <name>", "error")
|
||||
return {}
|
||||
|
||||
name = args[0]
|
||||
session_path = self._get_session_path(name)
|
||||
|
||||
if not os.path.exists(session_path):
|
||||
self.print_status(f"Session '{name}' not found", "error")
|
||||
return {}
|
||||
|
||||
try:
|
||||
import json
|
||||
with open(session_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
info = {
|
||||
"name": name,
|
||||
"path": session_path,
|
||||
"messages": len(data.get('messages', [])),
|
||||
"created": data.get('created_at', 'Unknown')
|
||||
}
|
||||
|
||||
self.print_status(f"\n📊 Session '{name}' Info:", "info")
|
||||
for key, value in info.items():
|
||||
self.print_status(f" {key}: {value}", "info")
|
||||
|
||||
return info
|
||||
except Exception as e:
|
||||
self.print_status(f"Failed to get session info: {e}", "error")
|
||||
return {}
|
||||
|
||||
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
|
||||
"""Execute session command action."""
|
||||
return super().execute(action, action_args, **kwargs)
|
||||
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
Workflow Handler for CLI.
|
||||
|
||||
Provides YAML workflow management commands.
|
||||
Usage: praisonai workflow run research.yaml
|
||||
praisonai workflow validate research.yaml
|
||||
praisonai workflow list
|
||||
praisonai workflow create --template routing
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from .base import CommandHandler
|
||||
|
||||
|
||||
class WorkflowHandler(CommandHandler):
|
||||
"""
|
||||
Handler for workflow command.
|
||||
|
||||
Manages YAML workflow files - run, validate, list, and create from templates.
|
||||
|
||||
Example:
|
||||
praisonai workflow run research.yaml --var topic="AI"
|
||||
praisonai workflow validate research.yaml
|
||||
praisonai workflow list
|
||||
praisonai workflow create --template routing --output my_workflow.yaml
|
||||
"""
|
||||
|
||||
# Workflow templates
|
||||
TEMPLATES = {
|
||||
"simple": """# Simple Sequential Workflow
|
||||
name: Simple Workflow
|
||||
description: A simple sequential workflow
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics
|
||||
instructions: "Provide concise, factual information."
|
||||
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Write content
|
||||
instructions: "Write clear, engaging content."
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research: {{input}}"
|
||||
|
||||
- agent: writer
|
||||
action: "Write summary based on: {{previous_output}}"
|
||||
""",
|
||||
"routing": """# Routing Workflow
|
||||
name: Routing Workflow
|
||||
description: Classifier routes to specialized agents
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
classifier:
|
||||
name: Classifier
|
||||
role: Request Classifier
|
||||
goal: Classify requests
|
||||
instructions: "Respond with ONLY 'technical', 'creative', or 'general'."
|
||||
|
||||
tech_agent:
|
||||
name: TechExpert
|
||||
role: Technical Expert
|
||||
goal: Handle technical questions
|
||||
instructions: "Provide technical answers."
|
||||
|
||||
creative_agent:
|
||||
name: Creative
|
||||
role: Creative Writer
|
||||
goal: Handle creative requests
|
||||
instructions: "Write creative content."
|
||||
|
||||
general_agent:
|
||||
name: General
|
||||
role: General Assistant
|
||||
goal: Handle general requests
|
||||
instructions: "Provide helpful responses."
|
||||
|
||||
steps:
|
||||
- agent: classifier
|
||||
action: "Classify: {{input}}"
|
||||
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_agent]
|
||||
creative: [creative_agent]
|
||||
default: [general_agent]
|
||||
""",
|
||||
"parallel": """# Parallel Workflow
|
||||
name: Parallel Research Workflow
|
||||
description: Multiple agents work concurrently
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher1:
|
||||
name: MarketResearcher
|
||||
role: Market Analyst
|
||||
goal: Research market trends
|
||||
instructions: "Provide market insights."
|
||||
|
||||
researcher2:
|
||||
name: CompetitorResearcher
|
||||
role: Competitor Analyst
|
||||
goal: Research competitors
|
||||
instructions: "Provide competitor insights."
|
||||
|
||||
aggregator:
|
||||
name: Aggregator
|
||||
role: Synthesizer
|
||||
goal: Combine findings
|
||||
instructions: "Synthesize all research."
|
||||
|
||||
steps:
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: researcher1
|
||||
action: "Research market for: {{input}}"
|
||||
- agent: researcher2
|
||||
action: "Research competitors for: {{input}}"
|
||||
|
||||
- agent: aggregator
|
||||
action: "Combine all findings"
|
||||
""",
|
||||
"loop": """# Loop Workflow
|
||||
name: Loop Processing Workflow
|
||||
description: Process multiple items in a loop
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
variables:
|
||||
items:
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
|
||||
agents:
|
||||
processor:
|
||||
name: Processor
|
||||
role: Item Processor
|
||||
goal: Process each item
|
||||
instructions: "Process the given item thoroughly."
|
||||
|
||||
summarizer:
|
||||
name: Summarizer
|
||||
role: Summarizer
|
||||
goal: Summarize results
|
||||
instructions: "Summarize all processed items."
|
||||
|
||||
steps:
|
||||
- agent: processor
|
||||
action: "Process: {{item}}"
|
||||
loop:
|
||||
over: items
|
||||
|
||||
- agent: summarizer
|
||||
action: "Summarize all processed items"
|
||||
""",
|
||||
"evaluator-optimizer": """# Evaluator-Optimizer Workflow
|
||||
name: Evaluator Optimizer Workflow
|
||||
description: Generate and improve until approved
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
generator:
|
||||
name: Generator
|
||||
role: Content Generator
|
||||
goal: Generate content
|
||||
instructions: "Generate content. Improve based on feedback if provided."
|
||||
|
||||
evaluator:
|
||||
name: Evaluator
|
||||
role: Evaluator
|
||||
goal: Evaluate content
|
||||
instructions: "If good, respond 'APPROVED'. Otherwise provide feedback."
|
||||
|
||||
steps:
|
||||
- agent: generator
|
||||
action: "Generate content for: {{input}}"
|
||||
|
||||
- agent: evaluator
|
||||
action: "Evaluate: {{previous_output}}"
|
||||
repeat:
|
||||
until: "approved"
|
||||
max_iterations: 3
|
||||
"""
|
||||
}
|
||||
|
||||
def __init__(self, verbose: bool = False):
|
||||
super().__init__(verbose)
|
||||
self._manager = None
|
||||
self._parser = None
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "workflow"
|
||||
|
||||
def get_actions(self) -> List[str]:
|
||||
return ["run", "validate", "list", "create", "auto", "help"]
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
return """
|
||||
Workflow Commands:
|
||||
praisonai workflow run <file.yaml> - Run a YAML workflow
|
||||
praisonai workflow run <file.yaml> --var key=value - Run with variables
|
||||
praisonai workflow validate <file.yaml> - Validate a YAML workflow
|
||||
praisonai workflow list - List workflows in .praison/workflows/
|
||||
praisonai workflow create --template <name> - Create from template
|
||||
praisonai workflow auto "topic" --pattern <pattern> - Auto-generate workflow
|
||||
|
||||
Templates: simple, routing, parallel, loop, evaluator-optimizer
|
||||
Patterns: sequential, routing, parallel
|
||||
|
||||
Example:
|
||||
praisonai workflow run research.yaml --var topic="AI trends"
|
||||
praisonai workflow create --template routing --output my_workflow.yaml
|
||||
praisonai workflow auto "Research AI trends" --pattern parallel
|
||||
"""
|
||||
|
||||
def _get_manager(self):
|
||||
"""Get WorkflowManager lazily."""
|
||||
if self._manager is None:
|
||||
try:
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
self._manager = WorkflowManager()
|
||||
except ImportError:
|
||||
self.print_status(
|
||||
"Workflows require praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
return self._manager
|
||||
|
||||
def _get_parser(self):
|
||||
"""Get YAMLWorkflowParser lazily."""
|
||||
if self._parser is None:
|
||||
try:
|
||||
from praisonaiagents.workflows import YAMLWorkflowParser
|
||||
self._parser = YAMLWorkflowParser()
|
||||
except ImportError:
|
||||
self.print_status(
|
||||
"Workflows require praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
return self._parser
|
||||
|
||||
def _parse_variables(self, args: List[str]) -> Dict[str, Any]:
|
||||
"""Parse --var key=value arguments."""
|
||||
variables = {}
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--var" and i + 1 < len(args):
|
||||
var_str = args[i + 1]
|
||||
if "=" in var_str:
|
||||
key, value = var_str.split("=", 1)
|
||||
variables[key.strip()] = value.strip()
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
return variables
|
||||
|
||||
def _get_file_from_args(self, args: List[str]) -> Optional[str]:
|
||||
"""Get the file path from arguments."""
|
||||
for arg in args:
|
||||
if arg.endswith(('.yaml', '.yml')) and not arg.startswith('--'):
|
||||
return arg
|
||||
return None
|
||||
|
||||
def action_run(self, args: List[str], **kwargs) -> Any:
|
||||
"""
|
||||
Run a YAML workflow file.
|
||||
|
||||
Args:
|
||||
args: [file.yaml, --var, key=value, ...]
|
||||
|
||||
Returns:
|
||||
Workflow execution result
|
||||
"""
|
||||
file_path = self._get_file_from_args(args)
|
||||
if not file_path:
|
||||
self.print_status("Usage: praisonai workflow run <file.yaml>", "error")
|
||||
return None
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.print_status(f"File not found: {file_path}", "error")
|
||||
return None
|
||||
|
||||
manager = self._get_manager()
|
||||
if not manager:
|
||||
return None
|
||||
|
||||
# Parse variables
|
||||
variables = self._parse_variables(args)
|
||||
|
||||
# Get input if provided
|
||||
input_data = kwargs.get('input', '')
|
||||
|
||||
# Check for --verbose flag
|
||||
verbose = '--verbose' in args or '-v' in args
|
||||
|
||||
self.print_status(f"Running workflow: {file_path}", "info")
|
||||
if variables:
|
||||
self.print_status(f"Variables: {variables}", "info")
|
||||
|
||||
try:
|
||||
result = manager.execute_yaml(
|
||||
file_path,
|
||||
input_data=input_data,
|
||||
variables=variables,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
self.print_status("Workflow completed!", "success")
|
||||
|
||||
# Print output
|
||||
if result.get('output'):
|
||||
from rich import print as rprint
|
||||
rprint("\n[bold]Output:[/bold]")
|
||||
rprint(result['output'])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.print_status(f"Workflow failed: {e}", "error")
|
||||
return None
|
||||
|
||||
def action_validate(self, args: List[str], **kwargs) -> bool:
|
||||
"""
|
||||
Validate a YAML workflow file.
|
||||
|
||||
Args:
|
||||
args: [file.yaml]
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
file_path = self._get_file_from_args(args)
|
||||
if not file_path:
|
||||
self.print_status("Usage: praisonai workflow validate <file.yaml>", "error")
|
||||
return False
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.print_status(f"File not found: {file_path}", "error")
|
||||
return False
|
||||
|
||||
parser = self._get_parser()
|
||||
if not parser:
|
||||
return False
|
||||
|
||||
self.print_status(f"Validating: {file_path}", "info")
|
||||
|
||||
try:
|
||||
# Load raw YAML to check for non-canonical names
|
||||
import yaml
|
||||
with open(file_path, 'r') as f:
|
||||
raw_data = yaml.safe_load(f)
|
||||
|
||||
# Check for non-canonical names and suggest canonical ones
|
||||
suggestions = self._get_canonical_suggestions(raw_data)
|
||||
|
||||
workflow = parser.parse_file(file_path)
|
||||
|
||||
# Print validation results
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="Workflow Validation")
|
||||
table.add_column("Property", style="cyan")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("Name", workflow.name)
|
||||
table.add_row("Description", getattr(workflow, 'description', 'N/A'))
|
||||
table.add_row("Steps", str(len(workflow.steps)))
|
||||
table.add_row("Variables", str(len(workflow.variables)))
|
||||
table.add_row("Planning", str(workflow.planning))
|
||||
table.add_row("Reasoning", str(workflow.reasoning))
|
||||
|
||||
rprint(table)
|
||||
self.print_status("✓ Workflow is valid!", "success")
|
||||
|
||||
# Show suggestions for canonical names
|
||||
if suggestions:
|
||||
rprint()
|
||||
rprint("[yellow]💡 Suggestions for canonical field names:[/yellow]")
|
||||
for suggestion in suggestions:
|
||||
rprint(f" [dim]•[/dim] {suggestion}")
|
||||
rprint()
|
||||
rprint("[dim]Note: Both old and new names work, but canonical names are recommended.[/dim]")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.print_status(f"✗ Validation failed: {e}", "error")
|
||||
return False
|
||||
|
||||
def _get_canonical_suggestions(self, data: Dict) -> List[str]:
|
||||
"""
|
||||
Check for non-canonical field names and return suggestions.
|
||||
|
||||
Canonical names (A-I-G-S mnemonic):
|
||||
- Agents (not roles)
|
||||
- Instructions (not backstory)
|
||||
- Goal (same)
|
||||
- Steps (not tasks)
|
||||
|
||||
Also:
|
||||
- name (not topic)
|
||||
- action (not description)
|
||||
|
||||
Args:
|
||||
data: Raw YAML data
|
||||
|
||||
Returns:
|
||||
List of suggestion strings
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
if not data:
|
||||
return suggestions
|
||||
|
||||
# Check top-level keys
|
||||
if 'roles' in data:
|
||||
suggestions.append("Use 'agents' instead of 'roles'")
|
||||
|
||||
if 'topic' in data and 'name' not in data:
|
||||
suggestions.append("Use 'name' instead of 'topic'")
|
||||
|
||||
# Check agent fields
|
||||
agents_data = data.get('agents', data.get('roles', {}))
|
||||
for agent_id, agent_config in agents_data.items():
|
||||
if isinstance(agent_config, dict):
|
||||
if 'backstory' in agent_config:
|
||||
suggestions.append(f"Agent '{agent_id}': Use 'instructions' instead of 'backstory'")
|
||||
|
||||
# Check nested tasks
|
||||
if 'tasks' in agent_config:
|
||||
suggestions.append(f"Agent '{agent_id}': Use 'steps' at top level instead of nested 'tasks'")
|
||||
|
||||
# Check step fields
|
||||
steps_data = data.get('steps', [])
|
||||
for i, step in enumerate(steps_data):
|
||||
if isinstance(step, dict):
|
||||
if 'description' in step and 'action' not in step:
|
||||
step_name = step.get('name', f'step {i+1}')
|
||||
suggestions.append(f"Step '{step_name}': Use 'action' instead of 'description'")
|
||||
|
||||
# Check parallel steps
|
||||
if 'parallel' in step:
|
||||
for j, parallel_step in enumerate(step['parallel']):
|
||||
if isinstance(parallel_step, dict):
|
||||
if 'description' in parallel_step and 'action' not in parallel_step:
|
||||
suggestions.append(f"Parallel step {j+1}: Use 'action' instead of 'description'")
|
||||
|
||||
return suggestions
|
||||
|
||||
def action_list(self, args: List[str], **kwargs) -> List[str]:
|
||||
"""
|
||||
List available workflows in .praison/workflows/ directory.
|
||||
|
||||
Returns:
|
||||
List of workflow file names
|
||||
"""
|
||||
workflows_dir = Path(".praison/workflows")
|
||||
|
||||
if not workflows_dir.exists():
|
||||
self.print_status("No .praison/workflows/ directory found", "warning")
|
||||
self.print_status("Create one with: mkdir -p .praison/workflows", "info")
|
||||
return []
|
||||
|
||||
# Find all YAML files
|
||||
yaml_files = list(workflows_dir.glob("*.yaml")) + list(workflows_dir.glob("*.yml"))
|
||||
|
||||
if not yaml_files:
|
||||
self.print_status("No workflow files found in .praison/workflows/", "warning")
|
||||
return []
|
||||
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="Available Workflows")
|
||||
table.add_column("File", style="cyan")
|
||||
table.add_column("Name", style="green")
|
||||
table.add_column("Steps", style="yellow")
|
||||
|
||||
parser = self._get_parser()
|
||||
workflow_names = []
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
try:
|
||||
if parser:
|
||||
workflow = parser.parse_file(yaml_file)
|
||||
table.add_row(
|
||||
yaml_file.name,
|
||||
workflow.name,
|
||||
str(len(workflow.steps))
|
||||
)
|
||||
else:
|
||||
table.add_row(yaml_file.name, "N/A", "N/A")
|
||||
workflow_names.append(yaml_file.name)
|
||||
except Exception as e:
|
||||
table.add_row(yaml_file.name, f"Error: {e}", "N/A")
|
||||
|
||||
rprint(table)
|
||||
return workflow_names
|
||||
|
||||
def action_create(self, args: List[str], **kwargs) -> Optional[str]:
|
||||
"""
|
||||
Create a workflow from a template.
|
||||
|
||||
Args:
|
||||
args: [--template, <name>, --output, <file.yaml>]
|
||||
|
||||
Returns:
|
||||
Path to created file
|
||||
"""
|
||||
# Parse arguments
|
||||
template_name = None
|
||||
output_file = None
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--template" and i + 1 < len(args):
|
||||
template_name = args[i + 1]
|
||||
i += 2
|
||||
elif args[i] == "--output" and i + 1 < len(args):
|
||||
output_file = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not template_name:
|
||||
self.print_status("Usage: praisonai workflow create --template <name>", "error")
|
||||
self.print_status(f"Available templates: {', '.join(self.TEMPLATES.keys())}", "info")
|
||||
return None
|
||||
|
||||
if template_name not in self.TEMPLATES:
|
||||
self.print_status(f"Unknown template: {template_name}", "error")
|
||||
self.print_status(f"Available templates: {', '.join(self.TEMPLATES.keys())}", "info")
|
||||
return None
|
||||
|
||||
# Default output file
|
||||
if not output_file:
|
||||
output_file = f"{template_name}_workflow.yaml"
|
||||
|
||||
# Check if file exists
|
||||
if os.path.exists(output_file):
|
||||
self.print_status(f"File already exists: {output_file}", "error")
|
||||
return None
|
||||
|
||||
# Write template
|
||||
template_content = self.TEMPLATES[template_name]
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(template_content)
|
||||
|
||||
self.print_status(f"✓ Created workflow: {output_file}", "success")
|
||||
self.print_status(f"Run with: praisonai workflow run {output_file}", "info")
|
||||
|
||||
return output_file
|
||||
|
||||
def action_auto(self, args: List[str], **kwargs) -> Optional[str]:
|
||||
"""
|
||||
Auto-generate a workflow from a topic description.
|
||||
|
||||
Args:
|
||||
args: ["topic description", --pattern, <pattern>, --output, <file.yaml>]
|
||||
|
||||
Returns:
|
||||
Path to created file
|
||||
"""
|
||||
# Parse arguments
|
||||
topic = None
|
||||
pattern = "sequential"
|
||||
output_file = None
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--pattern" and i + 1 < len(args):
|
||||
pattern = args[i + 1]
|
||||
i += 2
|
||||
elif args[i] == "--output" and i + 1 < len(args):
|
||||
output_file = args[i + 1]
|
||||
i += 2
|
||||
elif not args[i].startswith("--") and topic is None:
|
||||
topic = args[i]
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not topic:
|
||||
self.print_status('Usage: praisonai workflow auto "topic" --pattern <pattern>', "error")
|
||||
self.print_status("Patterns: sequential, routing, parallel", "info")
|
||||
return None
|
||||
|
||||
# Validate pattern
|
||||
valid_patterns = ["sequential", "routing", "parallel", "loop"]
|
||||
if pattern not in valid_patterns:
|
||||
self.print_status(f"Unknown pattern: {pattern}", "error")
|
||||
self.print_status(f"Valid patterns: {', '.join(valid_patterns)}", "info")
|
||||
return None
|
||||
|
||||
# Default output file
|
||||
if not output_file:
|
||||
# Create safe filename from topic
|
||||
safe_name = "".join(c if c.isalnum() else "_" for c in topic[:30]).lower()
|
||||
output_file = f"{safe_name}_workflow.yaml"
|
||||
|
||||
# Check if file exists
|
||||
if os.path.exists(output_file):
|
||||
self.print_status(f"File already exists: {output_file}", "error")
|
||||
return None
|
||||
|
||||
self.print_status(f"Generating {pattern} workflow for: {topic}", "info")
|
||||
|
||||
try:
|
||||
# Lazy import to avoid performance impact
|
||||
from praisonai.auto import WorkflowAutoGenerator
|
||||
|
||||
generator = WorkflowAutoGenerator(
|
||||
topic=topic,
|
||||
workflow_file=output_file
|
||||
)
|
||||
|
||||
result_path = generator.generate(pattern=pattern)
|
||||
|
||||
self.print_status(f"✓ Created workflow: {result_path}", "success")
|
||||
self.print_status(f"Run with: praisonai workflow run {output_file}", "info")
|
||||
|
||||
return result_path
|
||||
|
||||
except ImportError:
|
||||
self.print_status("Auto-generation requires instructor: pip install instructor", "error")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.print_status(f"Generation failed: {e}", "error")
|
||||
return None
|
||||
+1214
-18
File diff suppressed because it is too large.
Load diff
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user