mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab68b2d296 | ||
|
|
abcc5b3b04 | ||
|
|
e584f56099 | ||
|
|
b6b01af066 | ||
|
|
9aae3eff86 | ||
|
|
f90582c66b | ||
|
|
67a8d65d8e | ||
|
|
d60c31a86d | ||
|
|
fcb7b2a0d0 | ||
|
|
81ad7db679 | ||
|
|
0482c3ebb0 | ||
|
|
3107458016 | ||
|
|
44c8ceebae | ||
|
|
20c0b02528 | ||
|
|
cfc886b591 | ||
|
|
77ac4b7a09 | ||
|
|
5897cedc45 | ||
|
|
a4d1079813 | ||
|
|
4eed985c4d | ||
|
|
0658491072 | ||
|
|
9f82368b4c | ||
|
|
c267cdda87 | ||
|
|
b88bb04090 | ||
|
|
709a35fbdc | ||
|
|
8b792424b6 | ||
|
|
ef2a8cd658 | ||
|
|
d41d2dcba2 | ||
|
|
3bc1786b49 | ||
|
|
2eec3e2359 | ||
|
|
6168401a69 | ||
|
|
473fda653d | ||
|
|
30f1a440b6 | ||
|
|
f991f37cfe |
No files matched your search
@@ -1,45 +1,54 @@
|
||||
name: Daily Publish Docker
|
||||
name: Publish Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag (default: latest)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
jobs:
|
||||
check-and-build:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check last commit time
|
||||
id: check_commit
|
||||
run: |
|
||||
last_commit_time=$(git log -1 --format=%ct)
|
||||
current_time=$(date +%s)
|
||||
time_diff=$((current_time - last_commit_time))
|
||||
if [ $time_diff -ge 120 ] && [ $time_diff -lt 240 ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the GitHub Container Registry
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/mervinpraison/praisonai
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' }}
|
||||
type=raw,value=${{ github.event.inputs.tag || 'latest' }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: steps.check_commit.outputs.should_run == 'true'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/mervinpraison/praisonai:latest
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
+2
-1
@@ -14,9 +14,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN mkdir -p /root/.praison
|
||||
|
||||
# Install Python packages (using latest versions)
|
||||
# Note: praisonai is installed from PyPI - remove version pin to use latest available
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
"praisonai>=2.3.22" \
|
||||
praisonai \
|
||||
"praisonai[api]" \
|
||||
gunicorn \
|
||||
markdown
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.22" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.22" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.22" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
framework: crewai
|
||||
topic: "Technology Research and Content Creation using AWS Bedrock"
|
||||
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
research_specialist:
|
||||
role: "Research Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are an experienced technology researcher with deep expertise in analyzing
|
||||
emerging technologies and market trends. You excel at gathering comprehensive
|
||||
information from various sources and synthesizing it into actionable insights.
|
||||
@@ -35,7 +35,7 @@ roles:
|
||||
|
||||
content_creator:
|
||||
role: "Technical Content Creator"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a skilled technical writer and content creator who specializes in
|
||||
translating complex technology concepts into engaging, accessible content
|
||||
for various audiences including developers, business leaders, and end users.
|
||||
@@ -64,7 +64,7 @@ roles:
|
||||
|
||||
quality_reviewer:
|
||||
role: "Content Quality Reviewer"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a meticulous editor and quality assurance specialist with extensive
|
||||
experience in technical content review. You ensure accuracy, clarity, and
|
||||
consistency while maintaining the intended tone and message.
|
||||
|
||||
@@ -8,10 +8,10 @@ variables:
|
||||
industry: "Electric Vehicles"
|
||||
research_focus: "comprehensive market analysis"
|
||||
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
market_overview_specialist:
|
||||
role: "Market Overview Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a seasoned market research analyst with 10+ years of experience
|
||||
in analyzing market dynamics, trends, and growth patterns across various
|
||||
industries. You excel at synthesizing complex market data into clear,
|
||||
@@ -64,7 +64,7 @@ roles:
|
||||
|
||||
competitive_intelligence_analyst:
|
||||
role: "Competitive Intelligence Analyst"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a competitive intelligence expert specializing in analyzing
|
||||
competitive landscapes, market positioning, and strategic intelligence.
|
||||
You have deep expertise in competitor analysis, market share assessment,
|
||||
@@ -119,7 +119,7 @@ roles:
|
||||
|
||||
financial_performance_analyst:
|
||||
role: "Financial Performance Analyst"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a financial analyst with expertise in corporate finance,
|
||||
financial statement analysis, and industry benchmarking. You specialize
|
||||
in analyzing financial performance metrics, trends, and comparative
|
||||
@@ -173,7 +173,7 @@ roles:
|
||||
|
||||
growth_opportunities_researcher:
|
||||
role: "Growth Opportunities Researcher"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a strategic business analyst specializing in identifying growth
|
||||
opportunities, market expansion strategies, and emerging business models.
|
||||
You excel at spotting trends, gaps, and untapped potential in markets
|
||||
@@ -228,7 +228,7 @@ roles:
|
||||
|
||||
risk_assessment_specialist:
|
||||
role: "Risk Assessment Specialist"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are a risk management expert with extensive experience in identifying,
|
||||
analyzing, and assessing business risks across various industries. You
|
||||
specialize in market risks, competitive threats, regulatory challenges,
|
||||
@@ -287,7 +287,7 @@ roles:
|
||||
|
||||
research_report_synthesizer:
|
||||
role: "Research Report Synthesizer"
|
||||
backstory: |
|
||||
instructions: # Canonical |
|
||||
You are an expert business report writer and analyst with extensive
|
||||
experience in synthesizing complex research findings into clear,
|
||||
actionable business intelligence reports. You excel at creating
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Multiple MCP Servers Example
|
||||
|
||||
This example demonstrates how to use multiple MCP servers with a single agent.
|
||||
You can combine different MCP servers (time, memory, filesystem, etc.) along
|
||||
with regular Python functions as tools.
|
||||
|
||||
Requirements:
|
||||
- pip install praisonaiagents
|
||||
- uvx (for mcp-server-time)
|
||||
- npx (for @modelcontextprotocol servers)
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.mcp import MCP
|
||||
|
||||
|
||||
def my_custom_tool(query: str) -> str:
|
||||
"""
|
||||
A custom Python function that can be used alongside MCP tools.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
|
||||
Returns:
|
||||
A formatted result string
|
||||
"""
|
||||
return f"Custom tool result for: {query}"
|
||||
|
||||
|
||||
def main():
|
||||
# Example 1: Single MCP server (basic usage)
|
||||
print("=" * 60)
|
||||
print("Example 1: Single MCP Server")
|
||||
print("=" * 60)
|
||||
|
||||
agent_single = Agent(
|
||||
name="TimeAgent",
|
||||
instructions="You are a helpful assistant that can tell time.",
|
||||
tools=MCP("uvx mcp-server-time")
|
||||
)
|
||||
|
||||
response = agent_single.start("What time is it in Tokyo?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 2: Multiple MCP servers
|
||||
print("=" * 60)
|
||||
print("Example 2: Multiple MCP Servers")
|
||||
print("=" * 60)
|
||||
|
||||
agent_multi = Agent(
|
||||
name="MultiMCPAgent",
|
||||
instructions="""You are a helpful assistant with access to multiple tools:
|
||||
- Time tools for getting current time and converting timezones
|
||||
- Memory tools for storing and retrieving information
|
||||
Use the appropriate tool based on the user's request.""",
|
||||
tools=[
|
||||
MCP("uvx mcp-server-time"), # Time tools
|
||||
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
|
||||
]
|
||||
)
|
||||
|
||||
response = agent_multi.start("What time is it in New York and London?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 3: Multiple MCP servers + custom functions
|
||||
print("=" * 60)
|
||||
print("Example 3: MCP Servers + Custom Functions")
|
||||
print("=" * 60)
|
||||
|
||||
agent_mixed = Agent(
|
||||
name="MixedToolsAgent",
|
||||
instructions="""You are a helpful assistant with access to:
|
||||
- Time tools for timezone operations
|
||||
- A custom search tool for general queries
|
||||
Choose the right tool for each task.""",
|
||||
tools=[
|
||||
MCP("uvx mcp-server-time"), # MCP server
|
||||
my_custom_tool # Regular Python function
|
||||
]
|
||||
)
|
||||
|
||||
response = agent_mixed.start("What time is it in Paris?")
|
||||
print(f"Response: {response}\n")
|
||||
|
||||
# Example 4: MCP with environment variables
|
||||
print("=" * 60)
|
||||
print("Example 4: MCP with Environment Variables")
|
||||
print("=" * 60)
|
||||
|
||||
# Note: Replace with your actual API key
|
||||
# agent_with_env = Agent(
|
||||
# name="SearchAgent",
|
||||
# instructions="You can search the web for information.",
|
||||
# tools=[
|
||||
# MCP(
|
||||
# command="npx",
|
||||
# args=["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
# env={"BRAVE_API_KEY": "your-api-key-here"}
|
||||
# ),
|
||||
# MCP("uvx mcp-server-time")
|
||||
# ]
|
||||
# )
|
||||
print("(Skipped - requires BRAVE_API_KEY)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,8 @@
|
||||
framework: praisonai
|
||||
topic: crawl and extract all data from each url from this page https://news.ycombinator.com/
|
||||
roles:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
web_scraper:
|
||||
backstory: An expert in data extraction from websites, adept at navigating and
|
||||
instructions: # Canonical An expert in data extraction from websites, adept at navigating and
|
||||
retrieving detailed information.
|
||||
goal: Extract URLs from the source page
|
||||
role: Web Scraper
|
||||
@@ -15,7 +15,7 @@ roles:
|
||||
tools:
|
||||
- 'code_interpret'
|
||||
url_data_extractor:
|
||||
backstory: Specializes in crawling websites to gather comprehensive data, ensuring
|
||||
instructions: # Canonical Specializes in crawling websites to gather comprehensive data, ensuring
|
||||
nothing is missed from each link.
|
||||
goal: Crawl each URL for data extraction
|
||||
role: URL Data Extractor
|
||||
@@ -29,7 +29,7 @@ roles:
|
||||
tools:
|
||||
- 'code_interpret'
|
||||
data_organizer:
|
||||
backstory: Known for keen attention to detail, capable of structuring vast amounts
|
||||
instructions: # Canonical Known for keen attention to detail, capable of structuring vast amounts
|
||||
of information into usable formats efficiently.
|
||||
goal: Organize extracted data into a structured format
|
||||
role: Data Organizer
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
"""
|
||||
Simple Workflow Example
|
||||
Simple Agentic Workflow Example
|
||||
|
||||
The easiest way to create and run workflows in PraisonAI.
|
||||
Just pass functions or agents as steps - no complex configuration needed!
|
||||
Demonstrates using Agent instances directly as workflow steps.
|
||||
Each agent processes the input and passes output to the next agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow
|
||||
|
||||
# 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}")
|
||||
# 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."
|
||||
)
|
||||
|
||||
def process(ctx: WorkflowContext) -> StepResult:
|
||||
"""Process the validated data."""
|
||||
return StepResult(output=f"Processed: {ctx.previous_result}")
|
||||
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."
|
||||
)
|
||||
|
||||
def format_output(ctx: WorkflowContext) -> StepResult:
|
||||
"""Format the final output."""
|
||||
return StepResult(output=f"✅ Final: {ctx.previous_result}")
|
||||
|
||||
# Create workflow - just list your functions!
|
||||
# Create workflow with agents as steps
|
||||
workflow = Workflow(
|
||||
name="Simple Pipeline",
|
||||
steps=[validate, process, format_output]
|
||||
name="Simple Agentic Pipeline",
|
||||
steps=[researcher, writer]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the workflow - PraisonAI style uses .start()
|
||||
result = workflow.start("Hello, World!", verbose=True)
|
||||
# Run workflow - agents process sequentially
|
||||
result = workflow.start(
|
||||
"What are the key benefits of AI agents?",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"\nFinal output: {result['output']}")
|
||||
print(f"Steps completed: {len(result['steps'])}")
|
||||
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'])}")
|
||||
@@ -1,68 +1,83 @@
|
||||
"""
|
||||
Workflow Conditional Execution Example
|
||||
Agentic Conditional Workflow Example
|
||||
|
||||
Demonstrates using should_run to conditionally execute steps
|
||||
Demonstrates using should_run to conditionally execute agent steps
|
||||
based on input or previous results.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowStep, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow, WorkflowStep
|
||||
|
||||
# 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)
|
||||
# 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_translation(ctx: WorkflowContext) -> bool:
|
||||
"""Only translate if content is not in English."""
|
||||
return ctx.variables.get("language", "en") != "en"
|
||||
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)
|
||||
|
||||
# Handler functions
|
||||
def process_content(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"Processed: {ctx.input}")
|
||||
# Create agents
|
||||
analyzer = Agent(
|
||||
name="Analyzer",
|
||||
role="Content Analyzer",
|
||||
goal="Analyze and process content",
|
||||
instructions="Analyze the given content and provide insights."
|
||||
)
|
||||
|
||||
def compliance_check(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"✅ Compliance verified for: {ctx.previous_result}")
|
||||
tech_reviewer = Agent(
|
||||
name="TechReviewer",
|
||||
role="Technical Reviewer",
|
||||
goal="Review technical content",
|
||||
instructions="Review the technical aspects and provide technical feedback."
|
||||
)
|
||||
|
||||
def translate(ctx: WorkflowContext) -> StepResult:
|
||||
lang = ctx.variables.get("language", "unknown")
|
||||
return StepResult(output=f"🌐 Translated to {lang}: {ctx.previous_result}")
|
||||
creative_enhancer = Agent(
|
||||
name="CreativeEnhancer",
|
||||
role="Creative Enhancer",
|
||||
goal="Enhance creative content",
|
||||
instructions="Enhance the creative aspects and make it more engaging."
|
||||
)
|
||||
|
||||
def finalize(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output=f"📄 Final: {ctx.previous_result}")
|
||||
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 Pipeline",
|
||||
name="Conditional Agentic Pipeline",
|
||||
steps=[
|
||||
WorkflowStep(name="process", handler=process_content),
|
||||
analyzer, # Always runs
|
||||
WorkflowStep(
|
||||
name="compliance",
|
||||
handler=compliance_check,
|
||||
should_run=is_sensitive_content # Only runs for sensitive content
|
||||
name="tech_review",
|
||||
agent=tech_reviewer,
|
||||
should_run=is_technical # Only runs for technical content
|
||||
),
|
||||
WorkflowStep(
|
||||
name="translate",
|
||||
handler=translate,
|
||||
should_run=needs_translation # Only runs if language != "en"
|
||||
name="creative_enhance",
|
||||
agent=creative_enhancer,
|
||||
should_run=needs_creative # Only runs for creative content
|
||||
),
|
||||
WorkflowStep(name="finalize", handler=finalize)
|
||||
finalizer # Always runs
|
||||
]
|
||||
)
|
||||
|
||||
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 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: 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 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: 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']}")
|
||||
# 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']}")
|
||||
@@ -1,33 +1,36 @@
|
||||
"""
|
||||
Workflow Loop with CSV Example
|
||||
Agentic Loop with CSV Workflow Example
|
||||
|
||||
Demonstrates iterating over a CSV file, processing each row.
|
||||
Demonstrates iterating over a CSV file with an agent processing each row.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow
|
||||
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"""
|
||||
# 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"""
|
||||
|
||||
# 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}
|
||||
)
|
||||
# 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
|
||||
@@ -36,20 +39,21 @@ if __name__ == "__main__":
|
||||
csv_path = f.name
|
||||
|
||||
try:
|
||||
print("=== Testing Loop with CSV ===\n")
|
||||
print(f"CSV file: {csv_path}\n")
|
||||
print("=== Testing Agentic Loop with CSV ===\n")
|
||||
print(f"Processing CSV: {csv_path}\n")
|
||||
|
||||
# Create workflow
|
||||
# Create workflow with loop over CSV
|
||||
workflow = Workflow(
|
||||
name="CSV Task Processor",
|
||||
steps=[loop(process_task, from_csv=csv_path)]
|
||||
name="CSV Topic Processor",
|
||||
steps=[
|
||||
loop(processor, from_csv=csv_path),
|
||||
summarizer
|
||||
]
|
||||
)
|
||||
|
||||
result = workflow.start("Process all tasks", verbose=True)
|
||||
result = workflow.start("Analyze these AI topics", 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')}")
|
||||
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)
|
||||
@@ -1,53 +1,56 @@
|
||||
"""
|
||||
Workflow Parallel Execution Example
|
||||
Agentic Parallel Workflow Example
|
||||
|
||||
Demonstrates running multiple steps concurrently and
|
||||
combining their results.
|
||||
Demonstrates running multiple agents concurrently and
|
||||
combining their results with an aggregator agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow
|
||||
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")
|
||||
# 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."
|
||||
)
|
||||
|
||||
def research_competitors(ctx: WorkflowContext) -> StepResult:
|
||||
"""Research competitor analysis."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return StepResult(output="🏢 Competitors: 3 major players identified")
|
||||
competitor_researcher = Agent(
|
||||
name="CompetitorResearcher",
|
||||
role="Competitive Intelligence Analyst",
|
||||
goal="Research competitor strategies",
|
||||
instructions="Analyze competitors. Provide key competitive insights."
|
||||
)
|
||||
|
||||
def research_customers(ctx: WorkflowContext) -> StepResult:
|
||||
"""Research customer feedback."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return StepResult(output="👥 Customers: 85% satisfaction rate")
|
||||
customer_researcher = Agent(
|
||||
name="CustomerResearcher",
|
||||
role="Customer Research Analyst",
|
||||
goal="Research customer needs and behaviors",
|
||||
instructions="Analyze customer segments. Provide customer insights."
|
||||
)
|
||||
|
||||
# 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 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",
|
||||
name="Parallel Research Pipeline",
|
||||
steps=[
|
||||
parallel([research_market, research_competitors, research_customers]),
|
||||
summarize_research
|
||||
parallel([market_researcher, competitor_researcher, customer_researcher]),
|
||||
aggregator
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Parallel Workflow ===\n")
|
||||
print("=== Testing Agentic Parallel Workflow ===\n")
|
||||
|
||||
start_time = time.time()
|
||||
result = workflow.start("Analyze the business landscape", verbose=True)
|
||||
elapsed = time.time() - start_time
|
||||
# Run workflow - all researchers work in parallel, then aggregator summarizes
|
||||
result = workflow.start("Research the AI industry", verbose=True)
|
||||
|
||||
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'])}")
|
||||
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'])}")
|
||||
@@ -1,69 +1,53 @@
|
||||
"""
|
||||
Workflow Repeat (Evaluator-Optimizer) Example
|
||||
Agentic Evaluator-Optimizer (Repeat) Workflow Example
|
||||
|
||||
Demonstrates repeating a step until a condition is met.
|
||||
This is useful for iterative improvement patterns.
|
||||
Demonstrates the evaluator-optimizer pattern where an agent
|
||||
generates content and another evaluates it, repeating until approved.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow
|
||||
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()
|
||||
}
|
||||
)
|
||||
# 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."
|
||||
)
|
||||
|
||||
# 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 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."
|
||||
)
|
||||
|
||||
# Create generator instance
|
||||
generator = ContentGenerator()
|
||||
# Evaluation function to check if content is approved
|
||||
def is_approved(ctx) -> bool:
|
||||
return "approved" in ctx.previous_result.lower()
|
||||
|
||||
# Create workflow with repeat pattern
|
||||
# Create workflow with evaluator-optimizer pattern
|
||||
workflow = Workflow(
|
||||
name="Content Generator",
|
||||
name="Evaluator-Optimizer Pipeline",
|
||||
steps=[
|
||||
generator, # First, generate content
|
||||
repeat(
|
||||
generator.generate,
|
||||
until=has_enough_content,
|
||||
max_iterations=10
|
||||
evaluator, # Evaluate and provide feedback
|
||||
until=is_approved,
|
||||
max_iterations=3
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Testing Repeat (Evaluator-Optimizer) Pattern ===\n")
|
||||
print("=== Testing Agentic Evaluator-Optimizer Workflow ===\n")
|
||||
|
||||
result = workflow.start("Generate comprehensive content", verbose=True)
|
||||
# Run optimization workflow
|
||||
result = workflow.start(
|
||||
"Write a compelling product description for an AI assistant",
|
||||
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}")
|
||||
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}")
|
||||
@@ -1,61 +1,65 @@
|
||||
"""
|
||||
Workflow Routing Example
|
||||
Agentic Routing Workflow Example
|
||||
|
||||
Demonstrates decision-based routing where the workflow
|
||||
takes different paths based on the output of a decision step.
|
||||
Demonstrates decision-based routing where an agent classifier
|
||||
routes to specialized handler agents based on the request type.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Workflow, WorkflowContext, StepResult
|
||||
from praisonaiagents import Agent, Workflow
|
||||
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")
|
||||
# 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'."
|
||||
)
|
||||
|
||||
# Route handlers
|
||||
def handle_high_priority(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="🚨 HIGH PRIORITY: Escalating to senior team immediately!")
|
||||
# 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."
|
||||
)
|
||||
|
||||
def handle_support(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="💬 SUPPORT: Routing to help desk for assistance.")
|
||||
creative_agent = Agent(
|
||||
name="CreativeWriter",
|
||||
role="Creative Writer",
|
||||
goal="Handle creative requests",
|
||||
instructions="You are a creative writer. Write engaging, creative content."
|
||||
)
|
||||
|
||||
def handle_normal(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="📋 NORMAL: Added to standard processing queue.")
|
||||
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="Request Router",
|
||||
name="Agentic Router",
|
||||
steps=[
|
||||
classify_request,
|
||||
classifier,
|
||||
route({
|
||||
"high": [handle_high_priority],
|
||||
"support": [handle_support],
|
||||
"normal": [handle_normal],
|
||||
"default": [handle_normal]
|
||||
"technical": [tech_agent],
|
||||
"creative": [creative_agent],
|
||||
"default": [general_agent]
|
||||
})
|
||||
]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test different inputs
|
||||
print("=== Testing Routing Workflow ===\n")
|
||||
print("=== Testing Agentic Routing Workflow ===\n")
|
||||
|
||||
# Test 1: Urgent request
|
||||
result = workflow.start("This is an URGENT matter!", verbose=True)
|
||||
print(f"Result: {result['output']}\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: 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']}")
|
||||
# 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,117 @@
|
||||
# Extended agents.yaml with Workflow Patterns
|
||||
# This file demonstrates the extended agents.yaml format that supports
|
||||
# advanced workflow patterns while maintaining backward compatibility.
|
||||
#
|
||||
# Usage: praisonai agents_workflow.yaml
|
||||
#
|
||||
# Key differences from standard agents.yaml:
|
||||
# - process: workflow (triggers workflow mode)
|
||||
# - workflow: section for planning, reasoning, memory
|
||||
# - steps: section for workflow patterns (route, parallel, loop, repeat)
|
||||
|
||||
framework: praisonai
|
||||
process: workflow
|
||||
input: "Research AI trends and create a comprehensive report" # The input passed to {{input}} in steps
|
||||
|
||||
# Workflow configuration (new)
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
# Variables for template substitution
|
||||
variables:
|
||||
topic: AI trends
|
||||
topics:
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
# Agent definitions using familiar 'roles' format
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
classifier:
|
||||
role: Request Classifier
|
||||
instructions: # Canonical |
|
||||
You are an expert at classifying requests into categories.
|
||||
Respond with ONLY one word: 'technical', 'creative', or 'research'.
|
||||
goal: Classify incoming requests accurately
|
||||
|
||||
researcher:
|
||||
role: Research Analyst
|
||||
instructions: # Canonical |
|
||||
You are a skilled research analyst with expertise in technology trends.
|
||||
You provide concise, factual research findings with citations.
|
||||
goal: Research topics thoroughly and provide insights
|
||||
tools:
|
||||
- tavily_search
|
||||
llm:
|
||||
model: gpt-4o-mini
|
||||
|
||||
tech_expert:
|
||||
role: Technical Expert
|
||||
instructions: # Canonical |
|
||||
You are a technical expert with deep knowledge in AI and ML.
|
||||
You provide detailed technical explanations.
|
||||
goal: Handle technical questions with expertise
|
||||
|
||||
creative_writer:
|
||||
role: Creative Writer
|
||||
instructions: # Canonical |
|
||||
You are a creative writer who can make complex topics engaging.
|
||||
You write in a clear, accessible style.
|
||||
goal: Create engaging content from research
|
||||
|
||||
market_analyst:
|
||||
role: Market Analyst
|
||||
instructions: # Canonical |
|
||||
You analyze market trends and competitive landscapes.
|
||||
You provide actionable business insights.
|
||||
goal: Analyze market trends and opportunities
|
||||
|
||||
aggregator:
|
||||
role: Synthesizer
|
||||
instructions: # Canonical |
|
||||
You combine findings from multiple sources into coherent reports.
|
||||
You identify patterns and key insights.
|
||||
goal: Synthesize all findings into comprehensive reports
|
||||
|
||||
# Workflow steps with patterns (new)
|
||||
steps:
|
||||
# Step 1: Classify the request
|
||||
- agent: classifier
|
||||
action: "Classify this request: {{topic}}"
|
||||
|
||||
# Step 2: Route to appropriate handler based on classification
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [researcher]
|
||||
|
||||
# Step 3: Parallel research on multiple aspects
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: researcher
|
||||
action: "Research technology trends for {{topic}}"
|
||||
- agent: market_analyst
|
||||
action: "Analyze market opportunities for {{topic}}"
|
||||
|
||||
# Step 4: Loop over specific topics
|
||||
- agent: researcher
|
||||
action: "Deep dive into {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
# Step 5: Aggregate all findings
|
||||
- agent: aggregator
|
||||
action: "Synthesize all research findings into a comprehensive report"
|
||||
repeat:
|
||||
until: "comprehensive"
|
||||
max_iterations: 2
|
||||
|
||||
# Optional callbacks
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
@@ -0,0 +1,116 @@
|
||||
# Complete Workflow Example
|
||||
# Demonstrates all workflow patterns in a single comprehensive workflow
|
||||
|
||||
name: Complete Research Workflow
|
||||
description: A comprehensive workflow demonstrating all patterns - route, parallel, loop, repeat
|
||||
framework: praisonai
|
||||
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
variables:
|
||||
topic: AI trends
|
||||
topics:
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
agents:
|
||||
classifier:
|
||||
name: Classifier
|
||||
role: Request Classifier
|
||||
goal: Classify incoming requests
|
||||
instructions: |
|
||||
Classify the request. Respond with ONLY one word:
|
||||
- 'technical' for technical questions
|
||||
- 'creative' for creative requests
|
||||
- 'research' for research tasks
|
||||
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics thoroughly
|
||||
instructions: "You are a research analyst. Provide concise, factual research findings."
|
||||
|
||||
tech_expert:
|
||||
name: TechExpert
|
||||
role: Technical Expert
|
||||
goal: Handle technical questions
|
||||
instructions: "You are a technical expert. Provide detailed technical answers."
|
||||
|
||||
creative_writer:
|
||||
name: CreativeWriter
|
||||
role: Creative Writer
|
||||
goal: Handle creative requests
|
||||
instructions: "You are a creative writer. Write engaging, creative content."
|
||||
|
||||
market_analyst:
|
||||
name: MarketAnalyst
|
||||
role: Market Analyst
|
||||
goal: Analyze market trends
|
||||
instructions: "You are a market analyst. Provide market insights."
|
||||
|
||||
competitor_analyst:
|
||||
name: CompetitorAnalyst
|
||||
role: Competitor Analyst
|
||||
goal: Analyze competitors
|
||||
instructions: "You are a competitor analyst. Provide competitive insights."
|
||||
|
||||
aggregator:
|
||||
name: Aggregator
|
||||
role: Synthesizer
|
||||
goal: Combine and synthesize findings
|
||||
instructions: "You are a synthesizer. Combine all findings into a comprehensive report."
|
||||
|
||||
evaluator:
|
||||
name: Evaluator
|
||||
role: Quality Evaluator
|
||||
goal: Evaluate content quality
|
||||
instructions: |
|
||||
Evaluate the content. If it's comprehensive and well-structured, respond with "APPROVED".
|
||||
Otherwise, provide specific feedback for improvement.
|
||||
|
||||
steps:
|
||||
# Step 1: Classify the request
|
||||
- agent: classifier
|
||||
action: "Classify: {{input}}"
|
||||
|
||||
# Step 2: Route to appropriate handler
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [researcher]
|
||||
|
||||
# Step 3: Parallel research
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: market_analyst
|
||||
action: "Analyze market for {{topic}}"
|
||||
- agent: competitor_analyst
|
||||
action: "Analyze competitors for {{topic}}"
|
||||
|
||||
# Step 4: Loop over topics
|
||||
- agent: researcher
|
||||
action: "Research {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
# Step 5: Aggregate findings
|
||||
- agent: aggregator
|
||||
action: "Synthesize all findings into a comprehensive report"
|
||||
|
||||
# Step 6: Evaluator-optimizer
|
||||
- agent: evaluator
|
||||
action: "Evaluate the report quality"
|
||||
repeat:
|
||||
until: "approved"
|
||||
max_iterations: 2
|
||||
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
@@ -0,0 +1,43 @@
|
||||
# Evaluator-Optimizer Workflow
|
||||
# Demonstrates the repeat() pattern for iterative improvement
|
||||
|
||||
name: Evaluator Optimizer Workflow
|
||||
description: A generator creates content, an evaluator reviews it repeatedly until quality standards are met
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
generator:
|
||||
name: Generator
|
||||
role: Content Generator
|
||||
goal: Generate high-quality content
|
||||
instructions: |
|
||||
You are a content generator. Generate content based on the request.
|
||||
If feedback is provided, improve your content based on that feedback.
|
||||
Always strive for clarity, accuracy, and engagement.
|
||||
|
||||
evaluator:
|
||||
name: Evaluator
|
||||
role: Content Evaluator
|
||||
goal: Evaluate content quality and provide feedback
|
||||
instructions: |
|
||||
You are a content evaluator. Review the content provided.
|
||||
|
||||
If the content is high quality (clear, accurate, engaging), respond with:
|
||||
"APPROVED: [brief reason]"
|
||||
|
||||
If the content needs improvement, respond with:
|
||||
"NEEDS IMPROVEMENT: [specific feedback for improvement]"
|
||||
|
||||
Be constructive and specific in your feedback.
|
||||
|
||||
steps:
|
||||
- agent: generator
|
||||
action: "Generate content for: {{input}}"
|
||||
|
||||
- agent: evaluator
|
||||
action: "Evaluate this content and provide feedback: {{previous_output}}"
|
||||
repeat:
|
||||
until: "approved"
|
||||
max_iterations: 3
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Example usage of YAML Workflow Parser.
|
||||
|
||||
This script demonstrates how to load and execute YAML workflow files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure OPENAI_API_KEY is set
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
print("export OPENAI_API_KEY='your-api-key'")
|
||||
exit(1)
|
||||
|
||||
from praisonaiagents.workflows import YAMLWorkflowParser, WorkflowManager
|
||||
|
||||
|
||||
def example_1_parse_and_run():
|
||||
"""Example 1: Parse YAML and run workflow directly."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Parse YAML and run workflow")
|
||||
print("=" * 60)
|
||||
|
||||
yaml_content = """
|
||||
name: Quick Research
|
||||
description: A quick research workflow
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics
|
||||
instructions: "Provide brief, factual information."
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research: {{input}}"
|
||||
"""
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
print(f"Workflow Name: {workflow.name}")
|
||||
print(f"Steps: {len(workflow.steps)}")
|
||||
|
||||
# Run the workflow
|
||||
result = workflow.start("What is Python?")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_2_load_yaml_file():
|
||||
"""Example 2: Load workflow from YAML file using WorkflowManager."""
|
||||
print("=" * 60)
|
||||
print("Example 2: Load YAML file with WorkflowManager")
|
||||
print("=" * 60)
|
||||
|
||||
# Get the directory of this script
|
||||
script_dir = Path(__file__).parent
|
||||
yaml_file = script_dir / "simple_workflow.yaml"
|
||||
|
||||
if not yaml_file.exists():
|
||||
print(f"YAML file not found: {yaml_file}")
|
||||
return
|
||||
|
||||
manager = WorkflowManager()
|
||||
workflow = manager.load_yaml(yaml_file)
|
||||
|
||||
print(f"Loaded workflow: {workflow.name}")
|
||||
print(f"Steps: {len(workflow.steps)}")
|
||||
|
||||
# Run the workflow
|
||||
result = workflow.start("Benefits of renewable energy")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_3_execute_yaml_directly():
|
||||
"""Example 3: Execute YAML file directly with WorkflowManager."""
|
||||
print("=" * 60)
|
||||
print("Example 3: Execute YAML directly")
|
||||
print("=" * 60)
|
||||
|
||||
script_dir = Path(__file__).parent
|
||||
yaml_file = script_dir / "simple_workflow.yaml"
|
||||
|
||||
if not yaml_file.exists():
|
||||
print(f"YAML file not found: {yaml_file}")
|
||||
return
|
||||
|
||||
manager = WorkflowManager()
|
||||
result = manager.execute_yaml(
|
||||
yaml_file,
|
||||
input_data="The future of AI",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Status: {result['status']}")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
def example_4_with_variables():
|
||||
"""Example 4: YAML workflow with variables."""
|
||||
print("=" * 60)
|
||||
print("Example 4: Workflow with variables")
|
||||
print("=" * 60)
|
||||
|
||||
yaml_content = """
|
||||
name: Variable Workflow
|
||||
variables:
|
||||
topic: Machine Learning
|
||||
style: concise
|
||||
|
||||
agents:
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Write content
|
||||
instructions: "Write {{style}} content about the topic."
|
||||
|
||||
steps:
|
||||
- agent: writer
|
||||
action: "Write about {{topic}}"
|
||||
"""
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
print(f"Variables: {workflow.variables}")
|
||||
|
||||
# Run with default variables
|
||||
result = workflow.start("")
|
||||
print(f"Result: {result['output'][:200]}...")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("YAML Workflow Examples")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Run examples
|
||||
example_1_parse_and_run()
|
||||
example_2_load_yaml_file()
|
||||
example_3_execute_yaml_directly()
|
||||
example_4_with_variables()
|
||||
|
||||
print("=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Loop Workflow
|
||||
# Demonstrates the loop() pattern for iterating over items
|
||||
|
||||
name: Repetitive Processing Workflow
|
||||
description: An agent processes multiple items in a loop, then a summarizer combines results
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
variables:
|
||||
topics:
|
||||
- Artificial Intelligence
|
||||
- Machine Learning
|
||||
- Neural Networks
|
||||
- Natural Language Processing
|
||||
|
||||
agents:
|
||||
processor:
|
||||
name: Processor
|
||||
role: Topic Processor
|
||||
goal: Process and analyze each topic thoroughly
|
||||
instructions: |
|
||||
You are a topic processor. For each topic given, provide:
|
||||
1. A brief definition
|
||||
2. Key concepts
|
||||
3. Real-world applications
|
||||
Keep your response concise but informative.
|
||||
|
||||
summarizer:
|
||||
name: Summarizer
|
||||
role: Results Summarizer
|
||||
goal: Summarize all processed results into a final report
|
||||
instructions: "You are a summarizer. Combine all the processed topics into a cohesive final report."
|
||||
|
||||
steps:
|
||||
- agent: processor
|
||||
action: "Analyze this topic: {{item}}"
|
||||
loop:
|
||||
over: topics
|
||||
|
||||
- agent: summarizer
|
||||
action: "Create a comprehensive summary of all the analyzed topics based on these results: {{previous_output}}"
|
||||
@@ -0,0 +1,47 @@
|
||||
# Parallel Workflow
|
||||
# Demonstrates the parallel() pattern for concurrent execution
|
||||
|
||||
name: Parallel Research Workflow
|
||||
description: Multiple agents research different aspects concurrently, then an aggregator combines results
|
||||
input: "AI-powered customer service solutions" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
market_researcher:
|
||||
name: MarketResearcher
|
||||
role: Market Research Analyst
|
||||
goal: Research market trends and opportunities
|
||||
instructions: "You are a market research analyst. Analyze market trends and provide concise insights."
|
||||
|
||||
competitor_researcher:
|
||||
name: CompetitorResearcher
|
||||
role: Competitive Intelligence Analyst
|
||||
goal: Research competitor strategies and positioning
|
||||
instructions: "You are a competitive intelligence analyst. Analyze competitors and provide key insights."
|
||||
|
||||
customer_researcher:
|
||||
name: CustomerResearcher
|
||||
role: Customer Research Analyst
|
||||
goal: Research customer needs and behaviors
|
||||
instructions: "You are a customer research analyst. Analyze customer segments and provide insights."
|
||||
|
||||
aggregator:
|
||||
name: Aggregator
|
||||
role: Research Synthesizer
|
||||
goal: Synthesize all research findings into a comprehensive summary
|
||||
instructions: "You are a research synthesizer. Combine all research findings into a clear, comprehensive summary."
|
||||
|
||||
steps:
|
||||
- name: parallel_research
|
||||
parallel:
|
||||
- agent: market_researcher
|
||||
action: "Research market trends for: {{input}}"
|
||||
- agent: competitor_researcher
|
||||
action: "Research competitors for: {{input}}"
|
||||
- agent: customer_researcher
|
||||
action: "Research customer needs for: {{input}}"
|
||||
|
||||
- agent: aggregator
|
||||
action: "Synthesize all the research findings into a comprehensive report: {{previous_output}}"
|
||||
@@ -0,0 +1,50 @@
|
||||
# Routing Workflow
|
||||
# Demonstrates the route() pattern for decision-based branching
|
||||
|
||||
name: Agentic Routing Workflow
|
||||
description: A classifier agent routes requests to specialized handler agents
|
||||
input: "Write a poem about the ocean" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
classifier:
|
||||
name: Classifier
|
||||
role: Request Classifier
|
||||
goal: Classify incoming requests by type
|
||||
instructions: |
|
||||
You are a request classifier. Analyze the request and respond with ONLY one word:
|
||||
- 'technical' for technical/coding questions
|
||||
- 'creative' for creative writing requests
|
||||
- 'general' for everything else
|
||||
|
||||
Respond with ONLY the classification word, nothing else.
|
||||
|
||||
tech_expert:
|
||||
name: TechExpert
|
||||
role: Technical Expert
|
||||
goal: Answer technical questions with expertise
|
||||
instructions: "You are a technical expert. Provide detailed, accurate technical answers."
|
||||
|
||||
creative_writer:
|
||||
name: CreativeWriter
|
||||
role: Creative Writer
|
||||
goal: Create engaging creative content
|
||||
instructions: "You are a creative writer. Write engaging, imaginative content."
|
||||
|
||||
general_assistant:
|
||||
name: GeneralAssistant
|
||||
role: General Assistant
|
||||
goal: Help with general requests
|
||||
instructions: "You are a helpful assistant. Provide clear, helpful responses."
|
||||
|
||||
steps:
|
||||
- agent: classifier
|
||||
action: "Classify this request: {{input}}"
|
||||
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_expert]
|
||||
creative: [creative_writer]
|
||||
default: [general_assistant]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Simple Sequential Workflow
|
||||
# Demonstrates basic agent-to-agent workflow
|
||||
|
||||
name: Simple Research Workflow
|
||||
description: A simple workflow where a researcher gathers info and a writer summarizes it
|
||||
input: "The benefits of renewable energy" # The input passed to {{input}} in steps
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research and gather information about topics
|
||||
instructions: "You are a research analyst. Provide concise, factual information about the given topic."
|
||||
|
||||
writer:
|
||||
name: Writer
|
||||
role: Content Writer
|
||||
goal: Write clear and engaging summaries
|
||||
instructions: "You are a content writer. Take the research provided and write a clear, engaging summary."
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research the following topic: {{input}}"
|
||||
|
||||
- agent: writer
|
||||
action: "Write a summary based on this research: {{previous_output}}"
|
||||
@@ -87,3 +87,4 @@ graph.py
|
||||
chroma_db/
|
||||
.qodo
|
||||
*_COMPARISON_ANALYSIS.md
|
||||
*_REPORT.md
|
||||
File diff suppressed because it is too large.
Load diff
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../README.md
|
||||
@@ -0,0 +1,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}
|
||||
--------------------------------------------------
|
||||
@@ -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)
|
||||
|
||||
@@ -440,7 +440,7 @@ class Workflow:
|
||||
previous_output = output
|
||||
|
||||
if verbose:
|
||||
print(f"✅ {step.name}: {str(output)[:100]}...")
|
||||
print(f"✅ {step.name}: {str(output)}")
|
||||
|
||||
# Handle early stop
|
||||
if stop:
|
||||
@@ -571,7 +571,7 @@ class Workflow:
|
||||
output = f"Error: {e}"
|
||||
|
||||
if verbose:
|
||||
print(f"✅ {normalized.name}: {str(output)[:100]}...")
|
||||
print(f"✅ {normalized.name}: {str(output)}")
|
||||
|
||||
return {
|
||||
"step": normalized.name,
|
||||
|
||||
@@ -30,6 +30,8 @@ from .workflows import (
|
||||
StepOutput,
|
||||
)
|
||||
|
||||
from .yaml_parser import YAMLWorkflowParser
|
||||
|
||||
__all__ = [
|
||||
# Core
|
||||
"Workflow",
|
||||
@@ -39,6 +41,9 @@ __all__ = [
|
||||
"StepResult",
|
||||
"WorkflowManager",
|
||||
|
||||
# YAML Parser
|
||||
"YAMLWorkflowParser",
|
||||
|
||||
# Patterns
|
||||
"Route",
|
||||
"Parallel",
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Callable, Literal, Tuple
|
||||
from typing import Any, Dict, List, Optional, Callable, Literal, Tuple, Union
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -213,6 +213,19 @@ class WorkflowStep:
|
||||
# Guardrail (like process="workflow")
|
||||
guardrail: Optional[Callable[['StepResult'], Tuple[bool, Any]]] = None # Returns (is_valid, feedback)
|
||||
|
||||
# Output configuration (migrated from Task)
|
||||
output_file: Optional[str] = None # Save output to file
|
||||
output_json: Optional[Any] = None # Pydantic model for JSON output
|
||||
output_pydantic: Optional[Any] = None # Pydantic model for structured output
|
||||
|
||||
# Image handling (migrated from Task)
|
||||
images: Optional[List[str]] = None # Image paths/URLs for vision tasks
|
||||
|
||||
# Execution control (migrated from Task)
|
||||
async_execution: bool = False # Run step asynchronously
|
||||
quality_check: bool = True # Enable quality validation
|
||||
rerun: bool = True # Allow step to be rerun
|
||||
|
||||
# Status tracking
|
||||
status: str = "pending" # pending, running, completed, failed, skipped
|
||||
retry_count: int = 0
|
||||
@@ -245,7 +258,14 @@ class WorkflowStep:
|
||||
"next_steps": self.next_steps,
|
||||
"branch_condition": self.branch_condition,
|
||||
"loop_over": self.loop_over,
|
||||
"loop_var": self.loop_var
|
||||
"loop_var": self.loop_var,
|
||||
"output_file": self.output_file,
|
||||
"output_json": self.output_json is not None,
|
||||
"output_pydantic": self.output_pydantic is not None,
|
||||
"images": self.images,
|
||||
"async_execution": self.async_execution,
|
||||
"quality_check": self.quality_check,
|
||||
"rerun": self.rerun
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +284,9 @@ class Workflow:
|
||||
memory_config: Optional[Dict[str, Any]] = None # Memory configuration
|
||||
planning: bool = False # Enable planning mode
|
||||
planning_llm: Optional[str] = None # LLM for planning
|
||||
reasoning: bool = False # Enable chain-of-thought reasoning
|
||||
verbose: bool = False # Enable verbose output
|
||||
stream: bool = True # Enable streaming responses (default True like PraisonAIAgents)
|
||||
|
||||
# Callbacks (like process="workflow")
|
||||
on_workflow_start: Optional[Callable[['Workflow', str], None]] = None # (workflow, input)
|
||||
@@ -294,7 +317,8 @@ class Workflow:
|
||||
self,
|
||||
input: str = "",
|
||||
llm: Optional[str] = None,
|
||||
verbose: bool = False
|
||||
verbose: bool = False,
|
||||
stream: bool = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run the workflow with the given input.
|
||||
@@ -318,12 +342,21 @@ class Workflow:
|
||||
input: The input text/prompt for the workflow
|
||||
llm: LLM model to use (default: gpt-4o-mini)
|
||||
verbose: Print step outputs
|
||||
stream: Enable streaming responses (default: use workflow's stream setting)
|
||||
|
||||
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"
|
||||
logger.debug(f"Workflow using model: {model} (llm={llm}, default_llm={self.default_llm})")
|
||||
|
||||
# Use workflow verbose setting if not overridden
|
||||
verbose = verbose or self.verbose
|
||||
|
||||
# Use workflow stream setting if not overridden
|
||||
if stream is None:
|
||||
stream = self.stream
|
||||
|
||||
# Add input to variables
|
||||
all_variables = {**self.variables, "input": input}
|
||||
@@ -341,6 +374,13 @@ class Workflow:
|
||||
except Exception as e:
|
||||
logger.error(f"on_workflow_start callback failed: {e}")
|
||||
|
||||
# Planning mode - create execution plan before running
|
||||
if self.planning:
|
||||
plan = self._create_plan(input, model, verbose)
|
||||
if plan and verbose:
|
||||
print(f"📋 Execution Plan: {plan}")
|
||||
all_variables["execution_plan"] = plan
|
||||
|
||||
# Process steps (may include Route, Parallel, Loop, Repeat)
|
||||
i = 0
|
||||
while i < len(self.steps):
|
||||
@@ -350,7 +390,7 @@ class Workflow:
|
||||
if isinstance(step, Route):
|
||||
# Decision-based routing
|
||||
route_result = self._execute_route(
|
||||
step, previous_output, input, all_variables, model, verbose
|
||||
step, previous_output, input, all_variables, model, verbose, stream
|
||||
)
|
||||
results.extend(route_result["steps"])
|
||||
previous_output = route_result["output"]
|
||||
@@ -361,7 +401,7 @@ class Workflow:
|
||||
elif isinstance(step, Parallel):
|
||||
# Parallel execution
|
||||
parallel_result = self._execute_parallel(
|
||||
step, previous_output, input, all_variables, model, verbose
|
||||
step, previous_output, input, all_variables, model, verbose, stream
|
||||
)
|
||||
results.extend(parallel_result["steps"])
|
||||
previous_output = parallel_result["output"]
|
||||
@@ -372,7 +412,7 @@ class Workflow:
|
||||
elif isinstance(step, Loop):
|
||||
# Loop over items
|
||||
loop_result = self._execute_loop(
|
||||
step, previous_output, input, all_variables, model, verbose
|
||||
step, previous_output, input, all_variables, model, verbose, stream
|
||||
)
|
||||
results.extend(loop_result["steps"])
|
||||
previous_output = loop_result["output"]
|
||||
@@ -383,7 +423,7 @@ class Workflow:
|
||||
elif isinstance(step, Repeat):
|
||||
# Repeat until condition
|
||||
repeat_result = self._execute_repeat(
|
||||
step, previous_output, input, all_variables, model, verbose
|
||||
step, previous_output, input, all_variables, model, verbose, stream
|
||||
)
|
||||
results.extend(repeat_result["steps"])
|
||||
previous_output = repeat_result["output"]
|
||||
@@ -456,29 +496,83 @@ class Workflow:
|
||||
output = str(result)
|
||||
|
||||
elif step.agent:
|
||||
# Direct agent
|
||||
output = step.agent.chat(step.action or input)
|
||||
# Direct agent with tools
|
||||
# Substitute variables in action
|
||||
action = step.action or input
|
||||
for key, value in all_variables.items():
|
||||
action = action.replace(f"{{{{{key}}}}}", str(value))
|
||||
if previous_output:
|
||||
if "{{previous_output}}" in action:
|
||||
action = action.replace("{{previous_output}}", str(previous_output))
|
||||
else:
|
||||
# Auto-append context if not explicitly referenced
|
||||
action = f"{action}\n\nContext from previous step:\n{previous_output}"
|
||||
action = action.replace("{{input}}", input)
|
||||
|
||||
# Add reasoning prompt if enabled
|
||||
if self.reasoning:
|
||||
action = f"Think step by step and reason through this task:\n\n{action}"
|
||||
|
||||
# Add validation feedback if retrying
|
||||
if validation_feedback:
|
||||
action = f"{action}\n\nPrevious attempt feedback: {validation_feedback}"
|
||||
|
||||
# Handle images if present
|
||||
step_images = getattr(step, 'images', None)
|
||||
if step_images:
|
||||
output = step.agent.chat(action, images=step_images, stream=stream)
|
||||
else:
|
||||
output = step.agent.chat(action, stream=stream)
|
||||
|
||||
# Handle output_pydantic if present
|
||||
output_pydantic = getattr(step, 'output_pydantic', None)
|
||||
if output_pydantic and output:
|
||||
try:
|
||||
# Try to parse output as Pydantic model
|
||||
if hasattr(output_pydantic, 'model_validate_json'):
|
||||
parsed = output_pydantic.model_validate_json(output)
|
||||
output = parsed.model_dump_json()
|
||||
except Exception as e:
|
||||
logger.debug(f"Pydantic parsing failed: {e}")
|
||||
|
||||
elif step.action:
|
||||
# Action with agent_config - create temporary agent
|
||||
from ..agent.agent import Agent
|
||||
config = step.agent_config or self.default_agent_config or {}
|
||||
|
||||
# Get tools from step or config
|
||||
step_tools = step.tools or config.get("tools", [])
|
||||
|
||||
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
|
||||
tools=step_tools if step_tools else None,
|
||||
verbose=verbose,
|
||||
reasoning=self.reasoning,
|
||||
stream=stream
|
||||
)
|
||||
# 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))
|
||||
if "{{previous_output}}" in action:
|
||||
action = action.replace("{{previous_output}}", str(previous_output))
|
||||
else:
|
||||
# Auto-append context if not explicitly referenced
|
||||
action = f"{action}\n\nContext from previous step:\n{previous_output}"
|
||||
action = action.replace("{{input}}", input)
|
||||
|
||||
# Add reasoning prompt if enabled
|
||||
if self.reasoning:
|
||||
action = f"Think step by step and reason through this task:\n\n{action}"
|
||||
|
||||
if validation_feedback:
|
||||
action = f"{action}\n\nPrevious attempt feedback: {validation_feedback}"
|
||||
output = temp_agent.chat(action)
|
||||
|
||||
output = temp_agent.chat(action, stream=stream)
|
||||
|
||||
except Exception as e:
|
||||
step_error = e
|
||||
@@ -526,6 +620,23 @@ class Workflow:
|
||||
except Exception as e:
|
||||
logger.error(f"on_step_complete callback failed: {e}")
|
||||
|
||||
# Handle output_file - save output to file
|
||||
if hasattr(step, 'output_file') and step.output_file and output:
|
||||
try:
|
||||
import os
|
||||
output_path = step.output_file
|
||||
# Substitute variables in path
|
||||
for key, value in all_variables.items():
|
||||
output_path = output_path.replace(f"{{{{{key}}}}}", str(value))
|
||||
# Create directory if needed
|
||||
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else ".", exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
f.write(str(output))
|
||||
if verbose:
|
||||
print(f"📁 Saved output to: {output_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save output to file: {e}")
|
||||
|
||||
# Store result
|
||||
results.append({
|
||||
"step": step.name,
|
||||
@@ -536,7 +647,7 @@ class Workflow:
|
||||
previous_output = output
|
||||
|
||||
if verbose:
|
||||
print(f"✅ {step.name}: {str(output)[:100]}...")
|
||||
print(f"✅ {step.name}: {str(output)}")
|
||||
|
||||
# Handle early stop
|
||||
if stop:
|
||||
@@ -569,6 +680,41 @@ class Workflow:
|
||||
|
||||
return final_result
|
||||
|
||||
async def astart(
|
||||
self,
|
||||
input: str = "",
|
||||
llm: Optional[str] = None,
|
||||
verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Async version of start() for async workflow execution.
|
||||
|
||||
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)
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Run the synchronous version in a thread pool
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self.run(input, llm, verbose)
|
||||
)
|
||||
return result
|
||||
|
||||
async def arun(
|
||||
self,
|
||||
input: str = "",
|
||||
llm: Optional[str] = None,
|
||||
verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Alias for astart() for backward compatibility."""
|
||||
return await self.astart(input, llm, verbose)
|
||||
|
||||
def _normalize_steps(self) -> List['WorkflowStep']:
|
||||
"""Convert mixed steps (Agent, function, WorkflowStep) to WorkflowStep list."""
|
||||
normalized = []
|
||||
@@ -598,21 +744,94 @@ class Workflow:
|
||||
|
||||
return normalized
|
||||
|
||||
def _create_plan(self, input: str, model: str, verbose: bool) -> Optional[str]:
|
||||
"""Create an execution plan for the workflow using LLM.
|
||||
|
||||
Args:
|
||||
input: The workflow input
|
||||
model: LLM model to use
|
||||
verbose: Print verbose output
|
||||
|
||||
Returns:
|
||||
Execution plan as string, or None if planning fails
|
||||
"""
|
||||
try:
|
||||
from ..agent.agent import Agent
|
||||
|
||||
# Describe the steps
|
||||
step_descriptions = []
|
||||
for i, step in enumerate(self.steps):
|
||||
if isinstance(step, WorkflowStep):
|
||||
step_descriptions.append(f"{i+1}. {step.name}: {step.description or step.action or 'Execute step'}")
|
||||
elif hasattr(step, 'name'):
|
||||
role = getattr(step, 'role', 'Agent')
|
||||
step_descriptions.append(f"{i+1}. {step.name} ({role})")
|
||||
elif callable(step):
|
||||
step_descriptions.append(f"{i+1}. {getattr(step, '__name__', 'function')}")
|
||||
elif isinstance(step, (Route, Parallel, Loop, Repeat)):
|
||||
step_descriptions.append(f"{i+1}. {type(step).__name__} pattern")
|
||||
else:
|
||||
step_descriptions.append(f"{i+1}. Step {i+1}")
|
||||
|
||||
steps_text = "\n".join(step_descriptions)
|
||||
|
||||
planning_prompt = f"""You are a workflow planner. Given the following workflow steps and input, create a brief execution plan.
|
||||
|
||||
Workflow: {self.name}
|
||||
Description: {self.description}
|
||||
|
||||
Steps:
|
||||
{steps_text}
|
||||
|
||||
Input: {input}
|
||||
|
||||
Create a brief execution plan (2-3 sentences) describing how to best accomplish this task using the available steps. Focus on the key objectives and any important considerations."""
|
||||
|
||||
planner = Agent(
|
||||
name="Planner",
|
||||
role="Workflow Planner",
|
||||
goal="Create efficient execution plans",
|
||||
llm=self.planning_llm or model,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
plan = planner.chat(planning_prompt)
|
||||
return plan
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Planning failed: {e}")
|
||||
return None
|
||||
|
||||
def _normalize_single_step(self, step: Any, index: int) -> 'WorkflowStep':
|
||||
"""Normalize a single step to WorkflowStep."""
|
||||
"""Normalize a single step to WorkflowStep.
|
||||
|
||||
Supports:
|
||||
- WorkflowStep objects (passed through)
|
||||
- Agent objects (wrapped with agent reference and tools)
|
||||
- Callable functions (wrapped as handler)
|
||||
- Strings (used as action)
|
||||
"""
|
||||
if isinstance(step, WorkflowStep):
|
||||
return step
|
||||
elif hasattr(step, 'chat'):
|
||||
# It's an Agent - wrap with agent reference and preserve tools
|
||||
agent_tools = getattr(step, 'tools', None)
|
||||
# Check for _yaml_action from YAML parser (canonical: action, alias: description)
|
||||
yaml_action = getattr(step, '_yaml_action', None)
|
||||
# Use yaml_action if set, otherwise fall back to previous_output/input
|
||||
default_action = "{{previous_output}}" if index > 0 else "{{input}}"
|
||||
action = yaml_action if yaml_action else default_action
|
||||
return WorkflowStep(
|
||||
name=getattr(step, 'name', f'agent_{index+1}'),
|
||||
agent=step,
|
||||
tools=agent_tools,
|
||||
action=action
|
||||
)
|
||||
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}',
|
||||
@@ -627,7 +846,8 @@ class Workflow:
|
||||
all_variables: Dict[str, Any],
|
||||
model: str,
|
||||
verbose: bool,
|
||||
index: int = 0
|
||||
index: int = 0,
|
||||
stream: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute a single step and return result."""
|
||||
normalized = self._normalize_single_step(step, index)
|
||||
@@ -656,7 +876,18 @@ class Workflow:
|
||||
output = f"Error: {e}"
|
||||
elif normalized.agent:
|
||||
try:
|
||||
output = normalized.agent.chat(normalized.action or input)
|
||||
action = normalized.action or input
|
||||
# Substitute variables
|
||||
for key, value in all_variables.items():
|
||||
action = action.replace(f"{{{{{key}}}}}", str(value))
|
||||
if previous_output:
|
||||
if "{{previous_output}}" in action:
|
||||
action = action.replace("{{previous_output}}", str(previous_output))
|
||||
else:
|
||||
# Auto-append context if not explicitly referenced
|
||||
action = f"{action}\n\nContext from previous step:\n{previous_output}"
|
||||
action = action.replace("{{input}}", input)
|
||||
output = normalized.agent.chat(action, stream=stream)
|
||||
except Exception as e:
|
||||
output = f"Error: {e}"
|
||||
elif normalized.action:
|
||||
@@ -668,19 +899,25 @@ class Workflow:
|
||||
role=config.get("role", "Assistant"),
|
||||
goal=config.get("goal", "Complete the task"),
|
||||
llm=config.get("llm", model),
|
||||
verbose=verbose
|
||||
verbose=verbose,
|
||||
stream=stream
|
||||
)
|
||||
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)
|
||||
if "{{previous_output}}" in action:
|
||||
action = action.replace("{{previous_output}}", str(previous_output))
|
||||
else:
|
||||
# Auto-append context if not explicitly referenced
|
||||
action = f"{action}\n\nContext from previous step:\n{previous_output}"
|
||||
|
||||
output = temp_agent.chat(action, stream=stream)
|
||||
except Exception as e:
|
||||
output = f"Error: {e}"
|
||||
|
||||
if verbose:
|
||||
print(f"✅ {normalized.name}: {str(output)[:100]}...")
|
||||
print(f"✅ {normalized.name}: {str(output)}")
|
||||
|
||||
return {
|
||||
"step": normalized.name,
|
||||
@@ -696,7 +933,8 @@ class Workflow:
|
||||
input: str,
|
||||
all_variables: Dict[str, Any],
|
||||
model: str,
|
||||
verbose: bool
|
||||
verbose: bool,
|
||||
stream: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute routing based on previous output."""
|
||||
results = []
|
||||
@@ -722,7 +960,7 @@ class Workflow:
|
||||
# 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
|
||||
step, output, input, all_variables, model, verbose, idx, stream=stream
|
||||
)
|
||||
results.append({"step": step_result["step"], "output": step_result["output"]})
|
||||
output = step_result["output"]
|
||||
@@ -740,7 +978,8 @@ class Workflow:
|
||||
input: str,
|
||||
all_variables: Dict[str, Any],
|
||||
model: str,
|
||||
verbose: bool
|
||||
verbose: bool,
|
||||
stream: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute steps in parallel (simulated with sequential for now)."""
|
||||
import concurrent.futures
|
||||
@@ -757,7 +996,7 @@ class Workflow:
|
||||
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
|
||||
step, previous_output, input, all_variables.copy(), model, False, idx, stream
|
||||
)
|
||||
futures.append((idx, future))
|
||||
|
||||
@@ -786,7 +1025,8 @@ class Workflow:
|
||||
input: str,
|
||||
all_variables: Dict[str, Any],
|
||||
model: str,
|
||||
verbose: bool
|
||||
verbose: bool,
|
||||
stream: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute step for each item in loop."""
|
||||
import csv
|
||||
@@ -826,7 +1066,7 @@ class Workflow:
|
||||
loop_vars["loop_index"] = idx
|
||||
|
||||
step_result = self._execute_single_step_internal(
|
||||
loop_step.step, previous_output, input, loop_vars, model, verbose, idx
|
||||
loop_step.step, previous_output, input, loop_vars, model, verbose, idx, stream=stream
|
||||
)
|
||||
results.append({"step": f"{step_result['step']}_{idx}", "output": step_result["output"]})
|
||||
outputs.append(step_result["output"])
|
||||
@@ -844,7 +1084,8 @@ class Workflow:
|
||||
input: str,
|
||||
all_variables: Dict[str, Any],
|
||||
model: str,
|
||||
verbose: bool
|
||||
verbose: bool,
|
||||
stream: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Repeat step until condition is met."""
|
||||
results = []
|
||||
@@ -855,7 +1096,7 @@ class Workflow:
|
||||
|
||||
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
|
||||
repeat_step.step, output, input, all_variables, model, verbose, iteration, stream=stream
|
||||
)
|
||||
results.append({"step": f"{step_result['step']}_{iteration}", "output": step_result["output"]})
|
||||
output = step_result["output"]
|
||||
@@ -1031,6 +1272,13 @@ class WorkflowManager:
|
||||
loop_var_pattern = re.compile(r'loop_var:\s*(\w+)', re.IGNORECASE)
|
||||
branch_pattern = re.compile(r'```branch\s*\n(.*?)\n```', re.DOTALL)
|
||||
|
||||
# New patterns for route, parallel, images, output_file
|
||||
route_pattern = re.compile(r'```route\s*\n(.*?)\n```', re.DOTALL)
|
||||
parallel_pattern = re.compile(r'```parallel\s*\n(.*?)\n```', re.DOTALL)
|
||||
images_pattern = re.compile(r'```images\s*\n(.*?)\n```', re.DOTALL)
|
||||
output_file_pattern = re.compile(r'output_file:\s*(.+)', re.IGNORECASE)
|
||||
repeat_pattern = re.compile(r'```repeat\s*\n(.*?)\n```', re.DOTALL)
|
||||
|
||||
# Find all step headers
|
||||
matches = list(step_pattern.finditer(body))
|
||||
|
||||
@@ -1106,6 +1354,41 @@ class WorkflowManager:
|
||||
if loop_var_match:
|
||||
loop_var = loop_var_match.group(1)
|
||||
|
||||
# Extract route pattern (same as branch_condition)
|
||||
route_match = route_pattern.search(step_content)
|
||||
if route_match and not branch_condition:
|
||||
branch_condition = self._parse_branch_condition(route_match.group(1).strip())
|
||||
|
||||
# Extract parallel pattern
|
||||
parallel_steps = None
|
||||
parallel_match = parallel_pattern.search(step_content)
|
||||
if parallel_match:
|
||||
parallel_str = parallel_match.group(1).strip()
|
||||
# Parse as list of step names
|
||||
parallel_steps = [s.strip().lstrip('- ').strip('"\'') for s in parallel_str.split('\n') if s.strip()]
|
||||
# Set next_steps to parallel steps if not already set
|
||||
if not next_steps:
|
||||
next_steps = parallel_steps
|
||||
|
||||
# Extract images
|
||||
images = None
|
||||
images_match = images_pattern.search(step_content)
|
||||
if images_match:
|
||||
images_str = images_match.group(1).strip()
|
||||
images = [s.strip() for s in images_str.split('\n') if s.strip()]
|
||||
|
||||
# Extract output_file
|
||||
output_file = None
|
||||
output_file_match = output_file_pattern.search(step_content)
|
||||
if output_file_match:
|
||||
output_file = output_file_match.group(1).strip()
|
||||
|
||||
# Extract repeat config
|
||||
repeat_config = None
|
||||
repeat_match = repeat_pattern.search(step_content)
|
||||
if repeat_match:
|
||||
repeat_config = self._parse_agent_config(repeat_match.group(1).strip())
|
||||
|
||||
# Get description (text before action block)
|
||||
description = step_content
|
||||
if action_match:
|
||||
@@ -1119,8 +1402,14 @@ class WorkflowManager:
|
||||
description = re.sub(r'next_steps:.*', '', description)
|
||||
description = re.sub(r'loop_over:.*', '', description)
|
||||
description = re.sub(r'loop_var:.*', '', description)
|
||||
description = re.sub(r'output_file:.*', '', description)
|
||||
description = description.strip()
|
||||
|
||||
# Set max_retries from repeat config
|
||||
max_retries = 3
|
||||
if repeat_config and 'max_iterations' in repeat_config:
|
||||
max_retries = int(repeat_config['max_iterations'])
|
||||
|
||||
steps.append(WorkflowStep(
|
||||
name=step_name,
|
||||
description=description,
|
||||
@@ -1134,7 +1423,10 @@ class WorkflowManager:
|
||||
next_steps=next_steps,
|
||||
branch_condition=branch_condition,
|
||||
loop_over=loop_over,
|
||||
loop_var=loop_var
|
||||
loop_var=loop_var,
|
||||
output_file=output_file,
|
||||
images=images,
|
||||
max_retries=max_retries
|
||||
))
|
||||
|
||||
return steps
|
||||
@@ -1340,6 +1632,118 @@ class WorkflowManager:
|
||||
# Update previous_output for next step
|
||||
variables["previous_output"] = output
|
||||
|
||||
def load_yaml(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
tool_registry: Optional[Dict[str, Callable]] = None
|
||||
) -> "Workflow":
|
||||
"""
|
||||
Load a workflow from a YAML file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the YAML workflow file
|
||||
tool_registry: Optional dictionary mapping tool names to callable functions
|
||||
|
||||
Returns:
|
||||
Workflow object ready for execution
|
||||
|
||||
Example:
|
||||
```python
|
||||
manager = WorkflowManager()
|
||||
workflow = manager.load_yaml("research_workflow.yaml")
|
||||
result = workflow.start("Research AI trends")
|
||||
```
|
||||
"""
|
||||
from .yaml_parser import YAMLWorkflowParser
|
||||
|
||||
parser = YAMLWorkflowParser(tool_registry=tool_registry)
|
||||
workflow = parser.parse_file(file_path)
|
||||
|
||||
# Store in manager's workflow cache
|
||||
workflow_name = workflow.name.lower().replace(' ', '_')
|
||||
self._workflows[workflow_name] = workflow
|
||||
|
||||
return workflow
|
||||
|
||||
def execute_yaml(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
input_data: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
tool_registry: Optional[Dict[str, Callable]] = None,
|
||||
verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Load and execute a YAML workflow file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the YAML workflow file
|
||||
input_data: Initial input for the workflow
|
||||
variables: Additional variables to merge with workflow variables
|
||||
tool_registry: Optional dictionary mapping tool names to callable functions
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Workflow execution results
|
||||
|
||||
Example:
|
||||
```python
|
||||
manager = WorkflowManager()
|
||||
result = manager.execute_yaml(
|
||||
"research_workflow.yaml",
|
||||
input_data="Research AI trends",
|
||||
variables={"topic": "Machine Learning"}
|
||||
)
|
||||
print(result["output"])
|
||||
```
|
||||
"""
|
||||
workflow = self.load_yaml(file_path, tool_registry=tool_registry)
|
||||
|
||||
# Merge additional variables
|
||||
if variables:
|
||||
workflow.variables.update(variables)
|
||||
|
||||
# Set verbose if specified
|
||||
if verbose:
|
||||
workflow.verbose = verbose
|
||||
|
||||
# Execute the workflow
|
||||
return workflow.start(input_data or "")
|
||||
|
||||
async def aexecute_yaml(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
input_data: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
tool_registry: Optional[Dict[str, Callable]] = None,
|
||||
verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Async version of execute_yaml.
|
||||
|
||||
Args:
|
||||
file_path: Path to the YAML workflow file
|
||||
input_data: Initial input for the workflow
|
||||
variables: Additional variables to merge with workflow variables
|
||||
tool_registry: Optional dictionary mapping tool names to callable functions
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Workflow execution results
|
||||
"""
|
||||
workflow = self.load_yaml(file_path, tool_registry=tool_registry)
|
||||
|
||||
# Merge additional variables
|
||||
if variables:
|
||||
workflow.variables.update(variables)
|
||||
|
||||
# Set verbose if specified
|
||||
if verbose:
|
||||
workflow.verbose = verbose
|
||||
|
||||
# Execute the workflow asynchronously
|
||||
return await workflow.astart(input_data or "")
|
||||
|
||||
def execute(
|
||||
self,
|
||||
workflow_name: str,
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
"""
|
||||
YAML Workflow Parser for PraisonAI Agents.
|
||||
|
||||
Parses YAML workflow files into Workflow objects with support for:
|
||||
- Agent definitions with planning, reasoning, tools
|
||||
- Workflow patterns: route(), parallel(), loop(), repeat()
|
||||
- Variables and template substitution
|
||||
- Callbacks and guardrails
|
||||
- Memory configuration
|
||||
"""
|
||||
|
||||
import yaml
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
from ..agent.agent import Agent
|
||||
from .workflows import Workflow, WorkflowStep, route, parallel, loop, repeat
|
||||
|
||||
|
||||
class YAMLWorkflowParser:
|
||||
"""
|
||||
Parser for YAML workflow files.
|
||||
|
||||
Converts YAML workflow definitions into Workflow objects that can be executed.
|
||||
|
||||
Example YAML format:
|
||||
```yaml
|
||||
name: My Workflow
|
||||
description: A multi-agent workflow
|
||||
|
||||
workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
|
||||
variables:
|
||||
topic: AI trends
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Research topics
|
||||
instructions: "Provide research findings"
|
||||
tools:
|
||||
- tavily_search
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research {{topic}}"
|
||||
- name: routing
|
||||
route:
|
||||
technical: [tech_agent]
|
||||
default: [general_agent]
|
||||
|
||||
callbacks:
|
||||
on_step_complete: log_step
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, tool_registry: Optional[Dict[str, Callable]] = None):
|
||||
"""
|
||||
Initialize the YAML workflow parser.
|
||||
|
||||
Args:
|
||||
tool_registry: Optional dictionary mapping tool names to callable functions
|
||||
"""
|
||||
self.tool_registry = tool_registry or {}
|
||||
self._agents: Dict[str, Agent] = {}
|
||||
self._callbacks: Dict[str, Callable] = {}
|
||||
|
||||
def parse_file(self, file_path: Union[str, Path]) -> Workflow:
|
||||
"""
|
||||
Parse a YAML workflow file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the YAML file
|
||||
|
||||
Returns:
|
||||
Workflow object ready for execution
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Workflow file not found: {file_path}")
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
yaml_content = f.read()
|
||||
|
||||
return self.parse_string(yaml_content)
|
||||
|
||||
def parse_string(self, yaml_content: str) -> Workflow:
|
||||
"""
|
||||
Parse a YAML workflow string.
|
||||
|
||||
Args:
|
||||
yaml_content: YAML content as string
|
||||
|
||||
Returns:
|
||||
Workflow object ready for execution
|
||||
"""
|
||||
data = yaml.safe_load(yaml_content)
|
||||
# Normalize to canonical format (accept both old and new field names)
|
||||
normalized_data = self._normalize_yaml_config(data)
|
||||
return self._parse_workflow_data(normalized_data)
|
||||
|
||||
def _normalize_yaml_config(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Normalize YAML config to canonical format.
|
||||
|
||||
Accepts both old (agents.yaml) and new (workflow.yaml) field names.
|
||||
This implements "Accept liberally, suggest canonically" principle.
|
||||
|
||||
Canonical names (preferred):
|
||||
- agents (not roles)
|
||||
- instructions (not backstory)
|
||||
- action (not description)
|
||||
- steps (not tasks)
|
||||
- name (not topic)
|
||||
|
||||
Args:
|
||||
data: Raw parsed YAML data
|
||||
|
||||
Returns:
|
||||
Normalized data with canonical field names
|
||||
"""
|
||||
if not data:
|
||||
return data
|
||||
|
||||
normalized = data.copy()
|
||||
|
||||
# 1. Normalize workflow name: topic -> name (only if topic is used as name, not as input)
|
||||
# Note: 'topic' is legacy, 'input' is canonical for workflow input data
|
||||
if 'topic' in normalized and 'name' not in normalized and 'input' not in normalized:
|
||||
# Legacy behavior: topic was used as both name and input
|
||||
normalized['name'] = normalized.get('topic', 'Unnamed Workflow')
|
||||
|
||||
# 2. Normalize workflow input: 'input' is canonical, 'topic' is alias
|
||||
# Priority: input > topic (input takes precedence if both exist)
|
||||
if 'input' not in normalized and 'topic' in normalized:
|
||||
normalized['input'] = normalized['topic']
|
||||
|
||||
# 3. Normalize container: roles -> agents
|
||||
# Note: _convert_roles_to_agents handles backstory -> instructions
|
||||
if 'roles' in normalized and 'agents' not in normalized:
|
||||
normalized['agents'] = self._convert_roles_to_agents(normalized['roles'])
|
||||
# Extract steps from tasks nested in roles if no steps defined
|
||||
if 'steps' not in normalized:
|
||||
normalized['steps'] = self._extract_steps_from_roles(normalized['roles'])
|
||||
|
||||
# 4. Normalize agent fields: backstory -> instructions (for agents section)
|
||||
if 'agents' in normalized:
|
||||
for agent_id, agent_config in normalized['agents'].items():
|
||||
if isinstance(agent_config, dict):
|
||||
# backstory -> instructions
|
||||
if 'backstory' in agent_config and 'instructions' not in agent_config:
|
||||
agent_config['instructions'] = agent_config['backstory']
|
||||
|
||||
# 5. Normalize step fields: description -> action
|
||||
if 'steps' in normalized:
|
||||
for step in normalized['steps']:
|
||||
if isinstance(step, dict):
|
||||
# description -> action
|
||||
if 'description' in step and 'action' not in step:
|
||||
step['action'] = step['description']
|
||||
|
||||
# Handle parallel steps
|
||||
if 'parallel' in step:
|
||||
for parallel_step in step['parallel']:
|
||||
if isinstance(parallel_step, dict):
|
||||
if 'description' in parallel_step and 'action' not in parallel_step:
|
||||
parallel_step['action'] = parallel_step['description']
|
||||
|
||||
return normalized
|
||||
|
||||
def _extract_steps_from_roles(self, roles: Dict[str, Dict]) -> List[Dict]:
|
||||
"""
|
||||
Extract steps from tasks nested within roles (agents.yaml format).
|
||||
|
||||
Args:
|
||||
roles: The roles section from agents.yaml
|
||||
|
||||
Returns:
|
||||
List of step definitions
|
||||
"""
|
||||
steps = []
|
||||
for role_id, role_config in roles.items():
|
||||
if 'tasks' in role_config:
|
||||
for task_id, task_config in role_config['tasks'].items():
|
||||
step = {
|
||||
'name': task_id,
|
||||
'agent': role_id,
|
||||
}
|
||||
# description -> action
|
||||
if 'description' in task_config:
|
||||
step['action'] = task_config['description']
|
||||
elif 'action' in task_config:
|
||||
step['action'] = task_config['action']
|
||||
|
||||
# Copy other task fields
|
||||
for field in ['expected_output', 'context', 'output_file',
|
||||
'output_json', 'create_directory', 'callback',
|
||||
'async_execution', 'guardrail', 'max_retries']:
|
||||
if field in task_config:
|
||||
step[field] = task_config[field]
|
||||
|
||||
steps.append(step)
|
||||
return steps
|
||||
|
||||
def _parse_workflow_data(self, data: Dict[str, Any]) -> Workflow:
|
||||
"""
|
||||
Parse workflow data dictionary into a Workflow object.
|
||||
|
||||
Args:
|
||||
data: Parsed YAML data dictionary
|
||||
|
||||
Returns:
|
||||
Workflow object
|
||||
"""
|
||||
# Extract workflow metadata
|
||||
name = data.get('name', 'Unnamed Workflow')
|
||||
description = data.get('description', '')
|
||||
|
||||
# Extract framework and process (for feature parity with agents.yaml)
|
||||
framework = data.get('framework', 'praisonai')
|
||||
process = data.get('process', 'sequential')
|
||||
manager_llm = data.get('manager_llm')
|
||||
|
||||
# Parse workflow configuration
|
||||
workflow_config = data.get('workflow', {})
|
||||
planning = workflow_config.get('planning', False)
|
||||
planning_llm = workflow_config.get('planning_llm')
|
||||
default_llm = workflow_config.get('default_llm')
|
||||
reasoning = workflow_config.get('reasoning', False)
|
||||
verbose = workflow_config.get('verbose', False)
|
||||
memory_config = workflow_config.get('memory_config')
|
||||
|
||||
# Parse variables
|
||||
variables = data.get('variables', {})
|
||||
|
||||
# Parse agents - support both 'agents' and 'roles' keys for backward compatibility
|
||||
agents_data = data.get('agents', {})
|
||||
if not agents_data and 'roles' in data:
|
||||
# Convert roles format to agents format
|
||||
agents_data = self._convert_roles_to_agents(data['roles'])
|
||||
self._agents = self._parse_agents(agents_data)
|
||||
|
||||
# Parse callbacks
|
||||
callbacks_data = data.get('callbacks', {})
|
||||
self._parse_callbacks(callbacks_data)
|
||||
|
||||
# Parse steps
|
||||
steps_data = data.get('steps', [])
|
||||
steps = self._parse_steps(steps_data)
|
||||
|
||||
# Create workflow
|
||||
workflow = Workflow(
|
||||
name=name,
|
||||
steps=steps,
|
||||
variables=variables,
|
||||
planning=planning,
|
||||
planning_llm=planning_llm,
|
||||
default_llm=default_llm,
|
||||
reasoning=reasoning,
|
||||
verbose=verbose,
|
||||
memory_config=memory_config,
|
||||
on_workflow_start=self._callbacks.get('on_workflow_start'),
|
||||
on_step_complete=self._callbacks.get('on_step_complete'),
|
||||
on_workflow_complete=self._callbacks.get('on_workflow_complete'),
|
||||
)
|
||||
|
||||
# Store additional attributes for feature parity with agents.yaml
|
||||
workflow.description = description
|
||||
workflow.framework = framework
|
||||
workflow.process = process
|
||||
workflow.manager_llm = manager_llm
|
||||
|
||||
# Store workflow input (from 'input' or 'topic' field)
|
||||
# This is the default input passed to workflow.start() if no input is provided
|
||||
workflow.default_input = data.get('input', data.get('topic', ''))
|
||||
|
||||
return workflow
|
||||
|
||||
def _convert_roles_to_agents(self, roles: Dict[str, Dict]) -> Dict[str, Dict]:
|
||||
"""
|
||||
Convert agents.yaml 'roles' format to workflow 'agents' format.
|
||||
|
||||
This enables backward compatibility with the existing agents.yaml format.
|
||||
|
||||
Mapping:
|
||||
- backstory -> instructions
|
||||
- role -> role
|
||||
- goal -> goal
|
||||
- tools -> tools
|
||||
- llm -> llm
|
||||
- tasks -> (used to auto-generate steps if no steps defined)
|
||||
|
||||
Args:
|
||||
roles: The roles section from agents.yaml
|
||||
|
||||
Returns:
|
||||
Converted agents dictionary in workflow format
|
||||
"""
|
||||
agents = {}
|
||||
for role_id, role_config in roles.items():
|
||||
agent = {
|
||||
'name': role_config.get('role', role_id),
|
||||
'role': role_config.get('role', role_id),
|
||||
'goal': role_config.get('goal', ''),
|
||||
'instructions': role_config.get('backstory', ''),
|
||||
}
|
||||
|
||||
# Copy optional fields
|
||||
if 'llm' in role_config:
|
||||
llm_config = role_config['llm']
|
||||
if isinstance(llm_config, dict):
|
||||
agent['llm'] = llm_config.get('model', 'gpt-4o-mini')
|
||||
else:
|
||||
agent['llm'] = llm_config
|
||||
|
||||
if 'tools' in role_config:
|
||||
agent['tools'] = [t for t in role_config['tools'] if t]
|
||||
|
||||
if 'verbose' in role_config:
|
||||
agent['verbose'] = role_config['verbose']
|
||||
|
||||
if 'max_iter' in role_config:
|
||||
agent['max_iter'] = role_config['max_iter']
|
||||
|
||||
if 'planning' in role_config:
|
||||
agent['planning'] = role_config['planning']
|
||||
|
||||
if 'reasoning' in role_config:
|
||||
agent['reasoning'] = role_config['reasoning']
|
||||
|
||||
agents[role_id] = agent
|
||||
|
||||
return agents
|
||||
|
||||
def _parse_agents(self, agents_data: Dict[str, Dict]) -> Dict[str, Agent]:
|
||||
"""
|
||||
Parse agent definitions from YAML.
|
||||
|
||||
Args:
|
||||
agents_data: Dictionary of agent definitions
|
||||
|
||||
Returns:
|
||||
Dictionary mapping agent names to Agent objects
|
||||
"""
|
||||
agents = {}
|
||||
|
||||
for agent_id, agent_config in agents_data.items():
|
||||
agent = self._create_agent(agent_id, agent_config)
|
||||
agents[agent_id] = agent
|
||||
|
||||
return agents
|
||||
|
||||
def _create_agent(self, agent_id: str, config: Dict[str, Any]) -> Agent:
|
||||
"""
|
||||
Create an Agent from configuration.
|
||||
|
||||
Args:
|
||||
agent_id: Identifier for the agent
|
||||
config: Agent configuration dictionary
|
||||
|
||||
Returns:
|
||||
Agent object
|
||||
"""
|
||||
# Extract agent parameters
|
||||
name = config.get('name', agent_id)
|
||||
role = config.get('role', 'Assistant')
|
||||
goal = config.get('goal', '')
|
||||
instructions = config.get('instructions', '')
|
||||
backstory = config.get('backstory', '')
|
||||
|
||||
# LLM configuration
|
||||
llm = config.get('llm')
|
||||
|
||||
# Tools
|
||||
tools_config = config.get('tools', [])
|
||||
tools = self._resolve_tools(tools_config)
|
||||
|
||||
# Advanced parameters (feature parity with agents.yaml)
|
||||
planning = config.get('planning', False)
|
||||
reasoning = config.get('reasoning', False)
|
||||
verbose = config.get('verbose', False)
|
||||
allow_delegation = config.get('allow_delegation', False)
|
||||
max_iter = config.get('max_iter')
|
||||
cache = config.get('cache', True)
|
||||
|
||||
# Additional agents.yaml fields
|
||||
function_calling_llm = config.get('function_calling_llm')
|
||||
max_rpm = config.get('max_rpm')
|
||||
max_execution_time = config.get('max_execution_time')
|
||||
reflect_llm = config.get('reflect_llm')
|
||||
min_reflect = config.get('min_reflect')
|
||||
max_reflect = config.get('max_reflect')
|
||||
system_template = config.get('system_template')
|
||||
prompt_template = config.get('prompt_template')
|
||||
response_template = config.get('response_template')
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name=name,
|
||||
role=role,
|
||||
goal=goal,
|
||||
instructions=instructions,
|
||||
backstory=backstory,
|
||||
llm=llm,
|
||||
tools=tools if tools else None,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
# Store additional attributes for later use
|
||||
agent._yaml_planning = planning
|
||||
agent._yaml_reasoning = reasoning
|
||||
agent._yaml_allow_delegation = allow_delegation
|
||||
agent._yaml_max_iter = max_iter
|
||||
agent._yaml_cache = cache
|
||||
|
||||
# Store additional agents.yaml fields for feature parity
|
||||
agent._yaml_function_calling_llm = function_calling_llm
|
||||
agent._yaml_max_rpm = max_rpm
|
||||
agent._yaml_max_execution_time = max_execution_time
|
||||
agent._yaml_reflect_llm = reflect_llm
|
||||
agent._yaml_min_reflect = min_reflect
|
||||
agent._yaml_max_reflect = max_reflect
|
||||
agent._yaml_system_template = system_template
|
||||
agent._yaml_prompt_template = prompt_template
|
||||
agent._yaml_response_template = response_template
|
||||
|
||||
return agent
|
||||
|
||||
def _resolve_tools(self, tools_config: List[str]) -> List[Callable]:
|
||||
"""
|
||||
Resolve tool names to callable functions.
|
||||
|
||||
Args:
|
||||
tools_config: List of tool names
|
||||
|
||||
Returns:
|
||||
List of callable tool functions
|
||||
"""
|
||||
tools = []
|
||||
for tool_name in tools_config:
|
||||
if tool_name in self.tool_registry:
|
||||
tools.append(self.tool_registry[tool_name])
|
||||
# If tool not in registry, skip it (will be resolved later or is a string reference)
|
||||
return tools
|
||||
|
||||
def _parse_callbacks(self, callbacks_data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Parse callback definitions.
|
||||
|
||||
Args:
|
||||
callbacks_data: Dictionary of callback name to function name
|
||||
"""
|
||||
# For now, store callback names - they can be resolved later
|
||||
# In a full implementation, these would be resolved to actual functions
|
||||
for callback_name, func_name in callbacks_data.items():
|
||||
# Store as None for now - actual resolution happens at runtime
|
||||
self._callbacks[callback_name] = None
|
||||
|
||||
def _parse_steps(self, steps_data: List[Dict]) -> List:
|
||||
"""
|
||||
Parse workflow steps from YAML.
|
||||
|
||||
Args:
|
||||
steps_data: List of step definitions
|
||||
|
||||
Returns:
|
||||
List of workflow steps (Agent, WorkflowStep, or pattern objects)
|
||||
"""
|
||||
steps = []
|
||||
|
||||
for step_data in steps_data:
|
||||
step = self._parse_single_step(step_data)
|
||||
if step is not None:
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
def _parse_single_step(self, step_data: Dict) -> Any:
|
||||
"""
|
||||
Parse a single step definition.
|
||||
|
||||
Args:
|
||||
step_data: Step definition dictionary
|
||||
|
||||
Returns:
|
||||
Step object (Agent, WorkflowStep, or pattern)
|
||||
"""
|
||||
# Check for pattern types
|
||||
if 'route' in step_data:
|
||||
return self._parse_route_step(step_data)
|
||||
elif 'parallel' in step_data:
|
||||
return self._parse_parallel_step(step_data)
|
||||
elif 'loop' in step_data:
|
||||
return self._parse_loop_step(step_data)
|
||||
elif 'repeat' in step_data:
|
||||
return self._parse_repeat_step(step_data)
|
||||
elif 'agent' in step_data:
|
||||
return self._parse_agent_step(step_data)
|
||||
else:
|
||||
# Generic step
|
||||
return self._parse_generic_step(step_data)
|
||||
|
||||
def _parse_agent_step(self, step_data: Dict) -> Agent:
|
||||
"""
|
||||
Parse an agent step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'agent' key
|
||||
|
||||
Returns:
|
||||
Agent object
|
||||
"""
|
||||
agent_id = step_data['agent']
|
||||
|
||||
if agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store step name if provided
|
||||
step_name = step_data.get('name')
|
||||
if step_name:
|
||||
agent._yaml_step_name = step_name
|
||||
|
||||
# If there's an action, we need to handle it
|
||||
action = step_data.get('action')
|
||||
if action:
|
||||
# Store action as task for the agent
|
||||
agent._yaml_action = action
|
||||
|
||||
# Handle guardrail
|
||||
guardrail = step_data.get('guardrail')
|
||||
if guardrail:
|
||||
agent._yaml_guardrail = guardrail
|
||||
|
||||
# Handle max_retries
|
||||
max_retries = step_data.get('max_retries')
|
||||
if max_retries:
|
||||
agent._yaml_max_retries = max_retries
|
||||
|
||||
# Handle expected_output (feature parity with agents.yaml tasks)
|
||||
expected_output = step_data.get('expected_output')
|
||||
if expected_output:
|
||||
agent._yaml_expected_output = expected_output
|
||||
|
||||
# Handle context/dependencies (feature parity with agents.yaml tasks)
|
||||
context = step_data.get('context')
|
||||
if context:
|
||||
agent._yaml_context = context
|
||||
|
||||
# Handle output_file
|
||||
output_file = step_data.get('output_file')
|
||||
if output_file:
|
||||
agent._yaml_output_file = output_file
|
||||
|
||||
# Handle output_json (structured output)
|
||||
output_json = step_data.get('output_json')
|
||||
if output_json:
|
||||
agent._yaml_output_json = output_json
|
||||
|
||||
# Handle output_pydantic
|
||||
output_pydantic = step_data.get('output_pydantic')
|
||||
if output_pydantic:
|
||||
agent._yaml_output_pydantic = output_pydantic
|
||||
|
||||
# Handle create_directory
|
||||
create_directory = step_data.get('create_directory')
|
||||
if create_directory is not None:
|
||||
agent._yaml_create_directory = create_directory
|
||||
|
||||
# Handle callback
|
||||
callback = step_data.get('callback')
|
||||
if callback:
|
||||
agent._yaml_callback = callback
|
||||
|
||||
# Handle async_execution
|
||||
async_execution = step_data.get('async_execution')
|
||||
if async_execution is not None:
|
||||
agent._yaml_async_execution = async_execution
|
||||
|
||||
return agent
|
||||
else:
|
||||
raise ValueError(f"Agent '{agent_id}' not defined in agents section")
|
||||
|
||||
def _parse_route_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a route pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'route' key
|
||||
|
||||
Returns:
|
||||
Route pattern object
|
||||
"""
|
||||
route_config = step_data['route']
|
||||
|
||||
# Build routes dictionary with resolved agents
|
||||
routes = {}
|
||||
for route_key, agent_ids in route_config.items():
|
||||
if isinstance(agent_ids, list):
|
||||
routes[route_key] = [self._agents[aid] for aid in agent_ids if aid in self._agents]
|
||||
else:
|
||||
if agent_ids in self._agents:
|
||||
routes[route_key] = [self._agents[agent_ids]]
|
||||
|
||||
return route(routes)
|
||||
|
||||
def _parse_parallel_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a parallel pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'parallel' key
|
||||
|
||||
Returns:
|
||||
Parallel pattern object
|
||||
"""
|
||||
parallel_config = step_data['parallel']
|
||||
|
||||
# Build list of parallel steps
|
||||
parallel_steps = []
|
||||
for item in parallel_config:
|
||||
if 'agent' in item:
|
||||
agent_id = item['agent']
|
||||
if agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
# Store action if present
|
||||
if 'action' in item:
|
||||
agent._yaml_action = item['action']
|
||||
parallel_steps.append(agent)
|
||||
|
||||
return parallel(parallel_steps)
|
||||
|
||||
def _parse_loop_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a loop pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'loop' key
|
||||
|
||||
Returns:
|
||||
Loop pattern object
|
||||
"""
|
||||
loop_config = step_data['loop']
|
||||
agent_id = step_data.get('agent')
|
||||
|
||||
if agent_id and agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store action if present
|
||||
if 'action' in step_data:
|
||||
agent._yaml_action = step_data['action']
|
||||
|
||||
# Get loop parameters
|
||||
over = loop_config.get('over')
|
||||
from_csv = loop_config.get('from_csv')
|
||||
|
||||
return loop(agent, over=over, from_csv=from_csv)
|
||||
|
||||
raise ValueError("Loop step requires an agent")
|
||||
|
||||
def _parse_repeat_step(self, step_data: Dict) -> Dict:
|
||||
"""
|
||||
Parse a repeat pattern step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition with 'repeat' key
|
||||
|
||||
Returns:
|
||||
Repeat pattern object
|
||||
"""
|
||||
repeat_config = step_data['repeat']
|
||||
agent_id = step_data.get('agent')
|
||||
|
||||
if agent_id and agent_id in self._agents:
|
||||
agent = self._agents[agent_id]
|
||||
|
||||
# Store action if present
|
||||
if 'action' in step_data:
|
||||
agent._yaml_action = step_data['action']
|
||||
|
||||
# Get repeat parameters
|
||||
until = repeat_config.get('until')
|
||||
max_iterations = repeat_config.get('max_iterations', 5)
|
||||
|
||||
# Create condition function from 'until' string
|
||||
if isinstance(until, str):
|
||||
condition = self._create_condition_from_string(until)
|
||||
else:
|
||||
condition = until
|
||||
|
||||
return repeat(agent, until=condition, max_iterations=max_iterations)
|
||||
|
||||
raise ValueError("Repeat step requires an agent")
|
||||
|
||||
def _create_condition_from_string(self, condition_str: str) -> Callable:
|
||||
"""
|
||||
Create a condition function from a string.
|
||||
|
||||
Args:
|
||||
condition_str: Condition string (e.g., "approved", "comprehensive")
|
||||
|
||||
Returns:
|
||||
Callable condition function
|
||||
"""
|
||||
def condition(ctx) -> bool:
|
||||
result = str(ctx.previous_result).lower()
|
||||
return condition_str.lower() in result
|
||||
return condition
|
||||
|
||||
def _parse_generic_step(self, step_data: Dict) -> WorkflowStep:
|
||||
"""
|
||||
Parse a generic workflow step.
|
||||
|
||||
Args:
|
||||
step_data: Step definition dictionary
|
||||
|
||||
Returns:
|
||||
WorkflowStep object
|
||||
"""
|
||||
name = step_data.get('name', 'step')
|
||||
action = step_data.get('action', '')
|
||||
|
||||
return WorkflowStep(
|
||||
name=name,
|
||||
action=action,
|
||||
)
|
||||
|
||||
def register_tool(self, name: str, tool: Callable) -> None:
|
||||
"""
|
||||
Register a tool for use in workflows.
|
||||
|
||||
Args:
|
||||
name: Tool name as referenced in YAML
|
||||
tool: Callable tool function
|
||||
"""
|
||||
self.tool_registry[name] = tool
|
||||
|
||||
def register_callback(self, name: str, callback: Callable) -> None:
|
||||
"""
|
||||
Register a callback function.
|
||||
|
||||
Args:
|
||||
name: Callback name as referenced in YAML
|
||||
callback: Callable callback function
|
||||
"""
|
||||
self._callbacks[name] = callback
|
||||
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.0"
|
||||
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" }
|
||||
|
||||
@@ -493,17 +493,22 @@ class TestAgentIntegration:
|
||||
|
||||
def test_agent_store_memory(self):
|
||||
"""Test Agent store_memory method."""
|
||||
import uuid
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Use unique user_id to avoid stale data from previous test runs
|
||||
unique_user_id = f"test_user_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
memory=True
|
||||
memory=True,
|
||||
user_id=unique_user_id
|
||||
)
|
||||
|
||||
agent.store_memory("User prefers dark mode", memory_type="short_term")
|
||||
|
||||
items = agent._memory_instance.get_short_term()
|
||||
assert len(items) == 1
|
||||
assert len(items) == 1, f"Expected 1 item, got {len(items)} items"
|
||||
assert items[0].content == "User prefers dark mode"
|
||||
|
||||
def test_agent_get_memory_context(self):
|
||||
@@ -658,10 +663,12 @@ class TestMultiAgentMemorySharing:
|
||||
|
||||
def test_agents_share_memory_with_same_user_id(self):
|
||||
"""Test that agents with same user_id share memory."""
|
||||
import uuid
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
user_id = "shared_user"
|
||||
# Use unique user_id to avoid stale data from previous test runs
|
||||
user_id = f"shared_user_{uuid.uuid4().hex[:8]}"
|
||||
base_path = f"{tmpdir}/memory"
|
||||
|
||||
# Agent 1 stores memories
|
||||
@@ -676,9 +683,9 @@ class TestMultiAgentMemorySharing:
|
||||
agent1._memory_instance.add_entity("Alice", "person", {"role": "developer"})
|
||||
|
||||
# Verify Agent1 stored memories
|
||||
assert len(agent1._memory_instance.get_short_term()) == 1
|
||||
assert len(agent1._memory_instance.get_long_term()) == 1
|
||||
assert len(agent1._memory_instance.get_all_entities()) == 1
|
||||
assert len(agent1._memory_instance.get_short_term()) == 1, f"Expected 1 short_term, got {len(agent1._memory_instance.get_short_term())}"
|
||||
assert len(agent1._memory_instance.get_long_term()) == 1, f"Expected 1 long_term, got {len(agent1._memory_instance.get_long_term())}"
|
||||
assert len(agent1._memory_instance.get_all_entities()) == 1, f"Expected 1 entity, got {len(agent1._memory_instance.get_all_entities())}"
|
||||
|
||||
# Agent 2 with same user_id should load memories
|
||||
agent2 = Agent(
|
||||
|
||||
@@ -6,6 +6,8 @@ The agent should:
|
||||
3. Return the final result (200)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def get_stock_price(company_name: str) -> str:
|
||||
@@ -35,41 +37,60 @@ def multiply(a: int, b: int) -> int:
|
||||
print(f"[Tool Called] multiply({a}, {b})")
|
||||
return a * b
|
||||
|
||||
# Test with Gemini
|
||||
print("=" * 60)
|
||||
print("Testing with Gemini model")
|
||||
print("=" * 60)
|
||||
|
||||
agent_gemini = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gemini/gemini-2.5-pro",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_sequential_tool_calling_gpt():
|
||||
"""Test sequential tool calling with GPT model"""
|
||||
print("=" * 60)
|
||||
print("Testing with GPT model")
|
||||
print("=" * 60)
|
||||
|
||||
result_gemini = agent_gemini.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (Gemini): {result_gemini}")
|
||||
agent_gpt = Agent(
|
||||
instructions="You are a helpful assistant. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Test with GPT-4
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing with GPT-4 model")
|
||||
print("=" * 60)
|
||||
result = agent_gpt.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (GPT): {result}")
|
||||
|
||||
# Verify result
|
||||
assert result, "GPT returned empty result"
|
||||
print(f"GPT result contains '200': {'200' in str(result)}")
|
||||
|
||||
agent_gpt4 = Agent(
|
||||
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gpt-5-nano",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("GOOGLE_API_KEY"),
|
||||
reason="GOOGLE_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_sequential_tool_calling_gemini():
|
||||
"""Test sequential tool calling with Gemini model"""
|
||||
print("=" * 60)
|
||||
print("Testing with Gemini model")
|
||||
print("=" * 60)
|
||||
|
||||
result_gpt4 = agent_gpt4.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (GPT-4): {result_gpt4}")
|
||||
agent_gemini = Agent(
|
||||
instructions="You are a helpful assistant. When asked to multiply a stock price, first get the stock price, then multiply it.",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
tools=[get_stock_price, multiply],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Verify results
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Results Summary")
|
||||
print("=" * 60)
|
||||
print(f"Gemini result contains '200': {'200' in str(result_gemini) if result_gemini else False}")
|
||||
print(f"GPT-4 result contains '200': {'200' in str(result_gpt4) if result_gpt4 else False}")
|
||||
print(f"Gemini returned empty: {not result_gemini or result_gemini == ''}")
|
||||
print(f"GPT-4 returned empty: {not result_gpt4 or result_gpt4 == ''}")
|
||||
result = agent_gemini.start("what is the stock price of Google? multiply the Google stock price with 2")
|
||||
print(f"\nFinal Result (Gemini): {result}")
|
||||
|
||||
# Verify result
|
||||
assert result, "Gemini returned empty result"
|
||||
print(f"Gemini result contains '200': {'200' in str(result)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests manually
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
test_sequential_tool_calling_gpt()
|
||||
if os.environ.get("GOOGLE_API_KEY"):
|
||||
test_sequential_tool_calling_gemini()
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Test LLM class directly to verify self-reflection fix"""
|
||||
|
||||
from praisonaiagents.llm import LLM
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "2 + 2", "15 * 23")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_llm_direct():
|
||||
"""Test LLM class directly with self-reflection and tools"""
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Test-Driven Development tests for Multiple MCP Tools support.
|
||||
|
||||
This test file verifies that:
|
||||
1. Single MCP instance works (baseline)
|
||||
2. Multiple MCP instances in a list work for tool definition conversion
|
||||
3. Multiple MCP instances in a list work for tool execution
|
||||
4. Mixed tools (MCP + regular functions) work together
|
||||
|
||||
Uses mcp-server-time as the test MCP server (simple, no API keys needed).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
# Add the package to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def time_mcp():
|
||||
"""Create a Time MCP server instance."""
|
||||
return MCP("uvx mcp-server-time", timeout=30)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_with_tools():
|
||||
"""Create a mock MCP instance with predefined tools."""
|
||||
mock = Mock(spec=MCP)
|
||||
mock.is_sse = False
|
||||
mock.is_http_stream = False
|
||||
mock.is_websocket = False
|
||||
|
||||
# Create mock runner with tools
|
||||
mock.runner = Mock()
|
||||
mock_tool1 = Mock()
|
||||
mock_tool1.name = "get_current_time"
|
||||
mock_tool1.description = "Get current time in a timezone"
|
||||
mock_tool1.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {"type": "string", "description": "IANA timezone name"}
|
||||
},
|
||||
"required": ["timezone"]
|
||||
}
|
||||
|
||||
mock_tool2 = Mock()
|
||||
mock_tool2.name = "convert_time"
|
||||
mock_tool2.description = "Convert time between timezones"
|
||||
mock_tool2.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source_timezone": {"type": "string"},
|
||||
"time": {"type": "string"},
|
||||
"target_timezone": {"type": "string"}
|
||||
},
|
||||
"required": ["source_timezone", "time", "target_timezone"]
|
||||
}
|
||||
|
||||
mock.runner.tools = [mock_tool1, mock_tool2]
|
||||
mock._tools = []
|
||||
|
||||
# Mock to_openai_tool method
|
||||
def to_openai_tool():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"description": "Get current time in a timezone",
|
||||
"parameters": mock_tool1.inputSchema
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "convert_time",
|
||||
"description": "Convert time between timezones",
|
||||
"parameters": mock_tool2.inputSchema
|
||||
}
|
||||
}
|
||||
]
|
||||
mock.to_openai_tool = to_openai_tool
|
||||
|
||||
# Mock __iter__ to return tool functions
|
||||
def get_current_time(timezone: str) -> str:
|
||||
return f"Time in {timezone}: 12:00"
|
||||
def convert_time(source_timezone: str, time: str, target_timezone: str) -> str:
|
||||
return f"Converted {time} from {source_timezone} to {target_timezone}"
|
||||
get_current_time.__name__ = "get_current_time"
|
||||
convert_time.__name__ = "convert_time"
|
||||
mock._tools = [get_current_time, convert_time]
|
||||
mock.__iter__ = lambda self: iter(self._tools)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_filesystem():
|
||||
"""Create a mock MCP instance simulating filesystem server."""
|
||||
mock = Mock(spec=MCP)
|
||||
mock.is_sse = False
|
||||
mock.is_http_stream = False
|
||||
mock.is_websocket = False
|
||||
|
||||
mock.runner = Mock()
|
||||
mock_tool = Mock()
|
||||
mock_tool.name = "read_file"
|
||||
mock_tool.description = "Read a file"
|
||||
mock_tool.inputSchema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
mock.runner.tools = [mock_tool]
|
||||
mock._tools = []
|
||||
|
||||
def to_openai_tool():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": mock_tool.inputSchema
|
||||
}
|
||||
}
|
||||
]
|
||||
mock.to_openai_tool = to_openai_tool
|
||||
|
||||
def read_file(path: str) -> str:
|
||||
return f"Contents of {path}"
|
||||
read_file.__name__ = "read_file"
|
||||
mock._tools = [read_file]
|
||||
mock.__iter__ = lambda self: iter(self._tools)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
def sample_function(query: str) -> str:
|
||||
"""A sample regular function tool."""
|
||||
return f"Result for: {query}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 1: Single MCP Instance (Baseline)
|
||||
# ============================================================================
|
||||
|
||||
class TestSingleMCPInstance:
|
||||
"""Tests for single MCP instance - baseline functionality."""
|
||||
|
||||
def test_single_mcp_creates_tools(self, mock_mcp_with_tools):
|
||||
"""Test that a single MCP instance properly exposes tools."""
|
||||
tools = mock_mcp_with_tools.to_openai_tool()
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) == 2
|
||||
assert tools[0]["function"]["name"] == "get_current_time"
|
||||
assert tools[1]["function"]["name"] == "convert_time"
|
||||
|
||||
def test_single_mcp_is_iterable(self, mock_mcp_with_tools):
|
||||
"""Test that MCP instance is iterable."""
|
||||
tools_list = mock_mcp_with_tools._tools
|
||||
assert len(tools_list) == 2
|
||||
assert callable(tools_list[0])
|
||||
assert callable(tools_list[1])
|
||||
|
||||
def test_agent_accepts_single_mcp(self, mock_mcp_with_tools):
|
||||
"""Test that Agent accepts a single MCP instance as tools."""
|
||||
# This should not raise an error
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=mock_mcp_with_tools
|
||||
)
|
||||
# Agent wraps tools in a list if not already a list
|
||||
assert mock_mcp_with_tools in agent.tools or agent.tools == mock_mcp_with_tools
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 2: Multiple MCP Instances - Tool Definition Conversion
|
||||
# ============================================================================
|
||||
|
||||
class TestMultipleMCPToolDefinition:
|
||||
"""Tests for multiple MCP instances - tool definition conversion."""
|
||||
|
||||
def test_agent_accepts_multiple_mcp_in_list(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that Agent accepts multiple MCP instances in a list."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
assert isinstance(agent.tools, list)
|
||||
assert len(agent.tools) == 2
|
||||
|
||||
def test_format_tools_handles_multiple_mcp(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that _format_tools_for_llm handles multiple MCP instances."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Call the internal method to format tools
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
|
||||
# Should have 3 tools total (2 from time + 1 from filesystem)
|
||||
assert isinstance(formatted, list)
|
||||
assert len(formatted) == 3
|
||||
|
||||
# Check tool names
|
||||
tool_names = [t["function"]["name"] for t in formatted]
|
||||
assert "get_current_time" in tool_names
|
||||
assert "convert_time" in tool_names
|
||||
assert "read_file" in tool_names
|
||||
|
||||
def test_system_prompt_includes_all_mcp_tools(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that system prompt includes tools from all MCP instances."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
system_prompt = agent._build_system_prompt(agent.tools)
|
||||
|
||||
# Should mention all tools
|
||||
assert "get_current_time" in system_prompt
|
||||
assert "convert_time" in system_prompt
|
||||
assert "read_file" in system_prompt
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 3: Multiple MCP Instances - Tool Execution
|
||||
# ============================================================================
|
||||
|
||||
class TestMultipleMCPToolExecution:
|
||||
"""Tests for multiple MCP instances - tool execution."""
|
||||
|
||||
def test_execute_tool_finds_mcp_tool_in_list(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that execute_tool_call finds tools from MCP instances in a list."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Mock the runner.call_tool method
|
||||
mock_mcp_with_tools.runner.call_tool = Mock(return_value='{"timezone": "UTC", "datetime": "2024-01-01T12:00:00Z"}')
|
||||
|
||||
# This should find and execute the tool from the first MCP instance
|
||||
result = agent.execute_tool("get_current_time", {"timezone": "UTC"})
|
||||
|
||||
# Verify the tool was called
|
||||
mock_mcp_with_tools.runner.call_tool.assert_called_once_with("get_current_time", {"timezone": "UTC"})
|
||||
|
||||
def test_execute_tool_finds_tool_from_second_mcp(self, mock_mcp_with_tools, mock_mcp_filesystem):
|
||||
"""Test that execute_tool_call finds tools from the second MCP instance."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, mock_mcp_filesystem]
|
||||
)
|
||||
|
||||
# Mock the runner.call_tool method
|
||||
mock_mcp_filesystem.runner.call_tool = Mock(return_value='Contents of /test/file.txt')
|
||||
|
||||
# This should find and execute the tool from the second MCP instance
|
||||
result = agent.execute_tool("read_file", {"path": "/test/file.txt"})
|
||||
|
||||
# Verify the tool was called
|
||||
mock_mcp_filesystem.runner.call_tool.assert_called_once_with("read_file", {"path": "/test/file.txt"})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test 4: Mixed Tools (MCP + Regular Functions)
|
||||
# ============================================================================
|
||||
|
||||
class TestMixedTools:
|
||||
"""Tests for mixed tools - MCP instances and regular functions."""
|
||||
|
||||
def test_agent_accepts_mixed_tools(self, mock_mcp_with_tools):
|
||||
"""Test that Agent accepts a mix of MCP instances and regular functions."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
assert isinstance(agent.tools, list)
|
||||
assert len(agent.tools) == 2
|
||||
|
||||
def test_format_tools_handles_mixed(self, mock_mcp_with_tools):
|
||||
"""Test that _format_tools_for_llm handles mixed tools."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
|
||||
# Should have 3 tools (2 from MCP + 1 regular function)
|
||||
assert isinstance(formatted, list)
|
||||
assert len(formatted) == 3
|
||||
|
||||
tool_names = [t["function"]["name"] for t in formatted]
|
||||
assert "get_current_time" in tool_names
|
||||
assert "convert_time" in tool_names
|
||||
assert "sample_function" in tool_names
|
||||
|
||||
def test_execute_mixed_tools(self, mock_mcp_with_tools):
|
||||
"""Test executing both MCP tools and regular functions."""
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="Test agent",
|
||||
tools=[mock_mcp_with_tools, sample_function]
|
||||
)
|
||||
|
||||
# Execute regular function
|
||||
result = agent.execute_tool("sample_function", {"query": "test"})
|
||||
assert "Result for: test" in str(result)
|
||||
|
||||
# Mock and execute MCP tool
|
||||
mock_mcp_with_tools.runner.call_tool = Mock(return_value='{"timezone": "UTC"}')
|
||||
result = agent.execute_tool("get_current_time", {"timezone": "UTC"})
|
||||
mock_mcp_with_tools.runner.call_tool.assert_called_once()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Integration Tests (requires actual MCP server)
|
||||
# ============================================================================
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("SKIP_MCP_INTEGRATION") == "1",
|
||||
reason="MCP integration tests skipped"
|
||||
)
|
||||
class TestMCPIntegration:
|
||||
"""Integration tests using actual MCP servers."""
|
||||
|
||||
def test_real_time_mcp_server(self):
|
||||
"""Test with real mcp-server-time."""
|
||||
try:
|
||||
mcp = MCP("uvx mcp-server-time", timeout=30)
|
||||
tools = mcp.to_openai_tool()
|
||||
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) >= 1
|
||||
|
||||
tool_names = [t["function"]["name"] for t in tools]
|
||||
assert "get_current_time" in tool_names
|
||||
except Exception as e:
|
||||
pytest.skip(f"MCP server not available: {e}")
|
||||
|
||||
def test_multiple_real_mcp_servers(self):
|
||||
"""Test with multiple real MCP servers."""
|
||||
try:
|
||||
time_mcp = MCP("uvx mcp-server-time", timeout=30)
|
||||
|
||||
# Create agent with multiple MCP instances
|
||||
agent = Agent(
|
||||
name="MultiMCPAgent",
|
||||
instructions="Test agent with multiple MCP tools",
|
||||
tools=[time_mcp]
|
||||
)
|
||||
|
||||
# Verify tools are available
|
||||
formatted = agent._format_tools_for_completion(agent.tools)
|
||||
assert len(formatted) >= 1
|
||||
|
||||
except Exception as e:
|
||||
pytest.skip(f"MCP servers not available: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Run tests
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--tb=short"])
|
||||
@@ -5,7 +5,9 @@ Test script to verify Ollama empty response fix for both sync and async methods.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, TaskOutput
|
||||
import os
|
||||
import pytest
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from typing import Dict, Any
|
||||
|
||||
# Enable debug logging
|
||||
@@ -25,8 +27,8 @@ def search_tool(query: str) -> Dict[str, Any]:
|
||||
logging.debug(f"[TEST] Search tool returning: {results}")
|
||||
return results
|
||||
|
||||
# Test with both sync and async workflows
|
||||
async def test_model_async(model_name: str):
|
||||
# Helper async function to test a model
|
||||
async def _test_model_async_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing ASYNC with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -78,7 +80,7 @@ async def test_model_async(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_model_sync(model_name: str):
|
||||
def _test_model_sync_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing SYNC with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -130,18 +132,60 @@ def test_model_sync(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_ollama_async_openai():
|
||||
"""Test async with OpenAI model as baseline."""
|
||||
result = await _test_model_async_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI async model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_ollama_sync_openai():
|
||||
"""Test sync with OpenAI model as baseline."""
|
||||
result = _test_model_sync_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI sync model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_ollama_async_ollama():
|
||||
"""Test async with Ollama model."""
|
||||
result = await _test_model_async_helper("ollama/llama3.2")
|
||||
assert result, "Ollama async model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
def test_ollama_sync_ollama():
|
||||
"""Test sync with Ollama model."""
|
||||
result = _test_model_sync_helper("ollama/llama3.2")
|
||||
assert result, "Ollama sync model test failed"
|
||||
|
||||
|
||||
async def main():
|
||||
print("Testing Ollama empty response fix for both sync and async...")
|
||||
|
||||
# Test sync methods
|
||||
print("\n1. Testing SYNC methods:")
|
||||
openai_sync_success = test_model_sync("openai/gpt-5-nano")
|
||||
ollama_sync_success = test_model_sync("ollama/llama3.2")
|
||||
openai_sync_success = _test_model_sync_helper("gpt-4o-mini")
|
||||
ollama_sync_success = _test_model_sync_helper("ollama/llama3.2")
|
||||
|
||||
# Test async methods
|
||||
print("\n2. Testing ASYNC methods:")
|
||||
openai_async_success = await test_model_async("openai/gpt-5-nano")
|
||||
ollama_async_success = await test_model_async("ollama/llama3.2")
|
||||
openai_async_success = await _test_model_async_helper("gpt-4o-mini")
|
||||
ollama_async_success = await _test_model_async_helper("ollama/llama3.2")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
|
||||
@@ -4,7 +4,9 @@ Test script to verify Ollama empty response fix.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents, TaskOutput
|
||||
import os
|
||||
import pytest
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from typing import Dict, Any
|
||||
|
||||
# Enable debug logging
|
||||
@@ -24,8 +26,8 @@ def search_tool(query: str) -> Dict[str, Any]:
|
||||
logging.debug(f"[TEST] Search tool returning: {results}")
|
||||
return results
|
||||
|
||||
# Test with both OpenAI and Ollama models
|
||||
def test_model(model_name: str):
|
||||
# Helper function to test a model
|
||||
def _test_model_helper(model_name: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing with model: {model_name}")
|
||||
print('='*60)
|
||||
@@ -77,16 +79,36 @@ def test_model(model_name: str):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set - skipping integration test"
|
||||
)
|
||||
def test_ollama_fix_openai():
|
||||
"""Test with OpenAI model as baseline."""
|
||||
result = _test_model_helper("gpt-4o-mini")
|
||||
assert result, "OpenAI model test failed"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get("OLLAMA_HOST", "").strip(),
|
||||
reason="OLLAMA_HOST not set - skipping Ollama integration test"
|
||||
)
|
||||
def test_ollama_fix_ollama():
|
||||
"""Test with Ollama model."""
|
||||
result = _test_model_helper("ollama/llama3.2")
|
||||
assert result, "Ollama model test failed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing Ollama empty response fix...")
|
||||
|
||||
# Test with OpenAI first (as baseline)
|
||||
print("\n1. Testing with OpenAI (baseline):")
|
||||
openai_success = test_model("openai/gpt-5-nano")
|
||||
openai_success = _test_model_helper("gpt-4o-mini")
|
||||
|
||||
# Test with Ollama
|
||||
print("\n2. Testing with Ollama:")
|
||||
ollama_success = test_model("ollama/llama3.2")
|
||||
ollama_success = _test_model_helper("ollama/llama3.2")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
|
||||
@@ -53,11 +53,14 @@ def test_ollama_argument_validation():
|
||||
print("\n2. Testing mixed arguments (the main issue):")
|
||||
mixed_args = {"a": "get_stock_price", "company_name": "Google", "b": "2"}
|
||||
filtered_args = llm._validate_and_filter_ollama_arguments("multiply", mixed_args, tools)
|
||||
expected_filtered = {"a": "get_stock_price", "b": "2"} # Should remove 'company_name'
|
||||
# Note: The method may coerce "2" to int 2 based on type hints - both are acceptable
|
||||
expected_filtered_str = {"a": "get_stock_price", "b": "2"} # String version
|
||||
expected_filtered_int = {"a": "get_stock_price", "b": 2} # Int version (coerced)
|
||||
print(f"Original: {mixed_args}")
|
||||
print(f"Filtered: {filtered_args}")
|
||||
print(f"Expected: {expected_filtered}")
|
||||
assert filtered_args == expected_filtered, f"Expected {expected_filtered}, got {filtered_args}"
|
||||
print(f"Expected: {expected_filtered_str} or {expected_filtered_int}")
|
||||
assert filtered_args == expected_filtered_str or filtered_args == expected_filtered_int, \
|
||||
f"Expected {expected_filtered_str} or {expected_filtered_int}, got {filtered_args}"
|
||||
print("✅ Mixed arguments filtering test passed")
|
||||
|
||||
# Test case 3: All invalid arguments
|
||||
|
||||
@@ -704,7 +704,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value=json.dumps({
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value=json.dumps({
|
||||
"name": "Test Plan",
|
||||
"description": "A test plan",
|
||||
"steps": [
|
||||
@@ -733,7 +734,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value=json.dumps({
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value=json.dumps({
|
||||
"name": "Refined Plan",
|
||||
"description": "Updated plan",
|
||||
"steps": [
|
||||
@@ -765,7 +767,8 @@ class TestPlanningAgent:
|
||||
planner = PlanningAgent()
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.achat = AsyncMock(return_value="Analysis: Python project with FastAPI")
|
||||
# Mock get_response_async which is the method actually called
|
||||
mock_llm.get_response_async = AsyncMock(return_value="Analysis: Python project with FastAPI")
|
||||
|
||||
planner._llm = mock_llm
|
||||
|
||||
|
||||
@@ -2,7 +2,29 @@
|
||||
"""Comprehensive test script to verify self-reflection works with tools after the fix"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import duckduckgo_search
|
||||
|
||||
# Define duckduckgo_search tool locally to avoid import issues
|
||||
def duckduckgo_search(query: str) -> str:
|
||||
"""
|
||||
Search the web using DuckDuckGo.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
|
||||
Returns:
|
||||
Search results as a string
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
with DDGS() as ddgs:
|
||||
results = list(ddgs.text(query, max_results=3))
|
||||
if results:
|
||||
return "\n".join([f"- {r.get('title', '')}: {r.get('body', '')}" for r in results])
|
||||
return "No results found"
|
||||
except ImportError:
|
||||
return f"Mock search results for: {query}"
|
||||
except Exception as e:
|
||||
return f"Search error: {str(e)}"
|
||||
|
||||
def test_self_reflection_with_tools():
|
||||
"""Test self-reflection with tools - should work after the fix"""
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Simple test to verify the self-reflection fix works"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "2 + 2", "25 * 17")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_self_reflection_fix():
|
||||
"""Test that self-reflection works with tools after the fix"""
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
"""Test to verify the self-reflection fix works with tools"""
|
||||
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from praisonaiagents.tools import calculator
|
||||
|
||||
# Define calculator tool locally to avoid import issues
|
||||
def calculator(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate (e.g., "123 * 456")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
try:
|
||||
result = eval(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def test_self_reflection_fix_verification():
|
||||
"""Test that self-reflection now works with tools"""
|
||||
|
||||
@@ -319,7 +319,14 @@ class TestExecuteWithAgents:
|
||||
result = manager.execute("test_workflow")
|
||||
|
||||
assert not result["success"]
|
||||
assert "No executor available" in result.get("error", "")
|
||||
# Error can be at top level or in results array
|
||||
error_found = "No executor available" in result.get("error", "")
|
||||
if not error_found and "results" in result:
|
||||
error_found = any(
|
||||
"No executor available" in r.get("error", "")
|
||||
for r in result.get("results", [])
|
||||
)
|
||||
assert error_found, f"Expected 'No executor available' error, got: {result}"
|
||||
|
||||
|
||||
class TestWorkflowLevelDefaults:
|
||||
|
||||
@@ -1017,6 +1017,767 @@ class TestDocumentationExamples:
|
||||
assert attempts[0] == 2 # First attempt + 1 retry
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Agent Integration in Workflows
|
||||
# =============================================================================
|
||||
|
||||
class TestAgentWorkflows:
|
||||
"""Test Agent objects as workflow steps."""
|
||||
|
||||
def test_agent_normalization(self):
|
||||
"""Test that Agent objects are properly normalized to WorkflowStep."""
|
||||
# Create a mock agent
|
||||
class MockAgent:
|
||||
def __init__(self, name, tools=None):
|
||||
self.name = name
|
||||
self.tools = tools or []
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return f"Response from {self.name}: {message}"
|
||||
|
||||
agent = MockAgent("TestAgent", tools=["tool1", "tool2"])
|
||||
|
||||
workflow = Workflow(steps=[agent])
|
||||
|
||||
# Normalize and check
|
||||
normalized = workflow._normalize_single_step(agent, 0)
|
||||
assert normalized.name == "TestAgent"
|
||||
assert normalized.agent == agent
|
||||
assert normalized.tools == ["tool1", "tool2"]
|
||||
|
||||
def test_workflow_with_mock_agent(self):
|
||||
"""Test workflow execution with mock agent."""
|
||||
class MockAgent:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return f"Processed: {message}"
|
||||
|
||||
agent = MockAgent("Processor")
|
||||
|
||||
workflow = Workflow(steps=[agent])
|
||||
result = workflow.start("Hello World")
|
||||
|
||||
assert "Processed:" in result["output"]
|
||||
assert "Hello World" in result["output"]
|
||||
|
||||
def test_sequential_mock_agents(self):
|
||||
"""Test sequential execution of multiple mock agents."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, prefix):
|
||||
self.name = name
|
||||
self.prefix = prefix
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return f"{self.prefix}: {message}"
|
||||
|
||||
agent1 = MockAgent("Agent1", "First")
|
||||
agent2 = MockAgent("Agent2", "Second")
|
||||
agent3 = MockAgent("Agent3", "Third")
|
||||
|
||||
workflow = Workflow(steps=[agent1, agent2, agent3])
|
||||
result = workflow.start("Input")
|
||||
|
||||
# Final output should be from agent3
|
||||
assert "Third:" in result["output"]
|
||||
assert len(result["steps"]) == 3
|
||||
|
||||
def test_parallel_mock_agents(self):
|
||||
"""Test parallel execution of mock agents."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, output):
|
||||
self.name = name
|
||||
self.output = output
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return self.output
|
||||
|
||||
agent1 = MockAgent("Agent1", "Result A")
|
||||
agent2 = MockAgent("Agent2", "Result B")
|
||||
agent3 = MockAgent("Agent3", "Result C")
|
||||
|
||||
def aggregator(ctx: WorkflowContext) -> StepResult:
|
||||
outputs = ctx.variables.get("parallel_outputs", [])
|
||||
return StepResult(output=f"Combined: {len(outputs)} results")
|
||||
|
||||
workflow = Workflow(steps=[
|
||||
parallel([agent1, agent2, agent3]),
|
||||
aggregator
|
||||
])
|
||||
|
||||
result = workflow.start("Test")
|
||||
assert "Combined: 3 results" in result["output"]
|
||||
|
||||
def test_route_to_mock_agents(self):
|
||||
"""Test routing to different mock agents."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, output):
|
||||
self.name = name
|
||||
self.output = output
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return self.output
|
||||
|
||||
tech_agent = MockAgent("TechAgent", "Technical response")
|
||||
creative_agent = MockAgent("CreativeAgent", "Creative response")
|
||||
|
||||
def classifier(ctx: WorkflowContext) -> StepResult:
|
||||
if "tech" in ctx.input.lower():
|
||||
return StepResult(output="category: technical")
|
||||
return StepResult(output="category: creative")
|
||||
|
||||
workflow = Workflow(steps=[
|
||||
classifier,
|
||||
route({
|
||||
"technical": [tech_agent],
|
||||
"creative": [creative_agent]
|
||||
})
|
||||
])
|
||||
|
||||
# Test technical route
|
||||
result = workflow.start("tech question")
|
||||
assert "Technical response" in result["output"]
|
||||
|
||||
# Test creative route
|
||||
result = workflow.start("creative request")
|
||||
assert "Creative response" in result["output"]
|
||||
|
||||
def test_loop_with_mock_agent(self):
|
||||
"""Test loop pattern with mock agent."""
|
||||
class MockAgent:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return f"Processed: {message}"
|
||||
|
||||
processor = MockAgent("Processor")
|
||||
|
||||
workflow = Workflow(
|
||||
steps=[loop(processor, over="items")],
|
||||
variables={"items": ["A", "B", "C"]}
|
||||
)
|
||||
|
||||
result = workflow.start("Process items")
|
||||
outputs = result["variables"].get("loop_outputs", [])
|
||||
assert len(outputs) == 3
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Workflow Configuration
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowConfiguration:
|
||||
"""Test workflow configuration options."""
|
||||
|
||||
def test_workflow_verbose_setting(self):
|
||||
"""Test that workflow verbose setting is used."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1], verbose=True)
|
||||
assert workflow.verbose == True
|
||||
|
||||
def test_workflow_reasoning_setting(self):
|
||||
"""Test that workflow reasoning setting is stored."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1], reasoning=True)
|
||||
assert workflow.reasoning == True
|
||||
|
||||
def test_workflow_planning_setting(self):
|
||||
"""Test that workflow planning setting is stored."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-4o")
|
||||
assert workflow.planning == True
|
||||
assert workflow.planning_llm == "gpt-4o"
|
||||
|
||||
def test_workflow_default_llm(self):
|
||||
"""Test default LLM setting."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1], default_llm="gpt-4o-mini")
|
||||
assert workflow.default_llm == "gpt-4o-mini"
|
||||
|
||||
def test_workflow_default_agent_config(self):
|
||||
"""Test default agent config."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(
|
||||
steps=[step1],
|
||||
default_agent_config={
|
||||
"name": "DefaultAgent",
|
||||
"role": "Assistant",
|
||||
"tools": ["tool1"]
|
||||
}
|
||||
)
|
||||
assert workflow.default_agent_config["name"] == "DefaultAgent"
|
||||
assert workflow.default_agent_config["tools"] == ["tool1"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: WorkflowStep with Tools
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowStepTools:
|
||||
"""Test WorkflowStep with tools configuration."""
|
||||
|
||||
def test_step_with_tools(self):
|
||||
"""Test WorkflowStep with tools parameter."""
|
||||
def my_tool(): return "tool result"
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step_with_tools",
|
||||
action="Do something",
|
||||
tools=[my_tool]
|
||||
)
|
||||
|
||||
assert step.tools == [my_tool]
|
||||
|
||||
def test_step_with_agent_config_tools(self):
|
||||
"""Test WorkflowStep with tools in agent_config."""
|
||||
def my_tool(): return "tool result"
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step_with_config",
|
||||
action="Do something",
|
||||
agent_config={
|
||||
"name": "Agent",
|
||||
"role": "Helper",
|
||||
"tools": [my_tool]
|
||||
}
|
||||
)
|
||||
|
||||
assert step.agent_config["tools"] == [my_tool]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Migrated Features (output_file, images, async, etc.)
|
||||
# =============================================================================
|
||||
|
||||
class TestMigratedFeatures:
|
||||
"""Test features migrated from process='workflow'."""
|
||||
|
||||
def test_step_output_file(self):
|
||||
"""Test WorkflowStep with output_file parameter."""
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = os.path.join(tmpdir, "output.txt")
|
||||
|
||||
def generate_content(ctx: WorkflowContext) -> StepResult:
|
||||
return StepResult(output="Generated content for file")
|
||||
|
||||
workflow = Workflow(steps=[
|
||||
WorkflowStep(
|
||||
name="generator",
|
||||
handler=generate_content,
|
||||
output_file=output_path
|
||||
)
|
||||
])
|
||||
|
||||
result = workflow.start("Generate content")
|
||||
|
||||
# Check file was created
|
||||
assert os.path.exists(output_path)
|
||||
with open(output_path) as f:
|
||||
content = f.read()
|
||||
assert "Generated content" in content
|
||||
|
||||
def test_step_images_parameter(self):
|
||||
"""Test WorkflowStep with images parameter."""
|
||||
step = WorkflowStep(
|
||||
name="vision_step",
|
||||
action="Analyze this image",
|
||||
images=["image1.jpg", "image2.png"]
|
||||
)
|
||||
|
||||
assert step.images == ["image1.jpg", "image2.png"]
|
||||
|
||||
def test_step_output_pydantic_parameter(self):
|
||||
"""Test WorkflowStep with output_pydantic parameter."""
|
||||
from pydantic import BaseModel
|
||||
|
||||
class OutputModel(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
|
||||
step = WorkflowStep(
|
||||
name="structured_step",
|
||||
action="Generate structured output",
|
||||
output_pydantic=OutputModel
|
||||
)
|
||||
|
||||
assert step.output_pydantic == OutputModel
|
||||
|
||||
def test_step_async_execution_parameter(self):
|
||||
"""Test WorkflowStep with async_execution parameter."""
|
||||
step = WorkflowStep(
|
||||
name="async_step",
|
||||
action="Run async",
|
||||
async_execution=True
|
||||
)
|
||||
|
||||
assert step.async_execution == True
|
||||
|
||||
def test_step_quality_check_parameter(self):
|
||||
"""Test WorkflowStep with quality_check parameter."""
|
||||
step = WorkflowStep(
|
||||
name="quality_step",
|
||||
action="Check quality",
|
||||
quality_check=False
|
||||
)
|
||||
|
||||
assert step.quality_check == False
|
||||
|
||||
def test_step_rerun_parameter(self):
|
||||
"""Test WorkflowStep with rerun parameter."""
|
||||
step = WorkflowStep(
|
||||
name="rerun_step",
|
||||
action="Can rerun",
|
||||
rerun=False
|
||||
)
|
||||
|
||||
assert step.rerun == False
|
||||
|
||||
def test_workflow_astart_method_exists(self):
|
||||
"""Test that Workflow has astart method."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1])
|
||||
|
||||
assert hasattr(workflow, 'astart')
|
||||
assert callable(workflow.astart)
|
||||
|
||||
def test_workflow_arun_method_exists(self):
|
||||
"""Test that Workflow has arun method."""
|
||||
def step1(ctx): return StepResult(output="Done")
|
||||
|
||||
workflow = Workflow(steps=[step1])
|
||||
|
||||
assert hasattr(workflow, 'arun')
|
||||
assert callable(workflow.arun)
|
||||
|
||||
def test_step_to_dict_includes_new_fields(self):
|
||||
"""Test that to_dict includes all new fields."""
|
||||
step = WorkflowStep(
|
||||
name="test_step",
|
||||
action="Test action",
|
||||
output_file="output.txt",
|
||||
images=["img.jpg"],
|
||||
async_execution=True,
|
||||
quality_check=False,
|
||||
rerun=False
|
||||
)
|
||||
|
||||
d = step.to_dict()
|
||||
|
||||
assert d["output_file"] == "output.txt"
|
||||
assert d["images"] == ["img.jpg"]
|
||||
assert d["async_execution"] == True
|
||||
assert d["quality_check"] == False
|
||||
assert d["rerun"] == False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Planning and Reasoning Modes
|
||||
# =============================================================================
|
||||
|
||||
class TestPlanningAndReasoning:
|
||||
"""Test planning and reasoning modes in workflows."""
|
||||
|
||||
def test_workflow_planning_parameter_exists(self):
|
||||
"""Test that Workflow accepts planning parameter."""
|
||||
workflow = Workflow(
|
||||
steps=[lambda ctx: StepResult(output="test")],
|
||||
planning=True,
|
||||
planning_llm="gpt-4o"
|
||||
)
|
||||
assert workflow.planning == True
|
||||
assert workflow.planning_llm == "gpt-4o"
|
||||
|
||||
def test_workflow_reasoning_parameter_exists(self):
|
||||
"""Test that Workflow accepts reasoning parameter."""
|
||||
workflow = Workflow(
|
||||
steps=[lambda ctx: StepResult(output="test")],
|
||||
reasoning=True
|
||||
)
|
||||
assert workflow.reasoning == True
|
||||
|
||||
def test_workflow_memory_config_parameter_exists(self):
|
||||
"""Test that Workflow accepts memory_config parameter."""
|
||||
workflow = Workflow(
|
||||
steps=[lambda ctx: StepResult(output="test")],
|
||||
memory_config={"provider": "rag", "persist": True}
|
||||
)
|
||||
assert workflow.memory_config == {"provider": "rag", "persist": True}
|
||||
|
||||
def test_workflow_verbose_parameter_exists(self):
|
||||
"""Test that Workflow accepts verbose parameter."""
|
||||
workflow = Workflow(
|
||||
steps=[lambda ctx: StepResult(output="test")],
|
||||
verbose=True
|
||||
)
|
||||
assert workflow.verbose == True
|
||||
|
||||
|
||||
class TestToolsPerStep:
|
||||
"""Test tools per step functionality."""
|
||||
|
||||
def test_step_with_tools_list(self):
|
||||
"""Test WorkflowStep with tools list."""
|
||||
def my_tool():
|
||||
return "tool result"
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
action="Do something",
|
||||
tools=[my_tool]
|
||||
)
|
||||
|
||||
assert step.tools == [my_tool]
|
||||
|
||||
def test_step_with_agent_config_tools(self):
|
||||
"""Test WorkflowStep with tools in agent_config."""
|
||||
def my_tool():
|
||||
return "tool result"
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
action="Do something",
|
||||
agent_config={
|
||||
"name": "TestAgent",
|
||||
"role": "Tester",
|
||||
"tools": [my_tool]
|
||||
}
|
||||
)
|
||||
|
||||
assert step.agent_config["tools"] == [my_tool]
|
||||
|
||||
def test_agent_with_tools_in_workflow(self):
|
||||
"""Test Agent with tools is properly handled in workflow."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, tools=None):
|
||||
self.name = name
|
||||
self.tools = tools or []
|
||||
|
||||
def chat(self, message, **kwargs):
|
||||
return f"Response with {len(self.tools)} tools"
|
||||
|
||||
def mock_tool():
|
||||
return "tool result"
|
||||
|
||||
agent = MockAgent("TestAgent", tools=[mock_tool])
|
||||
workflow = Workflow(steps=[agent])
|
||||
|
||||
# Normalize and verify tools are preserved
|
||||
normalized = workflow._normalize_single_step(agent, 0)
|
||||
assert normalized.tools == [mock_tool]
|
||||
|
||||
# Execute and verify
|
||||
result = workflow.start("test")
|
||||
assert "1 tools" in result["output"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: WorkflowManager with Patterns in MD Files
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowManagerPatterns:
|
||||
"""Test WorkflowManager parsing of pattern syntax in markdown files."""
|
||||
|
||||
def test_parse_route_pattern_in_md(self):
|
||||
"""Test parsing route pattern from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Route Test
|
||||
description: Test routing pattern
|
||||
---
|
||||
|
||||
## Step 1: Classifier
|
||||
Classify the input.
|
||||
|
||||
```action
|
||||
Classify this input as technical or creative
|
||||
```
|
||||
|
||||
```route
|
||||
technical: [Tech Handler]
|
||||
creative: [Creative Handler]
|
||||
default: [General Handler]
|
||||
```
|
||||
|
||||
## Step 2: Tech Handler
|
||||
Handle technical requests.
|
||||
|
||||
```action
|
||||
Process technical request
|
||||
```
|
||||
|
||||
## Step 3: Creative Handler
|
||||
Handle creative requests.
|
||||
|
||||
```action
|
||||
Process creative request
|
||||
```
|
||||
|
||||
## Step 4: General Handler
|
||||
Handle general requests.
|
||||
|
||||
```action
|
||||
Process general request
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "route_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
assert workflow.name == "Route Test"
|
||||
|
||||
# Check that route pattern is parsed
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Classifier"
|
||||
assert step1.branch_condition is not None
|
||||
assert "technical" in step1.branch_condition
|
||||
|
||||
def test_parse_parallel_pattern_in_md(self):
|
||||
"""Test parsing parallel pattern from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Parallel Test
|
||||
description: Test parallel pattern
|
||||
---
|
||||
|
||||
## Step 1: Research
|
||||
Research in parallel.
|
||||
|
||||
```parallel
|
||||
- Market Research
|
||||
- Competitor Analysis
|
||||
- Customer Survey
|
||||
```
|
||||
|
||||
```action
|
||||
Research the topic
|
||||
```
|
||||
|
||||
## Step 2: Market Research
|
||||
Research the market.
|
||||
|
||||
```action
|
||||
Analyze market trends
|
||||
```
|
||||
|
||||
## Step 3: Competitor Analysis
|
||||
Analyze competitors.
|
||||
|
||||
```action
|
||||
Analyze competitors
|
||||
```
|
||||
|
||||
## Step 4: Customer Survey
|
||||
Survey customers.
|
||||
|
||||
```action
|
||||
Survey customers
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "parallel_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
assert workflow.name == "Parallel Test"
|
||||
|
||||
# Check that parallel steps are parsed
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Research"
|
||||
assert hasattr(step1, 'parallel_steps') or step1.next_steps is not None
|
||||
|
||||
def test_parse_loop_pattern_in_md(self):
|
||||
"""Test parsing loop pattern from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Loop Test
|
||||
description: Test loop pattern
|
||||
variables:
|
||||
items: ["item1", "item2", "item3"]
|
||||
---
|
||||
|
||||
## Step 1: Process Items
|
||||
Process each item in the list.
|
||||
|
||||
loop_over: items
|
||||
loop_var: current_item
|
||||
|
||||
```action
|
||||
Process {{current_item}}
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "loop_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Process Items"
|
||||
assert step1.loop_over == "items"
|
||||
assert step1.loop_var == "current_item"
|
||||
|
||||
def test_parse_repeat_pattern_in_md(self):
|
||||
"""Test parsing repeat pattern from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Repeat Test
|
||||
description: Test repeat pattern
|
||||
---
|
||||
|
||||
## Step 1: Generate
|
||||
Generate content repeatedly.
|
||||
|
||||
```repeat
|
||||
max_iterations: 5
|
||||
until: count >= 10
|
||||
```
|
||||
|
||||
```action
|
||||
Generate more content
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "repeat_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Generate"
|
||||
# Check repeat config is parsed
|
||||
assert hasattr(step1, 'max_retries') or hasattr(step1, 'repeat_config')
|
||||
|
||||
def test_parse_output_file_in_md(self):
|
||||
"""Test parsing output_file from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Output Test
|
||||
description: Test output file
|
||||
---
|
||||
|
||||
## Step 1: Generate Report
|
||||
Generate a report and save to file.
|
||||
|
||||
output_file: output/report.txt
|
||||
|
||||
```action
|
||||
Generate the report
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "output_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Generate Report"
|
||||
assert step1.output_file == "output/report.txt"
|
||||
|
||||
def test_parse_images_in_md(self):
|
||||
"""Test parsing images from markdown."""
|
||||
import tempfile
|
||||
import os
|
||||
from praisonaiagents.workflows import WorkflowManager
|
||||
|
||||
md_content = """---
|
||||
name: Vision Test
|
||||
description: Test image handling
|
||||
---
|
||||
|
||||
## Step 1: Analyze Image
|
||||
Analyze the provided image.
|
||||
|
||||
```images
|
||||
image1.jpg
|
||||
image2.png
|
||||
```
|
||||
|
||||
```action
|
||||
Analyze these images
|
||||
```
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = os.path.join(tmpdir, ".praison", "workflows")
|
||||
os.makedirs(workflows_dir)
|
||||
|
||||
with open(os.path.join(workflows_dir, "vision_test.md"), "w") as f:
|
||||
f.write(md_content)
|
||||
|
||||
manager = WorkflowManager(workspace_path=tmpdir)
|
||||
workflows = manager.list_workflows()
|
||||
|
||||
assert len(workflows) == 1
|
||||
workflow = workflows[0]
|
||||
|
||||
step1 = workflow.steps[0]
|
||||
assert step1.name == "Analyze Image"
|
||||
assert step1.images is not None
|
||||
assert "image1.jpg" in step1.images
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Run Tests
|
||||
# =============================================================================
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.0"
|
||||
version = "0.1.13"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+703
-429
File diff suppressed because it is too large.
Load diff
+41
-36
@@ -1,44 +1,49 @@
|
||||
# Movie Script Workflow - Cat in Mars
|
||||
# Uses the modern agents + steps format
|
||||
|
||||
name: Create Movie Script About Cat in Mars
|
||||
description: A sequential workflow to research, design narrative, and write a movie script
|
||||
framework: praisonai
|
||||
process: sequential
|
||||
topic: create movie script about cat in mars
|
||||
memory: false
|
||||
roles:
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
backstory: Skilled in research, with a focus on gathering accurate and relevant
|
||||
information.
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Gather information about Mars and cats
|
||||
role: Researcher
|
||||
min_reflect: 1
|
||||
max_reflect: 2
|
||||
tasks:
|
||||
research_task:
|
||||
description: Research about Mars, its environment, and the feasibility of
|
||||
a cat being on Mars. Also, research about cat behavior and characteristics.
|
||||
expected_output: Document with research findings on Mars and cats.
|
||||
backstory: Skilled in research, with a focus on gathering accurate and relevant information.
|
||||
instructions: |
|
||||
Research about Mars, its environment, and the feasibility of a cat being on Mars.
|
||||
Also, research about cat behavior and characteristics.
|
||||
tools:
|
||||
- search_tool
|
||||
- search_tool
|
||||
|
||||
narrative_designer:
|
||||
backstory: Skilled in narrative development, with a focus on creating engaging
|
||||
stories.
|
||||
name: Narrative Designer
|
||||
role: Story Concept Developer
|
||||
goal: Create a story concept for a movie about a cat in Mars
|
||||
role: Narrative Designer
|
||||
tasks:
|
||||
story_concept_development:
|
||||
description: Use the research findings to craft a unique story concept for
|
||||
a movie about a cat in Mars.
|
||||
expected_output: Document with narrative arcs, character bios, and settings.
|
||||
tools:
|
||||
- ''
|
||||
backstory: Skilled in narrative development, with a focus on creating engaging stories.
|
||||
instructions: |
|
||||
Use the research findings to craft a unique story concept for a movie about a cat in Mars.
|
||||
Create narrative arcs, character bios, and settings.
|
||||
|
||||
scriptwriter:
|
||||
backstory: Expert in dialogue and script structure, translating concepts into
|
||||
scripts.
|
||||
name: Scriptwriter
|
||||
role: Script Writer
|
||||
goal: Write a movie script about a cat in Mars
|
||||
role: Scriptwriter
|
||||
tasks:
|
||||
scriptwriting_task:
|
||||
description: Turn the story concept into a production-ready movie script,
|
||||
including dialogue and scene details.
|
||||
expected_output: Final movie script with dialogue and scene details.
|
||||
tools:
|
||||
- ''
|
||||
dependencies: []
|
||||
backstory: Expert in dialogue and script structure, translating concepts into scripts.
|
||||
instructions: |
|
||||
Turn the story concept into a production-ready movie script,
|
||||
including dialogue and scene details.
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research about Mars environment and cat behavior for the movie concept"
|
||||
|
||||
- agent: narrative_designer
|
||||
action: "Create story concept based on research: {{previous_output}}"
|
||||
|
||||
- agent: scriptwriter
|
||||
action: "Write the final movie script based on the story concept: {{previous_output}}"
|
||||
@@ -0,0 +1,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": []
|
||||
}
|
||||
@@ -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.22.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.22.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]"
|
||||
|
||||
@@ -318,7 +318,17 @@ class AgentsGenerator:
|
||||
print(f"File not found: {self.agent_file}")
|
||||
return
|
||||
|
||||
topic = config['topic']
|
||||
# Check if this is a workflow-mode YAML (process: workflow or has steps section)
|
||||
process_type = config.get('process', 'sequential')
|
||||
has_steps = 'steps' in config
|
||||
has_workflow_config = 'workflow' in config
|
||||
|
||||
if process_type == 'workflow' or (has_steps and has_workflow_config):
|
||||
# Route to YAMLWorkflowParser for advanced workflow patterns
|
||||
return self._run_yaml_workflow(config)
|
||||
|
||||
# Get workflow input: 'input' is canonical, 'topic' is alias for backward compatibility
|
||||
topic = config.get('input', config.get('topic', ''))
|
||||
tools_dict = {}
|
||||
|
||||
# Only try to use praisonai_tools if it's available and needed
|
||||
@@ -406,6 +416,62 @@ class AgentsGenerator:
|
||||
agentops.init(os.environ.get("AGENTOPS_API_KEY"), default_tags=["crewai"])
|
||||
return self._run_crewai(config, topic, tools_dict)
|
||||
|
||||
def _run_yaml_workflow(self, config):
|
||||
"""
|
||||
Run a YAML workflow using the YAMLWorkflowParser.
|
||||
|
||||
This method handles agents.yaml files that have:
|
||||
- process: workflow
|
||||
- steps section with workflow patterns (route, parallel, loop, repeat)
|
||||
|
||||
Args:
|
||||
config (dict): The parsed YAML configuration
|
||||
|
||||
Returns:
|
||||
str: Result of the workflow execution
|
||||
"""
|
||||
if not PRAISONAI_AVAILABLE:
|
||||
raise ImportError("PraisonAI is not installed. Please install it with 'pip install praisonaiagents'")
|
||||
|
||||
try:
|
||||
from praisonaiagents.workflows import YAMLWorkflowParser
|
||||
except ImportError:
|
||||
raise ImportError("YAMLWorkflowParser not available. Please update praisonaiagents.")
|
||||
|
||||
# Ensure name is present (YAMLWorkflowParser handles roles->agents conversion)
|
||||
if 'name' not in config:
|
||||
config['name'] = config.get('topic', 'Workflow')
|
||||
|
||||
# Pass model from config_list to workflow as default_llm
|
||||
if self.config_list and self.config_list[0].get('model'):
|
||||
model_from_cli = self.config_list[0]['model']
|
||||
# Set default_llm in workflow config if not already set
|
||||
if 'workflow' not in config:
|
||||
config['workflow'] = {}
|
||||
if 'default_llm' not in config['workflow']:
|
||||
config['workflow']['default_llm'] = model_from_cli
|
||||
|
||||
# Convert config back to YAML string for parser
|
||||
# Note: YAMLWorkflowParser handles 'roles' to 'agents' conversion internally
|
||||
import yaml as yaml_module
|
||||
yaml_content = yaml_module.dump(config, default_flow_style=False)
|
||||
|
||||
# Parse and execute
|
||||
parser = YAMLWorkflowParser()
|
||||
workflow = parser.parse_string(yaml_content)
|
||||
|
||||
# Get input: 'input' is canonical, 'topic' is alias for backward compatibility
|
||||
input_data = config.get('input', config.get('topic', ''))
|
||||
|
||||
# Execute workflow
|
||||
self.logger.debug(f"Running workflow: {workflow.name}")
|
||||
result = workflow.start(input_data)
|
||||
|
||||
if result.get("status") == "completed":
|
||||
return result.get("output", "Workflow completed successfully")
|
||||
else:
|
||||
return f"Workflow failed: {result.get('error', 'Unknown error')}"
|
||||
|
||||
def _run_autogen(self, config, topic, tools_dict):
|
||||
"""
|
||||
Run agents using the AutoGen framework.
|
||||
|
||||
@@ -335,4 +335,225 @@ The final role you create should satisfy the provided task: """ + self.topic + "
|
||||
|
||||
|
||||
# generator = AutoGenerator(framework="crewai", topic="Create a movie script about Cat in Mars")
|
||||
# print(generator.generate())
|
||||
# print(generator.generate())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Workflow Auto-Generation (Feature Parity)
|
||||
# =============================================================================
|
||||
|
||||
class WorkflowStepDetails(BaseModel):
|
||||
"""Details for a workflow step."""
|
||||
agent: str
|
||||
action: str
|
||||
expected_output: Optional[str] = None
|
||||
|
||||
class WorkflowRouteDetails(BaseModel):
|
||||
"""Details for a route step."""
|
||||
name: str
|
||||
route: Dict[str, List[str]]
|
||||
|
||||
class WorkflowParallelDetails(BaseModel):
|
||||
"""Details for a parallel step."""
|
||||
name: str
|
||||
parallel: List[WorkflowStepDetails]
|
||||
|
||||
class WorkflowAgentDetails(BaseModel):
|
||||
"""Details for a workflow agent."""
|
||||
name: str
|
||||
role: str
|
||||
goal: str
|
||||
instructions: str
|
||||
tools: Optional[List[str]] = None
|
||||
|
||||
class WorkflowStructure(BaseModel):
|
||||
"""Structure for auto-generated workflow."""
|
||||
name: str
|
||||
description: str
|
||||
agents: Dict[str, WorkflowAgentDetails]
|
||||
steps: List[Dict] # Can be agent steps, route, parallel, etc.
|
||||
|
||||
|
||||
class WorkflowAutoGenerator:
|
||||
"""
|
||||
Auto-generates workflow.yaml files from a topic description.
|
||||
|
||||
Uses lazy loading to avoid performance impact when not used.
|
||||
|
||||
Usage:
|
||||
generator = WorkflowAutoGenerator(topic="Research AI trends and write a report")
|
||||
path = generator.generate()
|
||||
"""
|
||||
|
||||
def __init__(self, topic: str = "Research and write about AI",
|
||||
workflow_file: str = "workflow.yaml",
|
||||
config_list: Optional[List[Dict]] = None):
|
||||
"""
|
||||
Initialize the WorkflowAutoGenerator.
|
||||
|
||||
Args:
|
||||
topic: The task/topic for the workflow
|
||||
workflow_file: Output file name
|
||||
config_list: Optional LLM configuration
|
||||
"""
|
||||
# Support multiple environment variable patterns
|
||||
model_name = os.environ.get("MODEL_NAME") or os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini")
|
||||
base_url = (
|
||||
os.environ.get("OPENAI_BASE_URL") or
|
||||
os.environ.get("OPENAI_API_BASE") or
|
||||
os.environ.get("OLLAMA_API_BASE", "https://api.openai.com/v1")
|
||||
)
|
||||
|
||||
self.config_list = config_list or [
|
||||
{
|
||||
'model': model_name,
|
||||
'base_url': base_url,
|
||||
'api_key': os.environ.get("OPENAI_API_KEY")
|
||||
}
|
||||
]
|
||||
self.topic = topic
|
||||
self.workflow_file = workflow_file
|
||||
self._client = None # Lazy loading
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Lazy load the OpenAI client to avoid performance impact."""
|
||||
if self._client is None:
|
||||
self._client = instructor.patch(
|
||||
OpenAI(
|
||||
base_url=self.config_list[0]['base_url'],
|
||||
api_key=self.config_list[0]['api_key'],
|
||||
),
|
||||
mode=instructor.Mode.JSON,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def generate(self, pattern: str = "sequential") -> str:
|
||||
"""
|
||||
Generate a workflow YAML file.
|
||||
|
||||
Args:
|
||||
pattern: Workflow pattern - "sequential", "routing", "parallel", "loop"
|
||||
|
||||
Returns:
|
||||
Path to the generated workflow file
|
||||
"""
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.config_list[0]['model'],
|
||||
response_model=WorkflowStructure,
|
||||
max_retries=5,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant that designs workflow structures."},
|
||||
{"role": "user", "content": self._get_prompt(pattern)}
|
||||
]
|
||||
)
|
||||
|
||||
json_data = json.loads(response.model_dump_json())
|
||||
return self._save_workflow(json_data, pattern)
|
||||
|
||||
def _get_prompt(self, pattern: str) -> str:
|
||||
"""Generate the prompt based on the workflow pattern."""
|
||||
base_prompt = f"""Generate a workflow structure for: "{self.topic}"
|
||||
|
||||
The workflow should use the "{pattern}" pattern.
|
||||
Create 2-4 agents with clear roles and instructions.
|
||||
Each step should have a clear action.
|
||||
|
||||
"""
|
||||
|
||||
if pattern == "routing":
|
||||
base_prompt += """
|
||||
Include a classifier agent that routes to different specialized agents.
|
||||
The route step should have at least 2 routes plus a default.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Routing Workflow",
|
||||
"description": "Routes requests to specialized agents",
|
||||
"agents": {
|
||||
"classifier": {"name": "Classifier", "role": "Request Classifier", "goal": "Classify requests", "instructions": "Respond with ONLY: technical, creative, or general"},
|
||||
"tech_agent": {"name": "TechExpert", "role": "Technical Expert", "goal": "Handle technical questions", "instructions": "Provide technical answers"}
|
||||
},
|
||||
"steps": [
|
||||
{"agent": "classifier", "action": "Classify: {{input}}"},
|
||||
{"name": "routing", "route": {"technical": ["tech_agent"], "default": ["tech_agent"]}}
|
||||
]
|
||||
}
|
||||
"""
|
||||
elif pattern == "parallel":
|
||||
base_prompt += """
|
||||
Include multiple agents that work in parallel, then an aggregator.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Parallel Workflow",
|
||||
"description": "Multiple agents work concurrently",
|
||||
"agents": {
|
||||
"researcher1": {"name": "Researcher1", "role": "Market Analyst", "goal": "Research market", "instructions": "Provide market insights"},
|
||||
"researcher2": {"name": "Researcher2", "role": "Competitor Analyst", "goal": "Research competitors", "instructions": "Provide competitor insights"},
|
||||
"aggregator": {"name": "Aggregator", "role": "Synthesizer", "goal": "Combine findings", "instructions": "Synthesize all research"}
|
||||
},
|
||||
"steps": [
|
||||
{"name": "parallel_research", "parallel": [
|
||||
{"agent": "researcher1", "action": "Research market for {{input}}"},
|
||||
{"agent": "researcher2", "action": "Research competitors for {{input}}"}
|
||||
]},
|
||||
{"agent": "aggregator", "action": "Combine all findings"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
else: # sequential
|
||||
base_prompt += """
|
||||
Create a sequential workflow where agents work one after another.
|
||||
|
||||
Example structure:
|
||||
{
|
||||
"name": "Sequential Workflow",
|
||||
"description": "Agents work in sequence",
|
||||
"agents": {
|
||||
"researcher": {"name": "Researcher", "role": "Research Analyst", "goal": "Research topics", "instructions": "Provide research findings"},
|
||||
"writer": {"name": "Writer", "role": "Content Writer", "goal": "Write content", "instructions": "Write clear content"}
|
||||
},
|
||||
"steps": [
|
||||
{"agent": "researcher", "action": "Research: {{input}}"},
|
||||
{"agent": "writer", "action": "Write based on: {{previous_output}}"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
base_prompt += f"\nGenerate a workflow for: {self.topic}"
|
||||
return base_prompt
|
||||
|
||||
def _save_workflow(self, data: Dict, pattern: str) -> str:
|
||||
"""Save the workflow to a YAML file."""
|
||||
# Build the workflow YAML structure
|
||||
workflow_yaml = {
|
||||
'name': data.get('name', 'Auto-Generated Workflow'),
|
||||
'description': data.get('description', ''),
|
||||
'framework': 'praisonai',
|
||||
'workflow': {
|
||||
'verbose': True,
|
||||
'planning': False,
|
||||
'reasoning': False
|
||||
},
|
||||
'agents': {},
|
||||
'steps': data.get('steps', [])
|
||||
}
|
||||
|
||||
# Convert agents
|
||||
for agent_id, agent_data in data.get('agents', {}).items():
|
||||
workflow_yaml['agents'][agent_id] = {
|
||||
'name': agent_data.get('name', agent_id),
|
||||
'role': agent_data.get('role', 'Assistant'),
|
||||
'goal': agent_data.get('goal', ''),
|
||||
'instructions': agent_data.get('instructions', '')
|
||||
}
|
||||
if agent_data.get('tools'):
|
||||
workflow_yaml['agents'][agent_id]['tools'] = agent_data['tools']
|
||||
|
||||
# Write to file
|
||||
full_path = os.path.abspath(self.workflow_file)
|
||||
with open(full_path, 'w') as f:
|
||||
yaml.dump(workflow_yaml, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
return full_path
|
||||
@@ -19,6 +19,8 @@ Features:
|
||||
- todo: Todo list management
|
||||
- router: Smart model selection
|
||||
- flow_display: Visual workflow tracking
|
||||
- workflow: YAML workflow management
|
||||
- n8n: Export workflows to n8n visual editor
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -39,6 +41,8 @@ __all__ = [
|
||||
'TodoHandler',
|
||||
'RouterHandler',
|
||||
'FlowDisplayHandler',
|
||||
'WorkflowHandler',
|
||||
'N8nHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -85,4 +89,10 @@ def __getattr__(name):
|
||||
elif name == 'FlowDisplayHandler':
|
||||
from .flow_display import FlowDisplayHandler
|
||||
return FlowDisplayHandler
|
||||
elif name == 'WorkflowHandler':
|
||||
from .workflow import WorkflowHandler
|
||||
return WorkflowHandler
|
||||
elif name == 'N8nHandler':
|
||||
from .n8n import N8nHandler
|
||||
return N8nHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -1,25 +1,31 @@
|
||||
"""
|
||||
Flow Display Handler for CLI.
|
||||
|
||||
Provides visual workflow tracking.
|
||||
Shows a visual flow diagram of the workflow WITHOUT executing it.
|
||||
Usage: praisonai agents.yaml --flow-display
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Tuple, List
|
||||
from typing import Any, Dict, Tuple
|
||||
from .base import FlagHandler
|
||||
import os
|
||||
import yaml
|
||||
|
||||
|
||||
class FlowDisplayHandler(FlagHandler):
|
||||
"""
|
||||
Handler for --flow-display flag.
|
||||
|
||||
Enables visual workflow tracking for multi-agent executions.
|
||||
Shows a visual flow diagram/chart of agents and tasks WITHOUT executing.
|
||||
This allows users to preview the workflow structure before running it.
|
||||
|
||||
Example:
|
||||
praisonai agents.yaml --flow-display
|
||||
praisonai "Complex task" --flow-display
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False):
|
||||
super().__init__(verbose)
|
||||
self._console = None
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "flow_display"
|
||||
@@ -30,160 +36,531 @@ class FlowDisplayHandler(FlagHandler):
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
return "Enable visual workflow tracking"
|
||||
return "Show workflow flow diagram without executing"
|
||||
|
||||
def _get_console(self):
|
||||
"""Get or create Rich Console."""
|
||||
if self._console is None:
|
||||
try:
|
||||
from rich.console import Console
|
||||
self._console = Console()
|
||||
except ImportError:
|
||||
pass
|
||||
return self._console
|
||||
|
||||
def check_dependencies(self) -> Tuple[bool, str]:
|
||||
"""Check if FlowDisplay is available."""
|
||||
"""Check if Rich is available."""
|
||||
try:
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("praisonaiagents") is not None:
|
||||
return True, ""
|
||||
return False, "praisonaiagents not installed"
|
||||
rich_available = importlib.util.find_spec("rich") is not None
|
||||
if not rich_available:
|
||||
return False, "rich not installed. Install with: pip install rich"
|
||||
return True, ""
|
||||
except ImportError:
|
||||
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
|
||||
|
||||
def _get_flow_display(self):
|
||||
"""Get FlowDisplay class lazily."""
|
||||
try:
|
||||
from praisonaiagents import FlowDisplay
|
||||
return FlowDisplay
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
def create_flow_display(self, title: str = "Workflow") -> Any:
|
||||
"""
|
||||
Create a FlowDisplay instance.
|
||||
|
||||
Args:
|
||||
title: Title for the workflow display
|
||||
|
||||
Returns:
|
||||
FlowDisplay instance or None
|
||||
"""
|
||||
FlowDisplay = self._get_flow_display()
|
||||
if not FlowDisplay:
|
||||
self.print_status(
|
||||
"FlowDisplay requires praisonaiagents. Install with: pip install praisonaiagents",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return FlowDisplay(title=title)
|
||||
except Exception as e:
|
||||
self.log(f"Failed to create FlowDisplay: {e}", "error")
|
||||
return None
|
||||
return False, "rich not installed. Install with: pip install rich"
|
||||
|
||||
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply flow display configuration.
|
||||
|
||||
Args:
|
||||
config: Agent configuration dictionary
|
||||
flag_value: Boolean indicating flow display
|
||||
|
||||
Returns:
|
||||
Modified configuration
|
||||
"""
|
||||
"""Apply flow display configuration."""
|
||||
if flag_value:
|
||||
config['flow_display'] = True
|
||||
return config
|
||||
|
||||
def display_workflow_start(self, workflow_name: str, agents: List[str] = None):
|
||||
def display_flow_diagram(self, yaml_file: str, show_footer: bool = True) -> bool:
|
||||
"""
|
||||
Display workflow start.
|
||||
Display a visual flow diagram of the workflow from YAML file.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow
|
||||
agents: List of agent names
|
||||
"""
|
||||
self.print_status(f"\n🚀 Starting Workflow: {workflow_name}", "info")
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
if agents:
|
||||
self.print_status("Agents:", "info")
|
||||
for i, agent in enumerate(agents, 1):
|
||||
self.print_status(f" {i}. {agent}", "info")
|
||||
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
def display_step(self, step_num: int, step_name: str, status: str = "running"):
|
||||
"""
|
||||
Display a workflow step.
|
||||
|
||||
Args:
|
||||
step_num: Step number
|
||||
step_name: Name of the step
|
||||
status: Step status (running, completed, failed)
|
||||
"""
|
||||
icons = {
|
||||
'running': '⏳',
|
||||
'completed': '✅',
|
||||
'failed': '❌',
|
||||
'pending': '⬜'
|
||||
}
|
||||
icon = icons.get(status, '•')
|
||||
self.print_status(f" {icon} Step {step_num}: {step_name}", "info")
|
||||
|
||||
def display_workflow_end(self, success: bool = True, duration: float = None):
|
||||
"""
|
||||
Display workflow end.
|
||||
|
||||
Args:
|
||||
success: Whether workflow completed successfully
|
||||
duration: Workflow duration in seconds
|
||||
"""
|
||||
self.print_status("=" * 50, "info")
|
||||
|
||||
if success:
|
||||
msg = "✅ Workflow completed successfully"
|
||||
else:
|
||||
msg = "❌ Workflow failed"
|
||||
|
||||
if duration:
|
||||
msg += f" ({duration:.2f}s)"
|
||||
|
||||
self.print_status(msg, "success" if success else "error")
|
||||
|
||||
def track_execution(self, func):
|
||||
"""
|
||||
Decorator to track function execution in flow display.
|
||||
|
||||
Args:
|
||||
func: Function to track
|
||||
yaml_file: Path to the agents.yaml file
|
||||
show_footer: Whether to show the "run without --flow-display" footer
|
||||
|
||||
Returns:
|
||||
Wrapped function
|
||||
True if displayed successfully, False otherwise
|
||||
"""
|
||||
import functools
|
||||
console = self._get_console()
|
||||
if not console:
|
||||
print("Rich library not available for flow display")
|
||||
return False
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
func_name = func.__name__
|
||||
# Load YAML config
|
||||
try:
|
||||
if not os.path.exists(yaml_file):
|
||||
console.print(f"[red]Error: File not found: {yaml_file}[/red]")
|
||||
return False
|
||||
|
||||
self.display_step(1, func_name, "running")
|
||||
with open(yaml_file, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error loading YAML: {e}[/red]")
|
||||
return False
|
||||
|
||||
# Detect YAML format: 'roles' (old) or 'agents'+'steps' (new workflow)
|
||||
has_roles = 'roles' in config and config['roles']
|
||||
has_agents = 'agents' in config and config['agents']
|
||||
has_steps = 'steps' in config and config['steps']
|
||||
|
||||
if not has_roles and not has_agents:
|
||||
console.print("[red]Error: Invalid YAML format. No 'roles' or 'agents' found.[/red]")
|
||||
return False
|
||||
|
||||
try:
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
# Determine format type
|
||||
is_workflow_format = has_agents and has_steps
|
||||
|
||||
# Extract workflow info based on format
|
||||
if is_workflow_format:
|
||||
name = config.get('name', 'Workflow')
|
||||
description = config.get('description', '')
|
||||
framework = config.get('framework', 'praisonai')
|
||||
workflow_config = config.get('workflow', {})
|
||||
memory = workflow_config.get('memory_config', {}).get('persist', False)
|
||||
num_agents = len(config['agents'])
|
||||
num_steps = len(config['steps'])
|
||||
else:
|
||||
name = config.get('topic', 'Workflow')
|
||||
description = ''
|
||||
framework = config.get('framework', 'praisonai')
|
||||
process = config.get('process', 'sequential')
|
||||
memory = config.get('memory', False)
|
||||
num_agents = len(config['roles'])
|
||||
num_steps = sum(len(details.get('tasks', {})) for details in config['roles'].values())
|
||||
|
||||
# Build the flow diagram
|
||||
console.print()
|
||||
|
||||
# === HEADER ===
|
||||
header = Text()
|
||||
header.append("📊 WORKFLOW FLOW DIAGRAM\n", style="bold cyan")
|
||||
header.append("─" * 40, style="dim")
|
||||
|
||||
header_panel = Panel(
|
||||
header,
|
||||
border_style="cyan",
|
||||
box=box.DOUBLE_EDGE,
|
||||
padding=(0, 2)
|
||||
)
|
||||
console.print(header_panel)
|
||||
|
||||
# === WORKFLOW INFO ===
|
||||
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
||||
info_table.add_column("Key", style="bold yellow")
|
||||
info_table.add_column("Value", style="white")
|
||||
|
||||
info_table.add_row("📋 Name:", name)
|
||||
if description:
|
||||
info_table.add_row("📝 Description:", description)
|
||||
|
||||
# Show input if present ('input' is canonical, 'topic' is alias)
|
||||
workflow_input = config.get('input', config.get('topic', ''))
|
||||
if workflow_input:
|
||||
info_table.add_row("💬 Input:", workflow_input)
|
||||
|
||||
info_table.add_row("⚙️ Framework:", framework)
|
||||
|
||||
if is_workflow_format:
|
||||
info_table.add_row("🔄 Format:", "WORKFLOW (steps-based)")
|
||||
planning = workflow_config.get('planning', False)
|
||||
info_table.add_row("🧠 Planning:", "✅ Enabled" if planning else "❌ Disabled")
|
||||
else:
|
||||
info_table.add_row("🔄 Process:", process.upper())
|
||||
|
||||
info_table.add_row("💾 Memory:", "✅ Enabled" if memory else "❌ Disabled")
|
||||
info_table.add_row("👥 Agents:", str(num_agents))
|
||||
info_table.add_row("📝 Steps:", str(num_steps))
|
||||
|
||||
console.print(Panel(info_table, title="[bold white]Workflow Configuration[/bold white]",
|
||||
border_style="blue", box=box.ROUNDED))
|
||||
console.print()
|
||||
|
||||
# === FLOW DIAGRAM ===
|
||||
if is_workflow_format:
|
||||
self._display_steps_flow(console, config)
|
||||
else:
|
||||
process = config.get('process', 'sequential')
|
||||
if process == 'sequential':
|
||||
self._display_sequential_flow(console, config)
|
||||
elif process == 'hierarchical':
|
||||
self._display_hierarchical_flow(console, config)
|
||||
else:
|
||||
self._display_sequential_flow(console, config)
|
||||
|
||||
# === DETAILED AGENT INFO ===
|
||||
console.print()
|
||||
if is_workflow_format:
|
||||
self._display_workflow_agent_details(console, config)
|
||||
else:
|
||||
self._display_agent_details(console, config)
|
||||
|
||||
# === FOOTER ===
|
||||
if show_footer:
|
||||
console.print()
|
||||
footer = Text()
|
||||
footer.append("💡 ", style="bold")
|
||||
footer.append("To execute this workflow, run without --flow-display flag", style="dim italic")
|
||||
console.print(Panel(footer, border_style="dim", box=box.ROUNDED))
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Error: Rich components not available: {e}[/red]")
|
||||
return False
|
||||
|
||||
def _display_sequential_flow(self, console, config: Dict):
|
||||
"""Display sequential flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
roles = list(config['roles'].items())
|
||||
|
||||
# Box width (content area)
|
||||
BOX_WIDTH = 36
|
||||
|
||||
def pad_line(content: str, width: int = BOX_WIDTH) -> str:
|
||||
"""Pad content to fixed width."""
|
||||
padding = width - len(content)
|
||||
if padding > 0:
|
||||
return content + " " * padding
|
||||
return content[:width]
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append(" SEQUENTIAL EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
role_name = details.get('role', role_key)
|
||||
tasks = list(details.get('tasks', {}).keys())
|
||||
|
||||
# Agent box
|
||||
if i == 0:
|
||||
flow_text.append(" ┌── START ──┐\n", style="green")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
# Agent node
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="cyan")
|
||||
line = f" Agent {i+1}: {role_name}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="cyan")
|
||||
|
||||
# Tasks
|
||||
for task in tasks:
|
||||
line = f" -> {task}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim white")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="cyan")
|
||||
|
||||
# Arrow to next
|
||||
if i < len(roles) - 1:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" output\n", style="dim italic")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
else:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
flow_text.append(" └── END ────┘\n", style="red")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_hierarchical_flow(self, console, config: Dict):
|
||||
"""Display hierarchical flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
manager_llm = config.get('manager_llm', 'default')
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append("👑 HIERARCHICAL EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
# Manager at top
|
||||
flow_text.append(" ┌──────────────────────┐\n", style="yellow")
|
||||
flow_text.append(" │ 👑 MANAGER AGENT │\n", style="bold yellow")
|
||||
flow_text.append(f" │ LLM: {manager_llm:<14}│\n", style="dim yellow")
|
||||
flow_text.append(" └──────────┬───────────┘\n", style="yellow")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ┌──────────┴───────────┐\n", style="dim")
|
||||
flow_text.append(" │ delegates to │\n", style="dim italic")
|
||||
flow_text.append(" └──────────┬───────────┘\n", style="dim")
|
||||
|
||||
roles = list(config['roles'].items())
|
||||
num_agents = len(roles)
|
||||
|
||||
# Draw branches
|
||||
if num_agents > 0:
|
||||
branch_line = " "
|
||||
for i in range(num_agents):
|
||||
if i == 0:
|
||||
branch_line += "┌" + "─" * 8
|
||||
elif i == num_agents - 1:
|
||||
branch_line += "┬" + "─" * 8 + "┐"
|
||||
else:
|
||||
branch_line += "┬" + "─" * 8
|
||||
flow_text.append(branch_line + "\n", style="dim")
|
||||
|
||||
# Agent boxes
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
flow_text.append("\n ╔═══════════════╗", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
role_name = details.get('role', role_key)[:12]
|
||||
flow_text.append(f" ║ 🤖 {role_name:<10} ║", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
for i, (role_key, details) in enumerate(roles):
|
||||
flow_text.append(" ╚═══════════════╝", style="cyan")
|
||||
flow_text.append("\n")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_agent_details(self, console, config: Dict):
|
||||
"""Display detailed agent information."""
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
table = Table(
|
||||
title="[bold cyan]📋 Agent & Task Details[/bold cyan]",
|
||||
box=box.ROUNDED,
|
||||
show_header=True,
|
||||
header_style="bold magenta",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
expand=True
|
||||
)
|
||||
|
||||
table.add_column("#", style="dim", width=3, justify="center")
|
||||
table.add_column("Agent", style="bold yellow", min_width=15)
|
||||
table.add_column("Role", style="cyan", min_width=15)
|
||||
table.add_column("Goal", style="white", min_width=25, overflow="fold")
|
||||
table.add_column("Tasks", style="green", min_width=20)
|
||||
table.add_column("Tools", style="magenta", min_width=15)
|
||||
|
||||
for i, (role_key, details) in enumerate(config['roles'].items(), 1):
|
||||
role_name = details.get('role', role_key)
|
||||
goal = details.get('goal', '-')
|
||||
if len(goal) > 50:
|
||||
goal = goal[:47] + "..."
|
||||
|
||||
tasks = list(details.get('tasks', {}).keys())
|
||||
tasks_str = "\n".join(f"• {t}" for t in tasks) if tasks else "-"
|
||||
|
||||
tools = details.get('tools', [])
|
||||
tools_str = ", ".join(t for t in tools if t) if tools else "-"
|
||||
|
||||
table.add_row(
|
||||
str(i),
|
||||
role_name,
|
||||
role_key,
|
||||
goal,
|
||||
tasks_str,
|
||||
tools_str
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def _display_steps_flow(self, console, config: Dict):
|
||||
"""Display steps-based workflow flow diagram."""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
steps = config.get('steps', [])
|
||||
agents = config.get('agents', {})
|
||||
|
||||
# Box width (content area)
|
||||
BOX_WIDTH = 36
|
||||
|
||||
def pad_line(content: str, width: int = BOX_WIDTH) -> str:
|
||||
"""Pad content to fixed width, accounting for emoji width."""
|
||||
# Emojis typically take 2 character widths
|
||||
emoji_count = sum(1 for c in content if ord(c) > 0x1F300)
|
||||
actual_len = len(content) + emoji_count # Add extra for emoji width
|
||||
padding = width - actual_len
|
||||
if padding > 0:
|
||||
return content + " " * padding
|
||||
return content[:width]
|
||||
|
||||
flow_text = Text()
|
||||
flow_text.append(" STEPS-BASED EXECUTION FLOW\n\n", style="bold magenta")
|
||||
|
||||
flow_text.append(" ┌── START ──┐\n", style="green")
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
# Determine step type
|
||||
agent_key = step.get('agent', '')
|
||||
step_name = step.get('name', '')
|
||||
|
||||
# Check for special step types
|
||||
has_route = 'route' in step
|
||||
has_parallel = 'parallel' in step
|
||||
has_loop = 'loop' in step
|
||||
has_repeat = 'repeat' in step
|
||||
|
||||
if has_route:
|
||||
# Route step
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="yellow")
|
||||
line = f" ROUTE: {step_name or 'routing'}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="yellow")
|
||||
|
||||
routes = step['route']
|
||||
for route_name, route_agents in routes.items():
|
||||
agents_str = ", ".join(route_agents) if isinstance(route_agents, list) else str(route_agents)
|
||||
line = f" -> {route_name}: {agents_str}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="yellow")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="yellow")
|
||||
|
||||
elif has_parallel:
|
||||
# Parallel step
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="green")
|
||||
line = f" PARALLEL: {step_name or 'parallel'}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="green")
|
||||
|
||||
for p_step in step['parallel']:
|
||||
p_agent = p_step.get('agent', '?')
|
||||
line = f" |-- {p_agent}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="green")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="green")
|
||||
|
||||
else:
|
||||
# Regular agent step
|
||||
agent_info = agents.get(agent_key, {})
|
||||
agent_name = agent_info.get('name', agent_key)
|
||||
|
||||
flow_text.append(" ╔════════════════════════════════════╗\n", style="cyan")
|
||||
line = f" Step {i+1}: {agent_name}"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="cyan")
|
||||
|
||||
# Show modifiers
|
||||
if has_loop:
|
||||
loop_over = step['loop'].get('over', '?')
|
||||
line = f" [loop over: {loop_over}]"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim magenta")
|
||||
|
||||
if has_repeat:
|
||||
until = step['repeat'].get('until', '?')
|
||||
max_iter = step['repeat'].get('max_iterations', '?')
|
||||
line = f" [repeat until: {until} max:{max_iter}]"
|
||||
flow_text.append(f" ║{pad_line(line)}║\n", style="dim magenta")
|
||||
|
||||
flow_text.append(" ╚════════════════════════════════════╝\n", style="cyan")
|
||||
|
||||
# Arrow to next
|
||||
if i < len(steps) - 1:
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
|
||||
flow_text.append(" │\n", style="dim")
|
||||
flow_text.append(" ▼\n", style="dim")
|
||||
flow_text.append(" └── END ────┘\n", style="red")
|
||||
|
||||
console.print(Panel(flow_text, border_style="magenta", box=box.DOUBLE_EDGE,
|
||||
title="[bold white]Execution Flow[/bold white]"))
|
||||
|
||||
def _display_workflow_agent_details(self, console, config: Dict):
|
||||
"""Display detailed agent information for workflow format."""
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
table = Table(
|
||||
title="[bold cyan]📋 Agent Details[/bold cyan]",
|
||||
box=box.ROUNDED,
|
||||
show_header=True,
|
||||
header_style="bold magenta",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
expand=True
|
||||
)
|
||||
|
||||
table.add_column("#", style="dim", width=3, justify="center")
|
||||
table.add_column("Agent Key", style="bold yellow", min_width=15)
|
||||
table.add_column("Name", style="cyan", min_width=15)
|
||||
table.add_column("Role", style="white", min_width=20)
|
||||
table.add_column("Goal", style="green", min_width=25, overflow="fold")
|
||||
|
||||
for i, (agent_key, details) in enumerate(config['agents'].items(), 1):
|
||||
name = details.get('name', agent_key)
|
||||
role = details.get('role', '-')
|
||||
goal = details.get('goal', '-')
|
||||
if len(goal) > 40:
|
||||
goal = goal[:37] + "..."
|
||||
|
||||
table.add_row(
|
||||
str(i),
|
||||
agent_key,
|
||||
name,
|
||||
role,
|
||||
goal
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def display_workflow_start(self, workflow_name: str, agents: list) -> None:
|
||||
"""
|
||||
Display workflow start message.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow being started
|
||||
agents: List of agent names in the workflow
|
||||
"""
|
||||
console = self._get_console()
|
||||
if console:
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
self.display_step(1, func_name, "completed")
|
||||
return result
|
||||
except Exception:
|
||||
self.display_step(1, func_name, "failed")
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
|
||||
agents_str = ", ".join(agents) if agents else "No agents"
|
||||
content = f"🚀 Starting: {workflow_name}\n👥 Agents: {agents_str}"
|
||||
console.print(Panel(content, title="[bold cyan]Workflow Started[/bold cyan]",
|
||||
border_style="cyan", box=box.ROUNDED))
|
||||
except ImportError:
|
||||
print(f"Starting workflow: {workflow_name} with agents: {agents}")
|
||||
|
||||
def execute(self, workflow_name: str = None, **kwargs) -> Any:
|
||||
def display_workflow_end(self, success: bool = True) -> None:
|
||||
"""
|
||||
Execute flow display setup.
|
||||
Display workflow end message.
|
||||
|
||||
Args:
|
||||
workflow_name: Name of the workflow
|
||||
success: Whether the workflow completed successfully
|
||||
"""
|
||||
console = self._get_console()
|
||||
if console:
|
||||
try:
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
|
||||
if success:
|
||||
content = "✅ Workflow completed successfully"
|
||||
style = "green"
|
||||
else:
|
||||
content = "❌ Workflow failed"
|
||||
style = "red"
|
||||
|
||||
console.print(Panel(content, title=f"[bold {style}]Workflow Ended[/bold {style}]",
|
||||
border_style=style, box=box.ROUNDED))
|
||||
except ImportError:
|
||||
status = "successfully" if success else "with errors"
|
||||
print(f"Workflow ended {status}")
|
||||
|
||||
def execute(self, yaml_file: str = None, **kwargs) -> bool:
|
||||
"""
|
||||
Execute flow display - shows diagram without running workflow.
|
||||
|
||||
Args:
|
||||
yaml_file: Path to the YAML file
|
||||
|
||||
Returns:
|
||||
FlowDisplay instance or None
|
||||
True if displayed successfully
|
||||
"""
|
||||
if workflow_name:
|
||||
self.display_workflow_start(workflow_name)
|
||||
|
||||
return self.create_flow_display(workflow_name or "Workflow")
|
||||
if yaml_file:
|
||||
return self.display_flow_diagram(yaml_file)
|
||||
return False
|
||||
@@ -0,0 +1,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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.22 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 @@
|
||||
__version__ = "2.3.22"
|
||||
__version__ = "2.3.32"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.0",
|
||||
"praisonaiagents>=0.1.13",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -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"])
|
||||
@@ -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...")
|
||||
|
||||
@@ -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
|
||||
Generated
+4
-4
@@ -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.1.0" },
|
||||
{ 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.1.0"
|
||||
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/1a/69/062b8e0867ec673483d83a7529b15d17c15196460060cd7e1542f96c8036/praisonaiagents-0.1.0.tar.gz", hash = "sha256:9c87d8ef965997ab6df2f76badda7911889c6409333b8d2afa20a4f60c80fef9", size = 545876, upload-time = "2025-12-17T00:24:37.466Z" }
|
||||
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/42/11/9f9a8802208e50a44d2e105c6b9743fa08f02378829fb5fad1b3ee9c8b8d/praisonaiagents-0.1.0-py3-none-any.whl", hash = "sha256:bb39f79e92b8a3abe215fcb8f49be896dee539a5e8998f55b14fb8bbbeb86905", size = 516692, upload-time = "2025-12-17T00:24:35.719Z" },
|
||||
{ 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]]
|
||||
|
||||
Reference in new issue
Block a user