Compare commits

...
49 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
Mervin Praison a43b3ac72c Release v2.3.22 2025-12-17 00:25:14 +00:00
Mervin Praison 06f82249e1 Release v2.3.21 2025-12-17 00:05:40 +00:00
Mervin Praison d3aa78241a Release v2.3.20 2025-12-16 23:46:47 +00:00
Mervin Praison dfb77c39f9 Release v2.3.19 2025-12-16 23:26:53 +00:00
Mervin Praison 7e8f9b0574 Release v2.3.18 2025-12-16 23:18:36 +00:00
Mervin Praison 19bcdcd000 Release v2.3.16 2025-12-16 22:59:29 +00:00
Mervin Praison 8f5160f827 feat(workflows): Add branching, loops, callbacks, and per-step variables
- Add branching support with next_steps and branch_condition fields
- Add loop support with loop_over and loop_var for iterating over variables
- Add on_task_start and on_task_complete callbacks to PraisonAIAgents
- Add variables parameter for global variable substitution across tasks
- Add per-step agent creation from agent_config when no agent assigned
- Add task-level variable substitution in task descriptions
- Refactor workflow
2025-12-16 22:58:56 +00:00
Mervin Praison 3574a4d2c0 Release v2.3.15 2025-12-16 22:50:37 +00:00
Mervin Praison b552843fa9 docs: Update README workflow CLI with new features
- Add inline workflow examples (--workflow 'step1,step2')
- Add planning mode example (--planning)
- Add tools and save examples (--tools, --save)
- Update to use global flags instead of workflow-specific flags
2025-12-16 21:46:30 +00:00
Mervin Praison 0461a133df fix(workflow): Actually use planning mode when --planning is enabled
Before: --planning flag was stored but never used, agent.chat() was called
After: --planning flag creates agent with planning=True and calls agent.start()

The agent.start() method checks if planning is enabled and calls
_start_with_planning() which:
1. Creates a plan using PlanningAgent
2. Displays the generated plan
3. Executes each step with context passing
4. Compiles results into final output
2025-12-16 19:12:28 +00:00
Mervin Praison b33857de33 refactor(cli): Use global flags for workflow instead of workflow-specific flags
BREAKING CHANGE: Removed workflow-specific flags in favor of global flags

Before:
  praisonai workflow run 'name' --workflow-tools tavily --workflow-save --workflow-verbose

After:
  praisonai workflow run 'name' --tools tavily --save --verbose

This simplifies the CLI by reusing existing global flags:
- --llm (instead of --workflow-llm)
- --tools (instead of --workflow-tools)
- --planning (instead of --workflow-planning)
- --memory (instead of --workflow-memory)
- --save (instead of --workflow-save)
- --verbose (instead of --workflow-verbose)

Only --workflow-var remains workflow-specific for variable substitution.
2025-12-16 19:09:52 +00:00
Mervin Praison 433c643f6c feat(cli): Add inline workflow support without .md template
Usage: praisonai 'prompt' --workflow 'step1:action1,step2:action2'

Example:
  praisonai 'What is AI?' --workflow 'Research:Research the topic,Summarize:Write a summary' --workflow-save

Features:
- Parse inline workflow steps from --workflow flag
- Sequential execution with context passing
- All workflow options work: --workflow-tools, --workflow-save, etc.
- No .md template file required
2025-12-16 19:05:20 +00:00
Mervin Praison 214119a063 feat(cli): Add complete workflow CLI options
- Add --workflow-llm for LLM model selection
- Add --workflow-tools for tools (comma-separated)
- Add --workflow-planning for planning mode
- Add --workflow-memory for memory support
- Add --workflow-stream for streaming output
- Add --workflow-verbose for verbose output
- Add --workflow-save to save output to file
- Show final output after workflow completion
- Save all step outputs to markdown file
- Update help text with all options
2025-12-16 19:01:57 +00:00
Mervin Praison 989a9f6fc5 Release v2.3.14 2025-12-16 18:50:59 +00:00
Mervin Praison cf276a5640 feat(workflows): Add context passing, per-step agents, async execution
## New Features:
- Context passing between workflow steps with {{previous_output}} and {{step_name_output}}
- Per-step agent configuration with role, goal, backstory, and tools
- Per-step tools support
- Async execution with aexecute() method
- Planning mode at workflow level
- Enhanced markdown parser for agent/tools blocks

## WorkflowStep Enhancements:
- agent_config: Optional[Dict] - Per-step agent configuration
- tools: Optional[List] - Per-step tools
- context_from: Optional[List[str]] - Specific steps to include context from
- retain_full_context: bool - Include all previous outputs vs only last
- output_variable: Optional[str] - Custom variable name for step output

## Workflow Enhancements:
- default_llm: Optional[str] - Default LLM for all steps
- planning: bool - Enable planning mode
- planning_llm: Optional[str] - LLM for planning
- memory_config: Optional[Dict] - Memory configuration

## Execute Method Enhancements:
- default_agent parameter (executor now optional)
- default_llm, memory, planning, stream, verbose parameters

## Tests:
- 74 new tests covering all features
2025-12-16 18:49:43 +00:00
Mervin Praison b5134ffb2f Add --final-agent flag for post-processing output with specialized agent 2025-12-16 16:40:22 +00:00
Mervin Praison 5664036712 Release v2.3.13 2025-12-16 16:33:03 +00:00
Mervin Praison 6c2ca64cbe Remove unused --compile-results flag 2025-12-16 16:32:42 +00:00
Mervin Praison 08c742287c Fix: Pass max_tokens as dict config to Agent 2025-12-16 16:27:29 +00:00
Mervin Praison 4ee32b8eb2 Improve planning mode: compile all step results into comprehensive final report 2025-12-16 16:22:44 +00:00
Mervin Praison 35f07e5653 Add --max-tokens flag and improve planning mode output compilation 2025-12-16 16:22:37 +00:00
Mervin Praison 6481aec376 Add --tools support for all agents including planning mode 2025-12-16 16:14:39 +00:00
Mervin Praison 98c846ef78 Improve: Dynamic tool instructions from docstrings instead of hardcoded search 2025-12-16 16:01:30 +00:00
Mervin Praison 6e4a9faaf8 Fix: Instruct PromptExpanderAgent to use tools for context gathering 2025-12-16 15:59:46 +00:00
Mervin Praison 05d62db355 Reorganize README.md by moving Prompt Expansion, Web Search/Fetch, and MCP sections after CLI Features table, adding comprehensive programmatic usage examples and feature comparison tables for better documentation structure and clarity. 2025-12-16 15:52:36 +00:00
Mervin Praison 375c8c9826 Fix CLI docs links to use /docs/cli/ path 2025-12-16 15:41:42 +00:00
Mervin Praison c6977c8221 Add CLI Features table with docs links 2025-12-16 15:19:29 +00:00
Mervin Praison d407c83cf3 Add comprehensive CLI feature documentation to README.md, including Guardrail, Metrics, Image Processing, Telemetry, MCP, Fast Context, Knowledge, Session, Tools, Handoff, Auto Memory, Todo, Router, Flow Display, and Prompt Expansion with usage examples and command options for each feature. 2025-12-16 15:06:41 +00:00
Mervin Praison 214add68d9 Release v2.3.12 2025-12-16 15:03:23 +00:00
Mervin Praison 6d2cb9676d Update README.md to consolidate and enhance the Supported Providers section, adding new AI providers and their corresponding example links for improved documentation and user guidance. 2025-12-16 11:54:00 +00:00
Mervin Praison c519cb1379 Add supported providers section to README.md, listing various AI providers with example links for each, enhancing documentation and usability. 2025-12-16 11:52:19 +00:00
Mervin Praison 356dc4909a Update README.md to refine feature table, enhancing clarity by renaming 'Automated AI Agents Creation' to 'Single Agent' and reorganizing agent types for better categorization. 2025-12-16 11:25:42 +00:00
Mervin Praison dc278ad07b Enhance README.md with new agent types and advanced features, including Image Generation, Data Analyst, and Finance Agents, along with updates to existing feature descriptions and examples. 2025-12-16 11:22:23 +00:00
Mervin Praison 5cc53eb4bb Add advanced features section to README.md, detailing new capabilities including Deep Research Agents, Query Rewriter Agent, and various MCP enhancements. 2025-12-16 11:13:38 +00:00
Mervin Praison b72576d086 Refactor feature table in README.md for improved clarity and consistency, updating formatting and links for AI agent capabilities. 2025-12-16 11:11:04 +00:00
Mervin Praison 30314b1573 Update README.md to include detailed feature table with example code and documentation links for each AI agent capability. 2025-12-16 11:05:51 +00:00
Mervin Praison f721bd5e9a Add Fast Context feature to README highlights 2025-12-16 10:52:37 +00:00
93 changed files with 21034 additions and 188 deletions

No files matched your search

