Compare commits

...
38 Commits
Author SHA1 Message Date
Mervin Praison ab68b2d296 Release v2.3.32 2025-12-18 13:17:32 +00:00
Mervin Praison abcc5b3b04 Release v2.3.31 2025-12-18 12:57:05 +00:00
Mervin Praison e584f56099 Release v2.3.30 2025-12-18 11:59:22 +00:00
Mervin Praison b6b01af066 Release v2.3.33 2025-12-18 11:19:29 +00:00
Mervin Praison 9aae3eff86 Release v2.3.32 2025-12-18 10:48:09 +00:00
Mervin Praison f90582c66b Release v2.3.31 2025-12-18 10:27:03 +00:00
Mervin Praison 67a8d65d8e Release v2.3.30 2025-12-18 09:07:42 +00:00
Mervin Praison d60c31a86d Release v2.3.29 2025-12-18 02:47:04 +00:00
Mervin Praison fcb7b2a0d0 fix(tests): Mark test_phase2_refactor tests as @pytest.mark.real
These tests make real API calls and should be excluded from CI.
2025-12-17 13:31:01 +00:00
Mervin Praison 81ad7db679 Release v2.3.28 2025-12-17 13:22:14 +00:00
Mervin Praison 0482c3ebb0 chore: Update dependencies and versioning for PraisonAI
- Bump PraisonAI version to 2.3.28 across Dockerfiles and deployment scripts.
- Update praisonaiagents version to 0.1.7 in pyproject.toml.
- Enhance README with a structured Table of Contents and improved section headers for better navigation.
- Remove outdated README.md from praisonai-agents and link to the main README.
2025-12-17 13:20:29 +00:00
Mervin Praison 3107458016 docs: Enhance README with emoji for section headers for better readability 2025-12-17 13:09:58 +00:00
Mervin Praison 44c8ceebae fix: Fix 15 test failures across multiple test files
Test fixes:
1. Import Errors (5 tests):
   - test_llm_self_reflection_direct.py: Define calculator locally
   - test_self_reflection_comprehensive.py: Define duckduckgo_search locally
   - test_self_reflection_fix_simple.py: Define calculator locally
   - test_self_reflection_fix_verification.py: Define calculator locally
   - test_fix_comprehensive.py: Convert to proper pytest with skipif

2. Workflow Assertion (1 test):
   - test_workflow_agents.py: Check error in results array

3. Ollama Type Assertion (1 test):
   - test_ollama_sequential_fix.py: Accept both str and int types

4. Ollama Missing Fixture (3 tests):
   - test_ollama_fix.py: Add proper pytest functions with skipif
   - test_ollama_async_fix.py: Add proper pytest functions with skipif

5. Memory Stale Data (2 tests):
   - test_file_memory.py: Use unique user_id per test

6. Planning Async Mocks (3 tests):
   - test_planning.py: Use get_response_async instead of achat

Results: 921 passed, 9 failed (down from 12), 16 skipped
2025-12-17 13:05:12 +00:00
Mervin Praison 20c0b02528 fix(ci): Fix Docker publish workflow - trigger on release, update actions 2025-12-17 13:00:51 +00:00
Mervin Praison cfc886b591 docs: Update examples to use canonical field names (agents, instructions) 2025-12-17 12:34:32 +00:00
Mervin Praison 77ac4b7a09 fix: Use _yaml_action from YAML parser when executing workflow steps
The action/description field from YAML was being stored on the agent but not
used when executing the workflow. Now the _normalize_single_step method
checks for _yaml_action and uses it as the prompt.

This fixes the issue where workflow steps were not using the action as the prompt.
2025-12-17 12:33:07 +00:00
Mervin Praison 5897cedc45 docs: Update README examples to use canonical field names 2025-12-17 12:29:47 +00:00
Mervin Praison a4d1079813 feat: Add unified field names with A-I-G-S mnemonic
- Add canonical field name suggestions to CLI validate command
- Document A-I-G-S mnemonic (Agents, Instructions, Goal, Steps)
- Add field name reference table to README
- Add comprehensive tests for field name normalization
- Accept both old and new names: roles/agents, backstory/instructions, description/action, topic/name

Implements 'Accept liberally, suggest canonically' principle
2025-12-17 12:24:11 +00:00
Mervin Praison 4eed985c4d Release v2.3.27 2025-12-17 12:17:02 +00:00
Mervin Praison 0658491072 docs: Update README with workflow features and feature parity
- Add YAML workflow file documentation
- Add workflow auto-generation documentation
- Add complete workflow.yaml reference with all features
- Add feature parity note between agents.yaml and workflow.yaml
- Update workflow CLI options table
2025-12-17 11:52:33 +00:00
Mervin Praison 9f82368b4c feat: Add workflow auto-generation (--auto for workflow.yaml)
- Add WorkflowAutoGenerator class in auto.py with lazy loading
- Support sequential, routing, parallel, loop patterns
- Add 'praisonai workflow auto' CLI command
- Add action_auto to WorkflowHandler
- Add _auto_generate_workflow to main.py
- Uses instructor for structured output generation
- Lazy loading ensures no performance impact when not used

Usage:
  praisonai workflow auto "Research AI trends" --pattern parallel
  praisonai workflow auto "Build a chatbot" --pattern routing
2025-12-17 11:50:02 +00:00
Mervin Praison c267cdda87 Release v2.3.26 2025-12-17 11:47:04 +00:00
Mervin Praison b88bb04090 feat: Add feature parity between agents.yaml and workflow.yaml
Workflow.yaml now supports all agents.yaml features:

Agent-level features:
- framework (praisonai, crewai, autogen)
- process (sequential, hierarchical, workflow)
- manager_llm for hierarchical process
- function_calling_llm
- max_rpm (rate limiting)
- max_execution_time (timeout)
- reflect_llm, min_reflect, max_reflect
- system_template, prompt_template, response_template
- backstory (alias for instructions)

Step-level features:
- expected_output (like agents.yaml tasks)
- context (task dependencies)
- output_json, output_pydantic (structured output)
- create_directory
- callback
- async_execution

Added 14 new TDD tests for feature parity verification.
All 41 YAML parser tests pass.
2025-12-17 11:40:38 +00:00
Mervin Praison 709a35fbdc refactor: Remove duplicate code
- Remove duplicate _convert_roles_to_agents from agents_generator.py
  (YAMLWorkflowParser already handles this conversion)
- Refactor _create_workflow_from_template in main.py to use
  WorkflowHandler.TEMPLATES instead of duplicating templates

This reduces code duplication and ensures single source of truth for:
1. Roles-to-agents conversion logic (in yaml_parser.py)
2. Workflow templates (in workflow.py)
2025-12-17 11:16:51 +00:00
Mervin Praison 8b792424b6 feat: Extend agents.yaml with workflow patterns (Option D)
- Add workflow detection in agents_generator.py (process: workflow)
- Route to YAMLWorkflowParser when workflow mode detected
- Support both 'roles' and 'agents' keys for backward compatibility
- Map 'backstory' to 'instructions' automatically
- Add _convert_roles_to_agents method in YAMLWorkflowParser
- Add _run_yaml_workflow method in AgentsGenerator
- Add 7 new tests for roles-to-agents conversion
- Add extended agents.yaml example with all workflow patterns
- Update README with extended agents.yaml documentation

This allows users to use advanced workflow patterns (route, parallel,
loop, repeat) directly in agents.yaml while maintaining full backward
compatibility with existing agents.yaml files.

Usage:
  praisonai agents.yaml  # Works for both standard and workflow mode
