Compare commits

...
1 Commits
Author SHA1 Message Date
Mervin Praison abcc5b3b04 Release v2.3.31 2025-12-18 12:57:05 +00:00
20 changed files with 852 additions and 163 deletions

No files matched your search

+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.30" \
"praisonai>=2.3.31" \
"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.30" \
"praisonai>=2.3.31" \
"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.30" \
"praisonai>=2.3.31" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Deep Profiling Benchmark
Profiles each step of agent initialization to identify performance bottlenecks.
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():
@@ -785,6 +786,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 +809,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 +847,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 +1258,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}"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.1.11"
version = "0.1.12"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.1.11"
version = "0.1.12"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+106
View File
@@ -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": []
}
+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.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.31.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.31.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}")
+158 -32
View File
@@ -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:
"""
+14
View File
@@ -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([])
+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.30 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.31 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.30"
__version__ = "2.3.31"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.1.7",
"praisonaiagents>=0.1.12",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+6 -5
View File
@@ -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."""
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.1.7" },
{ name = "praisonaiagents", specifier = ">=0.1.12" },
{ 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.12"
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/06/ea/0489a0f62da0ded124c7a4fe079d3c898537e7a4ec639dc774c8420855e1/praisonaiagents-0.1.12.tar.gz", hash = "sha256:9d0dcb64fa0fd642fef60939d4547c2aae07267bf894d565e0e9a8013dc8c035", size = 620248, upload-time = "2025-12-18T12:54:12.363Z" }
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/1b/71/fb62eb0745d56301f844c545fd66fa6f2933beacf9d4dd6a065fc38d9521/praisonaiagents-0.1.12-py3-none-any.whl", hash = "sha256:d02e676b9028b9420b49d784986812315b0f668fde0458980837de612a120791", size = 547369, upload-time = "2025-12-18T12:54:10.709Z" },
]
[[package]]