Compare commits

..
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 3e9826bda7 fix: resolve missing AgentTeam import and RouterAgent kwargs forwarding
- Fix missing AgentTeam import in self-reflection-optimization-patterns.py
- Forward kwargs properly in RouterAgent._build_system_prompt to super()
- Addresses critical issues identified by Gemini code reviewer

Co-authored-by: Mervin Praison <[email protected]>
2026-06-12 23:25:32 +00:00
praisonai-triage-agent[bot]andMervin Praison fc298049e2 fix: resolve critical RouterAgent **kwargs bug and add missing if __name__ guards
- Fixed TypeError in RouterAgent._build_system_prompt() by removing **kwargs from super() call
- Added if __name__ == '__main__' guards to 6 example files to prevent auto-execution on import
- Addresses Gemini Code Assist review feedback

Co-authored-by: Mervin Praison <[email protected]>
2026-06-12 23:25:32 +00:00
Dhivya-Bharathy 0b7bf10820 fix(examples): phase-4 API alignment for 40 validation failures on v4.6.52 2026-06-12 23:25:32 +00:00
Dhivya-Bharathy cbb7c726aa fix(examples): phase-3 API alignment for consolidated params on v4.6.52 2026-06-12 23:25:12 +00:00
Dhivya-Bharathy 21d1986d0c fix(examples): align AgentTeam memory and knowledge configs with v4.6.52 API 2026-06-12 23:25:12 +00:00
praisonai-triage-agent[bot]andMervin Praison 26b5792192 fix: correct TypeError in 3 example files by using proper parameter names
- Fix MemoryConfig(memory=...) → user_id=... in agent_centric_api.py
- Fix Session(memory=...) → user_id=... in session-example.py
- Fix build_context_for_task(memory=...) → user_id=... in memory-quality-example.py

Co-authored-by: Mervin Praison <[email protected]>
2026-06-12 23:25:12 +00:00
Dhivya-Bharathy afcea5800e fix(examples): resolve syntax errors and align with consolidated-params API 2026-06-12 23:25:12 +00:00
155 changed files with 1307 additions and 4280 deletions

No files matched your search

