Compare commits

...
1 Commits
Author SHA1 Message Date
Mervin Praison f991f37cfe Release v2.3.23 2025-12-17 00:41:48 +00:00
15 changed files with 977 additions and 27 deletions

No files matched your search

+21
View File
@@ -723,6 +723,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 +740,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.23" \
"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.23" \
"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.23" \
"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.23" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -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}',
@@ -1031,6 +1227,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 +1309,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 +1357,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 +1378,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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.1.0"
version = "0.1.1"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -1017,6 +1017,667 @@ 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: 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
# =============================================================================
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.1.0"
version = "0.1.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+21
View File
@@ -723,6 +723,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 +740,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.23.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.23.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+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.23 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.23"
+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.1",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+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.1" },
{ 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.1"
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/b0/41/14a8e49c644c26ee565511776c67cc978427f36aec68a1442ac666c35401/praisonaiagents-0.1.1.tar.gz", hash = "sha256:76f69188e4893b0e868f33ea02d26cccfa90b99d679c2db4185d8cf624fa740e", size = 550745, upload-time = "2025-12-17T00:37:35.974Z" }
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/f7/93/2cef070fae77bbef32a986ad7e1787f799442047f5dac831b9c137839353/praisonaiagents-0.1.1-py3-none-any.whl", hash = "sha256:c116551a35df0f8766496ba5102c54633f071735eae2a71b33eb4df5e2728e86", size = 518796, upload-time = "2025-12-17T00:37:33.824Z" },
]
[[package]]