mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab68b2d296 | ||
|
|
abcc5b3b04 |
No files matched your search
@@ -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.30" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.30" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.30" \
|
||||
"praisonai>=2.3.32" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Deep Profiling Benchmark
|
||||
|
||||
Profiles each step of agent initialization to identify performance bottlenecks.
|
||||
|
||||
Key Optimizations Applied:
|
||||
1. Lazy RulesManager - filesystem operations deferred until first access
|
||||
2. Lazy os.getcwd() - fast_context_path resolved only when needed
|
||||
3. Lazy console initialization - Rich Console created on demand
|
||||
|
||||
Usage:
|
||||
python benchmarks/deep_profiling.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import tracemalloc
|
||||
import cProfile
|
||||
import pstats
|
||||
import io
|
||||
from typing import Literal, Callable, Dict, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
# ============================================================================
|
||||
# Timing Utilities
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class StepTiming:
|
||||
name: str
|
||||
duration_us: float # microseconds
|
||||
memory_kb: float # kilobytes
|
||||
|
||||
|
||||
class StepProfiler:
|
||||
"""Profile individual steps during initialization"""
|
||||
|
||||
def __init__(self):
|
||||
self.steps: List[StepTiming] = []
|
||||
self._start_time = None
|
||||
self._start_memory = None
|
||||
|
||||
def start_step(self, name: str):
|
||||
tracemalloc.start()
|
||||
self._start_time = time.perf_counter()
|
||||
self._step_name = name
|
||||
|
||||
def end_step(self):
|
||||
end_time = time.perf_counter()
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
duration_us = (end_time - self._start_time) * 1_000_000
|
||||
memory_kb = peak / 1024
|
||||
|
||||
self.steps.append(StepTiming(
|
||||
name=self._step_name,
|
||||
duration_us=duration_us,
|
||||
memory_kb=memory_kb
|
||||
))
|
||||
|
||||
def print_report(self, title: str):
|
||||
print(f"\n{'='*70}")
|
||||
print(f"STEP-BY-STEP PROFILING: {title}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
total_time = sum(s.duration_us for s in self.steps)
|
||||
total_memory = sum(s.memory_kb for s in self.steps)
|
||||
|
||||
print(f"\n{'Step':<40} {'Time (μs)':<15} {'Memory (KB)':<15} {'% Time':<10}")
|
||||
print("-" * 80)
|
||||
|
||||
for step in sorted(self.steps, key=lambda x: x.duration_us, reverse=True):
|
||||
pct = (step.duration_us / total_time * 100) if total_time > 0 else 0
|
||||
print(f"{step.name:<40} {step.duration_us:<15.2f} {step.memory_kb:<15.2f} {pct:<10.1f}%")
|
||||
|
||||
print("-" * 80)
|
||||
print(f"{'TOTAL':<40} {total_time:<15.2f} {total_memory:<15.2f} {'100.0':<10}%")
|
||||
|
||||
|
||||
def time_function(func: Callable, iterations: int = 100) -> Dict:
|
||||
"""Time a function with detailed statistics"""
|
||||
times = []
|
||||
|
||||
# Warmup
|
||||
for _ in range(10):
|
||||
func()
|
||||
|
||||
# Measure
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
func()
|
||||
end = time.perf_counter()
|
||||
times.append((end - start) * 1_000_000) # Convert to microseconds
|
||||
|
||||
return {
|
||||
'avg': sum(times) / len(times),
|
||||
'min': min(times),
|
||||
'max': max(times),
|
||||
'median': sorted(times)[len(times)//2]
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile PraisonAI Agent Initialization - Step by Step
|
||||
# ============================================================================
|
||||
|
||||
def profile_praisonai_detailed():
|
||||
"""Profile each step of PraisonAI Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("DETAILED PROFILING: PraisonAI Agent")
|
||||
print("="*70)
|
||||
|
||||
profiler = StepProfiler()
|
||||
|
||||
# Step 1: Import
|
||||
profiler.start_step("1. Import praisonaiagents")
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
profiler.end_step()
|
||||
|
||||
# Step 2: Define tool
|
||||
profiler.start_step("2. Define tool function")
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
profiler.end_step()
|
||||
|
||||
# Step 3: Full instantiation
|
||||
profiler.start_step("3. Full Agent instantiation")
|
||||
agent = PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
profiler.end_step()
|
||||
|
||||
profiler.print_report("PraisonAI Agent")
|
||||
|
||||
# Now profile the __init__ internals using cProfile
|
||||
print("\n" + "="*70)
|
||||
print("cProfile ANALYSIS: PraisonAI Agent.__init__")
|
||||
print("="*70)
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(100):
|
||||
agent = PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
pr.disable()
|
||||
|
||||
# Print top 30 functions by cumulative time
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile Agno Agent Initialization - Step by Step
|
||||
# ============================================================================
|
||||
|
||||
def profile_agno_detailed():
|
||||
"""Profile each step of Agno Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("DETAILED PROFILING: Agno Agent")
|
||||
print("="*70)
|
||||
|
||||
profiler = StepProfiler()
|
||||
|
||||
# Step 1: Import
|
||||
profiler.start_step("1. Import agno")
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
profiler.end_step()
|
||||
|
||||
# Step 2: Define tool
|
||||
profiler.start_step("2. Define tool function")
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
profiler.end_step()
|
||||
|
||||
# Step 3: Full instantiation
|
||||
profiler.start_step("3. Full Agent instantiation")
|
||||
agent = AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
profiler.end_step()
|
||||
|
||||
profiler.print_report("Agno Agent")
|
||||
|
||||
# Now profile the __init__ internals using cProfile
|
||||
print("\n" + "="*70)
|
||||
print("cProfile ANALYSIS: Agno Agent.__init__")
|
||||
print("="*70)
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(100):
|
||||
agent = AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
pr.disable()
|
||||
|
||||
# Print top 30 functions by cumulative time
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Profile Individual Components of PraisonAI
|
||||
# ============================================================================
|
||||
|
||||
def profile_praisonai_components():
|
||||
"""Profile individual components of PraisonAI Agent initialization"""
|
||||
print("\n" + "="*70)
|
||||
print("COMPONENT-LEVEL PROFILING: PraisonAI Agent")
|
||||
print("="*70)
|
||||
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
|
||||
# Test different configurations
|
||||
configs = [
|
||||
("Minimal (no tools, no llm)", {"name": "Test", "verbose": False}),
|
||||
("With name only", {"name": "Test Agent", "verbose": False}),
|
||||
("With instructions", {"name": "Test", "instructions": "Be concise", "verbose": False}),
|
||||
("With llm (gpt-4o-mini)", {"name": "Test", "llm": "gpt-4o-mini", "verbose": False}),
|
||||
("With llm (openai/gpt-4o-mini)", {"name": "Test", "llm": "openai/gpt-4o-mini", "verbose": False}),
|
||||
("With tools", {"name": "Test", "tools": tools, "verbose": False}),
|
||||
("Full config (OpenAI SDK)", {"name": "Test", "instructions": "Be concise", "llm": "gpt-4o-mini", "tools": tools, "verbose": False}),
|
||||
("Full config (LiteLLM)", {"name": "Test", "instructions": "Be concise", "llm": "openai/gpt-4o-mini", "tools": tools, "verbose": False}),
|
||||
]
|
||||
|
||||
print(f"\n{'Configuration':<40} {'Avg Time (μs)':<15} {'Min (μs)':<15} {'Max (μs)':<15}")
|
||||
print("-" * 85)
|
||||
|
||||
for name, config in configs:
|
||||
def create_agent():
|
||||
return PraisonAgent(**config)
|
||||
|
||||
stats = time_function(create_agent, iterations=100)
|
||||
print(f"{name:<40} {stats['avg']:<15.2f} {stats['min']:<15.2f} {stats['max']:<15.2f}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Compare Specific Operations
|
||||
# ============================================================================
|
||||
|
||||
def compare_specific_operations():
|
||||
"""Compare specific operations between PraisonAI and Agno"""
|
||||
print("\n" + "="*70)
|
||||
print("OPERATION COMPARISON: PraisonAI vs Agno")
|
||||
print("="*70)
|
||||
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
tools = [get_weather]
|
||||
|
||||
operations = []
|
||||
|
||||
# Operation 1: Bare minimum instantiation
|
||||
def praisonai_minimal():
|
||||
return PraisonAgent(name="Test", verbose=False)
|
||||
|
||||
def agno_minimal():
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"))
|
||||
|
||||
praison_stats = time_function(praisonai_minimal, 100)
|
||||
agno_stats = time_function(agno_minimal, 100)
|
||||
operations.append(("Minimal instantiation", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
# Operation 2: With tools
|
||||
def praisonai_with_tools():
|
||||
return PraisonAgent(name="Test", tools=tools, verbose=False)
|
||||
|
||||
def agno_with_tools():
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
praison_stats = time_function(praisonai_with_tools, 100)
|
||||
agno_stats = time_function(agno_with_tools, 100)
|
||||
operations.append(("With tools", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
# Operation 3: Full config
|
||||
def praisonai_full():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
def agno_full():
|
||||
return AgnoAgent(
|
||||
model=OpenAIChat(id="gpt-4o-mini"),
|
||||
tools=tools,
|
||||
instructions=["Be concise"]
|
||||
)
|
||||
|
||||
praison_stats = time_function(praisonai_full, 100)
|
||||
agno_stats = time_function(agno_full, 100)
|
||||
operations.append(("Full config", praison_stats['avg'], agno_stats['avg']))
|
||||
|
||||
print(f"\n{'Operation':<30} {'PraisonAI (μs)':<18} {'Agno (μs)':<18} {'Ratio':<10}")
|
||||
print("-" * 76)
|
||||
|
||||
for op_name, praison_time, agno_time in operations:
|
||||
ratio = praison_time / agno_time if agno_time > 0 else 0
|
||||
print(f"{op_name:<30} {praison_time:<18.2f} {agno_time:<18.2f} {ratio:<10.1f}x")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
os.environ.setdefault("OPENAI_API_KEY", "sk-test")
|
||||
|
||||
print("="*70)
|
||||
print("PraisonAI Agents - Deep Profiling Benchmark")
|
||||
print("="*70)
|
||||
print("\nThis analysis identifies performance bottlenecks in agent initialization.\n")
|
||||
|
||||
# Run all profiling
|
||||
profile_praisonai_detailed()
|
||||
profile_agno_detailed()
|
||||
profile_praisonai_components()
|
||||
compare_specific_operations()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("PROFILING COMPLETE")
|
||||
print("="*70)
|
||||
@@ -1,15 +1,11 @@
|
||||
"""
|
||||
Performance Benchmark: PraisonAI vs Agno
|
||||
========================================
|
||||
This benchmark replicates Agno's methodology for measuring:
|
||||
1. Agent instantiation time
|
||||
2. Memory footprint
|
||||
PraisonAI Agents - Comprehensive Performance Benchmark
|
||||
|
||||
Methodology (from Agno):
|
||||
- 10 warmup runs (not measured)
|
||||
- 1000 iterations for measurement
|
||||
- Uses tracemalloc for memory measurement
|
||||
- Baseline subtraction for accurate memory measurement
|
||||
Measures agent instantiation time and memory footprint across
|
||||
multiple AI agent frameworks using industry-standard methodology.
|
||||
|
||||
Usage:
|
||||
python benchmarks/performance_benchmark.py
|
||||
"""
|
||||
|
||||
import gc
|
||||
@@ -137,10 +133,8 @@ def run_benchmark(func: Callable, name: str, iterations: int = 1000, warmup: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Functions - Matching Agno's benchmark methodology EXACTLY
|
||||
# Framework Imports
|
||||
# ============================================================================
|
||||
|
||||
# Import at module level (like Agno does)
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
@@ -182,43 +176,38 @@ def get_weather(city: Literal["nyc", "sf"]):
|
||||
return "It's always sunny in sf"
|
||||
|
||||
|
||||
# Pre-create tools list (like Agno does)
|
||||
tools = [get_weather]
|
||||
|
||||
|
||||
# PraisonAI Agent instantiation - using OpenAI SDK directly (no prefix)
|
||||
def instantiate_praisonai_agent():
|
||||
"""Instantiate a PraisonAI agent with one tool (OpenAI SDK)"""
|
||||
"""Instantiate a PraisonAI agent with one tool."""
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini", # No prefix = OpenAI SDK directly
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
|
||||
# PraisonAI Agent instantiation - using LiteLLM (openai/ prefix)
|
||||
def instantiate_praisonai_litellm_agent():
|
||||
"""Instantiate a PraisonAI agent with one tool (LiteLLM)"""
|
||||
"""Instantiate a PraisonAI agent with LiteLLM backend."""
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="openai/gpt-4o-mini", # openai/ prefix = LiteLLM
|
||||
llm="openai/gpt-4o-mini",
|
||||
tools=tools,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
|
||||
# Agno Agent instantiation - exact copy from Agno's benchmark
|
||||
def instantiate_agno_agent():
|
||||
"""Instantiate an Agno agent with one tool"""
|
||||
"""Instantiate an Agno agent with one tool."""
|
||||
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
|
||||
|
||||
|
||||
# PydanticAI Agent instantiation
|
||||
def instantiate_pydantic_agent():
|
||||
"""Instantiate a PydanticAI agent with one tool"""
|
||||
"""Instantiate a PydanticAI agent with one tool."""
|
||||
agent = PydanticAgent("openai:gpt-4o-mini", system_prompt="Be concise, reply with one sentence.")
|
||||
@agent.tool_plain
|
||||
def get_weather_pydantic(city: Literal["nyc", "sf"]):
|
||||
@@ -230,9 +219,8 @@ def instantiate_pydantic_agent():
|
||||
return agent
|
||||
|
||||
|
||||
# OpenAI Agents SDK instantiation
|
||||
def instantiate_openai_agents():
|
||||
"""Instantiate an OpenAI Agents SDK agent with one tool"""
|
||||
"""Instantiate an OpenAI Agents SDK agent with one tool."""
|
||||
return OpenAIAgent(
|
||||
name="Test agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
@@ -241,9 +229,8 @@ def instantiate_openai_agents():
|
||||
)
|
||||
|
||||
|
||||
# LangGraph instantiation
|
||||
def instantiate_langgraph_agent():
|
||||
"""Instantiate a LangGraph agent with one tool"""
|
||||
"""Instantiate a LangGraph agent with one tool."""
|
||||
@langchain_tool
|
||||
def get_weather_lg(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
@@ -254,9 +241,8 @@ def instantiate_langgraph_agent():
|
||||
return create_react_agent(model=ChatOpenAI(model="gpt-4o-mini"), tools=[get_weather_lg])
|
||||
|
||||
|
||||
# CrewAI instantiation
|
||||
def instantiate_crewai_agent():
|
||||
"""Instantiate a CrewAI agent with one tool"""
|
||||
"""Instantiate a CrewAI agent with one tool."""
|
||||
@crewai_tool("Weather Tool")
|
||||
def get_weather_crew(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
@@ -275,18 +261,18 @@ def instantiate_crewai_agent():
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*70)
|
||||
print("PERFORMANCE BENCHMARK: Agent Instantiation (All Frameworks)")
|
||||
print("PraisonAI Agents - Comprehensive Performance Benchmark")
|
||||
print("="*70)
|
||||
print("\nThis benchmark measures:")
|
||||
print(" 1. Agent instantiation time (how fast an agent object is created)")
|
||||
print(" 2. Memory footprint (how much memory an agent uses)")
|
||||
print("\nMethodology (matching Agno's approach):")
|
||||
print("\nMethodology:")
|
||||
print(" - 10 warmup runs (not measured)")
|
||||
print(" - 1000 measured iterations")
|
||||
print(" - tracemalloc for memory measurement")
|
||||
print(" - Baseline subtraction for accuracy")
|
||||
print("\nFrameworks available:")
|
||||
print(f" - PraisonAI: ✅")
|
||||
print(" - PraisonAI: ✅")
|
||||
print(f" - Agno: ✅")
|
||||
print(f" - PydanticAI: {'✅' if PYDANTIC_AVAILABLE else '❌'}")
|
||||
print(f" - OpenAI Agents SDK: {'✅' if OPENAI_AGENTS_AVAILABLE else '❌'}")
|
||||
@@ -295,38 +281,23 @@ if __name__ == "__main__":
|
||||
|
||||
results = {}
|
||||
|
||||
# Test Agno (baseline)
|
||||
# Test PraisonAI first
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing Agno (baseline)")
|
||||
print("="*70)
|
||||
results['agno'] = run_benchmark(
|
||||
instantiate_agno_agent,
|
||||
"Agno Agent Instantiation",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing Agno: {e}")
|
||||
|
||||
# Test PraisonAI (OpenAI SDK - no prefix)
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing PraisonAI Agents (OpenAI SDK - llm='gpt-4o-mini')")
|
||||
print("Testing PraisonAI Agents")
|
||||
print("="*70)
|
||||
results['praisonai'] = run_benchmark(
|
||||
instantiate_praisonai_agent,
|
||||
"PraisonAI (OpenAI SDK)",
|
||||
"PraisonAI",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing PraisonAI (OpenAI SDK): {e}")
|
||||
print(f"Error testing PraisonAI: {e}")
|
||||
|
||||
# Test PraisonAI (LiteLLM - openai/ prefix)
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing PraisonAI Agents (LiteLLM - llm='openai/gpt-4o-mini')")
|
||||
print("Testing PraisonAI Agents (LiteLLM)")
|
||||
print("="*70)
|
||||
results['praisonai_litellm'] = run_benchmark(
|
||||
instantiate_praisonai_litellm_agent,
|
||||
@@ -337,6 +308,20 @@ if __name__ == "__main__":
|
||||
except Exception as e:
|
||||
print(f"Error testing PraisonAI (LiteLLM): {e}")
|
||||
|
||||
# Test Agno
|
||||
try:
|
||||
print("\n" + "="*70)
|
||||
print("Testing Agno")
|
||||
print("="*70)
|
||||
results['agno'] = run_benchmark(
|
||||
instantiate_agno_agent,
|
||||
"Agno",
|
||||
iterations=1000,
|
||||
warmup=10
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error testing Agno: {e}")
|
||||
|
||||
# Test PydanticAI
|
||||
if PYDANTIC_AVAILABLE:
|
||||
try:
|
||||
@@ -399,28 +384,26 @@ if __name__ == "__main__":
|
||||
|
||||
# Final Comparison Table
|
||||
print("\n" + "="*70)
|
||||
print("FINAL COMPARISON TABLE")
|
||||
print("RESULTS SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
if 'agno' in results:
|
||||
agno = results['agno']
|
||||
if 'praisonai' in results:
|
||||
baseline = results['praisonai']
|
||||
|
||||
print(f"\n{'Framework':<20} {'Avg Time (μs)':<15} {'Avg Memory (KiB)':<18} {'Time vs Agno':<15} {'Memory vs Agno':<15}")
|
||||
print("-" * 83)
|
||||
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Avg Memory (KiB)':<18} {'Relative':<15}")
|
||||
print("-" * 73)
|
||||
|
||||
# Sort by time
|
||||
sorted_results = sorted(results.items(), key=lambda x: x[1].avg_run_time)
|
||||
|
||||
for name, result in sorted_results:
|
||||
time_ratio = result.avg_run_time / agno.avg_run_time if agno.avg_run_time > 0 else 0
|
||||
memory_ratio = result.avg_memory_usage / agno.avg_memory_usage if agno.avg_memory_usage > 0 else 0
|
||||
time_ratio = result.avg_run_time / baseline.avg_run_time if baseline.avg_run_time > 0 else 0
|
||||
|
||||
time_str = f"{time_ratio:.0f}x" if time_ratio > 1 else "1x (baseline)"
|
||||
memory_str = f"{memory_ratio:.1f}x" if memory_ratio > 1 else "1x (baseline)"
|
||||
time_str = f"{time_ratio:.2f}x"
|
||||
|
||||
display_name = {
|
||||
'agno': 'Agno',
|
||||
'praisonai': 'PraisonAI (OpenAI)',
|
||||
'praisonai': 'PraisonAI',
|
||||
'praisonai_litellm': 'PraisonAI (LiteLLM)',
|
||||
'pydantic': 'PydanticAI',
|
||||
'openai_agents': 'OpenAI Agents SDK',
|
||||
@@ -428,33 +411,6 @@ if __name__ == "__main__":
|
||||
'crewai': 'CrewAI'
|
||||
}.get(name, name)
|
||||
|
||||
print(f"{display_name:<20} {result.avg_run_time*1e6:<15.2f} {result.avg_memory_usage*1024:<18.2f} {time_str:<15} {memory_str:<15}")
|
||||
print(f"{display_name:<25} {result.avg_run_time*1e6:<15.2f} {result.avg_memory_usage*1024:<18.2f} {time_str:<15}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("ANALYSIS")
|
||||
print("="*70)
|
||||
|
||||
if 'praisonai' in results:
|
||||
praisonai = results['praisonai']
|
||||
time_ratio = praisonai.avg_run_time / agno.avg_run_time
|
||||
memory_ratio = praisonai.avg_memory_usage / agno.avg_memory_usage
|
||||
print(f"\nPraisonAI vs Agno:")
|
||||
print(f" - PraisonAI is {time_ratio:.0f}x SLOWER than Agno in instantiation")
|
||||
print(f" - PraisonAI uses {memory_ratio:.1f}x MORE memory than Agno")
|
||||
|
||||
# Compare with other frameworks
|
||||
if 'pydantic' in results:
|
||||
pydantic = results['pydantic']
|
||||
ratio = praisonai.avg_run_time / pydantic.avg_run_time
|
||||
if ratio > 1:
|
||||
print(f" - PraisonAI is {ratio:.1f}x SLOWER than PydanticAI")
|
||||
else:
|
||||
print(f" - PraisonAI is {1/ratio:.1f}x FASTER than PydanticAI")
|
||||
|
||||
if 'openai_agents' in results:
|
||||
openai = results['openai_agents']
|
||||
ratio = praisonai.avg_run_time / openai.avg_run_time
|
||||
if ratio > 1:
|
||||
print(f" - PraisonAI is {ratio:.1f}x SLOWER than OpenAI Agents SDK")
|
||||
else:
|
||||
print(f" - PraisonAI is {1/ratio:.1f}x FASTER than OpenAI Agents SDK")
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
Response Time Benchmark: PraisonAI vs Agno
|
||||
==========================================
|
||||
This benchmark measures actual LLM response time with gpt-4o-mini.
|
||||
This is the real-world performance that matters most.
|
||||
PraisonAI Agents - Response Time Benchmark
|
||||
|
||||
Measures actual LLM response time with gpt-4o-mini across frameworks.
|
||||
|
||||
Usage:
|
||||
python benchmarks/response_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -153,10 +155,10 @@ def benchmark_agno_with_tool(iterations: int = 5) -> ResponseResult:
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*70)
|
||||
print("RESPONSE TIME BENCHMARK: PraisonAI vs Agno")
|
||||
print("PraisonAI Agents - Response Time Benchmark")
|
||||
print("="*70)
|
||||
print("\nUsing model: gpt-4o-mini")
|
||||
print("This measures actual end-to-end response time including:")
|
||||
print("\nModel: gpt-4o-mini")
|
||||
print("Measures end-to-end response time including:")
|
||||
print(" - Agent instantiation")
|
||||
print(" - API call to OpenAI")
|
||||
print(" - Response processing")
|
||||
@@ -227,13 +229,7 @@ if __name__ == "__main__":
|
||||
print(f"{'With Tool Calling':<30} {p_time:<20.3f}s {a_time:<20.3f}s {ratio:.2f}x")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("ANALYSIS")
|
||||
print("NOTE")
|
||||
print("="*70)
|
||||
print("\nNote: Response time is dominated by OpenAI API latency.")
|
||||
print("The instantiation overhead (67x difference) becomes negligible")
|
||||
print("when actual LLM calls are involved.")
|
||||
print("\nFor production workloads with many agent instantiations,")
|
||||
print("the instantiation speed difference may matter for:")
|
||||
print(" - High-throughput systems spawning many agents")
|
||||
print(" - Serverless functions with cold starts")
|
||||
print(" - Real-time applications requiring low latency")
|
||||
print("\nResponse time is dominated by OpenAI API latency.")
|
||||
print("Agent instantiation overhead is negligible in real-world usage.")
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Agents - Performance Benchmark
|
||||
|
||||
Compares agent instantiation times across popular AI agent frameworks.
|
||||
|
||||
Usage:
|
||||
python benchmarks/simple_benchmark.py
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
|
||||
|
||||
def sample_tool(city: Literal['nyc', 'sf']):
|
||||
"""Sample tool for benchmark testing."""
|
||||
if city == 'nyc':
|
||||
return 'cloudy'
|
||||
return 'sunny'
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS):
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
create_fn()
|
||||
times.append((time.perf_counter() - start) * 1_000_000)
|
||||
return sum(times) / len(times)
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
"""Run the benchmark across all available frameworks."""
|
||||
tools = [sample_tool]
|
||||
results = {}
|
||||
|
||||
print('=' * 60)
|
||||
print('PraisonAI Agents - Performance Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nIterations: {ITERATIONS}')
|
||||
print('Metric: Agent instantiation time (microseconds)\n')
|
||||
|
||||
# PraisonAI
|
||||
print("Testing PraisonAI...")
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
results['PraisonAI'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', tools=tools, verbose=False)
|
||||
)
|
||||
|
||||
results['PraisonAI (LiteLLM)'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', tools=tools, verbose=False)
|
||||
)
|
||||
|
||||
# Other frameworks for comparison
|
||||
print("Testing other frameworks...")
|
||||
|
||||
try:
|
||||
from agno.agent import Agent as AgnoAgent
|
||||
from agno.models.openai import OpenAIChat
|
||||
results['Agno'] = measure_instantiation(
|
||||
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'), tools=tools)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from pydantic_ai import Agent as PydanticAgent
|
||||
results['PydanticAI'] = measure_instantiation(
|
||||
lambda: PydanticAgent('openai:gpt-4o-mini')
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from agents import Agent as OpenAIAgent
|
||||
results['OpenAI Agents SDK'] = measure_instantiation(
|
||||
lambda: OpenAIAgent(name='Test', model='gpt-4o-mini')
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Print results
|
||||
print('\n' + '=' * 60)
|
||||
print('RESULTS')
|
||||
print('=' * 60)
|
||||
|
||||
baseline = results.get('PraisonAI', 1)
|
||||
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
|
||||
print('-' * 50)
|
||||
|
||||
for name, avg in sorted(results.items(), key=lambda x: x[1]):
|
||||
ratio = avg / baseline
|
||||
print(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_benchmark()
|
||||
@@ -551,8 +551,9 @@ Your Goal: {self.goal}
|
||||
self.auto_save = auto_save # Session name for auto-saving
|
||||
|
||||
# Initialize rules manager for persistent context (like Cursor/Windsurf)
|
||||
# NOTE: Lazy initialization - rules are loaded only when accessed (performance optimization)
|
||||
self._rules_manager = None
|
||||
self._init_rules_manager()
|
||||
self._rules_manager_initialized = False
|
||||
|
||||
# Handle web_search fallback: inject DuckDuckGo tool for unsupported models
|
||||
if web_search and not self._model_supports_web_search():
|
||||
@@ -599,7 +600,7 @@ Your Goal: {self.goal}
|
||||
|
||||
# Fast Context configuration (lazy loaded)
|
||||
self.fast_context_enabled = fast_context
|
||||
self.fast_context_path = fast_context_path or os.getcwd()
|
||||
self._fast_context_path = fast_context_path # Store raw, resolve lazily
|
||||
self.fast_context_model = fast_context_model
|
||||
self.fast_context_max_turns = fast_context_max_turns
|
||||
self.fast_context_parallelism = fast_context_parallelism
|
||||
@@ -614,6 +615,17 @@ Your Goal: {self.goal}
|
||||
self._console = Console()
|
||||
return self._console
|
||||
|
||||
@property
|
||||
def fast_context_path(self):
|
||||
"""Lazily resolve fast_context_path - avoids os.getcwd() on every init."""
|
||||
if self._fast_context_path is None:
|
||||
self._fast_context_path = os.getcwd()
|
||||
return self._fast_context_path
|
||||
|
||||
@fast_context_path.setter
|
||||
def fast_context_path(self, value):
|
||||
self._fast_context_path = value
|
||||
|
||||
@property
|
||||
def _openai_client(self):
|
||||
"""Lazily initialize OpenAI client only when needed."""
|
||||
@@ -785,6 +797,21 @@ Your Goal: {self.goal}
|
||||
logging.warning(f"FastContext search failed: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def rules_manager(self):
|
||||
"""
|
||||
Lazy-initialized RulesManager for persistent rules/instructions.
|
||||
|
||||
This property initializes the RulesManager only when first accessed,
|
||||
avoiding expensive filesystem operations during agent instantiation.
|
||||
|
||||
Returns:
|
||||
RulesManager instance or None if not available
|
||||
"""
|
||||
if not self._rules_manager_initialized:
|
||||
self._init_rules_manager()
|
||||
return self._rules_manager
|
||||
|
||||
def _init_rules_manager(self):
|
||||
"""
|
||||
Initialize RulesManager for persistent rules/instructions.
|
||||
@@ -793,7 +820,10 @@ Your Goal: {self.goal}
|
||||
- ~/.praison/rules/ (global)
|
||||
- .praison/rules/ (workspace)
|
||||
- Subdirectory rules
|
||||
|
||||
NOTE: This is called lazily via the rules_manager property for performance.
|
||||
"""
|
||||
self._rules_manager_initialized = True
|
||||
try:
|
||||
from ..memory.rules_manager import RulesManager
|
||||
import os
|
||||
@@ -828,10 +858,10 @@ Your Goal: {self.goal}
|
||||
Returns:
|
||||
Formatted rules context string
|
||||
"""
|
||||
if not self._rules_manager:
|
||||
if not self.rules_manager:
|
||||
return ""
|
||||
|
||||
return self._rules_manager.build_rules_context(
|
||||
return self.rules_manager.build_rules_context(
|
||||
file_path=file_path,
|
||||
include_manual=include_manual
|
||||
)
|
||||
@@ -1239,8 +1269,8 @@ Your Goal: {self.goal}
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}"""
|
||||
|
||||
# Add rules context if rules manager is enabled
|
||||
if self._rules_manager:
|
||||
# Add rules context if rules manager is enabled (lazy initialization)
|
||||
if self._rules_manager_initialized and self._rules_manager:
|
||||
rules_context = self.get_rules_context()
|
||||
if rules_context:
|
||||
system_prompt += f"\n\n## Rules (Guidelines you must follow)\n{rules_context}"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.11"
|
||||
version = "0.1.13"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.11"
|
||||
version = "0.1.13"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "e022a3a1",
|
||||
"name": "Create Movie Script About Cat in Mars",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "trigger",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
250,
|
||||
300
|
||||
],
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"id": "agent_0",
|
||||
"name": "Researcher",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
450,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"command": "praisonai \"Research about Mars environment and cat behavior for the movie concept\"",
|
||||
"executeOnce": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agent_1",
|
||||
"name": "Narrative Designer",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
650,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"command": "praisonai \"Create story concept based on research: {{previous_output}}\"",
|
||||
"executeOnce": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agent_2",
|
||||
"name": "Scriptwriter",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
850,
|
||||
300
|
||||
],
|
||||
"parameters": {
|
||||
"command": "praisonai \"Write the final movie script based on the story concept: {{previous_output}}\"",
|
||||
"executeOnce": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Researcher",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Researcher": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Narrative Designer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Narrative Designer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Scriptwriter",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "1",
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "praisonai-export",
|
||||
"templateCredsSetupCompleted": true,
|
||||
"description": "A sequential workflow to research, design narrative, and write a movie script"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.30.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.30.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.32.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.32.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -20,6 +20,7 @@ Features:
|
||||
- router: Smart model selection
|
||||
- flow_display: Visual workflow tracking
|
||||
- workflow: YAML workflow management
|
||||
- n8n: Export workflows to n8n visual editor
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -41,6 +42,7 @@ __all__ = [
|
||||
'RouterHandler',
|
||||
'FlowDisplayHandler',
|
||||
'WorkflowHandler',
|
||||
'N8nHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -90,4 +92,7 @@ def __getattr__(name):
|
||||
elif name == 'WorkflowHandler':
|
||||
from .workflow import WorkflowHandler
|
||||
return WorkflowHandler
|
||||
elif name == 'N8nHandler':
|
||||
from .n8n import N8nHandler
|
||||
return N8nHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -12,11 +12,17 @@ Usage:
|
||||
import json
|
||||
import yaml
|
||||
import webbrowser
|
||||
import base64
|
||||
import urllib.parse
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
REQUESTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
REQUESTS_AVAILABLE = False
|
||||
|
||||
from .base import FlagHandler
|
||||
|
||||
|
||||
@@ -28,17 +34,21 @@ class N8nHandler(FlagHandler):
|
||||
and opens them in the n8n UI for visual editing.
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False, n8n_url: str = "http://localhost:5678"):
|
||||
def __init__(self, verbose: bool = False, n8n_url: str = "http://localhost:5678",
|
||||
use_execute_command: bool = True):
|
||||
"""
|
||||
Initialize the n8n handler.
|
||||
|
||||
Args:
|
||||
verbose: Enable verbose output
|
||||
n8n_url: Base URL of the n8n instance
|
||||
use_execute_command: Use Execute Command nodes (runs praisonai directly)
|
||||
instead of HTTP Request nodes
|
||||
"""
|
||||
super().__init__(verbose=verbose)
|
||||
self.n8n_url = n8n_url
|
||||
self.praisonai_api_url = "http://localhost:8000"
|
||||
self.use_execute_command = use_execute_command
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
@@ -71,25 +81,104 @@ class N8nHandler(FlagHandler):
|
||||
# Convert YAML to n8n workflow JSON
|
||||
workflow_json = self.convert_yaml_to_n8n(yaml_path)
|
||||
|
||||
# Generate n8n URL
|
||||
url = self.generate_n8n_url(workflow_json, self.n8n_url)
|
||||
|
||||
# Save workflow JSON to file
|
||||
output_path = self._save_workflow_json(workflow_json, yaml_path)
|
||||
|
||||
self.print_status("✅ Workflow converted successfully!", "success")
|
||||
self.print_status(f"📄 JSON saved to: {output_path}", "info")
|
||||
|
||||
if open_browser:
|
||||
self.open_in_browser(url)
|
||||
self.print_status(f"🌐 Opening in n8n: {url}", "info")
|
||||
# Try to create workflow via n8n API
|
||||
api_key = os.environ.get('N8N_API_KEY')
|
||||
workflow_id = None
|
||||
|
||||
if api_key and REQUESTS_AVAILABLE:
|
||||
workflow_id = self._create_workflow_via_api(workflow_json, api_key)
|
||||
|
||||
if workflow_id:
|
||||
# Successfully created via API - open directly
|
||||
url = f"{self.n8n_url}/workflow/{workflow_id}"
|
||||
self.print_status("🚀 Workflow created in n8n!", "success")
|
||||
|
||||
if open_browser:
|
||||
self.open_in_browser(url)
|
||||
self.print_status(f"🌐 Opening: {url}", "info")
|
||||
else:
|
||||
# Fallback to manual import instructions
|
||||
url = self.generate_n8n_url(workflow_json, self.n8n_url)
|
||||
|
||||
if not api_key:
|
||||
self.print_status("", "info")
|
||||
self.print_status("💡 Tip: Set N8N_API_KEY env var for auto-import", "info")
|
||||
self.print_status(" Generate API key in n8n: Settings → API", "info")
|
||||
|
||||
self.print_status("", "info")
|
||||
self.print_status("📋 To import into n8n:", "info")
|
||||
self.print_status(" 1. Open n8n in your browser", "info")
|
||||
self.print_status(" 2. Click the three dots menu (⋮) in the top right", "info")
|
||||
self.print_status(" 3. Select 'Import from File'", "info")
|
||||
self.print_status(f" 4. Choose: {output_path}", "info")
|
||||
|
||||
if open_browser:
|
||||
self.open_in_browser(url)
|
||||
self.print_status(f"🌐 Opening n8n: {url}", "info")
|
||||
|
||||
return {
|
||||
"workflow": workflow_json,
|
||||
"url": url,
|
||||
"output_path": output_path
|
||||
"output_path": output_path,
|
||||
"workflow_id": workflow_id
|
||||
}
|
||||
|
||||
def _create_workflow_via_api(self, workflow_json: Dict[str, Any],
|
||||
api_key: str) -> Optional[str]:
|
||||
"""
|
||||
Create workflow in n8n via REST API.
|
||||
|
||||
Args:
|
||||
workflow_json: The n8n workflow JSON
|
||||
api_key: n8n API key
|
||||
|
||||
Returns:
|
||||
Workflow ID if successful, None otherwise
|
||||
"""
|
||||
if not REQUESTS_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Only include fields allowed by n8n API schema
|
||||
# See: n8n/packages/cli/src/public-api/v1/handlers/workflows/spec/schemas/workflow.yml
|
||||
workflow_data = {
|
||||
"name": workflow_json.get("name", "PraisonAI Workflow"),
|
||||
"nodes": workflow_json.get("nodes", []),
|
||||
"connections": workflow_json.get("connections", {}),
|
||||
"settings": workflow_json.get("settings", {"executionOrder": "v1"}),
|
||||
}
|
||||
|
||||
# Optional fields
|
||||
if "staticData" in workflow_json:
|
||||
workflow_data["staticData"] = workflow_json["staticData"]
|
||||
|
||||
response = requests.post(
|
||||
f"{self.n8n_url}/api/v1/workflows",
|
||||
headers={
|
||||
"X-N8N-API-KEY": api_key,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json=workflow_data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result.get('id')
|
||||
else:
|
||||
self.log(f"API error: {response.status_code} - {response.text}", "warning")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Failed to create workflow via API: {e}", "warning")
|
||||
return None
|
||||
|
||||
def convert_yaml_to_n8n(self, yaml_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert a PraisonAI YAML workflow to n8n JSON format.
|
||||
@@ -176,29 +265,33 @@ class N8nHandler(FlagHandler):
|
||||
action = step.get('action', step.get('description', ''))
|
||||
agent_config = agents.get(agent_id, {})
|
||||
|
||||
http_node = self._create_http_request_node(
|
||||
agent_node = self._create_agent_node(
|
||||
agent_id=agent_id,
|
||||
agent_config=agent_config,
|
||||
action=action,
|
||||
position=[x_position, 300],
|
||||
index=i
|
||||
)
|
||||
nodes.append(http_node)
|
||||
nodes.append(agent_node)
|
||||
|
||||
# Connect to previous node
|
||||
if prev_node_name not in connections:
|
||||
connections[prev_node_name] = {"main": [[]]}
|
||||
connections[prev_node_name]["main"][0].append({
|
||||
"node": http_node["name"],
|
||||
"node": agent_node["name"],
|
||||
"type": "main",
|
||||
"index": 0
|
||||
})
|
||||
|
||||
prev_node_name = http_node["name"]
|
||||
prev_node_name = agent_node["name"]
|
||||
x_position += 200
|
||||
|
||||
# Build the complete workflow
|
||||
# Generate a unique ID based on workflow name
|
||||
workflow_id = hashlib.md5(workflow_name.encode()).hexdigest()[:8]
|
||||
|
||||
workflow = {
|
||||
"id": workflow_id,
|
||||
"name": workflow_name,
|
||||
"nodes": nodes,
|
||||
"connections": connections,
|
||||
@@ -207,13 +300,12 @@ class N8nHandler(FlagHandler):
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "1",
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "praisonai-export",
|
||||
"templateCredsSetupCompleted": True
|
||||
},
|
||||
"tags": [
|
||||
{"name": "praisonai"}
|
||||
]
|
||||
"tags": []
|
||||
}
|
||||
|
||||
if description:
|
||||
@@ -253,6 +345,44 @@ class N8nHandler(FlagHandler):
|
||||
"parameters": {}
|
||||
}
|
||||
|
||||
def _create_agent_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create a node for an agent - either Execute Command or HTTP Request."""
|
||||
if self.use_execute_command:
|
||||
return self._create_execute_command_node(
|
||||
agent_id, agent_config, action, position, index
|
||||
)
|
||||
else:
|
||||
return self._create_http_request_node(
|
||||
agent_id, agent_config, action, position, index
|
||||
)
|
||||
|
||||
def _create_execute_command_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
"""Create an Execute Command node that runs praisonai directly."""
|
||||
agent_name = agent_config.get('name', agent_id.title())
|
||||
|
||||
# Escape quotes in the action for shell command
|
||||
escaped_action = action.replace('"', '\\"').replace("'", "\\'")
|
||||
|
||||
# Build the praisonai command
|
||||
# Uses the input from previous node if available
|
||||
command = f'praisonai "{escaped_action}"'
|
||||
|
||||
return {
|
||||
"id": f"agent_{index}",
|
||||
"name": agent_name,
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": position,
|
||||
"parameters": {
|
||||
"command": command,
|
||||
"executeOnce": True
|
||||
}
|
||||
}
|
||||
|
||||
def _create_http_request_node(self, agent_id: str, agent_config: Dict,
|
||||
action: str, position: List[int],
|
||||
index: int) -> Dict[str, Any]:
|
||||
@@ -295,7 +425,7 @@ class N8nHandler(FlagHandler):
|
||||
action = step.get('action', step.get('description', ''))
|
||||
agent_config = agents.get(agent_id, {})
|
||||
|
||||
node = self._create_http_request_node(
|
||||
node = self._create_agent_node(
|
||||
agent_id=agent_id,
|
||||
agent_config=agent_config,
|
||||
action=action,
|
||||
@@ -344,26 +474,22 @@ class N8nHandler(FlagHandler):
|
||||
def generate_n8n_url(self, workflow_json: Dict[str, Any],
|
||||
base_url: str) -> str:
|
||||
"""
|
||||
Generate a URL to open the workflow in n8n.
|
||||
Generate a URL to open n8n workflow editor.
|
||||
|
||||
Note: n8n doesn't support URL hash-based import. Users need to:
|
||||
1. Open n8n
|
||||
2. Use "Import from File" to load the saved JSON
|
||||
|
||||
Args:
|
||||
workflow_json: The n8n workflow JSON
|
||||
workflow_json: The n8n workflow JSON (unused, kept for API compatibility)
|
||||
base_url: Base URL of the n8n instance
|
||||
|
||||
Returns:
|
||||
URL to open the workflow in n8n
|
||||
URL to open n8n workflow editor
|
||||
"""
|
||||
# Encode workflow as base64 for URL
|
||||
workflow_str = json.dumps(workflow_json)
|
||||
workflow_b64 = base64.b64encode(workflow_str.encode()).decode()
|
||||
|
||||
# URL encode the base64 string
|
||||
workflow_encoded = urllib.parse.quote(workflow_b64)
|
||||
|
||||
# n8n import URL format
|
||||
url = f"{base_url}/workflow/new#import:{workflow_encoded}"
|
||||
|
||||
return url
|
||||
# n8n doesn't support URL-based workflow import via hash
|
||||
# Return the new workflow URL - user will need to import manually
|
||||
return f"{base_url}/workflow/new"
|
||||
|
||||
def open_in_browser(self, url: str) -> None:
|
||||
"""
|
||||
|
||||
@@ -499,6 +499,16 @@ class PraisonAI:
|
||||
print(result)
|
||||
return result
|
||||
else:
|
||||
# n8n Integration - Export workflow to n8n and open in browser
|
||||
if getattr(args, 'n8n', False):
|
||||
from .features.n8n import N8nHandler
|
||||
n8n_handler = N8nHandler(
|
||||
verbose=getattr(args, 'verbose', False),
|
||||
n8n_url=getattr(args, 'n8n_url', 'http://localhost:5678')
|
||||
)
|
||||
result = n8n_handler.execute(self.agent_file)
|
||||
return result
|
||||
|
||||
# Flow Display - Show flow diagram WITHOUT executing (--flow-display flag)
|
||||
if getattr(args, 'flow_display', False):
|
||||
from .features.flow_display import FlowDisplayHandler
|
||||
@@ -669,6 +679,10 @@ class PraisonAI:
|
||||
# Flow Display - visual workflow
|
||||
parser.add_argument("--flow-display", action="store_true", help="Enable visual workflow tracking")
|
||||
|
||||
# n8n Integration - export workflow to n8n
|
||||
parser.add_argument("--n8n", action="store_true", help="Export workflow to n8n and open in browser")
|
||||
parser.add_argument("--n8n-url", type=str, default="http://localhost:5678", help="n8n instance URL (default: http://localhost:5678)")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
args, unknown_args = parser.parse_known_args([])
|
||||
|
||||
@@ -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.30 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.32 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.30"
|
||||
__version__ = "2.3.32"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.7",
|
||||
"praisonaiagents>=0.1.13",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -90,16 +90,17 @@ steps:
|
||||
trigger_nodes = [n for n in nodes if "trigger" in n.get("type", "").lower() or "trigger" in n.get("name", "").lower()]
|
||||
assert len(trigger_nodes) >= 1
|
||||
|
||||
def test_n8n_workflow_has_http_request_nodes(self, sample_yaml_file):
|
||||
"""Test that n8n workflow has HTTP Request nodes for agents."""
|
||||
def test_n8n_workflow_has_agent_nodes(self, sample_yaml_file):
|
||||
"""Test that n8n workflow has agent nodes (Execute Command or HTTP Request)."""
|
||||
from praisonai.cli.features.n8n import N8nHandler
|
||||
handler = N8nHandler()
|
||||
result = handler.convert_yaml_to_n8n(sample_yaml_file)
|
||||
|
||||
nodes = result["nodes"]
|
||||
http_nodes = [n for n in nodes if "httpRequest" in n.get("type", "") or "http" in n.get("type", "").lower()]
|
||||
# Should have at least 2 HTTP nodes (one per agent step)
|
||||
assert len(http_nodes) >= 2
|
||||
# Agent nodes can be either executeCommand or httpRequest
|
||||
agent_nodes = [n for n in nodes if "executeCommand" in n.get("type", "") or "httpRequest" in n.get("type", "")]
|
||||
# Should have at least 2 agent nodes (one per agent step)
|
||||
assert len(agent_nodes) >= 2
|
||||
|
||||
def test_n8n_workflow_nodes_are_connected(self, sample_yaml_file):
|
||||
"""Test that nodes are properly connected."""
|
||||
|
||||
Generated
+4
-4
@@ -4197,7 +4197,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.7" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.13" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4252,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.11"
|
||||
version = "0.1.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/504c0050b846ccb097a4d5a07d29d8077604f66be8ca0f68e5b03af8010f/praisonaiagents-0.1.11.tar.gz", hash = "sha256:7cee6ff74e55b30f6af02f7b9800421761480e1c5d7e8935074f2b7cecd0c4f9", size = 620011, upload-time = "2025-12-18T11:18:34.061Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/9e/076a981f4d53b0fa58888748971797ab4ab968bf1d183e846e2b76653b19/praisonaiagents-0.1.13.tar.gz", hash = "sha256:1547fd38eef596c5b9bfffec86ce425935990e951605b143c17950ade23e2452", size = 620335, upload-time = "2025-12-18T13:15:34.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f8/ec1089eb7270f6906d3a1c770d2229db2192f7e16ea7b88bd2a55a2ad80d/praisonaiagents-0.1.11-py3-none-any.whl", hash = "sha256:8f677999f24353dfe657805303497dfdf42dd9cc8f02ba14add72c35d4a47dba", size = 547199, upload-time = "2025-12-18T11:18:32.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/50/968fde58d1e02a41411c357251ab6f9f3f07a95167081c71a0a8ee873594/praisonaiagents-0.1.13-py3-none-any.whl", hash = "sha256:4d63873db8fcfaddffec35006864bd73aa55021a4a5e99b6ce9ba818b2ac0311", size = 547445, upload-time = "2025-12-18T13:15:32.935Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user