Compare commits

...
12 Commits
Author SHA1 Message Date
Mervin Praison c267cdda87 Release v2.3.26 2025-12-17 11:47:04 +00:00
Mervin Praison b88bb04090 feat: Add feature parity between agents.yaml and workflow.yaml
Workflow.yaml now supports all agents.yaml features:

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

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

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

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

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

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

No files matched your search

+262 -147
View File
@@ -40,7 +40,17 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
| 🔄 Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| 🧠 Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| 👁️ Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
| 🎭 AI Agent Workflow (Context Passing, Per-Step Agents) | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/workflows) |
| **🔄 Workflows** | | |
| ↳ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| ↳ Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/features/workflows) |
| 📚 Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| 🧠 Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| 📄 Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
@@ -103,20 +113,6 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| ⏸️ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/cli/session) |
| 🔀 Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔁 Workflow Loops | [Example](examples/python/workflows/workflow_loops.py) | [📖](https://docs.praison.ai/features/workflows) |
| 📞 Task Callbacks | [Example](examples/python/workflows/task_callbacks.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🛑 Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ⚡ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🤖 Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔄 Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🎭 Mixed Steps | [Example](examples/python/workflows/workflow_mixed_steps.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔀 Agentic Routing | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/workflow-routing) |
| ⚡ Parallel Execution | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/workflow-parallel) |
| 🔁 Loop over List | [Example](examples/python/workflows/workflow_loop_list.py) | [📖](https://docs.praison.ai/features/workflow-loop) |
| 📄 Loop over CSV | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/workflow-loop) |
| 🔄 Evaluator-Optimizer | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/workflow-repeat) |
## Supported Providers
@@ -410,174 +406,203 @@ memories = auto.process_interaction(
# Extracts: name="John", preference="Python for backend"
```
### 8. Workflows
### 8. Agentic Workflows
Create reusable multi-step workflows with context passing and per-step agents:
Create powerful multi-agent workflows with the `Workflow` class:
```python
from praisonaiagents import Agent
from praisonaiagents.memory import WorkflowManager, Workflow, WorkflowStep
from praisonaiagents import Agent, Workflow
# Simple execution with default agent
agent = Agent(name="Assistant", llm="gpt-4o-mini")
manager = WorkflowManager()
result = manager.execute(
"deploy",
default_agent=agent,
variables={"environment": "production"}
# Create agents
researcher = Agent(
name="Researcher",
role="Research Analyst",
goal="Research topics thoroughly",
instructions="Provide concise, factual information."
)
# Advanced: Per-step agent configuration
workflow = Workflow(
name="research_pipeline",
default_llm="gpt-4o-mini",
steps=[
WorkflowStep(
name="research",
action="Research {{topic}}",
agent_config={"role": "Researcher", "goal": "Find information"},
tools=["tavily_search"]
),
WorkflowStep(
name="write",
action="Write report based on {{previous_output}}",
agent_config={"role": "Writer", "goal": "Write content"},
context_from=["research"] # Only include research output
)
]
writer = Agent(
name="Writer",
role="Content Writer",
goal="Write engaging content",
instructions="Write clear, engaging content based on research."
)
# Async execution
import asyncio
result = asyncio.run(manager.aexecute("deploy", default_llm="gpt-4o-mini"))
# Create workflow with agents as steps
workflow = Workflow(steps=[researcher, writer])
# Run workflow - agents process sequentially
result = workflow.start("What are the benefits of AI agents?")
print(result["output"])
```
**Key Features:**
- **Context Passing**: Use `{{previous_output}}` and `{{step_name_output}}` variables
- **Per-Step Agents**: Configure different agents with roles, goals, tools for each step
- **Async Execution**: Use `aexecute()` for async workflows
- **Planning Mode**: Enable at workflow level with `planning=True`
- **Branching**: Use `next_steps` and `branch_condition` for conditional routing
- **Loops**: Use `loop_over` and `loop_var` to iterate over data
### Choosing the Right Workflow System
| Use Case | Recommended |
|----------|-------------|
| Simple function pipelines | `Workflow` class ⭐ |
| Agent-only pipelines | `Workflow` class |
| CSV batch processing | `process="workflow"` |
| Complex task routing | `process="workflow"` |
| Markdown templates | `WorkflowManager` |
| Early stop / conditional | `Workflow` class |
### Simple Workflow (Recommended)
The easiest way to create workflows - just pass functions as steps:
```python
from praisonaiagents import Workflow, WorkflowContext, StepResult
# Define simple handler functions
def validate(ctx: WorkflowContext) -> StepResult:
if not ctx.input:
return StepResult(output="No input", stop_workflow=True)
return StepResult(output=f"Valid: {ctx.input}")
def process(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"Processed: {ctx.previous_result}")
# Create and run workflow
workflow = Workflow(steps=[validate, process])
result = workflow.start("Hello World", verbose=True)
print(result["output"]) # "Processed: Valid: Hello World"
```
**Key Features:**
- **Just pass functions** - No complex configuration needed
- **Early stop** - Return `stop_workflow=True` to stop the workflow
- **Context passing** - Access `ctx.input`, `ctx.previous_result`, `ctx.variables`
- **Verbose mode** - See step-by-step progress
### Workflow Branching & Loops
```python
from praisonaiagents import WorkflowStep
# Branching step
decision_step = WorkflowStep(
name="decide",
action="Evaluate if task is complete",
next_steps=["success_step", "retry_step"],
branch_condition={"success": ["success_step"], "failure": ["retry_step"]}
)
# Loop step
loop_step = WorkflowStep(
name="process_items",
action="Process {{item}}",
loop_over="items", # Variable containing list
loop_var="item" # Current item variable name
)
```
- **Agent-first** - Pass `Agent` objects directly as workflow steps
- **Pattern helpers** - Use `route()`, `parallel()`, `loop()`, `repeat()`
- **Planning mode** - Enable with `planning=True`
- **Callbacks** - Monitor with `on_step_complete`, `on_workflow_complete`
- **Async execution** - Use `workflow.astart()` for async
### Workflow Patterns (route, parallel, loop, repeat)
```python
from praisonaiagents import Workflow, WorkflowContext, StepResult
# Or use Pipeline (alias for Workflow)
from praisonaiagents import Pipeline
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import route, parallel, loop, repeat
# 1. ROUTING - Decision-based branching
# 1. ROUTING - Classifier agent routes to specialized agents
classifier = Agent(name="Classifier", instructions="Respond with 'technical' or 'creative'")
tech_agent = Agent(name="TechExpert", role="Technical Expert")
creative_agent = Agent(name="Creative", role="Creative Writer")
workflow = Workflow(steps=[
classify_request, # Returns "approve" or "reject"
classifier,
route({
"approve": [approve_handler],
"reject": [reject_handler],
"default": [fallback_handler]
"technical": [tech_agent],
"creative": [creative_agent]
})
])
# 2. PARALLEL - Concurrent execution
# 2. PARALLEL - Multiple agents work concurrently
market_agent = Agent(name="Market", role="Market Researcher")
competitor_agent = Agent(name="Competitor", role="Competitor Analyst")
aggregator = Agent(name="Aggregator", role="Synthesizer")
workflow = Workflow(steps=[
parallel([research_market, research_competitors, research_customers]),
summarize_results # Gets all parallel outputs
parallel([market_agent, competitor_agent]),
aggregator
])
# 3. LOOP - Iterate over list or CSV
# 3. LOOP - Agent processes each item
processor = Agent(name="Processor", role="Item Processor")
summarizer = Agent(name="Summarizer", role="Summarizer")
workflow = Workflow(
steps=[loop(process_item, over="items")],
variables={"items": ["a", "b", "c"]}
steps=[loop(processor, over="items"), summarizer],
variables={"items": ["AI", "ML", "NLP"]}
)
# Or from CSV file:
workflow = Workflow(steps=[loop(process_row, from_csv="data.csv")])
# 4. REPEAT - Evaluator-Optimizer pattern
generator = Agent(name="Generator", role="Content Generator")
evaluator = Agent(name="Evaluator", instructions="Say 'APPROVED' if good")
workflow = Workflow(steps=[
repeat(
generator,
until=lambda ctx: "done" in ctx.previous_result,
max_iterations=5
)
generator,
repeat(evaluator, until=lambda ctx: "approved" in ctx.previous_result.lower(), max_iterations=3)
])
# 5. CALLBACKS - Monitor workflow execution
# 5. CALLBACKS
workflow = Workflow(
steps=[step1, step2],
on_workflow_start=lambda w, i: print(f"Starting: {i}"),
on_step_complete=lambda name, r: print(f"{name}: {r.output[:50]}"),
on_workflow_complete=lambda w, r: print(f"Done: {r['status']}")
steps=[researcher, writer],
on_step_complete=lambda name, r: print(f"✅ {name} done")
)
# 6. GUARDRAILS - Validate and retry
def validate(result):
return ("error" not in result.output, "Fix the error")
# 6. WITH PLANNING & REASONING
workflow = Workflow(
steps=[researcher, writer],
planning=True,
reasoning=True
)
workflow = Workflow(steps=[
WorkflowStep(name="gen", handler=generator, guardrail=validate, max_retries=3)
])
# 7. ASYNC EXECUTION
result = asyncio.run(workflow.astart("input"))
# 8. STATUS TRACKING
workflow.status # "not_started" | "running" | "completed"
workflow.step_statuses # {"step1": "completed", "step2": "skipped"}
```
### YAML Workflow Template
```yaml
# .praison/workflows/research.yaml
name: Research Workflow
description: Research and write content with all patterns
agents:
researcher:
role: Research Expert
goal: Find accurate information
tools: [tavily_search, web_scraper]
writer:
role: Content Writer
goal: Write engaging content
editor:
role: Editor
goal: Polish content
steps:
# Sequential
- agent: researcher
action: Research {{topic}}
output_variable: research_data
# Routing
- name: classifier
action: Classify content type
route:
technical: [tech_handler]
creative: [creative_handler]
default: [general_handler]
# Parallel
- name: parallel_research
parallel:
- agent: researcher
action: Research market
- agent: researcher
action: Research competitors
# Loop
- agent: writer
action: Write about {{item}}
loop_over: topics
loop_var: item
# Repeat (evaluator-optimizer)
- agent: editor
action: Review and improve
repeat:
until: "quality > 8"
max_iterations: 3
# Output to file
- agent: writer
action: Write final report
output_file: output/{{topic}}_report.md
variables:
topic: AI trends
topics: [ML, NLP, Vision]
workflow:
planning: true
planning_llm: gpt-4o
memory_config:
provider: chroma
persist: true
```
### Loading YAML Workflows
```python
from praisonaiagents.workflows import YAMLWorkflowParser, WorkflowManager
# Option 1: Parse YAML string
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
result = workflow.start("Research AI trends")
# Option 2: Load from file with WorkflowManager
manager = WorkflowManager()
workflow = manager.load_yaml("research_workflow.yaml")
result = workflow.start("Research AI trends")
# Option 3: Execute YAML directly
result = manager.execute_yaml(
"research_workflow.yaml",
input_data="Research AI trends",
variables={"topic": "Machine Learning"}
)
```
### 9. Hooks
@@ -596,6 +621,75 @@ hooks.register("pre_write_code", lambda ctx: print(f"Writing {ctx['file']}"))
result = hooks.execute("pre_write_code", {"file": "main.py"})
```
### 10. Extended agents.yaml with Workflow Patterns
You can now use advanced workflow patterns directly in agents.yaml by setting `process: workflow`:
```yaml
# agents.yaml with workflow patterns
framework: praisonai
process: workflow # Enables workflow mode
topic: "Research AI trends"
workflow:
planning: true
reasoning: true
verbose: true
variables:
topic: AI trends
roles:
classifier:
role: Request Classifier
backstory: "Classify requests into categories"
goal: Classify requests
researcher:
role: Research Analyst
backstory: "Expert researcher"
goal: Research topics
tools:
- tavily_search
steps:
# Sequential step
- agent: classifier
action: "Classify: {{topic}}"
# Route pattern - decision-based branching
- name: routing
route:
technical: [tech_expert]
default: [researcher]
# Parallel pattern - concurrent execution
- name: parallel_research
parallel:
- agent: researcher
action: "Research market trends"
- agent: researcher
action: "Research competitors"
# Loop pattern - iterate over items
- agent: researcher
action: "Analyze {{item}}"
loop:
over: topics
# Repeat pattern - evaluator-optimizer
- agent: aggregator
action: "Synthesize findings"
repeat:
until: "comprehensive"
max_iterations: 3
```
Run with the same simple command:
```bash
praisonai agents.yaml
```
## Using No Code
### Auto Mode:
@@ -723,6 +817,12 @@ praisonai workflow run deploy --workflow-var environment=staging --workflow-var
# Execute with planning mode (AI creates sub-steps for each workflow step)
praisonai workflow run "Research Blog" --planning --verbose
# Execute with reasoning mode (chain-of-thought)
praisonai workflow run "Analysis" --reasoning --verbose
# Execute with memory enabled
praisonai workflow run "Research" --memory
# Show workflow details
praisonai workflow show deploy
@@ -734,8 +834,23 @@ praisonai "What is AI?" --workflow "Research,Summarize" --save
# Inline workflow with step actions
praisonai "GPT-5" --workflow "Research:Search for info,Write:Write blog" --tools tavily
# Workflow CLI help
praisonai workflow help
```
**Workflow CLI Options:**
| Flag | Description |
|------|-------------|
| `--workflow-var key=value` | Set workflow variable (can be repeated) |
| `--llm <model>` | LLM model (e.g., openai/gpt-4o-mini) |
| `--tools <tools>` | Tools (comma-separated, e.g., tavily) |
| `--planning` | Enable planning mode |
| `--reasoning` | Enable reasoning mode |
| `--memory` | Enable memory |
| `--verbose` | Enable verbose output |
| `--save` | Save output to file |
### Hooks CLI:
```bash
# List configured hooks
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
flask \
"praisonai>=2.3.22" \
"praisonai>=2.3.26" \
"praisonai[api]" \
gunicorn \
markdown
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.22" \
"praisonai>=2.3.26" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.22" \
"praisonai>=2.3.26" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.22" \
"praisonai>=2.3.26" \
"praisonai[ui]" \
"praisonai[crewai]"
+26 -24
View File
@@ -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']}")
+34 -30
View File
@@ -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)
+37 -34
View File
@@ -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'])}")
+34 -50
View File
@@ -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}")
+44 -40
View File
@@ -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']}")
+117
View File
@@ -0,0 +1,117 @@
# Extended agents.yaml with Workflow Patterns
# This file demonstrates the extended agents.yaml format that supports
# advanced workflow patterns while maintaining backward compatibility.
#
# Usage: praisonai agents_workflow.yaml
#
# Key differences from standard agents.yaml:
# - process: workflow (triggers workflow mode)
# - workflow: section for planning, reasoning, memory
# - steps: section for workflow patterns (route, parallel, loop, repeat)
framework: praisonai
process: workflow
topic: "Research AI trends and create a comprehensive report"
# 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
roles:
classifier:
role: Request Classifier
backstory: |
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
backstory: |
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
backstory: |
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
backstory: |
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
backstory: |
You analyze market trends and competitive landscapes.
You provide actionable business insights.
goal: Analyze market trends and opportunities
aggregator:
role: Synthesizer
backstory: |
You combine findings from multiple sources into coherent reports.
You identify patterns and key insights.
goal: Synthesize all findings into comprehensive reports
# Workflow steps with patterns (new)
steps:
# Step 1: Classify the request
- agent: classifier
action: "Classify this request: {{topic}}"
# Step 2: Route to appropriate handler based on classification
- name: routing
route:
technical: [tech_expert]
creative: [creative_writer]
default: [researcher]
# Step 3: Parallel research on multiple aspects
- name: parallel_research
parallel:
- agent: researcher
action: "Research technology trends for {{topic}}"
- agent: market_analyst
action: "Analyze market opportunities for {{topic}}"
# Step 4: Loop over specific topics
- agent: researcher
action: "Deep dive into {{item}}"
loop:
over: topics
# Step 5: Aggregate all findings
- agent: aggregator
action: "Synthesize all research findings into a comprehensive report"
repeat:
until: "comprehensive"
max_iterations: 2
# Optional callbacks
callbacks:
on_step_complete: log_step
@@ -0,0 +1,116 @@
# Complete Workflow Example
# Demonstrates all workflow patterns in a single comprehensive workflow
name: Complete Research Workflow
description: A comprehensive workflow demonstrating all patterns - route, parallel, loop, repeat
framework: praisonai
workflow:
planning: true
reasoning: true
verbose: true
memory_config:
provider: chroma
persist: true
variables:
topic: AI trends
topics:
- Machine Learning
- Neural Networks
- Natural Language Processing
agents:
classifier:
name: Classifier
role: Request Classifier
goal: Classify incoming requests
instructions: |
Classify the request. Respond with ONLY one word:
- 'technical' for technical questions
- 'creative' for creative requests
- 'research' for research tasks
researcher:
name: Researcher
role: Research Analyst
goal: Research topics thoroughly
instructions: "You are a research analyst. Provide concise, factual research findings."
tech_expert:
name: TechExpert
role: Technical Expert
goal: Handle technical questions
instructions: "You are a technical expert. Provide detailed technical answers."
creative_writer:
name: CreativeWriter
role: Creative Writer
goal: Handle creative requests
instructions: "You are a creative writer. Write engaging, creative content."
market_analyst:
name: MarketAnalyst
role: Market Analyst
goal: Analyze market trends
instructions: "You are a market analyst. Provide market insights."
competitor_analyst:
name: CompetitorAnalyst
role: Competitor Analyst
goal: Analyze competitors
instructions: "You are a competitor analyst. Provide competitive insights."
aggregator:
name: Aggregator
role: Synthesizer
goal: Combine and synthesize findings
instructions: "You are a synthesizer. Combine all findings into a comprehensive report."
evaluator:
name: Evaluator
role: Quality Evaluator
goal: Evaluate content quality
instructions: |
Evaluate the content. If it's comprehensive and well-structured, respond with "APPROVED".
Otherwise, provide specific feedback for improvement.
steps:
# Step 1: Classify the request
- agent: classifier
action: "Classify: {{input}}"
# Step 2: Route to appropriate handler
- name: routing
route:
technical: [tech_expert]
creative: [creative_writer]
default: [researcher]
# Step 3: Parallel research
- name: parallel_research
parallel:
- agent: market_analyst
action: "Analyze market for {{topic}}"
- agent: competitor_analyst
action: "Analyze competitors for {{topic}}"
# Step 4: Loop over topics
- agent: researcher
action: "Research {{item}}"
loop:
over: topics
# Step 5: Aggregate findings
- agent: aggregator
action: "Synthesize all findings into a comprehensive report"
# Step 6: Evaluator-optimizer
- agent: evaluator
action: "Evaluate the report quality"
repeat:
until: "approved"
max_iterations: 2
callbacks:
on_step_complete: log_step
@@ -0,0 +1,43 @@
# Evaluator-Optimizer Workflow
# Demonstrates the repeat() pattern for iterative improvement
name: Evaluator Optimizer Workflow
description: A generator creates content, an evaluator reviews it repeatedly until quality standards are met
workflow:
verbose: true
agents:
generator:
name: Generator
role: Content Generator
goal: Generate high-quality content
instructions: |
You are a content generator. Generate content based on the request.
If feedback is provided, improve your content based on that feedback.
Always strive for clarity, accuracy, and engagement.
evaluator:
name: Evaluator
role: Content Evaluator
goal: Evaluate content quality and provide feedback
instructions: |
You are a content evaluator. Review the content provided.
If the content is high quality (clear, accurate, engaging), respond with:
"APPROVED: [brief reason]"
If the content needs improvement, respond with:
"NEEDS IMPROVEMENT: [specific feedback for improvement]"
Be constructive and specific in your feedback.
steps:
- agent: generator
action: "Generate content for: {{input}}"
- agent: evaluator
action: "Evaluate this content and provide feedback: {{previous_output}}"
repeat:
until: "approved"
max_iterations: 3
+152
View File
@@ -0,0 +1,152 @@
"""
Example usage of YAML Workflow Parser.
This script demonstrates how to load and execute YAML workflow files.
"""
import os
from pathlib import Path
# Ensure OPENAI_API_KEY is set
if not os.getenv("OPENAI_API_KEY"):
print("Please set OPENAI_API_KEY environment variable")
print("export OPENAI_API_KEY='your-api-key'")
exit(1)
from praisonaiagents.workflows import YAMLWorkflowParser, WorkflowManager
def example_1_parse_and_run():
"""Example 1: Parse YAML and run workflow directly."""
print("=" * 60)
print("Example 1: Parse YAML and run workflow")
print("=" * 60)
yaml_content = """
name: Quick Research
description: A quick research workflow
agents:
researcher:
name: Researcher
role: Research Analyst
goal: Research topics
instructions: "Provide brief, factual information."
steps:
- agent: researcher
action: "Research: {{input}}"
"""
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
print(f"Workflow Name: {workflow.name}")
print(f"Steps: {len(workflow.steps)}")
# Run the workflow
result = workflow.start("What is Python?")
print(f"Result: {result['output'][:200]}...")
print()
def example_2_load_yaml_file():
"""Example 2: Load workflow from YAML file using WorkflowManager."""
print("=" * 60)
print("Example 2: Load YAML file with WorkflowManager")
print("=" * 60)
# Get the directory of this script
script_dir = Path(__file__).parent
yaml_file = script_dir / "simple_workflow.yaml"
if not yaml_file.exists():
print(f"YAML file not found: {yaml_file}")
return
manager = WorkflowManager()
workflow = manager.load_yaml(yaml_file)
print(f"Loaded workflow: {workflow.name}")
print(f"Steps: {len(workflow.steps)}")
# Run the workflow
result = workflow.start("Benefits of renewable energy")
print(f"Result: {result['output'][:200]}...")
print()
def example_3_execute_yaml_directly():
"""Example 3: Execute YAML file directly with WorkflowManager."""
print("=" * 60)
print("Example 3: Execute YAML directly")
print("=" * 60)
script_dir = Path(__file__).parent
yaml_file = script_dir / "simple_workflow.yaml"
if not yaml_file.exists():
print(f"YAML file not found: {yaml_file}")
return
manager = WorkflowManager()
result = manager.execute_yaml(
yaml_file,
input_data="The future of AI",
verbose=True
)
print(f"Status: {result['status']}")
print(f"Result: {result['output'][:200]}...")
print()
def example_4_with_variables():
"""Example 4: YAML workflow with variables."""
print("=" * 60)
print("Example 4: Workflow with variables")
print("=" * 60)
yaml_content = """
name: Variable Workflow
variables:
topic: Machine Learning
style: concise
agents:
writer:
name: Writer
role: Content Writer
goal: Write content
instructions: "Write {{style}} content about the topic."
steps:
- agent: writer
action: "Write about {{topic}}"
"""
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
print(f"Variables: {workflow.variables}")
# Run with default variables
result = workflow.start("")
print(f"Result: {result['output'][:200]}...")
print()
if __name__ == "__main__":
print("YAML Workflow Examples")
print("=" * 60)
print()
# Run examples
example_1_parse_and_run()
example_2_load_yaml_file()
example_3_execute_yaml_directly()
example_4_with_variables()
print("=" * 60)
print("All examples completed!")
print("=" * 60)
@@ -0,0 +1,42 @@
# Loop Workflow
# Demonstrates the loop() pattern for iterating over items
name: Repetitive Processing Workflow
description: An agent processes multiple items in a loop, then a summarizer combines results
workflow:
verbose: true
variables:
topics:
- Artificial Intelligence
- Machine Learning
- Neural Networks
- Natural Language Processing
agents:
processor:
name: Processor
role: Topic Processor
goal: Process and analyze each topic thoroughly
instructions: |
You are a topic processor. For each topic given, provide:
1. A brief definition
2. Key concepts
3. Real-world applications
Keep your response concise but informative.
summarizer:
name: Summarizer
role: Results Summarizer
goal: Summarize all processed results into a final report
instructions: "You are a summarizer. Combine all the processed topics into a cohesive final report."
steps:
- agent: processor
action: "Analyze this topic: {{item}}"
loop:
over: topics
- agent: summarizer
action: "Create a comprehensive summary of all the analyzed topics"
@@ -0,0 +1,46 @@
# 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
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"
@@ -0,0 +1,49 @@
# Routing Workflow
# Demonstrates the route() pattern for decision-based branching
name: Agentic Routing Workflow
description: A classifier agent routes requests to specialized handler agents
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,28 @@
# 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
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}}"
@@ -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,8 @@ 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
# Callbacks (like process="workflow")
on_workflow_start: Optional[Callable[['Workflow', str], None]] = None # (workflow, input)
@@ -325,6 +347,9 @@ class Workflow:
# Use default LLM if not specified
model = llm or self.default_llm or "gpt-4o-mini"
# Use workflow verbose setting if not overridden
verbose = verbose or self.verbose
# Add input to variables
all_variables = {**self.variables, "input": input}
@@ -341,6 +366,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):
@@ -456,19 +488,57 @@ 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:
action = action.replace("{{previous_output}}", str(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)
else:
output = step.agent.chat(action)
# 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
)
# Substitute variables in action
action = step.action
@@ -476,6 +546,12 @@ class Workflow:
action = action.replace(f"{{{{{key}}}}}", str(value))
if previous_output:
action = action.replace("{{previous_output}}", str(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)
@@ -526,6 +602,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,
@@ -569,6 +662,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 +726,89 @@ 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)
return WorkflowStep(
name=getattr(step, 'name', f'agent_{index+1}'),
agent=step,
tools=agent_tools,
action="{{previous_output}}" if index > 0 else "{{input}}"
)
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}',
@@ -656,7 +852,14 @@ 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:
action = action.replace("{{previous_output}}", str(previous_output))
action = action.replace("{{input}}", input)
output = normalized.agent.chat(action)
except Exception as e:
output = f"Error: {e}"
elif normalized.action:
@@ -1031,6 +1234,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 +1316,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 +1364,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 +1385,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 +1594,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,641 @@
"""
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)
return self._parse_workflow_data(data)
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')
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,
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
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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.1.0"
version = "0.1.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -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):
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):
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):
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):
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):
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):
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):
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
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.1.0"
version = "0.1.4"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+262 -147
View File
@@ -40,7 +40,17 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
| 🔄 Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| 🧠 Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| 👁️ Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
| 🎭 AI Agent Workflow (Context Passing, Per-Step Agents) | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/workflows) |
| **🔄 Workflows** | | |
| ↳ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| ↳ Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/features/workflows) |
| 📚 Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| 🧠 Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| 📄 Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
@@ -103,20 +113,6 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| ⏸️ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/cli/session) |
| 🔀 Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔁 Workflow Loops | [Example](examples/python/workflows/workflow_loops.py) | [📖](https://docs.praison.ai/features/workflows) |
| 📞 Task Callbacks | [Example](examples/python/workflows/task_callbacks.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🛑 Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ⚡ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🤖 Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔄 Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🎭 Mixed Steps | [Example](examples/python/workflows/workflow_mixed_steps.py) | [📖](https://docs.praison.ai/features/workflows) |
| 🔀 Agentic Routing | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/workflow-routing) |
| ⚡ Parallel Execution | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/workflow-parallel) |
| 🔁 Loop over List | [Example](examples/python/workflows/workflow_loop_list.py) | [📖](https://docs.praison.ai/features/workflow-loop) |
| 📄 Loop over CSV | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/workflow-loop) |
| 🔄 Evaluator-Optimizer | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/workflow-repeat) |
## Supported Providers
@@ -410,174 +406,203 @@ memories = auto.process_interaction(
# Extracts: name="John", preference="Python for backend"
```
### 8. Workflows
### 8. Agentic Workflows
Create reusable multi-step workflows with context passing and per-step agents:
Create powerful multi-agent workflows with the `Workflow` class:
```python
from praisonaiagents import Agent
from praisonaiagents.memory import WorkflowManager, Workflow, WorkflowStep
from praisonaiagents import Agent, Workflow
# Simple execution with default agent
agent = Agent(name="Assistant", llm="gpt-4o-mini")
manager = WorkflowManager()
result = manager.execute(
"deploy",
default_agent=agent,
variables={"environment": "production"}
# Create agents
researcher = Agent(
name="Researcher",
role="Research Analyst",
goal="Research topics thoroughly",
instructions="Provide concise, factual information."
)
# Advanced: Per-step agent configuration
workflow = Workflow(
name="research_pipeline",
default_llm="gpt-4o-mini",
steps=[
WorkflowStep(
name="research",
action="Research {{topic}}",
agent_config={"role": "Researcher", "goal": "Find information"},
tools=["tavily_search"]
),
WorkflowStep(
name="write",
action="Write report based on {{previous_output}}",
agent_config={"role": "Writer", "goal": "Write content"},
context_from=["research"] # Only include research output
)
]
writer = Agent(
name="Writer",
role="Content Writer",
goal="Write engaging content",
instructions="Write clear, engaging content based on research."
)
# Async execution
import asyncio
result = asyncio.run(manager.aexecute("deploy", default_llm="gpt-4o-mini"))
# Create workflow with agents as steps
workflow = Workflow(steps=[researcher, writer])
# Run workflow - agents process sequentially
result = workflow.start("What are the benefits of AI agents?")
print(result["output"])
```
**Key Features:**
- **Context Passing**: Use `{{previous_output}}` and `{{step_name_output}}` variables
- **Per-Step Agents**: Configure different agents with roles, goals, tools for each step
- **Async Execution**: Use `aexecute()` for async workflows
- **Planning Mode**: Enable at workflow level with `planning=True`
- **Branching**: Use `next_steps` and `branch_condition` for conditional routing
- **Loops**: Use `loop_over` and `loop_var` to iterate over data
### Choosing the Right Workflow System
| Use Case | Recommended |
|----------|-------------|
| Simple function pipelines | `Workflow` class ⭐ |
| Agent-only pipelines | `Workflow` class |
| CSV batch processing | `process="workflow"` |
| Complex task routing | `process="workflow"` |
| Markdown templates | `WorkflowManager` |
| Early stop / conditional | `Workflow` class |
### Simple Workflow (Recommended)
The easiest way to create workflows - just pass functions as steps:
```python
from praisonaiagents import Workflow, WorkflowContext, StepResult
# Define simple handler functions
def validate(ctx: WorkflowContext) -> StepResult:
if not ctx.input:
return StepResult(output="No input", stop_workflow=True)
return StepResult(output=f"Valid: {ctx.input}")
def process(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"Processed: {ctx.previous_result}")
# Create and run workflow
workflow = Workflow(steps=[validate, process])
result = workflow.start("Hello World", verbose=True)
print(result["output"]) # "Processed: Valid: Hello World"
```
**Key Features:**
- **Just pass functions** - No complex configuration needed
- **Early stop** - Return `stop_workflow=True` to stop the workflow
- **Context passing** - Access `ctx.input`, `ctx.previous_result`, `ctx.variables`
- **Verbose mode** - See step-by-step progress
### Workflow Branching & Loops
```python
from praisonaiagents import WorkflowStep
# Branching step
decision_step = WorkflowStep(
name="decide",
action="Evaluate if task is complete",
next_steps=["success_step", "retry_step"],
branch_condition={"success": ["success_step"], "failure": ["retry_step"]}
)
# Loop step
loop_step = WorkflowStep(
name="process_items",
action="Process {{item}}",
loop_over="items", # Variable containing list
loop_var="item" # Current item variable name
)
```
- **Agent-first** - Pass `Agent` objects directly as workflow steps
- **Pattern helpers** - Use `route()`, `parallel()`, `loop()`, `repeat()`
- **Planning mode** - Enable with `planning=True`
- **Callbacks** - Monitor with `on_step_complete`, `on_workflow_complete`
- **Async execution** - Use `workflow.astart()` for async
### Workflow Patterns (route, parallel, loop, repeat)
```python
from praisonaiagents import Workflow, WorkflowContext, StepResult
# Or use Pipeline (alias for Workflow)
from praisonaiagents import Pipeline
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import route, parallel, loop, repeat
# 1. ROUTING - Decision-based branching
# 1. ROUTING - Classifier agent routes to specialized agents
classifier = Agent(name="Classifier", instructions="Respond with 'technical' or 'creative'")
tech_agent = Agent(name="TechExpert", role="Technical Expert")
creative_agent = Agent(name="Creative", role="Creative Writer")
workflow = Workflow(steps=[
classify_request, # Returns "approve" or "reject"
classifier,
route({
"approve": [approve_handler],
"reject": [reject_handler],
"default": [fallback_handler]
"technical": [tech_agent],
"creative": [creative_agent]
})
])
# 2. PARALLEL - Concurrent execution
# 2. PARALLEL - Multiple agents work concurrently
market_agent = Agent(name="Market", role="Market Researcher")
competitor_agent = Agent(name="Competitor", role="Competitor Analyst")
aggregator = Agent(name="Aggregator", role="Synthesizer")
workflow = Workflow(steps=[
parallel([research_market, research_competitors, research_customers]),
summarize_results # Gets all parallel outputs
parallel([market_agent, competitor_agent]),
aggregator
])
# 3. LOOP - Iterate over list or CSV
# 3. LOOP - Agent processes each item
processor = Agent(name="Processor", role="Item Processor")
summarizer = Agent(name="Summarizer", role="Summarizer")
workflow = Workflow(
steps=[loop(process_item, over="items")],
variables={"items": ["a", "b", "c"]}
steps=[loop(processor, over="items"), summarizer],
variables={"items": ["AI", "ML", "NLP"]}
)
# Or from CSV file:
workflow = Workflow(steps=[loop(process_row, from_csv="data.csv")])
# 4. REPEAT - Evaluator-Optimizer pattern
generator = Agent(name="Generator", role="Content Generator")
evaluator = Agent(name="Evaluator", instructions="Say 'APPROVED' if good")
workflow = Workflow(steps=[
repeat(
generator,
until=lambda ctx: "done" in ctx.previous_result,
max_iterations=5
)
generator,
repeat(evaluator, until=lambda ctx: "approved" in ctx.previous_result.lower(), max_iterations=3)
])
# 5. CALLBACKS - Monitor workflow execution
# 5. CALLBACKS
workflow = Workflow(
steps=[step1, step2],
on_workflow_start=lambda w, i: print(f"Starting: {i}"),
on_step_complete=lambda name, r: print(f"{name}: {r.output[:50]}"),
on_workflow_complete=lambda w, r: print(f"Done: {r['status']}")
steps=[researcher, writer],
on_step_complete=lambda name, r: print(f"✅ {name} done")
)
# 6. GUARDRAILS - Validate and retry
def validate(result):
return ("error" not in result.output, "Fix the error")
# 6. WITH PLANNING & REASONING
workflow = Workflow(
steps=[researcher, writer],
planning=True,
reasoning=True
)
workflow = Workflow(steps=[
WorkflowStep(name="gen", handler=generator, guardrail=validate, max_retries=3)
])
# 7. ASYNC EXECUTION
result = asyncio.run(workflow.astart("input"))
# 8. STATUS TRACKING
workflow.status # "not_started" | "running" | "completed"
workflow.step_statuses # {"step1": "completed", "step2": "skipped"}
```
### YAML Workflow Template
```yaml
# .praison/workflows/research.yaml
name: Research Workflow
description: Research and write content with all patterns
agents:
researcher:
role: Research Expert
goal: Find accurate information
tools: [tavily_search, web_scraper]
writer:
role: Content Writer
goal: Write engaging content
editor:
role: Editor
goal: Polish content
steps:
# Sequential
- agent: researcher
action: Research {{topic}}
output_variable: research_data
# Routing
- name: classifier
action: Classify content type
route:
technical: [tech_handler]
creative: [creative_handler]
default: [general_handler]
# Parallel
- name: parallel_research
parallel:
- agent: researcher
action: Research market
- agent: researcher
action: Research competitors
# Loop
- agent: writer
action: Write about {{item}}
loop_over: topics
loop_var: item
# Repeat (evaluator-optimizer)
- agent: editor
action: Review and improve
repeat:
until: "quality > 8"
max_iterations: 3
# Output to file
- agent: writer
action: Write final report
output_file: output/{{topic}}_report.md
variables:
topic: AI trends
topics: [ML, NLP, Vision]
workflow:
planning: true
planning_llm: gpt-4o
memory_config:
provider: chroma
persist: true
```
### Loading YAML Workflows
```python
from praisonaiagents.workflows import YAMLWorkflowParser, WorkflowManager
# Option 1: Parse YAML string
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
result = workflow.start("Research AI trends")
# Option 2: Load from file with WorkflowManager
manager = WorkflowManager()
workflow = manager.load_yaml("research_workflow.yaml")
result = workflow.start("Research AI trends")
# Option 3: Execute YAML directly
result = manager.execute_yaml(
"research_workflow.yaml",
input_data="Research AI trends",
variables={"topic": "Machine Learning"}
)
```
### 9. Hooks
@@ -596,6 +621,75 @@ hooks.register("pre_write_code", lambda ctx: print(f"Writing {ctx['file']}"))
result = hooks.execute("pre_write_code", {"file": "main.py"})
```
### 10. Extended agents.yaml with Workflow Patterns
You can now use advanced workflow patterns directly in agents.yaml by setting `process: workflow`:
```yaml
# agents.yaml with workflow patterns
framework: praisonai
process: workflow # Enables workflow mode
topic: "Research AI trends"
workflow:
planning: true
reasoning: true
verbose: true
variables:
topic: AI trends
roles:
classifier:
role: Request Classifier
backstory: "Classify requests into categories"
goal: Classify requests
researcher:
role: Research Analyst
backstory: "Expert researcher"
goal: Research topics
tools:
- tavily_search
steps:
# Sequential step
- agent: classifier
action: "Classify: {{topic}}"
# Route pattern - decision-based branching
- name: routing
route:
technical: [tech_expert]
default: [researcher]
# Parallel pattern - concurrent execution
- name: parallel_research
parallel:
- agent: researcher
action: "Research market trends"
- agent: researcher
action: "Research competitors"
# Loop pattern - iterate over items
- agent: researcher
action: "Analyze {{item}}"
loop:
over: topics
# Repeat pattern - evaluator-optimizer
- agent: aggregator
action: "Synthesize findings"
repeat:
until: "comprehensive"
max_iterations: 3
```
Run with the same simple command:
```bash
praisonai agents.yaml
```
## Using No Code
### Auto Mode:
@@ -723,6 +817,12 @@ praisonai workflow run deploy --workflow-var environment=staging --workflow-var
# Execute with planning mode (AI creates sub-steps for each workflow step)
praisonai workflow run "Research Blog" --planning --verbose
# Execute with reasoning mode (chain-of-thought)
praisonai workflow run "Analysis" --reasoning --verbose
# Execute with memory enabled
praisonai workflow run "Research" --memory
# Show workflow details
praisonai workflow show deploy
@@ -734,8 +834,23 @@ praisonai "What is AI?" --workflow "Research,Summarize" --save
# Inline workflow with step actions
praisonai "GPT-5" --workflow "Research:Search for info,Write:Write blog" --tools tavily
# Workflow CLI help
praisonai workflow help
```
**Workflow CLI Options:**
| Flag | Description |
|------|-------------|
| `--workflow-var key=value` | Set workflow variable (can be repeated) |
| `--llm <model>` | LLM model (e.g., openai/gpt-4o-mini) |
| `--tools <tools>` | Tools (comma-separated, e.g., tavily) |
| `--planning` | Enable planning mode |
| `--reasoning` | Enable reasoning mode |
| `--memory` | Enable memory |
| `--verbose` | Enable verbose output |
| `--save` | Save output to file |
### Hooks CLI:
```bash
# List configured hooks
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.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.26.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.26.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+57 -1
View File
@@ -318,7 +318,16 @@ 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)
topic = config.get('topic', '')
tools_dict = {}
# Only try to use praisonai_tools if it's available and needed
@@ -406,6 +415,53 @@ 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')
# 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 from topic or variables
input_data = config.get('topic', '')
# Execute workflow
self.logger.info(f"Running workflow: {workflow.name}")
result = workflow.start(input_data)
if result.get("status") == "completed":
return result.get("output", "Workflow completed successfully")
else:
return f"Workflow failed: {result.get('error', 'Unknown error')}"
def _run_autogen(self, config, topic, tools_dict):
"""
Run agents using the AutoGen framework.
+222 -1
View File
@@ -335,4 +335,225 @@ The final role you create should satisfy the provided task: """ + self.topic + "
# generator = AutoGenerator(framework="crewai", topic="Create a movie script about Cat in Mars")
# print(generator.generate())
# print(generator.generate())
# =============================================================================
# Workflow Auto-Generation (Feature Parity)
# =============================================================================
class WorkflowStepDetails(BaseModel):
"""Details for a workflow step."""
agent: str
action: str
expected_output: Optional[str] = None
class WorkflowRouteDetails(BaseModel):
"""Details for a route step."""
name: str
route: Dict[str, List[str]]
class WorkflowParallelDetails(BaseModel):
"""Details for a parallel step."""
name: str
parallel: List[WorkflowStepDetails]
class WorkflowAgentDetails(BaseModel):
"""Details for a workflow agent."""
name: str
role: str
goal: str
instructions: str
tools: Optional[List[str]] = None
class WorkflowStructure(BaseModel):
"""Structure for auto-generated workflow."""
name: str
description: str
agents: Dict[str, WorkflowAgentDetails]
steps: List[Dict] # Can be agent steps, route, parallel, etc.
class WorkflowAutoGenerator:
"""
Auto-generates workflow.yaml files from a topic description.
Uses lazy loading to avoid performance impact when not used.
Usage:
generator = WorkflowAutoGenerator(topic="Research AI trends and write a report")
path = generator.generate()
"""
def __init__(self, topic: str = "Research and write about AI",
workflow_file: str = "workflow.yaml",
config_list: Optional[List[Dict]] = None):
"""
Initialize the WorkflowAutoGenerator.
Args:
topic: The task/topic for the workflow
workflow_file: Output file name
config_list: Optional LLM configuration
"""
# Support multiple environment variable patterns
model_name = os.environ.get("MODEL_NAME") or os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini")
base_url = (
os.environ.get("OPENAI_BASE_URL") or
os.environ.get("OPENAI_API_BASE") or
os.environ.get("OLLAMA_API_BASE", "https://api.openai.com/v1")
)
self.config_list = config_list or [
{
'model': model_name,
'base_url': base_url,
'api_key': os.environ.get("OPENAI_API_KEY")
}
]
self.topic = topic
self.workflow_file = workflow_file
self._client = None # Lazy loading
@property
def client(self):
"""Lazy load the OpenAI client to avoid performance impact."""
if self._client is None:
self._client = instructor.patch(
OpenAI(
base_url=self.config_list[0]['base_url'],
api_key=self.config_list[0]['api_key'],
),
mode=instructor.Mode.JSON,
)
return self._client
def generate(self, pattern: str = "sequential") -> str:
"""
Generate a workflow YAML file.
Args:
pattern: Workflow pattern - "sequential", "routing", "parallel", "loop"
Returns:
Path to the generated workflow file
"""
response = self.client.chat.completions.create(
model=self.config_list[0]['model'],
response_model=WorkflowStructure,
max_retries=5,
messages=[
{"role": "system", "content": "You are a helpful assistant that designs workflow structures."},
{"role": "user", "content": self._get_prompt(pattern)}
]
)
json_data = json.loads(response.model_dump_json())
return self._save_workflow(json_data, pattern)
def _get_prompt(self, pattern: str) -> str:
"""Generate the prompt based on the workflow pattern."""
base_prompt = f"""Generate a workflow structure for: "{self.topic}"
The workflow should use the "{pattern}" pattern.
Create 2-4 agents with clear roles and instructions.
Each step should have a clear action.
"""
if pattern == "routing":
base_prompt += """
Include a classifier agent that routes to different specialized agents.
The route step should have at least 2 routes plus a default.
Example structure:
{
"name": "Routing Workflow",
"description": "Routes requests to specialized agents",
"agents": {
"classifier": {"name": "Classifier", "role": "Request Classifier", "goal": "Classify requests", "instructions": "Respond with ONLY: technical, creative, or general"},
"tech_agent": {"name": "TechExpert", "role": "Technical Expert", "goal": "Handle technical questions", "instructions": "Provide technical answers"}
},
"steps": [
{"agent": "classifier", "action": "Classify: {{input}}"},
{"name": "routing", "route": {"technical": ["tech_agent"], "default": ["tech_agent"]}}
]
}
"""
elif pattern == "parallel":
base_prompt += """
Include multiple agents that work in parallel, then an aggregator.
Example structure:
{
"name": "Parallel Workflow",
"description": "Multiple agents work concurrently",
"agents": {
"researcher1": {"name": "Researcher1", "role": "Market Analyst", "goal": "Research market", "instructions": "Provide market insights"},
"researcher2": {"name": "Researcher2", "role": "Competitor Analyst", "goal": "Research competitors", "instructions": "Provide competitor insights"},
"aggregator": {"name": "Aggregator", "role": "Synthesizer", "goal": "Combine findings", "instructions": "Synthesize all research"}
},
"steps": [
{"name": "parallel_research", "parallel": [
{"agent": "researcher1", "action": "Research market for {{input}}"},
{"agent": "researcher2", "action": "Research competitors for {{input}}"}
]},
{"agent": "aggregator", "action": "Combine all findings"}
]
}
"""
else: # sequential
base_prompt += """
Create a sequential workflow where agents work one after another.
Example structure:
{
"name": "Sequential Workflow",
"description": "Agents work in sequence",
"agents": {
"researcher": {"name": "Researcher", "role": "Research Analyst", "goal": "Research topics", "instructions": "Provide research findings"},
"writer": {"name": "Writer", "role": "Content Writer", "goal": "Write content", "instructions": "Write clear content"}
},
"steps": [
{"agent": "researcher", "action": "Research: {{input}}"},
{"agent": "writer", "action": "Write based on: {{previous_output}}"}
]
}
"""
base_prompt += f"\nGenerate a workflow for: {self.topic}"
return base_prompt
def _save_workflow(self, data: Dict, pattern: str) -> str:
"""Save the workflow to a YAML file."""
# Build the workflow YAML structure
workflow_yaml = {
'name': data.get('name', 'Auto-Generated Workflow'),
'description': data.get('description', ''),
'framework': 'praisonai',
'workflow': {
'verbose': True,
'planning': False,
'reasoning': False
},
'agents': {},
'steps': data.get('steps', [])
}
# Convert agents
for agent_id, agent_data in data.get('agents', {}).items():
workflow_yaml['agents'][agent_id] = {
'name': agent_data.get('name', agent_id),
'role': agent_data.get('role', 'Assistant'),
'goal': agent_data.get('goal', ''),
'instructions': agent_data.get('instructions', '')
}
if agent_data.get('tools'):
workflow_yaml['agents'][agent_id]['tools'] = agent_data['tools']
# Write to file
full_path = os.path.abspath(self.workflow_file)
with open(full_path, 'w') as f:
yaml.dump(workflow_yaml, f, default_flow_style=False, sort_keys=False)
return full_path
@@ -19,6 +19,7 @@ Features:
- todo: Todo list management
- router: Smart model selection
- flow_display: Visual workflow tracking
- workflow: YAML workflow management
"""
# Type hints available for IDE support
@@ -39,6 +40,7 @@ __all__ = [
'TodoHandler',
'RouterHandler',
'FlowDisplayHandler',
'WorkflowHandler',
]
def __getattr__(name):
@@ -85,4 +87,7 @@ def __getattr__(name):
elif name == 'FlowDisplayHandler':
from .flow_display import FlowDisplayHandler
return FlowDisplayHandler
elif name == 'WorkflowHandler':
from .workflow import WorkflowHandler
return WorkflowHandler
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,575 @@
"""
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:
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")
return True
except Exception as e:
self.print_status(f"✗ Validation failed: {e}", "error")
return False
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 as e:
self.print_status(f"Auto-generation requires instructor: pip install instructor", "error")
return None
except Exception as e:
self.print_status(f"Generation failed: {e}", "error")
return None
+207 -1
View File
@@ -1485,6 +1485,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 +1613,50 @@ 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 == '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("\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 +1667,178 @@ 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
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]")
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]")
except Exception as e:
print(f"[red]✗ Validation failed: {e}[/red]")
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 handle_hooks_command(self, action: str):
"""
Handle hooks subcommand actions.
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.22 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.26 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.22"
__version__ = "2.3.26"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.1.0",
"praisonaiagents>=0.1.4",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+1 -1
View File
@@ -224,7 +224,7 @@ def run_tests(pattern=None, verbose=False, coverage=False):
# Run only fast, non-integration tests
cmd.extend(["tests/unit/", "-m", "not slow"])
elif pattern == "all":
cmd.append("tests/")
cmd.extend(["tests/", "-m", "not real"]) # Exclude real tests that require API keys
elif pattern == "frameworks":
# Run both AutoGen and CrewAI integration tests (mock)
cmd.extend(["tests/integration/autogen/", "tests/integration/crewai/"])
@@ -270,9 +270,10 @@ class TestSessionHandler:
from praisonai.cli.features.session import SessionHandler
handler = SessionHandler()
actions = handler.get_actions()
assert "start" in actions
assert "list" in actions
assert "show" in actions
assert "resume" in actions
assert "delete" in actions
class TestToolsHandler:
@@ -0,0 +1,112 @@
"""
Unit tests for Workflow CLI commands.
Tests the WorkflowHandler CLI feature for YAML workflow management.
"""
import os
import tempfile
class TestWorkflowHandler:
"""Tests for WorkflowHandler CLI feature."""
def test_workflow_handler_import(self):
"""Test that WorkflowHandler can be imported."""
from praisonai.cli.features import WorkflowHandler
assert WorkflowHandler is not None
def test_workflow_handler_initialization(self):
"""Test WorkflowHandler initialization."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler(verbose=True)
assert handler.feature_name == "workflow"
def test_workflow_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
actions = handler.get_actions()
assert "run" in actions
assert "validate" in actions
assert "list" in actions
assert "create" in actions
def test_workflow_templates_exist(self):
"""Test that workflow templates are defined."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
assert "simple" in handler.TEMPLATES
assert "routing" in handler.TEMPLATES
assert "parallel" in handler.TEMPLATES
assert "loop" in handler.TEMPLATES
assert "evaluator-optimizer" in handler.TEMPLATES
def test_workflow_create_template(self):
"""Test creating a workflow from template."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
with tempfile.TemporaryDirectory() as tmpdir:
output_file = os.path.join(tmpdir, "test_workflow.yaml")
result = handler.action_create(
["--template", "simple", "--output", output_file]
)
assert result == output_file
assert os.path.exists(output_file)
# Check content
with open(output_file, 'r') as f:
content = f.read()
assert "name: Simple Workflow" in content
assert "agents:" in content
assert "steps:" in content
def test_workflow_validate(self):
"""Test validating a YAML workflow."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
yaml_content = """
name: Test Workflow
agents:
agent1:
name: Agent1
role: Worker
goal: Work
instructions: "Do work"
steps:
- agent: agent1
action: "Work"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
result = handler.action_validate([temp_path])
assert result == True
finally:
os.unlink(temp_path)
def test_workflow_validate_invalid_file(self):
"""Test validating a non-existent file."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
result = handler.action_validate(["nonexistent.yaml"])
assert result == False
def test_workflow_help_text(self):
"""Test help text contains expected commands."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
help_text = handler.get_help_text()
assert "run" in help_text
assert "validate" in help_text
assert "list" in help_text
assert "create" in help_text
assert "template" in help_text
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.1.0" },
{ name = "praisonaiagents", specifier = ">=0.1.4" },
{ 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.4"
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/ab/9d/d286e0ba5421cb8a37ea86f4714a7fbcb39fe1f38e9a202cf0cf6381ae8c/praisonaiagents-0.1.4.tar.gz", hash = "sha256:d00a326ac65701bcc66c44aba7d1616ef1317a2491034a242ad4290f0269e190", size = 560571, upload-time = "2025-12-17T11:43:41.829Z" }
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/f3/90/b6429a58f75aeec4da0034d7594863b3fd6e4bf916dbcbeb53a8597f0f4a/praisonaiagents-0.1.4-py3-none-any.whl", hash = "sha256:c7ad9b64e3d3a24b9560d695bc7447f286928e52f0979e84d02484662cb8a49d", size = 524190, upload-time = "2025-12-17T11:43:40.119Z" },
]
[[package]]