2025-12-17 11:11:46 +00:00
Mervin Praison ef2a8cd658 feat(cli): Add YAML workflow CLI commands
- Add WorkflowHandler for 'praisonai workflow' commands
- Add 'praisonai workflow run <file.yaml>' to run YAML workflows
- Add 'praisonai workflow validate <file.yaml>' to validate YAML workflows
- Add 'praisonai workflow template <name>' to create from templates
- Templates: simple, routing, parallel, loop, evaluator-optimizer
- Support --var key=value for passing variables
- Add unit tests for WorkflowHandler (8 tests)
- Update main.py to support YAML workflow execution
2025-12-17 10:52:23 +00:00
Mervin Praison d41d2dcba2 docs: Add YAML workflow loading documentation to README 2025-12-17 10:43:20 +00:00
Mervin Praison 3bc1786b49 feat(workflows): Add YAMLWorkflowParser for YAML-based workflow definitions
- Add YAMLWorkflowParser class to parse YAML workflow files
- Support all workflow patterns: route(), parallel(), loop(), repeat()
- Support agent definitions with planning, reasoning, tools
- Support workflow config: planning, reasoning, memory_config, verbose
- Support variables, callbacks, and guardrails
- Add load_yaml(), execute_yaml(), aexecute_yaml() to WorkflowManager
- Add comprehensive test suite (20 tests)
- Add example YAML workflow files:
  - simple_workflow.yaml
  - routing_workflow.yaml
  - parallel_workflow.yaml
  - loop_workflow.yaml
  - evaluator_optimizer_workflow.yaml
  - complete_workflow.yaml
- Add example_usage.py demonstrating YAML workflow usage
2025-12-17 10:42:44 +00:00
Mervin Praison 2eec3e2359 fix: exclude real tests from all pattern and fix session test assertions
- test_runner.py: Add '-m not real' to exclude tests requiring API keys when running 'all' pattern
- test_cli_features.py: Fix test_session_get_actions to match actual SessionHandler.get_actions() implementation (list, show, resume, delete instead of start)
2025-12-17 08:33:24 +00:00
Mervin Praison 6168401a69 feat(docs): Simplify workflow documentation to focus on agent-first approach
- Replace WorkflowManager examples with Workflow class
- Emphasize Agent objects as workflow steps
- Streamline pattern examples (route, parallel, loop, repeat) with agent-based usage
- Remove complex WorkflowStep configuration examples
- Consolidate workflow features into clearer, more concise examples
- Remove workflow comparison table and memory config examples
- Focus on core workflow patterns with practical agent examples
2025-12-17 08:30:10 +00:00
Mervin Praison 473fda653d Release v2.3.25 2025-12-17 08:17:15 +00:00
Mervin Praison 30f1a440b6 Release v2.3.24 2025-12-17 08:04:24 +00:00
Mervin Praison f991f37cfe Release v2.3.23 2025-12-17 00:41:48 +00:00
Mervin Praison a43b3ac72c Release v2.3.22 2025-12-17 00:25:14 +00:00
Mervin Praison 06f82249e1 Release v2.3.21 2025-12-17 00:05:40 +00:00
Mervin Praison d3aa78241a Release v2.3.20 2025-12-16 23:46:47 +00:00
Mervin Praison dfb77c39f9 Release v2.3.19 2025-12-16 23:26:53 +00:00
Mervin Praison 7e8f9b0574 Release v2.3.18 2025-12-16 23:18:36 +00:00
91 changed files with 17541 additions and 2124 deletions

No files matched your search