+919 -47
View File
File diff suppressed because it is too large. Load diff
+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.11" \
"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.11" \
"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.11" \
"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.11" \
"praisonai>=2.3.26" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -29,7 +29,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -71,7 +71,6 @@
"import pandas as pd\n",
"\n",
"print(\"📦 Dependencies installed!\")\n",
"print(\"🔄 Converting from AGNO → PraisonAI\")\n",
"print(\"🎯 Building Job Hunting Agent System\")"
]
},
@@ -0,0 +1,38 @@
"""
Simple Agentic Workflow Example
Demonstrates using Agent instances directly as workflow steps.
Each agent processes the input and passes output to the next agent.
"""
from praisonaiagents import Agent, Workflow
# Create agents with specific roles
researcher = Agent(
name="Researcher",
role="Research Analyst",
goal="Research and provide information about topics",
instructions="You are a research analyst. Provide concise, factual information."
)
writer = Agent(
name="Writer",
role="Content Writer",
goal="Write engaging content based on research",
instructions="You are a content writer. Write clear, engaging content based on the research provided."
)
# Create workflow with agents as steps
workflow = Workflow(
name="Simple Agentic Pipeline",
steps=[researcher, writer]
)
if __name__ == "__main__":
# Run workflow - agents process sequentially
result = workflow.start(
"What are the key benefits of AI agents?",
verbose=True
)
print(f"\nFinal output:\n{result['output']}")
@@ -0,0 +1,36 @@
"""
Simple Workflow Example
The easiest way to create and run workflows in PraisonAI.
Just pass functions or agents as steps - no complex configuration needed!
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
# Define simple handler functions
def validate(ctx: WorkflowContext) -> StepResult:
"""Validate the input data."""
if not ctx.input:
return StepResult(output="No input provided", stop_workflow=True)
return StepResult(output=f"Validated: {ctx.input}")
def process(ctx: WorkflowContext) -> StepResult:
"""Process the validated data."""
return StepResult(output=f"Processed: {ctx.previous_result}")
def format_output(ctx: WorkflowContext) -> StepResult:
"""Format the final output."""
return StepResult(output=f"✅ Final: {ctx.previous_result}")
# Create workflow - just list your functions!
workflow = Workflow(
name="Simple Pipeline",
steps=[validate, process, format_output]
)
if __name__ == "__main__":
# Run the workflow - PraisonAI style uses .start()
result = workflow.start("Hello, World!", verbose=True)
print(f"\nFinal output: {result['output']}")
print(f"Steps completed: {len(result['steps'])}")
@@ -0,0 +1,57 @@
"""
Task Callbacks Example
Demonstrates using on_task_start and on_task_complete callbacks
with PraisonAIAgents for workflow monitoring and logging.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
# Create an agent
researcher = Agent(
name="Researcher",
role="Research Assistant",
goal="Find and summarize information",
llm="gpt-4o-mini"
)
# Create tasks with variables
task1 = Task(
name="research",
description="Research the topic: {{topic}}",
expected_output="A brief summary of the topic",
agent=researcher,
variables={"topic": "artificial intelligence"}
)
task2 = Task(
name="summarize",
description="Create a one-paragraph summary of the research.",
expected_output="A concise summary",
agent=researcher
)
# Define callbacks
def on_start(task, task_id):
"""Called before each task starts."""
print(f"🚀 Starting task: {task.name} (ID: {task_id})")
def on_complete(task, output):
"""Called after each task completes."""
print(f"✅ Completed task: {task.name}")
print(f" Output preview: {str(output)[:100]}...")
if __name__ == "__main__":
# Create agents with callbacks
agents = PraisonAIAgents(
agents=[researcher],
tasks=[task1, task2],
process="workflow",
on_task_start=on_start,
on_task_complete=on_complete,
variables={"global_var": "shared_value"} # Global variables
)
# Run the workflow
result = agents.start()
print("\n🎉 Workflow completed!")
@@ -0,0 +1,48 @@
"""
Workflow Branching Example
Demonstrates conditional branching in workflows where steps can route
to different next steps based on output content.
"""
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
# Create a workflow with branching
workflow = Workflow(
name="Decision Workflow",
description="A workflow that branches based on validation result",
steps=[
WorkflowStep(
name="validate",
action="Check if the number 42 is positive. Reply with 'valid' or 'invalid'.",
next_steps=["success_handler", "error_handler"],
branch_condition={
"valid": ["success_handler"],
"invalid": ["error_handler"]
}
),
WorkflowStep(
name="success_handler",
action="The validation passed! Generate a success message."
),
WorkflowStep(
name="error_handler",
action="The validation failed. Generate an error message."
)
]
)
if __name__ == "__main__":
# Create manager and register workflow
manager = WorkflowManager()
manager.workflows["Decision Workflow"] = workflow
# Execute - will branch to success_handler since 42 is positive
result = manager.execute(
"Decision Workflow",
default_llm="gpt-4o-mini"
)
print("Workflow completed!")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
@@ -0,0 +1,63 @@
"""
Workflow Checkpoints Example
Demonstrates saving and resuming workflow execution using checkpoints.
Useful for long-running workflows that may be interrupted.
"""
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
# Create a multi-step workflow
workflow = Workflow(
name="Long Process",
description="A workflow with checkpoints for resumability",
steps=[
WorkflowStep(
name="step1",
action="Initialize the process and prepare data."
),
WorkflowStep(
name="step2",
action="Process the first batch of data."
),
WorkflowStep(
name="step3",
action="Process the second batch of data."
),
WorkflowStep(
name="step4",
action="Finalize and generate report."
)
]
)
if __name__ == "__main__":
manager = WorkflowManager()
manager.workflows["Long Process"] = workflow
# Execute with checkpoint - saves after each step
print("=== Starting workflow with checkpoint ===")
result = manager.execute(
"Long Process",
default_llm="gpt-4o-mini",
checkpoint="my-checkpoint" # Saves progress after each step
)
print(f"Completed {len(result['results'])} steps")
# List available checkpoints
print("\n=== Available checkpoints ===")
checkpoints = manager.list_checkpoints()
for cp in checkpoints:
print(f" {cp['name']}: {cp['completed_steps']} steps completed")
# Resume from checkpoint (if workflow was interrupted)
# result = manager.execute(
# "Long Process",
# default_llm="gpt-4o-mini",
# resume="my-checkpoint" # Resumes from last saved state
# )
# Clean up checkpoint
manager.delete_checkpoint("my-checkpoint")
print("\n✅ Checkpoint deleted")
@@ -0,0 +1,83 @@
"""
Agentic Conditional Workflow Example
Demonstrates using should_run to conditionally execute agent steps
based on input or previous results.
"""
from praisonaiagents import Agent, Workflow, WorkflowStep
# Condition functions
def is_technical(ctx) -> bool:
"""Only run technical review for technical content."""
keywords = ["code", "programming", "algorithm", "database", "api"]
return any(keyword in ctx.input.lower() for keyword in keywords)
def needs_creative(ctx) -> bool:
"""Only run creative enhancement for creative content."""
keywords = ["story", "poem", "creative", "write", "narrative"]
return any(keyword in ctx.input.lower() for keyword in keywords)
# Create agents
analyzer = Agent(
name="Analyzer",
role="Content Analyzer",
goal="Analyze and process content",
instructions="Analyze the given content and provide insights."
)
tech_reviewer = Agent(
name="TechReviewer",
role="Technical Reviewer",
goal="Review technical content",
instructions="Review the technical aspects and provide technical feedback."
)
creative_enhancer = Agent(
name="CreativeEnhancer",
role="Creative Enhancer",
goal="Enhance creative content",
instructions="Enhance the creative aspects and make it more engaging."
)
finalizer = Agent(
name="Finalizer",
role="Content Finalizer",
goal="Finalize the content",
instructions="Provide the final polished version of the content."
)
# Create workflow with conditional steps
workflow = Workflow(
name="Conditional Agentic Pipeline",
steps=[
analyzer, # Always runs
WorkflowStep(
name="tech_review",
agent=tech_reviewer,
should_run=is_technical # Only runs for technical content
),
WorkflowStep(
name="creative_enhance",
agent=creative_enhancer,
should_run=needs_creative # Only runs for creative content
),
finalizer # Always runs
]
)
if __name__ == "__main__":
# Test 1: Technical content
print("=== Test 1: Technical Content ===")
result = workflow.start("Review this Python code for best practices", verbose=True)
print(f"Output: {result['output'][:200]}...\n")
# Test 2: Creative content
print("=== Test 2: Creative Content ===")
result = workflow.start("Write a short story about a robot", verbose=True)
print(f"Output: {result['output'][:200]}...\n")
# Test 3: General content (neither technical nor creative)
print("=== Test 3: General Content ===")
result = workflow.start("What is the weather like today?", verbose=True)
print(f"Output: {result['output'][:200]}...")
@@ -0,0 +1,68 @@
"""
Workflow Conditional Execution Example
Demonstrates using should_run to conditionally execute steps
based on input or previous results.
"""
from praisonaiagents import Workflow, WorkflowStep, WorkflowContext, StepResult
# Condition functions - return True to run the step, False to skip
def is_sensitive_content(ctx: WorkflowContext) -> bool:
"""Only run compliance check for sensitive topics."""
sensitive_keywords = ["legal", "medical", "financial", "security"]
return any(keyword in ctx.input.lower() for keyword in sensitive_keywords)
def needs_translation(ctx: WorkflowContext) -> bool:
"""Only translate if content is not in English."""
return ctx.variables.get("language", "en") != "en"
# Handler functions
def process_content(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"Processed: {ctx.input}")
def compliance_check(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"✅ Compliance verified for: {ctx.previous_result}")
def translate(ctx: WorkflowContext) -> StepResult:
lang = ctx.variables.get("language", "unknown")
return StepResult(output=f"🌐 Translated to {lang}: {ctx.previous_result}")
def finalize(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"📄 Final: {ctx.previous_result}")
# Create workflow with conditional steps
workflow = Workflow(
name="Conditional Pipeline",
steps=[
WorkflowStep(name="process", handler=process_content),
WorkflowStep(
name="compliance",
handler=compliance_check,
should_run=is_sensitive_content # Only runs for sensitive content
),
WorkflowStep(
name="translate",
handler=translate,
should_run=needs_translation # Only runs if language != "en"
),
WorkflowStep(name="finalize", handler=finalize)
]
)
if __name__ == "__main__":
# Test 1: Normal content (skips compliance)
print("=== Test 1: Normal content ===")
result = workflow.start("Hello world", verbose=True)
print(f"Output: {result['output']}\n")
# Test 2: Sensitive content (runs compliance)
print("=== Test 2: Legal content ===")
result = workflow.start("Review this legal document", verbose=True)
print(f"Output: {result['output']}\n")
# Test 3: Non-English content (runs translation)
print("=== Test 3: Non-English content ===")
workflow.variables = {"language": "es"}
result = workflow.start("Hola mundo", verbose=True)
print(f"Output: {result['output']}")
@@ -0,0 +1,69 @@
"""
Workflow Early Stop Example
Demonstrates how to stop a workflow early using custom handler
functions that return StepResult with stop_workflow=True.
"""
from praisonaiagents.memory.workflows import (
WorkflowManager, WorkflowStep, Workflow,
WorkflowContext, StepResult
)
# Custom validator that can stop the workflow
def validate_data(context: WorkflowContext) -> StepResult:
"""Check if data is valid and stop workflow if not."""
data = context.variables.get("data", {})
if data.get("value", 0) < 0:
return StepResult(
output="❌ Validation failed: negative value detected. Stopping workflow.",
stop_workflow=True # This stops the entire workflow
)
return StepResult(
output="✅ Validation passed: data is valid.",
stop_workflow=False # Continue to next step
)
# Create workflow with early stop capability
workflow = Workflow(
name="Data Processing",
description="Process data with validation gate",
variables={
"data": {"value": -5} # Invalid data - will trigger early stop
},
steps=[
WorkflowStep(
name="validate",
handler=validate_data # Custom function
),
WorkflowStep(
name="process",
action="Process the validated data." # Won't run if validation fails
),
WorkflowStep(
name="report",
action="Generate final report." # Won't run if validation fails
)
]
)
if __name__ == "__main__":
manager = WorkflowManager()
manager.workflows["Data Processing"] = workflow
# Test with invalid data (will stop early)
print("=== Testing with invalid data ===")
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['output']}")
# Test with valid data (will complete)
print("\n=== Testing with valid data ===")
workflow.variables["data"] = {"value": 42}
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
@@ -0,0 +1,59 @@
"""
Agentic Loop with CSV Workflow Example
Demonstrates iterating over a CSV file with an agent processing each row.
"""
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import loop
import tempfile
import os
# Create sample CSV file content
csv_content = """topic,description
AI Ethics,Ethical considerations in artificial intelligence
Machine Learning,Algorithms that learn from data
Neural Networks,Computing systems inspired by biological neural networks
Natural Language Processing,AI understanding human language"""
# Create processor agent
processor = Agent(
name="TopicProcessor",
role="Topic Analyst",
goal="Analyze and explain topics",
instructions="Provide a brief, insightful analysis of the given topic."
)
# Create summarizer agent
summarizer = Agent(
name="Summarizer",
role="Results Summarizer",
goal="Summarize all processed results",
instructions="Create a comprehensive summary of all the analyzed topics."
)
if __name__ == "__main__":
# Create temp CSV file
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
f.write(csv_content)
csv_path = f.name
try:
print("=== Testing Agentic Loop with CSV ===\n")
print(f"Processing CSV: {csv_path}\n")
# Create workflow with loop over CSV
workflow = Workflow(
name="CSV Topic Processor",
steps=[
loop(processor, from_csv=csv_path),
summarizer
]
)
result = workflow.start("Analyze these AI topics", verbose=True)
print(f"\nFinal Summary:\n{result['output']}")
finally:
os.unlink(csv_path)
@@ -0,0 +1,55 @@
"""
Workflow Loop with CSV Example
Demonstrates iterating over a CSV file, processing each row.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import loop
import tempfile
import os
# Create sample CSV file
csv_content = """name,email,task
Alice,[email protected],Write documentation
Bob,[email protected],Review pull requests
Charlie,[email protected],Fix critical bug
Diana,[email protected],Deploy to production"""
# Processor for each CSV row
def process_task(ctx: WorkflowContext) -> StepResult:
"""Process a single task from CSV."""
item = ctx.variables.get("item", {})
name = item.get("name", "Unknown")
task = item.get("task", "No task")
index = ctx.variables.get("loop_index", 0)
return StepResult(
output=f"[{index+1}] {name}: {task} ✅",
variables={"last_processed": name}
)
if __name__ == "__main__":
# Create temp CSV file
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
f.write(csv_content)
csv_path = f.name
try:
print("=== Testing Loop with CSV ===\n")
print(f"CSV file: {csv_path}\n")
# Create workflow
workflow = Workflow(
name="CSV Task Processor",
steps=[loop(process_task, from_csv=csv_path)]
)
result = workflow.start("Process all tasks", verbose=True)
print(f"\nAll outputs:\n{result['output']}")
print(f"\nProcessed {len(result['variables']['loop_outputs'])} items")
print(f"Last processed: {result['variables'].get('last_processed')}")
finally:
os.unlink(csv_path)
@@ -0,0 +1,52 @@
"""
Workflow Loop with List Example
Demonstrates iterating over a list of items, processing each one.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import loop
# Sample data
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# Processor for each item
def process_fruit(ctx: WorkflowContext) -> StepResult:
"""Process a single fruit from the list."""
fruit = ctx.variables.get("item", "unknown")
index = ctx.variables.get("loop_index", 0)
# Simulate processing
result = f"🍎 [{index + 1}] {fruit.upper()} - processed successfully"
return StepResult(
output=result,
variables={"last_fruit": fruit}
)
# Aggregator to summarize results
def summarize(ctx: WorkflowContext) -> StepResult:
"""Summarize all processed fruits."""
outputs = ctx.variables.get("loop_outputs", [])
summary = f"📊 Processed {len(outputs)} fruits:\n"
summary += "\n".join(f" {o}" for o in outputs)
return StepResult(output=summary)
# Create workflow
workflow = Workflow(
name="Fruit Processor",
steps=[
loop(process_fruit, over="fruits"),
summarize
],
variables={"fruits": fruits}
)
if __name__ == "__main__":
print("=== Testing Loop with List ===\n")
result = workflow.start("Process all fruits", verbose=True)
print(f"\nFinal Output:\n{result['output']}")
print(f"\nLoop outputs count: {len(result['variables']['loop_outputs'])}")
print(f"Last processed: {result['variables'].get('last_fruit')}")
@@ -0,0 +1,46 @@
"""
Workflow Loops Example
Demonstrates iterating over a list of items in a workflow step.
Each item is processed individually with the loop variable.
"""
from praisonaiagents.memory.workflows import WorkflowManager, WorkflowStep, Workflow
# Create a workflow with loop
workflow = Workflow(
name="Process Items",
description="Process a list of items using loop",
variables={
"items": ["apple", "banana", "cherry"] # List to iterate over
},
steps=[
WorkflowStep(
name="process_each",
action="Describe the fruit: {{item}}",
loop_over="items", # Variable name containing the list
loop_var="item" # Variable name for current item
),
WorkflowStep(
name="summarize",
action="Summarize all the fruits that were processed."
)
]
)
if __name__ == "__main__":
# Create manager and register workflow
manager = WorkflowManager()
manager.workflows["Process Items"] = workflow
# Execute - will process each fruit in the list
result = manager.execute(
"Process Items",
default_llm="gpt-4o-mini"
)
print("Workflow completed!")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
if isinstance(step_result.get("output"), list):
print(f" Loop processed {len(step_result['output'])} items")
@@ -0,0 +1,68 @@
"""
Mixed Steps Workflow Example
Demonstrates combining different step types in a single workflow:
- Agent instances
- Handler functions
- WorkflowStep with action strings
"""
from praisonaiagents import Workflow, WorkflowStep, WorkflowContext, StepResult, Agent
# Create an agent
analyzer = Agent(
name="Analyzer",
role="Data Analyst",
goal="Analyze and summarize data",
llm="gpt-4o-mini"
)
# Custom handler functions
def validate_input(ctx: WorkflowContext) -> StepResult:
"""Validate the input before processing."""
if len(ctx.input) < 5:
return StepResult(
output="Input too short. Minimum 5 characters required.",
stop_workflow=True
)
return StepResult(output=f"✅ Valid input: {ctx.input}")
def format_output(ctx: WorkflowContext) -> StepResult:
"""Format the final output."""
return StepResult(
output=f"📊 Report:\n{ctx.previous_result}",
variables={"report_generated": True}
)
# Create workflow with mixed step types
workflow = Workflow(
name="Mixed Pipeline",
steps=[
# Step 1: Function - validates input
validate_input,
# Step 2: Agent - analyzes content
analyzer,
# Step 3: WorkflowStep with action string
WorkflowStep(
name="enhance",
action="Add a conclusion to this analysis: {{previous_output}}"
),
# Step 4: Function - formats output
format_output
],
default_llm="gpt-4o-mini" # Used for action-based steps
)
if __name__ == "__main__":
# Test with valid input
print("=== Running mixed workflow ===")
result = workflow.start(
"Analyze the trends in AI adoption for 2024",
verbose=True
)
print(f"\nFinal output:\n{result['output']}")
print(f"\nVariables: {result['variables']}")
@@ -0,0 +1,56 @@
"""
Agentic Parallel Workflow Example
Demonstrates running multiple agents concurrently and
combining their results with an aggregator agent.
"""
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import parallel
# Create parallel research agents
market_researcher = Agent(
name="MarketResearcher",
role="Market Research Analyst",
goal="Research market trends and opportunities",
instructions="Analyze market trends. Provide concise market insights."
)
competitor_researcher = Agent(
name="CompetitorResearcher",
role="Competitive Intelligence Analyst",
goal="Research competitor strategies",
instructions="Analyze competitors. Provide key competitive insights."
)
customer_researcher = Agent(
name="CustomerResearcher",
role="Customer Research Analyst",
goal="Research customer needs and behaviors",
instructions="Analyze customer segments. Provide customer insights."
)
# Create aggregator agent
aggregator = Agent(
name="Aggregator",
role="Research Synthesizer",
goal="Synthesize research findings",
instructions="Combine all research findings into a comprehensive summary."
)
# Create workflow with parallel execution
workflow = Workflow(
name="Parallel Research Pipeline",
steps=[
parallel([market_researcher, competitor_researcher, customer_researcher]),
aggregator
]
)
if __name__ == "__main__":
print("=== Testing Agentic Parallel Workflow ===\n")
# Run workflow - all researchers work in parallel, then aggregator summarizes
result = workflow.start("Research the AI industry", verbose=True)
print(f"\nFinal Summary:\n{result['output']}")
@@ -0,0 +1,53 @@
"""
Workflow Parallel Execution Example
Demonstrates running multiple steps concurrently and
combining their results.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import parallel
import time
# Parallel workers - each does independent work
def research_market(ctx: WorkflowContext) -> StepResult:
"""Research market trends."""
time.sleep(0.1) # Simulate work
return StepResult(output="📊 Market: Growth expected 15% YoY")
def research_competitors(ctx: WorkflowContext) -> StepResult:
"""Research competitor analysis."""
time.sleep(0.1) # Simulate work
return StepResult(output="🏢 Competitors: 3 major players identified")
def research_customers(ctx: WorkflowContext) -> StepResult:
"""Research customer feedback."""
time.sleep(0.1) # Simulate work
return StepResult(output="👥 Customers: 85% satisfaction rate")
# Aggregator - combines parallel results
def summarize_research(ctx: WorkflowContext) -> StepResult:
"""Summarize all research findings."""
outputs = ctx.variables.get("parallel_outputs", [])
summary = "📋 RESEARCH SUMMARY:\n" + "\n".join(f" • {o}" for o in outputs)
return StepResult(output=summary)
# Create workflow with parallel execution
workflow = Workflow(
name="Parallel Research",
steps=[
parallel([research_market, research_competitors, research_customers]),
summarize_research
]
)
if __name__ == "__main__":
print("=== Testing Parallel Workflow ===\n")
start_time = time.time()
result = workflow.start("Analyze the business landscape", verbose=True)
elapsed = time.time() - start_time
print(f"\nFinal Output:\n{result['output']}")
print(f"\nCompleted in {elapsed:.2f}s (parallel execution)")
print(f"Individual outputs: {len(result['variables']['parallel_outputs'])}")
@@ -0,0 +1,53 @@
"""
Agentic Evaluator-Optimizer (Repeat) Workflow Example
Demonstrates the evaluator-optimizer pattern where an agent
generates content and another evaluates it, repeating until approved.
"""
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import repeat
# Create generator agent
generator = Agent(
name="Generator",
role="Content Generator",
goal="Generate high-quality content",
instructions="Generate content based on the topic. Improve based on feedback if provided."
)
# Create evaluator agent
evaluator = Agent(
name="Evaluator",
role="Content Evaluator",
goal="Evaluate and provide feedback on content",
instructions="Evaluate the content quality. If good, respond with 'APPROVED'. Otherwise provide specific feedback for improvement."
)
# Evaluation function to check if content is approved
def is_approved(ctx) -> bool:
return "approved" in ctx.previous_result.lower()
# Create workflow with evaluator-optimizer pattern
workflow = Workflow(
name="Evaluator-Optimizer Pipeline",
steps=[
generator, # First, generate content
repeat(
evaluator, # Evaluate and provide feedback
until=is_approved,
max_iterations=3
)
]
)
if __name__ == "__main__":
print("=== Testing Agentic Evaluator-Optimizer Workflow ===\n")
# Run optimization workflow
result = workflow.start(
"Write a compelling product description for an AI assistant",
verbose=True
)
print(f"\nFinal Result:\n{result['output']}")
@@ -0,0 +1,69 @@
"""
Workflow Repeat (Evaluator-Optimizer) Example
Demonstrates repeating a step until a condition is met.
This is useful for iterative improvement patterns.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import repeat
# Simulated content generator that improves each iteration
class ContentGenerator:
def __init__(self):
self.iteration = 0
self.content = []
def generate(self, ctx: WorkflowContext) -> StepResult:
"""Generate content, adding more each iteration."""
self.iteration += 1
# Add new content each iteration
new_items = [
f"Point {self.iteration}.1: Key insight about the topic",
f"Point {self.iteration}.2: Supporting evidence"
]
self.content.extend(new_items)
output = f"Iteration {self.iteration}: Generated {len(self.content)} points total"
return StepResult(
output=output,
variables={
"total_points": len(self.content),
"content": self.content.copy()
}
)
# Condition: stop when we have enough content
def has_enough_content(ctx: WorkflowContext) -> bool:
"""Check if we have generated enough content."""
total = ctx.variables.get("total_points", 0)
return total >= 6 # Stop when we have 6+ points
# Create generator instance
generator = ContentGenerator()
# Create workflow with repeat pattern
workflow = Workflow(
name="Content Generator",
steps=[
repeat(
generator.generate,
until=has_enough_content,
max_iterations=10
)
]
)
if __name__ == "__main__":
print("=== Testing Repeat (Evaluator-Optimizer) Pattern ===\n")
result = workflow.start("Generate comprehensive content", verbose=True)
print(f"\nFinal output: {result['output']}")
print(f"Total iterations: {result['variables']['repeat_iterations']}")
print(f"Total points generated: {result['variables']['total_points']}")
print("\nGenerated content:")
for point in result['variables']['content']:
print(f" • {point}")
@@ -0,0 +1,65 @@
"""
Agentic Routing Workflow Example
Demonstrates decision-based routing where an agent classifier
routes to specialized handler agents based on the request type.
"""
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import route
# Create classifier agent
classifier = Agent(
name="Classifier",
role="Request Classifier",
goal="Classify incoming requests",
instructions="Classify the request. Respond with ONLY 'technical', 'creative', or 'general'."
)
# Create specialized handler agents
tech_agent = Agent(
name="TechExpert",
role="Technical Expert",
goal="Handle technical questions",
instructions="You are a technical expert. Provide detailed technical answers."
)
creative_agent = Agent(
name="CreativeWriter",
role="Creative Writer",
goal="Handle creative requests",
instructions="You are a creative writer. Write engaging, creative content."
)
general_agent = Agent(
name="GeneralAssistant",
role="General Assistant",
goal="Handle general requests",
instructions="You are a helpful assistant. Provide clear, helpful responses."
)
# Create workflow with routing
workflow = Workflow(
name="Agentic Router",
steps=[
classifier,
route({
"technical": [tech_agent],
"creative": [creative_agent],
"default": [general_agent]
})
]
)
if __name__ == "__main__":
print("=== Testing Agentic Routing Workflow ===\n")
# Test 1: Technical question
print("--- Technical Request ---")
result = workflow.start("How does machine learning work?", verbose=True)
print(f"Result: {result['output'][:200]}...\n")
# Test 2: Creative request
print("--- Creative Request ---")
result = workflow.start("Write a poem about the ocean", verbose=True)
print(f"Result: {result['output'][:200]}...")
@@ -0,0 +1,61 @@
"""
Workflow Routing Example
Demonstrates decision-based routing where the workflow
takes different paths based on the output of a decision step.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import route
# Decision maker - determines which route to take
def classify_request(ctx: WorkflowContext) -> StepResult:
"""Classify the input and decide the route."""
input_lower = ctx.input.lower()
if "urgent" in input_lower or "emergency" in input_lower:
return StepResult(output="priority: high")
elif "question" in input_lower or "help" in input_lower:
return StepResult(output="priority: support")
else:
return StepResult(output="priority: normal")
# Route handlers
def handle_high_priority(ctx: WorkflowContext) -> StepResult:
return StepResult(output="🚨 HIGH PRIORITY: Escalating to senior team immediately!")
def handle_support(ctx: WorkflowContext) -> StepResult:
return StepResult(output="💬 SUPPORT: Routing to help desk for assistance.")
def handle_normal(ctx: WorkflowContext) -> StepResult:
return StepResult(output="📋 NORMAL: Added to standard processing queue.")
# Create workflow with routing
workflow = Workflow(
name="Request Router",
steps=[
classify_request,
route({
"high": [handle_high_priority],
"support": [handle_support],
"normal": [handle_normal],
"default": [handle_normal]
})
]
)
if __name__ == "__main__":
# Test different inputs
print("=== Testing Routing Workflow ===\n")
# Test 1: Urgent request
result = workflow.start("This is an URGENT matter!", verbose=True)
print(f"Result: {result['output']}\n")
# Test 2: Support request
result = workflow.start("I have a question about my account", verbose=True)
print(f"Result: {result['output']}\n")
# Test 3: Normal request
result = workflow.start("Please process my order", verbose=True)
print(f"Result: {result['output']}")
@@ -0,0 +1,45 @@
"""
Workflow with Agents Example
Demonstrates using Agent instances directly as workflow steps.
Agents are automatically wrapped and executed in sequence.
"""
from praisonaiagents import Workflow, Agent
# Create agents
researcher = Agent(
name="Researcher",
role="Research Specialist",
goal="Find accurate information",
llm="gpt-4o-mini"
)
writer = Agent(
name="Writer",
role="Content Writer",
goal="Write engaging content",
llm="gpt-4o-mini"
)
editor = Agent(
name="Editor",
role="Content Editor",
goal="Polish and improve content",
llm="gpt-4o-mini"
)
# Create workflow with agents as steps
workflow = Workflow(
name="Content Pipeline",
steps=[researcher, writer, editor]
)
if __name__ == "__main__":
# Run the workflow
result = workflow.start(
"Write a short paragraph about artificial intelligence",
verbose=True
)
print(f"\nFinal output:\n{result['output']}")
+117
View File
@@ -0,0 +1,117 @@
# Extended agents.yaml with Workflow Patterns
# This file demonstrates the extended agents.yaml format that supports
# advanced workflow patterns while maintaining backward compatibility.
#
# Usage: praisonai agents_workflow.yaml
#
# Key differences from standard agents.yaml:
# - process: workflow (triggers workflow mode)
# - workflow: section for planning, reasoning, memory
# - steps: section for workflow patterns (route, parallel, loop, repeat)
framework: praisonai
process: workflow
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}}"
@@ -54,6 +54,13 @@ except ImportError:
MCP = None
from .session import Session
from .memory.memory import Memory
# Workflows - import from dedicated workflows module
from .workflows import (
Workflow, WorkflowStep, WorkflowContext, StepResult,
Route, Parallel, Loop, Repeat,
route, parallel, loop, repeat,
Pipeline # Alias for Workflow
)
from .guardrails import GuardrailResult, LLMGuardrail
# Fast Context support (lazy loaded to avoid performance impact)
@@ -248,7 +248,9 @@ class Agent:
fast_context_model: str = "gpt-4o-mini",
fast_context_max_turns: int = 4,
fast_context_parallelism: int = 8,
fast_context_timeout: float = 30.0
fast_context_timeout: float = 30.0,
history_in_context: Optional[int] = None,
auto_save: Optional[str] = None
):
"""Initialize an Agent instance.
@@ -544,6 +546,10 @@ Your Goal: {self.goal}
self.prompt_caching = prompt_caching
self.claude_memory = claude_memory
# Session management
self.history_in_context = history_in_context # Number of past sessions to include
self.auto_save = auto_save # Session name for auto-saving
# Initialize rules manager for persistent context (like Cursor/Windsurf)
self._rules_manager = None
self._init_rules_manager()
@@ -2627,10 +2633,10 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
# Execute the step
result = self.chat(step_prompt, **kwargs)
results.append(result)
results.append({"step": i + 1, "description": step.description, "result": result})
# Update context for next step
context += f"\n\nStep {i + 1} result: {result[:500] if result else 'No result'}"
# Update context for next step (use full result, not truncated)
context += f"\n\nStep {i + 1} result: {result if result else 'No result'}"
console.print(f" [green]✅ Completed[/green]")
@@ -2638,26 +2644,116 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
console.print(f"[dim]Progress: [{'█' * bar_length}] 100%[/dim]")
console.print(f"Completed {len(plan.steps)}/{len(plan.steps)} steps!\n")
# Return the final result
return results[-1] if results else None
# Compile all results into a comprehensive final output
if len(results) > 1:
# Create a compilation prompt
all_results_text = "\n\n".join([
f"## Step {r['step']}: {r['description']}\n\n{r['result']}"
for r in results
])
compilation_prompt = f"""You are tasked with compiling a comprehensive, detailed report from the following research steps.
IMPORTANT: Write a DETAILED and COMPREHENSIVE report. Do NOT summarize or compress the information.
Include ALL relevant details, data, statistics, and findings from each step.
Organize the information logically with clear sections and subsections.
## Research Results to Compile:
{all_results_text}
## Instructions:
1. Combine all the information into a single, well-organized document
2. Preserve ALL details, numbers, statistics, and specific findings
3. Use clear headings and subheadings
4. Do not omit any important information
5. Make it comprehensive and detailed
Write the complete compiled report:"""
console.print("\n[bold blue]📝 COMPILING FINAL REPORT[/bold blue]")
console.print("[dim]Creating comprehensive output from all steps...[/dim]\n")
final_result = self.chat(compilation_prompt, **kwargs)
return final_result
# Return the single result if only one step
return results[0]["result"] if results else None
def start(self, prompt: str, **kwargs):
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
# Load history from past sessions if history_in_context is set
self._load_history_context()
# Check if planning mode is enabled
if self.planning:
return self._start_with_planning(prompt, **kwargs)
# Check if streaming is enabled (either from kwargs or agent's stream attribute)
stream_enabled = kwargs.get('stream', getattr(self, 'stream', False))
if stream_enabled:
result = self._start_with_planning(prompt, **kwargs)
elif kwargs.get('stream', getattr(self, 'stream', False)):
# Return a generator for streaming response
return self._start_stream(prompt, **kwargs)
result = self._start_stream(prompt, **kwargs)
else:
# Return regular chat response for backward compatibility
# Explicitly pass the resolved stream parameter to avoid chat() method default
kwargs['stream'] = stream_enabled
return self.chat(prompt, **kwargs)
kwargs['stream'] = False
result = self.chat(prompt, **kwargs)
# Auto-save session if enabled
self._auto_save_session()
return result
def _load_history_context(self):
"""Load history from past sessions into context if history_in_context is set."""
if not self.history_in_context or not self._memory_instance:
return
try:
sessions = self._memory_instance.list_sessions()
if not sessions:
return
# Get the last N sessions
sessions_to_load = sessions[:self.history_in_context]
for session_info in reversed(sessions_to_load):
try:
session_data = self._memory_instance.resume_session(session_info["name"])
history = session_data.get("conversation_history", [])
# Add history to chat_history with a marker
for msg in history:
if msg not in self.chat_history:
# Mark as from history to avoid duplication
msg_copy = msg.copy()
msg_copy["_from_history"] = True
self.chat_history.append(msg_copy)
except Exception:
continue
if sessions_to_load:
logging.debug(f"Loaded history from {len(sessions_to_load)} past session(s)")
except Exception as e:
logging.debug(f"Error loading history context: {e}")
def _auto_save_session(self):
"""Auto-save session if auto_save is enabled."""
if not self.auto_save or not self._memory_instance:
return
try:
# Filter out history markers before saving
clean_history = [
{k: v for k, v in msg.items() if k != "_from_history"}
for msg in self.chat_history
]
self._memory_instance.save_session(
name=self.auto_save,
conversation_history=clean_history,
metadata={"agent_name": self.name, "user_id": self.user_id}
)
logging.debug(f"Auto-saved session: {self.auto_save}")
except Exception as e:
logging.debug(f"Error auto-saving session: {e}")
def _start_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
"""Stream generator for real-time response chunks."""
@@ -258,11 +258,24 @@ Expanded prompt:"""
prompt_lower = prompt.lower().strip()
return any(prompt_lower.startswith(verb) for verb in task_verbs)
def _is_technical_task(self, prompt: str) -> bool:
"""Check if prompt is a technical/coding task."""
technical_indicators = [
"python", "javascript", "java", "code", "function", "class",
"api", "database", "sql", "html", "css", "react", "node",
"algorithm", "debug", "fix", "error", "bug", "test",
"deploy", "server", "docker", "kubernetes", "aws", "azure",
"typescript", "rust", "go", "c++", "c#", "ruby", "php",
"script", "bash", "shell", "terminal", "command", "cli"
]
prompt_lower = prompt.lower()
return any(ind in prompt_lower for ind in technical_indicators)
def _is_creative_task(self, prompt: str) -> bool:
"""Check if prompt is a creative task."""
"""Check if prompt is a creative task (non-technical)."""
creative_indicators = [
"poem", "story", "script", "song", "art", "creative",
"fiction", "novel", "essay", "blog", "article", "write"
"poem", "story", "song", "art", "creative",
"fiction", "novel", "essay", "blog", "article"
]
prompt_lower = prompt.lower()
return any(ind in prompt_lower for ind in creative_indicators)
@@ -273,6 +286,10 @@ Expanded prompt:"""
if self._is_short_prompt(prompt, threshold=5):
return ExpandStrategy.DETAILED
# Technical/coding tasks get structured expansion (check before creative)
if self._is_technical_task(prompt):
return ExpandStrategy.STRUCTURED
# Creative tasks benefit from creative expansion
if self._is_creative_task(prompt):
return ExpandStrategy.CREATIVE
@@ -330,6 +347,37 @@ Expanded prompt:"""
current_date=current_date
)
# Add tool usage instructions if tools are available
if self.tools:
# Build dynamic tool descriptions from docstrings
tool_descriptions = []
for tool in self.tools:
tool_name = getattr(tool, '__name__', str(tool))
tool_doc = getattr(tool, '__doc__', '')
# Get first line of docstring as description
if tool_doc:
first_line = tool_doc.strip().split('\n')[0].strip()
tool_descriptions.append(f"- {tool_name}: {first_line}")
else:
tool_descriptions.append(f"- {tool_name}")
tools_list = '\n'.join(tool_descriptions)
tool_instruction = f"""
IMPORTANT: You have the following tools available:
{tools_list}
Before expanding this prompt, FIRST use the available tools to gather relevant current information about the topic. This will help you create a more informed and accurate expanded prompt.
Steps:
1. Use the appropriate tool(s) to gather relevant information about the topic
2. Use the gathered information to inform your expansion
3. Create an expanded prompt that incorporates the latest context
"""
expansion_prompt = tool_instruction + expansion_prompt
# Call the agent
expanded = self._call_agent(expansion_prompt).strip()
@@ -2,7 +2,7 @@ import os
import time
import json
import logging
from typing import Any, Dict, Optional, List
from typing import Any, Dict, Optional, List, Callable
from pydantic import BaseModel
from rich.text import Text
from rich.panel import Panel
@@ -127,7 +127,7 @@ def process_task_context(context_item, verbose=0, user_id=None):
return str(context_item) # Fallback for unknown types
class PraisonAIAgents:
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None, memory=False, memory_config=None, embedder=None, user_id=None, max_iter=10, stream=True, name: Optional[str] = None, planning: bool = False, planning_llm: Optional[str] = None, auto_approve_plan: bool = False, planning_tools: Optional[List] = None, planning_reasoning: bool = False):
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None, memory=False, memory_config=None, embedder=None, user_id=None, max_iter=10, stream=True, name: Optional[str] = None, planning: bool = False, planning_llm: Optional[str] = None, auto_approve_plan: bool = False, planning_tools: Optional[List] = None, planning_reasoning: bool = False, on_task_start: Optional[Callable] = None, on_task_complete: Optional[Callable] = None, variables: Optional[Dict[str, Any]] = None):
# Add check at the start if memory is requested
if memory:
try:
@@ -162,6 +162,11 @@ class PraisonAIAgents:
self.stream = stream
self.name = name # Store the name for the Agents collection
# Callbacks for workflow execution
self.on_task_start = on_task_start # Called before each task
self.on_task_complete = on_task_complete # Called after each task
self.variables = variables if variables else {} # Global variables for substitution
# Check for manager_llm in environment variable if not provided
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
@@ -661,13 +666,24 @@ Context:
executor_agent = task.agent
# Create agent from agent_config if provided and no agent assigned
if executor_agent is None and getattr(task, 'agent_config', None):
executor_agent = self._create_agent_from_config(task.agent_config)
task.agent = executor_agent
# Set current agent for token tracking
llm = getattr(executor_agent, 'llm', None) or getattr(executor_agent, 'llm_instance', None)
if llm and hasattr(llm, 'set_current_agent'):
llm.set_current_agent(executor_agent.name)
# Substitute variables in task description if provided
task_description = task.description
if getattr(task, 'variables', None):
for key, value in task.variables.items():
task_description = task_description.replace(f"{{{{{key}}}}}", str(value))
task_prompt = f"""
You need to do the following task: {task.description}.
You need to do the following task: {task_description}.
Expected Output: {task.expected_output}.
"""
if task.context:
@@ -825,6 +841,17 @@ Context:
logger.info(f"Task with ID {task_id} is already completed")
return
# Call on_task_start callback if provided
if self.on_task_start:
try:
self.on_task_start(task, task_id)
except Exception as e:
logger.error(f"Error in on_task_start callback: {e}")
# Apply global variables to task if not already set
if self.variables and not getattr(task, 'variables', None):
task.variables = self.variables
retries = 0
while task.status != "completed" and retries < self.max_retries:
logger.debug(f"Attempt {retries+1} for task {task_id}")
@@ -857,6 +884,14 @@ Context:
logger.exception(e)
self.save_output_to_file(task, task_output)
# Call on_task_complete callback if provided
if self.on_task_complete:
try:
self.on_task_complete(task, task_output)
except Exception as e:
logger.error(f"Error in on_task_complete callback: {e}")
if self.verbose >= 1:
logger.info(f"Task {task_id} completed successfully.")
else:
@@ -1561,6 +1596,29 @@ Context:
"""Get the current todo list."""
return self._todo_list
def _create_agent_from_config(self, agent_config: dict) -> 'Agent':
"""
Create an Agent from a configuration dictionary.
Args:
agent_config: Dict with keys like 'name', 'role', 'goal', 'backstory', 'llm', 'tools'
Returns:
Agent instance
"""
from ..agent.agent import Agent
return Agent(
name=agent_config.get('name', 'TaskAgent'),
role=agent_config.get('role'),
goal=agent_config.get('goal'),
backstory=agent_config.get('backstory'),
llm=agent_config.get('llm'),
tools=agent_config.get('tools'),
verbose=self.verbose >= 1,
memory=self.memory
)
def _get_planning_agent(self):
"""Lazy load PlanningAgent."""
if self._planning_agent is None and self.planning:
@@ -15,6 +15,8 @@ This module provides memory management capabilities including:
- Auto-generated memories (like Windsurf Cascade)
- Workflows (like Windsurf)
- Hooks (like Windsurf Cascade Hooks)
- Docs management (like Cursor docs)
- MCP config management (like Cursor .cursor/mcp/)
Memory Providers:
- FileMemory: Zero-dependency JSON file-based storage (default)
@@ -23,10 +25,14 @@ Memory Providers:
- AutoMemory: Automatic memory extraction from conversations
- WorkflowManager: Multi-step workflow execution
- HooksManager: Pre/post operation hooks
- DocsManager: Documentation context management
- MCPConfigManager: MCP server configuration management
"""
from .file_memory import FileMemory, create_memory
from .rules_manager import RulesManager, Rule, create_rules_manager
from .docs_manager import DocsManager, Doc
from .mcp_config import MCPConfigManager, MCPConfig
# Lazy imports for optional modules to avoid dependency issues and improve startup time
def __getattr__(name):
@@ -42,17 +48,58 @@ def __getattr__(name):
if name == "create_auto_memory":
from .auto_memory import create_auto_memory
return create_auto_memory
# Workflows - now in dedicated module, re-export for backward compatibility
if name == "WorkflowManager":
from .workflows import WorkflowManager
from ..workflows import WorkflowManager
return WorkflowManager
if name == "Workflow":
from .workflows import Workflow
from ..workflows import Workflow
return Workflow
if name == "WorkflowStep":
from .workflows import WorkflowStep
from ..workflows import WorkflowStep
return WorkflowStep
if name == "WorkflowContext":
from ..workflows import WorkflowContext
return WorkflowContext
if name == "StepResult":
from ..workflows import StepResult
return StepResult
if name == "Pipeline":
from ..workflows import Pipeline
return Pipeline
if name == "Route":
from ..workflows import Route
return Route
if name == "Parallel":
from ..workflows import Parallel
return Parallel
if name == "Loop":
from ..workflows import Loop
return Loop
if name == "Repeat":
from ..workflows import Repeat
return Repeat
if name == "route":
from ..workflows import route
return route
if name == "parallel":
from ..workflows import parallel
return parallel
if name == "loop":
from ..workflows import loop
return loop
if name == "repeat":
from ..workflows import repeat
return repeat
# Backward compatibility aliases
if name == "StepInput":
from ..workflows import WorkflowContext
return WorkflowContext
if name == "StepOutput":
from ..workflows import StepResult
return StepResult
if name == "create_workflow_manager":
from .workflows import create_workflow_manager
from ..workflows.workflows import create_workflow_manager
return create_workflow_manager
if name == "HooksManager":
from .hooks import HooksManager
@@ -63,6 +110,7 @@ def __getattr__(name):
if name == "create_hooks_manager":
from .hooks import create_hooks_manager
return create_hooks_manager
# DocsManager and MCPConfigManager are already imported at module level
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
@@ -78,13 +126,31 @@ __all__ = [
"AutoMemory",
"AutoMemoryExtractor",
"create_auto_memory",
# Workflows
# Workflows (re-exported from praisonaiagents.workflows for backward compatibility)
"WorkflowManager",
"Workflow",
"Pipeline",
"WorkflowStep",
"WorkflowContext",
"StepResult",
"create_workflow_manager",
# Workflow patterns
"Route",
"Parallel",
"Loop",
"Repeat",
"route",
"parallel",
"loop",
"repeat",
# Hooks
"HooksManager",
"HookResult",
"create_hooks_manager",
# Docs management
"DocsManager",
"Doc",
# MCP config management
"MCPConfigManager",
"MCPConfig",
]
@@ -0,0 +1,431 @@
"""
Docs Manager for PraisonAI Agents.
Provides reusable documentation context support similar to:
- Cursor Notepads (deprecated in favor of docs)
- Cursor .cursor/docs/
- Project documentation for AI context
Features:
- Auto-discovery of docs from .praison/docs/
- Support for markdown, text, and code files
- @doc mentions for including specific docs
- Priority ordering for docs
- Character limits for context window management
Storage Structure:
.praison/docs/
├── project-overview.md # Project overview
├── architecture.md # Architecture decisions
├── api-reference.md # API documentation
└── coding-standards.md # Coding guidelines
Doc File Format (YAML frontmatter + Markdown):
---
description: Project architecture overview
priority: 10
tags: [architecture, design]
---
# Architecture Overview
This project uses...
"""
import os
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Literal
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class Doc:
"""A single documentation file with metadata and content."""
name: str
content: str
description: str = ""
priority: int = 0
tags: List[str] = field(default_factory=list)
file_path: Optional[str] = None
def matches_tag(self, tag: str) -> bool:
"""Check if this doc has a specific tag."""
return tag.lower() in [t.lower() for t in self.tags]
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"content": self.content,
"description": self.description,
"priority": self.priority,
"tags": self.tags,
"file_path": self.file_path
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Doc':
return cls(**data)
class DocsManager:
"""
Manages documentation context for AI agents.
Provides:
- Auto-discovery of docs from .praison/docs/
- Global docs from ~/.praison/docs/
- @doc mentions for including specific docs
- Priority-based ordering
"""
DOCS_DIR_NAME = ".praison/docs"
SUPPORTED_EXTENSIONS = [".md", ".txt", ".rst"]
# Maximum characters per doc (context window management)
MAX_DOC_CHARS = 12000
def __init__(
self,
workspace_path: Optional[str] = None,
global_docs_path: Optional[str] = None,
verbose: int = 0
):
"""
Initialize DocsManager.
Args:
workspace_path: Path to workspace/project root
global_docs_path: Path to global docs (default: ~/.praison/docs)
verbose: Verbosity level
"""
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
self.global_docs_path = Path(global_docs_path) if global_docs_path else Path.home() / ".praison" / "docs"
self.verbose = verbose
self._docs: Dict[str, Doc] = {}
self._load_all_docs()
def _log(self, msg: str, level: int = logging.INFO):
"""Log message if verbose."""
if self.verbose >= 1:
logger.log(level, msg)
def _parse_frontmatter(self, content: str) -> tuple:
"""
Parse YAML frontmatter from content.
Returns:
Tuple of (frontmatter_dict, remaining_content)
"""
frontmatter = {}
remaining = content
# Check for YAML frontmatter
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
try:
import yaml
frontmatter = yaml.safe_load(parts[1]) or {}
remaining = parts[2].strip()
except Exception:
# If YAML parsing fails, use content as-is
pass
return frontmatter, remaining
def _load_doc_file(self, file_path: Path, scope: str = "workspace") -> Optional[Doc]:
"""Load a single doc file."""
try:
content = file_path.read_text(encoding="utf-8")
frontmatter, doc_content = self._parse_frontmatter(content)
name = file_path.stem
# Determine priority based on scope
base_priority = 100 if scope == "workspace" else 0
doc = Doc(
name=name,
content=doc_content,
description=frontmatter.get("description", ""),
priority=frontmatter.get("priority", base_priority),
tags=frontmatter.get("tags", []),
file_path=str(file_path)
)
return doc
except Exception as e:
self._log(f"Failed to load doc {file_path}: {e}", logging.WARNING)
return None
def _load_docs_from_dir(self, docs_dir: Path, scope: str = "workspace"):
"""Load all docs from a directory."""
if not docs_dir.exists():
return
for ext in self.SUPPORTED_EXTENSIONS:
for file_path in docs_dir.glob(f"*{ext}"):
if file_path.is_file():
doc = self._load_doc_file(file_path, scope)
if doc:
key = f"{scope}:{doc.name}"
self._docs[key] = doc
self._log(f"Loaded doc: {key}")
def _load_all_docs(self):
"""Load all docs from global and workspace directories."""
self._docs.clear()
# 1. Load global docs (lowest priority)
if self.global_docs_path.exists():
self._load_docs_from_dir(self.global_docs_path, "global")
for key, doc in self._docs.items():
if key.startswith("global:"):
doc.priority = doc.priority - 1000 # Lower priority for global
# 2. Load workspace docs
workspace_docs_dir = self.workspace_path / self.DOCS_DIR_NAME.replace("/", os.sep)
if workspace_docs_dir.exists():
self._load_docs_from_dir(workspace_docs_dir, "workspace")
self._log(f"Loaded {len(self._docs)} docs total")
def reload(self):
"""Reload all docs from disk."""
self._load_all_docs()
def get_all_docs(self) -> List[Doc]:
"""Get all loaded docs, sorted by priority."""
docs = list(self._docs.values())
docs.sort(key=lambda d: d.priority, reverse=True)
return docs
def get_doc(self, name: str) -> Optional[Doc]:
"""Get a specific doc by name."""
# Try workspace first, then global
for scope in ["workspace", "global"]:
key = f"{scope}:{name}"
if key in self._docs:
return self._docs[key]
return None
def get_docs_by_tag(self, tag: str) -> List[Doc]:
"""Get all docs with a specific tag."""
docs = [d for d in self._docs.values() if d.matches_tag(tag)]
docs.sort(key=lambda d: d.priority, reverse=True)
return docs
def get_docs_for_context(
self,
include_docs: Optional[List[str]] = None,
include_tags: Optional[List[str]] = None,
max_chars: int = 50000
) -> List[Doc]:
"""
Get docs for inclusion in agent context.
Args:
include_docs: Specific doc names to include (via @doc mentions)
include_tags: Tags to filter by
max_chars: Maximum total characters
Returns:
List of docs sorted by priority
"""
active = []
# Include specific docs by name
if include_docs:
for name in include_docs:
doc = self.get_doc(name)
if doc and doc not in active:
active.append(doc)
# Include docs by tag
if include_tags:
for tag in include_tags:
for doc in self.get_docs_by_tag(tag):
if doc not in active:
active.append(doc)
# If no specific docs requested, include all high-priority docs
if not include_docs and not include_tags:
for doc in self.get_all_docs():
if doc.priority >= 100: # Only high-priority docs by default
active.append(doc)
# Sort by priority
active.sort(key=lambda d: d.priority, reverse=True)
return active
def format_docs_for_prompt(
self,
include_docs: Optional[List[str]] = None,
include_tags: Optional[List[str]] = None,
max_chars: int = 50000
) -> str:
"""
Format docs as a string for inclusion in prompts.
Args:
include_docs: Specific doc names to include
include_tags: Tags to filter by
max_chars: Maximum total characters
Returns:
Formatted docs string
"""
active_docs = self.get_docs_for_context(include_docs, include_tags, max_chars)
if not active_docs:
return ""
parts = ["# Project Documentation\n"]
total_chars = len(parts[0])
for doc in active_docs:
if total_chars >= max_chars:
break
doc_text = doc.content.strip()
if not doc_text:
continue
# Add doc with header
if doc.description:
header = f"## {doc.name}: {doc.description}"
else:
header = f"## {doc.name}"
section = f"{header}\n{doc_text}\n"
if total_chars + len(section) <= max_chars:
parts.append(section)
total_chars += len(section)
else:
# Truncate last doc
remaining = max_chars - total_chars
if remaining > 100:
parts.append(section[:remaining] + "\n... (truncated)")
break
return "\n".join(parts)
def create_doc(
self,
name: str,
content: str,
description: str = "",
priority: int = 0,
tags: Optional[List[str]] = None,
scope: Literal["global", "workspace"] = "workspace"
) -> Doc:
"""
Create a new doc file.
Args:
name: Doc name (used as filename)
content: Doc content (markdown)
description: Short description
priority: Priority (higher = applied first)
tags: Tags for categorization
scope: Where to save (global or workspace)
Returns:
Created Doc object
"""
# Determine save path
if scope == "global":
docs_dir = self.global_docs_path
else:
docs_dir = self.workspace_path / self.DOCS_DIR_NAME.replace("/", os.sep)
docs_dir.mkdir(parents=True, exist_ok=True)
file_path = docs_dir / f"{name}.md"
# Build frontmatter
frontmatter_lines = ["---"]
if description:
frontmatter_lines.append(f'description: "{description}"')
if priority != 0:
frontmatter_lines.append(f"priority: {priority}")
if tags:
tags_str = ", ".join(f'"{t}"' for t in tags)
frontmatter_lines.append(f"tags: [{tags_str}]")
frontmatter_lines.append("---")
frontmatter_lines.append("")
# Write file
full_content = "\n".join(frontmatter_lines) + content
file_path.write_text(full_content, encoding="utf-8")
# Create and register doc
doc = Doc(
name=name,
content=content,
description=description,
priority=priority,
tags=tags or [],
file_path=str(file_path)
)
self._docs[f"{scope}:{name}"] = doc
self._log(f"Created doc '{name}' at {file_path}")
return doc
def delete_doc(self, name: str, scope: Optional[str] = None) -> bool:
"""
Delete a doc file.
Args:
name: Doc name to delete
scope: Scope to delete from (None = try both)
Returns:
True if deleted, False if not found
"""
scopes = [scope] if scope else ["workspace", "global"]
for s in scopes:
key = f"{s}:{name}"
if key in self._docs:
doc = self._docs[key]
if doc.file_path:
try:
Path(doc.file_path).unlink()
del self._docs[key]
self._log(f"Deleted doc '{name}' from {s}")
return True
except Exception as e:
self._log(f"Failed to delete doc file: {e}", logging.ERROR)
return False
def list_docs(self) -> List[Dict[str, Any]]:
"""
List all docs with metadata.
Returns:
List of doc info dicts
"""
docs = []
for key, doc in self._docs.items():
scope = key.split(":")[0]
docs.append({
"name": doc.name,
"scope": scope,
"description": doc.description,
"priority": doc.priority,
"tags": doc.tags,
"file_path": doc.file_path,
"content_length": len(doc.content)
})
# Sort by priority
docs.sort(key=lambda d: d["priority"], reverse=True)
return docs
@@ -0,0 +1,369 @@
"""
MCP Config Manager for PraisonAI Agents.
Provides JSON-based MCP server configuration support similar to:
- Cursor .cursor/mcp/*.mcpjson
- Windsurf MCP configuration
Features:
- Auto-discovery of MCP configs from .praison/mcp/
- JSON config files for MCP servers
- Environment variable support
- Easy server management
Storage Structure:
.praison/mcp/
├── filesystem.json # Filesystem MCP server
├── brave-search.json # Brave search MCP server
└── postgres.json # PostgreSQL MCP server
Config File Format (JSON):
{
"name": "filesystem",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"env": {
"SOME_VAR": "value"
},
"enabled": true,
"description": "Filesystem access for the agent"
}
"""
import os
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class MCPConfig:
"""A single MCP server configuration."""
name: str
command: str
args: List[str] = field(default_factory=list)
env: Dict[str, str] = field(default_factory=dict)
enabled: bool = True
description: str = ""
file_path: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"command": self.command,
"args": self.args,
"env": self.env,
"enabled": self.enabled,
"description": self.description
}
@classmethod
def from_dict(cls, data: Dict[str, Any], file_path: Optional[str] = None) -> 'MCPConfig':
return cls(
name=data.get("name", ""),
command=data.get("command", ""),
args=data.get("args", []),
env=data.get("env", {}),
enabled=data.get("enabled", True),
description=data.get("description", ""),
file_path=file_path
)
def to_mcp_instance(self):
"""
Convert this config to an MCP instance.
Returns:
MCP instance ready for use with agents
"""
try:
from praisonaiagents.tools.mcp import MCP
# Resolve environment variables
resolved_env = {}
for key, value in self.env.items():
if value.startswith("$"):
# Environment variable reference
env_var = value[1:]
resolved_env[key] = os.environ.get(env_var, value)
else:
resolved_env[key] = value
return MCP(
command=self.command,
args=self.args,
env=resolved_env if resolved_env else None
)
except ImportError:
logger.warning("MCP tools not available")
return None
class MCPConfigManager:
"""
Manages MCP server configurations for AI agents.
Provides:
- Auto-discovery of MCP configs from .praison/mcp/
- Global configs from ~/.praison/mcp/
- Easy server management
"""
MCP_DIR_NAME = ".praison/mcp"
SUPPORTED_EXTENSIONS = [".json", ".mcpjson"]
def __init__(
self,
workspace_path: Optional[str] = None,
global_mcp_path: Optional[str] = None,
verbose: int = 0
):
"""
Initialize MCPConfigManager.
Args:
workspace_path: Path to workspace/project root
global_mcp_path: Path to global MCP configs (default: ~/.praison/mcp)
verbose: Verbosity level
"""
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
self.global_mcp_path = Path(global_mcp_path) if global_mcp_path else Path.home() / ".praison" / "mcp"
self.verbose = verbose
self._configs: Dict[str, MCPConfig] = {}
self._load_all_configs()
def _log(self, msg: str, level: int = logging.INFO):
"""Log message if verbose."""
if self.verbose >= 1:
logger.log(level, msg)
def _load_config_file(self, file_path: Path, scope: str = "workspace") -> Optional[MCPConfig]:
"""Load a single MCP config file."""
try:
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)
# Use filename as name if not specified
if not data.get("name"):
data["name"] = file_path.stem
config = MCPConfig.from_dict(data, str(file_path))
return config
except json.JSONDecodeError as e:
self._log(f"Invalid JSON in {file_path}: {e}", logging.WARNING)
return None
except Exception as e:
self._log(f"Failed to load MCP config {file_path}: {e}", logging.WARNING)
return None
def _load_configs_from_dir(self, mcp_dir: Path, scope: str = "workspace"):
"""Load all MCP configs from a directory."""
if not mcp_dir.exists():
return
for ext in self.SUPPORTED_EXTENSIONS:
for file_path in mcp_dir.glob(f"*{ext}"):
if file_path.is_file():
config = self._load_config_file(file_path, scope)
if config:
key = f"{scope}:{config.name}"
self._configs[key] = config
self._log(f"Loaded MCP config: {key}")
def _load_all_configs(self):
"""Load all MCP configs from global and workspace directories."""
self._configs.clear()
# 1. Load global configs
if self.global_mcp_path.exists():
self._load_configs_from_dir(self.global_mcp_path, "global")
# 2. Load workspace configs (override global)
workspace_mcp_dir = self.workspace_path / self.MCP_DIR_NAME.replace("/", os.sep)
if workspace_mcp_dir.exists():
self._load_configs_from_dir(workspace_mcp_dir, "workspace")
self._log(f"Loaded {len(self._configs)} MCP configs total")
def reload(self):
"""Reload all configs from disk."""
self._load_all_configs()
def get_all_configs(self) -> List[MCPConfig]:
"""Get all loaded MCP configs."""
return list(self._configs.values())
def get_enabled_configs(self) -> List[MCPConfig]:
"""Get all enabled MCP configs."""
return [c for c in self._configs.values() if c.enabled]
def get_config(self, name: str) -> Optional[MCPConfig]:
"""Get a specific MCP config by name."""
# Try workspace first, then global
for scope in ["workspace", "global"]:
key = f"{scope}:{name}"
if key in self._configs:
return self._configs[key]
return None
def get_mcp_tools(self, names: Optional[List[str]] = None) -> List:
"""
Get MCP instances for use with agents.
Args:
names: Specific config names to load (None = all enabled)
Returns:
List of MCP instances
"""
tools = []
if names:
configs = [self.get_config(name) for name in names]
configs = [c for c in configs if c and c.enabled]
else:
configs = self.get_enabled_configs()
for config in configs:
mcp = config.to_mcp_instance()
if mcp:
tools.append(mcp)
return tools
def create_config(
self,
name: str,
command: str,
args: Optional[List[str]] = None,
env: Optional[Dict[str, str]] = None,
enabled: bool = True,
description: str = "",
scope: str = "workspace"
) -> MCPConfig:
"""
Create a new MCP config file.
Args:
name: Config name (used as filename)
command: Command to run (e.g., "npx")
args: Command arguments
env: Environment variables
enabled: Whether the config is enabled
description: Description of the MCP server
scope: Where to save (global or workspace)
Returns:
Created MCPConfig object
"""
# Determine save path
if scope == "global":
mcp_dir = self.global_mcp_path
else:
mcp_dir = self.workspace_path / self.MCP_DIR_NAME.replace("/", os.sep)
mcp_dir.mkdir(parents=True, exist_ok=True)
file_path = mcp_dir / f"{name}.json"
# Build config
config = MCPConfig(
name=name,
command=command,
args=args or [],
env=env or {},
enabled=enabled,
description=description,
file_path=str(file_path)
)
# Write file
file_path.write_text(
json.dumps(config.to_dict(), indent=2),
encoding="utf-8"
)
self._configs[f"{scope}:{name}"] = config
self._log(f"Created MCP config '{name}' at {file_path}")
return config
def delete_config(self, name: str, scope: Optional[str] = None) -> bool:
"""
Delete an MCP config file.
Args:
name: Config name to delete
scope: Scope to delete from (None = try both)
Returns:
True if deleted, False if not found
"""
scopes = [scope] if scope else ["workspace", "global"]
for s in scopes:
key = f"{s}:{name}"
if key in self._configs:
config = self._configs[key]
if config.file_path:
try:
Path(config.file_path).unlink()
del self._configs[key]
self._log(f"Deleted MCP config '{name}' from {s}")
return True
except Exception as e:
self._log(f"Failed to delete config file: {e}", logging.ERROR)
return False
def enable_config(self, name: str) -> bool:
"""Enable an MCP config."""
config = self.get_config(name)
if config:
config.enabled = True
self._save_config(config)
return True
return False
def disable_config(self, name: str) -> bool:
"""Disable an MCP config."""
config = self.get_config(name)
if config:
config.enabled = False
self._save_config(config)
return True
return False
def _save_config(self, config: MCPConfig):
"""Save a config back to its file."""
if config.file_path:
Path(config.file_path).write_text(
json.dumps(config.to_dict(), indent=2),
encoding="utf-8"
)
def list_configs(self) -> List[Dict[str, Any]]:
"""
List all MCP configs with metadata.
Returns:
List of config info dicts
"""
configs = []
for key, config in self._configs.items():
scope = key.split(":")[0]
configs.append({
"name": config.name,
"scope": scope,
"command": config.command,
"args": config.args,
"enabled": config.enabled,
"description": config.description,
"file_path": config.file_path
})
return configs
File diff suppressed because it is too large. Load diff
@@ -826,7 +826,34 @@ Provide a JSON with the structure:
logging.info("Hierarchical task execution finished")
def workflow(self):
"""Synchronous version of workflow method"""
"""Synchronous version of workflow method.
DEPRECATED: This method is deprecated. Use the Workflow class instead:
```python
from praisonaiagents import Workflow, StepResult
from praisonaiagents.workflows import route, parallel, loop, repeat
workflow = Workflow(steps=[step1, step2])
result = workflow.start("input")
```
The Workflow class provides a simpler API with more features:
- route() for decision-based branching
- parallel() for concurrent execution
- loop() for iteration over lists/CSV
- repeat() for evaluator-optimizer patterns
- Callbacks (on_step_start, on_step_complete, etc.)
- Guardrails with validation feedback
- Status tracking
"""
import warnings
warnings.warn(
"process='workflow' is deprecated. Use the Workflow class instead: "
"from praisonaiagents import Workflow; workflow = Workflow(steps=[...]); workflow.start()",
DeprecationWarning,
stacklevel=2
)
current_iter = 0 # Track how many times we've looped
# Build workflow relationships first
for task in self.tasks.values():
@@ -44,7 +44,9 @@ class Task:
retain_full_context: bool = False, # By default, only use previous task output, not all previous tasks
guardrail: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None,
max_retries: int = 3,
retry_count: int = 0
retry_count: int = 0,
agent_config: Optional[Dict[str, Any]] = None,
variables: Optional[Dict[str, Any]] = None
):
# Add check if memory config is provided
if memory is not None or (config and config.get('memory_config')):
@@ -89,6 +91,8 @@ class Task:
self.retry_count = retry_count
self._guardrail_fn = None
self.validation_feedback = None # Store validation failure feedback for retry attempts
self.agent_config = agent_config # Per-task agent configuration {role, goal, backstory, llm}
self.variables = variables if variables else {} # Variables for substitution in description
# Set logger level based on config verbose level
verbose = self.config.get("verbose", 0)
@@ -0,0 +1,319 @@
"""
Mentions Parser for PraisonAI Agents.
Provides @mention syntax support similar to Cursor IDE:
- @file:path/to/file.py - Include file content
- @web:query - Search the web
- @doc:name - Include doc from .praison/docs/
- @rule:name - Include specific rule
Usage:
from praisonaiagents.tools.mentions import MentionsParser
parser = MentionsParser()
context, cleaned_prompt = parser.process("@file:main.py explain this code")
"""
import re
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
class MentionsParser:
"""
Parse and process @mentions in prompts.
Supports:
- @file:path - Include file content
- @web:query - Web search (requires web search tool)
- @doc:name - Include doc from .praison/docs/
- @rule:name - Include specific rule
- @url:https://... - Fetch URL content
"""
# Patterns for different mention types
PATTERNS = {
"file": re.compile(r'@file:([^\s]+)'),
"web": re.compile(r'@web:([^\s]+(?:\s+[^\s@]+)*)'),
"doc": re.compile(r'@doc:([^\s]+)'),
"rule": re.compile(r'@rule:([^\s]+)'),
"url": re.compile(r'@url:(https?://[^\s]+)'),
}
def __init__(
self,
workspace_path: Optional[str] = None,
verbose: int = 0
):
"""
Initialize MentionsParser.
Args:
workspace_path: Path to workspace/project root
verbose: Verbosity level
"""
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
self.verbose = verbose
# Lazy-loaded managers
self._docs_manager = None
self._rules_manager = None
def _log(self, msg: str, level: int = logging.INFO):
"""Log message if verbose."""
if self.verbose >= 1:
logger.log(level, msg)
def _get_docs_manager(self):
"""Lazy load DocsManager."""
if self._docs_manager is None:
try:
from praisonaiagents.memory.docs_manager import DocsManager
self._docs_manager = DocsManager(workspace_path=str(self.workspace_path))
except ImportError:
self._log("DocsManager not available", logging.WARNING)
return self._docs_manager
def _get_rules_manager(self):
"""Lazy load RulesManager."""
if self._rules_manager is None:
try:
from praisonaiagents.memory.rules_manager import RulesManager
self._rules_manager = RulesManager(workspace_path=str(self.workspace_path))
except ImportError:
self._log("RulesManager not available", logging.WARNING)
return self._rules_manager
def process(self, prompt: str) -> Tuple[str, str]:
"""
Process a prompt and extract @mentions.
Args:
prompt: The input prompt with @mentions
Returns:
Tuple of (context_string, cleaned_prompt)
"""
context_parts = []
cleaned_prompt = prompt
# Process each mention type
for mention_type, pattern in self.PATTERNS.items():
matches = pattern.findall(prompt)
for match in matches:
context = self._process_mention(mention_type, match)
if context:
context_parts.append(context)
# Remove the mention from the prompt
cleaned_prompt = pattern.sub('', cleaned_prompt, count=1)
# Clean up extra whitespace
cleaned_prompt = ' '.join(cleaned_prompt.split())
# Build context string
context_string = ""
if context_parts:
context_string = "\n\n".join(context_parts) + "\n\n"
return context_string, cleaned_prompt
def _process_mention(self, mention_type: str, value: str) -> Optional[str]:
"""
Process a single mention.
Args:
mention_type: Type of mention (file, web, doc, rule, url)
value: The mention value
Returns:
Context string or None
"""
if mention_type == "file":
return self._process_file_mention(value)
elif mention_type == "web":
return self._process_web_mention(value)
elif mention_type == "doc":
return self._process_doc_mention(value)
elif mention_type == "rule":
return self._process_rule_mention(value)
elif mention_type == "url":
return self._process_url_mention(value)
return None
def _process_file_mention(self, file_path: str) -> Optional[str]:
"""Process @file:path mention."""
try:
# Resolve path relative to workspace
full_path = self.workspace_path / file_path
if not full_path.exists():
# Try as absolute path
full_path = Path(file_path)
if not full_path.exists():
self._log(f"File not found: {file_path}", logging.WARNING)
return f"# File: {file_path}\n[File not found]"
content = full_path.read_text(encoding="utf-8")
# Limit content size
if len(content) > 50000:
content = content[:50000] + "\n... (truncated)"
# Detect language for code fence
ext = full_path.suffix.lower()
lang_map = {
".py": "python",
".js": "javascript",
".ts": "typescript",
".jsx": "jsx",
".tsx": "tsx",
".java": "java",
".cpp": "cpp",
".c": "c",
".go": "go",
".rs": "rust",
".rb": "ruby",
".php": "php",
".swift": "swift",
".kt": "kotlin",
".md": "markdown",
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".xml": "xml",
".html": "html",
".css": "css",
".sql": "sql",
".sh": "bash",
}
lang = lang_map.get(ext, "")
return f"# File: {file_path}\n```{lang}\n{content}\n```"
except Exception as e:
self._log(f"Error reading file {file_path}: {e}", logging.ERROR)
return f"# File: {file_path}\n[Error reading file: {e}]"
def _process_web_mention(self, query: str) -> Optional[str]:
"""Process @web:query mention."""
try:
# Try to use web search
from praisonaiagents.tools import duckduckgo_search
results = duckduckgo_search(query, max_results=3)
if results:
return f"# Web Search: {query}\n{results}"
else:
return f"# Web Search: {query}\n[No results found]"
except ImportError:
self._log("Web search not available", logging.WARNING)
return f"# Web Search: {query}\n[Web search not available. Install with: pip install duckduckgo-search]"
except Exception as e:
self._log(f"Error in web search: {e}", logging.ERROR)
return f"# Web Search: {query}\n[Error: {e}]"
def _process_doc_mention(self, doc_name: str) -> Optional[str]:
"""Process @doc:name mention."""
docs = self._get_docs_manager()
if not docs:
return f"# Doc: {doc_name}\n[DocsManager not available]"
doc = docs.get_doc(doc_name)
if doc:
return f"# Doc: {doc.name}\n{doc.content}"
else:
return f"# Doc: {doc_name}\n[Doc not found]"
def _process_rule_mention(self, rule_name: str) -> Optional[str]:
"""Process @rule:name mention."""
rules = self._get_rules_manager()
if not rules:
return f"# Rule: {rule_name}\n[RulesManager not available]"
rule = rules.get_rule(rule_name)
if rule:
return f"# Rule: {rule.name}\n{rule.content}"
else:
return f"# Rule: {rule_name}\n[Rule not found]"
def _process_url_mention(self, url: str) -> Optional[str]:
"""Process @url:https://... mention."""
try:
import urllib.request
req = urllib.request.Request(
url,
headers={'User-Agent': 'Mozilla/5.0 (compatible; PraisonAI/1.0)'}
)
with urllib.request.urlopen(req, timeout=10) as response:
content = response.read().decode('utf-8', errors='ignore')
# Limit content size
if len(content) > 50000:
content = content[:50000] + "\n... (truncated)"
# Try to extract text from HTML
if '<html' in content.lower():
# Simple HTML text extraction
import re
# Remove scripts and styles
content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL | re.IGNORECASE)
# Remove tags
content = re.sub(r'<[^>]+>', ' ', content)
# Clean whitespace
content = ' '.join(content.split())
return f"# URL: {url}\n{content[:10000]}"
except Exception as e:
self._log(f"Error fetching URL {url}: {e}", logging.ERROR)
return f"# URL: {url}\n[Error fetching URL: {e}]"
def extract_mentions(self, prompt: str) -> Dict[str, List[str]]:
"""
Extract all mentions from a prompt without processing them.
Args:
prompt: The input prompt
Returns:
Dict mapping mention types to lists of values
"""
mentions = {}
for mention_type, pattern in self.PATTERNS.items():
matches = pattern.findall(prompt)
if matches:
mentions[mention_type] = matches
return mentions
def has_mentions(self, prompt: str) -> bool:
"""Check if a prompt contains any @mentions."""
for pattern in self.PATTERNS.values():
if pattern.search(prompt):
return True
return False
def process_mentions(prompt: str, workspace_path: Optional[str] = None) -> Tuple[str, str]:
"""
Convenience function to process mentions in a prompt.
Args:
prompt: The input prompt with @mentions
workspace_path: Optional workspace path
Returns:
Tuple of (context_string, cleaned_prompt)
"""
parser = MentionsParser(workspace_path=workspace_path)
return parser.process(prompt)
# Export
__all__ = ["MentionsParser", "process_mentions"]
@@ -0,0 +1,265 @@
"""
Rules Tools for PraisonAI Agents.
Provides tools for agents to create, read, and manage rules dynamically.
This enables Agent-Requested rules similar to Cursor's agent-requested rules feature.
Usage:
from praisonaiagents.tools.rules_tools import (
create_rule_tool,
list_rules_tool,
get_rule_tool,
delete_rule_tool
)
agent = Agent(
name="Assistant",
tools=[create_rule_tool, list_rules_tool, get_rule_tool]
)
"""
import logging
from typing import Optional
logger = logging.getLogger(__name__)
# Singleton RulesManager instance
_rules_manager = None
def _get_rules_manager():
"""Get or create the RulesManager singleton."""
global _rules_manager
if _rules_manager is None:
from praisonaiagents.memory.rules_manager import RulesManager
_rules_manager = RulesManager()
return _rules_manager
def create_rule_tool(
name: str,
content: str,
description: str = "",
globs: Optional[str] = None,
activation: str = "always",
priority: int = 0,
scope: str = "workspace"
) -> str:
"""
Create a new rule for the AI agent to follow.
This tool allows the agent to create persistent rules that will be
applied to future conversations and tasks.
Args:
name: Rule name (used as filename, no spaces or special chars)
content: The rule content in markdown format
description: Short description of what the rule does
globs: Comma-separated glob patterns for file matching (e.g., "**/*.py,**/*.js")
activation: When to apply the rule:
- "always": Always apply this rule
- "glob": Apply when working with files matching globs
- "manual": Only apply when explicitly mentioned
- "ai_decision": Let AI decide when to apply
priority: Priority (higher = applied first, default 0)
scope: Where to save ("workspace" or "global")
Returns:
Success message with rule details
Example:
create_rule_tool(
name="python-style",
content="- Use type hints for all functions\\n- Follow PEP 8",
description="Python coding standards",
globs="**/*.py",
activation="glob"
)
"""
try:
manager = _get_rules_manager()
# Parse globs
glob_list = None
if globs:
glob_list = [g.strip() for g in globs.split(",") if g.strip()]
# Validate activation
valid_activations = ["always", "glob", "manual", "ai_decision"]
if activation not in valid_activations:
return f"Error: activation must be one of {valid_activations}"
# Validate scope
if scope not in ["workspace", "global"]:
return "Error: scope must be 'workspace' or 'global'"
# Create the rule
rule = manager.create_rule(
name=name,
content=content,
description=description,
globs=glob_list,
activation=activation,
priority=priority,
scope=scope
)
return f"""Successfully created rule '{name}'
Location: {rule.file_path}
Activation: {activation}
Priority: {priority}
Description: {description or 'None'}
Globs: {glob_list or 'None'}
The rule will be applied according to its activation mode."""
except Exception as e:
logger.error(f"Error creating rule: {e}")
return f"Error creating rule: {str(e)}"
def list_rules_tool() -> str:
"""
List all available rules.
Returns a formatted list of all rules with their metadata.
Returns:
Formatted string with rule information
"""
try:
manager = _get_rules_manager()
rules = manager.get_all_rules()
if not rules:
return "No rules found. Use create_rule_tool to create new rules."
lines = ["# Available Rules\n"]
for rule in rules:
scope = "global" if "global:" in (rule.file_path or "") else "workspace"
lines.append(f"## {rule.name}")
lines.append(f"- **Activation**: {rule.activation}")
lines.append(f"- **Priority**: {rule.priority}")
if rule.description:
lines.append(f"- **Description**: {rule.description}")
if rule.globs:
lines.append(f"- **Globs**: {', '.join(rule.globs)}")
lines.append(f"- **Scope**: {scope}")
lines.append("")
return "\n".join(lines)
except Exception as e:
logger.error(f"Error listing rules: {e}")
return f"Error listing rules: {str(e)}"
def get_rule_tool(name: str) -> str:
"""
Get the content of a specific rule.
Args:
name: The name of the rule to retrieve
Returns:
The rule content and metadata
"""
try:
manager = _get_rules_manager()
rule = manager.get_rule(name)
if not rule:
return f"Rule '{name}' not found. Use list_rules_tool to see available rules."
return f"""# Rule: {rule.name}
**Description**: {rule.description or 'None'}
**Activation**: {rule.activation}
**Priority**: {rule.priority}
**Globs**: {', '.join(rule.globs) if rule.globs else 'None'}
**File**: {rule.file_path}
## Content:
{rule.content}
"""
except Exception as e:
logger.error(f"Error getting rule: {e}")
return f"Error getting rule: {str(e)}"
def delete_rule_tool(name: str, scope: Optional[str] = None) -> str:
"""
Delete a rule.
Args:
name: The name of the rule to delete
scope: Optional scope to delete from ("workspace" or "global")
Returns:
Success or error message
"""
try:
manager = _get_rules_manager()
if scope and scope not in ["workspace", "global"]:
return "Error: scope must be 'workspace' or 'global'"
success = manager.delete_rule(name, scope)
if success:
return f"Successfully deleted rule '{name}'"
else:
return f"Rule '{name}' not found or could not be deleted"
except Exception as e:
logger.error(f"Error deleting rule: {e}")
return f"Error deleting rule: {str(e)}"
def get_active_rules_tool(file_path: Optional[str] = None) -> str:
"""
Get all rules that are currently active for a given file or context.
Args:
file_path: Optional file path to check which rules apply
Returns:
Formatted string with active rules
"""
try:
manager = _get_rules_manager()
if file_path:
rules = manager.get_rules_for_file(file_path)
else:
rules = manager.get_active_rules()
if not rules:
return "No active rules for this context."
lines = ["# Active Rules\n"]
for rule in rules:
lines.append(f"## {rule.name}")
if rule.description:
lines.append(f"*{rule.description}*\n")
lines.append(rule.content)
lines.append("")
return "\n".join(lines)
except Exception as e:
logger.error(f"Error getting active rules: {e}")
return f"Error getting active rules: {str(e)}"
# Export all tools
__all__ = [
"create_rule_tool",
"list_rules_tool",
"get_rule_tool",
"delete_rule_tool",
"get_active_rules_tool"
]
@@ -0,0 +1,60 @@
"""
Workflows module for PraisonAI Agents.
Provides workflow/pipeline patterns for orchestrating agents and functions.
"""
from .workflows import (
# Core classes
Workflow,
Pipeline, # Alias for Workflow
WorkflowStep,
WorkflowContext,
StepResult,
WorkflowManager,
# Pattern helpers
Route,
Parallel,
Loop,
Repeat,
# Convenience functions
route,
parallel,
loop,
repeat,
# Backward compatibility aliases
StepInput,
StepOutput,
)
from .yaml_parser import YAMLWorkflowParser
__all__ = [
# Core
"Workflow",
"Pipeline",
"WorkflowStep",
"WorkflowContext",
"StepResult",
"WorkflowManager",
# YAML Parser
"YAMLWorkflowParser",
# Patterns
"Route",
"Parallel",
"Loop",
"Repeat",
"route",
"parallel",
"loop",
"repeat",
# Aliases
"StepInput",
"StepOutput",
]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,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.0.190"
version = "0.1.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -0,0 +1,390 @@
"""
Test suite for Workflow Per-Step Agent Configuration.
Tests the enhanced workflow system's ability to configure different agents
for each step, including custom roles, goals, backstories, and LLM settings.
"""
import pytest
from unittest.mock import Mock, patch
import sys
import os
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager
)
class TestWorkflowStepAgentFields:
"""Test WorkflowStep dataclass agent-related fields."""
def test_workflow_step_has_agent_config_field(self):
"""WorkflowStep should have agent_config field."""
step = WorkflowStep(
name="test_step",
action="do something",
agent_config={
"role": "Researcher",
"goal": "Find information",
"backstory": "Expert researcher"
}
)
assert hasattr(step, 'agent_config')
assert step.agent_config["role"] == "Researcher"
def test_workflow_step_has_tools_field(self):
"""WorkflowStep should have tools field."""
step = WorkflowStep(
name="test_step",
action="do something",
tools=["search", "calculator"]
)
assert hasattr(step, 'tools')
assert "search" in step.tools
def test_workflow_step_agent_config_default_none(self):
"""agent_config should default to None."""
step = WorkflowStep(name="test_step", action="do something")
assert step.agent_config is None
def test_workflow_step_tools_default_none(self):
"""tools should default to None."""
step = WorkflowStep(name="test_step", action="do something")
assert step.tools is None
def test_workflow_step_to_dict_includes_agent_fields(self):
"""to_dict() should include agent-related fields."""
step = WorkflowStep(
name="test_step",
action="do something",
agent_config={"role": "Writer"},
tools=["grammar_check"]
)
d = step.to_dict()
assert "agent_config" in d
assert "tools" in d
class TestWorkflowDefaultAgentConfig:
"""Test Workflow dataclass default agent configuration."""
def test_workflow_has_default_agent_config(self):
"""Workflow should have default_agent_config field."""
workflow = Workflow(
name="test_workflow",
default_agent_config={
"role": "Assistant",
"goal": "Help with tasks"
}
)
assert hasattr(workflow, 'default_agent_config')
assert workflow.default_agent_config["role"] == "Assistant"
def test_workflow_has_default_llm(self):
"""Workflow should have default_llm field."""
workflow = Workflow(
name="test_workflow",
default_llm="gpt-4o-mini"
)
assert hasattr(workflow, 'default_llm')
assert workflow.default_llm == "gpt-4o-mini"
def test_workflow_to_dict_includes_defaults(self):
"""to_dict() should include default agent config fields."""
workflow = Workflow(
name="test_workflow",
default_agent_config={"role": "Helper"},
default_llm="gpt-4o"
)
d = workflow.to_dict()
assert "default_agent_config" in d
assert "default_llm" in d
class TestCreateStepAgent:
"""Test _create_step_agent method."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_create_step_agent_with_config(self):
"""Should create agent from step's agent_config."""
manager = WorkflowManager()
step = WorkflowStep(
name="research",
action="Research topic",
agent_config={
"role": "Researcher",
"goal": "Find comprehensive information",
"backstory": "Expert in research"
}
)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
MockAgent.return_value = Mock()
agent = manager._create_step_agent(
step=step,
default_llm="gpt-4o-mini",
verbose=0
)
# Verify Agent was called with config
MockAgent.assert_called_once()
call_kwargs = MockAgent.call_args[1]
assert call_kwargs["role"] == "Researcher"
assert call_kwargs["goal"] == "Find comprehensive information"
def test_create_step_agent_uses_default_agent(self):
"""Should return default_agent when no agent_config."""
manager = WorkflowManager()
step = WorkflowStep(
name="simple_step",
action="Do something"
)
default_agent = Mock()
agent = manager._create_step_agent(
step=step,
default_agent=default_agent,
default_llm="gpt-4o-mini"
)
assert agent == default_agent
def test_create_step_agent_includes_tools(self):
"""Should include tools in agent config."""
manager = WorkflowManager()
mock_tool = Mock()
step = WorkflowStep(
name="research",
action="Research topic",
agent_config={"role": "Researcher"},
tools=[mock_tool]
)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
MockAgent.return_value = Mock()
manager._create_step_agent(
step=step,
default_llm="gpt-4o-mini"
)
call_kwargs = MockAgent.call_args[1]
assert "tools" in call_kwargs
assert mock_tool in call_kwargs["tools"]
def test_create_step_agent_includes_memory(self):
"""Should include memory in agent config."""
manager = WorkflowManager()
step = WorkflowStep(
name="research",
action="Research topic",
agent_config={"role": "Researcher"}
)
mock_memory = Mock()
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
MockAgent.return_value = Mock()
manager._create_step_agent(
step=step,
memory=mock_memory
)
call_kwargs = MockAgent.call_args[1]
assert "memory" in call_kwargs
assert call_kwargs["memory"] == mock_memory
class TestExecuteWithAgents:
"""Test execute() with per-step agent configuration."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_execute_with_default_agent(self):
"""Execute should use default_agent when provided."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
# Create mock default agent
mock_agent = Mock()
mock_agent.chat = Mock(return_value="Agent response")
result = manager.execute(
"test_workflow",
default_agent=mock_agent
)
assert result["success"]
assert mock_agent.chat.call_count == 2
def test_execute_with_step_agent_config(self):
"""Execute should create agent from step's agent_config."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="research",
action="Research AI",
agent_config={
"role": "Researcher",
"goal": "Find information"
}
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent_instance = Mock()
mock_agent_instance.chat = Mock(return_value="Research results")
MockAgent.return_value = mock_agent_instance
result = manager.execute(
"test_workflow",
default_llm="gpt-4o-mini"
)
assert result["success"]
MockAgent.assert_called_once()
def test_execute_with_executor_overrides_agent(self):
"""Executor function should override agent creation."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Should not be used"}
)
]
)
self._register_workflow(manager, workflow)
executor_called = [False]
def custom_executor(action):
executor_called[0] = True
return "Custom executor result"
result = manager.execute(
"test_workflow",
executor=custom_executor
)
assert result["success"]
assert executor_called[0]
def test_execute_fails_without_executor_or_agent(self):
"""Execute should fail if no executor or agent provided."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
result = manager.execute("test_workflow")
assert not result["success"]
assert "No executor available" in result.get("error", "")
class TestWorkflowLevelDefaults:
"""Test workflow-level default configurations."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_workflow_default_llm_used(self):
"""Workflow's default_llm should be used for agent creation."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
default_llm="gpt-4o",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Helper"}
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent_instance = Mock()
mock_agent_instance.chat = Mock(return_value="Response")
MockAgent.return_value = mock_agent_instance
manager.execute("test_workflow")
call_kwargs = MockAgent.call_args[1]
assert call_kwargs.get("llm") == "gpt-4o"
def test_execute_default_llm_overrides_workflow(self):
"""Execute's default_llm should override workflow's default_llm."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
default_llm="gpt-3.5-turbo",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Helper"}
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent_instance = Mock()
mock_agent_instance.chat = Mock(return_value="Response")
MockAgent.return_value = mock_agent_instance
manager.execute("test_workflow", default_llm="gpt-4o-mini")
call_kwargs = MockAgent.call_args[1]
assert call_kwargs.get("llm") == "gpt-4o-mini"
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,202 @@
"""
Test suite for Workflow Async Support.
Tests the aexecute() async method for workflow execution.
"""
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch
import sys
import os
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager
)
class TestAexecuteMethod:
"""Test aexecute() async method."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
@pytest.mark.asyncio
async def test_aexecute_exists(self):
"""WorkflowManager should have aexecute method."""
manager = WorkflowManager()
assert hasattr(manager, 'aexecute')
assert asyncio.iscoroutinefunction(manager.aexecute)
@pytest.mark.asyncio
async def test_aexecute_with_sync_executor(self):
"""aexecute should work with sync executor."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
def sync_executor(action):
return f"Sync result for {action}"
result = await manager.aexecute("test_workflow", executor=sync_executor)
assert result["success"]
assert len(result["results"]) == 2
@pytest.mark.asyncio
async def test_aexecute_with_async_executor(self):
"""aexecute should work with async executor."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
async def async_executor(action):
await asyncio.sleep(0.01) # Simulate async operation
return f"Async result for {action}"
result = await manager.aexecute("test_workflow", executor=async_executor)
assert result["success"]
assert len(result["results"]) == 2
assert "Async result" in result["results"][0]["output"]
@pytest.mark.asyncio
async def test_aexecute_context_passing(self):
"""aexecute should pass context between steps."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Generate data"),
WorkflowStep(name="step2", action="Process {{previous_output}}")
]
)
self._register_workflow(manager, workflow)
received_actions = []
async def async_executor(action):
received_actions.append(action)
if "Generate" in action:
return "DATA_ABC"
return "Processed"
result = await manager.aexecute("test_workflow", executor=async_executor)
assert result["success"]
# Step 2 should have received context
assert "DATA_ABC" in received_actions[1] or "previous_output" in received_actions[1]
@pytest.mark.asyncio
async def test_aexecute_workflow_not_found(self):
"""aexecute should return error for non-existent workflow."""
manager = WorkflowManager()
manager._loaded = True
result = await manager.aexecute("nonexistent_workflow")
assert not result["success"]
assert "not found" in result.get("error", "")
@pytest.mark.asyncio
async def test_aexecute_with_default_agent(self):
"""aexecute should work with default_agent."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
mock_agent = Mock()
mock_agent.chat = Mock(return_value="Agent response")
result = await manager.aexecute(
"test_workflow",
default_agent=mock_agent
)
assert result["success"]
mock_agent.chat.assert_called()
@pytest.mark.asyncio
async def test_aexecute_with_async_agent(self):
"""aexecute should use achat if available on agent."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Helper"}
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent = Mock()
mock_agent.achat = AsyncMock(return_value="Async agent response")
MockAgent.return_value = mock_agent
result = await manager.aexecute(
"test_workflow",
default_llm="gpt-4o-mini"
)
assert result["success"]
mock_agent.achat.assert_called()
@pytest.mark.asyncio
async def test_aexecute_returns_variables(self):
"""aexecute should return accumulated variables."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
variables={"initial": "value"},
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
async def async_executor(action):
return "Output"
result = await manager.aexecute("test_workflow", executor=async_executor)
assert "variables" in result
assert "previous_output" in result["variables"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,353 @@
"""
Test suite for Workflow Context Passing functionality.
Tests the enhanced workflow system's ability to pass context between steps,
including output accumulation, variable substitution, and selective context inclusion.
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
import sys
import os
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager,
create_workflow_manager
)
class TestWorkflowStepContextFields:
"""Test WorkflowStep dataclass context-related fields."""
def test_workflow_step_has_context_from_field(self):
"""WorkflowStep should have context_from field."""
step = WorkflowStep(
name="test_step",
action="do something",
context_from=["step1", "step2"]
)
assert hasattr(step, 'context_from')
assert step.context_from == ["step1", "step2"]
def test_workflow_step_has_retain_full_context_field(self):
"""WorkflowStep should have retain_full_context field with default True."""
step = WorkflowStep(name="test_step", action="do something")
assert hasattr(step, 'retain_full_context')
assert step.retain_full_context == True
def test_workflow_step_has_output_variable_field(self):
"""WorkflowStep should have output_variable field."""
step = WorkflowStep(
name="test_step",
action="do something",
output_variable="research_result"
)
assert hasattr(step, 'output_variable')
assert step.output_variable == "research_result"
def test_workflow_step_to_dict_includes_new_fields(self):
"""to_dict() should include context-related fields."""
step = WorkflowStep(
name="test_step",
action="do something",
context_from=["step1"],
retain_full_context=False,
output_variable="result"
)
d = step.to_dict()
assert "context_from" in d
assert "retain_full_context" in d
assert "output_variable" in d
class TestContextPassing:
"""Test context passing between workflow steps."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_step_receives_previous_step_output(self):
"""Step 2 should receive Step 1's output as context."""
manager = WorkflowManager()
# Create a simple workflow with 2 steps
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Generate data"),
WorkflowStep(name="step2", action="Process {{previous_output}}")
]
)
self._register_workflow(manager, workflow)
# Track what each step receives
received_actions = []
def mock_executor(action):
received_actions.append(action)
if "Generate" in action:
return "Generated data: ABC123"
return f"Processed: {action}"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
assert len(result["results"]) == 2
# Step 2 should have received the output from step 1
assert "ABC123" in received_actions[1] or "previous_output" in received_actions[1]
def test_context_accumulates_across_steps(self):
"""Each step should have access to all previous outputs when retain_full_context=True."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="First action", retain_full_context=True),
WorkflowStep(name="step2", action="Second action", retain_full_context=True),
WorkflowStep(name="step3", action="Third with context: {{step1_output}} and {{step2_output}}", retain_full_context=True)
]
)
self._register_workflow(manager, workflow)
outputs = ["Output1", "Output2", "Output3"]
call_count = [0]
def mock_executor(action):
idx = call_count[0]
call_count[0] += 1
return outputs[idx]
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
assert len(result["results"]) == 3
# All outputs should be stored
assert result["results"][0]["output"] == "Output1"
assert result["results"][1]["output"] == "Output2"
assert result["results"][2]["output"] == "Output3"
def test_context_from_specific_steps(self):
"""Step should only receive context from specified steps via context_from."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="research", action="Research topic"),
WorkflowStep(name="analysis", action="Analyze data"),
WorkflowStep(
name="summary",
action="Summarize based on: {{research_output}}",
context_from=["research"], # Only include research, not analysis
retain_full_context=False
)
]
)
self._register_workflow(manager, workflow)
received_actions = []
def mock_executor(action):
received_actions.append(action)
if "Research" in action:
return "Research findings: XYZ"
elif "Analyze" in action:
return "Analysis results: 123"
return "Summary complete"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
# The summary step should have research output but not analysis
# (This tests the context_from filtering)
def test_output_variable_substitution(self):
"""Output should be stored in specified variable name."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Generate report",
output_variable="report_data"
),
WorkflowStep(
name="step2",
action="Use report: {{report_data}}"
)
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
if "Generate" in action:
return "Report content here"
return f"Used: {action}"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
def test_retain_full_context_false_only_last_step(self):
"""When retain_full_context=False, only include last step's output."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="First"),
WorkflowStep(name="step2", action="Second"),
WorkflowStep(
name="step3",
action="Third with {{previous_output}}",
retain_full_context=False
)
]
)
self._register_workflow(manager, workflow)
received_actions = []
def mock_executor(action):
received_actions.append(action)
if "First" in action:
return "First output"
elif "Second" in action:
return "Second output"
return "Third output"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
# Step 3 should only have step 2's output, not step 1's
class TestContextVariableSubstitution:
"""Test variable substitution with step outputs."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_previous_output_variable(self):
"""{{previous_output}} should be replaced with last step's output."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Generate"),
WorkflowStep(name="step2", action="Process: {{previous_output}}")
]
)
self._register_workflow(manager, workflow)
received_actions = []
def mock_executor(action):
received_actions.append(action)
if "Generate" in action:
return "DATA_123"
return "Done"
result = manager.execute("test_workflow", executor=mock_executor)
# Check that previous_output was substituted
assert "DATA_123" in received_actions[1] or "previous_output" in received_actions[1]
def test_named_step_output_variable(self):
"""{{step_name_output}} should be replaced with that step's output."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="research", action="Research topic"),
WorkflowStep(name="write", action="Write about {{research_output}}")
]
)
self._register_workflow(manager, workflow)
received_actions = []
def mock_executor(action):
received_actions.append(action)
if "Research" in action:
return "Research findings"
return "Article written"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
class TestWorkflowExecuteResults:
"""Test that execute() returns proper results with context info."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_results_include_step_outputs(self):
"""Results should include output from each step."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return f"Output for {action}"
result = manager.execute("test_workflow", executor=mock_executor)
assert result["success"] == True
assert len(result["results"]) == 2
assert result["results"][0]["output"] is not None
assert result["results"][1]["output"] is not None
def test_results_include_accumulated_context(self):
"""Results should include accumulated context information."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return f"Output for {action}"
result = manager.execute("test_workflow", executor=mock_executor)
# Result should have context or variables with accumulated outputs
assert "results" in result
assert result["success"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,341 @@
"""
Test suite for Workflow Markdown Parser.
Tests the enhanced markdown parser for parsing agent config, tools,
context_from, and other new fields from workflow markdown files.
"""
import pytest
import tempfile
import os
import sys
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager
)
class TestParseAgentConfig:
"""Test parsing agent configuration from markdown."""
def test_parse_agent_config_block(self):
"""Should parse agent config from ```agent block."""
manager = WorkflowManager()
body = '''
## Step 1: Research
Research the topic.
```agent
role: Researcher
goal: Find comprehensive information
backstory: Expert researcher with 10 years experience
llm: gpt-4o
```
```action
Search for information about AI
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].agent_config is not None
assert steps[0].agent_config["role"] == "Researcher"
assert steps[0].agent_config["goal"] == "Find comprehensive information"
assert steps[0].agent_config["llm"] == "gpt-4o"
def test_parse_agent_config_helper(self):
"""_parse_agent_config should parse key-value pairs."""
manager = WorkflowManager()
agent_str = '''
role: Writer
goal: Write engaging content
verbose: true
max_iter: 10
'''
config = manager._parse_agent_config(agent_str)
assert config["role"] == "Writer"
assert config["goal"] == "Write engaging content"
assert config["verbose"] is True
assert config["max_iter"] == 10
class TestParseToolsBlock:
"""Test parsing tools from markdown."""
def test_parse_tools_block(self):
"""Should parse tools from ```tools block."""
manager = WorkflowManager()
body = '''
## Step 1: Search
Search for information.
```tools
tavily_search
web_browser
calculator
```
```action
Search for AI trends
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].tools is not None
assert len(steps[0].tools) == 3
assert "tavily_search" in steps[0].tools
assert "web_browser" in steps[0].tools
assert "calculator" in steps[0].tools
class TestParseContextFrom:
"""Test parsing context_from from markdown."""
def test_parse_context_from(self):
"""Should parse context_from directive."""
manager = WorkflowManager()
body = '''
## Step 1: Research
Research the topic.
```action
Research AI
```
## Step 2: Analyze
Analyze the research.
```action
Analyze data
```
## Step 3: Summary
Summarize findings.
context_from: [Research, Analyze]
```action
Write summary
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 3
assert steps[2].context_from is not None
assert "Research" in steps[2].context_from
assert "Analyze" in steps[2].context_from
class TestParseRetainFullContext:
"""Test parsing retain_full_context from markdown."""
def test_parse_retain_full_context_false(self):
"""Should parse retain_full_context: false."""
manager = WorkflowManager()
body = '''
## Step 1: Process
Process data.
retain_full_context: false
```action
Process the data
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].retain_full_context is False
def test_parse_retain_full_context_true(self):
"""Should parse retain_full_context: true."""
manager = WorkflowManager()
body = '''
## Step 1: Process
Process data.
retain_full_context: true
```action
Process the data
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].retain_full_context is True
def test_retain_full_context_default_true(self):
"""retain_full_context should default to True."""
manager = WorkflowManager()
body = '''
## Step 1: Process
Process data.
```action
Process the data
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].retain_full_context is True
class TestParseOutputVariable:
"""Test parsing output_variable from markdown."""
def test_parse_output_variable(self):
"""Should parse output_variable directive."""
manager = WorkflowManager()
body = '''
## Step 1: Generate
Generate report.
output_variable: report_data
```action
Generate the report
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 1
assert steps[0].output_variable == "report_data"
class TestLoadWorkflowWithNewFields:
"""Test loading workflow files with new fields."""
def test_load_workflow_with_default_llm(self):
"""Should load workflow with default_llm from frontmatter."""
manager = WorkflowManager()
content = '''---
name: Test Workflow
description: A test workflow
default_llm: gpt-4o
planning: true
planning_llm: gpt-4o-mini
---
## Step 1: Test
Test step.
```action
Do something
```
'''
# Create temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(content)
temp_path = f.name
try:
from pathlib import Path
workflow = manager._load_workflow(Path(temp_path))
assert workflow is not None
assert workflow.default_llm == "gpt-4o"
assert workflow.planning is True
assert workflow.planning_llm == "gpt-4o-mini"
finally:
os.unlink(temp_path)
class TestCompleteWorkflowParsing:
"""Test complete workflow parsing with all new features."""
def test_parse_complete_workflow(self):
"""Should parse a complete workflow with all new features."""
manager = WorkflowManager()
body = '''
## Step 1: Research
Research the topic thoroughly.
```agent
role: Researcher
goal: Find comprehensive information
backstory: Expert researcher
```
```tools
tavily_search
web_browser
```
output_variable: research_data
```action
Search for information about {{topic}}
```
## Step 2: Analyze
Analyze the research findings.
```agent
role: Analyst
goal: Analyze data patterns
```
context_from: [Research]
retain_full_context: false
```action
Analyze: {{research_data}}
```
## Step 3: Write
Write the final report.
```agent
role: Writer
goal: Write engaging content
```
```action
Write report based on analysis
```
'''
steps = manager._parse_steps(body)
assert len(steps) == 3
# Step 1
assert steps[0].name == "Research"
assert steps[0].agent_config is not None
assert steps[0].agent_config["role"] == "Researcher"
assert steps[0].tools is not None
assert "tavily_search" in steps[0].tools
assert steps[0].output_variable == "research_data"
# Step 2
assert steps[1].name == "Analyze"
assert steps[1].agent_config["role"] == "Analyst"
assert steps[1].context_from == ["Research"]
assert steps[1].retain_full_context is False
# Step 3
assert steps[2].name == "Write"
assert steps[2].agent_config["role"] == "Writer"
if __name__ == "__main__":
pytest.main([__file__, "-v"])
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,359 @@
"""
Test suite for Workflow Planning Mode Integration.
Tests the enhanced workflow system's ability to integrate with planning mode,
including workflow-level planning settings and planning LLM configuration.
"""
import pytest
from unittest.mock import Mock, patch
import sys
import os
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager
)
class TestWorkflowPlanningFields:
"""Test Workflow dataclass planning-related fields."""
def test_workflow_has_planning_field(self):
"""Workflow should have planning field."""
workflow = Workflow(
name="test_workflow",
planning=True
)
assert hasattr(workflow, 'planning')
assert workflow.planning is True
def test_workflow_planning_default_false(self):
"""Workflow planning should default to False."""
workflow = Workflow(name="test_workflow")
assert workflow.planning is False
def test_workflow_has_planning_llm_field(self):
"""Workflow should have planning_llm field."""
workflow = Workflow(
name="test_workflow",
planning_llm="gpt-4o"
)
assert hasattr(workflow, 'planning_llm')
assert workflow.planning_llm == "gpt-4o"
def test_workflow_planning_llm_default_none(self):
"""Workflow planning_llm should default to None."""
workflow = Workflow(name="test_workflow")
assert workflow.planning_llm is None
def test_workflow_to_dict_includes_planning_fields(self):
"""to_dict() should include planning-related fields."""
workflow = Workflow(
name="test_workflow",
planning=True,
planning_llm="gpt-4o"
)
d = workflow.to_dict()
assert "planning" in d
assert "planning_llm" in d
assert d["planning"] is True
assert d["planning_llm"] == "gpt-4o"
class TestWorkflowMemoryFields:
"""Test Workflow dataclass memory-related fields."""
def test_workflow_has_memory_config_field(self):
"""Workflow should have memory_config field."""
workflow = Workflow(
name="test_workflow",
memory_config={
"provider": "rag",
"use_embedding": True
}
)
assert hasattr(workflow, 'memory_config')
assert workflow.memory_config["provider"] == "rag"
def test_workflow_memory_config_default_none(self):
"""Workflow memory_config should default to None."""
workflow = Workflow(name="test_workflow")
assert workflow.memory_config is None
def test_workflow_to_dict_includes_memory_config(self):
"""to_dict() should include memory_config field."""
workflow = Workflow(
name="test_workflow",
memory_config={"provider": "sqlite"}
)
d = workflow.to_dict()
assert "memory_config" in d
class TestExecutePlanningParameter:
"""Test execute() planning parameter."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_execute_accepts_planning_parameter(self):
"""Execute should accept planning parameter."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return "Result"
# Should not raise error
result = manager.execute(
"test_workflow",
executor=mock_executor,
planning=True
)
assert result["success"]
def test_workflow_planning_used_when_not_overridden(self):
"""Workflow's planning setting should be used when not overridden."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
planning=True, # Workflow-level planning enabled
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return "Result"
# Execute without planning parameter - should use workflow's setting
result = manager.execute(
"test_workflow",
executor=mock_executor
)
assert result["success"]
class TestExecuteMemoryParameter:
"""Test execute() memory parameter."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_execute_accepts_memory_parameter(self):
"""Execute should accept memory parameter."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
mock_memory = Mock()
def mock_executor(action):
return "Result"
# Should not raise error
result = manager.execute(
"test_workflow",
executor=mock_executor,
memory=mock_memory
)
assert result["success"]
def test_memory_passed_to_step_agent(self):
"""Memory should be passed to created step agents."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Helper"}
)
]
)
self._register_workflow(manager, workflow)
mock_memory = Mock()
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent = Mock()
mock_agent.chat = Mock(return_value="Response")
MockAgent.return_value = mock_agent
manager.execute(
"test_workflow",
memory=mock_memory,
default_llm="gpt-4o-mini"
)
call_kwargs = MockAgent.call_args[1]
assert "memory" in call_kwargs
assert call_kwargs["memory"] == mock_memory
class TestExecuteStreamParameter:
"""Test execute() stream parameter."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_execute_accepts_stream_parameter(self):
"""Execute should accept stream parameter."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return "Result"
# Should not raise error
result = manager.execute(
"test_workflow",
executor=mock_executor,
stream=True
)
assert result["success"]
class TestExecuteVerboseParameter:
"""Test execute() verbose parameter."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_execute_accepts_verbose_parameter(self):
"""Execute should accept verbose parameter."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(name="step1", action="Action 1")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return "Result"
# Should not raise error
result = manager.execute(
"test_workflow",
executor=mock_executor,
verbose=2
)
assert result["success"]
def test_verbose_passed_to_step_agent(self):
"""Verbose level should be passed to created step agents."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
agent_config={"role": "Helper"}
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent = Mock()
mock_agent.chat = Mock(return_value="Response")
MockAgent.return_value = mock_agent
manager.execute(
"test_workflow",
verbose=3,
default_llm="gpt-4o-mini"
)
call_kwargs = MockAgent.call_args[1]
assert "verbose" in call_kwargs
assert call_kwargs["verbose"] == 3
class TestExecuteResultsWithVariables:
"""Test that execute() returns variables in results."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_results_include_variables(self):
"""Results should include accumulated variables."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
variables={"initial": "value"},
steps=[
WorkflowStep(name="step1", action="Action 1"),
WorkflowStep(name="step2", action="Action 2")
]
)
self._register_workflow(manager, workflow)
def mock_executor(action):
return f"Output for {action}"
result = manager.execute("test_workflow", executor=mock_executor)
assert "variables" in result
assert "initial" in result["variables"]
# Should also have step outputs as variables
assert "previous_output" in result["variables"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,306 @@
"""
Test suite for Workflow Per-Step Tools Support.
Tests the enhanced workflow system's ability to configure different tools
for each step, including tool execution and tool results in context.
"""
import pytest
from unittest.mock import Mock, patch
import sys
import os
# Add the package to path for testing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from praisonaiagents.memory.workflows import (
WorkflowStep,
Workflow,
WorkflowManager
)
class TestWorkflowStepToolsField:
"""Test WorkflowStep tools field."""
def test_workflow_step_tools_accepts_list(self):
"""WorkflowStep should accept a list of tools."""
mock_tool1 = Mock()
mock_tool2 = Mock()
step = WorkflowStep(
name="research",
action="Research topic",
tools=[mock_tool1, mock_tool2]
)
assert step.tools is not None
assert len(step.tools) == 2
assert mock_tool1 in step.tools
assert mock_tool2 in step.tools
def test_workflow_step_tools_accepts_callable(self):
"""WorkflowStep should accept callable tools."""
def search_tool(query):
return f"Results for {query}"
step = WorkflowStep(
name="search",
action="Search for info",
tools=[search_tool]
)
assert step.tools is not None
assert callable(step.tools[0])
def test_workflow_step_tools_in_to_dict(self):
"""to_dict() should include tools field."""
step = WorkflowStep(
name="test",
action="test action",
tools=["tool1", "tool2"]
)
d = step.to_dict()
assert "tools" in d
assert d["tools"] == ["tool1", "tool2"]
class TestToolsPassedToAgent:
"""Test that tools are passed to agent during step execution."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_step_tools_passed_to_agent(self):
"""Step's tools should be passed to created agent."""
manager = WorkflowManager()
mock_tool = Mock()
step = WorkflowStep(
name="research",
action="Research topic",
agent_config={"role": "Researcher"},
tools=[mock_tool]
)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
MockAgent.return_value = Mock()
manager._create_step_agent(step=step, default_llm="gpt-4o-mini")
call_kwargs = MockAgent.call_args[1]
assert "tools" in call_kwargs
assert mock_tool in call_kwargs["tools"]
def test_multiple_tools_passed_to_agent(self):
"""Multiple tools should all be passed to agent."""
manager = WorkflowManager()
tool1 = Mock(name="search")
tool2 = Mock(name="calculator")
tool3 = Mock(name="browser")
step = WorkflowStep(
name="research",
action="Research topic",
agent_config={"role": "Researcher"},
tools=[tool1, tool2, tool3]
)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
MockAgent.return_value = Mock()
manager._create_step_agent(step=step, default_llm="gpt-4o-mini")
call_kwargs = MockAgent.call_args[1]
assert len(call_kwargs["tools"]) == 3
class TestDifferentToolsPerStep:
"""Test different tools for different steps."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_different_steps_different_tools(self):
"""Different steps can have different tools."""
manager = WorkflowManager()
search_tool = Mock(name="search")
write_tool = Mock(name="write")
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="research",
action="Research AI",
agent_config={"role": "Researcher"},
tools=[search_tool]
),
WorkflowStep(
name="write",
action="Write article",
agent_config={"role": "Writer"},
tools=[write_tool]
)
]
)
self._register_workflow(manager, workflow)
created_agents = []
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
def track_agent(**kwargs):
agent = Mock()
agent.chat = Mock(return_value="Response")
agent.tools = kwargs.get("tools", [])
created_agents.append(kwargs)
return agent
MockAgent.side_effect = track_agent
manager.execute("test_workflow", default_llm="gpt-4o-mini")
# Verify different tools for each step
assert len(created_agents) == 2
assert search_tool in created_agents[0]["tools"]
assert write_tool in created_agents[1]["tools"]
def test_step_without_tools(self):
"""Steps without tools should work correctly."""
manager = WorkflowManager()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="think",
action="Think about topic",
agent_config={"role": "Thinker"}
# No tools
)
]
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
mock_agent = Mock()
mock_agent.chat = Mock(return_value="Thoughts")
MockAgent.return_value = mock_agent
result = manager.execute("test_workflow", default_llm="gpt-4o-mini")
assert result["success"]
# tools should not be in kwargs or should be None
call_kwargs = MockAgent.call_args[1]
assert call_kwargs.get("tools") is None or call_kwargs.get("tools") == []
class TestToolsWithExecutor:
"""Test tools behavior when using custom executor."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_executor_ignores_step_tools(self):
"""When executor is provided, step tools are not used for agent creation."""
manager = WorkflowManager()
mock_tool = Mock()
workflow = Workflow(
name="test_workflow",
steps=[
WorkflowStep(
name="step1",
action="Action 1",
tools=[mock_tool] # Tools defined but executor provided
)
]
)
self._register_workflow(manager, workflow)
executor_called = [False]
def custom_executor(action):
executor_called[0] = True
return "Executor result"
# Should not try to create agent since executor is provided
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
result = manager.execute("test_workflow", executor=custom_executor)
assert result["success"]
assert executor_called[0]
MockAgent.assert_not_called()
class TestToolsIntegration:
"""Integration tests for tools in workflow execution."""
@staticmethod
def _register_workflow(manager, workflow):
"""Helper to register workflow and mark as loaded."""
manager._workflows[workflow.name.lower()] = workflow
manager._loaded = True
def test_full_workflow_with_tools(self):
"""Full workflow execution with tools should work."""
manager = WorkflowManager()
# Create mock tools
search_tool = Mock(name="search")
analyze_tool = Mock(name="analyze")
workflow = Workflow(
name="research_workflow",
steps=[
WorkflowStep(
name="search",
action="Search for {{topic}}",
agent_config={"role": "Searcher"},
tools=[search_tool]
),
WorkflowStep(
name="analyze",
action="Analyze: {{previous_output}}",
agent_config={"role": "Analyzer"},
tools=[analyze_tool]
),
WorkflowStep(
name="summarize",
action="Summarize findings",
agent_config={"role": "Summarizer"}
# No tools for final step
)
],
variables={"topic": "AI trends"}
)
self._register_workflow(manager, workflow)
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
def create_mock_agent(**kwargs):
agent = Mock()
agent.chat = Mock(return_value=f"Response from {kwargs.get('role', 'agent')}")
return agent
MockAgent.side_effect = create_mock_agent
result = manager.execute("research_workflow")
assert result["success"]
assert len(result["results"]) == 3
assert MockAgent.call_count == 3
if __name__ == "__main__":
pytest.main([__file__, "-v"])
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.190"
version = "0.1.4"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+919 -47
View File
File diff suppressed because it is too large. Load diff
+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.11.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.11.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
View File
@@ -0,0 +1,19 @@
"""
PraisonAI CLI Package
This package provides the command-line interface for PraisonAI.
Structure:
- main.py: Main CLI entry point (PraisonAI class)
- features/: Feature handlers for CLI flags and commands
"""
from .main import PraisonAI
__all__ = ["PraisonAI"]
def main():
"""CLI entry point function."""
praison_ai = PraisonAI()
praison_ai.main()
@@ -0,0 +1,93 @@
"""
CLI Features Module for PraisonAI
This module contains modular CLI feature handlers that extend the base CLI functionality.
Each feature is implemented as a separate module for maintainability and testability.
Features:
- guardrail: Output validation using LLM guardrails
- metrics: Token usage and cost tracking
- image: Image processing with ImageAgent
- telemetry: Usage monitoring and analytics
- mcp: Model Context Protocol server integration
- fast_context: Codebase search capability
- knowledge: RAG/vector store management
- session: Session management for multi-turn conversations
- tools: Tool registry management
- handoff: Agent-to-agent delegation
- auto_memory: Automatic memory extraction
- todo: Todo list management
- router: Smart model selection
- flow_display: Visual workflow tracking
- workflow: YAML workflow management
"""
# Type hints available for IDE support
# Lazy imports to avoid performance impact
__all__ = [
'GuardrailHandler',
'MetricsHandler',
'ImageHandler',
'TelemetryHandler',
'MCPHandler',
'FastContextHandler',
'KnowledgeHandler',
'SessionHandler',
'ToolsHandler',
'HandoffHandler',
'AutoMemoryHandler',
'TodoHandler',
'RouterHandler',
'FlowDisplayHandler',
'WorkflowHandler',
]
def __getattr__(name):
"""Lazy load handlers to minimize import overhead."""
if name == 'GuardrailHandler':
from .guardrail import GuardrailHandler
return GuardrailHandler
elif name == 'MetricsHandler':
from .metrics import MetricsHandler
return MetricsHandler
elif name == 'ImageHandler':
from .image import ImageHandler
return ImageHandler
elif name == 'TelemetryHandler':
from .telemetry import TelemetryHandler
return TelemetryHandler
elif name == 'MCPHandler':
from .mcp import MCPHandler
return MCPHandler
elif name == 'FastContextHandler':
from .fast_context import FastContextHandler
return FastContextHandler
elif name == 'KnowledgeHandler':
from .knowledge import KnowledgeHandler
return KnowledgeHandler
elif name == 'SessionHandler':
from .session import SessionHandler
return SessionHandler
elif name == 'ToolsHandler':
from .tools import ToolsHandler
return ToolsHandler
elif name == 'HandoffHandler':
from .handoff import HandoffHandler
return HandoffHandler
elif name == 'AutoMemoryHandler':
from .auto_memory import AutoMemoryHandler
return AutoMemoryHandler
elif name == 'TodoHandler':
from .todo import TodoHandler
return TodoHandler
elif name == 'RouterHandler':
from .router import RouterHandler
return RouterHandler
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,173 @@
"""
Auto Memory Handler for CLI.
Provides automatic memory extraction and storage.
Usage: praisonai "Learn about user preferences" --auto-memory
"""
from typing import Any, Dict, Tuple
from .base import FlagHandler
class AutoMemoryHandler(FlagHandler):
"""
Handler for --auto-memory flag.
Automatically extracts and stores important information from conversations.
Example:
praisonai "Learn about user preferences" --auto-memory
praisonai "Remember this context" --auto-memory --user-id myuser
"""
@property
def feature_name(self) -> str:
return "auto_memory"
@property
def flag_name(self) -> str:
return "auto-memory"
@property
def flag_help(self) -> str:
return "Enable automatic memory extraction and storage"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if AutoMemory is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
return True, ""
return False, "praisonaiagents not installed"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def _get_auto_memory(self, user_id: str = None):
"""Get AutoMemory instance lazily."""
try:
from praisonaiagents.memory import AutoMemory
return AutoMemory(user_id=user_id or "default")
except ImportError:
self.print_status(
"AutoMemory requires praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply auto memory configuration.
Args:
config: Agent configuration dictionary
flag_value: Boolean or dict with user_id
Returns:
Modified configuration
"""
if flag_value:
config['auto_memory'] = True
if isinstance(flag_value, dict):
config['auto_memory_user_id'] = flag_value.get('user_id', 'default')
return config
def extract_memories(self, text: str, user_id: str = None) -> Dict[str, Any]:
"""
Extract memories from text.
Args:
text: Text to extract memories from
user_id: User ID for memory isolation
Returns:
Dictionary of extracted memories
"""
auto_memory = self._get_auto_memory(user_id)
if not auto_memory:
return {}
try:
if hasattr(auto_memory, 'extract'):
memories = auto_memory.extract(text)
self.print_status(f"🧠 Extracted {len(memories)} memories", "success")
return memories
elif hasattr(auto_memory, 'process'):
result = auto_memory.process(text)
return result
except Exception as e:
self.log(f"Memory extraction failed: {e}", "error")
return {}
def store_memory(self, content: str, user_id: str = None, importance: float = 0.5) -> bool:
"""
Store a memory.
Args:
content: Memory content
user_id: User ID for memory isolation
importance: Importance score (0-1)
Returns:
True if successful
"""
auto_memory = self._get_auto_memory(user_id)
if not auto_memory:
return False
try:
if hasattr(auto_memory, 'store'):
auto_memory.store(content, importance=importance)
elif hasattr(auto_memory, 'add'):
auto_memory.add(content, importance=importance)
self.print_status("✅ Memory stored", "success")
return True
except Exception as e:
self.log(f"Memory storage failed: {e}", "error")
return False
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process result to extract and store memories.
Args:
result: Agent output
flag_value: Boolean or dict with configuration
Returns:
Original result (memories are stored)
"""
if not flag_value:
return result
user_id = None
if isinstance(flag_value, dict):
user_id = flag_value.get('user_id')
# Extract memories from result
text = str(result)
memories = self.extract_memories(text, user_id)
if memories:
self.print_status("\n🧠 Auto-extracted Memories:", "info")
for key, value in memories.items():
self.print_status(f" • {key}: {value[:50]}...", "info")
return result
def execute(self, text: str = None, user_id: str = None, **kwargs) -> Dict[str, Any]:
"""
Execute auto memory extraction.
Args:
text: Text to process
user_id: User ID for memory isolation
Returns:
Dictionary of extracted memories
"""
if not text:
return {}
return self.extract_memories(text, user_id)
@@ -0,0 +1,168 @@
"""
Base handler class for CLI features.
All CLI feature handlers inherit from this base class to ensure
consistent interface and behavior.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List
import logging
logger = logging.getLogger(__name__)
class BaseHandler(ABC):
"""
Base class for all CLI feature handlers.
Provides common functionality and interface for feature handlers.
Each handler is responsible for a specific CLI feature.
"""
def __init__(self, verbose: bool = False):
"""
Initialize the handler.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self._initialized = False
@property
@abstractmethod
def feature_name(self) -> str:
"""Return the name of this feature."""
pass
@property
def is_available(self) -> bool:
"""Check if the feature dependencies are available."""
return True
def log(self, message: str, level: str = "info"):
"""Log a message if verbose mode is enabled."""
if self.verbose:
getattr(logger, level)(f"[{self.feature_name}] {message}")
def print_status(self, message: str, status: str = "info"):
"""Print a status message with color coding."""
from rich import print as rprint
colors = {
"info": "cyan",
"success": "green",
"warning": "yellow",
"error": "red"
}
color = colors.get(status, "white")
rprint(f"[{color}]{message}[/{color}]")
def check_dependencies(self) -> tuple[bool, str]:
"""
Check if required dependencies are installed.
Returns:
Tuple of (available, error_message)
"""
return True, ""
@abstractmethod
def execute(self, *args, **kwargs) -> Any:
"""
Execute the feature's main functionality.
This method must be implemented by each handler.
"""
pass
class CommandHandler(BaseHandler):
"""
Base class for CLI command handlers (e.g., 'praisonai knowledge').
Command handlers process subcommands like:
- praisonai <command> <action> [args]
"""
@abstractmethod
def get_actions(self) -> List[str]:
"""Return list of supported actions for this command."""
pass
def get_help_text(self) -> str:
"""Return help text for this command."""
return f"Available actions: {', '.join(self.get_actions())}"
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""
Execute a command action.
Args:
action: The action to perform
action_args: Arguments for the action
**kwargs: Additional keyword arguments
"""
if action == 'help' or action == '--help':
self.print_help()
return
method_name = f"action_{action.replace('-', '_')}"
if hasattr(self, method_name):
return getattr(self, method_name)(action_args, **kwargs)
else:
self.print_status(f"Unknown action: {action}", "error")
self.print_help()
def print_help(self):
"""Print help for this command."""
from rich import print as rprint
rprint(f"[bold]{self.feature_name.title()} Commands:[/bold]")
rprint(self.get_help_text())
class FlagHandler(BaseHandler):
"""
Base class for CLI flag handlers (e.g., '--guardrail').
Flag handlers modify agent behavior or add functionality
when a specific flag is provided.
"""
@property
@abstractmethod
def flag_name(self) -> str:
"""Return the flag name (without --)."""
pass
@property
def flag_help(self) -> str:
"""Return help text for this flag."""
return f"Enable {self.feature_name}"
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply this flag's configuration to an agent config.
Args:
config: The agent configuration dictionary
flag_value: The value of the flag
Returns:
Modified agent configuration
"""
return config
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process the agent result.
Args:
result: The agent's output
flag_value: The value of the flag
Returns:
Processed result
"""
return result
@@ -0,0 +1,189 @@
"""
Fast Context Handler for CLI.
Provides codebase search capability using FastContext.
Usage: praisonai "Find authentication code" --fast-context ./src
"""
import os
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
class FastContextHandler(FlagHandler):
"""
Handler for --fast-context flag.
Searches codebase for relevant context before agent execution.
Example:
praisonai "Find authentication code" --fast-context ./src
praisonai "Explain the database schema" --fast-context /path/to/project
"""
@property
def feature_name(self) -> str:
return "fast_context"
@property
def flag_name(self) -> str:
return "fast-context"
@property
def flag_help(self) -> str:
return "Path to search for relevant code context"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if FastContext is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
return True, ""
return False, "praisonaiagents not installed"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def validate_search_path(self, path: str) -> Tuple[bool, str]:
"""
Validate that the search path exists.
Args:
path: Path to search directory
Returns:
Tuple of (is_valid, error_message)
"""
if not path:
return False, "No search path provided"
if not os.path.isdir(path):
return False, f"Directory not found: {path}"
return True, ""
def search_context(self, query: str, path: str, **kwargs) -> List[Dict[str, Any]]:
"""
Search for relevant context in the codebase.
Args:
query: Search query
path: Path to search
**kwargs: Additional search options
Returns:
List of matching results
"""
available, msg = self.check_dependencies()
if not available:
self.print_status(msg, "error")
return []
valid, msg = self.validate_search_path(path)
if not valid:
self.print_status(msg, "error")
return []
try:
from praisonaiagents import FastContext
fc = FastContext(
path=path,
model=kwargs.get('model', 'gpt-4o-mini'),
max_turns=kwargs.get('max_turns', 4),
parallelism=kwargs.get('parallelism', 8),
timeout=kwargs.get('timeout', 30.0)
)
result = fc.search(query)
# Convert to list of dicts
matches = []
if hasattr(result, 'files'):
for file_match in result.files:
matches.append({
'file': file_match.path,
'lines': [(lr.start, lr.end) for lr in file_match.line_ranges],
'relevance': getattr(file_match, 'relevance', 1.0)
})
self.print_status(f"🔍 Found {len(matches)} relevant files", "success")
return matches
except Exception as e:
self.log(f"FastContext search error: {e}", "error")
return []
def format_context_for_prompt(self, matches: List[Dict[str, Any]], max_chars: int = 10000) -> str:
"""
Format search results as context for the prompt.
Args:
matches: List of search results
max_chars: Maximum characters to include
Returns:
Formatted context string
"""
if not matches:
return ""
context_parts = ["## Relevant Code Context\n"]
total_chars = 0
for match in matches:
file_path = match.get('file', '')
lines = match.get('lines', [])
if not file_path or not os.path.exists(file_path):
continue
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.readlines()
for start, end in lines:
snippet = ''.join(content[max(0, start-1):end])
if total_chars + len(snippet) > max_chars:
break
context_parts.append(f"\n### {file_path} (lines {start}-{end})\n```\n{snippet}```\n")
total_chars += len(snippet)
except Exception:
continue
return '\n'.join(context_parts)
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply fast context configuration.
Args:
config: Agent configuration dictionary
flag_value: Search path
Returns:
Modified configuration
"""
if flag_value:
config['fast_context'] = True
config['fast_context_path'] = flag_value
return config
def execute(self, query: str = None, path: str = None, **kwargs) -> str:
"""
Execute fast context search and return formatted context.
Args:
query: Search query
path: Search path
Returns:
Formatted context string
"""
if not query or not path:
return ""
matches = self.search_context(query, path, **kwargs)
return self.format_context_for_prompt(matches)
@@ -0,0 +1,189 @@
"""
Flow Display Handler for CLI.
Provides visual workflow tracking.
Usage: praisonai agents.yaml --flow-display
"""
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
class FlowDisplayHandler(FlagHandler):
"""
Handler for --flow-display flag.
Enables visual workflow tracking for multi-agent executions.
Example:
praisonai agents.yaml --flow-display
praisonai "Complex task" --flow-display
"""
@property
def feature_name(self) -> str:
return "flow_display"
@property
def flag_name(self) -> str:
return "flow-display"
@property
def flag_help(self) -> str:
return "Enable visual workflow tracking"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if FlowDisplay is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
return True, ""
return False, "praisonaiagents not installed"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def _get_flow_display(self):
"""Get FlowDisplay class lazily."""
try:
from praisonaiagents import FlowDisplay
return FlowDisplay
except ImportError:
return None
def create_flow_display(self, title: str = "Workflow") -> Any:
"""
Create a FlowDisplay instance.
Args:
title: Title for the workflow display
Returns:
FlowDisplay instance or None
"""
FlowDisplay = self._get_flow_display()
if not FlowDisplay:
self.print_status(
"FlowDisplay requires praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
try:
return FlowDisplay(title=title)
except Exception as e:
self.log(f"Failed to create FlowDisplay: {e}", "error")
return None
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply flow display configuration.
Args:
config: Agent configuration dictionary
flag_value: Boolean indicating flow display
Returns:
Modified configuration
"""
if flag_value:
config['flow_display'] = True
return config
def display_workflow_start(self, workflow_name: str, agents: List[str] = None):
"""
Display workflow start.
Args:
workflow_name: Name of the workflow
agents: List of agent names
"""
self.print_status(f"\n🚀 Starting Workflow: {workflow_name}", "info")
self.print_status("=" * 50, "info")
if agents:
self.print_status("Agents:", "info")
for i, agent in enumerate(agents, 1):
self.print_status(f" {i}. {agent}", "info")
self.print_status("=" * 50, "info")
def display_step(self, step_num: int, step_name: str, status: str = "running"):
"""
Display a workflow step.
Args:
step_num: Step number
step_name: Name of the step
status: Step status (running, completed, failed)
"""
icons = {
'running': '⏳',
'completed': '✅',
'failed': '❌',
'pending': '⬜'
}
icon = icons.get(status, '•')
self.print_status(f" {icon} Step {step_num}: {step_name}", "info")
def display_workflow_end(self, success: bool = True, duration: float = None):
"""
Display workflow end.
Args:
success: Whether workflow completed successfully
duration: Workflow duration in seconds
"""
self.print_status("=" * 50, "info")
if success:
msg = "✅ Workflow completed successfully"
else:
msg = "❌ Workflow failed"
if duration:
msg += f" ({duration:.2f}s)"
self.print_status(msg, "success" if success else "error")
def track_execution(self, func):
"""
Decorator to track function execution in flow display.
Args:
func: Function to track
Returns:
Wrapped function
"""
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
func_name = func.__name__
self.display_step(1, func_name, "running")
try:
result = func(*args, **kwargs)
self.display_step(1, func_name, "completed")
return result
except Exception:
self.display_step(1, func_name, "failed")
raise
return wrapper
def execute(self, workflow_name: str = None, **kwargs) -> Any:
"""
Execute flow display setup.
Args:
workflow_name: Name of the workflow
Returns:
FlowDisplay instance or None
"""
if workflow_name:
self.display_workflow_start(workflow_name)
return self.create_flow_display(workflow_name or "Workflow")
@@ -0,0 +1,165 @@
"""
Guardrail Handler for CLI.
Provides output validation using LLM guardrails.
Usage: praisonai "prompt" --guardrail "Ensure output is valid JSON"
"""
from typing import Any, Dict, Tuple
from .base import FlagHandler
# Lazy import to avoid performance impact
PRAISONAI_AVAILABLE = False
LLMGuardrail = None
def _check_praisonai():
"""Check if praisonaiagents is available."""
global PRAISONAI_AVAILABLE, LLMGuardrail
try:
from praisonaiagents import LLMGuardrail as _LLMGuardrail
LLMGuardrail = _LLMGuardrail
PRAISONAI_AVAILABLE = True
except ImportError:
PRAISONAI_AVAILABLE = False
return PRAISONAI_AVAILABLE
class GuardrailHandler(FlagHandler):
"""
Handler for --guardrail flag.
Validates agent output using LLM-based guardrails.
Example:
praisonai "Write code" --guardrail "Ensure code is secure"
"""
@property
def feature_name(self) -> str:
return "guardrail"
@property
def flag_name(self) -> str:
return "guardrail"
@property
def flag_help(self) -> str:
return "Validate output with LLM guardrail (provide description string)"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if praisonaiagents is available."""
if _check_praisonai():
return True, ""
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply guardrail configuration to agent config.
Args:
config: Agent configuration dictionary
flag_value: Guardrail description string
Returns:
Modified configuration with guardrail settings
"""
if flag_value:
config["guardrail_description"] = flag_value
# Store for later use in post_process_result
self._guardrail_description = flag_value
return config
def create_guardrail(self, description: str, llm: Any = None):
"""
Create an LLMGuardrail instance.
Args:
description: Natural language description of validation criteria
llm: Optional LLM instance for validation
Returns:
LLMGuardrail instance or None if unavailable
"""
if not _check_praisonai():
self.print_status(
"Guardrail requires praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
return LLMGuardrail(description=description, llm=llm)
def validate_output(self, output: str, description: str, llm: Any = None) -> Tuple[bool, str]:
"""
Validate output against guardrail criteria.
Args:
output: The output to validate
description: Validation criteria description
llm: Optional LLM for validation
Returns:
Tuple of (is_valid, message)
"""
guardrail = self.create_guardrail(description, llm)
if not guardrail:
return True, "Guardrail unavailable, skipping validation"
try:
# Create a mock TaskOutput for the guardrail
from praisonaiagents import TaskOutput
task_output = TaskOutput(
description="CLI output",
summary="CLI output",
raw=output,
agent="CLI",
output_format="RAW"
)
success, result = guardrail(task_output)
if success:
return True, "Guardrail validation passed"
else:
return False, f"Guardrail validation failed: {result}"
except Exception as e:
self.log(f"Guardrail error: {e}", "warning")
return True, f"Guardrail error (skipped): {e}"
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process result with guardrail validation.
Args:
result: Agent output
flag_value: Guardrail description
Returns:
Original result (validation is logged)
"""
if not flag_value:
return result
is_valid, message = self.validate_output(str(result), flag_value)
if is_valid:
self.print_status(f"✅ {message}", "success")
else:
self.print_status(f"⚠️ {message}", "warning")
return result
def execute(self, output: str = None, guardrail_description: str = None, **kwargs) -> Tuple[bool, str]:
"""
Execute guardrail validation.
Args:
output: Output to validate
guardrail_description: Validation criteria
Returns:
Tuple of (is_valid, message)
"""
if not output or not guardrail_description:
return True, "No output or description provided"
return self.validate_output(output, guardrail_description)
@@ -0,0 +1,166 @@
"""
Handoff Handler for CLI.
Provides agent-to-agent delegation capability.
Usage: praisonai "Complex task" --handoff "researcher,writer,editor"
"""
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
class HandoffHandler(FlagHandler):
"""
Handler for --handoff flag.
Enables agent-to-agent delegation for complex tasks.
Example:
praisonai "Research and write article" --handoff "researcher,writer"
praisonai "Analyze and visualize data" --handoff "analyst,visualizer"
"""
@property
def feature_name(self) -> str:
return "handoff"
@property
def flag_name(self) -> str:
return "handoff"
@property
def flag_help(self) -> str:
return "Comma-separated list of agent roles for task delegation"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if Handoff is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
return True, ""
return False, "praisonaiagents not installed"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def parse_agent_names(self, names_str: str) -> List[str]:
"""
Parse comma-separated agent names.
Args:
names_str: Comma-separated string of agent names/roles
Returns:
List of agent names
"""
if not names_str:
return []
return [name.strip() for name in names_str.split(',') if name.strip()]
def create_agents_with_handoff(self, agent_names: List[str], llm: str = None) -> List[Any]:
"""
Create agents with handoff capability.
Args:
agent_names: List of agent role names
llm: Optional LLM model name
Returns:
List of Agent instances with handoff configured
"""
available, msg = self.check_dependencies()
if not available:
self.print_status(msg, "error")
return []
from praisonaiagents import Agent, Handoff
agents = []
# Create agents for each role
for i, name in enumerate(agent_names):
agent_config = {
"name": name.title().replace("_", " "),
"role": name.title().replace("_", " "),
"goal": f"Complete tasks as {name}",
"backstory": f"Expert {name} with specialized skills"
}
if llm:
agent_config["llm"] = llm
agent = Agent(**agent_config)
agents.append(agent)
# Set up handoffs between agents
for i, agent in enumerate(agents[:-1]):
next_agent = agents[i + 1]
handoff = Handoff(
target=next_agent,
name=f"handoff_to_{agent_names[i + 1]}",
description=f"Hand off task to {agent_names[i + 1]} for further processing"
)
# Add handoff to agent
if not hasattr(agent, 'handoffs') or agent.handoffs is None:
agent.handoffs = []
agent.handoffs.append(handoff)
self.print_status(f"🤝 Created {len(agents)} agents with handoff chain", "success")
for i, name in enumerate(agent_names):
arrow = " → " if i < len(agent_names) - 1 else ""
self.print_status(f" {name}{arrow}", "info")
return agents
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply handoff configuration.
Args:
config: Agent configuration dictionary
flag_value: Comma-separated agent names
Returns:
Modified configuration
"""
if flag_value:
agent_names = self.parse_agent_names(flag_value)
config['handoff_agents'] = agent_names
config['use_handoff'] = True
return config
def execute(self, prompt: str = None, agent_names: str = None, llm: str = None, **kwargs) -> Any:
"""
Execute task with handoff agents.
Args:
prompt: Task prompt
agent_names: Comma-separated agent names
llm: Optional LLM model
Returns:
Task result
"""
if not agent_names:
self.print_status("No agent names provided for handoff", "error")
return None
names = self.parse_agent_names(agent_names)
if len(names) < 2:
self.print_status("Handoff requires at least 2 agents", "error")
return None
agents = self.create_agents_with_handoff(names, llm)
if not agents:
return None
if prompt:
# Execute with first agent
try:
result = agents[0].start(prompt)
return result
except Exception as e:
self.print_status(f"Handoff execution failed: {e}", "error")
return None
return agents
@@ -0,0 +1,207 @@
"""
Image Handler for CLI.
Provides image processing capabilities using ImageAgent.
Usage: praisonai "Describe this image" --image path/to/image.png
"""
import os
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
# Supported image extensions
SUPPORTED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'}
class ImageHandler(FlagHandler):
"""
Handler for --image flag.
Processes images using ImageAgent for vision-based tasks.
Example:
praisonai "Describe this image" --image photo.png
praisonai "What's in this image?" --image /path/to/image.jpg
"""
@property
def feature_name(self) -> str:
return "image"
@property
def flag_name(self) -> str:
return "image"
@property
def flag_help(self) -> str:
return "Path to image file for vision-based tasks"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if ImageAgent is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
return True, ""
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def validate_image_path(self, path: str) -> Tuple[bool, str]:
"""
Validate that the image path is valid.
Args:
path: Path to the image file
Returns:
Tuple of (is_valid, error_message)
"""
if not path:
return False, "No image path provided"
# Check extension
ext = os.path.splitext(path)[1].lower()
if ext not in SUPPORTED_EXTENSIONS:
return False, f"Unsupported image format: {ext}. Supported: {', '.join(SUPPORTED_EXTENSIONS)}"
# Check if file exists (for local files)
if not path.startswith(('http://', 'https://')):
if not os.path.exists(path):
return False, f"Image file not found: {path}"
return True, ""
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply image configuration to agent config.
Args:
config: Agent configuration dictionary
flag_value: Image path or list of paths
Returns:
Modified configuration with image settings
"""
if flag_value:
# Store image path(s) for use with ImageAgent
if isinstance(flag_value, str):
config["images"] = [flag_value]
elif isinstance(flag_value, list):
config["images"] = flag_value
config["use_image_agent"] = True
return config
def create_image_agent(self, llm: str = None, **kwargs) -> Any:
"""
Create an ImageAgent instance.
Args:
llm: Optional LLM model name
**kwargs: Additional agent configuration
Returns:
ImageAgent instance or None if unavailable
"""
available, msg = self.check_dependencies()
if not available:
self.print_status(msg, "error")
return None
from praisonaiagents import ImageAgent
agent_config = {
"name": "ImageAnalyzer",
"role": "Image Analyst",
"goal": "Analyze and describe images accurately",
"backstory": "Expert at understanding visual content"
}
if llm:
agent_config["llm"] = llm
agent_config.update(kwargs)
return ImageAgent(**agent_config)
def process_image(self, prompt: str, image_path: str, llm: str = None) -> str:
"""
Process an image with the given prompt.
Args:
prompt: The prompt/question about the image
image_path: Path to the image file
llm: Optional LLM model name
Returns:
Agent's response about the image
"""
# Validate image path
valid, msg = self.validate_image_path(image_path)
if not valid:
return f"Error: {msg}"
agent = self.create_image_agent(llm=llm)
if not agent:
return "Error: Could not create ImageAgent"
try:
# ImageAgent expects images as a list
result = agent.chat(prompt, images=[image_path])
return result
except Exception as e:
self.log(f"Image processing error: {e}", "error")
return f"Error processing image: {e}"
def process_multiple_images(self, prompt: str, image_paths: List[str], llm: str = None) -> str:
"""
Process multiple images with the given prompt.
Args:
prompt: The prompt/question about the images
image_paths: List of paths to image files
llm: Optional LLM model name
Returns:
Agent's response about the images
"""
# Validate all image paths
for path in image_paths:
valid, msg = self.validate_image_path(path)
if not valid:
return f"Error: {msg}"
agent = self.create_image_agent(llm=llm)
if not agent:
return "Error: Could not create ImageAgent"
try:
result = agent.chat(prompt, images=image_paths)
return result
except Exception as e:
self.log(f"Image processing error: {e}", "error")
return f"Error processing images: {e}"
def execute(self, prompt: str = None, image_path: str = None, llm: str = None, **kwargs) -> str:
"""
Execute image processing.
Args:
prompt: The prompt/question about the image
image_path: Path to the image file (or comma-separated paths)
llm: Optional LLM model name
Returns:
Agent's response
"""
if not prompt:
return "Error: No prompt provided"
if not image_path:
return "Error: No image path provided"
# Handle multiple images (comma-separated)
if ',' in image_path:
paths = [p.strip() for p in image_path.split(',')]
return self.process_multiple_images(prompt, paths, llm)
return self.process_image(prompt, image_path, llm)
@@ -0,0 +1,236 @@
"""
Knowledge Handler for CLI.
Provides RAG/vector store management.
Usage: praisonai knowledge add document.pdf
praisonai knowledge search "query"
"""
import os
from typing import Any, Dict, List
from .base import CommandHandler
class KnowledgeHandler(CommandHandler):
"""
Handler for knowledge command.
Manages knowledge base for RAG (Retrieval Augmented Generation).
Example:
praisonai knowledge add document.pdf
praisonai knowledge add ./docs/
praisonai knowledge search "How to authenticate?"
praisonai knowledge list
praisonai knowledge clear
"""
def __init__(self, verbose: bool = False, workspace: str = None):
super().__init__(verbose)
self.workspace = workspace or os.getcwd()
self._knowledge = None
@property
def feature_name(self) -> str:
return "knowledge"
def get_actions(self) -> List[str]:
return ["add", "search", "list", "clear", "info", "help"]
def get_help_text(self) -> str:
return """
Knowledge Commands:
praisonai knowledge add <file|dir> - Add document(s) to knowledge base
praisonai knowledge search <query> - Search knowledge base
praisonai knowledge list - List indexed documents
praisonai knowledge clear - Clear knowledge base
praisonai knowledge info - Show knowledge base info
Options:
--workspace <path> - Workspace directory (default: current)
"""
def _get_knowledge(self):
"""Lazy load Knowledge instance."""
if self._knowledge is None:
try:
from praisonaiagents import Knowledge
self._knowledge = Knowledge()
except ImportError:
self.print_status(
"Knowledge requires praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
return self._knowledge
def action_add(self, args: List[str], **kwargs) -> bool:
"""
Add document(s) to knowledge base.
Args:
args: List containing file/directory paths
Returns:
True if successful
"""
if not args:
self.print_status("Usage: praisonai knowledge add <file|directory>", "error")
return False
knowledge = self._get_knowledge()
if not knowledge:
return False
path = args[0]
# Expand path
if not os.path.isabs(path):
path = os.path.join(self.workspace, path)
if not os.path.exists(path):
self.print_status(f"Path not found: {path}", "error")
return False
try:
if os.path.isdir(path):
# Add all files in directory
count = 0
for root, _, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
knowledge.add(file_path)
count += 1
self.print_status(f"✅ Added {count} files from {path}", "success")
else:
knowledge.add(path)
self.print_status(f"✅ Added: {path}", "success")
return True
except Exception as e:
self.print_status(f"Failed to add: {e}", "error")
return False
def action_search(self, args: List[str], **kwargs) -> List[Dict[str, Any]]:
"""
Search knowledge base.
Args:
args: List containing search query
Returns:
List of search results
"""
if not args:
self.print_status("Usage: praisonai knowledge search <query>", "error")
return []
knowledge = self._get_knowledge()
if not knowledge:
return []
query = ' '.join(args)
limit = kwargs.get('limit', 5)
try:
results = knowledge.search(query, k=limit)
if results:
self.print_status(f"\n🔍 Search results for '{query}':\n", "info")
for i, result in enumerate(results, 1):
content = str(result)[:200]
self.print_status(f" {i}. {content}...", "info")
else:
self.print_status("No results found", "warning")
return results
except Exception as e:
self.print_status(f"Search failed: {e}", "error")
return []
def action_list(self, args: List[str], **kwargs) -> List[str]:
"""
List indexed documents.
Returns:
List of document names
"""
knowledge = self._get_knowledge()
if not knowledge:
return []
try:
if hasattr(knowledge, 'list_documents'):
docs = knowledge.list_documents()
elif hasattr(knowledge, 'documents'):
docs = list(knowledge.documents.keys()) if knowledge.documents else []
else:
docs = []
if docs:
self.print_status("\n📚 Indexed Documents:", "info")
for doc in docs:
self.print_status(f" - {doc}", "info")
else:
self.print_status("No documents indexed", "warning")
return docs
except Exception as e:
self.print_status(f"Failed to list documents: {e}", "error")
return []
def action_clear(self, args: List[str], **kwargs) -> bool:
"""
Clear knowledge base.
Returns:
True if successful
"""
knowledge = self._get_knowledge()
if not knowledge:
return False
try:
if hasattr(knowledge, 'clear'):
knowledge.clear()
elif hasattr(knowledge, 'reset'):
knowledge.reset()
self.print_status("✅ Knowledge base cleared", "success")
return True
except Exception as e:
self.print_status(f"Failed to clear: {e}", "error")
return False
def action_info(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Show knowledge base info.
Returns:
Dictionary of info
"""
knowledge = self._get_knowledge()
if not knowledge:
return {}
info = {
"workspace": self.workspace,
"type": type(knowledge).__name__
}
try:
if hasattr(knowledge, 'get_stats'):
info.update(knowledge.get_stats())
if hasattr(knowledge, 'count'):
info['document_count'] = knowledge.count()
except Exception:
pass
self.print_status("\n📊 Knowledge Base Info:", "info")
for key, value in info.items():
self.print_status(f" {key}: {value}", "info")
return info
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute knowledge command action."""
return super().execute(action, action_args, **kwargs)
+169
View File
@@ -0,0 +1,169 @@
"""
MCP Handler for CLI.
Provides Model Context Protocol server integration.
Usage: praisonai "prompt" --mcp "npx -y @modelcontextprotocol/server-filesystem ."
"""
import shlex
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
class MCPHandler(FlagHandler):
"""
Handler for --mcp flag.
Integrates MCP (Model Context Protocol) servers as tools for agents.
Example:
praisonai "List files" --mcp "npx -y @modelcontextprotocol/server-filesystem ."
praisonai "Search web" --mcp "npx -y @modelcontextprotocol/server-brave-search" --mcp-env "BRAVE_API_KEY=xxx"
"""
@property
def feature_name(self) -> str:
return "mcp"
@property
def flag_name(self) -> str:
return "mcp"
@property
def flag_help(self) -> str:
return "MCP server command (e.g., 'npx -y @modelcontextprotocol/server-filesystem .')"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if MCP is available."""
try:
import importlib.util
if importlib.util.find_spec("praisonaiagents") is not None:
# Check if MCP module is available
from praisonaiagents import MCP
if MCP is not None:
return True, ""
return False, "MCP requires praisonaiagents with MCP support"
except ImportError:
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
def parse_mcp_command(self, command: str, env_vars: str = None) -> Tuple[str, List[str], Dict[str, str]]:
"""
Parse MCP command string into command, args, and environment.
Args:
command: Full MCP command string
env_vars: Optional comma-separated environment variables (KEY=value,KEY2=value2)
Returns:
Tuple of (command, args, env_dict)
"""
# Parse command using shell-like splitting
parts = shlex.split(command)
if not parts:
return "", [], {}
cmd = parts[0]
args = parts[1:] if len(parts) > 1 else []
# Parse environment variables
env = {}
if env_vars:
for pair in env_vars.split(','):
if '=' in pair:
key, value = pair.split('=', 1)
env[key.strip()] = value.strip()
return cmd, args, env
def create_mcp_tools(self, command: str, env_vars: str = None, timeout: int = 30) -> Any:
"""
Create MCP tools from command.
Args:
command: MCP server command
env_vars: Optional environment variables
timeout: Connection timeout in seconds
Returns:
MCP instance or None if unavailable
"""
available, msg = self.check_dependencies()
if not available:
self.print_status(msg, "error")
return None
from praisonaiagents import MCP
cmd, args, env = self.parse_mcp_command(command, env_vars)
if not cmd:
self.print_status("Invalid MCP command", "error")
return None
try:
mcp = MCP(
command=cmd,
args=args,
env=env if env else None,
timeout=timeout
)
self.print_status(f"🔌 MCP server connected: {cmd}", "success")
return mcp
except Exception as e:
self.print_status(f"Failed to connect to MCP server: {e}", "error")
return None
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply MCP configuration to agent config.
Args:
config: Agent configuration dictionary
flag_value: MCP command string or dict with command and env
Returns:
Modified configuration with MCP tools
"""
if not flag_value:
return config
# Handle both string and dict formats
if isinstance(flag_value, str):
mcp = self.create_mcp_tools(flag_value)
elif isinstance(flag_value, dict):
mcp = self.create_mcp_tools(
flag_value.get('command', ''),
flag_value.get('env', ''),
flag_value.get('timeout', 30)
)
else:
return config
if mcp:
# Add MCP tools to existing tools
existing_tools = config.get('tools', [])
if isinstance(existing_tools, list):
existing_tools.extend(list(mcp))
else:
existing_tools = list(mcp)
config['tools'] = existing_tools
config['_mcp_instance'] = mcp # Store for cleanup
return config
def execute(self, command: str = None, env_vars: str = None, **kwargs) -> Any:
"""
Execute MCP setup.
Args:
command: MCP server command
env_vars: Environment variables
Returns:
MCP instance or None
"""
if not command:
self.print_status("No MCP command provided", "error")
return None
return self.create_mcp_tools(command, env_vars)
@@ -0,0 +1,167 @@
"""
Metrics Handler for CLI.
Provides token usage and cost tracking for agent executions.
Usage: praisonai "prompt" --metrics
"""
from typing import Any, Dict, Tuple
from .base import FlagHandler
class MetricsHandler(FlagHandler):
"""
Handler for --metrics flag.
Tracks and displays token usage and cost metrics after agent execution.
Example:
praisonai "Analyze data" --metrics
"""
@property
def feature_name(self) -> str:
return "metrics"
@property
def flag_name(self) -> str:
return "metrics"
@property
def flag_help(self) -> str:
return "Display token usage and cost metrics after execution"
def check_dependencies(self) -> Tuple[bool, str]:
"""Metrics are built into praisonaiagents, always available."""
return True, ""
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply metrics configuration to agent config.
Args:
config: Agent configuration dictionary
flag_value: Boolean indicating if metrics should be enabled
Returns:
Modified configuration with metrics enabled
"""
if flag_value:
config["metrics"] = True
return config
def format_metrics(self, metrics: Dict[str, Any]) -> str:
"""
Format metrics dictionary into human-readable string.
Args:
metrics: Dictionary containing token and cost metrics
Returns:
Formatted string for display
"""
lines = []
lines.append("\n📊 Execution Metrics:")
lines.append("-" * 40)
# Token metrics
if "prompt_tokens" in metrics:
lines.append(f" Prompt tokens: {metrics['prompt_tokens']:,}")
if "completion_tokens" in metrics:
lines.append(f" Completion tokens: {metrics['completion_tokens']:,}")
if "total_tokens" in metrics:
lines.append(f" Total tokens: {metrics['total_tokens']:,}")
# Cost metrics
if "cost" in metrics:
cost = metrics["cost"]
if isinstance(cost, (int, float)):
lines.append(f" Estimated cost: ${cost:.6f}")
# Timing metrics
if "duration" in metrics:
lines.append(f" Duration: {metrics['duration']:.2f}s")
if "tokens_per_second" in metrics:
lines.append(f" Tokens/second: {metrics['tokens_per_second']:.1f}")
# Model info
if "model" in metrics:
lines.append(f" Model: {metrics['model']}")
lines.append("-" * 40)
return "\n".join(lines)
def extract_metrics_from_agent(self, agent: Any) -> Dict[str, Any]:
"""
Extract metrics from an agent instance.
Args:
agent: Agent instance that may have metrics
Returns:
Dictionary of metrics
"""
metrics = {}
# Try to get metrics from various possible locations
if hasattr(agent, 'last_token_metrics'):
metrics.update(agent.last_token_metrics or {})
if hasattr(agent, 'llm') and agent.llm:
llm = agent.llm
if hasattr(llm, 'last_token_metrics'):
metrics.update(llm.last_token_metrics or {})
if hasattr(llm, 'model'):
metrics['model'] = llm.model
if hasattr(agent, 'metrics') and isinstance(agent.metrics, dict):
metrics.update(agent.metrics or {})
return metrics
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process result to display metrics.
Args:
result: Agent output (may contain metrics)
flag_value: Boolean indicating if metrics should be displayed
Returns:
Original result (metrics are printed)
"""
if not flag_value:
return result
# Try to extract metrics from result if it's a TaskOutput
metrics = {}
if hasattr(result, 'token_metrics'):
metrics = result.token_metrics or {}
elif isinstance(result, dict) and 'metrics' in result:
metrics = result['metrics']
if metrics:
formatted = self.format_metrics(metrics)
self.print_status(formatted, "info")
else:
self.print_status("📊 No metrics available for this execution", "warning")
return result
def execute(self, agent: Any = None, **kwargs) -> Dict[str, Any]:
"""
Execute metrics extraction and display.
Args:
agent: Agent instance to extract metrics from
Returns:
Dictionary of metrics
"""
if agent:
metrics = self.extract_metrics_from_agent(agent)
if metrics:
formatted = self.format_metrics(metrics)
self.print_status(formatted, "info")
return metrics
return {}
@@ -0,0 +1,160 @@
"""
Router Handler for CLI.
Provides smart model selection based on task complexity.
Usage: praisonai "Complex task" --router
"""
from typing import Any, Dict, Tuple
from .base import FlagHandler
class RouterHandler(FlagHandler):
"""
Handler for --router flag.
Automatically selects the best model based on task complexity.
Example:
praisonai "Simple question" --router
praisonai "Complex analysis" --router
"""
# Model tiers for routing
MODEL_TIERS = {
'simple': ['gpt-4o-mini', 'claude-3-haiku', 'gemini-1.5-flash'],
'medium': ['gpt-4o', 'claude-3-sonnet', 'gemini-1.5-pro'],
'complex': ['gpt-4-turbo', 'claude-3-opus', 'o1-preview'],
}
@property
def feature_name(self) -> str:
return "router"
@property
def flag_name(self) -> str:
return "router"
@property
def flag_help(self) -> str:
return "Auto-select best model based on task complexity"
def check_dependencies(self) -> Tuple[bool, str]:
"""Router is built-in, always available."""
return True, ""
def analyze_complexity(self, prompt: str) -> str:
"""
Analyze prompt complexity.
Args:
prompt: The task prompt
Returns:
Complexity level: 'simple', 'medium', or 'complex'
"""
# Simple heuristics for complexity
prompt_lower = prompt.lower()
word_count = len(prompt.split())
# Complex indicators
complex_keywords = [
'analyze', 'research', 'comprehensive', 'detailed',
'compare', 'evaluate', 'synthesize', 'multi-step',
'code review', 'architecture', 'design pattern',
'optimize', 'debug', 'refactor'
]
# Simple indicators
simple_keywords = [
'what is', 'define', 'list', 'name', 'when',
'where', 'who', 'simple', 'quick', 'brief'
]
complex_count = sum(1 for kw in complex_keywords if kw in prompt_lower)
simple_count = sum(1 for kw in simple_keywords if kw in prompt_lower)
# Determine complexity
if complex_count >= 2 or word_count > 100:
return 'complex'
elif simple_count >= 2 or word_count < 20:
return 'simple'
else:
return 'medium'
def select_model(self, prompt: str, preferred_provider: str = None) -> str:
"""
Select the best model for the prompt.
Args:
prompt: The task prompt
preferred_provider: Optional preferred provider (openai, anthropic, google)
Returns:
Selected model name
"""
complexity = self.analyze_complexity(prompt)
models = self.MODEL_TIERS.get(complexity, self.MODEL_TIERS['medium'])
# Filter by provider if specified
if preferred_provider:
provider_prefixes = {
'openai': ['gpt-', 'o1-'],
'anthropic': ['claude-'],
'google': ['gemini-']
}
prefixes = provider_prefixes.get(preferred_provider.lower(), [])
filtered = [m for m in models if any(m.startswith(p) for p in prefixes)]
if filtered:
models = filtered
selected = models[0] if models else 'gpt-4o-mini'
self.print_status(f"🎯 Task complexity: {complexity}", "info")
self.print_status(f"🤖 Selected model: {selected}", "success")
return selected
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply router configuration.
Args:
config: Agent configuration dictionary
flag_value: Boolean or dict with provider preference
Returns:
Modified configuration with selected model
"""
if not flag_value:
return config
# Get prompt from config if available
prompt = config.get('prompt', config.get('goal', ''))
preferred_provider = None
if isinstance(flag_value, dict):
preferred_provider = flag_value.get('provider')
if prompt:
selected_model = self.select_model(prompt, preferred_provider)
config['llm'] = selected_model
config['use_router'] = True
return config
def execute(self, prompt: str = None, provider: str = None, **kwargs) -> str:
"""
Execute model selection.
Args:
prompt: Task prompt
provider: Preferred provider
Returns:
Selected model name
"""
if not prompt:
return 'gpt-4o-mini' # Default
return self.select_model(prompt, provider)
@@ -0,0 +1,222 @@
"""
Session Handler for CLI.
Provides session management for multi-turn conversations.
Usage: praisonai session list
praisonai session show <name>
praisonai session resume <name>
praisonai session delete <name>
"""
from typing import Any, Dict, List
from .base import CommandHandler
class SessionHandler(CommandHandler):
"""
Handler for session command.
Manages conversation sessions using FileMemory.
Example:
praisonai session list
praisonai session show my-project
praisonai session resume my-project
praisonai session delete my-project
"""
def __init__(self, verbose: bool = False, user_id: str = "praison"):
super().__init__(verbose)
self.user_id = user_id
self._memory = None
@property
def feature_name(self) -> str:
return "session"
def get_actions(self) -> List[str]:
return ["list", "show", "resume", "delete", "help"]
def get_help_text(self) -> str:
return """
Session Commands:
praisonai session list - List all saved sessions
praisonai session show <name> - Show session details
praisonai session resume <name> - Resume a session (load into memory)
praisonai session delete <name> - Delete a session
Sessions store conversation history for multi-turn interactions.
Use --auto-save <name> flag when running agents to auto-save sessions.
"""
def _get_memory(self):
"""Get FileMemory instance lazily."""
if self._memory is None:
try:
from praisonaiagents.memory.file_memory import FileMemory
self._memory = FileMemory(user_id=self.user_id)
except ImportError:
self.print_status(
"Session requires praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
return self._memory
def action_list(self, args: List[str], **kwargs) -> List[Dict[str, Any]]:
"""
List all saved sessions.
Returns:
List of session info dicts
"""
memory = self._get_memory()
if not memory:
return []
try:
sessions = memory.list_sessions()
if sessions:
from rich.table import Table
from rich.console import Console
console = Console()
table = Table(title="Saved Sessions")
table.add_column("Name", style="cyan")
table.add_column("Saved At", style="green")
table.add_column("Messages", style="yellow")
table.add_column("Has History", style="magenta")
for s in sessions:
table.add_row(
s.get("name", ""),
s.get("saved_at", "")[:19] if s.get("saved_at") else "",
str(s.get("short_term_count", 0)),
"✅" if s.get("has_conversation") else "❌"
)
console.print(table)
else:
self.print_status("No sessions found. Use --auto-save <name> when running agents.", "warning")
return sessions
except Exception as e:
self.print_status(f"Failed to list sessions: {e}", "error")
return []
def action_show(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Show session details.
Args:
args: List containing session name
Returns:
Session data dict
"""
if not args:
self.print_status("Usage: praisonai session show <name>", "error")
return {}
memory = self._get_memory()
if not memory:
return {}
name = args[0]
try:
session_data = memory.resume_session(name)
print(f"\n📊 Session: {name}")
print(f"Saved at: {session_data.get('saved_at_iso', 'Unknown')}")
print(f"User ID: {session_data.get('user_id', 'Unknown')}")
print(f"Short-term items: {len(session_data.get('short_term', []))}")
print(f"Conversation messages: {len(session_data.get('conversation_history', []))}")
# Show last few messages
history = session_data.get('conversation_history', [])
if history:
print(f"\nLast {min(5, len(history))} messages:")
for msg in history[-5:]:
role = msg.get('role', 'unknown')
content = msg.get('content', '')[:100]
if len(msg.get('content', '')) > 100:
content += "..."
print(f" [{role}]: {content}")
return session_data
except FileNotFoundError:
self.print_status(f"Session '{name}' not found", "error")
return {}
except Exception as e:
self.print_status(f"Failed to show session: {e}", "error")
return {}
def action_resume(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Resume a session (load into memory).
Args:
args: List containing session name
Returns:
Session data dict
"""
if not args:
self.print_status("Usage: praisonai session resume <name>", "error")
return {}
memory = self._get_memory()
if not memory:
return {}
name = args[0]
try:
session_data = memory.resume_session(name)
self.print_status(f"✅ Session '{name}' resumed", "success")
print(f" Loaded {len(session_data.get('short_term', []))} memory items")
print(f" Loaded {len(session_data.get('conversation_history', []))} conversation messages")
return session_data
except FileNotFoundError:
self.print_status(f"Session '{name}' not found", "error")
return {}
except Exception as e:
self.print_status(f"Failed to resume session: {e}", "error")
return {}
def action_delete(self, args: List[str], **kwargs) -> bool:
"""
Delete a session.
Args:
args: List containing session name
Returns:
True if successful
"""
if not args:
self.print_status("Usage: praisonai session delete <name>", "error")
return False
memory = self._get_memory()
if not memory:
return False
name = args[0]
try:
if memory.delete_session(name):
self.print_status(f"✅ Session '{name}' deleted", "success")
return True
else:
self.print_status(f"Session '{name}' not found", "error")
return False
except Exception as e:
self.print_status(f"Failed to delete session: {e}", "error")
return False
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute session command action."""
return super().execute(action, action_args, **kwargs)
@@ -0,0 +1,179 @@
"""
Telemetry Handler for CLI.
Provides usage monitoring and analytics.
Usage: praisonai "prompt" --telemetry
"""
from typing import Any, Dict, Tuple
from .base import FlagHandler
# Lazy imports
enable_telemetry = None
disable_telemetry = None
get_telemetry = None
def _load_telemetry():
"""Load telemetry functions lazily."""
global enable_telemetry, disable_telemetry, get_telemetry
try:
from praisonaiagents import (
enable_telemetry as _enable,
disable_telemetry as _disable,
get_telemetry as _get
)
enable_telemetry = _enable
disable_telemetry = _disable
get_telemetry = _get
return True
except ImportError:
return False
class TelemetryHandler(FlagHandler):
"""
Handler for --telemetry flag.
Enables usage monitoring and analytics for agent executions.
Example:
praisonai "Task" --telemetry
"""
@property
def feature_name(self) -> str:
return "telemetry"
@property
def flag_name(self) -> str:
return "telemetry"
@property
def flag_help(self) -> str:
return "Enable usage monitoring and analytics"
def check_dependencies(self) -> Tuple[bool, str]:
"""Check if telemetry is available."""
if _load_telemetry():
return True, ""
return False, "Telemetry requires praisonaiagents[telemetry]. Install with: pip install praisonaiagents[telemetry]"
def enable(self, **kwargs) -> bool:
"""
Enable telemetry.
Args:
**kwargs: Additional telemetry configuration
Returns:
True if enabled successfully
"""
if not _load_telemetry():
self.print_status(
"Telemetry not available. Install with: pip install praisonaiagents[telemetry]",
"warning"
)
return False
try:
enable_telemetry(**kwargs)
self.print_status("📊 Telemetry enabled", "success")
return True
except Exception as e:
self.log(f"Failed to enable telemetry: {e}", "error")
return False
def disable(self) -> bool:
"""
Disable telemetry.
Returns:
True if disabled successfully
"""
if not _load_telemetry():
return True # Already disabled
try:
disable_telemetry()
self.print_status("📊 Telemetry disabled", "info")
return True
except Exception as e:
self.log(f"Failed to disable telemetry: {e}", "error")
return False
def get_stats(self) -> Dict[str, Any]:
"""
Get telemetry statistics.
Returns:
Dictionary of telemetry stats
"""
if not _load_telemetry():
return {}
try:
telemetry = get_telemetry()
if telemetry and hasattr(telemetry, 'get_stats'):
return telemetry.get_stats()
except Exception as e:
self.log(f"Failed to get telemetry stats: {e}", "warning")
return {}
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply telemetry configuration.
Args:
config: Agent configuration dictionary
flag_value: Boolean indicating if telemetry should be enabled
Returns:
Modified configuration
"""
if flag_value:
self.enable()
return config
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process result to show telemetry summary.
Args:
result: Agent output
flag_value: Boolean indicating if telemetry is enabled
Returns:
Original result (telemetry summary is printed)
"""
if not flag_value:
return result
stats = self.get_stats()
if stats:
self.print_status("\n📊 Telemetry Summary:", "info")
for key, value in stats.items():
self.print_status(f" {key}: {value}", "info")
return result
def execute(self, action: str = "enable", **kwargs) -> Any:
"""
Execute telemetry action.
Args:
action: Action to perform (enable, disable, stats)
Returns:
Result of the action
"""
if action == "enable":
return self.enable(**kwargs)
elif action == "disable":
return self.disable()
elif action == "stats":
return self.get_stats()
else:
self.print_status(f"Unknown telemetry action: {action}", "error")
return None
@@ -0,0 +1,334 @@
"""
Todo Handler for CLI.
Provides todo list management.
Usage: praisonai todo list
praisonai todo add "New task"
praisonai "Plan project" --todo
"""
import os
import json
from typing import Any, Dict, List
from .base import CommandHandler, FlagHandler
class TodoHandler(CommandHandler, FlagHandler):
"""
Handler for todo command and --todo flag.
Manages todo lists for task planning.
Example:
praisonai todo list
praisonai todo add "Implement feature X"
praisonai todo complete 1
praisonai "Plan project" --todo
"""
def __init__(self, verbose: bool = False, workspace: str = None):
CommandHandler.__init__(self, verbose)
self.workspace = workspace or os.path.join(os.path.expanduser("~"), ".praison")
self.todo_file = os.path.join(self.workspace, "todos.json")
os.makedirs(self.workspace, exist_ok=True)
self._todos = None
@property
def feature_name(self) -> str:
return "todo"
@property
def flag_name(self) -> str:
return "todo"
@property
def flag_help(self) -> str:
return "Generate todo list from task"
def get_actions(self) -> List[str]:
return ["list", "add", "complete", "delete", "clear", "help"]
def get_help_text(self) -> str:
return """
Todo Commands:
praisonai todo list - List all todos
praisonai todo add <task> - Add a new todo
praisonai todo complete <id> - Mark todo as complete
praisonai todo delete <id> - Delete a todo
praisonai todo clear - Clear all todos
Flag Usage:
praisonai "Plan project" --todo - Generate todos from task
"""
def _load_todos(self) -> List[Dict[str, Any]]:
"""Load todos from file."""
if self._todos is not None:
return self._todos
if os.path.exists(self.todo_file):
try:
with open(self.todo_file, 'r') as f:
self._todos = json.load(f)
except Exception:
self._todos = []
else:
self._todos = []
return self._todos
def _save_todos(self):
"""Save todos to file."""
try:
with open(self.todo_file, 'w') as f:
json.dump(self._todos or [], f, indent=2)
except Exception as e:
self.log(f"Failed to save todos: {e}", "error")
def action_list(self, args: List[str], **kwargs) -> List[Dict[str, Any]]:
"""
List all todos.
Returns:
List of todo items
"""
todos = self._load_todos()
if not todos:
self.print_status("📋 No todos found", "info")
return []
self.print_status("\n📋 Todo List:", "info")
self.print_status("-" * 50, "info")
for i, todo in enumerate(todos, 1):
status = "✅" if todo.get('completed') else "⬜"
task = todo.get('task', 'Unknown')
priority = todo.get('priority', 'medium')
priority_color = {
'high': '🔴',
'medium': '🟡',
'low': '🟢'
}.get(priority, '⚪')
self.print_status(f" {i}. {status} {priority_color} {task}", "info")
self.print_status("-" * 50, "info")
completed = sum(1 for t in todos if t.get('completed'))
self.print_status(f" {completed}/{len(todos)} completed", "info")
return todos
def action_add(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Add a new todo.
Args:
args: List containing task description
Returns:
Created todo item
"""
if not args:
self.print_status("Usage: praisonai todo add <task>", "error")
return {}
task = ' '.join(args)
priority = kwargs.get('priority', 'medium')
todos = self._load_todos()
todo = {
'id': len(todos) + 1,
'task': task,
'completed': False,
'priority': priority,
'created_at': self._get_timestamp()
}
todos.append(todo)
self._todos = todos
self._save_todos()
self.print_status(f"✅ Added: {task}", "success")
return todo
def action_complete(self, args: List[str], **kwargs) -> bool:
"""
Mark a todo as complete.
Args:
args: List containing todo ID
Returns:
True if successful
"""
if not args:
self.print_status("Usage: praisonai todo complete <id>", "error")
return False
try:
todo_id = int(args[0])
except ValueError:
self.print_status("Invalid todo ID", "error")
return False
todos = self._load_todos()
if todo_id < 1 or todo_id > len(todos):
self.print_status(f"Todo {todo_id} not found", "error")
return False
todos[todo_id - 1]['completed'] = True
todos[todo_id - 1]['completed_at'] = self._get_timestamp()
self._todos = todos
self._save_todos()
self.print_status(f"✅ Completed: {todos[todo_id - 1]['task']}", "success")
return True
def action_delete(self, args: List[str], **kwargs) -> bool:
"""
Delete a todo.
Args:
args: List containing todo ID
Returns:
True if successful
"""
if not args:
self.print_status("Usage: praisonai todo delete <id>", "error")
return False
try:
todo_id = int(args[0])
except ValueError:
self.print_status("Invalid todo ID", "error")
return False
todos = self._load_todos()
if todo_id < 1 or todo_id > len(todos):
self.print_status(f"Todo {todo_id} not found", "error")
return False
deleted = todos.pop(todo_id - 1)
self._todos = todos
self._save_todos()
self.print_status(f"🗑️ Deleted: {deleted['task']}", "success")
return True
def action_clear(self, args: List[str], **kwargs) -> bool:
"""
Clear all todos.
Returns:
True if successful
"""
self._todos = []
self._save_todos()
self.print_status("🗑️ All todos cleared", "success")
return True
def _get_timestamp(self) -> str:
"""Get current timestamp."""
from datetime import datetime
return datetime.now().isoformat()
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
"""
Apply todo configuration.
Args:
config: Agent configuration dictionary
flag_value: Boolean indicating todo generation
Returns:
Modified configuration
"""
if flag_value:
config['generate_todos'] = True
return config
def generate_todos_from_response(self, response: str) -> List[Dict[str, Any]]:
"""
Extract todos from agent response.
Args:
response: Agent response text
Returns:
List of extracted todos
"""
# Simple extraction - look for numbered lists or bullet points
import re
todos = []
lines = response.split('\n')
for line in lines:
line = line.strip()
# Match numbered lists (1. Task, 1) Task)
match = re.match(r'^[\d]+[\.\)]\s*(.+)$', line)
if match:
task = match.group(1).strip()
if task and len(task) > 3:
todos.append({
'task': task,
'completed': False,
'priority': 'medium'
})
# Match bullet points (- Task, * Task, • Task)
elif line.startswith(('-', '*', '•', '→')):
task = line[1:].strip()
if task and len(task) > 3:
todos.append({
'task': task,
'completed': False,
'priority': 'medium'
})
return todos
def post_process_result(self, result: Any, flag_value: Any) -> Any:
"""
Post-process result to extract todos.
Args:
result: Agent output
flag_value: Boolean indicating todo generation
Returns:
Original result (todos are saved)
"""
if not flag_value:
return result
extracted = self.generate_todos_from_response(str(result))
if extracted:
todos = self._load_todos()
for todo in extracted:
todo['id'] = len(todos) + 1
todo['created_at'] = self._get_timestamp()
todos.append(todo)
self._todos = todos
self._save_todos()
self.print_status(f"\n📋 Generated {len(extracted)} todos:", "success")
for todo in extracted:
self.print_status(f" • {todo['task']}", "info")
return result
def execute(self, action: str = None, action_args: List[str] = None, **kwargs) -> Any:
"""Execute todo command action."""
if action is None:
action = "list"
if action_args is None:
action_args = []
return CommandHandler.execute(self, action, action_args, **kwargs)
@@ -0,0 +1,218 @@
"""
Tools Handler for CLI.
Provides tool registry management.
Usage: praisonai tools list
praisonai tools info internet_search
"""
from typing import Any, Dict, List
from .base import CommandHandler
class ToolsHandler(CommandHandler):
"""
Handler for tools command.
Manages tool registry and provides tool information.
Example:
praisonai tools list
praisonai tools info internet_search
praisonai tools search "web"
"""
def __init__(self, verbose: bool = False):
super().__init__(verbose)
self._registry = None
@property
def feature_name(self) -> str:
return "tools"
def get_actions(self) -> List[str]:
return ["list", "info", "search", "help"]
def get_help_text(self) -> str:
return """
Tools Commands:
praisonai tools list - List all available tools
praisonai tools info <name> - Show tool details
praisonai tools search <query> - Search tools by name/description
Built-in tools include: internet_search, calculator, file operations, etc.
"""
def _get_registry(self):
"""Get tool registry lazily."""
if self._registry is None:
try:
from praisonaiagents.tools import get_registry
self._registry = get_registry()
except ImportError:
self.print_status(
"Tools require praisonaiagents. Install with: pip install praisonaiagents",
"error"
)
return None
return self._registry
def _get_builtin_tools(self) -> Dict[str, Any]:
"""Get dictionary of built-in tools."""
tools = {}
try:
from praisonaiagents.tools import TOOL_MAPPINGS
tools.update(TOOL_MAPPINGS)
except ImportError:
pass
# Add common tool descriptions
tool_descriptions = {
"internet_search": "Search the internet using DuckDuckGo",
"calculator": "Perform mathematical calculations",
"read_file": "Read contents of a file",
"write_file": "Write content to a file",
"list_files": "List files in a directory",
"execute_code": "Execute Python code",
"shell_command": "Execute shell commands",
"wikipedia_search": "Search Wikipedia",
"arxiv_search": "Search arXiv papers",
"tavily_search": "Search using Tavily API",
"csv_read": "Read CSV files",
"json_read": "Read JSON files",
"yaml_read": "Read YAML files",
}
for name, desc in tool_descriptions.items():
if name not in tools:
tools[name] = {"description": desc, "available": name in tools}
return tools
def action_list(self, args: List[str], **kwargs) -> List[str]:
"""
List all available tools.
Returns:
List of tool names
"""
tools = self._get_builtin_tools()
self.print_status("\n🔧 Available Tools:", "info")
self.print_status("-" * 50, "info")
# Group by category
categories = {
"Search": ["internet_search", "wikipedia_search", "arxiv_search", "tavily_search"],
"File": ["read_file", "write_file", "list_files", "csv_read", "json_read", "yaml_read"],
"Code": ["execute_code", "shell_command", "calculator"],
}
for category, tool_names in categories.items():
self.print_status(f"\n {category}:", "info")
for name in tool_names:
if name in tools:
desc = tools[name].get("description", "No description") if isinstance(tools[name], dict) else "Available"
self.print_status(f" • {name}: {desc}", "info")
# List any uncategorized tools
categorized = set(t for tools_list in categories.values() for t in tools_list)
uncategorized = [t for t in tools.keys() if t not in categorized]
if uncategorized:
self.print_status("\n Other:", "info")
for name in uncategorized[:10]: # Limit to 10
self.print_status(f" • {name}", "info")
return list(tools.keys())
def action_info(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Show tool information.
Args:
args: List containing tool name
Returns:
Dictionary of tool info
"""
if not args:
self.print_status("Usage: praisonai tools info <tool_name>", "error")
return {}
tool_name = args[0]
try:
import praisonaiagents.tools as tools_module
if hasattr(tools_module, tool_name):
tool = getattr(tools_module, tool_name)
info = {
"name": tool_name,
"type": type(tool).__name__,
"doc": tool.__doc__ or "No documentation",
}
# Get function signature if available
if callable(tool):
import inspect
try:
sig = inspect.signature(tool)
info["signature"] = str(sig)
info["parameters"] = list(sig.parameters.keys())
except (ValueError, TypeError):
pass
self.print_status(f"\n🔧 Tool: {tool_name}", "info")
self.print_status("-" * 40, "info")
for key, value in info.items():
if key == "doc":
self.print_status(f"\nDescription:\n{value}", "info")
else:
self.print_status(f" {key}: {value}", "info")
return info
else:
self.print_status(f"Tool '{tool_name}' not found", "error")
return {}
except ImportError:
self.print_status("Could not load tools module", "error")
return {}
def action_search(self, args: List[str], **kwargs) -> List[str]:
"""
Search tools by name or description.
Args:
args: List containing search query
Returns:
List of matching tool names
"""
if not args:
self.print_status("Usage: praisonai tools search <query>", "error")
return []
query = ' '.join(args).lower()
tools = self._get_builtin_tools()
matches = []
for name, info in tools.items():
desc = info.get("description", "") if isinstance(info, dict) else ""
if query in name.lower() or query in desc.lower():
matches.append(name)
if matches:
self.print_status(f"\n🔍 Tools matching '{query}':", "info")
for name in matches:
self.print_status(f" • {name}", "info")
else:
self.print_status(f"No tools found matching '{query}'", "warning")
return matches
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute tools command action."""
return super().execute(action, action_args, **kwargs)
@@ -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
File diff suppressed because it is too large. Load diff
+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.11 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.11"
__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.0.190",
"praisonaiagents>=0.1.4",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+16
View File
@@ -0,0 +1,16 @@
roles:
researcher:
name: "Research Agent"
role: "Information Researcher"
goal: "Find and analyze information using multiple tools"
backstory: "You are an expert researcher with access to multiple information sources"
llm: "gpt-5-nano"
tools:
- search_web
- get_weather
- calculate
tasks:
test_task:
description: "Use all available tools to gather information about {topic}"
expected_output: "A comprehensive report using data from all tools"
+37
View File
@@ -0,0 +1,37 @@
def search_web(query: str) -> str:
"""Search the web for information.
Args:
query: The search query
Returns:
Search results
"""
return f"Web search results for: {query}"
def get_weather(location: str) -> str:
"""Get weather information for a location.
Args:
location: The location to get weather for
Returns:
Weather information
"""
return f"Weather in {location}: Sunny, 25°C"
def calculate(expression: str) -> str:
"""Perform a calculation.
Args:
expression: Mathematical expression to evaluate
Returns:
Calculation result
"""
try:
result = eval(expression)
return f"Result: {result}"
except:
return "Invalid expression"
+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/"])
@@ -0,0 +1,166 @@
"""
Integration tests for CLI features with Agent execution.
These tests verify that CLI features are properly integrated into
the agent execution flow in handle_direct_prompt().
"""
import pytest
import sys
import os
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
class TestAgentFeatureIntegration:
"""Tests for CLI features integrated with agent execution."""
def test_router_handler_integration(self):
"""Test that RouterHandler can select model for agent."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
model = handler.select_model("Simple question")
assert model is not None
assert isinstance(model, str)
def test_metrics_handler_integration(self):
"""Test that MetricsHandler can be applied to agent config."""
from praisonai.cli.features.metrics import MetricsHandler
handler = MetricsHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, True)
assert result.get("metrics") is True
def test_mcp_handler_integration(self):
"""Test that MCPHandler can parse MCP commands."""
from praisonai.cli.features.mcp import MCPHandler
handler = MCPHandler()
cmd, args, env = handler.parse_mcp_command("npx -y @mcp/server .")
assert cmd == "npx"
assert "-y" in args
def test_fast_context_handler_integration(self):
"""Test that FastContextHandler can validate paths."""
from praisonai.cli.features.fast_context import FastContextHandler
handler = FastContextHandler()
valid, msg = handler.validate_search_path("/tmp")
# /tmp should exist on most systems
assert isinstance(valid, bool)
def test_guardrail_handler_integration(self):
"""Test that GuardrailHandler can be applied to config."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, "Check output")
assert "guardrail_description" in result
def test_todo_handler_integration(self):
"""Test that TodoHandler can extract todos from text."""
from praisonai.cli.features.todo import TodoHandler
handler = TodoHandler()
todos = handler.generate_todos_from_response("1. First task\n2. Second task\n3. Third task")
assert len(todos) == 3
assert todos[0]['task'] == "First task"
def test_auto_memory_handler_integration(self):
"""Test that AutoMemoryHandler can be applied to config."""
from praisonai.cli.features.auto_memory import AutoMemoryHandler
handler = AutoMemoryHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, True)
assert result.get("auto_memory") is True
def test_image_handler_integration(self):
"""Test that ImageHandler can validate image paths."""
from praisonai.cli.features.image import ImageHandler
handler = ImageHandler()
valid, msg = handler.validate_image_path("/path/to/image.png")
# Path doesn't exist, but extension is valid
assert "not found" in msg.lower() or valid is False
valid2, msg2 = handler.validate_image_path("/path/to/file.txt")
assert valid2 is False
assert "format" in msg2.lower() or "extension" in msg2.lower()
def test_handoff_handler_integration(self):
"""Test that HandoffHandler can parse agent names."""
from praisonai.cli.features.handoff import HandoffHandler
handler = HandoffHandler()
agents = handler.parse_agent_names("researcher,writer,editor")
assert len(agents) == 3
assert "researcher" in agents
assert "writer" in agents
assert "editor" in agents
def test_telemetry_handler_integration(self):
"""Test that TelemetryHandler can check dependencies."""
from praisonai.cli.features.telemetry import TelemetryHandler
handler = TelemetryHandler()
available, msg = handler.check_dependencies()
assert isinstance(available, bool)
def test_flow_display_handler_integration(self):
"""Test that FlowDisplayHandler can display workflow."""
from praisonai.cli.features.flow_display import FlowDisplayHandler
handler = FlowDisplayHandler()
# Should not raise
handler.display_workflow_start("Test Workflow", ["Agent1", "Agent2"])
handler.display_workflow_end(success=True)
class TestAgentConfigApplication:
"""Tests for verifying features are applied to agent config."""
def test_metrics_in_agent_config(self):
"""Verify metrics flag is applied to agent config."""
from praisonai.cli.features.metrics import MetricsHandler
handler = MetricsHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, True)
assert result.get("metrics") is True
def test_guardrail_in_agent_config(self):
"""Verify guardrail is stored for post-processing."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, "Check validity")
assert "guardrail_description" in result
def test_router_selects_appropriate_model(self):
"""Verify router selects model based on complexity."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler()
# Simple prompt should get simpler model
simple_model = handler.select_model("What is 2+2?")
assert simple_model in ['gpt-4o-mini', 'claude-3-haiku', 'gemini-1.5-flash']
# Complex prompt should get more capable model
complex_model = handler.select_model(
"Analyze the comprehensive architecture of this system, "
"evaluate all design patterns, and synthesize a detailed report"
)
assert complex_model in ['gpt-4-turbo', 'claude-3-opus', 'o1-preview', 'gpt-4o', 'claude-3-sonnet', 'gemini-1.5-pro']
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,424 @@
"""
Unit tests for CLI features module.
Tests are organized by feature and use mocking to avoid actual API calls.
Each test is designed to run quickly (<1s) to maintain fast feedback loops.
"""
import pytest
import sys
import os
from unittest.mock import Mock, patch
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
class TestGuardrailHandler:
"""Tests for --guardrail flag functionality."""
def test_guardrail_handler_import(self):
"""Test that GuardrailHandler can be imported."""
from praisonai.cli.features import GuardrailHandler
assert GuardrailHandler is not None
def test_guardrail_handler_initialization(self):
"""Test GuardrailHandler initialization."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler(verbose=True)
assert handler.feature_name == "guardrail"
assert handler.flag_name == "guardrail"
assert handler.verbose is True
def test_guardrail_check_dependencies_available(self):
"""Test dependency check when praisonaiagents is available."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler()
available, msg = handler.check_dependencies()
# Should be available if praisonaiagents is installed
assert isinstance(available, bool)
assert isinstance(msg, str)
def test_guardrail_apply_to_agent_config(self):
"""Test applying guardrail to agent config."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, "Ensure output is valid JSON")
assert "guardrail" in result or "guardrail_description" in result
@patch('praisonai.cli.features.guardrail.PRAISONAI_AVAILABLE', True)
def test_guardrail_execute_with_string(self):
"""Test executing guardrail with string description."""
from praisonai.cli.features.guardrail import GuardrailHandler
handler = GuardrailHandler()
# Mock the LLMGuardrail
with patch('praisonai.cli.features.guardrail.LLMGuardrail') as mock_guardrail:
mock_instance = Mock()
mock_instance.return_value = (True, "Valid output")
mock_guardrail.return_value = mock_instance
result = handler.execute(
output="Test output",
guardrail_description="Check if valid"
)
assert result is not None
class TestMetricsHandler:
"""Tests for --metrics flag functionality."""
def test_metrics_handler_import(self):
"""Test that MetricsHandler can be imported."""
from praisonai.cli.features import MetricsHandler
assert MetricsHandler is not None
def test_metrics_handler_initialization(self):
"""Test MetricsHandler initialization."""
from praisonai.cli.features.metrics import MetricsHandler
handler = MetricsHandler(verbose=True)
assert handler.feature_name == "metrics"
assert handler.flag_name == "metrics"
def test_metrics_apply_to_agent_config(self):
"""Test applying metrics to agent config."""
from praisonai.cli.features.metrics import MetricsHandler
handler = MetricsHandler()
config = {"name": "TestAgent"}
result = handler.apply_to_agent_config(config, True)
assert result.get("metrics") is True
def test_metrics_format_output(self):
"""Test formatting metrics output."""
from praisonai.cli.features.metrics import MetricsHandler
handler = MetricsHandler()
mock_metrics = {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
"cost": 0.0015
}
formatted = handler.format_metrics(mock_metrics)
assert "100" in formatted or "prompt" in formatted.lower()
class TestImageHandler:
"""Tests for --image flag functionality."""
def test_image_handler_import(self):
"""Test that ImageHandler can be imported."""
from praisonai.cli.features import ImageHandler
assert ImageHandler is not None
def test_image_handler_initialization(self):
"""Test ImageHandler initialization."""
from praisonai.cli.features.image import ImageHandler
handler = ImageHandler(verbose=True)
assert handler.feature_name == "image"
assert handler.flag_name == "image"
def test_image_validate_path_valid(self):
"""Test validating a valid image path."""
from praisonai.cli.features.image import ImageHandler
handler = ImageHandler()
# Test with a mock path
with patch('os.path.exists', return_value=True):
valid, msg = handler.validate_image_path("/path/to/image.png")
assert valid is True
def test_image_validate_path_invalid_extension(self):
"""Test validating an invalid image extension."""
from praisonai.cli.features.image import ImageHandler
handler = ImageHandler()
valid, msg = handler.validate_image_path("/path/to/file.txt")
assert valid is False
assert "extension" in msg.lower() or "format" in msg.lower()
class TestTelemetryHandler:
"""Tests for --telemetry flag functionality."""
def test_telemetry_handler_import(self):
"""Test that TelemetryHandler can be imported."""
from praisonai.cli.features import TelemetryHandler
assert TelemetryHandler is not None
def test_telemetry_handler_initialization(self):
"""Test TelemetryHandler initialization."""
from praisonai.cli.features.telemetry import TelemetryHandler
handler = TelemetryHandler(verbose=True)
assert handler.feature_name == "telemetry"
assert handler.flag_name == "telemetry"
def test_telemetry_enable(self):
"""Test enabling telemetry."""
from praisonai.cli.features.telemetry import TelemetryHandler
handler = TelemetryHandler()
with patch('praisonai.cli.features.telemetry.enable_telemetry'):
handler.enable()
# Should attempt to enable telemetry
class TestMCPHandler:
"""Tests for --mcp flag functionality."""
def test_mcp_handler_import(self):
"""Test that MCPHandler can be imported."""
from praisonai.cli.features import MCPHandler
assert MCPHandler is not None
def test_mcp_handler_initialization(self):
"""Test MCPHandler initialization."""
from praisonai.cli.features.mcp import MCPHandler
handler = MCPHandler(verbose=True)
assert handler.feature_name == "mcp"
assert handler.flag_name == "mcp"
def test_mcp_parse_command_simple(self):
"""Test parsing a simple MCP command."""
from praisonai.cli.features.mcp import MCPHandler
handler = MCPHandler()
command, args, env = handler.parse_mcp_command("npx -y @modelcontextprotocol/server-filesystem .")
assert command == "npx"
assert "-y" in args
assert isinstance(env, dict)
def test_mcp_parse_command_with_env(self):
"""Test parsing MCP command with environment variables."""
from praisonai.cli.features.mcp import MCPHandler
handler = MCPHandler()
result = handler.parse_mcp_command(
"npx server",
env_vars="API_KEY=test123,DEBUG=true"
)
assert result is not None
class TestFastContextHandler:
"""Tests for --fast-context flag functionality."""
def test_fast_context_handler_import(self):
"""Test that FastContextHandler can be imported."""
from praisonai.cli.features import FastContextHandler
assert FastContextHandler is not None
def test_fast_context_handler_initialization(self):
"""Test FastContextHandler initialization."""
from praisonai.cli.features.fast_context import FastContextHandler
handler = FastContextHandler(verbose=True)
assert handler.feature_name == "fast_context"
assert handler.flag_name == "fast-context"
def test_fast_context_validate_path(self):
"""Test validating search path."""
from praisonai.cli.features.fast_context import FastContextHandler
handler = FastContextHandler()
with patch('os.path.isdir', return_value=True):
valid, msg = handler.validate_search_path("/valid/path")
assert valid is True
class TestKnowledgeHandler:
"""Tests for knowledge command functionality."""
def test_knowledge_handler_import(self):
"""Test that KnowledgeHandler can be imported."""
from praisonai.cli.features import KnowledgeHandler
assert KnowledgeHandler is not None
def test_knowledge_handler_initialization(self):
"""Test KnowledgeHandler initialization."""
from praisonai.cli.features.knowledge import KnowledgeHandler
handler = KnowledgeHandler(verbose=True)
assert handler.feature_name == "knowledge"
def test_knowledge_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.knowledge import KnowledgeHandler
handler = KnowledgeHandler()
actions = handler.get_actions()
assert "add" in actions
assert "search" in actions
assert "list" in actions
class TestSessionHandler:
"""Tests for session command functionality."""
def test_session_handler_import(self):
"""Test that SessionHandler can be imported."""
from praisonai.cli.features import SessionHandler
assert SessionHandler is not None
def test_session_handler_initialization(self):
"""Test SessionHandler initialization."""
from praisonai.cli.features.session import SessionHandler
handler = SessionHandler(verbose=True)
assert handler.feature_name == "session"
def test_session_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.session import SessionHandler
handler = SessionHandler()
actions = handler.get_actions()
assert "list" in actions
assert "show" in actions
assert "resume" in actions
assert "delete" in actions
class TestToolsHandler:
"""Tests for tools command functionality."""
def test_tools_handler_import(self):
"""Test that ToolsHandler can be imported."""
from praisonai.cli.features import ToolsHandler
assert ToolsHandler is not None
def test_tools_handler_initialization(self):
"""Test ToolsHandler initialization."""
from praisonai.cli.features.tools import ToolsHandler
handler = ToolsHandler(verbose=True)
assert handler.feature_name == "tools"
def test_tools_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.tools import ToolsHandler
handler = ToolsHandler()
actions = handler.get_actions()
assert "list" in actions
assert "info" in actions
class TestHandoffHandler:
"""Tests for --handoff flag functionality."""
def test_handoff_handler_import(self):
"""Test that HandoffHandler can be imported."""
from praisonai.cli.features import HandoffHandler
assert HandoffHandler is not None
def test_handoff_handler_initialization(self):
"""Test HandoffHandler initialization."""
from praisonai.cli.features.handoff import HandoffHandler
handler = HandoffHandler(verbose=True)
assert handler.feature_name == "handoff"
assert handler.flag_name == "handoff"
def test_handoff_parse_agents(self):
"""Test parsing agent names from comma-separated string."""
from praisonai.cli.features.handoff import HandoffHandler
handler = HandoffHandler()
agents = handler.parse_agent_names("researcher,writer,editor")
assert len(agents) == 3
assert "researcher" in agents
class TestAutoMemoryHandler:
"""Tests for --auto-memory flag functionality."""
def test_auto_memory_handler_import(self):
"""Test that AutoMemoryHandler can be imported."""
from praisonai.cli.features import AutoMemoryHandler
assert AutoMemoryHandler is not None
def test_auto_memory_handler_initialization(self):
"""Test AutoMemoryHandler initialization."""
from praisonai.cli.features.auto_memory import AutoMemoryHandler
handler = AutoMemoryHandler(verbose=True)
assert handler.feature_name == "auto_memory"
assert handler.flag_name == "auto-memory"
class TestTodoHandler:
"""Tests for todo command and --todo flag functionality."""
def test_todo_handler_import(self):
"""Test that TodoHandler can be imported."""
from praisonai.cli.features import TodoHandler
assert TodoHandler is not None
def test_todo_handler_initialization(self):
"""Test TodoHandler initialization."""
from praisonai.cli.features.todo import TodoHandler
handler = TodoHandler(verbose=True)
assert handler.feature_name == "todo"
def test_todo_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.todo import TodoHandler
handler = TodoHandler()
actions = handler.get_actions()
assert "list" in actions
assert "add" in actions
class TestRouterHandler:
"""Tests for --router flag functionality."""
def test_router_handler_import(self):
"""Test that RouterHandler can be imported."""
from praisonai.cli.features import RouterHandler
assert RouterHandler is not None
def test_router_handler_initialization(self):
"""Test RouterHandler initialization."""
from praisonai.cli.features.router import RouterHandler
handler = RouterHandler(verbose=True)
assert handler.feature_name == "router"
assert handler.flag_name == "router"
class TestFlowDisplayHandler:
"""Tests for --flow-display flag functionality."""
def test_flow_display_handler_import(self):
"""Test that FlowDisplayHandler can be imported."""
from praisonai.cli.features import FlowDisplayHandler
assert FlowDisplayHandler is not None
def test_flow_display_handler_initialization(self):
"""Test FlowDisplayHandler initialization."""
from praisonai.cli.features.flow_display import FlowDisplayHandler
handler = FlowDisplayHandler(verbose=True)
assert handler.feature_name == "flow_display"
assert handler.flag_name == "flow-display"
class TestCLIArgumentParsing:
"""Tests for CLI argument parsing of new features."""
def test_guardrail_argument_parsed(self):
"""Test that --guardrail argument is parsed correctly."""
from praisonai.cli import PraisonAI
with patch.object(sys, 'argv', ['praisonai', '--guardrail', 'Check output']):
PraisonAI() # Just verify no exception is raised
def test_metrics_argument_parsed(self):
"""Test that --metrics argument is parsed correctly."""
from praisonai.cli import PraisonAI
with patch.object(sys, 'argv', ['praisonai', '--metrics']):
PraisonAI() # Just verify no exception is raised
def test_image_argument_parsed(self):
"""Test that --image argument is parsed correctly."""
from praisonai.cli import PraisonAI
with patch.object(sys, 'argv', ['praisonai', '--image', '/path/to/image.png']):
PraisonAI() # Just verify no exception is raised
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short", "-x"])
@@ -162,8 +162,8 @@ class TestHandleDirectPromptWithRewrite:
)
with patch.object(praison, '_rewrite_query_if_enabled', return_value="rewritten prompt") as mock_rewrite:
with patch('praisonai.cli.PRAISONAI_AVAILABLE', True):
with patch('praisonai.cli.PraisonAgent') as mock_agent_class:
with patch('praisonai.cli.main.PRAISONAI_AVAILABLE', True):
with patch('praisonai.cli.main.PraisonAgent') as mock_agent_class:
mock_agent = MagicMock()
mock_agent.start.return_value = "result"
mock_agent_class.return_value = mock_agent
@@ -0,0 +1,112 @@
"""
Unit tests for Workflow CLI commands.
Tests the WorkflowHandler CLI feature for YAML workflow management.
"""
import os
import tempfile
class TestWorkflowHandler:
"""Tests for WorkflowHandler CLI feature."""
def test_workflow_handler_import(self):
"""Test that WorkflowHandler can be imported."""
from praisonai.cli.features import WorkflowHandler
assert WorkflowHandler is not None
def test_workflow_handler_initialization(self):
"""Test WorkflowHandler initialization."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler(verbose=True)
assert handler.feature_name == "workflow"
def test_workflow_get_actions(self):
"""Test getting available actions."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
actions = handler.get_actions()
assert "run" in actions
assert "validate" in actions
assert "list" in actions
assert "create" in actions
def test_workflow_templates_exist(self):
"""Test that workflow templates are defined."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
assert "simple" in handler.TEMPLATES
assert "routing" in handler.TEMPLATES
assert "parallel" in handler.TEMPLATES
assert "loop" in handler.TEMPLATES
assert "evaluator-optimizer" in handler.TEMPLATES
def test_workflow_create_template(self):
"""Test creating a workflow from template."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
with tempfile.TemporaryDirectory() as tmpdir:
output_file = os.path.join(tmpdir, "test_workflow.yaml")
result = handler.action_create(
["--template", "simple", "--output", output_file]
)
assert result == output_file
assert os.path.exists(output_file)
# Check content
with open(output_file, 'r') as f:
content = f.read()
assert "name: Simple Workflow" in content
assert "agents:" in content
assert "steps:" in content
def test_workflow_validate(self):
"""Test validating a YAML workflow."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
yaml_content = """
name: Test Workflow
agents:
agent1:
name: Agent1
role: Worker
goal: Work
instructions: "Do work"
steps:
- agent: agent1
action: "Work"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
result = handler.action_validate([temp_path])
assert result == True
finally:
os.unlink(temp_path)
def test_workflow_validate_invalid_file(self):
"""Test validating a non-existent file."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
result = handler.action_validate(["nonexistent.yaml"])
assert result == False
def test_workflow_help_text(self):
"""Test help text contains expected commands."""
from praisonai.cli.features.workflow import WorkflowHandler
handler = WorkflowHandler()
help_text = handler.get_help_text()
assert "run" in help_text
assert "validate" in help_text
assert "list" in help_text
assert "create" in help_text
assert "template" in help_text
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.0.190" },
{ 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.0.190"
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/23/1d/d258091ca26f8ea3794b68835ec3ee43245bdc24e3bd4e7b0606cdf5139e/praisonaiagents-0.0.190.tar.gz", hash = "sha256:a596c56fc956c96f8927fbe6e08eb2ca58e7c2624b3f0bc7b2ed6655b9f7c084", size = 492228, upload-time = "2025-12-16T10:43:29.059Z" }
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/22/8a/6b1905e99452711c5631e34ca42bfa193025e122d12ec9800c0415eb5d1e/praisonaiagents-0.0.190-py3-none-any.whl", hash = "sha256:0481deb62fb0a9a6599ab973bd2ce91f3e700986326dca122a8e8a00850f2154", size = 474458, upload-time = "2025-12-16T10:43:26.984Z" },
{ 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]]