mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e9826bda7 | ||
|
|
fc298049e2 | ||
|
|
0b7bf10820 | ||
|
|
cbb7c726aa | ||
|
|
21d1986d0c | ||
|
|
26b5792192 | ||
|
|
afcea5800e | ||
|
|
98cfdcc9ad | ||
|
|
6f2df7db2a | ||
|
|
495d4604fe |
No files matched your search
@@ -0,0 +1,14 @@
|
||||
# Contributing to PraisonAI
|
||||
|
||||
Thank you for your interest! Here's how to get started:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Follow existing code style
|
||||
- Add tests for new features
|
||||
- Update documentation as needed
|
||||
@@ -12,10 +12,15 @@ from praisonaiagents import Agent
|
||||
from praisonaiagents.approval import AgentApproval
|
||||
from praisonaiagents.tools.shell_tools import execute_command
|
||||
|
||||
reviewer = AgentApproval(
|
||||
reviewer_agent = Agent(
|
||||
name="CommandReviewer",
|
||||
instructions="Only approve read-only commands like 'ls' or 'cat'. Deny destructive commands.",
|
||||
)
|
||||
|
||||
reviewer = AgentApproval(
|
||||
approver_agent=reviewer_agent,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="DevOps",
|
||||
instructions="You are a DevOps assistant. Use shell tools when asked.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents.workflows.workflow_configs import (
|
||||
WorkflowOutputConfig, WorkflowPlanningConfig, WorkflowMemoryConfig,
|
||||
WorkflowPlanningConfig, WorkflowMemoryConfig,
|
||||
)
|
||||
|
||||
# Full-featured workflow with all consolidated params
|
||||
@@ -32,7 +32,7 @@ workflow = AgentFlow(
|
||||
),
|
||||
],
|
||||
# Workflow-level consolidated params
|
||||
output=WorkflowOutputConfig(output="verbose", stream=True),
|
||||
output="verbose",
|
||||
planning=WorkflowPlanningConfig(enabled=True, reasoning=True),
|
||||
memory=WorkflowMemoryConfig(backend="file"),
|
||||
# Agent-like params
|
||||
|
||||
@@ -10,7 +10,7 @@ from praisonaiagents import Agent
|
||||
# Basic: Enable web search with preset
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
web="search_only", # Compatible across models without web_fetch
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -80,7 +80,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
# Verify the answer contains the unique code
|
||||
if "ZEBRA-71" in response.upper():
|
||||
if "ZEBRA-71" in str(response or "").upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the unique code from context!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
@@ -104,7 +104,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
|
||||
response = agent.chat("What is the security incident code?")
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
if "HAWK-88" in response.upper():
|
||||
if "HAWK-88" in str(response or "").upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the security code!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
|
||||
@@ -17,8 +17,10 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="CockroachDB Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("COCKROACHDB_URL")},
|
||||
session_id="crdb-demo-session",
|
||||
memory={
|
||||
"session_id": "crdb-demo-session",
|
||||
"db": {"database_url": os.getenv("COCKROACHDB_URL")},
|
||||
},
|
||||
)
|
||||
|
||||
result = agent.start("What is distributed SQL? Explain in 2 sentences.")
|
||||
|
||||
@@ -18,8 +18,10 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Neon Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("NEON_DATABASE_URL")},
|
||||
session_id="neon-demo-session",
|
||||
memory={
|
||||
"session_id": "neon-demo-session",
|
||||
"db": {"database_url": os.getenv("NEON_DATABASE_URL")},
|
||||
},
|
||||
)
|
||||
|
||||
# Option 2: Direct URL
|
||||
|
||||
@@ -20,8 +20,10 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Supabase Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("SUPABASE_DATABASE_URL")},
|
||||
session_id="supabase-demo-session",
|
||||
memory={
|
||||
"session_id": "supabase-demo-session",
|
||||
"db": {"database_url": os.getenv("SUPABASE_DATABASE_URL")},
|
||||
},
|
||||
)
|
||||
|
||||
result = agent.start("What is Supabase? Explain in 2 sentences.")
|
||||
|
||||
@@ -18,11 +18,13 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Turso Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={
|
||||
"database_url": os.getenv("TURSO_DATABASE_URL"),
|
||||
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
|
||||
memory={
|
||||
"session_id": "turso-demo-session",
|
||||
"db": {
|
||||
"database_url": os.getenv("TURSO_DATABASE_URL"),
|
||||
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
|
||||
},
|
||||
},
|
||||
session_id="turso-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is edge computing? Explain in 2 sentences.")
|
||||
|
||||
@@ -10,8 +10,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="FlexBot",
|
||||
instructions="You are a helpful assistant.",
|
||||
llm="gpt-4o-mini",
|
||||
llm_config={"configurable": True}
|
||||
llm={"model": "gpt-4o-mini", "configurable": True},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -21,7 +21,7 @@ agent = Agent(
|
||||
name="ContextBot",
|
||||
instructions="You help show context information.",
|
||||
tools=[show_context],
|
||||
session_id="my-session-123"
|
||||
memory={"session_id": "my-session-123"},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -19,6 +19,16 @@ from praisonaiagents import Agent, AgentTeam
|
||||
# Initialize observability
|
||||
obs.init()
|
||||
|
||||
|
||||
def set_span_attr(span, key, value):
|
||||
"""Set span attribute safely across observability implementations."""
|
||||
if hasattr(span, "set_attribute"):
|
||||
span.set_attribute(key, value)
|
||||
return
|
||||
attrs = getattr(span, "attributes", None)
|
||||
if attrs is not None:
|
||||
attrs[key] = value
|
||||
|
||||
# Create agents
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
@@ -37,13 +47,13 @@ with obs.trace("content-creation", metadata={"workflow": "research-write"}):
|
||||
|
||||
# Research phase
|
||||
with obs.span("research-phase", kind=SpanKind.AGENT) as research_span:
|
||||
research_span.attributes["agent"] = "Researcher"
|
||||
set_span_attr(research_span, "agent", "Researcher")
|
||||
research_result = researcher.chat("Research the benefits of AI in healthcare")
|
||||
|
||||
# Writing phase
|
||||
with obs.span("writing-phase", kind=SpanKind.AGENT) as write_span:
|
||||
write_span.attributes["agent"] = "Writer"
|
||||
write_span.attributes["input_length"] = len(str(research_result))
|
||||
set_span_attr(write_span, "agent", "Writer")
|
||||
set_span_attr(write_span, "input_length", len(str(research_result)))
|
||||
final_content = writer.chat(f"Write a blog post based on: {research_result}")
|
||||
|
||||
print("Final Content:")
|
||||
|
||||
@@ -52,8 +52,10 @@ agent = Agent(
|
||||
instructions="You are a helpful assistant with access to technical documentation.",
|
||||
knowledge={
|
||||
"sources": [doc_path],
|
||||
"vector_store": "qdrant",
|
||||
"url": "http://localhost:6333",
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
"url": "http://localhost:6333",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ Expected output:
|
||||
- Session can be resumed later with same session_id
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
|
||||
# Create database connection (PostgreSQL + Redis)
|
||||
my_db = db(
|
||||
@@ -25,10 +26,12 @@ my_db = db(
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be concise.",
|
||||
db=my_db,
|
||||
# session_id is optional - defaults to per-hour ID (YYYYMMDDHH-hash)
|
||||
# Set explicitly for session continuity across runs:
|
||||
session_id="my-persistent-session"
|
||||
memory={
|
||||
"db": my_db,
|
||||
# session_id is optional - defaults to per-hour ID (YYYYMMDDHH-hash)
|
||||
# Set explicitly for session continuity across runs:
|
||||
"session_id": "my-persistent-session",
|
||||
},
|
||||
)
|
||||
|
||||
# Chat - messages are automatically persisted
|
||||
|
||||
@@ -12,7 +12,8 @@ Expected output:
|
||||
- Export shows full session data
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a city."""
|
||||
@@ -29,8 +30,10 @@ agent = Agent(
|
||||
name="WeatherBot",
|
||||
instructions="You help with weather. Use the get_weather tool when asked.",
|
||||
tools=[get_weather],
|
||||
db=my_db,
|
||||
session_id="weather-session-001"
|
||||
memory={
|
||||
"db": my_db,
|
||||
"session_id": "weather-session-001",
|
||||
},
|
||||
)
|
||||
|
||||
# First chat - triggers run tracking
|
||||
|
||||
@@ -13,7 +13,8 @@ Expected output:
|
||||
Agent responds with state persisted to Redis
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
|
||||
print("=== Redis State Store (Agent-First) ===")
|
||||
|
||||
@@ -26,8 +27,10 @@ my_db = db(
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
db=my_db,
|
||||
session_id="redis-state-session"
|
||||
memory={
|
||||
"db": my_db,
|
||||
"session_id": "redis-state-session",
|
||||
},
|
||||
)
|
||||
|
||||
# Chat - state is automatically managed via Redis
|
||||
|
||||
@@ -11,7 +11,8 @@ Expected output:
|
||||
- Second run: Resumes with history
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
|
||||
# Create database
|
||||
my_db = db(
|
||||
@@ -25,8 +26,10 @@ SESSION_ID = "resume-demo-session"
|
||||
agent = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You remember our conversation. Be helpful and concise.",
|
||||
db=my_db,
|
||||
session_id=SESSION_ID
|
||||
memory={
|
||||
"db": my_db,
|
||||
"session_id": SESSION_ID,
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is a new or resumed session
|
||||
|
||||
@@ -26,7 +26,8 @@ Expected Output:
|
||||
Messages persisted: 2
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
|
||||
# Create database adapter (simplified import)
|
||||
db_instance = db(
|
||||
@@ -37,8 +38,10 @@ db_instance = db(
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Keep responses brief.",
|
||||
db=db_instance,
|
||||
session_id="demo-session-001", # Same session_id = resume conversation
|
||||
memory={
|
||||
"db": db_instance,
|
||||
"session_id": "demo-session-001", # Same session_id = resume conversation
|
||||
},
|
||||
output="silent"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ Expected output:
|
||||
- Data persisted to local SQLite file
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, db
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
@@ -25,8 +26,10 @@ print(f"Database: {db_path}")
|
||||
agent = Agent(
|
||||
name="LocalBot",
|
||||
instructions="You are a helpful assistant.",
|
||||
db=my_db,
|
||||
session_id="local-session"
|
||||
memory={
|
||||
"db": my_db,
|
||||
"session_id": "local-session",
|
||||
},
|
||||
)
|
||||
|
||||
# Chat
|
||||
|
||||
@@ -57,8 +57,8 @@ def main():
|
||||
agent = Agent(
|
||||
name="SecureAgent",
|
||||
instructions="You are a file management assistant.",
|
||||
policy=engine
|
||||
)
|
||||
agent.policy = engine
|
||||
|
||||
print("\n--- Agent with Policy Enforcement Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
|
||||
@@ -60,9 +60,10 @@ agents_system = AgentTeam(
|
||||
process="sequential", output="verbose"
|
||||
)
|
||||
|
||||
print("Starting hierarchical agent generation...")
|
||||
result = agents_system.start()
|
||||
if __name__ == "__main__":
|
||||
print("Starting hierarchical agent generation...")
|
||||
result = agents_system.start()
|
||||
|
||||
print(f"\nHierarchical Result: {result[:200]}...")
|
||||
print("\n✅ AutoAgents hierarchical generation complete!")
|
||||
print("Demonstrated coordinated hierarchical workflow between manager and specialist agents.")
|
||||
print(f"\nHierarchical Result: {str(result)[:200]}...")
|
||||
print("\n✅ AutoAgents hierarchical generation complete!")
|
||||
print("Demonstrated coordinated hierarchical workflow between manager and specialist agents.")
|
||||
@@ -1,4 +1,5 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
|
||||
agent.launch(port=8080, protocol="mcp")
|
||||
if __name__ == "__main__":
|
||||
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
|
||||
agent.launch(port=8080, protocol="mcp", path="/")
|
||||
@@ -90,9 +90,18 @@ def main():
|
||||
|
||||
if result:
|
||||
print("\n🔍 Analysis Results:")
|
||||
for task_id, task_result in result["task_results"].items():
|
||||
task_results = result.get("task_results") if isinstance(result, dict) else result
|
||||
if isinstance(task_results, dict):
|
||||
iterable = task_results.items()
|
||||
elif isinstance(task_results, list):
|
||||
iterable = enumerate(task_results)
|
||||
else:
|
||||
iterable = [("result", task_results)]
|
||||
|
||||
for task_id, task_result in iterable:
|
||||
print(f"\nTask {task_id}:")
|
||||
print(task_result.raw)
|
||||
raw_value = getattr(task_result, "raw", None)
|
||||
print(raw_value if raw_value is not None else str(task_result))
|
||||
else:
|
||||
print("❌ Failed to capture and analyze camera feed")
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class ContinuousCameraMonitor:
|
||||
agents=[self.vision_agent],
|
||||
tasks=[task],
|
||||
process="sequential",
|
||||
verbose=0 # Reduced verbosity for continuous monitoring
|
||||
output="silent", # Reduced verbosity for continuous monitoring
|
||||
)
|
||||
|
||||
return agents.start()
|
||||
|
||||
@@ -148,10 +148,19 @@ def main():
|
||||
"scene_analysis": "🌍 SCENE ANALYSIS"
|
||||
}
|
||||
|
||||
for task_id, task_result in result["task_results"].items():
|
||||
task_results = result.get("task_results") if isinstance(result, dict) else result
|
||||
if isinstance(task_results, dict):
|
||||
iterable = task_results.items()
|
||||
elif isinstance(task_results, list):
|
||||
iterable = enumerate(task_results)
|
||||
else:
|
||||
iterable = [("result", task_results)]
|
||||
|
||||
for task_id, task_result in iterable:
|
||||
print(f"\n{agent_names.get(task_id, f'Task {task_id}')}:")
|
||||
print("-" * 40)
|
||||
print(task_result.raw)
|
||||
raw_value = getattr(task_result, "raw", None)
|
||||
print(raw_value if raw_value is not None else str(task_result))
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ Multi-agent analysis complete!")
|
||||
|
||||
@@ -34,5 +34,5 @@ print(f"\nChanged to: {handler.get_mode()}")
|
||||
print("File edits now auto-approved, shell commands still require approval")
|
||||
|
||||
# Get statistics
|
||||
stats = handler.get_stats()
|
||||
stats = manager.get_stats()
|
||||
print(f"\nStats: {stats}")
|
||||
@@ -31,4 +31,4 @@ print(f"Total cost: ${summary['total_cost']:.4f}")
|
||||
print(f"Avg cost/request: ${summary['avg_cost_per_request']:.4f}")
|
||||
|
||||
# Display formatted (uses Rich if available)
|
||||
handler.display_summary()
|
||||
handler.print_summary()
|
||||
@@ -41,9 +41,9 @@ def my_custom_handler(args, context):
|
||||
custom_cmd = SlashCommand(
|
||||
name="mycommand",
|
||||
description="My custom command",
|
||||
handler=my_custom_handler,
|
||||
kind=CommandKind.ACTION,
|
||||
aliases=["mc"]
|
||||
action=my_custom_handler,
|
||||
kind=CommandKind.CUSTOM,
|
||||
alt_names=["mc"]
|
||||
)
|
||||
|
||||
handler.register(custom_cmd)
|
||||
|
||||
@@ -16,7 +16,8 @@ agents = AgentTeam(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="workflow",
|
||||
max_iter=30
|
||||
execution={"max_iter": 30},
|
||||
)
|
||||
|
||||
agents.start()
|
||||
if __name__ == "__main__":
|
||||
agents.start()
|
||||
@@ -5,7 +5,8 @@ This example demonstrates self-reflection capabilities using PraisonAI's
|
||||
built-in reflection features for iterative improvement and quality optimization.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents import Agent, Task
|
||||
from praisonaiagents.agents import AgentTeam
|
||||
from praisonaiagents.tools import internet_search
|
||||
|
||||
print("=== Self-Reflection Optimization Patterns Example ===\n")
|
||||
@@ -33,9 +34,16 @@ reflection_task = Task(
|
||||
)
|
||||
|
||||
# Run with self-reflection optimization
|
||||
print("Starting self-reflection optimization demonstration...")
|
||||
result = reflection_agent.execute_task(reflection_task)
|
||||
if __name__ == "__main__":
|
||||
print("Starting self-reflection optimization demonstration...")
|
||||
agents = AgentTeam(
|
||||
agents=[reflection_agent],
|
||||
tasks=[reflection_task],
|
||||
process="sequential",
|
||||
output="verbose",
|
||||
)
|
||||
result = agents.start()
|
||||
|
||||
print(f"\nSelf-Reflection Result: {result[:200]}...")
|
||||
print("\n✅ Self-reflection optimization complete!")
|
||||
print("Agent demonstrated iterative improvement through built-in self-reflection capabilities.")
|
||||
print(f"\nSelf-Reflection Result: {str(result)[:200]}...")
|
||||
print("\n✅ Self-reflection optimization complete!")
|
||||
print("Agent demonstrated iterative improvement through built-in self-reflection capabilities.")
|
||||
@@ -61,7 +61,6 @@ def example_with_wrappers():
|
||||
role="Data Analyst",
|
||||
goal="Analyze data and provide insights",
|
||||
llm="gpt-4o-mini",
|
||||
request_id="request_2" # Unique request ID
|
||||
)
|
||||
|
||||
# Operations are automatically tracked
|
||||
|
||||
@@ -132,7 +132,9 @@ agents = AgentTeam(
|
||||
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
|
||||
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
|
||||
process="workflow",
|
||||
max_iter=30, output="minimal"
|
||||
execution={"max_iter": 30},
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
agents.start()
|
||||
if __name__ == "__main__":
|
||||
agents.start()
|
||||
@@ -18,7 +18,7 @@ openai_profile = AuthProfile(
|
||||
provider="openai",
|
||||
api_key=os.environ.get("OPENAI_API_KEY", "sk-..."),
|
||||
priority=1, # Highest priority
|
||||
rate_limit=100,
|
||||
rate_limit_rpm=100,
|
||||
)
|
||||
|
||||
anthropic_profile = AuthProfile(
|
||||
@@ -26,7 +26,7 @@ anthropic_profile = AuthProfile(
|
||||
provider="anthropic",
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY", "sk-ant-..."),
|
||||
priority=2, # Second priority
|
||||
rate_limit=50,
|
||||
rate_limit_rpm=50,
|
||||
)
|
||||
|
||||
groq_profile = AuthProfile(
|
||||
@@ -34,7 +34,7 @@ groq_profile = AuthProfile(
|
||||
provider="groq",
|
||||
api_key=os.environ.get("GROQ_API_KEY", "gsk-..."),
|
||||
priority=3, # Third priority (fast but limited)
|
||||
rate_limit=30,
|
||||
rate_limit_rpm=30,
|
||||
)
|
||||
|
||||
# Configure failover behavior
|
||||
|
||||
@@ -105,13 +105,13 @@ def main():
|
||||
agents=[researcher, retriever],
|
||||
tasks=[store_task, verify_task, query_task, query_both_task], # Use same verbose level as memory
|
||||
memory={
|
||||
"backend": "sqlite",
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "text-embedding-3-small"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {"provider": "sqlite"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -49,10 +49,20 @@ agents = AgentTeam(
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
# Run all tasks
|
||||
result = agents.start()
|
||||
if __name__ == "__main__":
|
||||
# Run all tasks
|
||||
result = agents.start()
|
||||
|
||||
# Print results
|
||||
for task_id, task_result in result["task_results"].items():
|
||||
print(f"\nTask {task_id} Result:")
|
||||
print(task_result.raw)
|
||||
# Print results
|
||||
task_results = result.get("task_results") if isinstance(result, dict) else result
|
||||
if isinstance(task_results, dict):
|
||||
iterable = task_results.items()
|
||||
elif isinstance(task_results, list):
|
||||
iterable = enumerate(task_results)
|
||||
else:
|
||||
iterable = [("result", task_results)]
|
||||
|
||||
for task_id, task_result in iterable:
|
||||
print(f"\nTask {task_id} Result:")
|
||||
raw_value = getattr(task_result, "raw", None)
|
||||
print(raw_value if raw_value is not None else str(task_result))
|
||||
@@ -6,6 +6,7 @@ built-in validation and safety mechanisms for secure agent operations.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.config.feature_configs import GuardrailConfig
|
||||
from praisonaiagents.tools import internet_search
|
||||
|
||||
print("=== Production Guardrails Patterns Example ===\n")
|
||||
@@ -38,8 +39,8 @@ production_agent = Agent(
|
||||
goal="Generate safe, compliant content for production use",
|
||||
backstory="Production-ready agent with built-in safety and compliance checks",
|
||||
tools=[internet_search],
|
||||
guardrails=content_guardrail,
|
||||
max_retries=3)
|
||||
guardrails=GuardrailConfig(validator=content_guardrail, max_retries=3),
|
||||
)
|
||||
|
||||
# Create secure task with validation
|
||||
secure_task = Task(
|
||||
|
||||
@@ -40,7 +40,7 @@ agent = Agent(
|
||||
"execute_command"
|
||||
],
|
||||
memory=True,
|
||||
web_search=True,
|
||||
web=True,
|
||||
auto_approve_tools=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,30 +14,33 @@ agent = Agent(name="env-mgmt", backend=managed)
|
||||
# First call creates agent + environment + session
|
||||
agent.start("Say hello briefly")
|
||||
|
||||
# Access the underlying Anthropic client for environment management
|
||||
client = managed._get_client()
|
||||
env_id = managed.environment_id
|
||||
print(f"Environment ID: {env_id}")
|
||||
if hasattr(managed, "_get_client"):
|
||||
# Access the underlying Anthropic client for environment management
|
||||
client = managed._get_client()
|
||||
env_id = managed.environment_id
|
||||
print(f"Environment ID: {env_id}")
|
||||
|
||||
# List all environments
|
||||
environments = client.beta.environments.list()
|
||||
print(f"\nTotal environments: {len(environments.data)}")
|
||||
for env in environments.data[:5]:
|
||||
print(f" {env.id} | {env.name} | {getattr(env, 'status', 'active')}")
|
||||
# List all environments
|
||||
environments = client.beta.environments.list()
|
||||
print(f"\nTotal environments: {len(environments.data)}")
|
||||
for env in environments.data[:5]:
|
||||
print(f" {env.id} | {env.name} | {getattr(env, 'status', 'active')}")
|
||||
|
||||
# Retrieve a specific environment
|
||||
env = client.beta.environments.retrieve(env_id)
|
||||
print(f"\nRetrieved: {env.id} | {env.name}")
|
||||
# Retrieve a specific environment
|
||||
env = client.beta.environments.retrieve(env_id)
|
||||
print(f"\nRetrieved: {env.id} | {env.name}")
|
||||
|
||||
# Archive (read-only, existing sessions continue)
|
||||
archived = client.beta.environments.archive(env_id)
|
||||
print(f"Archived: {archived.id} | status: {getattr(archived, 'status', 'archived')}")
|
||||
# Archive (read-only, existing sessions continue)
|
||||
archived = client.beta.environments.archive(env_id)
|
||||
print(f"Archived: {archived.id} | status: {getattr(archived, 'status', 'archived')}")
|
||||
|
||||
# Delete (only if no sessions reference it — may fail if sessions exist)
|
||||
try:
|
||||
client.beta.environments.delete(env_id)
|
||||
print(f"Deleted: {env_id}")
|
||||
except Exception as e:
|
||||
print(f"Delete skipped (expected if sessions exist): {e}")
|
||||
# Delete (only if no sessions reference it — may fail if sessions exist)
|
||||
try:
|
||||
client.beta.environments.delete(env_id)
|
||||
print(f"Deleted: {env_id}")
|
||||
except Exception as e:
|
||||
print(f"Delete skipped (expected if sessions exist): {e}")
|
||||
else:
|
||||
print("Environment management demo skipped: backend does not expose _get_client().")
|
||||
|
||||
print("\nEnvironment management complete.")
|
||||
@@ -15,13 +15,6 @@ Protocol: MCP 2025-11-25
|
||||
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
# Agent with resumability support (automatic)
|
||||
agent = Agent(
|
||||
name="Resumable Assistant",
|
||||
instructions="You are a helpful assistant with resumable connections.",
|
||||
tools=MCP("https://api.example.com/mcp")
|
||||
)
|
||||
|
||||
# Resumability is automatic - the MCP client:
|
||||
# 1. Tracks event IDs from SSE streams (id: field)
|
||||
# 2. Stores last event ID per stream
|
||||
@@ -29,6 +22,12 @@ agent = Agent(
|
||||
# 4. Respects retry delay from server (retry: field)
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = Agent(
|
||||
name="Resumable Assistant",
|
||||
instructions="You are a helpful assistant with resumable connections.",
|
||||
tools=MCP("https://api.example.com/mcp"),
|
||||
)
|
||||
|
||||
print("MCP Resumability Example")
|
||||
print("=" * 40)
|
||||
print()
|
||||
|
||||
@@ -15,13 +15,6 @@ Protocol: MCP 2025-11-25
|
||||
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
# Basic agent with automatic session management
|
||||
agent = Agent(
|
||||
name="Session Assistant",
|
||||
instructions="You are a helpful assistant with session support.",
|
||||
tools=MCP("https://api.example.com/mcp")
|
||||
)
|
||||
|
||||
# Session management is automatic - the MCP client:
|
||||
# 1. Receives Mcp-Session-Id from server during initialization
|
||||
# 2. Includes session ID in all subsequent requests
|
||||
@@ -29,6 +22,12 @@ agent = Agent(
|
||||
# 4. Can terminate session explicitly when done
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = Agent(
|
||||
name="Session Assistant",
|
||||
instructions="You are a helpful assistant with session support.",
|
||||
tools=MCP("https://api.example.com/mcp"),
|
||||
)
|
||||
|
||||
print("MCP Session Management Example")
|
||||
print("=" * 40)
|
||||
print()
|
||||
|
||||
@@ -16,34 +16,31 @@ Protocol: MCP 2025-11-25
|
||||
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
# Basic Streamable HTTP connection
|
||||
agent = Agent(
|
||||
name="HTTP Stream Assistant",
|
||||
instructions="You are a helpful assistant connected via Streamable HTTP.",
|
||||
tools=MCP("https://api.example.com/mcp")
|
||||
)
|
||||
|
||||
# With authentication headers
|
||||
agent_auth = Agent(
|
||||
name="Authenticated Assistant",
|
||||
instructions="You are a helpful assistant with authenticated connection.",
|
||||
tools=MCP(
|
||||
"https://api.example.com/mcp",
|
||||
headers={"Authorization": "Bearer your-token"}
|
||||
)
|
||||
)
|
||||
|
||||
# With timeout and debug
|
||||
agent_debug = Agent(
|
||||
name="Debug Assistant",
|
||||
tools=MCP(
|
||||
"https://api.example.com/mcp",
|
||||
timeout=120,
|
||||
debug=True
|
||||
)
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = Agent(
|
||||
name="HTTP Stream Assistant",
|
||||
instructions="You are a helpful assistant connected via Streamable HTTP.",
|
||||
tools=MCP("https://api.example.com/mcp"),
|
||||
)
|
||||
|
||||
agent_auth = Agent(
|
||||
name="Authenticated Assistant",
|
||||
instructions="You are a helpful assistant with authenticated connection.",
|
||||
tools=MCP(
|
||||
"https://api.example.com/mcp",
|
||||
headers={"Authorization": "Bearer your-token"},
|
||||
),
|
||||
)
|
||||
|
||||
agent_debug = Agent(
|
||||
name="Debug Assistant",
|
||||
tools=MCP(
|
||||
"https://api.example.com/mcp",
|
||||
timeout=120,
|
||||
debug=True,
|
||||
),
|
||||
)
|
||||
|
||||
print("Streamable HTTP MCP Transport Example")
|
||||
print("=" * 40)
|
||||
print("This example requires a Streamable HTTP MCP server running.")
|
||||
|
||||
@@ -16,36 +16,33 @@ Protocol: SEP-1288 (WebSocket Transport)
|
||||
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
# Basic WebSocket connection
|
||||
agent = Agent(
|
||||
name="WebSocket Assistant",
|
||||
instructions="You are a helpful assistant connected via WebSocket.",
|
||||
tools=MCP("ws://localhost:8080/mcp")
|
||||
)
|
||||
|
||||
# Secure WebSocket with authentication
|
||||
agent_secure = Agent(
|
||||
name="Secure WebSocket Assistant",
|
||||
instructions="You are a helpful assistant with secure WebSocket connection.",
|
||||
tools=MCP(
|
||||
"wss://api.example.com/mcp",
|
||||
auth_token="Bearer your-secret-token",
|
||||
timeout=60
|
||||
)
|
||||
)
|
||||
|
||||
# WebSocket with custom options
|
||||
agent_custom = Agent(
|
||||
name="Custom WebSocket Assistant",
|
||||
tools=MCP(
|
||||
"wss://api.example.com/mcp",
|
||||
auth_token="your-token",
|
||||
timeout=120,
|
||||
debug=True
|
||||
)
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = Agent(
|
||||
name="WebSocket Assistant",
|
||||
instructions="You are a helpful assistant connected via WebSocket.",
|
||||
tools=MCP("ws://localhost:8080/mcp"),
|
||||
)
|
||||
|
||||
agent_secure = Agent(
|
||||
name="Secure WebSocket Assistant",
|
||||
instructions="You are a helpful assistant with secure WebSocket connection.",
|
||||
tools=MCP(
|
||||
"wss://api.example.com/mcp",
|
||||
auth_token="Bearer your-secret-token",
|
||||
timeout=60,
|
||||
),
|
||||
)
|
||||
|
||||
agent_custom = Agent(
|
||||
name="Custom WebSocket Assistant",
|
||||
tools=MCP(
|
||||
"wss://api.example.com/mcp",
|
||||
auth_token="your-token",
|
||||
timeout=120,
|
||||
debug=True,
|
||||
),
|
||||
)
|
||||
|
||||
# Note: Requires a running WebSocket MCP server
|
||||
# Example server: https://github.com/modelcontextprotocol/servers
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ print("=== Advanced Graph Memory Integration Example ===\n")
|
||||
|
||||
# Configure simple graph memory
|
||||
memory_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"provider": "mem0",
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
|
||||
@@ -12,8 +12,8 @@ print("=== Advanced Memory Search and Optimization Example ===\n")
|
||||
|
||||
# Configure optimized memory settings
|
||||
memory_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"provider": "mem0",
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
|
||||
@@ -86,13 +86,13 @@ def main():
|
||||
agents=[researcher, retriever],
|
||||
tasks=[store_task, retrieve_task],
|
||||
memory={
|
||||
"backend": "sqlite",
|
||||
"embedder": {
|
||||
"provider": "gemini",
|
||||
"config": {
|
||||
"model": "text-embedding-004",
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {"provider": "sqlite"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ config = {
|
||||
agent = Agent(
|
||||
name="Knowledge Agent",
|
||||
instructions="You answer questions based on the provided knowledge.",
|
||||
knowledge={**config, "sources": ["kag-research-paper.pdf"]},
|
||||
knowledge={
|
||||
"sources": ["kag-research-paper.pdf"],
|
||||
"vector_store": config["vector_store"],
|
||||
"embedder": config["embedder"],
|
||||
},
|
||||
memory={"user_id": "user1"},
|
||||
llm="deepseek-r1"
|
||||
)
|
||||
|
||||
@@ -31,10 +31,9 @@ llm_config = {
|
||||
agent = Agent(
|
||||
instructions="You are a helpful Assistant specialized in scientific explanations. "
|
||||
"Provide clear, accurate, and engaging responses.",
|
||||
llm=llm_config, # Pass the detailed configuration
|
||||
# Enable detailed output
|
||||
markdown=True, # Format responses in markdown
|
||||
reflection=True # Enable self-reflection
|
||||
llm=llm_config,
|
||||
output={"markdown": True},
|
||||
reflection=True,
|
||||
)
|
||||
|
||||
# Test the agent
|
||||
|
||||
@@ -28,8 +28,8 @@ if not os.environ.get("OPENAI_API_KEY"):
|
||||
def main():
|
||||
# MongoDB memory configuration
|
||||
mongodb_memory_config = {
|
||||
"provider": "mongodb",
|
||||
"config": {
|
||||
"provider": "mongodb",
|
||||
"connection_string": "mongodb://localhost:27017/", # Replace with your MongoDB connection string
|
||||
"database": "praisonai_memory",
|
||||
"use_vector_search": True, # Enable Atlas Vector Search (requires MongoDB Atlas)
|
||||
|
||||
@@ -4,7 +4,7 @@ Example 3: Save Agent Output Using Workflow output_file
|
||||
Workflow step output is automatically saved with variable substitution.
|
||||
"""
|
||||
|
||||
from praisonaiagents import AgentFlow
|
||||
from praisonaiagents import Agent, AgentFlow, Task
|
||||
|
||||
# Define workflow with output_file
|
||||
workflow_config = {
|
||||
@@ -33,9 +33,30 @@ workflow_config = {
|
||||
]
|
||||
}
|
||||
|
||||
# Run workflow
|
||||
workflow = AgentFlow(config=workflow_config)
|
||||
result = workflow.run()
|
||||
# Build agent and workflow from config dict
|
||||
writer_cfg = workflow_config["agents"]["writer"]
|
||||
writer = Agent(
|
||||
name="writer",
|
||||
role=writer_cfg["role"],
|
||||
goal=writer_cfg["goal"],
|
||||
llm=writer_cfg["llm"],
|
||||
)
|
||||
|
||||
step_cfg = workflow_config["steps"][0]
|
||||
workflow = AgentFlow(
|
||||
name=workflow_config["metadata"]["name"],
|
||||
variables=workflow_config["variables"],
|
||||
steps=[
|
||||
Task(
|
||||
name="write_tutorial",
|
||||
description=step_cfg["action"],
|
||||
expected_output=step_cfg["expected_output"],
|
||||
output_file=step_cfg["output_file"],
|
||||
agent=writer,
|
||||
)
|
||||
],
|
||||
)
|
||||
result = workflow.start()
|
||||
|
||||
print("✅ Workflow completed!")
|
||||
print("Output saved to: generated/tutorial.md")
|
||||
@@ -28,7 +28,7 @@ def example_basic_persistence():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
memory={"session_id": session_id},
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ def example_session_restoration():
|
||||
agent1 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
memory={"session_id": session_id},
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ def example_session_restoration():
|
||||
agent2 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
session_id=session_id,
|
||||
memory={"session_id": session_id},
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
|
||||
@@ -69,9 +69,10 @@ agents_system = AgentTeam(
|
||||
memory=True, output="verbose"
|
||||
)
|
||||
|
||||
print("Starting state management demonstration...")
|
||||
result = agents_system.start()
|
||||
if __name__ == "__main__":
|
||||
print("Starting state management demonstration...")
|
||||
result = agents_system.start()
|
||||
|
||||
print(f"\nState Management Result: {result[:200]}...")
|
||||
print("\n✅ State management patterns complete!")
|
||||
print("Agent demonstrated persistent state tracking across multiple tasks.")
|
||||
print(f"\nState Management Result: {str(result)[:200]}...")
|
||||
print("\n✅ State management patterns complete!")
|
||||
print("Agent demonstrated persistent state tracking across multiple tasks.")
|
||||
@@ -179,7 +179,7 @@ def main():
|
||||
|
||||
context = memory.build_context_for_task(
|
||||
task_descr="Explain AI alignment challenges",
|
||||
memory={"user_id": "quality_demo_user"},
|
||||
user_id="quality_demo_user",
|
||||
max_items=3
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ def main():
|
||||
# Create a session with persistent storage
|
||||
session = Session(
|
||||
session_id="demo_chat_001",
|
||||
memory={"user_id": "demo_user"},
|
||||
user_id="demo_user",
|
||||
)
|
||||
|
||||
print(f"📝 Created session: {session}")
|
||||
|
||||
@@ -1,284 +1,55 @@
|
||||
"""
|
||||
Production Telemetry Example
|
||||
|
||||
This example demonstrates comprehensive telemetry and monitoring setup for
|
||||
production AI agent systems, including metrics collection, performance tracking,
|
||||
and observability.
|
||||
|
||||
Features demonstrated:
|
||||
- OpenTelemetry integration for distributed tracing
|
||||
- Custom metrics collection and monitoring
|
||||
- Performance tracking and analytics
|
||||
- Error tracking and alerting
|
||||
- Usage analytics and reporting
|
||||
Demonstrates compatible telemetry setup with `enable_telemetry()` and
|
||||
`TelemetryCollector` patterns.
|
||||
"""
|
||||
|
||||
from contextlib import nullcontext
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.tools import duckduckgo
|
||||
from praisonaiagents.telemetry import enable_telemetry
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime
|
||||
from praisonaiagents.telemetry import enable_telemetry, TelemetryCollector
|
||||
|
||||
# Initialize telemetry for production monitoring
|
||||
print("="*80)
|
||||
print("PRODUCTION TELEMETRY SETUP")
|
||||
print("="*80)
|
||||
|
||||
# Configure telemetry (in production, you'd use actual telemetry backends)
|
||||
telemetry = enable_telemetry(
|
||||
backend="opentelemetry",
|
||||
config={
|
||||
"service_name": "praisonai_production_agents",
|
||||
"service_version": "1.0.0",
|
||||
"environment": "production",
|
||||
"export_console": True, # For demo purposes
|
||||
"export_file": True, # Export to file for analysis
|
||||
"sample_rate": 1.0, # 100% sampling for demo
|
||||
"attributes": {
|
||||
"deployment": "cloud",
|
||||
"region": "us-east-1",
|
||||
"cluster": "main"
|
||||
}
|
||||
}
|
||||
)
|
||||
def main() -> None:
|
||||
print("=" * 80)
|
||||
print("PRODUCTION TELEMETRY SETUP")
|
||||
print("=" * 80)
|
||||
|
||||
# Create agents for production workflow with telemetry
|
||||
customer_service_agent = Agent(
|
||||
name="CustomerServiceAgent",
|
||||
role="Customer Service Representative",
|
||||
goal="Provide excellent customer service with fast response times",
|
||||
backstory="You are a customer service representative focused on resolving customer issues quickly and effectively.",
|
||||
tools=[duckduckgo],
|
||||
instructions="Provide helpful, accurate customer service responses. Research solutions when needed."
|
||||
)
|
||||
# New API: no args
|
||||
enable_telemetry()
|
||||
collector = TelemetryCollector()
|
||||
|
||||
technical_support_agent = Agent(
|
||||
name="TechnicalSupportAgent",
|
||||
role="Technical Support Specialist",
|
||||
goal="Resolve technical issues with detailed solutions",
|
||||
backstory="You are a technical support specialist who provides detailed technical solutions and troubleshooting.",
|
||||
instructions="Provide detailed technical solutions with step-by-step instructions."
|
||||
)
|
||||
customer_service_agent = Agent(
|
||||
name="CustomerServiceAgent",
|
||||
role="Customer Service Representative",
|
||||
goal="Provide excellent customer service with fast response times",
|
||||
backstory="Customer service expert focused on quick issue resolution.",
|
||||
tools=[duckduckgo],
|
||||
instructions="Provide accurate customer-service responses.",
|
||||
)
|
||||
|
||||
quality_assurance_agent = Agent(
|
||||
name="QualityAssuranceAgent",
|
||||
role="Quality Assurance Specialist",
|
||||
goal="Ensure all customer interactions meet quality standards",
|
||||
backstory="You review and assess the quality of customer service interactions to ensure high standards.",
|
||||
instructions="Review customer service interactions and provide quality scores with detailed feedback."
|
||||
)
|
||||
|
||||
# Production workflow simulation with telemetry tracking
|
||||
def simulate_customer_requests():
|
||||
"""Simulate various customer requests for telemetry demonstration"""
|
||||
|
||||
customer_requests = [
|
||||
{
|
||||
"type": "billing_inquiry",
|
||||
"complexity": "low",
|
||||
"description": "Customer asking about their monthly bill charges",
|
||||
"expected_duration": 30
|
||||
},
|
||||
{
|
||||
"type": "technical_issue",
|
||||
"complexity": "high",
|
||||
"description": "Customer experiencing connectivity issues with the service",
|
||||
"expected_duration": 120
|
||||
},
|
||||
{
|
||||
"type": "account_setup",
|
||||
"complexity": "medium",
|
||||
"description": "New customer needs help setting up their account",
|
||||
"expected_duration": 60
|
||||
},
|
||||
{
|
||||
"type": "feature_question",
|
||||
"complexity": "low",
|
||||
"description": "Customer asking about product features and capabilities",
|
||||
"expected_duration": 45
|
||||
}
|
||||
requests = [
|
||||
"Customer asking about monthly bill charges",
|
||||
"Customer experiencing connectivity issues",
|
||||
]
|
||||
|
||||
return customer_requests
|
||||
|
||||
# Execute production workflow with comprehensive telemetry
|
||||
print("Starting production workflow with telemetry tracking...")
|
||||
for idx, description in enumerate(requests, start=1):
|
||||
trace_ctx = getattr(collector, "trace", None)
|
||||
trace_cm = trace_ctx(f"customer_request_{idx}") if callable(trace_ctx) else nullcontext()
|
||||
with trace_cm:
|
||||
task = Task(
|
||||
name=f"customer_request_{idx}",
|
||||
description=description,
|
||||
expected_output="Professional customer service response with solution",
|
||||
agent=customer_service_agent,
|
||||
)
|
||||
agents = AgentTeam(agents=[customer_service_agent], tasks=[task], output="minimal")
|
||||
result = agents.start()
|
||||
print(f"✅ Request {idx} completed, response length={len(str(result))}")
|
||||
|
||||
customer_requests = simulate_customer_requests()
|
||||
print("\nTelemetry demonstration completed.")
|
||||
|
||||
for i, request in enumerate(customer_requests):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"PROCESSING CUSTOMER REQUEST {i+1}: {request['type'].upper()}")
|
||||
print(f"Complexity: {request['complexity']} | Expected Duration: {request['expected_duration']}s")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Start telemetry trace for this customer interaction
|
||||
with telemetry.trace_operation(f"customer_request_{request['type']}") as trace:
|
||||
|
||||
# Add custom attributes to the trace
|
||||
trace.set_attributes({
|
||||
"request.type": request['type'],
|
||||
"request.complexity": request['complexity'],
|
||||
"request.expected_duration": request['expected_duration'],
|
||||
"customer.id": f"customer_{random.randint(1000, 9999)}",
|
||||
"agent.assigned": "CustomerServiceAgent"
|
||||
})
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Create customer service task with telemetry
|
||||
customer_task = Task(
|
||||
name=f"customer_request_{i+1}",
|
||||
description=request['description'],
|
||||
expected_output="Professional customer service response with solution",
|
||||
agent=customer_service_agent
|
||||
)
|
||||
|
||||
# Execute with telemetry tracking
|
||||
try:
|
||||
# Customer service response
|
||||
with telemetry.trace_agent_execution("CustomerServiceAgent") as agent_trace:
|
||||
agents = AgentTeam(
|
||||
agents=[customer_service_agent],
|
||||
tasks=[customer_task],
|
||||
output="minimal" # Reduce verbosity for telemetry demo
|
||||
)
|
||||
|
||||
cs_result = agents.start()
|
||||
agent_trace.set_attributes({
|
||||
"agent.response_length": len(str(cs_result)),
|
||||
"agent.tools_used": len(customer_service_agent.tools) if customer_service_agent.tools else 0
|
||||
})
|
||||
|
||||
# Technical support (if needed for high complexity)
|
||||
if request['complexity'] == 'high':
|
||||
with telemetry.trace_agent_execution("TechnicalSupportAgent") as tech_trace:
|
||||
tech_task = Task(
|
||||
name=f"technical_support_{i+1}",
|
||||
description=f"Provide detailed technical support for: {request['description']}",
|
||||
expected_output="Detailed technical solution with troubleshooting steps",
|
||||
agent=technical_support_agent,
|
||||
context=[customer_task]
|
||||
)
|
||||
|
||||
tech_agents = AgentTeam(
|
||||
agents=[technical_support_agent],
|
||||
tasks=[tech_task], output="minimal"
|
||||
)
|
||||
|
||||
tech_result = tech_agents.start()
|
||||
tech_trace.set_attributes({
|
||||
"agent.response_length": len(str(tech_result)),
|
||||
"agent.escalation": True
|
||||
})
|
||||
|
||||
# Quality assurance check
|
||||
with telemetry.trace_agent_execution("QualityAssuranceAgent") as qa_trace:
|
||||
qa_task = Task(
|
||||
name=f"quality_check_{i+1}",
|
||||
description=f"Review the quality of customer service provided for: {request['description']}",
|
||||
expected_output="Quality assessment score and feedback",
|
||||
agent=quality_assurance_agent
|
||||
)
|
||||
|
||||
qa_agents = AgentTeam(
|
||||
agents=[quality_assurance_agent],
|
||||
tasks=[qa_task], output="minimal"
|
||||
)
|
||||
|
||||
qa_result = qa_agents.start()
|
||||
|
||||
# Extract quality score (simulated)
|
||||
quality_score = random.uniform(0.8, 1.0) # Simulated score
|
||||
qa_trace.set_attributes({
|
||||
"qa.score": quality_score,
|
||||
"qa.passed": quality_score >= 0.85
|
||||
})
|
||||
|
||||
# Calculate performance metrics
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# Record custom metrics
|
||||
telemetry.record_metric("request_duration", duration, {
|
||||
"request_type": request['type'],
|
||||
"complexity": request['complexity']
|
||||
})
|
||||
|
||||
telemetry.record_metric("request_success", 1, {
|
||||
"request_type": request['type']
|
||||
})
|
||||
|
||||
# Set final trace attributes
|
||||
trace.set_attributes({
|
||||
"request.duration_seconds": duration,
|
||||
"request.status": "completed",
|
||||
"request.quality_score": quality_score,
|
||||
"request.escalated": request['complexity'] == 'high'
|
||||
})
|
||||
|
||||
print(f"✅ Request completed successfully in {duration:.2f}s (Quality: {quality_score:.2f})")
|
||||
|
||||
except Exception as e:
|
||||
# Record error metrics
|
||||
telemetry.record_metric("request_error", 1, {
|
||||
"request_type": request['type'],
|
||||
"error_type": type(e).__name__
|
||||
})
|
||||
|
||||
trace.set_attributes({
|
||||
"request.status": "error",
|
||||
"request.error": str(e)
|
||||
})
|
||||
|
||||
print(f"❌ Request failed: {str(e)}")
|
||||
|
||||
# Add some delay between requests
|
||||
time.sleep(2)
|
||||
|
||||
# Generate telemetry summary
|
||||
print(f"\n{'='*80}")
|
||||
print("TELEMETRY SUMMARY AND ANALYTICS")
|
||||
print(f"{'='*80}")
|
||||
|
||||
# Simulated telemetry analytics (in production, this would come from your telemetry backend)
|
||||
print("📊 Performance Metrics:")
|
||||
print("- Total Requests Processed: 4")
|
||||
print("- Average Response Time: 3.2s")
|
||||
print("- Success Rate: 100%")
|
||||
print("- Quality Score Average: 0.91")
|
||||
print("- High Complexity Requests: 25%")
|
||||
|
||||
print("\n📈 Usage Analytics:")
|
||||
print("- Most Common Request Type: technical_issue")
|
||||
print("- Peak Performance Window: All requests within SLA")
|
||||
print("- Agent Utilization: CustomerService (100%), TechnicalSupport (25%), QA (100%)")
|
||||
|
||||
print("\n🔍 Observability Features Demonstrated:")
|
||||
print("- Distributed tracing across agent interactions")
|
||||
print("- Custom metrics for business KPIs")
|
||||
print("- Performance monitoring and SLA tracking")
|
||||
print("- Error tracking and alerting")
|
||||
print("- Quality assurance metrics")
|
||||
print("- User journey tracking")
|
||||
print("- Resource utilization monitoring")
|
||||
|
||||
print("\n⚙️ Production Monitoring Setup:")
|
||||
print("- OpenTelemetry integration for standardized observability")
|
||||
print("- Custom metrics for business-specific KPIs")
|
||||
print("- Trace correlation across multi-agent workflows")
|
||||
print("- Performance benchmarking against expected durations")
|
||||
print("- Quality scoring and automated validation")
|
||||
print("- Error categorization and alerting")
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print("TELEMETRY DEMONSTRATION COMPLETED")
|
||||
print(f"{'='*80}")
|
||||
print("This example demonstrated comprehensive production telemetry including:")
|
||||
print("- Request tracing and performance monitoring")
|
||||
print("- Custom business metrics collection")
|
||||
print("- Quality assurance tracking")
|
||||
print("- Error handling and alerting")
|
||||
print("- Multi-agent workflow observability")
|
||||
print("- Production-ready monitoring patterns")
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Find information about a topic",
|
||||
backstory="You are an expert researcher who finds comprehensive information.",
|
||||
llm="gpt-4o-mini", # Use a cost-effective model for examples
|
||||
metrics=True # 🎯 SIMPLIFIED: Just add this parameter!
|
||||
output={"metrics": True} # Token metrics via consolidated output config
|
||||
)
|
||||
|
||||
# Create a simple task
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Analyze data and provide insights",
|
||||
backstory="You are an experienced data analyst with expertise in market research.",
|
||||
llm="gpt-4o-mini",
|
||||
metrics=True # 🎯 Enable metrics tracking
|
||||
output={"metrics": True}
|
||||
)
|
||||
|
||||
writer_agent = Agent(
|
||||
@@ -23,7 +23,7 @@ def main():
|
||||
goal="Create well-structured content",
|
||||
backstory="You are a skilled technical writer who creates clear, engaging content.",
|
||||
llm="gpt-4o-mini",
|
||||
metrics=True # 🎯 Enable metrics tracking
|
||||
output={"metrics": True}
|
||||
)
|
||||
|
||||
# Create multiple tasks
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Conduct comprehensive market research",
|
||||
backstory="You are a seasoned market researcher with 10+ years of experience.",
|
||||
llm="gpt-4o-mini",
|
||||
metrics=True # 🎯 Enable automatic metrics tracking
|
||||
output={"metrics": True}
|
||||
)
|
||||
|
||||
strategy_agent = Agent(
|
||||
@@ -23,7 +23,7 @@ def main():
|
||||
goal="Develop actionable business strategies",
|
||||
backstory="You are a strategic consultant who translates research into business plans.",
|
||||
llm="gpt-4o-mini",
|
||||
metrics=True # 🎯 Enable automatic metrics tracking
|
||||
output={"metrics": True}
|
||||
)
|
||||
|
||||
# Create tasks that will generate substantial token usage
|
||||
|
||||
@@ -22,7 +22,8 @@ rag_agent = Agent(
|
||||
|
||||
You can use the internet_search function to search the web when needed.""",
|
||||
llm="gpt-4o",
|
||||
markdown=True)
|
||||
output={"markdown": True},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🤖 Thai Recipe RAG Agent is ready!")
|
||||
|
||||
@@ -5,11 +5,17 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ['SERPER_API_KEY'] = os.getenv('SERPER_API_KEY')
|
||||
serper_api_key = os.getenv("SERPER_API_KEY")
|
||||
if serper_api_key is not None:
|
||||
os.environ["SERPER_API_KEY"] = serper_api_key
|
||||
|
||||
search = GoogleSerperAPIWrapper()
|
||||
|
||||
data_agent = Agent(instructions="Suggest me top 5 most visited websites for Dosa Recipe", tools=[search])
|
||||
editor_agent = Agent(instructions="List out the websites with their url and a short description")
|
||||
agents = AgentTeam(agents=[data_agent, editor_agent])
|
||||
agents.start()
|
||||
if __name__ == "__main__":
|
||||
if not serper_api_key:
|
||||
print("SERPER_API_KEY is not set. Skipping Google Serper example.")
|
||||
else:
|
||||
search = GoogleSerperAPIWrapper()
|
||||
data_agent = Agent(instructions="Suggest me top 5 most visited websites for Dosa Recipe", tools=[search])
|
||||
editor_agent = Agent(instructions="List out the websites with their url and a short description")
|
||||
agents = AgentTeam(agents=[data_agent, editor_agent])
|
||||
agents.start()
|
||||
@@ -2,6 +2,7 @@ from praisonaiagents import Agent, Task, AgentTeam
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict
|
||||
from gitingest import ingest
|
||||
import sys
|
||||
|
||||
class CodeMetrics(BaseModel):
|
||||
category: str
|
||||
@@ -60,7 +61,10 @@ def analyze_code(code_source: str) -> CodeAnalysisReport:
|
||||
Analyze code from directory path or GitHub URL
|
||||
"""
|
||||
# Ingest code content
|
||||
summary, tree, content = ingest(code_source)
|
||||
try:
|
||||
summary, tree, content = ingest(code_source)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Failed to ingest code source: {exc}") from exc
|
||||
|
||||
# Concatenate context into structured format
|
||||
context_text = f"""
|
||||
@@ -86,10 +90,22 @@ def analyze_code(code_source: str) -> CodeAnalysisReport:
|
||||
tasks=[code_analysis_task]
|
||||
)
|
||||
|
||||
return agents.start(context_text)
|
||||
result = agents.start(context_text)
|
||||
if result is None:
|
||||
raise RuntimeError("No analysis result returned by AgentTeam.start()")
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main() -> None:
|
||||
# Example usage
|
||||
code_source = "https://github.com/openai/openai-python/tree/main/src/openai/cli/_api/chat" # GitHub URL or local directory
|
||||
result = analyze_code(code_source)
|
||||
print(result)
|
||||
try:
|
||||
result = analyze_code(code_source)
|
||||
except RuntimeError as exc:
|
||||
print(f"Code analysis failed: {exc}")
|
||||
sys.exit(1)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -77,25 +77,44 @@ agents = AgentTeam(
|
||||
process="sequential", output="verbose"
|
||||
)
|
||||
|
||||
|
||||
def _extract_pydantic_result(result_obj):
|
||||
"""Extract pydantic payload from AgentTeam.start() output shapes."""
|
||||
if hasattr(result_obj, "pydantic"):
|
||||
return result_obj.pydantic
|
||||
if isinstance(result_obj, dict):
|
||||
task_results = result_obj.get("task_results")
|
||||
if isinstance(task_results, dict):
|
||||
values = list(task_results.values())
|
||||
if values and hasattr(values[-1], "pydantic"):
|
||||
return values[-1].pydantic
|
||||
if isinstance(task_results, list) and task_results and hasattr(task_results[-1], "pydantic"):
|
||||
return task_results[-1].pydantic
|
||||
return None
|
||||
|
||||
|
||||
# Start the analysis
|
||||
result = agents.start()
|
||||
pydantic_result = _extract_pydantic_result(result)
|
||||
if pydantic_result is None:
|
||||
raise ValueError("Could not extract structured CVAnalysisReport from workflow result")
|
||||
|
||||
# Access the structured results
|
||||
print("\nCV Analysis Results:")
|
||||
print(f"\nOverall Score: {result.pydantic.overall_score}/100")
|
||||
print(f"\nOverall Score: {pydantic_result.overall_score}/100")
|
||||
print("\nKey Strengths:")
|
||||
for strength in result.pydantic.key_strengths:
|
||||
for strength in pydantic_result.key_strengths:
|
||||
print(f"- {strength}")
|
||||
|
||||
print("\nSkill Analysis:")
|
||||
print("\nTechnical Skills:")
|
||||
for skill in result.pydantic.skill_analysis.technical_skills:
|
||||
for skill in pydantic_result.skill_analysis.technical_skills:
|
||||
print(f"- {skill}")
|
||||
|
||||
print("\nExperience Highlights:")
|
||||
for achievement in result.pydantic.experience_analysis.key_achievements:
|
||||
for achievement in pydantic_result.experience_analysis.key_achievements:
|
||||
print(f"- {achievement}")
|
||||
|
||||
print("\nRecommendations:")
|
||||
for recommendation in result.pydantic.recommendations:
|
||||
for recommendation in pydantic_result.recommendations:
|
||||
print(f"- {recommendation}")
|
||||
@@ -100,6 +100,8 @@ def evaluate_project(video_path: str) -> ProjectEvaluation:
|
||||
evaluation_data = response
|
||||
|
||||
print(f"Debug - Parsed evaluation_data: {evaluation_data}")
|
||||
if not evaluation_data or not isinstance(evaluation_data, dict):
|
||||
raise ValueError("No structured evaluation data returned by agent")
|
||||
|
||||
# Create and return ProjectEvaluation instance
|
||||
return ProjectEvaluation(
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
from praisonaiagents import VideoAgent
|
||||
import os
|
||||
|
||||
agent = VideoAgent(llm="azure/sora-2")
|
||||
video = agent.generate("Mountain landscape")
|
||||
print(video.id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not os.getenv("AZURE_API_BASE"):
|
||||
print("AZURE_API_BASE is not configured. Skipping Azure video example.")
|
||||
else:
|
||||
agent = VideoAgent(llm="azure/sora-2")
|
||||
video = agent.generate("Mountain landscape")
|
||||
print(video.id)
|
||||
@@ -6,6 +6,7 @@ with Agents for workflow monitoring and logging.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.config.feature_configs import MultiAgentHooksConfig
|
||||
|
||||
# Create an agent
|
||||
researcher = Agent(
|
||||
@@ -47,8 +48,10 @@ if __name__ == "__main__":
|
||||
agents=[researcher],
|
||||
tasks=[task1, task2],
|
||||
process="workflow",
|
||||
on_task_start=on_start,
|
||||
on_task_complete=on_complete,
|
||||
hooks=MultiAgentHooksConfig(
|
||||
on_task_start=on_start,
|
||||
on_task_complete=on_complete,
|
||||
),
|
||||
variables={"global_var": "shared_value"} # Global variables
|
||||
)
|
||||
|
||||
|
||||
@@ -54,13 +54,13 @@ workflow = AgentFlow(
|
||||
analyzer, # Always runs
|
||||
Task(
|
||||
name="tech_review",
|
||||
description="Review technical aspects of the content",
|
||||
description="Review technical aspects when technical keywords are detected.",
|
||||
agent=tech_reviewer,
|
||||
should_run=is_technical # Only runs for technical content
|
||||
),
|
||||
Task(
|
||||
name="creative_enhance",
|
||||
description="Enhance the creative aspects of the content",
|
||||
description="Enhance narrative quality when creative keywords are detected.",
|
||||
agent=creative_enhancer,
|
||||
should_run=needs_creative # Only runs for creative content
|
||||
),
|
||||
|
||||
@@ -53,7 +53,7 @@ workflow = AgentFlow(
|
||||
# Step 4: Function - formats output
|
||||
format_output
|
||||
],
|
||||
default_llm="gpt-4o-mini" # Used for action-based steps
|
||||
llm="gpt-4o-mini", # Used for action-based steps
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -42,6 +42,7 @@ def main():
|
||||
|
||||
# Create tasks with robustness features
|
||||
research_task = Task(
|
||||
name="research",
|
||||
description="Research the latest trends in AI agents for 2025",
|
||||
expected_output="A summary of key AI agent trends",
|
||||
agent=researcher,
|
||||
@@ -51,6 +52,7 @@ def main():
|
||||
|
||||
# Optional enrichment step - workflow continues even if this fails
|
||||
enrich_task = Task(
|
||||
name="enrich",
|
||||
description="Enrich the research with real-world examples",
|
||||
expected_output="Research with added examples",
|
||||
agent=enricher,
|
||||
@@ -60,6 +62,7 @@ def main():
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
name="write",
|
||||
description="Write a brief summary based on the research",
|
||||
expected_output="A well-written summary paragraph",
|
||||
agent=writer,
|
||||
|
||||
@@ -51,13 +51,15 @@ print("=" * 60)
|
||||
agent = Agent(
|
||||
name="Knowledge Agent",
|
||||
instructions="You are a helpful assistant that answers questions based on the provided knowledge. Be concise.",
|
||||
knowledge=[".praison/test_docs/overview.txt"],
|
||||
retrieval_config={
|
||||
"policy": "always", # Always retrieve for this demo
|
||||
"top_k": 3, # Get top 3 chunks
|
||||
"citations": True, # Include citations
|
||||
"max_context_tokens": 2000,
|
||||
}
|
||||
knowledge={
|
||||
"sources": [".praison/test_docs/overview.txt"],
|
||||
"config": {
|
||||
"policy": "always",
|
||||
"top_k": 3,
|
||||
"citations": True,
|
||||
"max_context_tokens": 2000,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Test 1: Basic chat with retrieval
|
||||
|
||||
@@ -60,7 +60,7 @@ print("=" * 50)
|
||||
agent_with_discovery = Agent(
|
||||
name="Multi-Skill Agent",
|
||||
instructions="You are a versatile assistant with multiple skills.",
|
||||
skills_dirs=["./"], # Scan current directory for skill subdirectories
|
||||
skills=["./"], # Scan current directory for skill subdirectories
|
||||
)
|
||||
|
||||
if agent_with_discovery.skill_manager:
|
||||
|
||||
@@ -190,7 +190,7 @@ def example_run_history_with_backend():
|
||||
|
||||
# Use with RunHistory
|
||||
history = RunHistory(backend=backend)
|
||||
runs = history.list()
|
||||
runs = history.list_runs()
|
||||
|
||||
print(f"RunHistory created with SQLite backend, {len(runs)} runs")
|
||||
print("Recipe runs will be stored in database instead of files")
|
||||
|
||||
@@ -257,7 +257,7 @@ if __name__ == "__main__":
|
||||
# Check dependencies
|
||||
try:
|
||||
import praisonaiagents
|
||||
print(f"✅ PraisonAI version: {praisonaiagents.__version__}")
|
||||
print(f"✅ PraisonAI version: {getattr(praisonaiagents, '__version__', None)}")
|
||||
except ImportError:
|
||||
print("❌ PraisonAI not installed: pip install praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -12,7 +12,7 @@ assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY must be set"
|
||||
# Enable web search and fetch
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web=True,
|
||||
web="search_only",
|
||||
)
|
||||
|
||||
# Disable web (default)
|
||||
|
||||
@@ -4,7 +4,7 @@ WebConfig Example
|
||||
Demonstrates using WebConfig for fine-grained control.
|
||||
"""
|
||||
import os
|
||||
from praisonaiagents import Agent, WebConfig
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Ensure API key is set from environment
|
||||
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY must be set"
|
||||
@@ -12,21 +12,13 @@ assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY must be set"
|
||||
# Custom web configuration
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant.",
|
||||
web=WebConfig(
|
||||
search=True,
|
||||
fetch=True,
|
||||
search_provider="duckduckgo",
|
||||
max_results=5,
|
||||
),
|
||||
web="search_only",
|
||||
)
|
||||
|
||||
# Search only (no fetch)
|
||||
agent_search_only = Agent(
|
||||
instructions="You are a research assistant.",
|
||||
web=WebConfig(
|
||||
search=True,
|
||||
fetch=False,
|
||||
),
|
||||
web="search_only",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -93,7 +93,7 @@ def example_3_execute_yaml_directly():
|
||||
result = manager.execute_yaml(
|
||||
yaml_file,
|
||||
input_data="The future of AI",
|
||||
output="verbose"
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
print(f"Status: {result['status']}")
|
||||
|
||||
@@ -554,9 +554,6 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
skills: Optional[Union[List[str], str, Dict[str, Any], 'SkillsConfig']] = None,
|
||||
approval: Optional[Union[bool, str, Dict[str, Any], 'ApprovalConfig', 'ApprovalProtocol']] = None,
|
||||
tool_config: Optional[Union[bool, 'ToolConfig']] = None, # Tool execution configuration (timeout, retry, parallel)
|
||||
tool_timeout: Optional[int] = None, # **Deprecated**: use tool_config=ToolConfig(timeout=60)
|
||||
tool_retry_policy: Optional['RetryPolicy'] = None, # **Deprecated**: use tool_config=ToolConfig(retry_policy=policy)
|
||||
parallel_tool_calls: bool = False, # **Deprecated**: use tool_config=ToolConfig(parallel=True)
|
||||
learn: Optional[Union[bool, str, Dict[str, Any], 'LearnConfig']] = None, # Continuous learning (peer to memory)
|
||||
backend: Optional[Any] = None, # External managed agent backend (e.g., ManagedAgentIntegration)
|
||||
cli_backend: Optional[Union[str, Any]] = None, # CLI backend for delegating turns (e.g., "claude-code")
|
||||
@@ -654,11 +651,7 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
tool_config: Tool execution configuration. Accepts:
|
||||
- bool: True enables defaults, False disables
|
||||
- ToolConfig: Custom configuration for timeout, retry policy, parallel execution
|
||||
Consolidates tool_timeout, tool_retry_policy, and parallel_tool_calls settings.
|
||||
tool_retry_policy: **Deprecated** — use ``tool_config=ToolConfig(retry_policy=policy)`` instead.
|
||||
Still works for backward compatibility.
|
||||
parallel_tool_calls: **Deprecated** — use ``tool_config=ToolConfig(parallel=True)`` instead.
|
||||
Still works for backward compatibility.
|
||||
Consolidates tool timeout, retry policy, and parallel execution settings.
|
||||
backend: External managed agent backend for hybrid execution. Accepts:
|
||||
- ManagedAgentIntegration: External managed agent service
|
||||
- None: Use local execution (default)
|
||||
@@ -698,7 +691,7 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
- auto_save → memory=MemoryConfig(auto_save=)
|
||||
- rate_limiter → execution=ExecutionConfig(rate_limiter=)
|
||||
- verification_hooks → autonomy=AutonomyConfig(verification_hooks=)
|
||||
- tool_timeout, tool_retry_policy, parallel_tool_calls → tool_config=ToolConfig(...)
|
||||
- Use tool_config=ToolConfig(...) for tool execution configuration
|
||||
"""
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -809,8 +802,7 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
alternative="use 'execution=ExecutionConfig(rate_limiter=obj)' instead",
|
||||
stacklevel=3
|
||||
)
|
||||
# Note: parallel_tool_calls is NOT deprecated - it's a new Gap 2 feature
|
||||
# Both direct parameter and ExecutionConfig.parallel_tool_calls are supported
|
||||
# Note: parallel_tool_calls moved to tool_config pattern
|
||||
if verification_hooks is not None:
|
||||
warn_deprecated_param(
|
||||
"verification_hooks",
|
||||
@@ -986,8 +978,8 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
allow_code_execution = True
|
||||
if _exec_config.code_mode != "safe":
|
||||
code_execution_mode = _exec_config.code_mode
|
||||
# Get parallel_tool_calls from ExecutionConfig, fall back to parameter
|
||||
parallel_tool_calls = getattr(_exec_config, 'parallel_tool_calls', parallel_tool_calls)
|
||||
# Get parallel_tool_calls from ExecutionConfig
|
||||
parallel_tool_calls = getattr(_exec_config, 'parallel_tool_calls', False)
|
||||
# Budget guard extraction
|
||||
_max_budget = getattr(_exec_config, 'max_budget', None)
|
||||
_on_budget_exceeded = getattr(_exec_config, 'on_budget_exceeded', 'stop') or 'stop'
|
||||
@@ -995,7 +987,8 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
max_iter, max_rpm, max_execution_time, max_retry_limit = 20, None, None, 2
|
||||
_max_budget = None
|
||||
_on_budget_exceeded = 'stop'
|
||||
# Keep parallel_tool_calls parameter value when no ExecutionConfig provided
|
||||
# Default to False when no ExecutionConfig provided
|
||||
parallel_tool_calls = False
|
||||
# (already set from parameter, no need to override)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1523,12 +1516,9 @@ class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMix
|
||||
|
||||
self.instructions = instructions
|
||||
|
||||
# Resolve tool_config early for immediate use in parallel_tool_calls
|
||||
# Resolve tool_config for tool execution settings
|
||||
_tool_config = Agent._resolve_tool_config(
|
||||
tool_config=tool_config,
|
||||
tool_timeout=tool_timeout,
|
||||
tool_retry_policy=tool_retry_policy,
|
||||
parallel_tool_calls=parallel_tool_calls
|
||||
tool_config=tool_config
|
||||
)
|
||||
|
||||
# Gap 2: Store parallel tool calls setting for ToolCallExecutor selection
|
||||
@@ -1935,10 +1925,10 @@ Your Goal: {self.goal}
|
||||
self._tool_config = _tool_config
|
||||
|
||||
# P8/G11: Tool timeout - prevent slow tools from blocking
|
||||
self._tool_timeout = _tool_config.timeout if _tool_config else tool_timeout
|
||||
self._tool_timeout = _tool_config.timeout if _tool_config else None
|
||||
|
||||
# Store tool retry policy for tool execution with exponential backoff
|
||||
self._tool_retry_policy = _tool_config.retry_policy if _tool_config else tool_retry_policy
|
||||
self._tool_retry_policy = _tool_config.retry_policy if _tool_config else None
|
||||
|
||||
# Cache for system prompts and formatted tools with eager thread-safe lock
|
||||
# Use OrderedDict for LRU behavior
|
||||
@@ -2055,7 +2045,7 @@ Your Goal: {self.goal}
|
||||
super().__init__(sandbox=sandbox)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_tool_config(tool_config, tool_timeout, tool_retry_policy, parallel_tool_calls):
|
||||
def _resolve_tool_config(tool_config):
|
||||
"""Resolve tool_config parameter with backward compatibility."""
|
||||
# Import here to avoid circular imports
|
||||
from ..config.feature_configs import ToolConfig, resolve_tools
|
||||
@@ -2063,30 +2053,7 @@ Your Goal: {self.goal}
|
||||
# Handle the new consolidated parameter
|
||||
if tool_config is not None:
|
||||
resolved_config = resolve_tools(tool_config)
|
||||
if resolved_config:
|
||||
# Apply deprecated parameter overrides if specified
|
||||
if tool_timeout is not None:
|
||||
resolved_config.timeout = tool_timeout
|
||||
if tool_retry_policy is not None:
|
||||
resolved_config.retry_policy = tool_retry_policy
|
||||
if parallel_tool_calls:
|
||||
resolved_config.parallel = parallel_tool_calls
|
||||
return resolved_config
|
||||
|
||||
# Fallback: create config from deprecated parameters if any are specified
|
||||
if tool_timeout is not None or tool_retry_policy is not None or parallel_tool_calls:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"tool_timeout, tool_retry_policy, and parallel_tool_calls are deprecated. "
|
||||
"Use tool_config=ToolConfig(timeout=60, retry_policy=policy, parallel=True) instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=4 # Increased to account for static method call
|
||||
)
|
||||
return ToolConfig(
|
||||
timeout=tool_timeout,
|
||||
retry_policy=tool_retry_policy,
|
||||
parallel=parallel_tool_calls
|
||||
)
|
||||
return resolved_config
|
||||
|
||||
return None
|
||||
|
||||
@@ -2168,10 +2135,6 @@ Your Goal: {self.goal}
|
||||
|
||||
# Tool configuration - use consolidated config when available
|
||||
'tool_config': getattr(self, '_tool_config', None),
|
||||
# Backward compatibility - always include for now
|
||||
'tool_timeout': getattr(self, '_tool_timeout', None),
|
||||
'tool_retry_policy': getattr(self, '_tool_retry_policy', None),
|
||||
'parallel_tool_calls': getattr(self, 'parallel_tool_calls', False),
|
||||
|
||||
# CLI backend
|
||||
'cli_backend': getattr(self, '_cli_backend', None),
|
||||
|
||||
@@ -353,9 +353,9 @@ class RouterAgent(Agent):
|
||||
|
||||
return report
|
||||
|
||||
def _build_system_prompt(self) -> str:
|
||||
def _build_system_prompt(self, tools=None, **kwargs) -> str:
|
||||
"""Build system prompt (inherited from parent but can be customized)."""
|
||||
base_prompt = super()._build_system_prompt()
|
||||
base_prompt = super()._build_system_prompt(tools=tools, **kwargs)
|
||||
|
||||
# Add multi-model context if needed
|
||||
if self.routing_strategy == "auto":
|
||||
|
||||
@@ -848,21 +848,21 @@ class ToolConfig:
|
||||
"""
|
||||
Configuration for tool execution behavior.
|
||||
|
||||
Consolidates: tool_timeout, tool_retry_policy, parallel_tool_calls
|
||||
Configuration for tool execution behavior including timeout, retry policy, and parallel execution.
|
||||
|
||||
Usage:
|
||||
# Simple enable with defaults
|
||||
Agent(tools=ToolConfig())
|
||||
Agent(tool_config=ToolConfig())
|
||||
|
||||
# With custom settings
|
||||
Agent(tools=ToolConfig(
|
||||
Agent(tool_config=ToolConfig(
|
||||
timeout=60,
|
||||
retry_policy=RetryPolicy(max_attempts=5),
|
||||
parallel=True,
|
||||
))
|
||||
|
||||
# With timeout only
|
||||
Agent(tools=ToolConfig(timeout=30))
|
||||
Agent(tool_config=ToolConfig(timeout=30))
|
||||
"""
|
||||
# Tool execution timeout in seconds
|
||||
timeout: Optional[int] = None
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Any, Dict, Optional
|
||||
from praisonaiagents import Agent, tool
|
||||
from praisonaiagents.config.feature_configs import (
|
||||
OutputConfig, ExecutionConfig, ReflectionConfig,
|
||||
TemplateConfig, CachingConfig,
|
||||
TemplateConfig, CachingConfig, ToolConfig,
|
||||
)
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ class TestAgentParamSmoke:
|
||||
agent = Agent(name="AP", approval=False)
|
||||
|
||||
def test_param_tool_timeout(self):
|
||||
agent = Agent(name="TT", tool_timeout=30)
|
||||
agent = Agent(name="TT", tool_config=ToolConfig(timeout=30))
|
||||
|
||||
def test_param_learn(self):
|
||||
agent = Agent(name="LN", learn=False)
|
||||
@@ -349,12 +349,12 @@ class TestAgentParamReal:
|
||||
|
||||
# 33: tool_timeout
|
||||
def test_real_tool_timeout(self, api_key_check):
|
||||
"""Tests: tool_timeout=30"""
|
||||
"""Tests: tool_config=ToolConfig(timeout=30)"""
|
||||
agent = Agent(
|
||||
name="TimeoutAgent",
|
||||
instructions="Use echo_tool.",
|
||||
tools=[echo_tool],
|
||||
tool_timeout=30,
|
||||
tool_config=ToolConfig(timeout=30),
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
result = agent.start("Echo the phrase 'timeout_test'.")
|
||||
@@ -548,7 +548,7 @@ class TestMethodExistence:
|
||||
'memory', 'knowledge', 'planning', 'reflection', 'guardrails',
|
||||
'web', 'context', 'autonomy', 'verification_hooks',
|
||||
'output', 'execution', 'templates', 'caching', 'hooks', 'skills',
|
||||
'approval', 'tool_timeout', 'learn',
|
||||
'approval', 'tool_config', 'learn',
|
||||
]
|
||||
|
||||
missing = [p for p in expected if p not in params]
|
||||
|
||||
@@ -18,6 +18,7 @@ import asyncio
|
||||
import time
|
||||
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
||||
from praisonaiagents import Agent, tool
|
||||
from praisonaiagents.config.feature_configs import ToolConfig
|
||||
from praisonaiagents.agent.unified_execution_mixin import UnifiedExecutionMixin
|
||||
from praisonaiagents.llm.streaming_protocol import (
|
||||
get_streaming_adapter,
|
||||
@@ -220,7 +221,7 @@ class TestGap2ParallelToolExecution:
|
||||
agent = Agent(
|
||||
name="parallel_test",
|
||||
instructions="You are a test agent",
|
||||
parallel_tool_calls=True
|
||||
tool_config=ToolConfig(parallel=True)
|
||||
)
|
||||
|
||||
assert hasattr(agent, 'parallel_tool_calls')
|
||||
@@ -370,7 +371,7 @@ class TestRealAgenticIntegration:
|
||||
name="integration_test",
|
||||
instructions="You are a helpful assistant. Use tools when needed.",
|
||||
tools=[get_weather, get_time, get_news],
|
||||
parallel_tool_calls=True, # Gap 2: Enable parallel execution
|
||||
tool_config=ToolConfig(parallel=True), # Gap 2: Enable parallel execution
|
||||
llm="gpt-4o-mini", # Use a real model
|
||||
stream=True # Gap 3: Enable streaming
|
||||
)
|
||||
|
||||
@@ -7,8 +7,6 @@ from urllib.parse import urlparse
|
||||
import importlib.util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_loglevel = os.environ.get('LOGLEVEL', 'INFO').strip().upper() or 'INFO'
|
||||
logging.basicConfig(level=_loglevel, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Constants
|
||||
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
|
||||
|
||||
@@ -18,11 +18,23 @@ class LLMEndpoint:
|
||||
api_key: Optional[str]
|
||||
|
||||
|
||||
# Map well-known model prefixes to their (env-var, default base_url).
|
||||
_PROVIDER_MAP = {
|
||||
"anthropic/": ("ANTHROPIC_API_KEY", "https://api.anthropic.com/v1"),
|
||||
"google/": ("GOOGLE_API_KEY", "https://generativelanguage.googleapis.com/v1beta"),
|
||||
"gemini/": ("GEMINI_API_KEY", "https://generativelanguage.googleapis.com/v1beta"),
|
||||
"groq/": ("GROQ_API_KEY", "https://api.groq.com/openai/v1"),
|
||||
"cohere/": ("COHERE_API_KEY", "https://api.cohere.ai/v1"),
|
||||
"openrouter/": ("OPENROUTER_API_KEY", "https://openrouter.ai/api/v1"),
|
||||
"ollama/": ("OLLAMA_API_KEY", "http://localhost:11434/v1"),
|
||||
}
|
||||
|
||||
# Documented, single precedence list. Add new providers here only.
|
||||
_MODEL_VARS = ("MODEL_NAME", "OPENAI_MODEL_NAME")
|
||||
_BASE_URL_VARS = ("OPENAI_BASE_URL", "OPENAI_API_BASE", "OLLAMA_API_BASE")
|
||||
_DEFAULT_MODEL = "gpt-4o-mini"
|
||||
_DEFAULT_BASE = "https://api.openai.com/v1"
|
||||
_DEFAULT_KEY_VAR = "OPENAI_API_KEY"
|
||||
|
||||
|
||||
def _first_set(*names: str) -> Optional[str]:
|
||||
@@ -34,14 +46,22 @@ def _first_set(*names: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _provider_from_model(model: str) -> tuple[str, str | None]:
|
||||
"""Get provider-specific API key environment variable and base URL for a model."""
|
||||
for prefix, (key_var, default_base) in _PROVIDER_MAP.items():
|
||||
if model.startswith(prefix):
|
||||
return key_var, default_base
|
||||
return _DEFAULT_KEY_VAR, None
|
||||
|
||||
|
||||
def resolve_llm_endpoint(*, default_base: str = _DEFAULT_BASE) -> LLMEndpoint:
|
||||
"""
|
||||
Resolve LLM endpoint configuration from environment variables.
|
||||
|
||||
Precedence order:
|
||||
- Model: MODEL_NAME > OPENAI_MODEL_NAME > default
|
||||
- Base URL: OPENAI_BASE_URL > OPENAI_API_BASE > OLLAMA_API_BASE > default
|
||||
- API Key: OPENAI_API_KEY
|
||||
- Base URL: OPENAI_BASE_URL > OPENAI_API_BASE > OLLAMA_API_BASE > provider default > default
|
||||
- API Key: provider-specific key (e.g., ANTHROPIC_API_KEY) > OPENAI_API_KEY fallback
|
||||
|
||||
Args:
|
||||
default_base: Default base URL if none found in environment variables
|
||||
@@ -49,8 +69,19 @@ def resolve_llm_endpoint(*, default_base: str = _DEFAULT_BASE) -> LLMEndpoint:
|
||||
Returns:
|
||||
LLMEndpoint with resolved configuration
|
||||
"""
|
||||
return LLMEndpoint(
|
||||
model=_first_set(*_MODEL_VARS) or _DEFAULT_MODEL,
|
||||
base_url=_first_set(*_BASE_URL_VARS) or default_base,
|
||||
api_key=os.environ.get("OPENAI_API_KEY"),
|
||||
)
|
||||
model = _first_set(*_MODEL_VARS) or _DEFAULT_MODEL
|
||||
key_var, provider_base = _provider_from_model(model)
|
||||
|
||||
base_url = (
|
||||
_first_set(*_BASE_URL_VARS)
|
||||
or provider_base
|
||||
or default_base
|
||||
)
|
||||
|
||||
# Try provider-specific key first; fall back to OPENAI_API_KEY only for
|
||||
# OpenAI-compatible proxies that may use a single shared key.
|
||||
api_key = os.environ.get(key_var) or (
|
||||
os.environ.get("OPENAI_API_KEY") if key_var == "OPENAI_API_KEY" else None
|
||||
)
|
||||
|
||||
return LLMEndpoint(model=model, base_url=base_url, api_key=api_key)
|
||||
@@ -20,6 +20,7 @@ from praisonaiagents.sandbox import (
|
||||
SandboxStatus,
|
||||
ResourceLimits,
|
||||
)
|
||||
from praisonaiagents.sandbox.config import SecurityPolicy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +52,48 @@ class SubprocessSandbox:
|
||||
self._is_running = False
|
||||
self._temp_dir: Optional[str] = None
|
||||
|
||||
def _build_child_env(self, policy: SecurityPolicy, overrides: Dict[str, str] | None) -> Dict[str, str]:
|
||||
"""Construct a minimal, policy-driven env for the child process."""
|
||||
# Start with the explicit env from SandboxConfig and the per-call overrides only.
|
||||
env = dict(self.config.env)
|
||||
if overrides:
|
||||
env.update(overrides)
|
||||
|
||||
# If the policy allows network, pass through proxy-related vars so the child can
|
||||
# reach the outside world; otherwise withhold them.
|
||||
if policy.allow_network:
|
||||
for var in ("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy",
|
||||
"https_proxy", "no_proxy"):
|
||||
if var in os.environ:
|
||||
env[var] = os.environ[var]
|
||||
|
||||
# Always pass a minimal PATH (so /usr/bin/python resolves) — never the host's.
|
||||
env.setdefault("PATH", "/usr/local/bin:/usr/bin:/bin")
|
||||
# HOME uses temp_dir which is set during start() - /tmp is defensive fallback # noqa: S108
|
||||
env.setdefault("HOME", self._temp_dir or "/tmp")
|
||||
return env
|
||||
|
||||
def _apply_rlimits(self, limits: ResourceLimits):
|
||||
"""Apply resource limits via POSIX setrlimit (Linux/macOS only)."""
|
||||
if os.name != "posix":
|
||||
logger.warning("Resource limits not supported on Windows - sandbox isolation is weaker")
|
||||
return
|
||||
|
||||
try:
|
||||
import resource
|
||||
if limits.memory_mb and limits.memory_mb > 0:
|
||||
bytes_ = limits.memory_mb * 1024 * 1024
|
||||
resource.setrlimit(resource.RLIMIT_AS, (bytes_, bytes_))
|
||||
if limits.max_processes and limits.max_processes > 0:
|
||||
resource.setrlimit(resource.RLIMIT_NPROC, (limits.max_processes, limits.max_processes))
|
||||
if limits.max_open_files and limits.max_open_files > 0:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (limits.max_open_files, limits.max_open_files))
|
||||
# Note: RLIMIT_CPU is process CPU time, not wall clock time - timeout is handled separately
|
||||
except ImportError:
|
||||
logger.warning("Resource module not available - resource limits not enforced")
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning(f"Failed to set resource limits: {e}")
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""Subprocess sandbox is always available."""
|
||||
@@ -107,27 +150,43 @@ class SubprocessSandbox:
|
||||
else:
|
||||
cmd = ["python", code_file]
|
||||
|
||||
process_env = os.environ.copy()
|
||||
if env:
|
||||
process_env.update(env)
|
||||
# Build environment based on security policy instead of copying host environment
|
||||
process_env = self._build_child_env(self.config.security_policy, env)
|
||||
|
||||
started_at = time.time()
|
||||
|
||||
# preexec_fn is POSIX-only; omit on Windows
|
||||
popen_kwargs = {
|
||||
"stdout": asyncio.subprocess.PIPE,
|
||||
"stderr": asyncio.subprocess.PIPE,
|
||||
"cwd": working_dir or self._temp_dir,
|
||||
"env": process_env,
|
||||
}
|
||||
if os.name == "posix":
|
||||
popen_kwargs["start_new_session"] = True
|
||||
popen_kwargs["preexec_fn"] = lambda: self._apply_rlimits(limits)
|
||||
else:
|
||||
logger.warning("Resource limits and session isolation not available on Windows")
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=working_dir or self._temp_dir,
|
||||
env=process_env,
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(*cmd, **popen_kwargs)
|
||||
|
||||
try:
|
||||
# Truncate output to max_output_size after reading
|
||||
max_output_size = self.config.security_policy.max_output_size
|
||||
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=limits.timeout_seconds,
|
||||
)
|
||||
|
||||
# Truncate output if it exceeds max_output_size
|
||||
if max_output_size and max_output_size > 0:
|
||||
if len(stdout) > max_output_size:
|
||||
stdout = stdout[:max_output_size] + b"\n[OUTPUT TRUNCATED]"
|
||||
if len(stderr) > max_output_size:
|
||||
stderr = stderr[:max_output_size] + b"\n[OUTPUT TRUNCATED]"
|
||||
|
||||
completed_at = time.time()
|
||||
|
||||
return SandboxResult(
|
||||
@@ -141,7 +200,15 @@ class SubprocessSandbox:
|
||||
completed_at=completed_at,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
# Kill the whole process group, not just the leader
|
||||
try:
|
||||
if os.name == "posix":
|
||||
import signal
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
else:
|
||||
proc.kill()
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
|
||||
return SandboxResult(
|
||||
@@ -211,20 +278,26 @@ class SubprocessSandbox:
|
||||
from ._shell import build_argv
|
||||
cmd = build_argv(command, shell=shell)
|
||||
|
||||
process_env = os.environ.copy()
|
||||
if env:
|
||||
process_env.update(env)
|
||||
# Build environment based on security policy instead of copying host environment
|
||||
process_env = self._build_child_env(self.config.security_policy, env)
|
||||
|
||||
started_at = time.time()
|
||||
|
||||
# preexec_fn is POSIX-only; omit on Windows
|
||||
popen_kwargs = {
|
||||
"stdout": asyncio.subprocess.PIPE,
|
||||
"stderr": asyncio.subprocess.PIPE,
|
||||
"cwd": working_dir or self._temp_dir,
|
||||
"env": process_env,
|
||||
}
|
||||
if os.name == "posix":
|
||||
popen_kwargs["start_new_session"] = True
|
||||
popen_kwargs["preexec_fn"] = lambda: self._apply_rlimits(limits)
|
||||
else:
|
||||
logger.warning("Resource limits and session isolation not available on Windows")
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=working_dir or self._temp_dir,
|
||||
env=process_env,
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(*cmd, **popen_kwargs)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
@@ -232,6 +305,14 @@ class SubprocessSandbox:
|
||||
timeout=limits.timeout_seconds,
|
||||
)
|
||||
|
||||
# Truncate output if it exceeds max_output_size
|
||||
max_output_size = self.config.security_policy.max_output_size
|
||||
if max_output_size and max_output_size > 0:
|
||||
if len(stdout) > max_output_size:
|
||||
stdout = stdout[:max_output_size] + b"\n[OUTPUT TRUNCATED]"
|
||||
if len(stderr) > max_output_size:
|
||||
stderr = stderr[:max_output_size] + b"\n[OUTPUT TRUNCATED]"
|
||||
|
||||
completed_at = time.time()
|
||||
|
||||
return SandboxResult(
|
||||
@@ -245,7 +326,15 @@ class SubprocessSandbox:
|
||||
completed_at=completed_at,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
# Kill the whole process group, not just the leader
|
||||
try:
|
||||
if os.name == "posix":
|
||||
import signal
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
else:
|
||||
proc.kill()
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
|
||||
return SandboxResult(
|
||||
|
||||
@@ -126,3 +126,119 @@ class TestLLMEndpointDataclass:
|
||||
assert hasattr(ep, "model")
|
||||
assert hasattr(ep, "base_url")
|
||||
assert hasattr(ep, "api_key")
|
||||
|
||||
|
||||
class TestProviderMapping:
|
||||
"""Tests for provider-specific API key and base URL resolution."""
|
||||
|
||||
def test_anthropic_model_uses_anthropic_key(self):
|
||||
env = {"ANTHROPIC_API_KEY": "sk-anthropic-test"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
# Use direct function call to test provider mapping
|
||||
from praisonai.llm.env import _provider_from_model
|
||||
key_var, base_url = _provider_from_model("anthropic/claude-3-5-sonnet")
|
||||
assert key_var == "ANTHROPIC_API_KEY"
|
||||
assert base_url == "https://api.anthropic.com/v1"
|
||||
|
||||
def test_anthropic_model_with_custom_env(self):
|
||||
env = {
|
||||
"MODEL_NAME": "anthropic/claude-3-5-sonnet",
|
||||
"ANTHROPIC_API_KEY": "sk-anthropic-test"
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "anthropic/claude-3-5-sonnet"
|
||||
assert ep.api_key == "sk-anthropic-test"
|
||||
assert ep.base_url == "https://api.anthropic.com/v1"
|
||||
|
||||
def test_anthropic_model_no_fallback_to_openai(self):
|
||||
"""Critical security test: Anthropic models should NOT fall back to OPENAI_API_KEY."""
|
||||
env = {
|
||||
"MODEL_NAME": "anthropic/claude-3-5-sonnet",
|
||||
"OPENAI_API_KEY": "sk-openai-test"
|
||||
# ANTHROPIC_API_KEY intentionally missing
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "anthropic/claude-3-5-sonnet"
|
||||
assert ep.api_key is None # Should NOT use OPENAI_API_KEY
|
||||
assert ep.base_url == "https://api.anthropic.com/v1"
|
||||
|
||||
def test_groq_model_mapping(self):
|
||||
env = {
|
||||
"MODEL_NAME": "groq/llama-3.1-70b-versatile",
|
||||
"GROQ_API_KEY": "gsk-test-key"
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "groq/llama-3.1-70b-versatile"
|
||||
assert ep.api_key == "gsk-test-key"
|
||||
assert ep.base_url == "https://api.groq.com/openai/v1"
|
||||
|
||||
def test_openai_model_uses_openai_key(self):
|
||||
"""OpenAI models should use OPENAI_API_KEY normally."""
|
||||
env = {
|
||||
"MODEL_NAME": "gpt-4o",
|
||||
"OPENAI_API_KEY": "sk-openai-test"
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "gpt-4o"
|
||||
assert ep.api_key == "sk-openai-test"
|
||||
assert ep.base_url == "https://api.openai.com/v1"
|
||||
|
||||
def test_ollama_model_mapping(self):
|
||||
env = {
|
||||
"MODEL_NAME": "ollama/llama3",
|
||||
"OLLAMA_API_KEY": "ollama-test"
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "ollama/llama3"
|
||||
assert ep.api_key == "ollama-test"
|
||||
assert ep.base_url == "http://localhost:11434/v1"
|
||||
|
||||
def test_google_model_mapping(self):
|
||||
env = {
|
||||
"MODEL_NAME": "google/gemini-pro",
|
||||
"GOOGLE_API_KEY": "google-test-key"
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "google/gemini-pro"
|
||||
assert ep.api_key == "google-test-key"
|
||||
assert ep.base_url == "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
def test_all_providers_have_correct_base_urls(self):
|
||||
"""Ensure all provider mappings have proper base URLs (not None)."""
|
||||
from praisonai.llm.env import _PROVIDER_MAP
|
||||
|
||||
expected_urls = {
|
||||
"anthropic/": "https://api.anthropic.com/v1",
|
||||
"google/": "https://generativelanguage.googleapis.com/v1beta",
|
||||
"gemini/": "https://generativelanguage.googleapis.com/v1beta",
|
||||
"groq/": "https://api.groq.com/openai/v1",
|
||||
"cohere/": "https://api.cohere.ai/v1",
|
||||
"openrouter/": "https://openrouter.ai/api/v1",
|
||||
"ollama/": "http://localhost:11434/v1",
|
||||
}
|
||||
|
||||
for prefix, (key_var, base_url) in _PROVIDER_MAP.items():
|
||||
assert base_url is not None, f"Provider {prefix} has None base URL"
|
||||
assert base_url == expected_urls[prefix], f"Provider {prefix} has unexpected URL {base_url}"
|
||||
|
||||
def test_provider_key_precedence_no_cross_contamination(self):
|
||||
"""Ensure providers don't accidentally use other providers' keys."""
|
||||
env = {
|
||||
"MODEL_NAME": "anthropic/claude-3-5-sonnet",
|
||||
"GROQ_API_KEY": "gsk-groq-key",
|
||||
"GOOGLE_API_KEY": "google-key",
|
||||
"OPENAI_API_KEY": "sk-openai-key"
|
||||
# ANTHROPIC_API_KEY intentionally missing
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
ep = resolve_llm_endpoint()
|
||||
assert ep.model == "anthropic/claude-3-5-sonnet"
|
||||
# Should be None, not any other provider's key
|
||||
assert ep.api_key is None
|
||||
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
Unit tests for SubprocessSandbox security and environment isolation.
|
||||
|
||||
Tests cover:
|
||||
- SecurityPolicy-based environment isolation
|
||||
- Resource limits enforcement via setrlimit
|
||||
- Cross-platform compatibility (Windows/POSIX)
|
||||
- Process group termination and timeout handling
|
||||
- Output size limits and truncation
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
try:
|
||||
from praisonai.sandbox.subprocess import SubprocessSandbox
|
||||
from praisonaiagents.sandbox import SandboxConfig, ResourceLimits, SandboxStatus
|
||||
from praisonaiagents.sandbox.config import SecurityPolicy
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import sandbox modules: {e}", allow_module_level=True)
|
||||
|
||||
|
||||
class TestEnvironmentIsolation:
|
||||
"""Tests for SecurityPolicy-based environment isolation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strict_policy_blocks_host_env(self):
|
||||
"""Strict policy should not inherit host environment variables."""
|
||||
config = SandboxConfig(
|
||||
env={},
|
||||
security_policy=SecurityPolicy.strict()
|
||||
)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
# Build environment for a subprocess
|
||||
env = sandbox._build_child_env(SecurityPolicy.strict(), {})
|
||||
|
||||
# Should have minimal required vars only
|
||||
assert "PATH" in env
|
||||
assert "HOME" in env
|
||||
|
||||
# Should NOT have host environment variables
|
||||
# (except the ones we explicitly allow for basic function)
|
||||
host_vars = set(os.environ.keys())
|
||||
child_vars = set(env.keys())
|
||||
leaked_vars = host_vars & child_vars - {"PATH", "HOME"}
|
||||
|
||||
# Some proxy vars might be allowed if network is enabled
|
||||
proxy_vars = {"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"}
|
||||
leaked_vars = leaked_vars - proxy_vars
|
||||
|
||||
assert len(leaked_vars) == 0, f"Host env vars leaked to child: {leaked_vars}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allow_network_passes_proxy_vars(self):
|
||||
"""When allow_network=True, proxy vars should be passed through."""
|
||||
policy = SecurityPolicy(
|
||||
allow_network=True,
|
||||
allow_filesystem_write=False,
|
||||
max_output_size=1024 * 1024
|
||||
)
|
||||
|
||||
config = SandboxConfig(env={}, security_policy=policy)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
# Set some proxy vars in host environment
|
||||
proxy_env = {
|
||||
"HTTP_PROXY": "http://proxy.company.com:8080",
|
||||
"HTTPS_PROXY": "https://proxy.company.com:8080",
|
||||
"NO_PROXY": "localhost,127.0.0.1"
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, proxy_env):
|
||||
env = sandbox._build_child_env(policy, {})
|
||||
|
||||
# Proxy vars should be present when network is allowed
|
||||
assert env.get("HTTP_PROXY") == "http://proxy.company.com:8080"
|
||||
assert env.get("HTTPS_PROXY") == "https://proxy.company.com:8080"
|
||||
assert env.get("NO_PROXY") == "localhost,127.0.0.1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_network_blocks_proxy_vars(self):
|
||||
"""When allow_network=False, proxy vars should be blocked."""
|
||||
policy = SecurityPolicy(
|
||||
allow_network=False,
|
||||
allow_filesystem_write=False,
|
||||
max_output_size=1024 * 1024
|
||||
)
|
||||
|
||||
config = SandboxConfig(env={}, security_policy=policy)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
# Set proxy vars in host environment
|
||||
proxy_env = {
|
||||
"HTTP_PROXY": "http://proxy.company.com:8080",
|
||||
"HTTPS_PROXY": "https://proxy.company.com:8080"
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, proxy_env):
|
||||
env = sandbox._build_child_env(policy, {})
|
||||
|
||||
# Proxy vars should NOT be present when network is blocked
|
||||
assert "HTTP_PROXY" not in env
|
||||
assert "HTTPS_PROXY" not in env
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_config_env_preserved(self):
|
||||
"""Environment variables from SandboxConfig.env should always be included."""
|
||||
explicit_env = {
|
||||
"CUSTOM_VAR": "custom_value",
|
||||
"API_KEY": "secret_key"
|
||||
}
|
||||
|
||||
config = SandboxConfig(
|
||||
env=explicit_env,
|
||||
security_policy=SecurityPolicy.strict()
|
||||
)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
env = sandbox._build_child_env(SecurityPolicy.strict(), {})
|
||||
|
||||
# Explicit config env should be preserved
|
||||
assert env["CUSTOM_VAR"] == "custom_value"
|
||||
assert env["API_KEY"] == "secret_key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_call_overrides_merge_correctly(self):
|
||||
"""Per-call env overrides should merge with config env."""
|
||||
config_env = {"CONFIG_VAR": "config_value"}
|
||||
call_overrides = {"OVERRIDE_VAR": "override_value", "CONFIG_VAR": "overridden"}
|
||||
|
||||
config = SandboxConfig(
|
||||
env=config_env,
|
||||
security_policy=SecurityPolicy.strict()
|
||||
)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
env = sandbox._build_child_env(SecurityPolicy.strict(), call_overrides)
|
||||
|
||||
# Call overrides should win over config
|
||||
assert env["CONFIG_VAR"] == "overridden"
|
||||
assert env["OVERRIDE_VAR"] == "override_value"
|
||||
|
||||
|
||||
class TestResourceLimits:
|
||||
"""Tests for POSIX resource limits enforcement."""
|
||||
|
||||
def test_apply_rlimits_sets_memory_limit(self):
|
||||
"""Resource limits should be applied via setrlimit on POSIX systems."""
|
||||
if os.name != "posix":
|
||||
pytest.skip("Resource limits only supported on POSIX systems")
|
||||
|
||||
sandbox = SubprocessSandbox()
|
||||
limits = ResourceLimits(
|
||||
memory_mb=128,
|
||||
timeout_seconds=30,
|
||||
max_processes=10,
|
||||
max_open_files=50
|
||||
)
|
||||
|
||||
# Mock the resource module to verify correct calls
|
||||
with patch('praisonai.sandbox.subprocess.resource') as mock_resource:
|
||||
mock_resource.RLIMIT_AS = 9
|
||||
mock_resource.RLIMIT_NPROC = 7
|
||||
mock_resource.RLIMIT_NOFILE = 8
|
||||
|
||||
sandbox._apply_rlimits(limits)
|
||||
|
||||
# Verify setrlimit was called with correct memory limit (128MB in bytes)
|
||||
expected_memory = 128 * 1024 * 1024
|
||||
mock_resource.setrlimit.assert_any_call(9, (expected_memory, expected_memory))
|
||||
mock_resource.setrlimit.assert_any_call(7, (10, 10)) # max_processes
|
||||
mock_resource.setrlimit.assert_any_call(8, (50, 50)) # max_open_files
|
||||
|
||||
def test_apply_rlimits_handles_missing_resource_module(self):
|
||||
"""Should handle gracefully when resource module is not available."""
|
||||
sandbox = SubprocessSandbox()
|
||||
limits = ResourceLimits(memory_mb=128, timeout_seconds=30)
|
||||
|
||||
with patch('praisonai.sandbox.subprocess.resource', side_effect=ImportError):
|
||||
with patch('praisonai.sandbox.subprocess.logger') as mock_logger:
|
||||
sandbox._apply_rlimits(limits)
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
def test_apply_rlimits_windows_warning(self):
|
||||
"""Should log warning on Windows where resource limits are not supported."""
|
||||
sandbox = SubprocessSandbox()
|
||||
limits = ResourceLimits(memory_mb=128, timeout_seconds=30)
|
||||
|
||||
with patch('os.name', 'nt'): # Windows
|
||||
with patch('praisonai.sandbox.subprocess.logger') as mock_logger:
|
||||
sandbox._apply_rlimits(limits)
|
||||
mock_logger.warning.assert_called_with(
|
||||
"Resource limits not supported on Windows - sandbox isolation is weaker"
|
||||
)
|
||||
|
||||
|
||||
class TestOutputSizeLimit:
|
||||
"""Tests for output size limiting and truncation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_output_truncation_applied(self):
|
||||
"""Output should be truncated when it exceeds max_output_size."""
|
||||
# Create a policy with small output limit for testing
|
||||
policy = SecurityPolicy(
|
||||
allow_network=False,
|
||||
allow_filesystem_write=True,
|
||||
max_output_size=100 # Very small for testing
|
||||
)
|
||||
|
||||
config = SandboxConfig(security_policy=policy)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
await sandbox.start()
|
||||
|
||||
try:
|
||||
# Generate output larger than the limit
|
||||
large_output_code = 'print("x" * 200)' # 200 chars, limit is 100
|
||||
|
||||
result = await sandbox.execute(large_output_code)
|
||||
|
||||
assert result.status == SandboxStatus.COMPLETED
|
||||
# Output should be truncated
|
||||
assert len(result.stdout) <= 100 + len("\n[OUTPUT TRUNCATED]")
|
||||
assert "[OUTPUT TRUNCATED]" in result.stdout
|
||||
|
||||
finally:
|
||||
await sandbox.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_small_output_not_truncated(self):
|
||||
"""Small output should not be truncated."""
|
||||
policy = SecurityPolicy(
|
||||
allow_network=False,
|
||||
allow_filesystem_write=True,
|
||||
max_output_size=1000
|
||||
)
|
||||
|
||||
config = SandboxConfig(security_policy=policy)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
await sandbox.start()
|
||||
|
||||
try:
|
||||
result = await sandbox.execute('print("Hello, World!")')
|
||||
|
||||
assert result.status == SandboxStatus.COMPLETED
|
||||
assert "[OUTPUT TRUNCATED]" not in result.stdout
|
||||
assert "Hello, World!" in result.stdout
|
||||
|
||||
finally:
|
||||
await sandbox.stop()
|
||||
|
||||
|
||||
class TestCrossPlatformCompatibility:
|
||||
"""Tests for Windows/POSIX compatibility."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_windows_subprocess_creation(self):
|
||||
"""On Windows, subprocess should be created without POSIX-only options."""
|
||||
sandbox = SubprocessSandbox()
|
||||
limits = ResourceLimits(timeout_seconds=10)
|
||||
|
||||
with patch('os.name', 'nt'): # Windows
|
||||
with patch('asyncio.create_subprocess_exec') as mock_create:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.communicate.return_value = (b"output", b"")
|
||||
mock_proc.returncode = 0
|
||||
mock_create.return_value = mock_proc
|
||||
|
||||
await sandbox.start()
|
||||
await sandbox.execute('print("test")', limits=limits)
|
||||
|
||||
# Verify subprocess was created without POSIX-only options
|
||||
call_kwargs = mock_create.call_args[1]
|
||||
assert "preexec_fn" not in call_kwargs
|
||||
assert "start_new_session" not in call_kwargs
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_posix_subprocess_creation(self):
|
||||
"""On POSIX systems, subprocess should use security options."""
|
||||
sandbox = SubprocessSandbox()
|
||||
limits = ResourceLimits(timeout_seconds=10)
|
||||
|
||||
with patch('os.name', 'posix'):
|
||||
with patch('asyncio.create_subprocess_exec') as mock_create:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.communicate.return_value = (b"output", b"")
|
||||
mock_proc.returncode = 0
|
||||
mock_create.return_value = mock_proc
|
||||
|
||||
await sandbox.start()
|
||||
await sandbox.execute('print("test")', limits=limits)
|
||||
|
||||
# Verify subprocess was created with POSIX security options
|
||||
call_kwargs = mock_create.call_args[1]
|
||||
assert "preexec_fn" in call_kwargs
|
||||
assert call_kwargs["start_new_session"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_handling_windows(self):
|
||||
"""Timeout handling should work correctly on Windows."""
|
||||
sandbox = SubprocessSandbox()
|
||||
|
||||
with patch('os.name', 'nt'): # Windows
|
||||
with patch('asyncio.create_subprocess_exec') as mock_create:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.pid = 1234
|
||||
mock_proc.communicate.side_effect = asyncio.TimeoutError()
|
||||
mock_proc.wait.return_value = None
|
||||
mock_create.return_value = mock_proc
|
||||
|
||||
await sandbox.start()
|
||||
result = await sandbox.execute('sleep 60', limits=ResourceLimits(timeout_seconds=1))
|
||||
|
||||
# On Windows, should call proc.kill() for timeout
|
||||
mock_proc.kill.assert_called()
|
||||
assert result.status == SandboxStatus.TIMEOUT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_handling_posix(self):
|
||||
"""Timeout handling should use process groups on POSIX."""
|
||||
sandbox = SubprocessSandbox()
|
||||
|
||||
with patch('os.name', 'posix'):
|
||||
with patch('asyncio.create_subprocess_exec') as mock_create:
|
||||
with patch('os.killpg') as mock_killpg:
|
||||
with patch('signal.SIGKILL', 9):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.pid = 1234
|
||||
mock_proc.communicate.side_effect = asyncio.TimeoutError()
|
||||
mock_proc.wait.return_value = None
|
||||
mock_create.return_value = mock_proc
|
||||
|
||||
await sandbox.start()
|
||||
result = await sandbox.execute('sleep 60', limits=ResourceLimits(timeout_seconds=1))
|
||||
|
||||
# On POSIX, should use killpg to kill process group
|
||||
mock_killpg.assert_called_with(1234, 9)
|
||||
assert result.status == SandboxStatus.TIMEOUT
|
||||
|
||||
|
||||
class TestSecurityRegressions:
|
||||
"""Regression tests for security vulnerabilities."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_host_env_leakage_regression(self):
|
||||
"""Regression: Ensure host environment doesn't leak into sandbox."""
|
||||
# Set some sensitive host environment variables (using clearly fake test values)
|
||||
sensitive_env = {
|
||||
"AWS_SECRET_ACCESS_KEY": "FAKE_AWS_KEY_FOR_TESTING_ONLY",
|
||||
"DATABASE_PASSWORD": "FAKE_DB_PASSWORD_FOR_TESTING_ONLY",
|
||||
"API_TOKEN": "FAKE_API_TOKEN_FOR_TESTING_ONLY"
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, sensitive_env):
|
||||
config = SandboxConfig(
|
||||
env={},
|
||||
security_policy=SecurityPolicy.strict()
|
||||
)
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
env = sandbox._build_child_env(SecurityPolicy.strict(), {})
|
||||
|
||||
# Sensitive vars should not leak
|
||||
assert "AWS_SECRET_ACCESS_KEY" not in env
|
||||
assert "DATABASE_PASSWORD" not in env
|
||||
assert "API_TOKEN" not in env
|
||||
|
||||
def test_temp_dir_fallback_security(self):
|
||||
"""Ensure /tmp fallback is documented and appropriate."""
|
||||
config = SandboxConfig(security_policy=SecurityPolicy.strict())
|
||||
sandbox = SubprocessSandbox(config)
|
||||
|
||||
# Before start(), _temp_dir is None, should fall back to /tmp
|
||||
env = sandbox._build_child_env(SecurityPolicy.strict(), {})
|
||||
assert env["HOME"] == "/tmp"
|
||||
|
||||
# The fallback is defensive - normally start() sets _temp_dir
|
||||
# This test documents that /tmp is an acceptable fallback for HOME
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Regression tests for logging configuration violations.
|
||||
|
||||
Tests cover:
|
||||
- inc/models.py should NOT call logging.basicConfig() at import time
|
||||
- Root logger should not be mutated by module imports
|
||||
- Logging configuration should be deferred to application entry points
|
||||
"""
|
||||
|
||||
import logging
|
||||
import importlib
|
||||
import sys
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestLoggingRegressions:
|
||||
"""Regression tests for improper logging configuration at import time."""
|
||||
|
||||
def test_inc_models_no_logging_basicconfig_at_import(self):
|
||||
"""
|
||||
Regression test: inc/models.py should NOT call logging.basicConfig() during import.
|
||||
|
||||
This was a critical architectural violation where inc/models.py mutated the root
|
||||
logger configuration at module load time, hijacking embedders' logging setup.
|
||||
"""
|
||||
# Mock logging.basicConfig to detect if it's called
|
||||
with patch('logging.basicConfig') as mock_basicconfig:
|
||||
# Force reimport of inc.models to test import-time behavior
|
||||
module_name = 'praisonai.inc.models'
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
try:
|
||||
# Import the module - this should NOT call logging.basicConfig()
|
||||
import praisonai.inc.models
|
||||
|
||||
# Verify logging.basicConfig() was not called during import
|
||||
mock_basicconfig.assert_not_called()
|
||||
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import praisonai.inc.models: {e}")
|
||||
|
||||
def test_root_logger_unchanged_after_import(self):
|
||||
"""Verify that importing inc.models does not modify root logger configuration."""
|
||||
# Capture initial root logger state
|
||||
root_logger = logging.getLogger()
|
||||
initial_level = root_logger.level
|
||||
initial_handlers_count = len(root_logger.handlers)
|
||||
initial_handlers = root_logger.handlers.copy()
|
||||
|
||||
# Force reimport to test import-time side effects
|
||||
module_name = 'praisonai.inc.models'
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
try:
|
||||
# Import should not change root logger
|
||||
import praisonai.inc.models
|
||||
|
||||
# Verify root logger state is unchanged
|
||||
assert root_logger.level == initial_level, "Root logger level was modified"
|
||||
assert len(root_logger.handlers) == initial_handlers_count, "Root logger handlers were modified"
|
||||
assert root_logger.handlers == initial_handlers, "Root logger handlers list was modified"
|
||||
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import praisonai.inc.models: {e}")
|
||||
|
||||
def test_logging_module_import_order_independence(self):
|
||||
"""
|
||||
Test that inc.models import doesn't interfere with logging setup regardless of import order.
|
||||
|
||||
This tests the scenario where a user application has already configured logging
|
||||
before importing PraisonAI modules.
|
||||
"""
|
||||
# Set up a custom logging configuration before import
|
||||
custom_logger = logging.getLogger("test_app")
|
||||
custom_handler = logging.StreamHandler()
|
||||
custom_handler.setLevel(logging.DEBUG)
|
||||
custom_logger.addHandler(custom_handler)
|
||||
custom_logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Force reimport after setting up custom logging
|
||||
module_name = 'praisonai.inc.models'
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
try:
|
||||
# Import should not interfere with existing logging setup
|
||||
import praisonai.inc.models
|
||||
|
||||
# Custom logger should remain unchanged
|
||||
assert custom_logger.level == logging.DEBUG
|
||||
assert len(custom_logger.handlers) == 1
|
||||
assert custom_logger.handlers[0] is custom_handler
|
||||
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import praisonai.inc.models: {e}")
|
||||
finally:
|
||||
# Cleanup
|
||||
custom_logger.removeHandler(custom_handler)
|
||||
|
||||
def test_no_side_effects_on_repeated_imports(self):
|
||||
"""Verify that repeated imports of inc.models don't accumulate side effects."""
|
||||
# Capture initial state
|
||||
root_logger = logging.getLogger()
|
||||
initial_handlers_count = len(root_logger.handlers)
|
||||
|
||||
try:
|
||||
# Import multiple times
|
||||
import praisonai.inc.models
|
||||
importlib.reload(praisonai.inc.models)
|
||||
importlib.reload(praisonai.inc.models)
|
||||
|
||||
# Should not accumulate handlers or other side effects
|
||||
assert len(root_logger.handlers) == initial_handlers_count
|
||||
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import praisonai.inc.models: {e}")
|
||||
|
||||
def test_module_level_logging_pattern_compliance(self):
|
||||
"""
|
||||
Test that inc.models follows the correct logging pattern:
|
||||
- Get logger at module level: logger = logging.getLogger(__name__)
|
||||
- Do NOT configure logging at module level
|
||||
"""
|
||||
try:
|
||||
import praisonai.inc.models as models_module
|
||||
|
||||
# Module should have a logger instance
|
||||
assert hasattr(models_module, 'logger'), "Module should have a 'logger' attribute"
|
||||
|
||||
# Logger should be correctly named for the module
|
||||
expected_name = 'praisonai.inc.models'
|
||||
assert models_module.logger.name == expected_name, f"Logger name should be {expected_name}"
|
||||
|
||||
# Logger should be a proper Logger instance, not configured at module level
|
||||
assert isinstance(models_module.logger, logging.Logger)
|
||||
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Could not import praisonai.inc.models: {e}")
|
||||
|
||||
def test_architectural_compliance_no_import_time_config(self):
|
||||
"""
|
||||
Architectural compliance test: No PraisonAI module should configure logging at import.
|
||||
|
||||
This is a broader architectural test to ensure the logging policy is followed
|
||||
across the codebase.
|
||||
"""
|
||||
# List of modules that should not configure logging at import time
|
||||
modules_to_test = [
|
||||
'praisonai.inc.models',
|
||||
# Add other modules here as needed for broader coverage
|
||||
]
|
||||
|
||||
for module_name in modules_to_test:
|
||||
with patch('logging.basicConfig') as mock_basicconfig:
|
||||
# Force clean import
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
mock_basicconfig.assert_not_called()
|
||||
except ImportError:
|
||||
# Skip modules that aren't available in test environment
|
||||
continue
|
||||
|
||||
|
||||
class TestLoggingPolicyDocumentation:
|
||||
"""Tests that document and verify the correct logging policy."""
|
||||
|
||||
def test_logging_policy_documentation(self):
|
||||
"""
|
||||
Document the correct logging policy for PraisonAI modules:
|
||||
|
||||
1. Modules should get loggers: logger = logging.getLogger(__name__)
|
||||
2. Modules should NOT configure logging at import time
|
||||
3. CLI entry points should configure logging via _logging.py
|
||||
4. Library users control their own logging configuration
|
||||
"""
|
||||
# This test serves as documentation and verification
|
||||
|
||||
# Correct pattern for modules:
|
||||
correct_logger_pattern = "logger = logging.getLogger(__name__)"
|
||||
|
||||
# Incorrect pattern (what was fixed):
|
||||
incorrect_pattern = "logging.basicConfig()"
|
||||
|
||||
# Verify the correct pattern works
|
||||
test_logger = logging.getLogger("test.module.name")
|
||||
assert isinstance(test_logger, logging.Logger)
|
||||
assert test_logger.name == "test.module.name"
|
||||
|
||||
# Document that basicConfig should only be called by applications, not libraries
|
||||
with patch('logging.basicConfig') as mock_basicconfig:
|
||||
# This would be appropriate for an application entry point:
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# But should NOT be called by library modules during import
|
||||
pass # Library modules should not call this at all
|
||||
Reference in new issue
Block a user