+4 -4
View File
@@ -60,16 +60,16 @@ jobs:
- name: Run PraisonAI integration tests
run: |
cd PraisonAI/src/praisonai
python -m pytest tests/integration/test_aiui_* -v
python -m pytest tests/integration/test_aiui_* -v --timeout=300
- name: Run PraisonAIUI integration tests
run: |
cd PraisonAIUI
if [ -f tests/integration/test_agentic_roundtrip.py ]; then
python -m pytest tests/integration/test_agentic_roundtrip.py -v
python -m pytest tests/integration/test_agentic_roundtrip.py -v --timeout=300
fi
if [ -f tests/test_feature_sdk_backends.py ]; then
python -m pytest tests/test_feature_sdk_backends.py -v
python -m pytest tests/test_feature_sdk_backends.py -v --timeout=300
fi
- name: Test Pattern C CLI
@@ -152,7 +152,7 @@ jobs:
run: |
cd PraisonAI/src/praisonai
if [ -n "$OPENAI_API_KEY" ]; then
python -m pytest tests/integration/test_aiui_host_agentic.py -v
python -m pytest tests/integration/test_aiui_host_agentic.py -v --timeout=600
else
echo "OPENAI_API_KEY not available, skipping agentic tests"
fi
-10
View File
@@ -1,10 +0,0 @@
# Agent Instructions
You are working on the PraisonAI project.
## Project Guidelines
- Follow the existing code style and conventions
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design: core protocols in praisonaiagents/, heavy implementations in praisonai/
+1 -5
View File
@@ -240,7 +240,6 @@ from praisonaiagents import Memory, MemoryBackend, MemoryConfig
Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_cache_optimized_context</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_cache_boundary: bool = True, include_in_output: Optional[bool] = None) -> Dict[str, str]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
@@ -699,7 +698,7 @@ Methods:
Types:
```python
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, aembedding, aembeddings, embedding, embeddings, get_dimensions
```
Methods:
@@ -915,9 +914,6 @@ Methods:
* <code title="cli">praisonai gateway install <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway logs <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway mint-link <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway pause <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway reconnect <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway resume <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.57" \
"praisonai>=4.6.55" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.57" \
"praisonai>=4.6.55" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.57" \
"praisonai>=4.6.55" \
"praisonai[ui]" \
"praisonai[crewai]"
+6 -1
View File
@@ -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(verbose=True, stream=True),
output="verbose",
planning=WorkflowPlanningConfig(enabled=True, reasoning=True),
memory=WorkflowMemoryConfig(backend="file"),
# Agent-like params
+3 -3
View File
@@ -5,12 +5,12 @@ Demonstrates web search with consolidated params.
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
"""
from praisonaiagents import Agent, WebConfig
from praisonaiagents import Agent
# Basic: Enable web search with preset (search only — fetch needs Claude)
# Basic: Enable web search with preset
agent = Agent(
instructions="You are a research assistant with web access.",
web=WebConfig(search=True, fetch=False, search_provider="duckduckgo"),
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.")
+1 -2
View File
@@ -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__":
+1 -1
View File
@@ -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__":
+13 -3
View File
@@ -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:")
+4 -2
View File
@@ -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",
},
}
)
+8 -5
View File
@@ -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
+6 -3
View File
@@ -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
+6 -3
View File
@@ -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
+6 -3
View File
@@ -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
+6 -3
View File
@@ -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"
)
+6 -3
View File
@@ -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
+1 -1
View File
@@ -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.")
+12 -16
View File
@@ -6,18 +6,10 @@ It includes self-reflection for improving code quality and multiple specialized
"""
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.tools import CodeInterpreter
def run_python_code(code: str) -> str:
"""Execute Python code safely and return stdout or errors."""
import io
import contextlib
try:
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
exec(code, {"__builtins__": __builtins__})
return buf.getvalue() or "Executed successfully (no output)"
except Exception as e:
return f"Execution error: {e}"
# Initialize code execution tool
code_interpreter = CodeInterpreter()
# Custom code analysis tool
def analyze_code_complexity(code: str) -> str:
@@ -40,7 +32,9 @@ code_writer = Agent(
goal="Write clean, efficient, and well-documented Python code",
backstory="You are a senior Python developer with 10+ years of experience in writing production-quality code.",
instructions="Write Python code that follows PEP 8 standards, includes proper error handling, and has clear documentation.",
output="silent",
reflection=True,
)
code_reviewer = Agent(
@@ -58,7 +52,7 @@ code_executor = Agent(
goal="Safely execute and test Python code",
backstory="You are responsible for running code and reporting results or errors.",
instructions="Execute the provided code safely and report the output or any errors encountered.",
tools=[run_python_code]
tools=[code_interpreter]
)
# Example tasks
@@ -93,7 +87,7 @@ def demonstrate_code_agent():
workflow = AgentTeam(
agents=[code_writer, code_reviewer, code_executor],
tasks=[write_task, review_task, execute_task],
process="sequential", output="silent"
process="sequential", output="verbose"
)
# Run the workflow
@@ -112,8 +106,10 @@ general_code_agent = Agent(
3. Explain code concepts
4. Refactor code for better performance
5. Add tests and documentation""",
tools=[run_python_code, analyze_code_complexity],
output="silent",
tools=[code_interpreter, analyze_code_complexity],
reflection=True,
)
if __name__ == "__main__":
+16 -19
View File
@@ -41,25 +41,21 @@ def calculate_statistics(numbers: list) -> str:
"""Calculate basic statistics for a list of numbers."""
if not numbers:
return "No numbers provided"
try:
nums = [float(x) for x in numbers]
except (TypeError, ValueError):
return "Error: all values must be numbers"
n = len(nums)
mean = sum(nums) / n
sorted_nums = sorted(nums)
n = len(numbers)
mean = sum(numbers) / n
sorted_nums = sorted(numbers)
median = sorted_nums[n//2] if n % 2 else (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
variance = sum((x - mean)**2 for x in nums) / n
variance = sum((x - mean)**2 for x in numbers) / n
std_dev = math.sqrt(variance)
return f"""Statistics:
- Count: {n}
- Mean: {mean:.4f}
- Median: {median:.4f}
- Standard Deviation: {std_dev:.4f}
- Min: {min(nums)}
- Max: {max(nums)}"""
- Min: {min(numbers)}
- Max: {max(numbers)}"""
# Create specialized math agents
calculation_agent = Agent(
@@ -70,7 +66,8 @@ calculation_agent = Agent(
instructions="Perform the requested calculations step by step, showing your work clearly.",
tools=[basic_calculator, solve_quadratic, calculate_statistics],
reflection=True,
output="silent",
)
problem_solver_agent = Agent(
@@ -85,7 +82,8 @@ problem_solver_agent = Agent(
4. Show each step of the solution
5. Verify the answer makes sense""",
reflection=True,
output="silent",
)
math_tutor_agent = Agent(
@@ -93,8 +91,7 @@ math_tutor_agent = Agent(
role="Mathematics tutor and explainer",
goal="Explain mathematical concepts clearly and help users understand math",
backstory="You are a patient math tutor who can explain concepts at any level from elementary to advanced.",
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding.",
output="silent",
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding."
)
# Example workflow for solving complex problems
@@ -129,8 +126,7 @@ def solve_math_problem_workflow():
workflow = AgentTeam(
agents=[problem_solver_agent, calculation_agent, math_tutor_agent],
tasks=[understand_task, solve_task, verify_task],
process="sequential",
output="silent",
process="sequential", output="verbose"
)
return workflow.start()
@@ -152,7 +148,8 @@ math_assistant = Agent(
Always show your work step by step.""",
tools=[basic_calculator, solve_quadratic, calculate_statistics],
reflection=True,
output="silent",
)
if __name__ == "__main__":
+3 -2
View File
@@ -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="/")
+11 -2
View File
@@ -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")
+1 -1
View File
@@ -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()
+11 -2
View File
@@ -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}")
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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()
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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"},
}
)
+16 -6
View File
@@ -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(
+1 -1
View File
@@ -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.")
+6 -7
View File
@@ -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()
+24 -27
View File
@@ -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.")
+26 -29
View File
@@ -16,36 +16,33 @@ Protocol: SEP-1288 (WebSocket Transport)
from praisonaiagents import Agent, MCP
# Basic WebSocket connection
agent = Agent(
name="WebSocket Assistant",
instructions="You are a helpful assistant connected via WebSocket.",
tools=MCP("ws://localhost:8080/mcp")
)
# Secure WebSocket with authentication
agent_secure = Agent(
name="Secure WebSocket Assistant",
instructions="You are a helpful assistant with secure WebSocket connection.",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="Bearer your-secret-token",
timeout=60
)
)
# WebSocket with custom options
agent_custom = Agent(
name="Custom WebSocket Assistant",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="your-token",
timeout=120,
debug=True
)
)
if __name__ == "__main__":
agent = Agent(
name="WebSocket Assistant",
instructions="You are a helpful assistant connected via WebSocket.",
tools=MCP("ws://localhost:8080/mcp"),
)
agent_secure = Agent(
name="Secure WebSocket Assistant",
instructions="You are a helpful assistant with secure WebSocket connection.",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="Bearer your-secret-token",
timeout=60,
),
)
agent_custom = Agent(
name="Custom WebSocket Assistant",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="your-token",
timeout=120,
debug=True,
),
)
# Note: Requires a running WebSocket MCP server
# Example server: https://github.com/modelcontextprotocol/servers
@@ -12,8 +12,8 @@ print("=== Advanced Graph Memory Integration Example ===\n")
# Configure simple graph memory
memory_config = {
"provider": "mem0",
"config": {
"provider": "mem0",
"vector_store": {
"provider": "chroma",
"config": {
@@ -12,8 +12,8 @@ print("=== Advanced Memory Search and Optimization Example ===\n")
# Configure optimized memory settings
memory_config = {
"provider": "mem0",
"config": {
"provider": "mem0",
"vector_store": {
"provider": "chroma",
"config": {
@@ -86,13 +86,13 @@ def main():
agents=[researcher, retriever],
tasks=[store_task, retrieve_task],
memory={
"backend": "sqlite",
"embedder": {
"provider": "gemini",
"config": {
"model": "text-embedding-004",
}
}
},
"config": {"provider": "sqlite"},
}
)
@@ -30,7 +30,11 @@ config = {
agent = Agent(
name="Knowledge Agent",
instructions="You answer questions based on the provided knowledge.",
knowledge={**config, "sources": ["kag-research-paper.pdf"]},
knowledge={
"sources": ["kag-research-paper.pdf"],
"vector_store": config["vector_store"],
"embedder": config["embedder"],
},
memory={"user_id": "user1"},
llm="deepseek-r1"
)
@@ -31,10 +31,9 @@ llm_config = {
agent = Agent(
instructions="You are a helpful Assistant specialized in scientific explanations. "
"Provide clear, accurate, and engaging responses.",
llm=llm_config, # Pass the detailed configuration
# Enable detailed output
markdown=True, # Format responses in markdown
reflection=True # Enable self-reflection
llm=llm_config,
output={"markdown": True},
reflection=True,
)
# Test the agent
@@ -28,8 +28,8 @@ if not os.environ.get("OPENAI_API_KEY"):
def main():
# MongoDB memory configuration
mongodb_memory_config = {
"provider": "mongodb",
"config": {
"provider": "mongodb",
"connection_string": "mongodb://localhost:27017/", # Replace with your MongoDB connection string
"database": "praisonai_memory",
"use_vector_search": True, # Enable Atlas Vector Search (requires MongoDB Atlas)
@@ -4,7 +4,7 @@ Example 3: Save Agent Output Using Workflow output_file
Workflow step output is automatically saved with variable substitution.
"""
from praisonaiagents import AgentFlow
from praisonaiagents import Agent, AgentFlow, Task
# Define workflow with output_file
workflow_config = {
@@ -33,9 +33,30 @@ workflow_config = {
]
}
# Run workflow
workflow = AgentFlow(config=workflow_config)
result = workflow.run()
# Build agent and workflow from config dict
writer_cfg = workflow_config["agents"]["writer"]
writer = Agent(
name="writer",
role=writer_cfg["role"],
goal=writer_cfg["goal"],
llm=writer_cfg["llm"],
)
step_cfg = workflow_config["steps"][0]
workflow = AgentFlow(
name=workflow_config["metadata"]["name"],
variables=workflow_config["variables"],
steps=[
Task(
name="write_tutorial",
description=step_cfg["action"],
expected_output=step_cfg["expected_output"],
output_file=step_cfg["output_file"],
agent=writer,
)
],
)
result = workflow.start()
print("✅ Workflow completed!")
print("Output saved to: generated/tutorial.md")
@@ -28,7 +28,7 @@ def example_basic_persistence():
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -59,7 +59,7 @@ def example_session_restoration():
agent1 = Agent(
name="MemoryBot",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -72,7 +72,7 @@ def example_session_restoration():
agent2 = Agent(
name="MemoryBot",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -69,9 +69,10 @@ agents_system = AgentTeam(
memory=True, output="verbose"
)
print("Starting state management demonstration...")
result = agents_system.start()
if __name__ == "__main__":
print("Starting state management demonstration...")
result = agents_system.start()
print(f"\nState Management Result: {result[:200]}...")
print("\n✅ State management patterns complete!")
print("Agent demonstrated persistent state tracking across multiple tasks.")
print(f"\nState Management Result: {str(result)[:200]}...")
print("\n✅ State management patterns complete!")
print("Agent demonstrated persistent state tracking across multiple tasks.")
@@ -179,7 +179,7 @@ def main():
context = memory.build_context_for_task(
task_descr="Explain AI alignment challenges",
memory={"user_id": "quality_demo_user"},
user_id="quality_demo_user",
max_items=3
)
+1 -1
View File
@@ -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(
+9 -3
View File
@@ -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 -2
View File
@@ -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
)
@@ -6,6 +6,7 @@ to different next steps based on output content.
"""
from praisonaiagents import AgentFlow, Task
from praisonaiagents import AgentFlowManager
# Create a workflow with branching
workflow = AgentFlow(
@@ -35,9 +36,16 @@ workflow = AgentFlow(
)
if __name__ == "__main__":
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
# Create manager and register workflow
manager = WorkflowManager()
manager.workflows["Decision Workflow"] = workflow
# Execute - will branch to success_handler since 42 is positive
result = manager.execute(
"Decision Workflow",
default_llm="gpt-4o-mini"
)
print("Workflow completed!")
for step in result.get("steps", []):
name = step.get("step", step.get("name", "unknown"))
status = step.get("status", "completed")
print(f" {name}: {status}")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
@@ -54,13 +54,13 @@ workflow = AgentFlow(
analyzer, # Always runs
Task(
name="tech_review",
description="Review technical aspects of the content",
description="Review technical aspects when technical keywords are detected.",
agent=tech_reviewer,
should_run=is_technical # Only runs for technical content
),
Task(
name="creative_enhance",
description="Enhance the creative aspects of the content",
description="Enhance narrative quality when creative keywords are detected.",
agent=creative_enhancer,
should_run=needs_creative # Only runs for creative content
),
@@ -6,8 +6,9 @@ functions that return StepResult with stop_workflow=True.
"""
from praisonaiagents import (
AgentFlow, Task, WorkflowContext, StepResult
Workflow, Task, WorkflowContext, StepResult
)
from praisonaiagents import AgentFlowManager
# Custom validator that can stop the workflow
def validate_data(context: WorkflowContext) -> StepResult:
@@ -49,13 +50,20 @@ workflow = AgentFlow(
)
if __name__ == "__main__":
manager = WorkflowManager()
manager.workflows["Data Processing"] = workflow
# Test with invalid data (will stop early)
print("=== Testing with invalid data ===")
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
for step in result.get("steps", []):
print(f" {step.get('step', 'step')}: {step.get('output', step.get('status'))}")
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['output']}")
# Test with valid data (will complete)
print("\n=== Testing with valid data ===")
workflow.variables["data"] = {"value": 42}
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
for step in result.get("steps", []):
print(f" {step.get('step', 'step')}: {step.get('status', 'completed')}")
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
+15 -8
View File
@@ -6,6 +6,7 @@ Each item is processed individually with the loop variable.
"""
from praisonaiagents import AgentFlow, Task
from praisonaiagents import AgentFlowManager
# Create a workflow with loop
workflow = AgentFlow(
@@ -29,12 +30,18 @@ workflow = AgentFlow(
)
if __name__ == "__main__":
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
# Create manager and register workflow
manager = WorkflowManager()
manager.workflows["Process Items"] = workflow
# Execute - will process each fruit in the list
result = manager.execute(
"Process Items",
default_llm="gpt-4o-mini"
)
print("Workflow completed!")
for step in result.get("steps", []):
name = step.get("step", step.get("name", "unknown"))
status = step.get("status", "completed")
print(f" {name}: {status}")
out = step.get("output")
if isinstance(out, list):
print(f" Loop processed {len(out)} items")
for step_result in result["results"]:
print(f" {step_result['step']}: {step_result['status']}")
if isinstance(step_result.get("output"), list):
print(f" Loop processed {len(step_result['output'])} items")
@@ -53,7 +53,7 @@ workflow = AgentFlow(
# Step 4: Function - formats output
format_output
],
llm="gpt-4o-mini" # Used for action-based steps
llm="gpt-4o-mini", # Used for action-based steps
)
if __name__ == "__main__":
@@ -5,7 +5,7 @@ Demonstrates running multiple agents concurrently and
combining their results with an aggregator agent.
"""
from praisonaiagents import Agent, AgentFlow
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import parallel
# Create parallel research agents
@@ -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,
@@ -5,7 +5,7 @@ Demonstrates decision-based routing where an agent classifier
routes to specialized handler agents based on the request type.
"""
from praisonaiagents import Agent, AgentFlow
from praisonaiagents import Agent, Workflow
from praisonaiagents.workflows import route
# Create classifier agent
+9 -7
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+3 -11
View File
@@ -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__":
+1 -1
View File
@@ -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']}")
@@ -114,9 +114,6 @@ def _get_lazy_cache():
# ============================================================================
_LAZY_IMPORTS = {
# Version information (lazy loaded to avoid file I/O on import)
'__version__': ('praisonaiagents._version', '__version__'),
# Tools (moved from eager imports for lazy loading)
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
@@ -778,9 +775,6 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
# ============================================================================
__all__ = [
# Version information
'__version__',
# Core classes - the essentials
'Agent',
'AgentTeam', # Primary class for multi-agent coordination (v1.0+)
@@ -1,51 +0,0 @@
"""
Version utilities for praisonaiagents package.
This module provides version information by reading from pyproject.toml
to avoid duplication and ensure the single source of truth.
"""
import re
from pathlib import Path
def get_version() -> str:
"""
Get the version string from pyproject.toml.
Returns:
str: Version string (e.g., "1.6.52")
Raises:
RuntimeError: If version cannot be found or parsed
"""
try:
# Get the package root (praisonaiagents directory)
package_root = Path(__file__).parent
# Go up to src/praisonai-agents directory
pyproject_path = package_root.parent / "pyproject.toml"
if not pyproject_path.exists():
raise RuntimeError(f"pyproject.toml not found at {pyproject_path}")
content = pyproject_path.read_text()
# Look for version = "X.Y.Z" pattern
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if not match:
raise RuntimeError("Version not found in pyproject.toml")
return match.group(1)
except Exception as e:
# Fallback to "unknown" to avoid breaking imports
import warnings
warnings.warn(
f"Failed to read version from pyproject.toml: {e}. Using 'unknown'.",
RuntimeWarning
)
return "unknown"
# Cache the version to avoid reading the file multiple times
__version__ = get_version()
@@ -87,41 +87,12 @@ Your Goal: {self.goal}"""
# Add memory context if memory is enabled
if self._memory_instance:
# Use cache-optimized context if model supports prompt caching and cache boundary is requested
use_cache_boundaries = hasattr(self, '_model_supports_prompt_caching') and self._model_supports_prompt_caching()
if use_cache_boundaries and hasattr(self._memory_instance, 'build_cache_optimized_context'):
try:
# Get the goal/task description for context building
task_desc = getattr(self, 'goal', '') or 'general assistance'
cache_result = self._memory_instance.build_cache_optimized_context(
task_descr=task_desc,
include_cache_boundary=True
)
memory_context = cache_result.get('stable_prefix', '')
cache_boundary = cache_result.get('cache_boundary', '')
if memory_context:
system_prompt += f"\n\n## Memory (Information you remember about the user)\n{memory_context}"
if cache_boundary:
system_prompt += cache_boundary
# Display memory info to user if verbose
if self.verbose:
self._display_memory_info()
except (AttributeError, KeyError, TypeError) as e:
# Fall back to standard memory context if cache optimization fails
memory_context = self.get_memory_context()
if memory_context:
system_prompt += f"\n\n## Memory (Information you remember about the user)\n{memory_context}"
# Display memory info to user if verbose
if self.verbose:
self._display_memory_info()
else:
# Standard memory context without cache boundaries
memory_context = self.get_memory_context()
if memory_context:
system_prompt += f"\n\n## Memory (Information you remember about the user)\n{memory_context}"
# Display memory info to user if verbose
if self.verbose:
self._display_memory_info()
memory_context = self.get_memory_context()
if memory_context:
system_prompt += f"\n\n## Memory (Information you remember about the user)\n{memory_context}"
# Display memory info to user if verbose
if self.verbose:
self._display_memory_info()
# Add learn context if learn is enabled (auto-inject when memory="learn")
learn_context = self.get_learn_context()
@@ -481,17 +452,6 @@ Your Goal: {self.goal}"""
else:
cleaned_tools.append(tool)
# Sort tools deterministically for cache stability
def sort_formatted_tools(tools_list):
"""Sort already-formatted tool schemas by function name."""
def sort_key(tool):
if isinstance(tool, dict) and tool.get('type') == 'function':
return str(tool.get('function', {}).get('name') or '')
return ''
return sorted(tools_list, key=sort_key)
cleaned_tools = sort_formatted_tools(cleaned_tools)
# Cache the formatted tools with LRU eviction, including tool search metadata
self._cache_put(
self._formatted_tools_cache,
@@ -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":
@@ -245,20 +245,7 @@ Expected Output: {task.expected_output}."""
# Add memory context if available
if task.memory:
try:
# Use cache-optimized context if available for better prompt caching
if hasattr(task.memory, 'build_cache_optimized_context'):
try:
cache_result = task.memory.build_cache_optimized_context(
task_descr=task.description,
include_cache_boundary=False # Don't include boundary for agent task context
)
memory_context = cache_result.get('stable_prefix', '')
except Exception as e:
# Fall back to standard context building
logger.debug(f"Cache-optimized context failed for task '{task.description or task.id}', falling back to standard: {e}")
memory_context = task.memory.build_context_for_task(task.description)
else:
memory_context = task.memory.build_context_for_task(task.description)
memory_context = task.memory.build_context_for_task(task.description)
if memory_context:
# Log detailed memory context for debugging
logger.debug(f"Memory context for task '{task.description}': {memory_context}")
@@ -665,7 +652,7 @@ class AgentTeam(SpawnAnnounceProtocol):
_stream = _output_config.stream
else:
_verbose = 0
_stream = False
_stream = True
# ─────────────────────────────────────────────────────────────────────
# Resolve EXECUTION param using canonical resolver
@@ -33,10 +33,6 @@ __all__ = [
"CompactionStrategy",
# Results
"CompactionResult",
# Protocols
"ToolResultPrunerProtocol",
"MessageFormatterProtocol",
"SummaryBuilderProtocol",
]
@@ -58,16 +54,4 @@ def __getattr__(name: str):
from .result import CompactionResult
return CompactionResult
if name == "ToolResultPrunerProtocol":
from .protocols import ToolResultPrunerProtocol
return ToolResultPrunerProtocol
if name == "MessageFormatterProtocol":
from .protocols import MessageFormatterProtocol
return MessageFormatterProtocol
if name == "SummaryBuilderProtocol":
from .protocols import SummaryBuilderProtocol
return SummaryBuilderProtocol
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -5,13 +5,12 @@ Manages context window by compacting messages when needed.
"""
import re
from typing import List, Dict, Any, Optional, Callable, Awaitable, Tuple
from typing import List, Dict, Any, Optional, Callable, Awaitable
import asyncio
from .config import CompactionConfig, COMPACTION_PREFIX, SUMMARY_TEMPLATE
from .strategy import CompactionStrategy
from .result import CompactionResult
from .protocols import ToolResultPrunerProtocol, MessageFormatterProtocol, SummaryBuilderProtocol
class ContextCompactor:
@@ -35,10 +34,7 @@ class ContextCompactor:
preserve_system: bool = True,
preserve_recent: int = 5,
config: Optional[CompactionConfig] = None,
llm_summarize_fn: Optional[Callable[[str], Awaitable[str]]] = None,
tool_pruner: Optional[ToolResultPrunerProtocol] = None,
message_formatter: Optional[MessageFormatterProtocol] = None,
summary_builder: Optional[SummaryBuilderProtocol] = None
llm_summarize_fn: Optional[Callable[[str], Awaitable[str]]] = None
):
"""
Initialize the compactor.
@@ -49,43 +45,30 @@ class ContextCompactor:
strategy: Compaction strategy to use
preserve_system: Keep system messages
preserve_recent: Number of recent messages to preserve
config: Optional CompactionConfig for advanced settings
<<<<<<< HEAD
config: Optional CompactionConfig to override defaults
llm_summarize_fn: Async function to call LLM for summarization
tool_pruner: Protocol implementation for tool result pruning
message_formatter: Protocol implementation for message formatting
summary_builder: Protocol implementation for summary building
"""
# Use provided config or create default
self.config = config or CompactionConfig(
max_tokens=max_tokens,
target_tokens=target_tokens or int(max_tokens * 0.75),
preserve_system=preserve_system,
preserve_recent=preserve_recent
)
# Initialize config first
self.config = config or CompactionConfig()
# Set instance attributes from config to ensure consistency
self.max_tokens = self.config.max_tokens
self.target_tokens = self.config.target_tokens
# Use config values if config provided, otherwise use constructor args
if config is not None:
self.max_tokens = config.max_tokens
self.target_tokens = config.target_tokens
self.preserve_system = config.preserve_system
self.preserve_recent = config.preserve_recent
else:
self.max_tokens = max_tokens
self.target_tokens = target_tokens or int(max_tokens * 0.75)
self.preserve_system = preserve_system
self.preserve_recent = preserve_recent
self.strategy = strategy
self.preserve_system = self.config.preserve_system
self.preserve_recent = self.config.preserve_recent
self.llm_summarize_fn = llm_summarize_fn
# Protocol implementations (defaults to None - no heavy implementations in core)
self.tool_pruner = tool_pruner
self.message_formatter = message_formatter
self.summary_builder = summary_builder
# Anti-thrashing state tracking
self._last_savings_pct: float = 100.0 # Start high to allow first compaction
self._low_savings_streak: int = 0
# Iterative summary state
# Track previous summaries for iterative update
self._previous_summary: Optional[str] = None
self._previous_summary_global_idx: int = 0
# Reset state tracking for each compact() call
self._used_previous_summary: bool = False
def estimate_tokens(self, text: str) -> int:
"""Estimate token count for text."""
@@ -113,26 +96,12 @@ class ContextCompactor:
return sum(self.count_message_tokens(m) for m in messages)
def needs_compaction(self, messages: List[Dict[str, Any]]) -> bool:
"""
Check if messages need compaction with anti-thrashing protection.
Returns False if:
- Token count is below threshold
- We've had too many consecutive low-savings attempts
"""
if self.count_total_tokens(messages) <= self.max_tokens:
return False
# Anti-thrashing: skip if we've had too many low-savings attempts
if self._low_savings_streak >= self.config.max_consecutive_low_savings:
return False
return True
"""Check if messages need compaction."""
return self.count_total_tokens(messages) > self.max_tokens
def compact(
self,
messages: List[Dict[str, Any]],
focus_topic: str = ""
messages: List[Dict[str, Any]]
) -> tuple[List[Dict[str, Any]], CompactionResult]:
"""
Compact messages to fit within token limit (synchronous version).
@@ -142,49 +111,31 @@ class ContextCompactor:
Args:
messages: List of messages to compact
focus_topic: Optional topic to focus on during summarization
Returns:
Tuple of (compacted messages, result)
"""
# Reset state for this compaction call
self._used_previous_summary = False
original_tokens = self.count_total_tokens(messages)
if original_tokens <= self.max_tokens:
result = CompactionResult(
return messages, CompactionResult(
original_tokens=original_tokens,
compacted_tokens=original_tokens,
messages_removed=0,
messages_kept=len(messages),
strategy_used=self.strategy
)
result.calculate_savings_pct()
return messages, result
# Apply tool result deduplication pre-pass if enabled
processed_messages = messages
tool_results_pruned = 0
if self.config.tool_prune_before_summarise and self.tool_pruner:
processed_messages, tool_results_pruned = self.tool_pruner.prune(
messages,
self.config.max_tool_result_size
)
# Skip early exit - proceed with full compaction strategy
# Anti-thrashing check will be applied after strategy runs
if self.strategy == CompactionStrategy.TRUNCATE:
compacted = self._truncate(processed_messages)
compacted = self._truncate(messages)
elif self.strategy == CompactionStrategy.SLIDING:
compacted = self._sliding_window(processed_messages)
compacted = self._sliding_window(messages)
elif self.strategy == CompactionStrategy.SUMMARIZE:
compacted = self._summarize(processed_messages)
compacted = self._summarize(messages)
elif self.strategy == CompactionStrategy.SMART:
compacted = self._smart_compact(processed_messages)
compacted = self._smart_compact(messages)
elif self.strategy == CompactionStrategy.PRUNE:
compacted = self._prune(processed_messages)
compacted = self._prune(messages)
elif self.strategy == CompactionStrategy.LLM_SUMMARIZE:
if self.llm_summarize_fn:
# For sync calls with LLM function, we need to run async
@@ -193,17 +144,17 @@ class ContextCompactor:
try:
loop = asyncio.get_running_loop()
# If in async context, fallback to naive summarization
compacted = self._summarize(processed_messages)
compacted = self._summarize(messages)
except RuntimeError:
# No running loop, safe to create one
compacted = asyncio.run(self._llm_summarize_async(processed_messages, focus_topic))
compacted = asyncio.run(self._llm_summarize_async(messages))
except Exception:
# Fallback to naive summarization if async fails
compacted = self._summarize(processed_messages)
compacted = self._summarize(messages)
else:
compacted = self._llm_summarize(processed_messages, focus_topic)
compacted = self._llm_summarize(messages)
else:
compacted = self._truncate(processed_messages)
compacted = self._truncate(messages)
compacted_tokens = self.count_total_tokens(compacted)
@@ -212,75 +163,49 @@ class ContextCompactor:
compacted_tokens=compacted_tokens,
messages_removed=len(messages) - len(compacted),
messages_kept=len(compacted),
strategy_used=self.strategy,
tool_results_pruned=tool_results_pruned,
previous_summary_reused=getattr(self, '_used_previous_summary', False)
strategy_used=self.strategy
)
result.calculate_savings_pct()
# Update anti-thrashing tracking based on actual results
self._last_savings_pct = result.savings_pct
if result.savings_pct >= self.config.min_savings_pct:
self._low_savings_streak = 0
else:
self._low_savings_streak += 1
return compacted, result
async def compact_async(
self,
messages: List[Dict[str, Any]],
focus_topic: str = ""
messages: List[Dict[str, Any]]
) -> tuple[List[Dict[str, Any]], CompactionResult]:
"""
Compact messages to fit within token limit (asynchronous version).
Args:
messages: List of messages to compact
focus_topic: Optional topic to focus on during summarization
Returns:
Tuple of (compacted messages, result)
"""
# Reset state for this compaction call
self._used_previous_summary = False
original_tokens = self.count_total_tokens(messages)
if original_tokens <= self.max_tokens:
result = CompactionResult(
return messages, CompactionResult(
original_tokens=original_tokens,
compacted_tokens=original_tokens,
messages_removed=0,
messages_kept=len(messages),
strategy_used=self.strategy
)
result.calculate_savings_pct()
return messages, result
# Apply tool result deduplication pre-pass if enabled
processed_messages = messages
tool_results_pruned = 0
if self.config.tool_prune_before_summarise and self.tool_pruner:
processed_messages, tool_results_pruned = self.tool_pruner.prune(
messages,
self.config.max_tool_result_size
)
if self.strategy == CompactionStrategy.TRUNCATE:
compacted = self._truncate(processed_messages)
compacted = self._truncate(messages)
elif self.strategy == CompactionStrategy.SLIDING:
compacted = self._sliding_window(processed_messages)
compacted = self._sliding_window(messages)
elif self.strategy == CompactionStrategy.SUMMARIZE:
compacted = self._summarize(processed_messages)
compacted = self._summarize(messages)
elif self.strategy == CompactionStrategy.SMART:
compacted = self._smart_compact(processed_messages)
compacted = self._smart_compact(messages)
elif self.strategy == CompactionStrategy.PRUNE:
compacted = self._prune(processed_messages)
compacted = self._prune(messages)
elif self.strategy == CompactionStrategy.LLM_SUMMARIZE:
compacted = await self._llm_summarize_async(processed_messages, focus_topic)
compacted = await self._llm_summarize_async(messages)
else:
compacted = self._truncate(processed_messages)
compacted = self._truncate(messages)
compacted_tokens = self.count_total_tokens(compacted)
@@ -289,35 +214,11 @@ class ContextCompactor:
compacted_tokens=compacted_tokens,
messages_removed=len(messages) - len(compacted),
messages_kept=len(compacted),
strategy_used=self.strategy,
tool_results_pruned=tool_results_pruned,
previous_summary_reused=getattr(self, '_used_previous_summary', False)
strategy_used=self.strategy
)
result.calculate_savings_pct()
# Update anti-thrashing tracking based on actual results
self._last_savings_pct = result.savings_pct
if result.savings_pct >= self.config.min_savings_pct:
self._low_savings_streak = 0
else:
self._low_savings_streak += 1
return compacted, result
def _prune_tool_results(self, messages: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], int]:
"""
Delegate tool result pruning to injected protocol implementation.
If no tool pruner is provided, returns messages unchanged.
This maintains backward compatibility while enforcing protocol-driven design.
Returns:
Tuple of (processed messages, number of tool results pruned)
"""
if self.tool_pruner:
return self.tool_pruner.prune(messages, self.config.max_tool_result_size)
return messages, 0
def _truncate(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Truncate oldest messages."""
result = []
@@ -459,117 +360,72 @@ class ContextCompactor:
result.extend(recent)
return result
def _llm_summarize(self, messages: List[Dict[str, Any]], focus_topic: str = "") -> List[Dict[str, Any]]:
def _llm_summarize(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Use LLM to summarize older messages with iterative support.
Use LLM to summarize older messages with anti-injection framing.
Supports iterative summarization - if we have a previous summary,
we only summarize the new messages since that summary.
Also includes anti-injection framing when configured.
Args:
messages: List of messages to compact
focus_topic: Optional topic to focus on during summarization
Note: This implementation uses structured templates and anti-injection prefixes
to prevent the model from treating summarized content as active instructions.
"""
self._used_previous_summary = False
result = []
# Keep system messages
system_msgs = [m for m in messages if m.get("role") == "system"]
# Keep system messages (excluding previous compacted summaries)
system_msgs = [
m for m in messages
if m.get("role") == "system" and not m.get("_compacted")
]
other_msgs = [m for m in messages if m.get("role") != "system"]
result.extend(system_msgs)
# Keep recent messages
recent = other_msgs[-self.preserve_recent:]
older = other_msgs[:-self.preserve_recent] if len(other_msgs) > self.preserve_recent else []
# Determine what to summarize
total_original_messages = len(messages)
if (self.config.enable_iterative_summary and
self._previous_summary and
total_original_messages > self._previous_summary_global_idx):
# Iterative: summarize only new messages since previous summary
# Calculate how many old messages to summarize based on global position
messages_since_summary = total_original_messages - self._previous_summary_global_idx
new_older_messages = max(0, messages_since_summary - self.preserve_recent)
if new_older_messages > 0:
to_summarize = other_msgs[-messages_since_summary:-self.preserve_recent]
if older:
if self.config.structured_template:
structured = self._build_structured_summary(older)
else:
to_summarize = []
self._used_previous_summary = True
else:
# Fresh summary: summarize all older messages
to_summarize = other_msgs[:-self.preserve_recent] if len(other_msgs) > self.preserve_recent else []
self._used_previous_summary = False
if to_summarize or self._previous_summary:
# Build summary content
summary_parts = []
# Include previous summary if doing iterative summarization
if self._used_previous_summary and self._previous_summary:
summary_parts.append(f"[Previous Summary]: {self._previous_summary}")
summary_parts.append("[New Activity]:")
# Add new messages to summarize
for msg in to_summarize:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str) and content:
# Extract key information with focus consideration
if focus_topic and focus_topic.lower() in content.lower():
# Prioritize content related to focus topic
summary_parts.append(f"[{role}] *FOCUS*: {content[:200]}...")
else:
# Fallback to simple summary
summary_parts = []
for msg in older:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str) and content:
summary_parts.append(f"[{role}]: {content[:150]}...")
structured = "\n".join(summary_parts[:10])
if summary_parts:
focus_hint = f"\nFocus on: {focus_topic}" if focus_topic else ""
if structured:
# Apply anti-injection prefix
prefixed = f"{self.config.compaction_prefix}\n\n{structured}"
if self._used_previous_summary:
summary = (
f"[Iterative conversation summary - update with new activity{focus_hint}]\n"
+ "\n".join(summary_parts[:15])
)
else:
summary = (
f"[Compacted conversation history - summarize key points{focus_hint}]\n"
+ "\n".join(summary_parts[:10])
)
# Handle iterative update if enabled
if self.config.iterative_update and self._previous_summary:
structured = self._merge_summaries(self._previous_summary, structured)
prefixed = f"{self.config.compaction_prefix}\n\n{structured}"
# Apply anti-injection prefix if configured
if hasattr(self.config, 'compaction_prefix') and self.config.compaction_prefix:
summary = f"{self.config.compaction_prefix}\n\n{summary}"
# Store for next iteration
if self.config.iterative_update:
self._previous_summary = structured
summary_msg = {
result.append({
"role": "system",
"content": summary,
"content": prefixed,
"_compacted": True,
"_original_count": len(to_summarize),
"_iterative": self._used_previous_summary,
"_focus_topic": focus_topic
}
result.append(summary_msg)
# Update tracking for future iterative summarization
self._previous_summary = summary
self._previous_summary_global_idx = len(messages)
"_original_count": len(older),
"_anti_injection": True,
})
result.extend(recent)
return result
async def _llm_summarize_async(self, messages: List[Dict[str, Any]], focus_topic: str = "") -> List[Dict[str, Any]]:
async def _llm_summarize_async(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Use LLM to intelligently summarize older messages.
This method invokes the agent's LLM to create a meaningful summary
that preserves key facts, identifiers, and the user's intent.
Args:
messages: List of messages to compact
focus_topic: Optional topic to focus on during summarization
"""
self._used_previous_summary = False
result = []
# Keep system messages
@@ -580,71 +436,33 @@ class ContextCompactor:
# Keep recent messages
recent = other_msgs[-self.preserve_recent:]
# Determine what to summarize based on iterative settings
total_original_messages = len(messages)
if (self.config.enable_iterative_summary and
self._previous_summary and
total_original_messages > self._previous_summary_global_idx):
# Iterative: summarize only new messages since previous summary
messages_since_summary = total_original_messages - self._previous_summary_global_idx
new_older_messages = max(0, messages_since_summary - self.preserve_recent)
if new_older_messages > 0:
older = other_msgs[-messages_since_summary:-self.preserve_recent]
else:
older = []
self._used_previous_summary = True
else:
# Fresh summary: summarize all older messages
older = other_msgs[:-self.preserve_recent] if len(other_msgs) > self.preserve_recent else []
self._used_previous_summary = False
older = other_msgs[:-self.preserve_recent] if len(other_msgs) > self.preserve_recent else []
if older and self.llm_summarize_fn:
try:
# Format messages for summarization using protocol
if self.message_formatter:
history_text = self.message_formatter.format_for_summary(older)
else:
# Fallback to basic formatting if no protocol implementation
history_text = str(older)
# Format messages for summarization
history_text = self._format_messages_for_summary(older)
# Create summarization prompt that preserves important information
focus_hint = f" Focus especially on: {focus_topic}." if focus_topic else ""
iterative_hint = ""
if self._used_previous_summary and self._previous_summary:
iterative_hint = f"\n\n[Previous Summary]: {self._previous_summary}\n[New Activity to Add]:"
prompt = (
"Summarise the following conversation history. Preserve verbatim: "
"all file paths, IDs, hashes, URLs, task references, error messages, "
"tool outputs, and the user's requests. Be concise but complete. "
f"Focus on facts and actions taken, not general conversation.{focus_hint}"
f"{iterative_hint}\n\n{history_text}"
"Focus on facts and actions taken, not general conversation.\n\n"
f"{history_text}"
)
# Call the LLM for summarization
summary = await self.llm_summarize_fn(prompt)
# Add the LLM-generated summary as a system message
summary_content = summary
if self._used_previous_summary:
summary_content = f"[Iterative conversation summary]\n{summary}"
else:
summary_content = f"[Previous conversation summary]\n{summary}"
result.append({
"role": "system",
"content": summary_content,
"content": f"[Previous conversation summary]\n{summary}",
"_compacted": True,
"_original_count": len(older),
"_llm_generated": True,
"_iterative": self._used_previous_summary,
"_focus_topic": focus_topic
})
# Update tracking for future iterative summarization
self._previous_summary = summary
self._previous_summary_global_idx = len(messages)
except Exception as e:
# Fallback to naive summarization if LLM call fails
import logging
@@ -688,41 +506,133 @@ class ContextCompactor:
"content": summary,
"_compacted": True,
"_original_count": len(older),
"_fallback": True,
})
result.extend(recent)
return result
def _format_messages_for_summary(self, messages: List[Dict[str, Any]]) -> str:
"""
Delegate message formatting to protocol implementation.
"""Format messages for LLM summarization."""
formatted = []
for i, msg in enumerate(messages):
role = msg.get("role", "unknown")
content = msg.get("content", "")
# Handle tool calls
if msg.get("tool_calls"):
tool_calls = msg.get("tool_calls", [])
tool_names = [tc.get("function", {}).get("name", "unknown") for tc in tool_calls]
formatted.append(f"{i+1}. {role}: Called tools: {', '.join(tool_names)}")
# Handle content
if isinstance(content, str) and content.strip():
# Truncate very long content but preserve structure
if len(content) > 1000:
content = content[:800] + "...[truncated]..." + content[-200:]
formatted.append(f"{i+1}. {role}: {content}")
elif isinstance(content, list):
# Handle multi-part content
parts = []
for part in content[:3]: # Limit to first 3 parts
if isinstance(part, dict):
text = part.get("text", "")
if text:
parts.append(text[:200] + "..." if len(text) > 200 else text)
if parts:
formatted.append(f"{i+1}. {role}: {' | '.join(parts)}")
# Handle tool results with IDs
if msg.get("tool_call_id"):
tool_id = msg.get("tool_call_id", "")
formatted.append(f"{i+1}. {role} (tool {tool_id}): {str(content)[:500]}...")
If no formatter is provided, returns basic string representation.
"""
if self.message_formatter:
return self.message_formatter.format_for_summary(messages)
return str(messages)
return "\n".join(formatted)
def _build_structured_summary(self, messages: List[Dict[str, Any]]) -> str:
"""
Delegate structured summary building to protocol implementation.
Build a structured summary using the configured template.
If no summary builder is provided, returns basic summary.
Args:
messages: Messages to summarize
Returns:
Structured summary string
"""
if self.summary_builder:
return self.summary_builder.build_structured_summary(messages)
return f"Summary of {len(messages)} messages"
# Extract information from messages
active_task = "No specific task identified"
completed = []
in_progress = []
pending = []
files = set()
remaining = []
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "")
if not isinstance(content, str):
continue
content_lower = content.lower()
# Extract file paths
file_matches = re.findall(r'[\w/\.\-]+\.[a-zA-Z]{1,4}', content)
files.update(file_matches[:5]) # Limit to 5 files
# Categorize content based on keywords and role
content_snippet = content[:200] + ("..." if len(content) > 200 else "")
if role == "user":
if any(word in content_lower for word in ["please", "can you", "help"]) or re.search(r'\bdo\b', content_lower):
active_task = content_snippet
elif "?" in content:
pending.append(content_snippet)
elif role == "assistant":
if any(word in content_lower for word in ["completed", "done", "finished"]):
completed.append(content_snippet)
elif any(word in content_lower for word in ["working", "processing", "analyzing"]):
in_progress.append(content_snippet)
elif any(word in content_lower for word in ["will", "plan to", "next"]):
remaining.append(content_snippet)
elif role == "tool":
# Tool results are generally completed actions
tool_name = msg.get("name", "unknown")
completed.append(f"Tool {tool_name}: {content_snippet}")
# Format using template
return SUMMARY_TEMPLATE.format(
active_task=active_task,
completed="\n".join(f"- {item}" for item in completed[:3]) or "None identified",
in_progress="\n".join(f"- {item}" for item in in_progress[:3]) or "None identified",
pending="\n".join(f"- {item}" for item in pending[:3]) or "None identified",
files=", ".join(list(files)[:5]) or "None mentioned",
remaining="\n".join(f"- {item}" for item in remaining[:3]) or "None identified"
)
def _merge_summaries(self, previous: str, current: str) -> str:
"""
Delegate summary merging to protocol implementation.
Merge previous summary with current one for iterative updates.
If no summary builder is provided, returns current summary only.
Args:
previous: Previous summary content
current: Current summary content
Returns:
Merged summary
"""
if self.summary_builder:
return self.summary_builder.merge_summaries(previous, current)
return current
# Simple merge strategy: prioritize current but preserve unique info from previous
if not previous:
return current
# Preserve context but avoid excessive growth
MAX_PREVIOUS_LENGTH = 500
if len(previous) > MAX_PREVIOUS_LENGTH:
truncated_previous = previous[:MAX_PREVIOUS_LENGTH]
return f"{current}\n\n[Previous context]: {truncated_previous}..."
else:
return f"{current}\n\n[Previous context]: {previous}"
def get_stats(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Get statistics about messages."""
@@ -50,28 +50,3 @@ class CompactionConfig:
compaction_prefix: str = COMPACTION_PREFIX # Override for custom framing
structured_template: bool = True # Emit Active Task / Remaining Work sections
iterative_update: bool = True # Merge previous summary on re-compaction
# Anti-thrashing protection
min_savings_pct: float = 10.0 # Skip if projected saving < 10% (0-100 scale)
max_consecutive_low_savings: int = 2 # Abort after N low-savings attempts
# Tool result optimization
tool_prune_before_summarise: bool = True # Deduplicate tool results before summarization
max_tool_result_size: int = 500 # Maximum size for tool results before pruning
# Iterative summarization
enable_iterative_summary: bool = True # Build on previous summaries instead of starting from scratch
def __post_init__(self):
"""Validate and normalize configuration values."""
# Normalize min_savings_pct to 0-100 scale if provided as ratio
if self.min_savings_pct < 1.0:
self.min_savings_pct *= 100.0
# Validate ranges
if not 0.0 <= self.min_savings_pct <= 100.0:
raise ValueError("min_savings_pct must be between 0 and 100")
if self.max_consecutive_low_savings < 0:
raise ValueError("max_consecutive_low_savings must be >= 0")
if self.max_tool_result_size <= 0:
raise ValueError("max_tool_result_size must be > 0")
@@ -1,73 +0,0 @@
"""
Compaction Protocols for PraisonAI Agents.
Protocol-driven interfaces for context compaction functionality.
"""
from typing import Protocol, List, Dict, Any, Tuple, Optional
class ToolResultPrunerProtocol(Protocol):
"""Protocol for pruning and deduplicating tool results."""
def prune(
self,
messages: List[Dict[str, Any]],
max_tool_result_size: int = 500
) -> Tuple[List[Dict[str, Any]], int]:
"""
Prune and deduplicate tool results to reduce token usage.
Args:
messages: List of messages to process
max_tool_result_size: Maximum size for tool results before truncation
Returns:
Tuple of (processed messages, number of tool results pruned)
"""
...
class MessageFormatterProtocol(Protocol):
"""Protocol for formatting messages for summarization."""
def format_for_summary(self, messages: List[Dict[str, Any]]) -> str:
"""
Format messages for LLM summarization.
Args:
messages: Messages to format
Returns:
Formatted string suitable for summarization
"""
...
class SummaryBuilderProtocol(Protocol):
"""Protocol for building structured summaries."""
def build_structured_summary(self, messages: List[Dict[str, Any]]) -> str:
"""
Build a structured summary from messages.
Args:
messages: Messages to summarize
Returns:
Structured summary string
"""
...
def merge_summaries(self, previous: str, current: str) -> str:
"""
Merge previous summary with current one for iterative updates.
Args:
previous: Previous summary content
current: Current summary content
Returns:
Merged summary
"""
...
@@ -18,16 +18,6 @@ class CompactionResult:
strategy_used: CompactionStrategy
summary: str = ""
# Anti-thrashing and optimization info
savings_pct: float = field(default=0.0, init=False) # Percentage of tokens saved (computed)
tool_results_pruned: int = 0 # Number of tool results deduplicated/pruned
previous_summary_reused: bool = False # Whether iterative summary was used
was_skipped_due_to_low_savings: bool = False # Whether compaction was skipped due to anti-thrashing
def __post_init__(self):
"""Initialize computed fields after construction."""
self.calculate_savings_pct()
@property
def tokens_saved(self) -> int:
"""Get number of tokens saved."""
@@ -40,13 +30,6 @@ class CompactionResult:
return 0.0
return self.compacted_tokens / self.original_tokens
def calculate_savings_pct(self) -> None:
"""Calculate and set the savings percentage."""
if self.original_tokens > 0:
self.savings_pct = (self.tokens_saved / self.original_tokens) * 100.0
else:
self.savings_pct = 0.0
@property
def was_compacted(self) -> bool:
"""Check if compaction occurred."""
@@ -62,9 +45,5 @@ class CompactionResult:
"messages_kept": self.messages_kept,
"strategy_used": self.strategy_used.value,
"compression_ratio": self.compression_ratio,
"savings_pct": self.savings_pct,
"tool_results_pruned": self.tool_results_pruned,
"previous_summary_reused": self.previous_summary_reused,
"was_skipped_due_to_low_savings": self.was_skipped_due_to_low_savings,
"summary": self.summary
}
@@ -1089,8 +1089,8 @@ class MultiAgentOutputConfig:
# Verbosity level (0=silent, 1=minimal, 2+=verbose)
verbose: int = 0
# Streaming output - False by default for sync multi-agent compatibility
stream: bool = False
# Streaming output
stream: bool = True
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
@@ -322,8 +322,8 @@ CACHING_PRESETS: Dict[str, Dict[str, Any]] = {
# =============================================================================
MULTI_AGENT_OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"verbose": {"verbose": 2, "stream": False},
"minimal": {"verbose": 1, "stream": False},
"verbose": {"verbose": 2, "stream": True},
"minimal": {"verbose": 1, "stream": True},
"silent": {"verbose": 0, "stream": False},
}
+43 -84
View File
@@ -24,7 +24,6 @@ import xml.etree.ElementTree as ET
from ..errors import AgentErrorKind, FailoverDecision, IdleTimeoutBreaker
# Gap 2: Tool call execution imports
from ..tools.call_executor import ToolCall, create_tool_call_executor
from ..tools.schema import annotation_to_json_schema, get_parameter_requirements
# Display functions - lazy loaded to avoid importing rich at startup
# These are only needed when output=verbose
_display_module = None
@@ -5887,74 +5886,34 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
function_name = function_or_name
logging.debug(f"Attempting to generate tool definition for: {function_name}")
# First try to get from the tool registry (preferred method)
try:
from ..tools.registry import get_registry
registry = get_registry()
tool = registry.get(function_name)
if tool:
logging.debug(f"Found tool in registry: {function_name}")
# If it's a BaseTool, get its schema
if hasattr(tool, 'get_schema'):
tool_def = tool.get_schema()
# Apply same normalization as the callable path
if (
isinstance(tool_def, dict)
and isinstance(tool_def.get("function"), dict)
and isinstance(tool_def["function"].get("parameters"), dict)
):
tool_def = tool_def.copy()
tool_def["function"] = tool_def["function"].copy()
tool_def["function"]["parameters"] = self._fix_array_schemas(
tool_def["function"]["parameters"]
)
return tool_def
# If it's a callable, use it as func
if callable(tool):
func = tool
else:
logging.debug(f"Tool {function_name} in registry is not callable")
return None
else:
logging.debug(f"Tool {function_name} not found in registry, falling back to globals/__main__")
# Fall back to globals and __main__ for backward compatibility
tool_def_name = f"{function_name}_definition"
tool_def = globals().get(tool_def_name)
if not tool_def:
import __main__
tool_def = getattr(__main__, tool_def_name, None)
if tool_def:
return tool_def
func = globals().get(function_name)
if not func:
import __main__
func = getattr(__main__, function_name, None)
if not func or not callable(func):
logging.debug(f"Function {function_name} not found or not callable")
return None
except ImportError:
logging.debug("Tool registry not available, falling back to globals/__main__")
# Fall back to globals and __main__ when registry unavailable
tool_def_name = f"{function_name}_definition"
tool_def = globals().get(tool_def_name)
if not tool_def:
import __main__
tool_def = getattr(__main__, tool_def_name, None)
if tool_def:
return tool_def
func = globals().get(function_name)
if not func:
import __main__
func = getattr(__main__, function_name, None)
if not func or not callable(func):
logging.debug(f"Function {function_name} not found or not callable")
return None
# First try to get the tool definition if it exists
tool_def_name = f"{function_name}_definition"
tool_def = globals().get(tool_def_name)
logging.debug(f"Looking for {tool_def_name} in globals: {tool_def is not None}")
if not tool_def:
import __main__
tool_def = getattr(__main__, tool_def_name, None)
logging.debug(f"Looking for {tool_def_name} in __main__: {tool_def is not None}")
if tool_def:
logging.debug(f"Found tool definition: {tool_def}")
return tool_def
# Try to find the function
func = globals().get(function_name)
logging.debug(f"Looking for {function_name} in globals: {func is not None}")
if not func:
import __main__
func = getattr(__main__, function_name, None)
logging.debug(f"Looking for {function_name} in __main__: {func is not None}")
if not func or not callable(func):
logging.debug(f"Function {function_name} not found or not callable")
return None
import inspect
from typing import get_type_hints
# Handle Langchain and CrewAI tools
if inspect.isclass(func) and hasattr(func, 'run') and not hasattr(func, '_run'):
original_func = func
@@ -6002,26 +5961,26 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
logging.debug(f"Parameter descriptions: {param_descriptions}")
# Get type hints for proper schema generation
try:
hints = get_type_hints(func) if getattr(func, "__annotations__", None) else {}
except (NameError, TypeError, AttributeError):
hints = getattr(func, "__annotations__", {}) or {}
for name, param in parameters_list:
# Get type annotation
param_type = hints.get(name, param.annotation if param.annotation != inspect.Parameter.empty else str)
param_type = "string" # Default type
if param.annotation != inspect.Parameter.empty:
if param.annotation == int:
param_type = "integer"
elif param.annotation == float:
param_type = "number"
elif param.annotation == bool:
param_type = "boolean"
elif param.annotation == list:
param_type = "array"
elif param.annotation == dict:
param_type = "object"
# Use new schema utility for proper type handling
prop_schema = annotation_to_json_schema(param_type)
parameters["properties"][name] = {
"type": param_type,
"description": param_descriptions.get(name, "Parameter description not available")
}
# Add description from docstring
prop_schema["description"] = param_descriptions.get(name, "Parameter description not available")
parameters["properties"][name] = prop_schema
# Check if required using improved logic
if get_parameter_requirements(sig, name):
if param.default == inspect.Parameter.empty:
parameters["required"].append(name)
logging.debug(f"Generated parameters: {parameters}")
@@ -29,11 +29,6 @@ logger = get_logger(__name__, extra_data={"subsystem": "memory"})
TRACE_LEVEL = 5
logging.addLevelName(TRACE_LEVEL, 'TRACE')
# Cache boundary constants for prompt prefix caching optimization
# NOTE: This is a placeholder - current implementation does not provide
# functional prompt caching as it requires structured API metadata
CACHE_BOUNDARY = ""
class Memory(StorageMixin, SearchMixin, MemoryCoreMixin):
@@ -1709,41 +1704,6 @@ class Memory(StorageMixin, SearchMixin, MemoryCoreMixin):
entities = self.search_entity(q, limit=max_items)
user_mem = self.search_user_memory(user_id, q, limit=max_items) if user_id else []
# Apply stable sorting to ensure deterministic order for prompt caching
# Sort by timestamp descending, then by content hash for stable ordering
def _sort_memory_results(results: List[Any]) -> List[Any]:
"""Stable sort so identical query sets produce identical context strings."""
import hashlib
if not results:
return results
def sort_key(r):
if not isinstance(r, dict):
return (0.0, hashlib.sha256(str(r).encode()).hexdigest())
# Prefer created_at over timestamp for better determinism
timestamp = r.get("created_at") or r.get("timestamp") or 0
if isinstance(timestamp, str):
try:
from datetime import datetime
timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
except ValueError:
timestamp = 0.0
timestamp = -(float(timestamp)) # Negative for descending order
# Use full content hash for true deterministic ordering
content = r.get("text", "") if isinstance(r, dict) else str(r)
content_hash = hashlib.sha256(content.encode()).hexdigest()
return (timestamp, content_hash)
return sorted(results, key=sort_key)
# Apply stable sorting to all memory results
short_term = _sort_memory_results(short_term)
long_term = _sort_memory_results(long_term)
entities = _sort_memory_results(entities)
user_mem = _sort_memory_results(user_mem)
# Add sections in order of priority
add_section("Short-term Memory Context", short_term)
add_section("Long-term Memory Context", long_term)
@@ -1753,51 +1713,6 @@ class Memory(StorageMixin, SearchMixin, MemoryCoreMixin):
return "\n".join(lines) if lines else ""
def build_cache_optimized_context(
self,
task_descr: str,
user_id: Optional[str] = None,
additional: str = "",
max_items: int = 3,
include_cache_boundary: bool = True,
include_in_output: Optional[bool] = None
) -> Dict[str, str]:
"""
Build context with cache boundary markers for prompt prefix caching optimization.
Returns a dictionary with 'stable_prefix' and 'cache_boundary' keys.
The stable prefix contains deterministically ordered content that should
remain constant between turns for effective prompt caching.
Args:
task_descr: Task description for memory search
user_id: Optional user ID for personalized memory
additional: Additional context to include in search
max_items: Maximum items per memory category
include_cache_boundary: Whether to include cache boundary marker
include_in_output: Whether to include memory content in output
Returns:
Dict with 'stable_prefix' and 'cache_boundary' keys
"""
# Preserve original behavior: include_in_output=None means debug-level-only
# Don't change the default behavior for backward compatibility
# Build the stable context using existing method
stable_context = self.build_context_for_task(
task_descr=task_descr,
user_id=user_id,
additional=additional,
max_items=max_items,
include_in_output=include_in_output
)
result = {"stable_prefix": stable_context}
result["cache_boundary"] = CACHE_BOUNDARY if include_cache_boundary else ""
return result
# -------------------------------------------------------------------------
# Master Reset (Everything)
# -------------------------------------------------------------------------
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more