mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e9826bda7 | ||
|
|
fc298049e2 | ||
|
|
0b7bf10820 | ||
|
|
cbb7c726aa | ||
|
|
21d1986d0c | ||
|
|
26b5792192 | ||
|
|
afcea5800e | ||
|
|
98cfdcc9ad | ||
|
|
6f2df7db2a | ||
|
|
495d4604fe | ||
|
|
a4386796c9 | ||
|
|
e1f9ae5b6b | ||
|
|
aa5315b90e | ||
|
|
b9d3ede110 | ||
|
|
441b428cc9 | ||
|
|
bcae07cf58 | ||
|
|
bf28787705 | ||
|
|
5a8f4aedf8 | ||
|
|
459a12a656 | ||
|
|
d9224adc70 | ||
|
|
82f1d8881b | ||
|
|
ca07ebf813 | ||
|
|
2b2443ff63 | ||
|
|
c8938c6aeb |
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
|
||||
@@ -357,7 +357,7 @@ Methods:
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None, tool_policy_mode: Optional[Literal['intersect', 'passthrough']] = None, blocked_tools: Optional[List[str]] = None) -> Handoff</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">compress_history</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.53" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.53" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.53" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ def example_with_config():
|
||||
instructions="You are a research assistant",
|
||||
memory=MemoryConfig(
|
||||
backend="file",
|
||||
memory={"user_id": "researcher_001"},
|
||||
user_id="researcher_001",
|
||||
auto_memory=True,
|
||||
),
|
||||
reflection=ReflectionConfig(
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Message Steering Example
|
||||
|
||||
Demonstrates real-time message steering for agents during execution.
|
||||
Shows how to send guidance messages to agents while they're running tasks.
|
||||
|
||||
Usage:
|
||||
# Python API
|
||||
python message_steering_example.py
|
||||
|
||||
# CLI with steering enabled
|
||||
praisonai --message-steering "Write a detailed report about AI trends"
|
||||
|
||||
# YAML (add message_steering: true to agent config)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def basic_message_steering():
|
||||
"""Basic message steering example - enable steering and send guidance."""
|
||||
print("🎯 Basic Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Create agent with message steering enabled
|
||||
agent = Agent(
|
||||
name="researcher",
|
||||
instructions="You are a helpful research assistant",
|
||||
message_steering=True, # Enable real-time message steering
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("✅ Agent created with message steering enabled")
|
||||
print(f"📊 Steering status: {agent.get_steering_status()}")
|
||||
|
||||
# Send some guidance
|
||||
msg_id_1 = agent.steer("Focus on recent developments from 2024")
|
||||
msg_id_2 = agent.steer("Keep response under 200 words", priority=10)
|
||||
|
||||
print(f"📨 Sent steering messages: {msg_id_1}, {msg_id_2}")
|
||||
print(f"📊 Steering status: {agent.get_steering_status()}")
|
||||
|
||||
# Execute task - steering will be automatically injected
|
||||
result = agent.start("Summarize the latest AI trends")
|
||||
|
||||
print("\n📝 Result:")
|
||||
print(result)
|
||||
|
||||
|
||||
def threaded_message_steering():
|
||||
"""Advanced example - send steering messages while agent is running."""
|
||||
print("\n🚀 Threaded Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
agent = Agent(
|
||||
name="writer",
|
||||
instructions="You are a content writer",
|
||||
message_steering=True,
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
result_holder = [None]
|
||||
|
||||
def run_agent():
|
||||
"""Run agent in background thread."""
|
||||
result = agent.start("Write a comprehensive article about quantum computing")
|
||||
result_holder[0] = result
|
||||
|
||||
# Start agent in background
|
||||
thread = threading.Thread(target=run_agent)
|
||||
thread.start()
|
||||
|
||||
# Send steering messages while it's running
|
||||
time.sleep(0.5) # Let execution start
|
||||
agent.steer("Make it accessible to beginners")
|
||||
|
||||
time.sleep(1.0)
|
||||
agent.steer("Include practical applications", priority=15)
|
||||
|
||||
time.sleep(1.5)
|
||||
agent.steer("Keep it under 300 words total", priority=20)
|
||||
|
||||
# Wait for completion
|
||||
thread.join(timeout=60)
|
||||
|
||||
if result_holder[0]:
|
||||
print("\n📝 Final Result:")
|
||||
print(result_holder[0])
|
||||
else:
|
||||
print("⚠️ Agent execution timed out")
|
||||
|
||||
|
||||
async def async_message_steering():
|
||||
"""Async example showing message steering with async execution."""
|
||||
print("\n⚡ Async Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
agent = Agent(
|
||||
name="analyst",
|
||||
instructions="You are a data analyst",
|
||||
message_steering=True,
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Start async task
|
||||
async def run_analysis():
|
||||
return await agent.astart("Analyze the impact of AI on job markets")
|
||||
|
||||
# Create task
|
||||
task = asyncio.create_task(run_analysis())
|
||||
|
||||
# Send steering while running
|
||||
await asyncio.sleep(0.3)
|
||||
agent.steer("Focus on positive opportunities")
|
||||
|
||||
await asyncio.sleep(0.7)
|
||||
agent.steer("Include specific examples", priority=12)
|
||||
|
||||
# Wait for result
|
||||
result = await task
|
||||
|
||||
print("\n📝 Analysis Result:")
|
||||
print(result)
|
||||
|
||||
|
||||
def cli_yaml_examples():
|
||||
"""Show CLI and YAML usage examples."""
|
||||
print("\n💻 CLI & YAML Examples")
|
||||
print("=" * 50)
|
||||
|
||||
print("🔧 CLI Usage:")
|
||||
print(" praisonai --message-steering 'Your task here'")
|
||||
print(" # Then in another terminal:")
|
||||
print(" # agent.steer('Additional guidance')")
|
||||
|
||||
print("\n📄 YAML Configuration:")
|
||||
yaml_example = """
|
||||
# agents.yaml
|
||||
framework: praisonai
|
||||
roles:
|
||||
researcher:
|
||||
role: Research Assistant
|
||||
goal: Conduct thorough research
|
||||
backstory: Expert researcher with steering support
|
||||
message_steering: true # Enable steering for this agent
|
||||
tools:
|
||||
- internet_search
|
||||
- write_file
|
||||
tasks:
|
||||
research_task:
|
||||
description: Research AI trends and write report
|
||||
expected_output: Comprehensive research report
|
||||
"""
|
||||
print(yaml_example.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run examples
|
||||
basic_message_steering()
|
||||
threaded_message_steering()
|
||||
|
||||
# Run async example
|
||||
asyncio.run(async_message_steering())
|
||||
|
||||
# Show CLI/YAML usage
|
||||
cli_yaml_examples()
|
||||
|
||||
print("\n✨ Message steering examples completed!")
|
||||
print("📚 See docs for more advanced patterns and priority levels")
|
||||
@@ -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)
|
||||
@@ -5,7 +5,7 @@ Demonstrates using Agent instances directly as workflow steps.
|
||||
Each agent processes the input and passes output to the next agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
|
||||
# Create agents with specific roles
|
||||
researcher = Agent(
|
||||
|
||||
@@ -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,11 +54,13 @@ workflow = AgentFlow(
|
||||
analyzer, # Always runs
|
||||
Task(
|
||||
name="tech_review",
|
||||
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 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__":
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
framework: praisonai
|
||||
topic: AI Research with Message Steering
|
||||
|
||||
roles:
|
||||
researcher:
|
||||
role: AI Research Analyst
|
||||
goal: Conduct comprehensive AI research with real-time guidance
|
||||
backstory: You are an expert AI researcher who can receive and incorporate real-time guidance during your work
|
||||
message_steering: true # Enable real-time message steering
|
||||
tools:
|
||||
- internet_search
|
||||
- write_file
|
||||
tasks:
|
||||
research_task:
|
||||
description: |
|
||||
Research the latest developments in AI and write a comprehensive report.
|
||||
You may receive additional guidance during execution via message steering.
|
||||
expected_output: |
|
||||
A well-structured research report covering recent AI developments,
|
||||
incorporating any real-time guidance received during execution.
|
||||
|
||||
writer:
|
||||
role: Technical Writer
|
||||
goal: Write clear, engaging content based on research
|
||||
backstory: You are a skilled technical writer who can adapt content based on real-time feedback
|
||||
message_steering: true # Enable steering for dynamic content adjustment
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
tasks:
|
||||
writing_task:
|
||||
description: |
|
||||
Take the research findings and create an accessible article for a general audience.
|
||||
Adjust style and focus based on any steering messages received.
|
||||
expected_output: |
|
||||
A polished article that transforms technical research into accessible content,
|
||||
refined based on real-time steering guidance.
|
||||
|
||||
dependencies:
|
||||
- writing_task: [research_task]
|
||||
|
||||
# Usage Instructions:
|
||||
# 1. Run: praisonai --agent-file message_steering_example.yaml
|
||||
# 2. While running, use steering API to send guidance:
|
||||
# agent.steer("Focus more on practical applications")
|
||||
# agent.steer("Keep it under 500 words", priority=15)
|
||||
# agent.steer("Add more examples", priority=10)
|
||||
@@ -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']}")
|
||||
|
||||
@@ -191,6 +191,7 @@ _LAZY_IMPORTS = {
|
||||
'RECOMMENDED_PROMPT_PREFIX': ('praisonaiagents.agent.handoff', 'RECOMMENDED_PROMPT_PREFIX'),
|
||||
'prompt_with_handoff_instructions': ('praisonaiagents.agent.handoff', 'prompt_with_handoff_instructions'),
|
||||
'HandoffConfig': ('praisonaiagents.agent.handoff', 'HandoffConfig'),
|
||||
'HandoffToolPolicy': ('praisonaiagents.agent.handoff', 'HandoffToolPolicy'),
|
||||
'HandoffResult': ('praisonaiagents.agent.handoff', 'HandoffResult'),
|
||||
'HandoffInputData': ('praisonaiagents.agent.handoff', 'HandoffInputData'),
|
||||
'ContextPolicy': ('praisonaiagents.agent.handoff', 'ContextPolicy'),
|
||||
|
||||
@@ -166,6 +166,21 @@ def __getattr__(name):
|
||||
_lazy_cache[name] = value
|
||||
return value
|
||||
|
||||
# Message Steering - lightweight protocols and implementations
|
||||
_steering_names = {
|
||||
'MessageSteeringProtocol', 'AgentSteeringProtocol', 'SteeringPriority',
|
||||
'SteeringMessage', 'MessageSteering', 'SteeringMixin'
|
||||
}
|
||||
if name in _steering_names:
|
||||
if name in {'MessageSteeringProtocol', 'AgentSteeringProtocol', 'SteeringPriority', 'SteeringMessage'}:
|
||||
from . import protocols as _protocols_module
|
||||
value = getattr(_protocols_module, name)
|
||||
else:
|
||||
from . import message_steering as _steering_module
|
||||
value = getattr(_steering_module, name)
|
||||
_lazy_cache[name] = value
|
||||
return value
|
||||
|
||||
# Decomposed mixins - for advanced use cases
|
||||
if name == 'ToolExecutionMixin':
|
||||
from .tool_execution import ToolExecutionMixin
|
||||
@@ -256,6 +271,13 @@ __all__ = [
|
||||
'MemoryAwareAgentProtocol',
|
||||
'FullAgentProtocol',
|
||||
'ContextEngineerProtocol',
|
||||
# Message Steering
|
||||
'MessageSteeringProtocol',
|
||||
'AgentSteeringProtocol',
|
||||
'SteeringPriority',
|
||||
'SteeringMessage',
|
||||
'MessageSteering',
|
||||
'SteeringMixin',
|
||||
# Decomposed mixins (for advanced use cases)
|
||||
'ToolExecutionMixin',
|
||||
'ChatHandlerMixin',
|
||||
|
||||
@@ -22,6 +22,7 @@ from .session_manager import SessionManagerMixin
|
||||
from .async_safety import AsyncSafeState
|
||||
from .unified_execution_mixin import UnifiedExecutionMixin
|
||||
from .sandbox_mixin import SandboxMixin
|
||||
from .message_steering import SteeringMixin
|
||||
|
||||
# Module-level logger for thread safety errors and debugging
|
||||
logger = get_logger(__name__)
|
||||
@@ -242,7 +243,7 @@ def _get_default_server_registry() -> ServerRegistry:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..approval.protocols import ApprovalConfig, ApprovalProtocol
|
||||
from ..config.feature_configs import LearnConfig, MemoryConfig
|
||||
from ..config.feature_configs import LearnConfig, MemoryConfig, ToolConfig
|
||||
from ..context.models import ContextConfig
|
||||
from ..context.manager import ContextManager
|
||||
from ..knowledge.knowledge import Knowledge
|
||||
@@ -252,11 +253,12 @@ if TYPE_CHECKING:
|
||||
from .handoff import Handoff, HandoffConfig, HandoffResult
|
||||
from ..rag.models import RAGResult, ContextPack
|
||||
from ..eval.results import EvaluationLoopResult
|
||||
from ..tools.retry import RetryPolicy
|
||||
|
||||
# Import structured error from central errors module
|
||||
from ..errors import BudgetExceededError
|
||||
|
||||
class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin, ExecutionMixin, MemoryMixin, AsyncMemoryMixin):
|
||||
class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin, ExecutionMixin, MemoryMixin, AsyncMemoryMixin):
|
||||
# Class-level counter for generating unique display names for nameless agents
|
||||
_agent_counter = 0
|
||||
_agent_counter_lock = threading.Lock()
|
||||
@@ -551,13 +553,13 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
hooks: Optional[Union[List[Any], Dict[str, Any], 'HooksConfig']] = None,
|
||||
skills: Optional[Union[List[str], str, Dict[str, Any], 'SkillsConfig']] = None,
|
||||
approval: Optional[Union[bool, str, Dict[str, Any], 'ApprovalConfig', 'ApprovalProtocol']] = None,
|
||||
tool_timeout: Optional[int] = None, # P8/G11: Timeout in seconds for each tool call
|
||||
parallel_tool_calls: bool = False, # Gap 2: Enable parallel execution of batched LLM tool calls
|
||||
tool_config: Optional[Union[bool, 'ToolConfig']] = None, # Tool execution configuration (timeout, retry, parallel)
|
||||
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")
|
||||
interrupt_controller: Optional['InterruptController'] = None, # G2: Cooperative cancellation
|
||||
tool_search: Optional[Union[bool, str, Dict[str, Any], 'ToolSearchConfig']] = False, # Progressive tool disclosure
|
||||
message_steering: Optional[Union[bool, 'MessageSteeringProtocol']] = False, # Real-time message steering during execution
|
||||
sandbox: Optional[Union[bool, 'SandboxConfig']] = None, # Sandbox for safe code execution
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
@@ -646,10 +648,10 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
- LearnConfig: Custom configuration
|
||||
Learning is a first-class citizen, peer to memory. It captures patterns,
|
||||
preferences, and insights from interactions to improve future responses.
|
||||
parallel_tool_calls: Enable parallel execution of batched LLM tool calls (default False).
|
||||
When True and LLM returns multiple tool calls in a single response, they execute
|
||||
concurrently instead of sequentially. Provides ~3x speedup for I/O-bound tools.
|
||||
Maintains backward compatibility with False default.
|
||||
tool_config: Tool execution configuration. Accepts:
|
||||
- bool: True enables defaults, False disables
|
||||
- ToolConfig: Custom configuration for timeout, retry policy, parallel execution
|
||||
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)
|
||||
@@ -689,6 +691,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
- auto_save → memory=MemoryConfig(auto_save=)
|
||||
- rate_limiter → execution=ExecutionConfig(rate_limiter=)
|
||||
- verification_hooks → autonomy=AutonomyConfig(verification_hooks=)
|
||||
- Use tool_config=ToolConfig(...) for tool execution configuration
|
||||
"""
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -799,8 +802,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
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",
|
||||
@@ -976,8 +978,8 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
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'
|
||||
@@ -985,7 +987,8 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
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)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1512,8 +1515,14 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.self_reflect = True if self_reflect is None else self_reflect
|
||||
|
||||
self.instructions = instructions
|
||||
|
||||
# Resolve tool_config for tool execution settings
|
||||
_tool_config = Agent._resolve_tool_config(
|
||||
tool_config=tool_config
|
||||
)
|
||||
|
||||
# Gap 2: Store parallel tool calls setting for ToolCallExecutor selection
|
||||
self.parallel_tool_calls = parallel_tool_calls
|
||||
self.parallel_tool_calls = _tool_config.parallel if _tool_config else parallel_tool_calls
|
||||
# G2: Store interrupt controller for cooperative cancellation
|
||||
self.interrupt_controller = interrupt_controller
|
||||
# Check for model name in environment variable if not provided
|
||||
@@ -1729,6 +1738,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.max_execution_time = max_execution_time
|
||||
self._memory_instance = None
|
||||
self._init_memory(memory, user_id)
|
||||
self._init_message_steering(message_steering)
|
||||
self.verbose = verbose
|
||||
self._has_explicit_output_config = _has_explicit_output # Track if user set output mode
|
||||
self.tool_output_limit = tool_output_limit # Configurable tool output limit
|
||||
@@ -1911,8 +1921,14 @@ Your Goal: {self.goal}
|
||||
self._pending_approvals = {}
|
||||
self._approvals_lock = asyncio.Lock()
|
||||
|
||||
# Store the resolved tool_config for cloning (resolved earlier in __init__)
|
||||
self._tool_config = _tool_config
|
||||
|
||||
# P8/G11: Tool timeout - prevent slow tools from blocking
|
||||
self._tool_timeout = 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 None
|
||||
|
||||
# Cache for system prompts and formatted tools with eager thread-safe lock
|
||||
# Use OrderedDict for LRU behavior
|
||||
@@ -2028,6 +2044,19 @@ Your Goal: {self.goal}
|
||||
# Sandbox configuration - initialize SandboxMixin
|
||||
super().__init__(sandbox=sandbox)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
# Handle the new consolidated parameter
|
||||
if tool_config is not None:
|
||||
resolved_config = resolve_tools(tool_config)
|
||||
return resolved_config
|
||||
|
||||
return None
|
||||
|
||||
def __deepcopy__(self, memo: dict) -> "Agent":
|
||||
"""Custom deepcopy that creates fresh threading primitives.
|
||||
|
||||
@@ -2104,9 +2133,8 @@ Your Goal: {self.goal}
|
||||
'learn': getattr(self, '_learn_config', None),
|
||||
'tool_search': getattr(self, '_tool_search_config', None),
|
||||
|
||||
# Tool configuration
|
||||
'tool_timeout': getattr(self, '_tool_timeout', None),
|
||||
'parallel_tool_calls': getattr(self, 'parallel_tool_calls', False),
|
||||
# Tool configuration - use consolidated config when available
|
||||
'tool_config': getattr(self, '_tool_config', None),
|
||||
|
||||
# CLI backend
|
||||
'cli_backend': getattr(self, '_cli_backend', None),
|
||||
|
||||
@@ -239,7 +239,7 @@ Your Goal: {self.goal}"""
|
||||
try:
|
||||
from ..llm.model_capabilities import supports_structured_outputs
|
||||
except ImportError:
|
||||
return False # Module genuinely not available — acceptable
|
||||
return False # Module genuinely not available - acceptable
|
||||
|
||||
try:
|
||||
return supports_structured_outputs(self.llm)
|
||||
@@ -343,13 +343,19 @@ Your Goal: {self.goal}"""
|
||||
- Tool Search progressive disclosure (if enabled)
|
||||
|
||||
Args:
|
||||
tools: List of tools in various formats or None to use self.tools
|
||||
tools: List of tools in various formats or None to use self.tools.
|
||||
Note: [] (empty list) means explicitly no tools (security boundary),
|
||||
None means inherit from self.tools
|
||||
|
||||
Returns:
|
||||
List of formatted tools or empty list
|
||||
"""
|
||||
# Security fix: Distinguish None (inherit) vs [] (explicit deny)
|
||||
if tools is None:
|
||||
tools = self.tools
|
||||
elif isinstance(tools, list) and len(tools) == 0:
|
||||
# Explicit empty list - return immediately to enforce boundary
|
||||
return []
|
||||
|
||||
if not tools:
|
||||
return []
|
||||
@@ -900,7 +906,7 @@ Your Goal: {self.goal}"""
|
||||
temperature=temperature
|
||||
)
|
||||
self._hook_runner.execute_sync(HookEvent.BEFORE_LLM, before_llm_input)
|
||||
# C7 — honour any BEFORE_LLM hook that mutated the message stream
|
||||
# C7 - honour any BEFORE_LLM hook that mutated the message stream
|
||||
# (e.g. PII redactor). The runner applies modified_input in-place on
|
||||
# before_llm_input.messages; adopt that value for the actual LLM call.
|
||||
messages = before_llm_input.messages
|
||||
@@ -1058,7 +1064,6 @@ Your Goal: {self.goal}"""
|
||||
# Use structured error classification for all error types (replaces legacy heuristic checks)
|
||||
from ..llm.error_classifier import classify_llm_error
|
||||
from ..llm.retry_utils import jittered_backoff
|
||||
import time
|
||||
|
||||
model_name = self.llm if isinstance(self.llm, str) else "unknown"
|
||||
session_id = getattr(self, '_session_id', 'unknown')
|
||||
@@ -1931,6 +1936,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
# body before any backend/LLM call.
|
||||
prompt = self._resolve_skill_invocation(prompt)
|
||||
|
||||
# Check for steering messages before processing
|
||||
if hasattr(self, '_check_steering_messages'):
|
||||
try:
|
||||
steering_msg = self._check_steering_messages()
|
||||
if steering_msg:
|
||||
# Inject steering message into prompt with clear separator
|
||||
prompt = f"{prompt}\n\n{steering_msg}"
|
||||
except Exception as e:
|
||||
logger.warning(f"Steering check failed, continuing without steering: {e}")
|
||||
|
||||
# Check if external managed backend is configured
|
||||
if hasattr(self, 'backend') and self.backend is not None:
|
||||
# Extract kwargs for delegation, excluding 'self' and function locals
|
||||
@@ -1958,7 +1973,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
_trace_emitter.agent_start(self.name, {"role": self.role, "goal": self.goal})
|
||||
|
||||
try:
|
||||
# C2 — cooperative cancellation: abort early if a pre-set token is given
|
||||
# C2 - cooperative cancellation: abort early if a pre-set token is given
|
||||
_cancel = cancel_token if cancel_token is not None else getattr(self, "interrupt_controller", None)
|
||||
if _cancel is not None and getattr(_cancel, "is_set", lambda: False)():
|
||||
reason = getattr(_cancel, "reason", None) or "cancelled before LLM call"
|
||||
@@ -2089,9 +2104,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
if self._using_custom_llm:
|
||||
try:
|
||||
# Special handling for MCP tools when using provider/model format
|
||||
# Fix: Handle empty tools list properly - use self.tools if tools is None or empty
|
||||
if tools is None or (isinstance(tools, list) and len(tools) == 0):
|
||||
# Security fix: Distinguish None (inherit) vs [] (explicit deny)
|
||||
if tools is None:
|
||||
# None means inherit agent's configured tools
|
||||
tool_param = self.tools
|
||||
elif isinstance(tools, list) and len(tools) == 0:
|
||||
# Empty list means explicitly deny all tools (security boundary)
|
||||
tool_param = []
|
||||
else:
|
||||
tool_param = tools
|
||||
|
||||
@@ -2198,6 +2217,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
task_id=task_id,
|
||||
execute_tool_fn=self.execute_tool,
|
||||
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False),
|
||||
max_tool_calls_per_turn=getattr(getattr(self, "execution", None), "max_tool_calls_per_turn", 10),
|
||||
reasoning_steps=reasoning_steps,
|
||||
stream=stream
|
||||
)
|
||||
@@ -2208,11 +2228,11 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
if effective_tool_choice:
|
||||
llm_kwargs['tool_choice'] = effective_tool_choice
|
||||
|
||||
# C1 — per-call seed overrides llm_instance.seed for determinism
|
||||
# C1 - per-call seed overrides llm_instance.seed for determinism
|
||||
if seed is not None:
|
||||
llm_kwargs['seed'] = seed
|
||||
|
||||
# C2 — last-chance cancel check before handing to the LLM
|
||||
# C2 - last-chance cancel check before handing to the LLM
|
||||
if cancel_token is not None and getattr(cancel_token, 'is_set', lambda: False)():
|
||||
reason = getattr(cancel_token, 'reason', None) or 'cancelled'
|
||||
raise InterruptedError(f"Agent chat cancelled: {reason}")
|
||||
@@ -2323,7 +2343,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
agent_tools=agent_tools
|
||||
)
|
||||
|
||||
response = self._chat_completion(messages, temperature=temperature, tools=tools if tools else None, reasoning_steps=reasoning_steps, stream=stream, task_name=task_name, task_description=task_description, task_id=task_id, response_format=response_format)
|
||||
response = self._chat_completion(messages, temperature=temperature, tools=tools, reasoning_steps=reasoning_steps, stream=stream, task_name=task_name, task_description=task_description, task_id=task_id, response_format=response_format)
|
||||
if not response:
|
||||
# Rollback chat history on response failure
|
||||
self._truncate_chat_history(chat_history_length)
|
||||
@@ -2523,6 +2543,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
# Slash-command invocation: /skill-name [args] renders the skill body.
|
||||
prompt = self._resolve_skill_invocation(prompt)
|
||||
|
||||
# Check for steering messages before processing
|
||||
if hasattr(self, '_check_steering_messages'):
|
||||
try:
|
||||
steering_msg = self._check_steering_messages()
|
||||
if steering_msg:
|
||||
# Inject steering message into prompt with clear separator
|
||||
prompt = f"{prompt}\n\n{steering_msg}"
|
||||
except Exception as e:
|
||||
logger.warning(f"Steering check failed, continuing without steering: {e}")
|
||||
|
||||
# Emit context trace event (zero overhead when not set)
|
||||
from ..trace.context_events import get_context_emitter
|
||||
_trace_emitter = get_context_emitter()
|
||||
@@ -2543,7 +2573,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
async def _achat_impl(self, prompt, temperature, tools, output_json, output_pydantic, reasoning_steps, stream, task_name, task_description, task_id, config, force_retrieval, skip_retrieval, attachments, _trace_emitter, tool_choice=None, seed=None, cancel_token=None):
|
||||
"""Internal async chat implementation (extracted for trace wrapping)."""
|
||||
# C2 — cooperative cancellation: abort early if a pre-set token is given
|
||||
# C2 - cooperative cancellation: abort early if a pre-set token is given
|
||||
_cancel = cancel_token if cancel_token is not None else getattr(self, "interrupt_controller", None)
|
||||
if _cancel is not None and getattr(_cancel, "is_set", lambda: False)():
|
||||
reason = getattr(_cancel, "reason", None) or "cancelled before LLM call"
|
||||
@@ -2661,7 +2691,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
pass
|
||||
|
||||
try:
|
||||
# C1 — per-call seed forwarding (async path)
|
||||
# C1 - per-call seed forwarding (async path)
|
||||
llm_kwargs = {
|
||||
'prompt': prompt,
|
||||
'system_prompt': self._build_system_prompt(tools),
|
||||
@@ -2684,15 +2714,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
'task_id': task_id,
|
||||
'execute_tool_fn': self.execute_tool_async,
|
||||
'parallel_tool_calls': getattr(getattr(self, "execution", None), "parallel_tool_calls", False),
|
||||
'max_tool_calls_per_turn': getattr(getattr(self, "execution", None), "max_tool_calls_per_turn", 10),
|
||||
'reasoning_steps': reasoning_steps,
|
||||
'stream': stream
|
||||
}
|
||||
|
||||
# C1 — per-call seed overrides llm_instance.seed for determinism
|
||||
# C1 - per-call seed overrides llm_instance.seed for determinism
|
||||
if seed is not None:
|
||||
llm_kwargs['seed'] = seed
|
||||
|
||||
# C2 — last-chance cancel check before handing to the LLM
|
||||
# C2 - last-chance cancel check before handing to the LLM
|
||||
if _cancel is not None and getattr(_cancel, 'is_set', lambda: False)():
|
||||
reason = getattr(_cancel, 'reason', None) or 'cancelled'
|
||||
raise InterruptedError(f"Agent chat cancelled: {reason}")
|
||||
@@ -3424,7 +3455,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
task_description=kwargs.get('task_description'),
|
||||
task_id=kwargs.get('task_id'),
|
||||
execute_tool_fn=self.execute_tool,
|
||||
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False)
|
||||
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False),
|
||||
max_tool_calls_per_turn=getattr(getattr(self, "execution", None), "max_tool_calls_per_turn", 10)
|
||||
):
|
||||
response_content += chunk
|
||||
yield chunk
|
||||
|
||||
@@ -9,6 +9,7 @@ import time
|
||||
import json
|
||||
import logging
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
@@ -805,7 +806,25 @@ Write the complete compiled report:"""
|
||||
# Suppress verbose during animation - we'll display result ourselves
|
||||
self.verbose = False
|
||||
current_status[0] = "Sending to LLM..."
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
|
||||
# Check for steering messages before starting chat
|
||||
if hasattr(self, '_check_steering_messages'):
|
||||
try:
|
||||
steering_msg = self._check_steering_messages()
|
||||
if steering_msg:
|
||||
# Inject steering message into prompt with clear separator
|
||||
prompt_with_steering = f"{prompt}\n\n{steering_msg}"
|
||||
current_status[0] = "Processing steering guidance..."
|
||||
result_holder[0] = self.chat(prompt_with_steering, **kwargs)
|
||||
else:
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
except Exception as e:
|
||||
logger.warning(f"Steering check failed, continuing without steering: {e}")
|
||||
current_status[0] = "Steering failed, proceeding normally..."
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
else:
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
|
||||
current_status[0] = "Finalizing response..."
|
||||
except Exception as e:
|
||||
error_holder[0] = e
|
||||
@@ -947,7 +966,80 @@ Write the complete compiled report:"""
|
||||
return await self.achat(prompt, task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
|
||||
async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Async version of execute_tool"""
|
||||
"""Async version of execute_tool with retry policy support"""
|
||||
|
||||
# Get retry policy (tool-level > agent-level > default)
|
||||
retry_policy = self._get_tool_retry_policy(function_name)
|
||||
|
||||
last_exception = None
|
||||
|
||||
# Retry loop with exponential backoff
|
||||
for attempt in range(retry_policy.max_attempts):
|
||||
try:
|
||||
result = await self._execute_tool_async_impl(function_name, arguments, tool_call_id, tools_override)
|
||||
|
||||
# Check if result is an error that should be retried
|
||||
if isinstance(result, dict) and result.get("error"):
|
||||
# Skip retry for non-retryable errors (approval, permission, etc.)
|
||||
if (result.get("approval_denied") or
|
||||
result.get("permission_denied") or
|
||||
result.get("approval_error") or
|
||||
result.get("circuit_open")):
|
||||
return result
|
||||
|
||||
# Determine error type for retry policy
|
||||
error_type = self._classify_error_type(result, last_exception)
|
||||
|
||||
# Check if we should retry
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
return result
|
||||
|
||||
# Don't retry on last attempt
|
||||
if attempt == retry_policy.max_attempts - 1:
|
||||
return result
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
await self._emit_retry_hook_async(function_name, attempt + 1, delay_ms, result.get("error", "Unknown error"), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry (async)
|
||||
await asyncio.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
else:
|
||||
# Success - return result
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
# Determine if retryable
|
||||
is_retryable = not isinstance(e, (ValueError, TypeError, AttributeError))
|
||||
|
||||
# Check if retryable and not last attempt
|
||||
if not is_retryable or attempt == retry_policy.max_attempts - 1:
|
||||
# Return error dict instead of raising for consistency with sync version
|
||||
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
||||
|
||||
# Determine error type
|
||||
error_type = self._classify_error_type(None, e)
|
||||
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
await self._emit_retry_hook_async(function_name, attempt + 1, delay_ms, str(e), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry (async)
|
||||
await asyncio.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
|
||||
# Should never reach here due to loop logic, but safety fallback
|
||||
if last_exception:
|
||||
return {"error": f"Error in execute_tool_async: {str(last_exception)}"}
|
||||
return {"error": "Maximum retry attempts exceeded"}
|
||||
|
||||
async def _execute_tool_async_impl(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Internal async tool execution implementation"""
|
||||
try:
|
||||
logging.info(f"Executing async tool: {function_name} with arguments: {arguments}")
|
||||
|
||||
@@ -1286,3 +1378,51 @@ Write the complete compiled report:"""
|
||||
print(f"\nTo add MCP capabilities, install: pip install {missing_module}")
|
||||
return None
|
||||
|
||||
async def _emit_retry_hook_async(self, tool_name, attempt, delay_ms, error, max_attempts, error_type):
|
||||
"""Emit ON_RETRY hook event (async version).
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool being retried
|
||||
attempt: Current attempt number (1-based)
|
||||
delay_ms: Delay before retry in milliseconds
|
||||
error: Error message or description
|
||||
max_attempts: Maximum number of attempts configured
|
||||
error_type: Classified error type
|
||||
"""
|
||||
try:
|
||||
from ..hooks import HookEvent, OnRetryInput
|
||||
|
||||
# Only emit if we have a hook runner
|
||||
hook_runner = getattr(self, '_hook_runner', None)
|
||||
if hook_runner is None:
|
||||
return
|
||||
|
||||
retry_input = OnRetryInput(
|
||||
session_id=getattr(self, '_session_id', 'default'),
|
||||
cwd=os.getcwd(),
|
||||
event_name=HookEvent.ON_RETRY,
|
||||
timestamp=str(time.time()),
|
||||
agent_name=self.name,
|
||||
tool_name=tool_name,
|
||||
attempt=attempt,
|
||||
delay_ms=delay_ms,
|
||||
error=error,
|
||||
max_attempts=max_attempts,
|
||||
error_type=error_type
|
||||
)
|
||||
|
||||
# Execute hook asynchronously if supported, otherwise use sync
|
||||
if hasattr(hook_runner, 'execute_async'):
|
||||
await hook_runner.execute_async(HookEvent.ON_RETRY, retry_input, target=tool_name)
|
||||
else:
|
||||
# Fallback to sync execution in executor to avoid blocking event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: hook_runner.execute_sync(HookEvent.ON_RETRY, retry_input, target=tool_name)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Don't let hook failures break retry logic
|
||||
logging.debug(f"Failed to emit async retry hook: {e}")
|
||||
|
||||
@@ -10,7 +10,7 @@ Unified Handoff System:
|
||||
- Replaces/absorbs Agent.delegate() and SubagentDelegator functionality
|
||||
"""
|
||||
|
||||
from typing import Optional, Any, Callable, Dict, List, Union, TYPE_CHECKING, TypeVar, Type, Generic
|
||||
from typing import Optional, Any, Callable, Dict, List, Union, TYPE_CHECKING, Literal, TypeVar, Type, Generic
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import inspect
|
||||
@@ -48,6 +48,24 @@ class ContextPolicy(Enum):
|
||||
NONE = "none" # No context sharing
|
||||
LAST_N = "last_n" # Share last N messages
|
||||
|
||||
@dataclass
|
||||
class HandoffToolPolicy:
|
||||
"""
|
||||
Policy for tool boundary enforcement during handoff.
|
||||
|
||||
Defines how tools are filtered when handing off to a target agent.
|
||||
Default mode is 'intersect' for security by default - sub-agents only
|
||||
get tools that both they and the source agent have access to.
|
||||
|
||||
Attributes:
|
||||
mode: Tool filtering mode
|
||||
- "intersect": Target gets intersection of source and target tools (DEFAULT - secure)
|
||||
- "passthrough": Target keeps its full tool set (legacy behavior - opt-in)
|
||||
blocked_tools: List of tool names to always strip, regardless of intersection
|
||||
"""
|
||||
mode: Literal["intersect", "passthrough"] = "intersect"
|
||||
blocked_tools: List[str] = field(default_factory=list)
|
||||
|
||||
@dataclass
|
||||
class HandoffConfig:
|
||||
"""
|
||||
@@ -61,6 +79,7 @@ class HandoffConfig:
|
||||
max_context_tokens: Maximum tokens to include in context
|
||||
max_context_messages: Maximum messages to include (for LAST_N policy)
|
||||
preserve_system: Whether to preserve system messages in context
|
||||
tool_policy: Tool boundary enforcement policy (NEW - security by default)
|
||||
timeout_seconds: Timeout for handoff execution
|
||||
max_concurrent: Maximum concurrent handoffs (0 = unlimited)
|
||||
detect_cycles: Enable cycle detection to prevent infinite loops
|
||||
@@ -77,6 +96,9 @@ class HandoffConfig:
|
||||
max_context_messages: int = 10
|
||||
preserve_system: bool = True
|
||||
|
||||
# Tool security boundary (NEW)
|
||||
tool_policy: HandoffToolPolicy = field(default_factory=HandoffToolPolicy)
|
||||
|
||||
# Execution control
|
||||
timeout_seconds: float = 300.0
|
||||
max_concurrent: int = 5
|
||||
@@ -101,6 +123,10 @@ class HandoffConfig:
|
||||
"max_context_tokens": self.max_context_tokens,
|
||||
"max_context_messages": self.max_context_messages,
|
||||
"preserve_system": self.preserve_system,
|
||||
"tool_policy": {
|
||||
"mode": self.tool_policy.mode,
|
||||
"blocked_tools": self.tool_policy.blocked_tools.copy()
|
||||
},
|
||||
"timeout_seconds": self.timeout_seconds,
|
||||
"max_concurrent": self.max_concurrent,
|
||||
"detect_cycles": self.detect_cycles,
|
||||
@@ -112,9 +138,21 @@ class HandoffConfig:
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'HandoffConfig':
|
||||
"""Create config from dictionary."""
|
||||
if "context_policy" in data and isinstance(data["context_policy"], str):
|
||||
data["context_policy"] = ContextPolicy(data["context_policy"])
|
||||
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
||||
data_copy = data.copy()
|
||||
|
||||
# Handle context_policy enum conversion
|
||||
if "context_policy" in data_copy and isinstance(data_copy["context_policy"], str):
|
||||
data_copy["context_policy"] = ContextPolicy(data_copy["context_policy"])
|
||||
|
||||
# Handle tool_policy conversion
|
||||
if "tool_policy" in data_copy and isinstance(data_copy["tool_policy"], dict):
|
||||
tool_policy_data = data_copy["tool_policy"]
|
||||
data_copy["tool_policy"] = HandoffToolPolicy(
|
||||
mode=tool_policy_data.get("mode", "intersect"),
|
||||
blocked_tools=tool_policy_data.get("blocked_tools", [])
|
||||
)
|
||||
|
||||
return cls(**{k: v for k, v in data_copy.items() if k in cls.__dataclass_fields__})
|
||||
|
||||
# Import structured error hierarchy from central errors module
|
||||
from ..errors import (
|
||||
@@ -321,6 +359,52 @@ class Handoff:
|
||||
agent_desc += f" - {self.agent.goal}"
|
||||
return agent_desc
|
||||
|
||||
def _compute_effective_tools(self, source_agent: 'Agent') -> List[Any] | None:
|
||||
"""
|
||||
Compute the effective tool set for the target agent based on tool policy.
|
||||
|
||||
Args:
|
||||
source_agent: The agent initiating the handoff
|
||||
|
||||
Returns:
|
||||
List of tools the target agent should have access to during handoff.
|
||||
Returns None to indicate unrestricted access (for passthrough mode without blocked tools).
|
||||
Returns [] to indicate no tools allowed (for intersect mode with empty intersection).
|
||||
"""
|
||||
policy = self.config.tool_policy
|
||||
|
||||
if policy.mode == "passthrough":
|
||||
# Legacy behavior - target keeps its full tool set, minus blocked tools
|
||||
if not policy.blocked_tools:
|
||||
# No restrictions - return None so chat() uses the agent's configured tools
|
||||
return None
|
||||
else:
|
||||
# Filter out blocked tools
|
||||
target_tools = getattr(self.agent, 'tools', None) or []
|
||||
effective_tools = [
|
||||
tool for tool in target_tools
|
||||
if getattr(tool, 'name', getattr(tool, '__name__', str(tool))) not in policy.blocked_tools
|
||||
]
|
||||
return effective_tools
|
||||
else: # intersect mode (default)
|
||||
# Security by default - intersection of source and target tools
|
||||
source_tools = getattr(source_agent, 'tools', None) or []
|
||||
source_tool_names = {
|
||||
getattr(tool, 'name', getattr(tool, '__name__', str(tool)))
|
||||
for tool in source_tools
|
||||
}
|
||||
|
||||
target_tools = getattr(self.agent, 'tools', None) or []
|
||||
effective_tools = []
|
||||
for tool in target_tools:
|
||||
tool_name = getattr(tool, 'name', getattr(tool, '__name__', str(tool)))
|
||||
if (tool_name in source_tool_names and
|
||||
tool_name not in policy.blocked_tools):
|
||||
effective_tools.append(tool)
|
||||
|
||||
# In intersect mode, empty list means no tools allowed - this is the security boundary
|
||||
return effective_tools
|
||||
|
||||
def _check_safety(self, source_agent: 'Agent') -> None:
|
||||
"""
|
||||
Check safety constraints before handoff.
|
||||
@@ -482,8 +566,11 @@ class Handoff:
|
||||
|
||||
logger.info(f"Programmatic handoff to {self.agent.name}")
|
||||
|
||||
# Execute with timeout if sync
|
||||
response = self.agent.chat(full_prompt)
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
# Execute with tool boundary enforcement
|
||||
response = self.agent.chat(full_prompt, tools=effective_tools)
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
@@ -585,13 +672,16 @@ class Handoff:
|
||||
|
||||
logger.info(f"Async handoff to {self.agent.name}")
|
||||
|
||||
# Execute - check for async chat method
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
# Execute with tool boundary enforcement - check for async chat method
|
||||
if hasattr(self.agent, 'achat'):
|
||||
response = await self.agent.achat(full_prompt)
|
||||
response = await self.agent.achat(full_prompt, tools=effective_tools)
|
||||
else:
|
||||
# Run sync chat in executor
|
||||
# Run sync chat in executor with tool constraint
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self.agent.chat, full_prompt)
|
||||
response = await loop.run_in_executor(None, lambda: self.agent.chat(full_prompt, tools=effective_tools))
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
@@ -719,11 +809,15 @@ class Handoff:
|
||||
if kwargs and self.input_type:
|
||||
context_info += f"Context: {kwargs} "
|
||||
|
||||
# Execute the target agent
|
||||
# Execute the target agent with tool boundary enforcement
|
||||
if last_message:
|
||||
prompt = context_info + last_message
|
||||
logger.info(f"Handing off to {self.agent.name} with prompt: {prompt}")
|
||||
response = self.agent.chat(prompt)
|
||||
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
response = self.agent.chat(prompt, tools=effective_tools)
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
@@ -806,13 +900,16 @@ def handoff(
|
||||
max_concurrent: Optional[int] = None,
|
||||
detect_cycles: Optional[bool] = None,
|
||||
max_depth: Optional[int] = None,
|
||||
# Tool security boundary kwargs (NEW)
|
||||
tool_policy_mode: Optional[Literal["intersect", "passthrough"]] = None,
|
||||
blocked_tools: Optional[List[str]] = None,
|
||||
) -> Handoff:
|
||||
"""
|
||||
Create a handoff configuration for delegating tasks to another agent.
|
||||
|
||||
This is a convenience function that creates a Handoff instance with the
|
||||
specified configuration. It supports both the legacy API and the new
|
||||
unified HandoffConfig.
|
||||
unified HandoffConfig with tool security boundary enforcement.
|
||||
|
||||
Args:
|
||||
agent: The target agent to hand off to
|
||||
@@ -827,29 +924,39 @@ def handoff(
|
||||
max_concurrent: Shorthand for config.max_concurrent
|
||||
detect_cycles: Shorthand for config.detect_cycles
|
||||
max_depth: Shorthand for config.max_depth
|
||||
tool_policy_mode: Tool boundary mode ("intersect" = secure by default, "passthrough" = legacy)
|
||||
blocked_tools: List of tool names to always block regardless of intersection
|
||||
|
||||
Returns:
|
||||
A configured Handoff instance
|
||||
|
||||
Example:
|
||||
```python
|
||||
from praisonaiagents import Agent, handoff, HandoffConfig
|
||||
from praisonaiagents import Agent, handoff, HandoffConfig, HandoffToolPolicy
|
||||
|
||||
billing_agent = Agent(name="Billing Agent")
|
||||
refund_agent = Agent(name="Refund Agent")
|
||||
refund_agent = Agent(name="Refund Agent")
|
||||
|
||||
# Simple usage
|
||||
# Simple usage (secure by default - tools intersected)
|
||||
triage_agent = Agent(
|
||||
name="Triage Agent",
|
||||
handoffs=[billing_agent, handoff(refund_agent)]
|
||||
)
|
||||
|
||||
# With config
|
||||
# With tool security config
|
||||
triage_agent = Agent(
|
||||
name="Triage Agent",
|
||||
name="Triage Agent",
|
||||
handoffs=[
|
||||
handoff(billing_agent, context_policy="summary", timeout_seconds=60),
|
||||
handoff(refund_agent, config=HandoffConfig(detect_cycles=True))
|
||||
handoff(billing_agent,
|
||||
tool_policy_mode="intersect", # Only shared tools
|
||||
blocked_tools=["execute_code", "shell_tools"]),
|
||||
handoff(refund_agent,
|
||||
config=HandoffConfig(
|
||||
tool_policy=HandoffToolPolicy(
|
||||
mode="passthrough", # Legacy behavior
|
||||
blocked_tools=["dangerous_tool"]
|
||||
)
|
||||
))
|
||||
]
|
||||
)
|
||||
```
|
||||
@@ -870,6 +977,14 @@ def handoff(
|
||||
if max_depth is not None:
|
||||
config.max_depth = max_depth
|
||||
|
||||
# Apply tool policy kwargs to config
|
||||
if tool_policy_mode is not None or blocked_tools is not None:
|
||||
# Create new tool policy with updated values
|
||||
config.tool_policy = HandoffToolPolicy(
|
||||
mode=tool_policy_mode if tool_policy_mode is not None else config.tool_policy.mode,
|
||||
blocked_tools=blocked_tools if blocked_tools is not None else config.tool_policy.blocked_tools
|
||||
)
|
||||
|
||||
return Handoff(
|
||||
agent=agent,
|
||||
tool_name_override=tool_name_override,
|
||||
|
||||
@@ -403,6 +403,12 @@ class MemoryMixin:
|
||||
self._session_store.add_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._session_store.add_assistant_message(self._session_id, content)
|
||||
# Keep auto_save index in sync when per-turn persist shares session_id
|
||||
if self.auto_save and self.auto_save == self._session_id:
|
||||
with self._history_lock:
|
||||
self._auto_save_last_index = (
|
||||
getattr(self, "_auto_save_last_index", 0) + 1
|
||||
)
|
||||
self._persist_session_stats()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to session store: {e}")
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Message Steering Implementation.
|
||||
|
||||
Core implementation of real-time message steering during agent execution.
|
||||
Integrates with existing message queue infrastructure.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from .message_queue import AgentMessageQueue, MessagePriority
|
||||
from .protocols import (
|
||||
MessageSteeringProtocol,
|
||||
SteeringMessage,
|
||||
SteeringPriority,
|
||||
AgentSteeringProtocol
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageSteering:
|
||||
"""
|
||||
Core message steering implementation.
|
||||
|
||||
Provides real-time message queuing and processing for agents
|
||||
during execution. Uses the existing AgentMessageQueue infrastructure.
|
||||
"""
|
||||
|
||||
def __init__(self, max_messages: int = 50):
|
||||
"""
|
||||
Initialize message steering.
|
||||
|
||||
Args:
|
||||
max_messages: Maximum number of pending messages
|
||||
"""
|
||||
self._message_queue = AgentMessageQueue(max_size=max_messages)
|
||||
self._enabled = True
|
||||
self._last_check = 0.0
|
||||
self._check_interval = 0.1 # Check every 100ms
|
||||
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
"""Queue a steering message."""
|
||||
if not self._enabled:
|
||||
return ""
|
||||
|
||||
# Convert priority to SteeringPriority
|
||||
try:
|
||||
steering_priority = SteeringPriority(priority)
|
||||
except ValueError:
|
||||
steering_priority = SteeringPriority.NORMAL
|
||||
|
||||
steering_msg = SteeringMessage(
|
||||
content=message,
|
||||
priority=steering_priority,
|
||||
metadata={"timestamp": time.time()}
|
||||
)
|
||||
|
||||
# Use existing message queue with priority mapping
|
||||
# Special handling for INTERRUPT - give it maximum priority
|
||||
if steering_priority == SteeringPriority.INTERRUPT:
|
||||
queue_priority = MessagePriority.URGENT.value + 1 # Higher than URGENT
|
||||
else:
|
||||
queue_priority = min(priority, MessagePriority.URGENT.value)
|
||||
success = self._message_queue.enqueue(
|
||||
content=steering_msg,
|
||||
priority=queue_priority,
|
||||
metadata={"type": "steering", "original_priority": priority}
|
||||
)
|
||||
|
||||
if success:
|
||||
msg_id = f"steer_{int(time.time() * 1000)}"
|
||||
logger.debug(f"Queued steering message: {msg_id}")
|
||||
return msg_id
|
||||
else:
|
||||
logger.warning("Failed to queue steering message - queue full")
|
||||
return ""
|
||||
|
||||
def get_pending_messages(self) -> List[SteeringMessage]:
|
||||
"""Get all pending steering messages."""
|
||||
if not self._enabled:
|
||||
return []
|
||||
|
||||
messages = []
|
||||
all_messages = self._message_queue.get_all()
|
||||
|
||||
for msg in all_messages:
|
||||
if isinstance(msg, SteeringMessage):
|
||||
messages.append(msg)
|
||||
|
||||
return messages
|
||||
|
||||
def process_steering(self, context: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Process pending steering messages.
|
||||
|
||||
This is called during agent execution to check for and process
|
||||
any steering messages. Uses rate limiting to avoid excessive checking,
|
||||
but allows INTERRUPT priority messages to bypass rate limiting.
|
||||
|
||||
Args:
|
||||
context: Execution context that can be updated with steering info
|
||||
|
||||
Returns:
|
||||
True if messages were processed, False otherwise
|
||||
"""
|
||||
if not self._enabled:
|
||||
return False
|
||||
|
||||
# Check if we have high priority messages that bypass rate limiting
|
||||
has_urgent_messages = False
|
||||
all_messages = self._message_queue.get_all()
|
||||
for msg in all_messages:
|
||||
if isinstance(msg, SteeringMessage) and msg.priority.value >= SteeringPriority.HIGH.value:
|
||||
has_urgent_messages = True
|
||||
break
|
||||
|
||||
# Rate limiting - only check every 100ms unless we have urgent messages
|
||||
current_time = time.time()
|
||||
if not has_urgent_messages and current_time - self._last_check < self._check_interval:
|
||||
return False
|
||||
|
||||
self._last_check = current_time
|
||||
|
||||
if self._message_queue.is_empty():
|
||||
return False
|
||||
|
||||
# Process one message per check to avoid blocking
|
||||
msg_content = self._message_queue.dequeue(timeout=0)
|
||||
if msg_content is None:
|
||||
return False
|
||||
|
||||
if not isinstance(msg_content, SteeringMessage):
|
||||
return False
|
||||
|
||||
# Update context with steering message
|
||||
if context is not None:
|
||||
if "steering_messages" not in context:
|
||||
context["steering_messages"] = []
|
||||
context["steering_messages"].append({
|
||||
"content": msg_content.content,
|
||||
"priority": msg_content.priority.name,
|
||||
"timestamp": msg_content.metadata.get("timestamp") if msg_content.metadata else None
|
||||
})
|
||||
|
||||
logger.info(f"Processed steering message: {msg_content.content[:50]}...")
|
||||
return True
|
||||
|
||||
def clear_messages(self) -> int:
|
||||
"""Clear all pending messages."""
|
||||
return self._message_queue.clear()
|
||||
|
||||
def has_pending_messages(self) -> bool:
|
||||
"""Check if there are pending steering messages."""
|
||||
return not self._message_queue.is_empty()
|
||||
|
||||
def enable(self) -> None:
|
||||
"""Enable message steering."""
|
||||
self._enabled = True
|
||||
|
||||
def disable(self) -> None:
|
||||
"""Disable message steering."""
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Whether message steering is enabled."""
|
||||
return self._enabled
|
||||
|
||||
|
||||
class SteeringMixin:
|
||||
"""
|
||||
Mixin to add message steering capabilities to Agent class.
|
||||
|
||||
This provides the agent interface for message steering while
|
||||
delegating implementation to MessageSteering.
|
||||
"""
|
||||
|
||||
def _init_message_steering(self, message_steering=False):
|
||||
"""Initialize message steering if enabled."""
|
||||
if message_steering is True:
|
||||
# Use default implementation
|
||||
self._message_steering = MessageSteering()
|
||||
elif message_steering is False or message_steering is None:
|
||||
# Disabled
|
||||
self._message_steering = None
|
||||
else:
|
||||
# Custom implementation provided
|
||||
self._message_steering = message_steering
|
||||
|
||||
def steer(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Send a steering message to the agent during execution.
|
||||
|
||||
This can be called while the agent is running to provide
|
||||
real-time guidance or course corrections.
|
||||
|
||||
Args:
|
||||
message: The steering message
|
||||
priority: Message priority (1=low, 5=normal, 10=high, 20=urgent, 30=interrupt)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking, empty string if steering disabled
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(name="research", message_steering=True)
|
||||
|
||||
# Start long-running task
|
||||
import threading
|
||||
def run_task():
|
||||
return agent.start("Research AI trends comprehensively")
|
||||
thread = threading.Thread(target=run_task)
|
||||
thread.start()
|
||||
|
||||
# Send steering messages while running
|
||||
agent.steer("Focus on the business impact, not technical details")
|
||||
agent.steer("Also include information about market size", priority=10)
|
||||
```
|
||||
"""
|
||||
if self._message_steering is None:
|
||||
logger.warning("Message steering not enabled - call ignored")
|
||||
return ""
|
||||
|
||||
return self._message_steering.queue_message(message, priority)
|
||||
|
||||
def get_steering_status(self) -> Dict[str, Any]:
|
||||
"""Get current steering status."""
|
||||
if self._message_steering is None:
|
||||
return {"enabled": False, "pending_count": 0}
|
||||
|
||||
return {
|
||||
"enabled": self._message_steering.enabled,
|
||||
"pending_count": len(self._message_steering.get_pending_messages()),
|
||||
"has_pending": self._message_steering.has_pending_messages()
|
||||
}
|
||||
|
||||
@property
|
||||
def message_steering_enabled(self) -> bool:
|
||||
"""Whether message steering is enabled for this agent."""
|
||||
return self._message_steering is not None and self._message_steering.enabled
|
||||
|
||||
def _check_steering_messages(self) -> Optional[str]:
|
||||
"""
|
||||
Check for and process steering messages.
|
||||
|
||||
This is called during agent execution loops to process
|
||||
any pending steering messages.
|
||||
|
||||
Returns:
|
||||
Steering message to inject into conversation, or None
|
||||
"""
|
||||
if self._message_steering is None:
|
||||
return None
|
||||
|
||||
# Process steering messages
|
||||
context = {}
|
||||
if self._message_steering.process_steering(context):
|
||||
steering_messages = context.get("steering_messages", [])
|
||||
if steering_messages:
|
||||
# Get the most recent message
|
||||
latest = steering_messages[-1]
|
||||
content = latest["content"]
|
||||
priority = latest["priority"]
|
||||
|
||||
# Format as system message
|
||||
if priority == "INTERRUPT":
|
||||
return f"\n[INTERRUPT USER GUIDANCE]: {content}\nPlease stop current work and follow this guidance immediately."
|
||||
elif priority in ("HIGH", "URGENT"):
|
||||
return f"\n[URGENT USER GUIDANCE]: {content}\nPlease acknowledge and adjust your approach accordingly."
|
||||
else:
|
||||
return f"\n[USER GUIDANCE]: {content}\nPlease consider this feedback as you continue."
|
||||
|
||||
return None
|
||||
@@ -10,6 +10,25 @@ This enables:
|
||||
These protocols are lightweight and have zero performance impact.
|
||||
"""
|
||||
from typing import Protocol, runtime_checkable, Optional, Any, AsyncIterator, Dict, List
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class SteeringPriority(Enum):
|
||||
"""Priority levels for steering messages."""
|
||||
LOW = 1
|
||||
NORMAL = 5
|
||||
HIGH = 10
|
||||
URGENT = 20
|
||||
INTERRUPT = 30 # Immediate interruption
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteeringMessage:
|
||||
"""A steering message with priority and metadata."""
|
||||
content: str
|
||||
priority: SteeringPriority = SteeringPriority.NORMAL
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -431,6 +450,140 @@ class ManagedBackendProtocol(Protocol):
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MessageSteeringProtocol(Protocol):
|
||||
"""
|
||||
Protocol for message steering during agent execution.
|
||||
|
||||
This protocol enables real-time communication with agents during
|
||||
long-running operations, allowing users to provide mid-execution
|
||||
guidance, feedback, and course corrections.
|
||||
|
||||
Example:
|
||||
```python
|
||||
class BasicMessageSteering:
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
# Add to internal queue
|
||||
return "msg_001"
|
||||
|
||||
def get_pending_messages(self) -> List[SteeringMessage]:
|
||||
# Return queued messages
|
||||
return []
|
||||
|
||||
def process_steering(self, context: dict) -> bool:
|
||||
# Check for messages and update context
|
||||
return False
|
||||
|
||||
agent = Agent(name="assistant", message_steering=BasicMessageSteering())
|
||||
```
|
||||
"""
|
||||
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Queue a steering message for the agent.
|
||||
|
||||
Args:
|
||||
message: The steering message content
|
||||
priority: Message priority (higher = more urgent)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking
|
||||
"""
|
||||
...
|
||||
|
||||
def get_pending_messages(self) -> List[Any]:
|
||||
"""
|
||||
Get all pending steering messages.
|
||||
|
||||
Returns:
|
||||
List of pending messages ordered by priority
|
||||
"""
|
||||
...
|
||||
|
||||
def process_steering(self, context: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Process any pending steering messages and update execution context.
|
||||
|
||||
This method is called during agent execution to check for
|
||||
and process any steering messages that have been queued.
|
||||
|
||||
Args:
|
||||
context: Optional execution context to update
|
||||
|
||||
Returns:
|
||||
True if messages were processed, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def clear_messages(self) -> int:
|
||||
"""
|
||||
Clear all pending messages.
|
||||
|
||||
Returns:
|
||||
Number of messages cleared
|
||||
"""
|
||||
...
|
||||
|
||||
def has_pending_messages(self) -> bool:
|
||||
"""
|
||||
Check if there are pending steering messages.
|
||||
|
||||
Returns:
|
||||
True if messages are pending, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""
|
||||
Whether message steering is enabled.
|
||||
|
||||
Returns:
|
||||
True if steering is enabled, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AgentSteeringProtocol(Protocol):
|
||||
"""
|
||||
Protocol for agents that support message steering.
|
||||
|
||||
This extends the agent interface with steering capabilities,
|
||||
allowing real-time message injection during execution.
|
||||
"""
|
||||
|
||||
def steer(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Send a steering message to the agent during execution.
|
||||
|
||||
This can be called while the agent is running to provide
|
||||
real-time guidance or course corrections.
|
||||
|
||||
Args:
|
||||
message: The steering message
|
||||
priority: Message priority (higher = more urgent)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking
|
||||
"""
|
||||
...
|
||||
|
||||
def get_steering_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current steering status.
|
||||
|
||||
Returns:
|
||||
Dict with pending message count, last processed time, etc.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def message_steering_enabled(self) -> bool:
|
||||
"""Whether message steering is enabled for this agent."""
|
||||
...
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AgentProtocol',
|
||||
'RunnableAgentProtocol',
|
||||
@@ -441,4 +594,8 @@ __all__ = [
|
||||
'HttpLauncherProtocol',
|
||||
'McpLauncherProtocol',
|
||||
'ManagedBackendProtocol',
|
||||
'SteeringPriority',
|
||||
'SteeringMessage',
|
||||
'MessageSteeringProtocol',
|
||||
'AgentSteeringProtocol',
|
||||
]
|
||||
@@ -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":
|
||||
|
||||
@@ -196,6 +196,19 @@ class ToolExecutionMixin:
|
||||
))
|
||||
|
||||
try:
|
||||
# Check for steering messages before tool execution
|
||||
if hasattr(self, '_check_steering_messages'):
|
||||
try:
|
||||
steering_msg = self._check_steering_messages()
|
||||
if steering_msg:
|
||||
# Check if steering message indicates interruption priority
|
||||
# (SteeringMixin formats HIGH/URGENT as "[URGENT USER GUIDANCE]" and INTERRUPT as "[INTERRUPT USER GUIDANCE]")
|
||||
if "[URGENT USER GUIDANCE]" in steering_msg or "[INTERRUPT USER GUIDANCE]" in steering_msg:
|
||||
logger.info(f"Tool {function_name} execution interrupted by high-priority steering")
|
||||
return f"Tool execution interrupted by user guidance: {steering_msg}"
|
||||
except Exception as e:
|
||||
logger.warning(f"Steering check failed, continuing with tool execution: {e}")
|
||||
|
||||
# Trigger BEFORE_TOOL hook
|
||||
from ..hooks import HookEvent, BeforeToolInput
|
||||
before_tool_input = BeforeToolInput(
|
||||
@@ -700,7 +713,111 @@ class ToolExecutionMixin:
|
||||
return None, arguments
|
||||
|
||||
def _execute_tool_with_circuit_breaker(self, function_name, arguments):
|
||||
"""Execute tool with circuit breaker protection.
|
||||
"""Execute tool with retry policy and circuit breaker protection.
|
||||
|
||||
Args:
|
||||
function_name: Name of the tool to execute
|
||||
arguments: Arguments for the tool
|
||||
|
||||
Returns:
|
||||
Tool execution result or circuit breaker error
|
||||
"""
|
||||
# Get retry policy (tool-level > agent-level > default)
|
||||
retry_policy = self._get_tool_retry_policy(function_name)
|
||||
|
||||
last_exception = None
|
||||
|
||||
# Retry loop with exponential backoff
|
||||
for attempt in range(retry_policy.max_attempts):
|
||||
try:
|
||||
result = self._execute_tool_with_circuit_breaker_impl(function_name, arguments)
|
||||
|
||||
# Check if result is an error that should be retried
|
||||
if isinstance(result, dict) and result.get("error"):
|
||||
# Skip retry for non-retryable errors (approval, permission, etc.)
|
||||
if (result.get("approval_denied") or
|
||||
result.get("permission_denied") or
|
||||
result.get("approval_error") or
|
||||
result.get("circuit_open")):
|
||||
return result
|
||||
|
||||
# Determine error type for retry policy
|
||||
error_type = self._classify_error_type(result, last_exception)
|
||||
|
||||
# Check if we should retry
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
return result
|
||||
|
||||
# Don't retry on last attempt
|
||||
if attempt == retry_policy.max_attempts - 1:
|
||||
return result
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
self._emit_retry_hook(function_name, attempt + 1, delay_ms, result.get("error", "Unknown error"), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
else:
|
||||
# Success - return result
|
||||
return result
|
||||
|
||||
except ToolExecutionError as e:
|
||||
last_exception = e
|
||||
# Check if the error is retryable
|
||||
if not e.is_retryable or attempt == retry_policy.max_attempts - 1:
|
||||
raise
|
||||
|
||||
# Determine error type
|
||||
error_type = self._classify_error_type(None, e)
|
||||
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
raise
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
self._emit_retry_hook(function_name, attempt + 1, delay_ms, str(e), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
# Wrap in ToolExecutionError for consistency
|
||||
is_retryable = not isinstance(e, (ValueError, TypeError, AttributeError))
|
||||
wrapped_error = ToolExecutionError(
|
||||
f"Tool '{function_name}' failed: {e}",
|
||||
tool_name=function_name,
|
||||
agent_id=self.name,
|
||||
is_retryable=is_retryable,
|
||||
)
|
||||
|
||||
# Check if retryable and not last attempt
|
||||
if not is_retryable or attempt == retry_policy.max_attempts - 1:
|
||||
raise wrapped_error from e
|
||||
|
||||
# Determine error type
|
||||
error_type = self._classify_error_type(None, wrapped_error)
|
||||
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
raise wrapped_error from e
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
self._emit_retry_hook(function_name, attempt + 1, delay_ms, str(e), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
|
||||
# Should never reach here due to loop logic, but safety fallback
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
return {"error": "Maximum retry attempts exceeded"}
|
||||
|
||||
def _execute_tool_with_circuit_breaker_impl(self, function_name, arguments):
|
||||
"""Execute tool with circuit breaker protection (internal implementation).
|
||||
|
||||
Args:
|
||||
function_name: Name of the tool to execute
|
||||
@@ -1110,3 +1227,110 @@ class ToolExecutionMixin:
|
||||
|
||||
else:
|
||||
return f"Unknown bridge tool: {function_name}"
|
||||
|
||||
def _get_tool_retry_policy(self, tool_name):
|
||||
"""Get retry policy for a tool (tool-level > agent-level > default).
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to get retry policy for
|
||||
|
||||
Returns:
|
||||
RetryPolicy instance
|
||||
"""
|
||||
from ..tools.retry import RetryPolicy
|
||||
|
||||
# Check for tool-level retry policy first
|
||||
tools = getattr(self, 'tools', [])
|
||||
# Handle non-iterable tools (e.g., single MCP instance)
|
||||
if not isinstance(tools, (list, tuple)):
|
||||
tools = [] # MCP or single tool instance - no tool-level policy lookup
|
||||
for tool in tools:
|
||||
if (callable(tool) and
|
||||
getattr(tool, '__name__', '') == tool_name and
|
||||
hasattr(tool, 'retry_policy')):
|
||||
return tool.retry_policy
|
||||
|
||||
# Check for agent-level retry policy
|
||||
agent_policy = getattr(self, '_tool_retry_policy', None)
|
||||
if agent_policy is not None:
|
||||
return agent_policy
|
||||
|
||||
# Return default retry policy (cached class-level instance)
|
||||
if not hasattr(ToolExecutionMixin, '_default_retry_policy'):
|
||||
ToolExecutionMixin._default_retry_policy = RetryPolicy()
|
||||
return ToolExecutionMixin._default_retry_policy
|
||||
|
||||
def _classify_error_type(self, error_dict, exception):
|
||||
"""Classify error type for retry policy matching.
|
||||
|
||||
Args:
|
||||
error_dict: Error dictionary from tool execution (if any)
|
||||
exception: Exception that was raised (if any)
|
||||
|
||||
Returns:
|
||||
String error type for retry policy checking
|
||||
"""
|
||||
# Check error dict first
|
||||
if error_dict and isinstance(error_dict, dict):
|
||||
error_msg = error_dict.get("error", "").lower()
|
||||
if "timeout" in error_msg or "timed out" in error_msg:
|
||||
return "timeout"
|
||||
elif "rate" in error_msg and "limit" in error_msg:
|
||||
return "rate_limit"
|
||||
elif "connection" in error_msg or "network" in error_msg:
|
||||
return "connection_error"
|
||||
|
||||
# Check exception type
|
||||
if exception:
|
||||
exc_msg = str(exception).lower()
|
||||
exc_type = type(exception).__name__.lower()
|
||||
|
||||
if "timeout" in exc_msg or "timeout" in exc_type:
|
||||
return "timeout"
|
||||
elif "rate" in exc_msg and "limit" in exc_msg:
|
||||
return "rate_limit"
|
||||
elif ("connection" in exc_msg or "network" in exc_msg or
|
||||
"connection" in exc_type):
|
||||
return "connection_error"
|
||||
|
||||
return "unknown"
|
||||
|
||||
def _emit_retry_hook(self, tool_name, attempt, delay_ms, error, max_attempts, error_type):
|
||||
"""Emit ON_RETRY hook event.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool being retried
|
||||
attempt: Current attempt number (1-based)
|
||||
delay_ms: Delay before retry in milliseconds
|
||||
error: Error message or description
|
||||
max_attempts: Maximum number of attempts configured
|
||||
error_type: Classified error type
|
||||
"""
|
||||
try:
|
||||
from ..hooks import HookEvent, OnRetryInput
|
||||
|
||||
# Only emit if we have a hook runner
|
||||
hook_runner = getattr(self, '_hook_runner', None)
|
||||
if hook_runner is None:
|
||||
return
|
||||
|
||||
retry_input = OnRetryInput(
|
||||
session_id=getattr(self, '_session_id', 'default'),
|
||||
cwd=os.getcwd(),
|
||||
event_name=HookEvent.ON_RETRY,
|
||||
timestamp=str(time.time()),
|
||||
agent_name=self.name,
|
||||
tool_name=tool_name,
|
||||
attempt=attempt,
|
||||
delay_ms=delay_ms,
|
||||
error=error,
|
||||
max_attempts=max_attempts,
|
||||
error_type=error_type
|
||||
)
|
||||
|
||||
# Execute hook synchronously
|
||||
hook_runner.execute_sync(HookEvent.ON_RETRY, retry_input, target=tool_name)
|
||||
|
||||
except Exception as e:
|
||||
# Don't let hook failures break retry logic
|
||||
logging.debug(f"Failed to emit retry hook: {e}")
|
||||
@@ -725,6 +725,9 @@ class ExecutionConfig:
|
||||
# Retry settings
|
||||
max_retry_limit: int = 2
|
||||
|
||||
# Tool call limits (loop protection)
|
||||
max_tool_calls_per_turn: int = 10
|
||||
|
||||
# Code execution (consolidated from allow_code_execution + code_execution_mode)
|
||||
code_execution: bool = False
|
||||
code_mode: str = "safe" # "safe" or "unsafe"
|
||||
@@ -840,6 +843,67 @@ class ExecutionConfig:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolConfig:
|
||||
"""
|
||||
Configuration for tool execution behavior.
|
||||
|
||||
Configuration for tool execution behavior including timeout, retry policy, and parallel execution.
|
||||
|
||||
Usage:
|
||||
# Simple enable with defaults
|
||||
Agent(tool_config=ToolConfig())
|
||||
|
||||
# With custom settings
|
||||
Agent(tool_config=ToolConfig(
|
||||
timeout=60,
|
||||
retry_policy=RetryPolicy(max_attempts=5),
|
||||
parallel=True,
|
||||
))
|
||||
|
||||
# With timeout only
|
||||
Agent(tool_config=ToolConfig(timeout=30))
|
||||
"""
|
||||
# Tool execution timeout in seconds
|
||||
timeout: Optional[int] = None
|
||||
|
||||
# Retry policy for tool execution with exponential backoff
|
||||
retry_policy: Optional[Any] = None # RetryPolicy instance
|
||||
|
||||
# Enable parallel execution of batched LLM tool calls
|
||||
parallel: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"timeout": self.timeout,
|
||||
"retry_policy": (
|
||||
self.retry_policy.to_dict()
|
||||
if hasattr(self.retry_policy, 'to_dict')
|
||||
else self.retry_policy
|
||||
),
|
||||
"parallel": self.parallel,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ToolConfig":
|
||||
"""Create ToolConfig from dictionary."""
|
||||
retry_policy = data.get("retry_policy")
|
||||
if isinstance(retry_policy, dict):
|
||||
try:
|
||||
from ..tools.retry import RetryPolicy
|
||||
retry_policy = RetryPolicy.from_dict(retry_policy)
|
||||
except ImportError:
|
||||
# Keep as dict if RetryPolicy not available
|
||||
pass
|
||||
|
||||
return cls(
|
||||
timeout=data.get("timeout"),
|
||||
retry_policy=retry_policy,
|
||||
parallel=data.get("parallel", False),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateConfig:
|
||||
"""
|
||||
@@ -1185,6 +1249,7 @@ HooksParam = Union[List[Any], HooksConfig]
|
||||
SkillsParam = Union[List[str], SkillsConfig]
|
||||
AutonomyParam = Union[bool, Dict[str, Any], "AutonomyConfig"]
|
||||
ToolSearchParam = Union[bool, str, Dict[str, Any], ToolSearchConfig]
|
||||
ToolParam = Union[bool, ToolConfig] # bool = defaults, ToolConfig = custom
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -1451,6 +1516,19 @@ def resolve_tool_search(value: ToolSearchParam) -> Optional[ToolSearchConfig]:
|
||||
return value
|
||||
|
||||
|
||||
def resolve_tools(value: ToolParam) -> Optional[ToolConfig]:
|
||||
"""Resolve tools= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return ToolConfig()
|
||||
if isinstance(value, dict):
|
||||
return ToolConfig.from_dict(value)
|
||||
if isinstance(value, ToolConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"MemoryBackend",
|
||||
@@ -1471,6 +1549,7 @@ __all__ = [
|
||||
"WebConfig",
|
||||
"OutputConfig",
|
||||
"ExecutionConfig",
|
||||
"ToolConfig",
|
||||
"TemplateConfig",
|
||||
"CachingConfig",
|
||||
"HooksConfig",
|
||||
@@ -1497,7 +1576,8 @@ __all__ = [
|
||||
"HooksParam",
|
||||
"SkillsParam",
|
||||
"AutonomyParam",
|
||||
"ToolSearchParam",
|
||||
"ToolSearchParam",
|
||||
"ToolParam",
|
||||
# Precedence ladder resolvers
|
||||
"resolve_memory",
|
||||
"resolve_knowledge",
|
||||
@@ -1510,4 +1590,5 @@ __all__ = [
|
||||
"resolve_caching",
|
||||
"resolve_autonomy",
|
||||
"resolve_tool_search",
|
||||
"resolve_tools",
|
||||
]
|
||||
@@ -178,7 +178,14 @@ class OnErrorInput(HookInput):
|
||||
|
||||
@dataclass
|
||||
class OnRetryInput(HookInput):
|
||||
"""Input for OnRetry hooks."""
|
||||
"""Input for OnRetry hooks fired during tool execution retries."""
|
||||
tool_name: str = ""
|
||||
attempt: int = 1
|
||||
delay_ms: int = 0
|
||||
error: str = ""
|
||||
max_attempts: int = 0
|
||||
error_type: str = "unknown"
|
||||
# Legacy fields for backward compatibility
|
||||
retry_count: int = 0
|
||||
max_retries: int = 3
|
||||
error_message: str = ""
|
||||
@@ -187,6 +194,13 @@ class OnRetryInput(HookInput):
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
base = super().to_dict()
|
||||
base.update({
|
||||
"tool_name": self.tool_name,
|
||||
"attempt": self.attempt,
|
||||
"delay_ms": self.delay_ms,
|
||||
"error": self.error,
|
||||
"max_attempts": self.max_attempts,
|
||||
"error_type": self.error_type,
|
||||
# Legacy fields
|
||||
"retry_count": self.retry_count,
|
||||
"max_retries": self.max_retries,
|
||||
"error_message": self.error_message,
|
||||
|
||||
@@ -2066,6 +2066,7 @@ Now provide your final answer using this result. Summarize the information natur
|
||||
task_id: Optional[str] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
parallel_tool_calls: bool = False, # Gap 2: Enable parallel tool execution
|
||||
max_tool_calls_per_turn: int = 10, # Loop guardrails
|
||||
stream: bool = True,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
@@ -2190,6 +2191,7 @@ Now provide your final answer using this result. Summarize the information natur
|
||||
# Sequential tool calling loop - similar to agent.py
|
||||
max_iterations = self.max_iter # Use configurable iteration limit
|
||||
iteration_count = 0
|
||||
tool_call_count = 0 # Track total tool calls for guardrails
|
||||
final_response_text = ""
|
||||
response_text = "" # Initialize to prevent UnboundLocalError on API errors
|
||||
stored_reasoning_content = None # Store reasoning content from tool execution
|
||||
@@ -3049,6 +3051,18 @@ Now provide your final answer using this result. Summarize the information natur
|
||||
tool_results = [] # Store current iteration tool results
|
||||
tool_result_mapping = {} # Store function results by name for Ollama chaining
|
||||
|
||||
# Guardrail: Check tool call limit to prevent infinite loops
|
||||
# Allow execution if we haven't reached the limit yet, but limit the batch size
|
||||
if tool_call_count >= max_tool_calls_per_turn:
|
||||
logging.warning(f"Tool call limit reached ({max_tool_calls_per_turn}). Stopping to prevent infinite loop.")
|
||||
final_response_text = f"Tool call limit reached ({max_tool_calls_per_turn} calls). Task may be too complex or there may be a broken tool causing repeated calls."
|
||||
break
|
||||
elif tool_call_count + len(tool_calls) > max_tool_calls_per_turn:
|
||||
# Limit the batch to stay within the total limit
|
||||
remaining_calls = max_tool_calls_per_turn - tool_call_count
|
||||
tool_calls = tool_calls[:remaining_calls]
|
||||
logging.warning(f"Limiting batch to {remaining_calls} tool calls to stay within limit of {max_tool_calls_per_turn}.")
|
||||
|
||||
for tool_call in tool_calls:
|
||||
# Handle both object and dict access patterns
|
||||
is_ollama = self._is_ollama_provider()
|
||||
@@ -3069,7 +3083,8 @@ Now provide your final answer using this result. Summarize the information natur
|
||||
|
||||
logging.debug(f"[TOOL_EXEC_DEBUG] About to execute tool {function_name} with args: {arguments}")
|
||||
tool_result = execute_tool_fn(function_name, arguments, tool_call_id=tool_call_id)
|
||||
logging.debug(f"[TOOL_EXEC_DEBUG] Tool execution result: {tool_result}")
|
||||
tool_call_count += 1 # Increment tool call counter for guardrails
|
||||
logging.debug(f"[TOOL_EXEC_DEBUG] Tool execution result: {tool_result} (call #{tool_call_count})")
|
||||
tool_results.append(tool_result) # Store the result
|
||||
accumulated_tool_results.append(tool_result) # Accumulate across iterations
|
||||
|
||||
@@ -3581,6 +3596,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
task_id: Optional[str] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
parallel_tool_calls: bool = False, # Gap 2: Enable parallel tool execution
|
||||
max_tool_calls_per_turn: int = 10, # Loop guardrails
|
||||
**kwargs
|
||||
):
|
||||
"""Generator that yields real-time response chunks from the LLM.
|
||||
@@ -3727,6 +3743,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
# After streaming completes, handle tool calls if present
|
||||
if tool_calls and execute_tool_fn:
|
||||
# Guardrail: Check tool call limit to prevent infinite loops
|
||||
if len(tool_calls) > max_tool_calls_per_turn:
|
||||
logging.warning(f"Tool call limit reached ({max_tool_calls_per_turn}). Stopping to prevent infinite loop.")
|
||||
error_message = f"Tool call limit reached ({max_tool_calls_per_turn} calls). Task may be too complex or there may be a broken tool causing repeated calls."
|
||||
yield error_message
|
||||
return
|
||||
|
||||
# Add assistant message with tool calls to conversation
|
||||
if self._is_ollama_provider():
|
||||
messages.append({
|
||||
@@ -3903,6 +3926,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
task_description: Optional[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
max_tool_calls_per_turn: int = 10, # Loop guardrails
|
||||
stream: bool = True,
|
||||
**kwargs
|
||||
) -> str:
|
||||
@@ -3981,6 +4005,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
# Initialize variables for iteration loop
|
||||
max_iterations = self.max_iter # Use configurable iteration limit
|
||||
iteration_count = 0
|
||||
tool_call_count = 0 # Track total tool calls for guardrails
|
||||
final_response_text = ""
|
||||
stored_reasoning_content = None # Store reasoning content from tool execution
|
||||
accumulated_tool_results = [] # Store all tool results across iterations
|
||||
@@ -4053,6 +4078,18 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
"tool_calls": serializable_tool_calls,
|
||||
})
|
||||
|
||||
# Guardrail: Check tool call limit to prevent infinite loops
|
||||
# Allow execution if we haven't reached the limit yet, but limit the batch size
|
||||
if tool_call_count >= max_tool_calls_per_turn:
|
||||
logging.warning(f"Tool call limit reached ({max_tool_calls_per_turn}). Stopping to prevent infinite loop.")
|
||||
final_response_text = f"Tool call limit reached ({max_tool_calls_per_turn} calls). Task may be too complex or there may be a broken tool causing repeated calls."
|
||||
break
|
||||
elif tool_call_count + len(tool_calls) > max_tool_calls_per_turn:
|
||||
# Limit the batch to stay within the total limit
|
||||
remaining_calls = max_tool_calls_per_turn - tool_call_count
|
||||
tool_calls = tool_calls[:remaining_calls]
|
||||
logging.warning(f"Limiting batch to {remaining_calls} tool calls to stay within limit of {max_tool_calls_per_turn}.")
|
||||
|
||||
for tool_call in tool_calls:
|
||||
function_name, arguments, tool_call_id = self._extract_tool_call_info(tool_call)
|
||||
logging.debug(f"[RESPONSES_API_ASYNC] Executing tool {function_name}")
|
||||
@@ -4060,6 +4097,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
tool_result = await execute_tool_fn(function_name, arguments, tool_call_id=tool_call_id)
|
||||
else:
|
||||
tool_result = execute_tool_fn(function_name, arguments, tool_call_id=tool_call_id)
|
||||
tool_call_count += 1 # Increment tool call counter for guardrails
|
||||
accumulated_tool_results.append(tool_result)
|
||||
|
||||
if verbose:
|
||||
@@ -4266,6 +4304,18 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
"tool_calls": serializable_tool_calls
|
||||
})
|
||||
|
||||
# Guardrail: Check tool call limit to prevent infinite loops
|
||||
# Allow execution if we haven't reached the limit yet, but limit the batch size
|
||||
if tool_call_count >= max_tool_calls_per_turn:
|
||||
logging.warning(f"Tool call limit reached ({max_tool_calls_per_turn}). Stopping to prevent infinite loop.")
|
||||
final_response_text = f"Tool call limit reached ({max_tool_calls_per_turn} calls). Task may be too complex or there may be a broken tool causing repeated calls."
|
||||
break
|
||||
elif tool_call_count + len(tool_calls) > max_tool_calls_per_turn:
|
||||
# Limit the batch to stay within the total limit
|
||||
remaining_calls = max_tool_calls_per_turn - tool_call_count
|
||||
tool_calls = tool_calls[:remaining_calls]
|
||||
logging.warning(f"Limiting batch to {remaining_calls} tool calls to stay within limit of {max_tool_calls_per_turn}.")
|
||||
|
||||
tool_results = [] # Store current iteration tool results
|
||||
for tool_call in tool_calls:
|
||||
# Handle both object and dict access patterns
|
||||
@@ -4277,6 +4327,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
arguments = self._validate_and_filter_ollama_arguments(function_name, arguments, tools)
|
||||
|
||||
tool_result = await execute_tool_fn(function_name, arguments)
|
||||
tool_call_count += 1 # Increment tool call counter for guardrails
|
||||
tool_results.append(tool_result) # Store the result
|
||||
accumulated_tool_results.append(tool_result) # Accumulate across iterations
|
||||
|
||||
@@ -4915,6 +4966,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
'task_name', 'task_description', 'task_id', # Task metadata
|
||||
'execute_tool_fn', 'stream_callback', 'emit_events', # Callbacks
|
||||
'console', # Rich console
|
||||
'max_tool_calls_per_turn', 'parallel_tool_calls', # Tool execution settings
|
||||
]
|
||||
for param in internal_params:
|
||||
params.pop(param, None)
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -340,14 +341,31 @@ class Session:
|
||||
pass
|
||||
|
||||
if session_store is not None:
|
||||
# Use SessionStore for conversation history
|
||||
# Replace atomically to avoid duplicate appends on repeated save_state()
|
||||
session_id = f"{self.session_id}_{agent_key}"
|
||||
for msg in chat_history:
|
||||
if isinstance(msg, dict):
|
||||
messages = [
|
||||
{
|
||||
"role": msg.get("role", "user"),
|
||||
"content": msg.get("content", ""),
|
||||
}
|
||||
for msg in chat_history
|
||||
if isinstance(msg, dict)
|
||||
]
|
||||
if not messages:
|
||||
logging.debug(f"No chat history to persist for session {session_id}")
|
||||
elif hasattr(session_store, "set_chat_history"):
|
||||
session_store.set_chat_history(session_id, messages)
|
||||
else:
|
||||
# Fallback to add_message - may create duplicates on repeated calls
|
||||
logging.warning(
|
||||
f"Session store lacks 'set_chat_history' method. Using fallback "
|
||||
f"'add_message' which may create duplicates on repeated save_state() calls."
|
||||
)
|
||||
for msg in messages:
|
||||
session_store.add_message(
|
||||
session_id,
|
||||
role=msg.get("role", "user"),
|
||||
content=msg.get("content", ""),
|
||||
role=msg["role"],
|
||||
content=msg["content"],
|
||||
)
|
||||
else:
|
||||
# Fallback to Memory.store_short_term() for backward compatibility
|
||||
|
||||
@@ -70,7 +70,8 @@ class FunctionTool(BaseTool):
|
||||
description: Optional[str] = None,
|
||||
version: str = "1.0.0",
|
||||
availability: Optional[Callable[[], tuple[bool, str]]] = None,
|
||||
dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None
|
||||
dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
|
||||
retry_policy: Optional[Any] = None
|
||||
):
|
||||
self._func = func
|
||||
self.name = name or func.__name__
|
||||
@@ -78,6 +79,7 @@ class FunctionTool(BaseTool):
|
||||
self.version = version
|
||||
self._availability = availability
|
||||
self._schema_override = dynamic_schema_overrides
|
||||
self.retry_policy = retry_policy
|
||||
|
||||
# Detect injected parameters
|
||||
self._injected_params = get_injected_params(func)
|
||||
@@ -205,7 +207,8 @@ def tool(
|
||||
description: Optional[str] = None,
|
||||
version: str = "1.0.0",
|
||||
availability: Optional[Callable[[], tuple[bool, str]]] = None,
|
||||
dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None
|
||||
dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
|
||||
retry_policy: Optional[Any] = None
|
||||
) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]:
|
||||
"""Decorator to convert a function into a tool.
|
||||
|
||||
@@ -223,6 +226,10 @@ def tool(
|
||||
@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing"))
|
||||
def my_func(x: str) -> str:
|
||||
return x
|
||||
|
||||
@tool(retry_policy=RetryPolicy(max_attempts=5))
|
||||
def my_func(x: str) -> str:
|
||||
return x
|
||||
|
||||
Args:
|
||||
func: The function to wrap (when used without parentheses)
|
||||
@@ -231,6 +238,7 @@ def tool(
|
||||
version: Tool version (default: "1.0.0")
|
||||
availability: Function that returns (is_available, reason) tuple
|
||||
dynamic_schema_overrides: Function to dynamically modify tool schema at runtime
|
||||
retry_policy: RetryPolicy for tool execution with exponential backoff
|
||||
|
||||
Returns:
|
||||
FunctionTool instance that wraps the function
|
||||
@@ -242,7 +250,8 @@ def tool(
|
||||
description=description,
|
||||
version=version,
|
||||
availability=availability,
|
||||
dynamic_schema_overrides=dynamic_schema_overrides
|
||||
dynamic_schema_overrides=dynamic_schema_overrides,
|
||||
retry_policy=retry_policy
|
||||
)
|
||||
|
||||
# Validate the tool at creation time for early error detection
|
||||
|
||||
@@ -21,13 +21,28 @@ from __future__ import annotations
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import os
|
||||
from typing import Optional
|
||||
from typing import Optional, Union, List
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Lazy-loaded AgentMail client (module-level singleton)
|
||||
_client = None
|
||||
|
||||
def _parse_email_list(emails: Union[str, List[str]]) -> List[str]:
|
||||
"""Parse email addresses from string or list format.
|
||||
|
||||
Args:
|
||||
emails: Email address(es) as string (comma-separated) or list
|
||||
|
||||
Returns:
|
||||
List of individual email addresses
|
||||
"""
|
||||
if not emails:
|
||||
return []
|
||||
if isinstance(emails, str):
|
||||
return [email.strip() for email in emails.split(',') if email.strip()]
|
||||
return [email.strip() for email in emails if email and email.strip()]
|
||||
|
||||
def _detect_backend() -> str:
|
||||
"""Detect which email backend is available.
|
||||
|
||||
@@ -83,15 +98,28 @@ def _normalize_message_id(message_id: str) -> str:
|
||||
return f"<{message_id}>"
|
||||
return message_id
|
||||
|
||||
def _agentmail_send_email(to: str, subject: str, body: str) -> str:
|
||||
def _agentmail_send_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
client = _get_client()
|
||||
inbox_id = _get_inbox_id()
|
||||
try:
|
||||
result = client.inboxes.messages.send(inbox_id, to=to, subject=subject, text=body)
|
||||
kwargs = {"to": to, "subject": subject, "text": body}
|
||||
|
||||
cc_list = _parse_email_list(cc) if cc else []
|
||||
bcc_list = _parse_email_list(bcc) if bcc else []
|
||||
|
||||
if cc_list:
|
||||
kwargs["cc"] = cc_list
|
||||
if bcc_list:
|
||||
kwargs["bcc"] = bcc_list
|
||||
|
||||
result = client.inboxes.messages.send(inbox_id, **kwargs)
|
||||
msg_id = getattr(result, "message_id", "unknown")
|
||||
thread_id = getattr(result, "thread_id", "")
|
||||
logger.info(f"Email sent to {to}: {msg_id}")
|
||||
return f"Email sent successfully to {to}. Message ID: {msg_id}, Thread ID: {thread_id}"
|
||||
|
||||
recipients = [to] + cc_list + bcc_list
|
||||
|
||||
logger.info(f"Email sent to {', '.join(recipients)}: {msg_id}")
|
||||
return f"Email sent successfully to {', '.join(recipients)}. Message ID: {msg_id}, Thread ID: {thread_id}"
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {to}: {e}")
|
||||
return f"Failed to send email: {e}"
|
||||
@@ -232,11 +260,21 @@ def _agentmail_forward_email(message_id: str, to: str, note: Optional[str] = Non
|
||||
logger.error(f"Failed to forward email {message_id}: {e}")
|
||||
return f"Failed to forward email: {e}"
|
||||
|
||||
def _agentmail_draft_email(to: str, subject: str, body: str) -> str:
|
||||
def _agentmail_draft_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
client = _get_client()
|
||||
inbox_id = _get_inbox_id()
|
||||
try:
|
||||
result = client.inboxes.drafts.create(inbox_id, to=[to], subject=subject, text=body)
|
||||
kwargs = {"to": [to] if isinstance(to, str) else to, "subject": subject, "text": body}
|
||||
|
||||
cc_list = _parse_email_list(cc) if cc else []
|
||||
bcc_list = _parse_email_list(bcc) if bcc else []
|
||||
|
||||
if cc_list:
|
||||
kwargs["cc"] = cc_list
|
||||
if bcc_list:
|
||||
kwargs["bcc"] = bcc_list
|
||||
|
||||
result = client.inboxes.drafts.create(inbox_id, **kwargs)
|
||||
draft_id = getattr(result, "draft_id", "unknown")
|
||||
logger.info(f"Draft created: {draft_id}")
|
||||
return f"Draft created (ID: {draft_id}). Use send_draft to send it."
|
||||
@@ -349,7 +387,7 @@ def _extract_body_preview(msg, max_len: int = 100) -> str:
|
||||
)[:max_len]
|
||||
return ""
|
||||
|
||||
def _smtp_send_email(to: str, subject: str, body: str) -> str:
|
||||
def _smtp_send_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
try:
|
||||
@@ -358,12 +396,27 @@ def _smtp_send_email(to: str, subject: str, body: str) -> str:
|
||||
msg["From"] = email_addr
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
|
||||
# Parse CC and BCC lists
|
||||
cc_list = _parse_email_list(cc) if cc else []
|
||||
bcc_list = _parse_email_list(bcc) if bcc else []
|
||||
|
||||
# Add CC header if provided (BCC is not added to headers by design)
|
||||
all_recipients = [to]
|
||||
if cc_list:
|
||||
msg["CC"] = ", ".join(cc_list)
|
||||
all_recipients.extend(cc_list)
|
||||
if bcc_list:
|
||||
all_recipients.extend(bcc_list)
|
||||
|
||||
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(email_addr, password)
|
||||
server.send_message(msg)
|
||||
logger.info(f"SMTP email sent to {to}")
|
||||
return f"Email sent successfully to {to} from {email_addr}"
|
||||
# Send to all recipients (to, cc, bcc)
|
||||
server.send_message(msg, to_addrs=all_recipients)
|
||||
|
||||
logger.info(f"SMTP email sent to {', '.join(all_recipients)}")
|
||||
return f"Email sent successfully to {', '.join(all_recipients)} from {email_addr}"
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
except Exception as e:
|
||||
@@ -562,7 +615,7 @@ def _smtp_archive_email(message_id: str) -> str:
|
||||
logger.error(f"Failed to archive email {message_id}: {e}")
|
||||
return f"Failed to archive email: {e}"
|
||||
|
||||
def _smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
def _smtp_draft_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
import imaplib
|
||||
import time
|
||||
from email.mime.text import MIMEText
|
||||
@@ -572,6 +625,17 @@ def _smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
msg["From"] = email_addr
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
|
||||
# Parse CC and BCC lists
|
||||
cc_list = _parse_email_list(cc) if cc else []
|
||||
bcc_list = _parse_email_list(bcc) if bcc else []
|
||||
|
||||
# Add CC header if provided (BCC is not added to headers in drafts)
|
||||
if cc_list:
|
||||
msg["CC"] = ", ".join(cc_list)
|
||||
if bcc_list:
|
||||
msg["Bcc"] = ", ".join(bcc_list)
|
||||
|
||||
mail = imaplib.IMAP4_SSL(imap_server, imap_port)
|
||||
mail.login(email_addr, password)
|
||||
# Gmail uses [Gmail]/Drafts, others use Drafts or DRAFTS
|
||||
@@ -582,8 +646,11 @@ def _smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
drafts_folder = "Drafts"
|
||||
mail.append(drafts_folder, "\\Draft", imaplib.Time2Internaldate(time.time()), msg.as_bytes())
|
||||
mail.logout()
|
||||
|
||||
recipients = [to] + cc_list + bcc_list
|
||||
|
||||
logger.info(f"Draft saved to {drafts_folder}")
|
||||
return f"Draft saved to {drafts_folder} (To: {to}, Subject: {subject})"
|
||||
return f"Draft saved to {drafts_folder} (To: {', '.join(recipients)}, Subject: {subject})"
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
except Exception as e:
|
||||
@@ -594,8 +661,8 @@ def _smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
# Generic Tools — Auto-detect backend
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
def send_email(to: str, subject: str, body: str) -> str:
|
||||
"""Send an email to someone.
|
||||
def send_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
"""Send an email to someone with optional CC and BCC recipients.
|
||||
|
||||
Auto-detects backend: AgentMail (if AGENTMAIL_API_KEY set) or
|
||||
SMTP (if EMAIL_ADDRESS + EMAIL_PASSWORD set).
|
||||
@@ -604,14 +671,18 @@ def send_email(to: str, subject: str, body: str) -> str:
|
||||
to: Recipient email address (e.g. bob@example.com)
|
||||
subject: Email subject line
|
||||
body: Email body text content
|
||||
cc: Optional CC (carbon copy) recipient(s). Can be a single email address or
|
||||
comma-separated string or list of email addresses
|
||||
bcc: Optional BCC (blind carbon copy) recipient(s). Can be a single email address or
|
||||
comma-separated string or list of email addresses
|
||||
|
||||
Returns:
|
||||
Confirmation message with the sent message ID
|
||||
"""
|
||||
backend = _detect_backend()
|
||||
if backend == "agentmail":
|
||||
return _agentmail_send_email(to, subject, body)
|
||||
return _smtp_send_email(to, subject, body)
|
||||
return _agentmail_send_email(to, subject, body, cc, bcc)
|
||||
return _smtp_send_email(to, subject, body, cc, bcc)
|
||||
|
||||
def list_emails(limit: int = 10) -> str:
|
||||
"""List recent emails in the inbox.
|
||||
@@ -730,8 +801,8 @@ def forward_email(message_id: str, to: str, note: Optional[str] = None) -> str:
|
||||
return _agentmail_forward_email(message_id, to, note)
|
||||
return "Forward not supported with IMAP. Use read_email + send_email as a workaround."
|
||||
|
||||
def draft_email(to: str, subject: str, body: str) -> str:
|
||||
"""Create an email draft without sending it.
|
||||
def draft_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
"""Create an email draft without sending it, with optional CC and BCC.
|
||||
|
||||
Auto-detects backend: AgentMail or IMAP.
|
||||
AgentMail: use send_draft() to send later.
|
||||
@@ -741,14 +812,16 @@ def draft_email(to: str, subject: str, body: str) -> str:
|
||||
to: Recipient email address
|
||||
subject: Email subject line
|
||||
body: Email body text
|
||||
cc: Optional CC (carbon copy) recipient email address or comma-separated list
|
||||
bcc: Optional BCC (blind carbon copy) recipient email address or comma-separated list
|
||||
|
||||
Returns:
|
||||
Confirmation with draft details
|
||||
"""
|
||||
backend = _detect_backend()
|
||||
if backend == "agentmail":
|
||||
return _agentmail_draft_email(to, subject, body)
|
||||
return _smtp_draft_email(to, subject, body)
|
||||
return _agentmail_draft_email(to, subject, body, cc, bcc)
|
||||
return _smtp_draft_email(to, subject, body, cc, bcc)
|
||||
|
||||
def send_draft(draft_id: str) -> str:
|
||||
"""Send a previously created email draft.
|
||||
@@ -827,8 +900,8 @@ def create_inbox(display_name: Optional[str] = None) -> str:
|
||||
# Backward-compatible aliases (smtp_ prefix)
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
def smtp_send_email(to: str, subject: str, body: str) -> str:
|
||||
"""Send an email using SMTP (Gmail, Outlook, etc.).
|
||||
def smtp_send_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
"""Send an email using SMTP (Gmail, Outlook, etc.) with optional CC/BCC.
|
||||
|
||||
Uses EMAIL_ADDRESS and EMAIL_PASSWORD env vars.
|
||||
|
||||
@@ -836,11 +909,13 @@ def smtp_send_email(to: str, subject: str, body: str) -> str:
|
||||
to: Recipient email address
|
||||
subject: Email subject line
|
||||
body: Email body text
|
||||
cc: Optional CC (carbon copy) recipient email address
|
||||
bcc: Optional BCC (blind carbon copy) recipient email address
|
||||
|
||||
Returns:
|
||||
Confirmation message
|
||||
"""
|
||||
return _smtp_send_email(to, subject, body)
|
||||
return _smtp_send_email(to, subject, body, cc, bcc)
|
||||
|
||||
def smtp_read_inbox(limit: int = 10, folder: str = "INBOX") -> str:
|
||||
"""Read recent emails from your mailbox using IMAP.
|
||||
@@ -890,8 +965,8 @@ def smtp_archive_email(message_id: str) -> str:
|
||||
"""
|
||||
return _smtp_archive_email(message_id)
|
||||
|
||||
def smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
"""Save an email draft using IMAP APPEND.
|
||||
def smtp_draft_email(to: str, subject: str, body: str, cc: Optional[Union[str, List[str]]] = None, bcc: Optional[Union[str, List[str]]] = None) -> str:
|
||||
"""Save an email draft using IMAP APPEND with optional CC/BCC.
|
||||
|
||||
Saves to Drafts folder ([Gmail]/Drafts for Gmail).
|
||||
|
||||
@@ -899,8 +974,10 @@ def smtp_draft_email(to: str, subject: str, body: str) -> str:
|
||||
to: Recipient email address
|
||||
subject: Email subject line
|
||||
body: Email body text
|
||||
cc: Optional CC (carbon copy) recipient email address
|
||||
bcc: Optional BCC (blind carbon copy) recipient email address
|
||||
|
||||
Returns:
|
||||
Confirmation message
|
||||
"""
|
||||
return _smtp_draft_email(to, subject, body)
|
||||
return _smtp_draft_email(to, subject, body, cc, bcc)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.53"
|
||||
version = "1.6.55"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user