+37 -28
View File
@@ -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
+729 -321
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -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.16" \
praisonai \
"praisonai[api]" \
gunicorn \
markdown
+1 -1
View File
@@ -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.16" \
"praisonai>=2.3.32" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -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.16" \
"praisonai>=2.3.32" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -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.16" \
"praisonai>=2.3.32" \
"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
+108
View File
@@ -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()
+4 -4
View File
@@ -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']}")
+117
View File
@@ -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
+152
View File
@@ -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}}"
+1
View File
@@ -87,3 +87,4 @@ graph.py
chroma_db/
.qodo
*_COMPARISON_ANALYSIS.md
*_REPORT.md
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
../../README.md
@@ -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,416 @@
"""
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",
)
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)
@@ -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,103 @@
#!/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."""
tools = [sample_tool]
results = {}
print('=' * 60)
print('PraisonAI Agents - Performance Benchmark')
print('=' * 60)
print(f'\nIterations: {ITERATIONS}')
print('Metric: Agent instantiation time (microseconds)\n')
# PraisonAI
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 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'), tools=tools)
)
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
# 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
if __name__ == '__main__':
run_benchmark()
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)
@@ -551,8 +551,9 @@ Your Goal: {self.goal}
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():
@@ -599,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
@@ -614,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."""
@@ -785,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.
@@ -793,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
@@ -828,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
)
@@ -1239,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}"
@@ -1539,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
@@ -1870,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
@@ -1879,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)
@@ -48,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
@@ -85,11 +126,23 @@ __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",
@@ -29,13 +29,166 @@ from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class WorkflowContext:
"""Context passed to step handlers. Contains all information about the current workflow state."""
input: str = "" # Original workflow input
previous_result: Optional[str] = None # Output from previous step
current_step: str = "" # Current step name
variables: Dict[str, Any] = field(default_factory=dict) # All workflow variables
@dataclass
class StepResult:
"""Result returned from step handlers."""
output: str = "" # Step output content
stop_workflow: bool = False # If True, stop the entire workflow early
variables: Dict[str, Any] = field(default_factory=dict) # Variables to add/update
# Aliases for backward compatibility
StepInput = WorkflowContext
StepOutput = StepResult
# =============================================================================
# Workflow Pattern Helpers
# =============================================================================
@dataclass
class Route:
"""
Decision-based branching. Routes to different steps based on previous output.
Usage:
workflow = Workflow(steps=[
decision_maker,
route({
"approve": [approve_step],
"reject": [reject_step],
"default": [fallback_step]
})
])
"""
routes: Dict[str, List] = field(default_factory=dict)
default: Optional[List] = None
def __init__(self, routes: Dict[str, List], default: Optional[List] = None):
self.routes = routes
self.default = default or routes.get("default", [])
@dataclass
class Parallel:
"""
Execute multiple steps concurrently and combine results.
Usage:
workflow = Workflow(steps=[
parallel([agent1, agent2, agent3]),
aggregator
])
"""
steps: List = field(default_factory=list)
def __init__(self, steps: List):
self.steps = steps
@dataclass
class Loop:
"""
Iterate over a list or CSV file, executing step for each item.
Usage:
# Loop over list variable
workflow = Workflow(
steps=[loop(processor, over="items")],
variables={"items": ["a", "b", "c"]}
)
# Loop over CSV file
workflow = Workflow(steps=[
loop(processor, from_csv="data.csv")
])
"""
step: Any = None
over: Optional[str] = None # Variable name containing list
from_csv: Optional[str] = None # CSV file path
from_file: Optional[str] = None # Text file path (one item per line)
var_name: str = "item" # Variable name for current item
def __init__(
self,
step: Any,
over: Optional[str] = None,
from_csv: Optional[str] = None,
from_file: Optional[str] = None,
var_name: str = "item"
):
self.step = step
self.over = over
self.from_csv = from_csv
self.from_file = from_file
self.var_name = var_name
@dataclass
class Repeat:
"""
Repeat a step until a condition is met (evaluator-optimizer pattern).
Usage:
workflow = Workflow(steps=[
repeat(
generator,
until=lambda ctx: "done" in ctx.previous_result.lower(),
max_iterations=5
)
])
"""
step: Any = None
until: Optional[Callable[[WorkflowContext], bool]] = None
max_iterations: int = 10
def __init__(
self,
step: Any,
until: Optional[Callable[[WorkflowContext], bool]] = None,
max_iterations: int = 10
):
self.step = step
self.until = until
self.max_iterations = max_iterations
# Convenience functions for cleaner API
def route(routes: Dict[str, List], default: Optional[List] = None) -> Route:
"""Create a routing decision point."""
return Route(routes=routes, default=default)
def parallel(steps: List) -> Parallel:
"""Execute steps in parallel."""
return Parallel(steps=steps)
def loop(step: Any, over: Optional[str] = None, from_csv: Optional[str] = None,
from_file: Optional[str] = None, var_name: str = "item") -> Loop:
"""Loop over items executing step for each."""
return Loop(step=step, over=over, from_csv=from_csv, from_file=from_file, var_name=var_name)
def repeat(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None,
max_iterations: int = 10) -> Repeat:
"""Repeat step until condition is met."""
return Repeat(step=step, until=until, max_iterations=max_iterations)
@dataclass
class WorkflowStep:
"""A single step in a workflow."""
name: str
description: str = ""
action: str = "" # The action/prompt to execute
condition: Optional[str] = None # Optional condition for execution
condition: Optional[str] = None # Optional condition for execution (string)
should_run: Optional[Callable[['WorkflowContext'], bool]] = None # Function to check if step should run
handler: Optional[Callable[['WorkflowContext'], 'StepResult']] = None # Custom function instead of agent
on_error: Literal["stop", "continue", "retry"] = "stop"
max_retries: int = 1
@@ -46,6 +199,7 @@ class WorkflowStep:
# Agent configuration fields
agent_config: Optional[Dict[str, Any]] = None # Per-step agent config {role, goal, backstory, llm}
agent: Optional[Any] = None # Direct agent instance for this step
tools: Optional[List[Any]] = None # Tools for this step
# Branching fields
@@ -56,12 +210,24 @@ class WorkflowStep:
loop_over: Optional[str] = None # Variable name to iterate over (e.g., "items")
loop_var: str = "item" # Variable name for current item in loop
# Backward compatibility aliases
@property
def evaluator(self):
return self.should_run
@property
def executor(self):
return self.handler
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"description": self.description,
"action": self.action,
"condition": self.condition,
"should_run": self.should_run is not None,
"handler": self.handler is not None,
"agent": self.agent is not None,
"on_error": self.on_error,
"max_retries": self.max_retries,
"context_from": self.context_from,
@@ -79,9 +245,9 @@ class WorkflowStep:
@dataclass
class Workflow:
"""A complete workflow with multiple steps."""
name: str
name: str = "Workflow"
description: str = ""
steps: List[WorkflowStep] = field(default_factory=list)
steps: List = field(default_factory=list) # Can be WorkflowStep, Agent, or function
variables: Dict[str, Any] = field(default_factory=dict)
file_path: Optional[str] = None
@@ -105,6 +271,516 @@ class Workflow:
"planning": self.planning,
"planning_llm": self.planning_llm
}
def run(
self,
input: str = "",
llm: Optional[str] = None,
verbose: bool = False
) -> Dict[str, Any]:
"""
Run the workflow with the given input.
This is the simplest way to execute a workflow:
```python
from praisonaiagents import Workflow, Agent
workflow = Workflow(
steps=[
Agent(name="Writer", role="Write content"),
my_validator_function,
Agent(name="Editor", role="Edit content"),
]
)
result = workflow.run("Write about AI")
```
Args:
input: The input text/prompt for the workflow
llm: LLM model to use (default: gpt-4o-mini)
verbose: Print step outputs
Returns:
Dict with 'output' (final result) and 'steps' (all step results)
"""
# Use default LLM if not specified
model = llm or self.default_llm or "gpt-4o-mini"
# Add input to variables
all_variables = {**self.variables, "input": input}
results = []
previous_output = None
# Process steps (may include Route, Parallel, Loop, Repeat)
i = 0
while i < len(self.steps):
step = self.steps[i]
# Handle special pattern types
if isinstance(step, Route):
# Decision-based routing
route_result = self._execute_route(
step, previous_output, input, all_variables, model, verbose
)
results.extend(route_result["steps"])
previous_output = route_result["output"]
all_variables.update(route_result.get("variables", {}))
i += 1
continue
elif isinstance(step, Parallel):
# Parallel execution
parallel_result = self._execute_parallel(
step, previous_output, input, all_variables, model, verbose
)
results.extend(parallel_result["steps"])
previous_output = parallel_result["output"]
all_variables.update(parallel_result.get("variables", {}))
i += 1
continue
elif isinstance(step, Loop):
# Loop over items
loop_result = self._execute_loop(
step, previous_output, input, all_variables, model, verbose
)
results.extend(loop_result["steps"])
previous_output = loop_result["output"]
all_variables.update(loop_result.get("variables", {}))
i += 1
continue
elif isinstance(step, Repeat):
# Repeat until condition
repeat_result = self._execute_repeat(
step, previous_output, input, all_variables, model, verbose
)
results.extend(repeat_result["steps"])
previous_output = repeat_result["output"]
all_variables.update(repeat_result.get("variables", {}))
i += 1
continue
# Normalize single step
step = self._normalize_single_step(step, i)
# Create context for handlers
context = WorkflowContext(
input=input,
previous_result=str(previous_output) if previous_output else None,
current_step=step.name,
variables=all_variables.copy()
)
# Check should_run condition
if step.should_run:
try:
if not step.should_run(context):
if verbose:
print(f"⏭️ Skipped: {step.name}")
i += 1
continue
except Exception as e:
logger.error(f"should_run failed for {step.name}: {e}")
# Execute step
output = None
stop = False
if step.handler:
# Custom handler function
try:
result = step.handler(context)
if isinstance(result, StepResult):
output = result.output
stop = result.stop_workflow
if result.variables:
all_variables.update(result.variables)
else:
output = str(result)
except Exception as e:
output = f"Error: {e}"
elif step.agent:
# Direct agent
try:
output = step.agent.chat(step.action or input)
except Exception as e:
output = f"Error: {e}"
elif step.action:
# Action with agent_config - create temporary agent
try:
from ..agent.agent import Agent
config = step.agent_config or self.default_agent_config or {}
temp_agent = Agent(
name=config.get("name", step.name),
role=config.get("role", "Assistant"),
goal=config.get("goal", "Complete the task"),
llm=config.get("llm", model),
verbose=verbose
)
# Substitute variables in action
action = step.action
for key, value in all_variables.items():
action = action.replace(f"{{{{{key}}}}}", str(value))
if previous_output:
action = action.replace("{{previous_output}}", str(previous_output))
output = temp_agent.chat(action)
except Exception as e:
output = f"Error: {e}"
# Store result
results.append({
"step": step.name,
"output": output
})
previous_output = output
if verbose:
print(f"✅ {step.name}: {str(output)}")
# Handle early stop
if stop:
if verbose:
print(f"🛑 Workflow stopped at: {step.name}")
break
# Store output in variables
var_name = step.output_variable or f"{step.name}_output"
all_variables[var_name] = output
i += 1
return {
"output": previous_output,
"steps": results,
"variables": all_variables
}
def _normalize_steps(self) -> List['WorkflowStep']:
"""Convert mixed steps (Agent, function, WorkflowStep) to WorkflowStep list."""
normalized = []
for i, step in enumerate(self.steps):
if isinstance(step, WorkflowStep):
normalized.append(step)
elif callable(step):
# It's a function - wrap as handler
normalized.append(WorkflowStep(
name=getattr(step, '__name__', f'step_{i+1}'),
handler=step
))
elif hasattr(step, 'chat'):
# It's an Agent - wrap with agent reference
normalized.append(WorkflowStep(
name=getattr(step, 'name', f'agent_{i+1}'),
agent=step,
action="{{input}}"
))
else:
# Unknown type - try to use as string action
normalized.append(WorkflowStep(
name=f'step_{i+1}',
action=str(step)
))
return normalized
def _normalize_single_step(self, step: Any, index: int) -> 'WorkflowStep':
"""Normalize a single step to WorkflowStep."""
if isinstance(step, WorkflowStep):
return step
elif callable(step):
return WorkflowStep(
name=getattr(step, '__name__', f'step_{index+1}'),
handler=step
)
elif hasattr(step, 'chat'):
return WorkflowStep(
name=getattr(step, 'name', f'agent_{index+1}'),
agent=step,
action="{{input}}"
)
else:
return WorkflowStep(
name=f'step_{index+1}',
action=str(step)
)
def _execute_single_step_internal(
self,
step: Any,
previous_output: Optional[str],
input: str,
all_variables: Dict[str, Any],
model: str,
verbose: bool,
index: int = 0
) -> Dict[str, Any]:
"""Execute a single step and return result."""
normalized = self._normalize_single_step(step, index)
context = WorkflowContext(
input=input,
previous_result=str(previous_output) if previous_output else None,
current_step=normalized.name,
variables=all_variables.copy()
)
output = None
stop = False
if normalized.handler:
try:
result = normalized.handler(context)
if isinstance(result, StepResult):
output = result.output
stop = result.stop_workflow
if result.variables:
all_variables.update(result.variables)
else:
output = str(result)
except Exception as e:
output = f"Error: {e}"
elif normalized.agent:
try:
output = normalized.agent.chat(normalized.action or input)
except Exception as e:
output = f"Error: {e}"
elif normalized.action:
try:
from ..agent.agent import Agent
config = normalized.agent_config or self.default_agent_config or {}
temp_agent = Agent(
name=config.get("name", normalized.name),
role=config.get("role", "Assistant"),
goal=config.get("goal", "Complete the task"),
llm=config.get("llm", model),
verbose=verbose
)
action = normalized.action
for key, value in all_variables.items():
action = action.replace(f"{{{{{key}}}}}", str(value))
if previous_output:
action = action.replace("{{previous_output}}", str(previous_output))
output = temp_agent.chat(action)
except Exception as e:
output = f"Error: {e}"
if verbose:
print(f"✅ {normalized.name}: {str(output)}")
return {
"step": normalized.name,
"output": output,
"stop": stop,
"variables": all_variables
}
def _execute_route(
self,
route_step: Route,
previous_output: Optional[str],
input: str,
all_variables: Dict[str, Any],
model: str,
verbose: bool
) -> Dict[str, Any]:
"""Execute routing based on previous output."""
results = []
output = previous_output
# Find matching route
matched_route = None
prev_lower = str(previous_output).lower() if previous_output else ""
for key in route_step.routes:
if key.lower() in prev_lower or key == "default":
if key != "default":
matched_route = route_step.routes[key]
break
if matched_route is None:
matched_route = route_step.default or []
if verbose:
route_name = next((k for k in route_step.routes if route_step.routes[k] == matched_route), "default")
print(f"🔀 Routing to: {route_name}")
# Execute matched route steps
for idx, step in enumerate(matched_route):
step_result = self._execute_single_step_internal(
step, output, input, all_variables, model, verbose, idx
)
results.append({"step": step_result["step"], "output": step_result["output"]})
output = step_result["output"]
all_variables.update(step_result.get("variables", {}))
if step_result.get("stop"):
break
return {"steps": results, "output": output, "variables": all_variables}
def _execute_parallel(
self,
parallel_step: Parallel,
previous_output: Optional[str],
input: str,
all_variables: Dict[str, Any],
model: str,
verbose: bool
) -> Dict[str, Any]:
"""Execute steps in parallel (simulated with sequential for now)."""
import concurrent.futures
results = []
outputs = []
if verbose:
print(f"⚡ Running {len(parallel_step.steps)} steps in parallel...")
# Use ThreadPoolExecutor for parallel execution
with concurrent.futures.ThreadPoolExecutor(max_workers=len(parallel_step.steps)) as executor:
futures = []
for idx, step in enumerate(parallel_step.steps):
future = executor.submit(
self._execute_single_step_internal,
step, previous_output, input, all_variables.copy(), model, False, idx
)
futures.append((idx, future))
for idx, future in futures:
try:
step_result = future.result()
results.append({"step": step_result["step"], "output": step_result["output"]})
outputs.append(step_result["output"])
except Exception as e:
results.append({"step": f"parallel_{idx}", "output": f"Error: {e}"})
outputs.append(f"Error: {e}")
# Combine outputs
combined_output = "\n---\n".join(str(o) for o in outputs)
all_variables["parallel_outputs"] = outputs
if verbose:
print(f"✅ Parallel complete: {len(outputs)} results")
return {"steps": results, "output": combined_output, "variables": all_variables}
def _execute_loop(
self,
loop_step: Loop,
previous_output: Optional[str],
input: str,
all_variables: Dict[str, Any],
model: str,
verbose: bool
) -> Dict[str, Any]:
"""Execute step for each item in loop."""
import csv
results = []
outputs = []
items = []
# Get items from variable, CSV, or file
if loop_step.over:
items = all_variables.get(loop_step.over, [])
if isinstance(items, str):
items = [items]
elif loop_step.from_csv:
try:
with open(loop_step.from_csv, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
items = list(reader)
except Exception as e:
logger.error(f"Failed to read CSV {loop_step.from_csv}: {e}")
items = []
elif loop_step.from_file:
try:
with open(loop_step.from_file, 'r', encoding='utf-8') as f:
items = [line.strip() for line in f if line.strip()]
except Exception as e:
logger.error(f"Failed to read file {loop_step.from_file}: {e}")
items = []
if verbose:
print(f"🔁 Looping over {len(items)} items...")
for idx, item in enumerate(items):
# Add current item to variables
loop_vars = all_variables.copy()
loop_vars[loop_step.var_name] = item
loop_vars["loop_index"] = idx
step_result = self._execute_single_step_internal(
loop_step.step, previous_output, input, loop_vars, model, verbose, idx
)
results.append({"step": f"{step_result['step']}_{idx}", "output": step_result["output"]})
outputs.append(step_result["output"])
previous_output = step_result["output"]
all_variables["loop_outputs"] = outputs
combined_output = "\n".join(str(o) for o in outputs) if outputs else ""
return {"steps": results, "output": combined_output, "variables": all_variables}
def _execute_repeat(
self,
repeat_step: Repeat,
previous_output: Optional[str],
input: str,
all_variables: Dict[str, Any],
model: str,
verbose: bool
) -> Dict[str, Any]:
"""Repeat step until condition is met."""
results = []
output = previous_output
if verbose:
print(f"🔄 Repeating up to {repeat_step.max_iterations} times...")
for iteration in range(repeat_step.max_iterations):
step_result = self._execute_single_step_internal(
repeat_step.step, output, input, all_variables, model, verbose, iteration
)
results.append({"step": f"{step_result['step']}_{iteration}", "output": step_result["output"]})
output = step_result["output"]
all_variables.update(step_result.get("variables", {}))
# Check until condition
if repeat_step.until:
context = WorkflowContext(
input=input,
previous_result=str(output) if output else None,
current_step=f"repeat_{iteration}",
variables=all_variables.copy()
)
try:
if repeat_step.until(context):
if verbose:
print(f"✅ Repeat condition met at iteration {iteration + 1}")
break
except Exception as e:
logger.error(f"Repeat until condition failed: {e}")
if step_result.get("stop"):
break
all_variables["repeat_iterations"] = iteration + 1
return {"steps": results, "output": output, "variables": all_variables}
def start(self, input: str = "", **kwargs) -> Dict[str, Any]:
"""Alias for run() for consistency with PraisonAIAgents."""
return self.run(input, **kwargs)
# Alias: Pipeline = Workflow (they are the same concept)
Pipeline = Workflow
class WorkflowManager:
@@ -717,6 +1393,16 @@ class WorkflowManager:
on_result=on_result
)
# Handle skipped steps
if step_result.get("skipped"):
results.append({
"step": step.name,
"status": "skipped",
"output": None
})
current_step_idx += 1
continue
results.append({
"step": step.name,
"status": "success" if step_result["success"] else "failed",
@@ -724,6 +1410,11 @@ class WorkflowManager:
"error": step_result.get("error")
})
# Handle early stop
if step_result.get("stop"):
self._log(f"Workflow stopped early at step '{step.name}'")
break
# Save checkpoint after each step if enabled
if checkpoint:
self._save_checkpoint(
@@ -784,9 +1475,64 @@ class WorkflowManager:
planning: bool = False,
verbose: int = 0,
on_step: Optional[Callable[[WorkflowStep, int], None]] = None,
on_result: Optional[Callable[[WorkflowStep, str], None]] = None
on_result: Optional[Callable[[WorkflowStep, str], None]] = None,
original_input: str = ""
) -> Dict[str, Any]:
"""Execute a single workflow step."""
# Get previous step output
previous_output = results[-1].get("output") if results else None
# Create context for step handlers
context = WorkflowContext(
input=original_input,
previous_result=str(previous_output) if previous_output else None,
current_step=step.name,
variables=all_variables.copy()
)
# Check should_run condition if provided
if step.should_run:
try:
if not step.should_run(context):
return {
"success": True,
"output": None,
"skipped": True,
"stop": False
}
except Exception as e:
self._log(f"should_run check for step '{step.name}' failed: {e}")
# If step has a custom handler function
if step.handler:
try:
result = step.handler(context)
# Handle StepResult
if isinstance(result, StepResult):
# Update variables from result
if result.variables:
all_variables.update(result.variables)
return {
"success": True,
"output": result.output,
"stop": result.stop_workflow, # Early termination flag
"error": None
}
else:
return {
"success": True,
"output": str(result),
"stop": False,
"error": None
}
except Exception as e:
return {
"success": False,
"output": None,
"stop": False,
"error": str(e)
}
# Build context from previous steps
context = self._build_step_context(step, step_idx, results, all_variables)
@@ -800,15 +1546,20 @@ class WorkflowManager:
# Get or create executor for this step
step_executor = executor
if step_executor is None:
# Create agent for this step
step_agent = self._create_step_agent(
step=step,
default_agent=default_agent,
default_llm=default_llm,
memory=memory,
planning=planning,
verbose=verbose
)
# Use step's direct agent if provided
step_agent = step.agent
# Otherwise create agent from config
if step_agent is None:
step_agent = self._create_step_agent(
step=step,
default_agent=default_agent,
default_llm=default_llm,
memory=memory,
planning=planning,
verbose=verbose
)
if step_agent:
if planning and hasattr(step_agent, 'start'):
def agent_executor(prompt, agent=step_agent):
@@ -823,6 +1574,7 @@ class WorkflowManager:
return {
"success": False,
"output": None,
"stop": False,
"error": f"No executor available for step '{step.name}'"
}
@@ -853,6 +1605,7 @@ class WorkflowManager:
return {
"success": step_success,
"output": output,
"stop": False, # Normal execution doesn't request stop
"error": error
}
@@ -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():
@@ -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
+2 -1
View File
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.194"
version = "0.1.13"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{ name="Mervin Praison" }
+13 -6
View File
@@ -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}")
+27 -5
View File
@@ -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
+6 -3
View File
@@ -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
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.194"
version = "0.1.13"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+729 -321
View File
File diff suppressed because it is too large. Load diff
+41 -36
View File
@@ -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}}"
+106
View File
@@ -0,0 +1,106 @@
{
"id": "e022a3a1",
"name": "Create Movie Script About Cat in Mars",
"nodes": [
{
"id": "trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {}
},
{
"id": "agent_0",
"name": "Researcher",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
450,
300
],
"parameters": {
"command": "praisonai \"Research about Mars environment and cat behavior for the movie concept\"",
"executeOnce": true
}
},
{
"id": "agent_1",
"name": "Narrative Designer",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
650,
300
],
"parameters": {
"command": "praisonai \"Create story concept based on research: {{previous_output}}\"",
"executeOnce": true
}
},
{
"id": "agent_2",
"name": "Scriptwriter",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
850,
300
],
"parameters": {
"command": "praisonai \"Write the final movie script based on the story concept: {{previous_output}}\"",
"executeOnce": true
}
}
],
"connections": {
"Manual Trigger": {
"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": []
}
+2 -2
View File
@@ -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.16.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.16.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.32.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.32.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+67 -1
View File
@@ -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.
+222 -1
View File
@@ -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
+539
View File
@@ -0,0 +1,539 @@
"""
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 = True):
"""
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://localhost:8000"
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)
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")
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
}
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 convert_yaml_to_n8n(self, yaml_path: str) -> Dict[str, Any]:
"""
Convert a PraisonAI YAML workflow to n8n JSON format.
Args:
yaml_path: Path to the agents.yaml file
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', '')
# 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 manual trigger node
trigger_node = self._create_trigger_node()
nodes.append(trigger_node)
# Track previous node for connections
prev_node_name = trigger_node["name"]
# Process each step
x_position = 450 # Start position after trigger
for i, step in enumerate(steps):
if isinstance(step, dict):
if 'parallel' in step:
# Handle parallel steps
parallel_nodes, parallel_connections = self._create_parallel_nodes(
step['parallel'], x_position, agents
)
nodes.extend(parallel_nodes)
connections.update(parallel_connections)
# Connect previous node to first parallel node
if prev_node_name not in connections:
connections[prev_node_name] = {"main": [[]]}
for pnode in parallel_nodes:
connections[prev_node_name]["main"][0].append({
"node": pnode["name"],
"type": "main",
"index": 0
})
x_position += 200 * len(parallel_nodes)
prev_node_name = parallel_nodes[-1]["name"] if parallel_nodes else prev_node_name
elif 'route' in step:
# Handle route/decision steps
route_node = self._create_route_node(step, x_position, i)
nodes.append(route_node)
# Connect previous node
if prev_node_name not in connections:
connections[prev_node_name] = {"main": [[]]}
connections[prev_node_name]["main"][0].append({
"node": route_node["name"],
"type": "main",
"index": 0
})
prev_node_name = route_node["name"]
x_position += 200
else:
# Regular agent step
agent_id = step.get('agent', '')
action = step.get('action', step.get('description', ''))
agent_config = agents.get(agent_id, {})
agent_node = self._create_agent_node(
agent_id=agent_id,
agent_config=agent_config,
action=action,
position=[x_position, 300],
index=i
)
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 += 200
# 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) -> Dict[str, Any]:
"""Create a manual trigger node."""
return {
"id": "trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [250, 300],
"parameters": {}
}
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)
@@ -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
+392 -1
View File
@@ -210,6 +210,10 @@ class PraisonAI:
version_string = f"PraisonAI version {__version__}"
self.framework = args.framework or self.framework
# Update config_list model if --model flag is provided
if getattr(args, 'model', None):
self.config_list[0]['model'] = args.model
# Check for piped input from stdin
stdin_input = self.read_stdin_if_available()
@@ -495,6 +499,31 @@ class PraisonAI:
print(result)
return result
else:
# n8n Integration - Export workflow to n8n and open in browser
if getattr(args, 'n8n', False):
from .features.n8n import N8nHandler
n8n_handler = N8nHandler(
verbose=getattr(args, 'verbose', False),
n8n_url=getattr(args, 'n8n_url', 'http://localhost:5678')
)
result = n8n_handler.execute(self.agent_file)
return result
# Flow Display - Show flow diagram WITHOUT executing (--flow-display flag)
if getattr(args, 'flow_display', False):
from .features.flow_display import FlowDisplayHandler
flow_handler = FlowDisplayHandler(verbose=getattr(args, 'verbose', False))
flow_handler.display_flow_diagram(self.agent_file)
return # Exit without executing
# Always show flow diagram before execution (default behavior)
try:
from .features.flow_display import FlowDisplayHandler
flow_handler = FlowDisplayHandler(verbose=getattr(args, 'verbose', False))
flow_handler.display_flow_diagram(self.agent_file, show_footer=False)
except Exception:
pass # Continue even if flow display fails
agents_generator = AgentsGenerator(
self.agent_file,
self.framework,
@@ -650,6 +679,10 @@ class PraisonAI:
# Flow Display - visual workflow
parser.add_argument("--flow-display", action="store_true", help="Enable visual workflow tracking")
# n8n Integration - export workflow to n8n
parser.add_argument("--n8n", action="store_true", help="Export workflow to n8n and open in browser")
parser.add_argument("--n8n-url", type=str, default="http://localhost:5678", help="n8n instance URL (default: http://localhost:5678)")
# If we're in a test environment, parse with empty args to avoid pytest interference
if in_test_env:
args, unknown_args = parser.parse_known_args([])
@@ -1485,6 +1518,12 @@ class PraisonAI:
return
workflow_name = action_args[0]
# Check if it's a YAML file
if workflow_name.endswith(('.yaml', '.yml')) and os.path.exists(workflow_name):
# Use new YAML workflow parser
self._run_yaml_workflow(workflow_name, action_args, variables, args)
return
# Use global flags (--llm, --tools, --planning, --memory, --save, --verbose)
workflow_llm = getattr(args, 'llm', None) if args else None
workflow_tools_str = getattr(args, 'tools', None) if args else None
@@ -1607,22 +1646,55 @@ class PraisonAI:
print(f"[green]✅ Workflow created: {workflow_name}[/green]")
print(f"[cyan]Edit the workflow in .praison/workflows/{workflow_name}.md[/cyan]")
elif action == 'validate':
# Validate a YAML workflow file
if not action_args:
print("[red]ERROR: YAML file required. Usage: praisonai workflow validate <file.yaml>[/red]")
return
yaml_file = action_args[0]
if not yaml_file.endswith(('.yaml', '.yml')):
print("[red]ERROR: File must be a YAML file (.yaml or .yml)[/red]")
return
self._validate_yaml_workflow(yaml_file)
elif action == 'template':
# Create from template
template_name = action_args[0] if action_args else None
output_file = None
for i, arg in enumerate(action_args):
if arg == '--output' and i + 1 < len(action_args):
output_file = action_args[i + 1]
self._create_workflow_from_template(template_name, output_file)
elif action == 'auto':
# Auto-generate workflow from topic
self._auto_generate_workflow(action_args)
elif action == 'help' or action == '--help':
print("[bold]Workflow Commands:[/bold]")
print(" praisonai workflow list - List available workflows")
print(" praisonai workflow run <name> - Execute a workflow")
print(" praisonai workflow run <file.yaml> - Execute a YAML workflow")
print(" praisonai workflow show <name> - Show workflow details")
print(" praisonai workflow create <name> - Create a new workflow")
print(" praisonai workflow validate <file.yaml> - Validate a YAML workflow")
print(" praisonai workflow template <name> - Create from template")
print(' praisonai workflow auto "topic" - Auto-generate workflow')
print("\n[bold]Templates:[/bold]")
print(" simple, routing, parallel, loop, evaluator-optimizer")
print("\n[bold]Options (uses global flags):[/bold]")
print(" --workflow-var key=value - Set workflow variable (can be repeated)")
print(" --var key=value - Set variable for YAML workflows")
print(" --llm <model> - LLM model (e.g., openai/gpt-4o-mini)")
print(" --tools <tools> - Tools (comma-separated, e.g., tavily)")
print(" --planning - Enable planning mode")
print(" --memory - Enable memory")
print(" --verbose - Enable verbose output")
print(" --save - Save output to file")
print("\n[bold]Example:[/bold]")
print("\n[bold]Examples:[/bold]")
print(" praisonai workflow run 'Research Blog' --tools tavily --save")
print(" praisonai workflow run research.yaml --var topic='AI trends'")
print(" praisonai workflow template routing --output my_workflow.yaml")
else:
print(f"[red]Unknown workflow action: {action}[/red]")
print("Use 'praisonai workflow help' for available commands")
@@ -1633,6 +1705,325 @@ class PraisonAI:
except Exception as e:
print(f"[red]ERROR: Workflow command failed: {e}[/red]")
def _run_yaml_workflow(self, yaml_file: str, action_args: list, variables: dict = None, args=None):
"""
Run a YAML workflow file using the new YAMLWorkflowParser.
Args:
yaml_file: Path to the YAML workflow file
action_args: Additional arguments
variables: Workflow variables
args: Parsed command line arguments
"""
try:
from praisonaiagents.workflows import WorkflowManager
from rich import print
from rich.table import Table
from rich.console import Console
console = Console()
manager = WorkflowManager()
# Parse --var arguments from action_args
parsed_vars = variables or {}
i = 0
while i < len(action_args):
if action_args[i] == "--var" and i + 1 < len(action_args):
var_str = action_args[i + 1]
if "=" in var_str:
key, value = var_str.split("=", 1)
parsed_vars[key.strip()] = value.strip()
i += 2
else:
i += 1
# Get verbose flag
verbose = '--verbose' in action_args or '-v' in action_args or (getattr(args, 'verbose', False) if args else False)
print(f"[bold cyan]Running YAML workflow: {yaml_file}[/bold cyan]")
if parsed_vars:
print(f"[cyan]Variables: {parsed_vars}[/cyan]")
# Load and execute the YAML workflow
workflow = manager.load_yaml(yaml_file)
# Show workflow info
table = Table(title=f"Workflow: {workflow.name}")
table.add_column("Property", style="cyan")
table.add_column("Value", style="green")
table.add_row("Steps", str(len(workflow.steps)))
table.add_row("Planning", str(workflow.planning))
table.add_row("Reasoning", str(workflow.reasoning))
console.print(table)
# Merge variables
if parsed_vars:
workflow.variables.update(parsed_vars)
# Set verbose
if verbose:
workflow.verbose = True
# Execute
print("\n[bold]Executing workflow...[/bold]\n")
result = workflow.start("")
if result.get("status") == "completed":
print("\n[green]✅ Workflow completed successfully![/green]")
# Show output
if result.get("output"):
print("\n[bold]Output:[/bold]")
output = result["output"]
if len(output) > 2000:
print(output[:2000] + "...")
else:
print(output)
else:
print(f"\n[red]❌ Workflow failed: {result.get('error', 'Unknown error')}[/red]")
except FileNotFoundError:
print(f"[red]ERROR: YAML file not found: {yaml_file}[/red]")
except Exception as e:
print(f"[red]ERROR: YAML workflow failed: {e}[/red]")
import traceback
traceback.print_exc()
def _validate_yaml_workflow(self, yaml_file: str):
"""
Validate a YAML workflow file.
Args:
yaml_file: Path to the YAML workflow file
"""
try:
from praisonaiagents.workflows import YAMLWorkflowParser
from rich import print
from rich.table import Table
from rich.console import Console
import yaml
console = Console()
if not os.path.exists(yaml_file):
print(f"[red]ERROR: File not found: {yaml_file}[/red]")
return
print(f"[cyan]Validating: {yaml_file}[/cyan]")
# Load raw YAML to check for non-canonical names
with open(yaml_file, '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)
parser = YAMLWorkflowParser()
workflow = parser.parse_file(yaml_file)
# Show validation results
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))
console.print(table)
print("[green]✓ Workflow is valid![/green]")
# Show suggestions for canonical names
if suggestions:
print()
print("[yellow]💡 Suggestions for canonical field names:[/yellow]")
for suggestion in suggestions:
print(f" [dim]•[/dim] {suggestion}")
print()
print("[dim]Note: Both old and new names work, but canonical names are recommended.[/dim]")
except Exception as e:
print(f"[red]✗ Validation failed: {e}[/red]")
def _get_canonical_suggestions(self, data: dict) -> list:
"""
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 _create_workflow_from_template(self, template_name: str = None, output_file: str = None):
"""
Create a workflow from a template.
Uses templates from WorkflowHandler to avoid duplication.
Args:
template_name: Name of the template
output_file: Output file path
"""
from rich import print
# Use templates from WorkflowHandler to avoid duplication
try:
from .features.workflow import WorkflowHandler
templates = WorkflowHandler.TEMPLATES
except ImportError:
print("[red]ERROR: WorkflowHandler not available.[/red]")
return
if not template_name:
print("[red]ERROR: Template name required.[/red]")
print(f"[cyan]Available templates: {', '.join(templates.keys())}[/cyan]")
return
if template_name not in templates:
print(f"[red]ERROR: Unknown template: {template_name}[/red]")
print(f"[cyan]Available templates: {', '.join(templates.keys())}[/cyan]")
return
# Default output file
if not output_file:
output_file = f"{template_name}_workflow.yaml"
# Check if file exists
if os.path.exists(output_file):
print(f"[red]ERROR: File already exists: {output_file}[/red]")
return
# Write template
with open(output_file, 'w') as f:
f.write(templates[template_name])
print(f"[green]✓ Created workflow: {output_file}[/green]")
print(f"[cyan]Run with: praisonai workflow run {output_file}[/cyan]")
def _auto_generate_workflow(self, action_args: list):
"""
Auto-generate a workflow from a topic description.
Args:
action_args: ["topic description", --pattern, <pattern>, --output, <file.yaml>]
"""
from rich import print
# Parse arguments
topic = None
pattern = "sequential"
output_file = None
i = 0
while i < len(action_args):
if action_args[i] == "--pattern" and i + 1 < len(action_args):
pattern = action_args[i + 1]
i += 2
elif action_args[i] == "--output" and i + 1 < len(action_args):
output_file = action_args[i + 1]
i += 2
elif not action_args[i].startswith("--") and topic is None:
topic = action_args[i]
i += 1
else:
i += 1
if not topic:
print('[red]Usage: praisonai workflow auto "topic" --pattern <pattern>[/red]')
print("[cyan]Patterns: sequential, routing, parallel[/cyan]")
return
# Validate pattern
valid_patterns = ["sequential", "routing", "parallel", "loop"]
if pattern not in valid_patterns:
print(f"[red]Unknown pattern: {pattern}[/red]")
print(f"[cyan]Valid patterns: {', '.join(valid_patterns)}[/cyan]")
return
# Default output file
if not output_file:
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):
print(f"[red]ERROR: File already exists: {output_file}[/red]")
return
print(f"[cyan]Generating {pattern} workflow for: {topic}[/cyan]")
try:
from praisonai.auto import WorkflowAutoGenerator
generator = WorkflowAutoGenerator(
topic=topic,
workflow_file=output_file
)
result_path = generator.generate(pattern=pattern)
print(f"[green]✓ Created workflow: {result_path}[/green]")
print(f"[cyan]Run with: praisonai workflow run {output_file}[/cyan]")
except ImportError:
print("[red]Auto-generation requires instructor: pip install instructor[/red]")
except Exception as e:
print(f"[red]Generation failed: {e}[/red]")
def handle_hooks_command(self, action: str):
"""
Handle hooks subcommand actions.
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.16 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.32 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.16"
__version__ = "2.3.32"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.0.194",
"praisonaiagents>=0.1.13",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+239
View File
@@ -0,0 +1,239 @@
"""
Test-Driven Development tests for n8n integration.
Tests for converting PraisonAI agents.yaml to n8n workflow JSON format.
"""
import pytest
import json
import os
import tempfile
from pathlib import Path
class TestN8nConverter:
"""Tests for YAML to n8n JSON conversion."""
@pytest.fixture
def sample_agents_yaml(self):
"""Sample agents.yaml content for testing."""
return """
name: Test Workflow
description: A test workflow for n8n integration
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
instructions: Research and gather information
writer:
name: Writer
role: Content Writer
goal: Write content
instructions: Write based on research
steps:
- agent: researcher
action: "Research the topic"
- agent: writer
action: "Write content based on research"
"""
@pytest.fixture
def sample_yaml_file(self, sample_agents_yaml):
"""Create a temporary YAML file for testing."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(sample_agents_yaml)
f.flush()
yield f.name
os.unlink(f.name)
def test_n8n_handler_import(self):
"""Test that N8nHandler can be imported."""
from praisonai.cli.features.n8n import N8nHandler
assert N8nHandler is not None
def test_n8n_handler_initialization(self):
"""Test N8nHandler initialization."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler(verbose=True)
assert handler.feature_name == "n8n"
assert handler.verbose is True
def test_convert_yaml_to_n8n_returns_dict(self, sample_yaml_file):
"""Test that convert_yaml_to_n8n returns a dictionary."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
assert isinstance(result, dict)
def test_n8n_workflow_has_required_keys(self, sample_yaml_file):
"""Test that n8n workflow JSON has required keys."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
assert "name" in result
assert "nodes" in result
assert "connections" in result
def test_n8n_workflow_has_trigger_node(self, sample_yaml_file):
"""Test that n8n workflow has a manual trigger node."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
nodes = result["nodes"]
trigger_nodes = [n for n in nodes if "trigger" in n.get("type", "").lower() or "trigger" in n.get("name", "").lower()]
assert len(trigger_nodes) >= 1
def test_n8n_workflow_has_agent_nodes(self, sample_yaml_file):
"""Test that n8n workflow has agent nodes (Execute Command or HTTP Request)."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
nodes = result["nodes"]
# Agent nodes can be either executeCommand or httpRequest
agent_nodes = [n for n in nodes if "executeCommand" in n.get("type", "") or "httpRequest" in n.get("type", "")]
# Should have at least 2 agent nodes (one per agent step)
assert len(agent_nodes) >= 2
def test_n8n_workflow_nodes_are_connected(self, sample_yaml_file):
"""Test that nodes are properly connected."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
connections = result["connections"]
assert len(connections) > 0
def test_n8n_workflow_name_from_yaml(self, sample_yaml_file):
"""Test that workflow name is taken from YAML."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
assert result["name"] == "Test Workflow"
def test_generate_n8n_url(self, sample_yaml_file):
"""Test URL generation for n8n."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
workflow_json = handler.convert_yaml_to_n8n(sample_yaml_file)
url = handler.generate_n8n_url(workflow_json, "http://localhost:5678")
assert url.startswith("http://localhost:5678")
assert "workflow" in url.lower()
def test_node_positions_are_sequential(self, sample_yaml_file):
"""Test that node positions are laid out sequentially."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(sample_yaml_file)
nodes = result["nodes"]
positions = [n.get("position", [0, 0]) for n in nodes]
# X positions should increase for sequential workflow
x_positions = [p[0] for p in positions]
assert x_positions == sorted(x_positions), "Nodes should be positioned left to right"
class TestN8nCLIIntegration:
"""Tests for CLI integration of n8n feature."""
def test_n8n_flag_in_argparser(self):
"""Test that --n8n flag is recognized by argparser."""
import argparse
# This will be tested after we add the flag to main.py
pass
def test_execute_method_exists(self):
"""Test that execute method exists."""
from praisonai.cli.features.n8n import N8nHandler
handler = N8nHandler()
assert hasattr(handler, 'execute')
assert callable(handler.execute)
class TestN8nWorkflowPatterns:
"""Tests for different workflow patterns."""
@pytest.fixture
def parallel_workflow_yaml(self):
"""YAML with parallel steps."""
return """
name: Parallel Workflow
agents:
agent1:
name: Agent1
role: Role1
goal: Goal1
agent2:
name: Agent2
role: Role2
goal: Goal2
steps:
- parallel:
- agent: agent1
action: "Do task 1"
- agent: agent2
action: "Do task 2"
"""
@pytest.fixture
def route_workflow_yaml(self):
"""YAML with route/decision steps."""
return """
name: Route Workflow
agents:
classifier:
name: Classifier
role: Classifier
goal: Classify input
handler_a:
name: HandlerA
role: Handler
goal: Handle type A
handler_b:
name: HandlerB
role: Handler
goal: Handle type B
steps:
- agent: classifier
action: "Classify the input"
- route:
type_a: [handler_a]
type_b: [handler_b]
"""
def test_parallel_workflow_creates_split_node(self, parallel_workflow_yaml):
"""Test that parallel steps create appropriate n8n structure."""
import tempfile
import os
from praisonai.cli.features.n8n import N8nHandler
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(parallel_workflow_yaml)
f.flush()
handler = N8nHandler()
result = handler.convert_yaml_to_n8n(f.name)
# Should have nodes for parallel execution
assert len(result["nodes"]) >= 2
os.unlink(f.name)
if __name__ == "__main__":
pytest.main([__file__, "-v"])
+9 -1
View File
@@ -1,15 +1,21 @@
#!/usr/bin/env python3
"""Test script to verify Phase 2 refactoring maintains backward compatibility"""
"""Test script to verify Phase 2 refactoring maintains backward compatibility
NOTE: These tests make real API calls and require valid API keys.
They are marked with @pytest.mark.real to be excluded from CI.
"""
import asyncio
import sys
import os
import pytest
# Add the correct path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai-agents'))
from praisonaiagents.llm import LLM
@pytest.mark.real
def test_sync_methods():
"""Test synchronous methods still work correctly"""
print("Testing synchronous methods...")
@@ -52,6 +58,7 @@ def test_sync_methods():
print("✅ Synchronous methods working correctly\n")
@pytest.mark.real
async def test_async_methods():
"""Test asynchronous methods still work correctly"""
print("Testing asynchronous methods...")
@@ -94,6 +101,7 @@ async def test_async_methods():
print("✅ Asynchronous methods working correctly\n")
@pytest.mark.real
def test_edge_cases():
"""Test edge cases and special scenarios"""
print("Testing edge cases...")
+1 -1
View File
@@ -224,7 +224,7 @@ def run_tests(pattern=None, verbose=False, coverage=False):
# Run only fast, non-integration tests
cmd.extend(["tests/unit/", "-m", "not slow"])
elif pattern == "all":
cmd.append("tests/")
cmd.extend(["tests/", "-m", "not real"]) # Exclude real tests that require API keys
elif pattern == "frameworks":
# Run both AutoGen and CrewAI integration tests (mock)
cmd.extend(["tests/integration/autogen/", "tests/integration/crewai/"])
@@ -270,9 +270,10 @@ class TestSessionHandler:
from praisonai.cli.features.session import SessionHandler
handler = SessionHandler()
actions = handler.get_actions()
assert "start" in actions
assert "list" in actions
assert "show" in actions
assert "resume" in actions
assert "delete" in actions
class TestToolsHandler:
@@ -0,0 +1,112 @@
"""
Unit tests for Workflow CLI commands.
Tests the WorkflowHandler CLI feature for YAML workflow management.
"""
import os
import tempfile
class TestWorkflowHandler:
"""Tests for WorkflowHandler CLI feature."""
def test_workflow_handler_import(self):
"""Test that WorkflowHandler can be imported."""
from praisonai.cli.features import WorkflowHandler
assert WorkflowHandler is not None
def test_workflow_handler_initialization(self):
"""Test WorkflowHandler initialization."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler(verbose=True)
assert handler.feature_name == "workflow"
def test_workflow_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
actions = handler.get_actions()
assert "run" in actions
assert "validate" in actions
assert "list" in actions
assert "create" in actions
def test_workflow_templates_exist(self):
"""Test that workflow templates are defined."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
assert "simple" in handler.TEMPLATES
assert "routing" in handler.TEMPLATES
assert "parallel" in handler.TEMPLATES
assert "loop" in handler.TEMPLATES
assert "evaluator-optimizer" in handler.TEMPLATES
def test_workflow_create_template(self):
"""Test creating a workflow from template."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
with tempfile.TemporaryDirectory() as tmpdir:
output_file = os.path.join(tmpdir, "test_workflow.yaml")
result = handler.action_create(
["--template", "simple", "--output", output_file]
)
assert result == output_file
assert os.path.exists(output_file)
# Check content
with open(output_file, 'r') as f:
content = f.read()
assert "name: Simple Workflow" in content
assert "agents:" in content
assert "steps:" in content
def test_workflow_validate(self):
"""Test validating a YAML workflow."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
yaml_content = """
name: Test Workflow
agents:
agent1:
name: Agent1
role: Worker
goal: Work
instructions: "Do work"
steps:
- agent: agent1
action: "Work"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
result = handler.action_validate([temp_path])
assert result == True
finally:
os.unlink(temp_path)
def test_workflow_validate_invalid_file(self):
"""Test validating a non-existent file."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
result = handler.action_validate(["nonexistent.yaml"])
assert result == False
def test_workflow_help_text(self):
"""Test help text contains expected commands."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
help_text = handler.get_help_text()
assert "run" in help_text
assert "validate" in help_text
assert "list" in help_text
assert "create" in help_text
assert "template" in help_text
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.0.194" },
{ name = "praisonaiagents", specifier = ">=0.1.13" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4252,7 +4252,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.194"
version = "0.1.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4261,9 +4261,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/5f/f572e88d4148e61da3d114f71357c9103a822099103b0b57adc7d88bedc2/praisonaiagents-0.0.194.tar.gz", hash = "sha256:e70c85203c9cf3792697535844a8bba4da158ecb9959abc5025967199eb86d36", size = 518340, upload-time = "2025-12-16T22:58:43.725Z" }
sdist = { url = "https://files.pythonhosted.org/packages/79/9e/076a981f4d53b0fa58888748971797ab4ab968bf1d183e846e2b76653b19/praisonaiagents-0.1.13.tar.gz", hash = "sha256:1547fd38eef596c5b9bfffec86ce425935990e951605b143c17950ade23e2452", size = 620335, upload-time = "2025-12-18T13:15:34.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/18/d0d0d99ec828d0cc749eb642e0246abda45575c18646893786cdad01fd9c/praisonaiagents-0.0.194-py3-none-any.whl", hash = "sha256:a1f89d3169a4df8c0c96706afcc7c71f20560c4fd2a37aaa0b8193ee6445fdef", size = 494859, upload-time = "2025-12-16T22:58:41.237Z" },
{ url = "https://files.pythonhosted.org/packages/35/50/968fde58d1e02a41411c357251ab6f9f3f07a95167081c71a0a8ee873594/praisonaiagents-0.1.13-py3-none-any.whl", hash = "sha256:4d63873db8fcfaddffec35006864bd73aa55021a4a5e99b6ce9ba818b2ac0311", size = 547445, upload-time = "2025-12-18T13:15:32.935Z" },
]
[[package]]