Compare commits

...
26 Commits
Author SHA1 Message Date
Mervin Praison 13a9a3e07d Release v3.8.8 2026-01-13 13:06:26 +00:00
Mervin Praison b95f6b0665 Release v3.8.7 2026-01-13 12:55:43 +00:00
github-actions[bot] e8c4410014 docs: auto-update api.md [skip ci] 2026-01-13 09:30:27 +00:00
Mervin Praison 6c2d852ed1 Release v3.8.6 2026-01-13 09:30:11 +00:00
Mervin Praison 6f95ae8d9d Release v3.8.5 2026-01-13 07:04:53 +00:00
Mervin Praison 8e2dd09b3b Release v3.8.4 2026-01-13 06:17:09 +00:00
github-actions[bot] 8ecfad5c5a docs: auto-update api.md [skip ci] 2026-01-13 06:01:35 +00:00
Mervin Praison 35a88a0913 Release v3.8.3 2026-01-13 06:01:20 +00:00
Mervin Praison 6df3e32640 Release v3.8.2 2026-01-13 04:16:08 +00:00
github-actions[bot] fb36e1c50c docs: auto-update api.md [skip ci] 2026-01-12 17:00:39 +00:00
Mervin Praison ac5454cb00 Release v3.8.1 2026-01-12 17:00:20 +00:00
github-actions[bot] bbe95795d1 docs: auto-update api.md [skip ci] 2026-01-12 12:49:30 +00:00
Mervin Praison bdaf5340f9 Release v3.8.0 2026-01-12 12:49:16 +00:00
Mervin Praison 5579614001 Release v3.7.9 2026-01-12 09:10:46 +00:00
Mervin Praison 2c4dd0e2d4 Release v3.7.8 2026-01-12 07:39:39 +00:00
Mervin Praison fd23d62841 Release v3.7.7 2026-01-11 21:30:04 +00:00
Mervin Praison 5f3bcb78ac Release v3.7.6 2026-01-11 08:23:08 +00:00
Mervin Praison 3c40c2278e Release v3.7.5 2026-01-11 07:34:59 +00:00
Mervin Praison 1921b03aab chore: remove empty trace and comparison test files 2026-01-10 21:09:11 +00:00
Mervin Praison f33bf57662 Release v3.7.4 2026-01-10 21:06:16 +00:00
github-actions[bot] df21c419ea docs: auto-update api.md [skip ci] 2026-01-10 19:45:30 +00:00
Mervin Praison fe997d525c ci: add workflow to auto-update api.md on main branch push 2026-01-10 19:45:04 +00:00
Mervin Praison ca26988774 Release v3.7.3 2026-01-10 19:43:43 +00:00
Mervin Praison 3600594316 Before AI example generation 2026-01-10 16:54:16 +00:00
Mervin Praison 097cd42890 Before testing 2026-01-10 16:04:21 +00:00
Mervin Praison 595add2991 Before testing AI generation 2026-01-10 15:55:26 +00:00
283 changed files with 25607 additions and 1443 deletions

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
name: Update API Reference
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai/praisonai/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- '.github/workflows/update-api-md.yml'
permissions:
contents: write
jobs:
update-api-md:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
persist-credentials: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Generate api.md
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_api_md.py
- name: Check for changes
id: check_changes
run: |
if git diff --quiet api.md; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to api.md"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "api.md has been updated"
git diff api.md | head -50
fi
- name: Commit and push changes
if: steps.check_changes.outputs.changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update api.md [skip ci]"
file_pattern: 'api.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
+2 -1
View File
@@ -83,4 +83,5 @@ issue_details.json
# MCP Registry tokens
.mcpregistry_*
*.log
*.log
.internal
+21 -445
View File
@@ -3,27 +3,11 @@
This file is auto-generated. Do not edit manually.
Regenerate with: `praisonai docs api-md --write`
# Shared Types
Types:
```python
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, TaskOutput, ToolResult, ToolValidationError, WorkflowContext
```
Methods:
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
# Agents
Types:
```python
from praisonaiagents import Agent, Agents, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PraisonAIAgents, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
from praisonaiagents import Agent, Agents
```
Methods:
@@ -60,6 +44,7 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">iter_stream</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">launch</a>(path: str = '/', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>()</code>
@@ -76,7 +61,7 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str = None, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
@@ -116,455 +101,31 @@ Methods:
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">chat</a>(message: str, **kwargs) -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">name</a>() -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">rag</a>() -> Optional['RAG']</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_codebase_with_gitingest</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_integration_points</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_test_patterns</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">build_implementation_blueprint</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">compile_context_documentation</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_quality_gates</a>(requirements: List[str]) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_validation_framework</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">execute_prp</a>(prp_file_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">extract_implementation_patterns</a>(project_path: str, ast_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_comprehensive_prp</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_feature_prp</a>(feature_request: str) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">get_agent_interaction_summary</a>() -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">log_debug</a>(message: str, **kwargs)</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">perform_ast_analysis</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_comprehensive_session_report</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_markdown_output</a>(content: str, filename: str, section_title: str = 'Output')</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_logging</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_output_directories</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">start</a>(input_text: str) -> str</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">aresearch</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">async_openai_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">clarify</a>(query: str, model: Optional[str] = None) -> str</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">follow_up</a>(query: str, previous_interaction_id: str, model: Optional[str] = None) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">gemini_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">openai_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">research</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None, stream: bool = True) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">rewrite_query</a>(query: str, model: Optional[str] = None) -> str</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">achat</a>(prompt: str, temperature: float = 0.2, tools: Optional[List[Any]] = None, output_json: Optional[str] = None, output_pydantic: Optional[Any] = None, reasoning_steps: bool = False, **kwargs) -> Union[str, Dict[str, Any]]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">agenerate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">chat</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">generate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">litellm</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">agent</a>()</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand</a>(prompt: str, strategy: ExpandStrategy = ..., context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_basic</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_creative</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_detailed</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_structured</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviation</a>(abbrev: str, expansion: str) -> None</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviations</a>(abbreviations: Dict[str, str]) -> None</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">agent</a>()</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite</a>(query: str, strategy: RewriteStrategy = ..., chat_history: Optional[List[Dict[str, str]]] = None, context: Optional[str] = None, num_queries: int = None) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_basic</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_contextual</a>(query: str, chat_history: List[Dict[str, str]]) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_hyde</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_multi_query</a>(query: str, num_queries: int = None) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_step_back</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_sub_queries</a>(query: str) -> RewriteResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_context_agent</a>(llm: Optional[Union[str, Any]] = None, **kwargs) -> ContextAgent</code>
# Tools
Types:
```python
from praisonaiagents import BaseTool, FunctionTool, ToolRegistry, Tools, get_registry, get_tool, register_tool, tool, validate_tool
from praisonaiagents import Tools, tool
```
Methods:
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">__call__</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
# Workflows
Types:
```python
from praisonaiagents import Loop, Parallel, Pipeline, Repeat, Route, Workflow, WorkflowStep, loop, parallel, repeat, route
```
Methods:
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">arun</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">astart</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Workflow'</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">memory_config</a>() -> Optional[Dict[str, Any]]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_complete</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_error</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_start</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_complete</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_start</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">planning_llm</a>() -> Optional[str]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">reasoning</a>() -> bool</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">run</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False, stream: bool = None) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">start</a>(input: str = '', **kwargs) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">stream</a>() -> bool</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>() -> bool</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">async_execution</a>() -> bool</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">branch_condition</a>() -> Optional[Dict[str, List[str]]]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">context_from</a>() -> Optional[List[str]]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">evaluator</a>()</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">executor</a>()</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">max_retries</a>() -> int</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">next_steps</a>() -> Optional[List[str]]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_error</a>() -> str</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_file</a>() -> Optional[str]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_json</a>() -> Optional[Any]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_pydantic</a>() -> Optional[Any]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_variable</a>() -> Optional[str]</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">quality_check</a>() -> bool</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">rerun</a>() -> bool</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">retain_full_context</a>() -> bool</code>
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">loop</a>(step: Any, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item') -> Loop</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">parallel</a>(steps: List) -> Parallel</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
# DB
Types:
```python
from praisonaiagents import db
```
# Memory
Types:
```python
from praisonaiagents import Memory
```
Methods:
* <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">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_short_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_quality</a>(text: str, quality_score: float, task_id: Optional[str] = None, iteration: Optional[int] = None, metrics: Optional[Dict[str, float]] = None, memory_type: Literal['short', 'long'] = 'long') -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_user_memory</a>(user_id: str, text: str, extra: Dict[str, Any] = None)</code>
# Knowledge
Types:
```python
from praisonaiagents import Chunking, Knowledge
```
Methods:
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">SUPPORTED_CHUNKERS</a>() -> Dict[str, Any]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">__call__</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunk</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunker</a>()</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">embedding_model</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">add</a>(file_path, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">chunker</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">config</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete_all</a>(user_id = None, agent_id = None, run_id = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_all</a>(user_id = None, agent_id = None, run_id = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_corpus_stats</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">history</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">index</a>(path: str, incremental: bool = True, force: bool = False, include_glob: list = None, exclude_glob: list = None, user_id: str = None, agent_id: str = None, run_id: str = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">markdown</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">memory</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">normalize_content</a>(content)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">search</a>(query, user_id = None, agent_id = None, run_id = None, rerank = None, **kwargs)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">update</a>(memory_id, data)</code>
# RAG
Types:
```python
from praisonaiagents import RAG, RAGCitation, RAGConfig, RAGResult
```
Methods:
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aquery</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aretrieve</a>(query: str, **kwargs) -> ContextPack</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">astream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> AsyncIterator[str]</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">get_citations</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> List[Citation]</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">llm</a>()</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">query</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">retrieve</a>(query: str, **kwargs) -> ContextPack</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">stream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> Iterator[str]</code>
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'Citation'</code>
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGConfig'</code>
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">format_answer_with_citations</a>() -> str</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGResult'</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">has_citations</a>() -> bool</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
# Handoff
Types:
```python
from praisonaiagents import Handoff, RECOMMENDED_PROMPT_PREFIX, handoff, handoff_filters, prompt_with_handoff_instructions
```
Methods:
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_description</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_name</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_async</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_programmatic</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_system_messages</a>(data: HandoffInputData) -> HandoffInputData</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">prompt_with_handoff_instructions</a>(base_prompt: str, agent: 'Agent') -> str</code>
# Guardrails
Types:
```python
from praisonaiagents import LLMGuardrail
```
Methods:
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]</code>
# Skills
Types:
```python
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties
```
Methods:
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load</a>(skill_path: str, activate: bool = False) -> Optional[LoadedSkill]</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_all_resources</a>(skill: LoadedSkill) -> None</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_assets</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_metadata</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_references</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills</a>() -> List[SkillMetadata]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">load_resources</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skills</a>() -> List[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">to_prompt</a>() -> str</code>
* <code title="class SkillMetadata">SkillMetadata.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">from_properties</a>(props: SkillProperties) -> 'SkillMetadata'</code>
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">to_dict</a>() -> dict</code>
# Session
Types:
```python
from praisonaiagents import Session
```
Methods:
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">Agent</a>(name: str, role: str = 'Assistant', instructions: Optional[str] = None, tools: Optional[List[Any]] = None, memory: bool = True, knowledge: Optional[List[str]] = None, **kwargs) -> 'Agent'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_knowledge</a>(source: str) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">save_state</a>(state_data: Dict[str, Any]) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_knowledge</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
# Telemetry
Types:
```python
from praisonaiagents import get_telemetry
```
Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">get_telemetry</a>() -> 'MinimalTelemetry'</code>
# Observability
Types:
```python
from praisonaiagents import FlowDisplay, obs, track_workflow
```
Methods:
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">display</a>()</code>
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">start</a>()</code>
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">stop</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">track_workflow</a>()</code>
# Context
Types:
```python
from praisonaiagents import ContextManager, ManagerConfig
```
Methods:
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">capture_llm_boundary</a>(messages: List[Dict[str, Any]], tools: List[Dict[str, Any]]) -> SnapshotHookData</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">estimate_tokens</a>(text: str, validate: bool = False) -> Tuple[int, Optional[EstimationMetrics]]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_history</a>() -> List[Dict[str, Any]]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_last_snapshot_hook</a>() -> Optional[SnapshotHookData]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_resolved_config</a>() -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_stats</a>() -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_tool_budget</a>(tool_name: str) -> int</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">process</a>(messages: List[Dict[str, Any]], system_prompt: str = '', tools: Optional[List[Dict[str, Any]]] = None, trigger: Literal['turn', 'tool_call', 'manual', 'overflow'] = 'turn') -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">from_env</a>() -> 'ManagerConfig'</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">merge</a>(**overrides) -> 'ManagerConfig'</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">to_dict</a>() -> Dict[str, Any]</code>
# Display
Types:
```python
from praisonaiagents import async_display_callbacks, clean_triple_backticks, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, error_logs, register_display_callback, sync_display_callbacks
```
Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">clean_triple_backticks</a>(text: str) -> str</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_error</a>(message: str, console = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_generating</a>(content: str = '', start_time: Optional[float] = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_instruction</a>(message: str, console = None, agent_name: str = None, agent_role: str = None, agent_tools: List[str] = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_interaction</a>(message, response, markdown = True, generation_time = None, console = None, agent_name = None, agent_role = None, agent_tools = None, task_name = None, task_description = None, task_id = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_self_reflection</a>(message: str, console = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_tool_call</a>(message: str, console = None, tool_name: str = None, tool_input: dict = None, tool_output: str = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">register_display_callback</a>(display_type: str, callback_fn, is_async: bool = False)</code>
# Other
Types:
```python
from praisonaiagents import AutoRagConfig, Citation, CodeExecutionStep, DeepResearchResponse, ExpandResult, ExpandStrategy, FileSearchCall, MCPCall, Provider, RagRetrievalPolicy, ReasoningStep, RewriteResult, RewriteStrategy, Task, WebSearchCall
from praisonaiagents import Task
```
Methods:
* <code title="class AutoRagConfig">AutoRagConfig.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">from_dict</a>(data: Dict[str, Any]) -> 'AutoRagConfig'</code>
* <code title="class AutoRagConfig">AutoRagConfig.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class Citation">Citation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'Citation'</code>
* <code title="class Citation">Citation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class DeepResearchResponse">DeepResearchResponse.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">get_all_sources</a>() -> List[Dict[str, str]]</code>
* <code title="class DeepResearchResponse">DeepResearchResponse.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">get_citation_text</a>(citation: Citation) -> str</code>
* <code title="class RewriteResult">RewriteResult.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">all_queries</a>() -> List[str]</code>
* <code title="class RewriteResult">RewriteResult.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">primary_query</a>() -> str</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback</a>(task_output: TaskOutput) -> None</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
@@ -614,8 +175,11 @@ Methods:
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context export <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context grep <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context list <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug interactive <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
@@ -670,14 +234,19 @@ Methods:
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp tools <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai memory add <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory clear <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
@@ -728,12 +297,19 @@ Methods:
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai templates create <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
* <code title="cli">praisonai templates list <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
* <code title="cli">praisonai test info <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai test interactive <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai test run <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai todo add <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
* <code title="cli">praisonai todo done <a href="./src/praisonai/praisonai/cli/commands/todo.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>=3.7.2" \
"praisonai>=3.8.8" \
"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>=3.7.2" \
"praisonai>=3.8.8" \
"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>=3.7.2" \
"praisonai>=3.8.8" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -22,7 +22,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.3
tools:
- "InternetSearchTool"
@@ -76,7 +76,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.3
tools:
- "InternetSearchTool"
@@ -130,7 +130,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.3
tools:
- "InternetSearchTool"
@@ -185,7 +185,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.4
tools:
- "InternetSearchTool"
@@ -239,7 +239,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.3
tools:
- "InternetSearchTool"
@@ -299,7 +299,7 @@ agents: # Canonical: use 'agents' instead of 'roles'
verbose: true
allow_delegation: false
llm:
model: "gpt-5-nano"
model: "gpt-4o-mini"
temperature: 0.3
tasks:
report_synthesis:
@@ -46,7 +46,7 @@ agent = RouterAgent(
name="Smart Assistant",
role="Adaptive AI Assistant",
goal="Complete tasks using the most appropriate model",
models=["gpt-5-nano", "gpt-5-nano", "claude-3-5-sonnet-20241022"],
models=["gpt-4o-mini", "gpt-4o-mini", "claude-3-5-sonnet-20241022"],
routing_strategy="auto" # Automatic model selection
)
@@ -90,7 +90,7 @@ analyzer = RouterAgent(
goal="Analyze data efficiently",
models={
"gemini/gemini-1.5-flash": {},
"gpt-5-nano": {},
"gpt-4o-mini": {},
"claude-3-haiku-20240307": {}
},
model_router=router,
@@ -207,7 +207,7 @@ for i, agent in enumerate(auto.agents):
name=agent.name,
role=agent.role,
goal=agent.goal,
models=["gpt-5-nano", "gpt-5-nano", "claude-3-5-sonnet"],
models=["gpt-4o-mini", "gpt-4o-mini", "claude-3-5-sonnet"],
routing_strategy="auto"
)
+1 -1
View File
@@ -97,7 +97,7 @@ researcher = Agent(
name="Researcher",
role="Information Researcher",
goal="Store information with semantic understanding",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create task
+1 -1
View File
@@ -17,7 +17,7 @@ def my_function():
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
with track_api_call("agent_request"):
+1 -1
View File
@@ -13,7 +13,7 @@ agent = Agent(
role="Assistant",
goal="Help users",
backstory="You are helpful.",
llm="gpt-5-nano",
llm="gpt-4o-mini",
metrics=True # 🎯 That's it!
)
@@ -177,7 +177,7 @@ Set your API keys:
```bash
export OPENAI_API_KEY="your-api-key"
export OPENAI_MODEL_NAME="gpt-5-nano"
export OPENAI_MODEL_NAME="gpt-4o-mini"
```
## 📝 Customization
@@ -38,7 +38,7 @@ def main():
# Create agent
agent = Agent(
instructions="You are a helpful assistant that explains scientific concepts simply",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Track the API call performance
@@ -13,8 +13,8 @@ def main():
role="Research Specialist",
goal="Find information about a topic",
backstory="You are an expert researcher who finds comprehensive information.",
verbose=True,
llm="gpt-5-nano", # Use a cost-effective model for examples
output="verbose",
llm="gpt-4o-mini", # Use a cost-effective model for examples
metrics=True # 🎯 SIMPLIFIED: Just add this parameter!
)
@@ -29,7 +29,7 @@ def main():
agents = Agents(
agents=[research_agent],
tasks=[research_task],
verbose=True
output="verbose"
)
print("🚀 Running agent task...")
@@ -54,7 +54,7 @@ def create_agents():
goal="Gather comprehensive information on renewable energy",
backstory="Expert researcher with deep knowledge of energy sector",
instructions="You are a thorough researcher. Provide detailed, fact-based information.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
analyst = Agent(
@@ -63,7 +63,7 @@ def create_agents():
goal="Analyze trends and patterns in renewable energy data",
backstory="Experienced data analyst specializing in energy markets",
instructions="You are an analytical thinker. Focus on trends, patterns, and insights.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
writer = Agent(
@@ -72,7 +72,7 @@ def create_agents():
goal="Create clear, compelling reports",
backstory="Professional technical writer with expertise in energy sector",
instructions="You are a skilled writer. Create clear, well-structured reports.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
return researcher, analyst, writer
@@ -104,8 +104,7 @@ def main():
workflow = Agents(
agents=[researcher, analyst, writer],
tasks=[research_task, analysis_task, report_task],
process="sequential",
verbose=True
process="sequential"
)
result = workflow.start()
@@ -11,7 +11,7 @@ Shows how to monitor agent performance when using external tools.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import duckduckgo, wikipedia
from praisonaiagents.tools import duckduckgo, wikipedia_tools
from praisonaiagents.telemetry import (
monitor_function, track_api_call, get_api_stats,
get_slowest_functions, performance_monitor
@@ -48,8 +48,8 @@ def main():
instructions="""You are a research assistant that helps find information.
Use the available tools to search for information and provide comprehensive answers.
Be thorough in your research and cite your sources.""",
llm="gpt-5-nano",
tools=[duckduckgo, wikipedia] # Add search tools
llm="gpt-4o-mini",
tools=[duckduckgo, wikipedia_tools] # Add search tools
)
# Test queries to demonstrate tool performance monitoring
@@ -97,7 +97,9 @@ def main():
# Slowest functions analysis
print("\n🐌 Slowest Functions:")
slowest = get_slowest_functions()
for func_name, avg_time in slowest:
for item in slowest[:5]:
func_name = item.get('function', 'unknown')
avg_time = item.get('average_time', 0)
print(f" {func_name}: {avg_time:.3f}s average")
# Real-time performance data
@@ -54,7 +54,7 @@ async def run_async_agent(query: str):
# Create agent
agent = Agent(
instructions="You are an efficient AI assistant that processes requests quickly",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Track the agent execution
@@ -60,7 +60,7 @@ def run_robust_agent(query: str, with_error_simulation: bool = True):
instructions="""You are a resilient AI assistant.
Even when facing challenges, you provide helpful responses.
If you encounter errors, acknowledge them and offer alternatives.""",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
try:
@@ -65,7 +65,7 @@ def run_stateful_agent(query: str, session_id: str, conversation_turn: int):
instructions="""You are a memory-enabled AI assistant. You can remember
previous conversations and build on past interactions. Use your memory
to provide personalized and contextual responses.""",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Pre-processing: retrieve relevant memories
@@ -165,7 +165,9 @@ def main():
# Slowest operations
print("\n🐌 Slowest Memory Operations:")
slowest = get_slowest_functions()
for func_name, avg_time in slowest[:5]: # Top 5 slowest
for item in slowest[:5]: # Top 5 slowest
func_name = item.get('function', 'unknown')
avg_time = item.get('average_time', 0)
print(f" {func_name}: {avg_time:.3f}s average")
# Memory operation breakdown
@@ -77,7 +77,7 @@ def run_hierarchical_workflow():
backstory="Experienced project manager with expertise in team coordination",
instructions="""You are a project manager. Break down complex projects into tasks,
assign them to appropriate team members, and coordinate the overall effort.""",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create worker agents
@@ -87,7 +87,7 @@ def run_hierarchical_workflow():
goal="Conduct thorough research on assigned topics",
backstory="Expert researcher with deep analytical skills",
instructions="You are a research specialist. Provide detailed, factual information.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
analysis_worker = Agent(
@@ -96,7 +96,7 @@ def run_hierarchical_workflow():
goal="Analyze data and identify patterns and insights",
backstory="Experienced data analyst with strong analytical thinking",
instructions="You are a data analyst. Focus on patterns, trends, and insights.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
writing_worker = Agent(
@@ -105,7 +105,7 @@ def run_hierarchical_workflow():
goal="Create clear, well-structured documentation and reports",
backstory="Professional writer specializing in technical content",
instructions="You are a technical writer. Create clear, comprehensive reports.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Project tasks
@@ -180,7 +180,7 @@ def run_agent_with_custom_tools():
instructions="""You are a technical assistant with access to various tools.
Use the appropriate tools to help with data processing, API calls, database queries,
and file operations. Always explain what tools you're using and why.""",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=[process_data, call_external_api, query_database, file_operation]
)
@@ -263,7 +263,7 @@ def main():
# Slowest operations
print("\n🐌 Slowest Tool Operations:")
slowest = get_slowest_functions()
tool_slowest = [(name, time) for name, time in slowest if 'tool' in name.lower()]
tool_slowest = [(item.get('function', 'unknown'), item.get('average_time', 0)) for item in slowest if 'tool' in item.get('function', '').lower()]
for tool_name, avg_time in tool_slowest[:3]:
print(f" {tool_name}: {avg_time:.3f}s")
@@ -60,7 +60,7 @@ def get_real_time_agent_response(query: str, response_id: int):
# Create a lightweight agent for streaming
agent = Agent(
instructions="You are a real-time assistant. Provide quick, helpful responses.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
with track_api_call(f"streaming_response_{response_id}"):
@@ -42,19 +42,19 @@ def collect_comprehensive_data():
agents = {
'quick_agent': Agent(
instructions="You are a quick-response agent. Provide concise answers.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
),
'analytical_agent': Agent(
instructions="You are an analytical agent. Provide detailed analysis.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
),
'research_agent': Agent(
instructions="You are a research agent. Provide comprehensive information.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
),
'creative_agent': Agent(
instructions="You are a creative agent. Provide imaginative responses.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
}
@@ -98,19 +98,19 @@ def simulate_complex_workflow():
planner = Agent(
name="planner",
instructions="You are a project planner. Break down tasks and plan execution.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
executor = Agent(
name="executor",
instructions="You are a task executor. Complete assigned tasks efficiently.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
reviewer = Agent(
name="reviewer",
instructions="You are a quality reviewer. Review and improve work.",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create workflow tasks
@@ -302,13 +302,17 @@ def display_dashboard(dashboard_data, benchmark_results):
perf_data = dashboard_data['performance_data']
print(" Fastest Functions:")
fastest_functions = sorted(perf_data['slowest_functions'], key=lambda x: x[1])[:3]
for name, time in fastest_functions:
print(f" {name}: {time:.3f}s")
fastest_functions = sorted(perf_data['slowest_functions'], key=lambda x: x.get('average_time', float('inf')))[:3]
for item in fastest_functions:
name = item.get('function', 'unknown')
time_val = item.get('average_time', 0)
print(f" {name}: {time_val:.3f}s")
print(" Slowest Functions:")
for name, time in perf_data['slowest_functions'][:3]:
print(f" {name}: {time:.3f}s")
for item in perf_data['slowest_functions'][:3]:
name = item.get('function', 'unknown')
time_val = item.get('average_time', 0)
print(f" {name}: {time_val:.3f}s")
# Benchmark Results
print(f"\n⚡ BENCHMARK RESULTS")
+1 -1
View File
@@ -111,7 +111,7 @@ agent = Agent(
role="Agent Role",
goal="Agent Goal",
backstory="Agent Background",
llm="gpt-5-nano", # or other LLM
llm="gpt-4o-mini", # or other LLM
self_reflect=True, # Enable self-reflection
min_reflect=1, # Minimum reflection iterations
max_reflect=3, # Maximum reflection iterations
+63
View File
@@ -0,0 +1,63 @@
"""
Streaming Example with Profiling
Key Points:
1. output="stream" preset enables streaming by default
2. stream=True in start() is redundant when using output="stream" (but doesn't hurt)
3. Profiler.enable() MUST be called before profiling works
4. Use Profiler.streaming() context manager for TTFT metrics
"""
from praisonai.profiler import Profiler, profile, profile_imports
# IMPORTANT: Enable profiler BEFORE any profiling operations
Profiler.enable()
# Profile imports
with profile_imports():
from praisonaiagents import Agent
@profile
def main():
agent = Agent(
instructions="You are a helpful assistant",
output="stream" # This already enables streaming
)
print("\nStreaming response:")
print("-" * 40)
# Use Profiler.streaming() to track TTFT (Time To First Token)
with Profiler.streaming("llm_stream") as tracker:
first_chunk = True
for chunk in agent.start("Write a short paragraph about the history of computing"):
if first_chunk:
tracker.first_token() # Mark TTFT
first_chunk = False
tracker.chunk() # Count chunks
print(chunk, end="", flush=True)
print("\n" + "-" * 40)
# Run with profiling blocks
with Profiler.block("full_workflow"):
with Profiler.block("setup"):
pass
with Profiler.block("execution"):
result = main()
with Profiler.block("cleanup"):
pass
# Generate report
Profiler.report()
# Show streaming-specific stats
streaming_records = Profiler.get_streaming_records()
if streaming_records:
print("\nStreaming Metrics:")
for record in streaming_records:
print(f" {record.name}: TTFT={record.ttft_ms:.2f}ms, Total={record.total_ms:.2f}ms, Chunks={record.chunk_count}")
stats = Profiler.get_statistics()
print(f"\nMedian execution time: {stats['p50']:.2f}ms")
@@ -1,98 +1,75 @@
#!/usr/bin/env python3
"""
Agent-level guardrails example.
This example demonstrates how to use guardrails at the Agent level,
This example demonstrates how to use guardrails at the Agent level
which will apply to all tasks executed by that agent.
"""
from typing import Tuple, Any
from praisonaiagents import Agent, TaskOutput
def validate_content_length(task_output: TaskOutput) -> Tuple[bool, Any]:
"""
Validate that task output content meets minimum length requirement.
Args:
task_output: The task output to validate
Returns:
Tuple of (success, result_or_error_message)
"""
if len(task_output.raw) < 50:
return False, "Content too short - must be at least 50 characters"
return True, task_output
def validate_professional_tone(task_output: TaskOutput) -> Tuple[bool, Any]:
"""
Validate that the content has a professional tone.
Args:
task_output: The task output to validate
Returns:
Tuple of (success, result_or_error_message)
"""
content = task_output.raw.lower()
unprofessional_words = ['yo', 'dude', 'awesome', 'cool', 'lol']
for word in unprofessional_words:
if word in content:
return False, f"Content contains unprofessional word: '{word}'"
return True, task_output
def main():
"""Demonstrate Agent-level guardrails with function-based and LLM-based validation."""
print("=== Agent Guardrail Examples ===\n")
# Example 1: Function-based guardrail
print("1. Function-based guardrail (content length validation):")
agent1 = Agent(
name="ContentWriter",
instructions="You are a professional content writer who creates detailed responses",
guardrail=validate_content_length,
max_guardrail_retries=2
name="ContentWriter"
instructions="You are a professional content writer who creates detailed responses"
guardrails=validate_content_length
)
try:
result1 = agent1.start("Write a brief welcome message")
print(f"Result: {result1}\n")
except Exception as e:
print(f"Error: {e}\n")
# Example 2: LLM-based guardrail (string description)
print("2. LLM-based guardrail (professional tone validation):")
agent2 = Agent(
name="BusinessWriter",
instructions="You are a business communication expert",
guardrail="Ensure the content is professional, formal, and suitable for business communication. No casual language or slang.",
max_guardrail_retries=3
instructions="You are a business communication expert"
guardrails="Ensure the content is professional, formal, and suitable for business communication. No casual language or slang."
)
try:
result2 = agent2.start("Write a welcome message for new employees")
print(f"Result: {result2}\n")
except Exception as e:
print(f"Error: {e}\n")
# Example 3: Multiple agents with different guardrails
print("3. Professional tone function-based guardrail:")
agent3 = Agent(
name="ProfessionalWriter",
instructions="Write professional business content",
guardrail=validate_professional_tone,
max_guardrail_retries=2
name="ProfessionalWriter"
instructions="Write professional business content"
guardrails=validate_professional_tone
)
try:
result3 = agent3.start("Write a casual greeting message")
print(f"Result: {result3}\n")
except Exception as e:
print(f"Error: {e}\n")
print("=== Agent Guardrails Demonstration Complete ===")
if __name__ == "__main__":
main()
@@ -21,7 +21,7 @@ article_agent = Agent(
goal="Fetch the user's favorite morning article",
tools=[fetch_favorite_article],
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
verbose=True
output="verbose"
)
news_agent = Agent(
@@ -30,7 +30,7 @@ news_agent = Agent(
goal="Search for trending news in Kenya",
tools=[search_trending_kenya],
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
verbose=True
output="verbose"
)
twitter_agent = Agent(
@@ -39,7 +39,7 @@ twitter_agent = Agent(
goal="Fetch the latest Twitter feed",
tools=[fetch_twitter_feed],
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
verbose=True
output="verbose"
)
aggregator = Agent(
@@ -47,7 +47,7 @@ aggregator = Agent(
role="Result Aggregator",
goal="Aggregate and summarize all results",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
verbose=True
output="verbose"
)
# Tasks for each agent
@@ -92,7 +92,7 @@ async def main():
agents=[article_agent, news_agent, twitter_agent, aggregator],
tasks=[article_task, news_task, twitter_task, aggregate_task],
process="workflow",
verbose=True
output="verbose"
)
results = await workflow.astart(dict_output=True)
+2 -2
View File
@@ -5,8 +5,8 @@ async def main():
agent = Agent(
instructions="You are a helpful assistant",
llm="gemini/gemini-1.5-flash-8b",
self_reflect=True,
verbose=True
reflection=True,
output="verbose"
)
# Use achat instead of start/chat for async operation
@@ -10,7 +10,7 @@ import os
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
llm_config = {
"model": "gpt-5-nano", # Changed from Gemini to gpt-5-nano for better tool support
"model": "gpt-4o-mini", # Changed from Gemini to gpt-5-nano for better tool support
"temperature": 0.7,
"response_format": {
"type": "text"
@@ -79,7 +79,7 @@ async def run_parallel_searches():
backstory="Expert in web search and information retrieval. You have access to the tavily_search tool to search the web.",
tools=[tavily_search],
llm=llm_config,
verbose=True
output="verbose"
)
# Create summary agent
+1 -1
View File
@@ -5,7 +5,7 @@ def get_weather(city: str) -> str:
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=[get_weather]
)
+2 -13
View File
@@ -1,14 +1,3 @@
#!/usr/bin/env python3
from praisonaiagents import Agent
print("Creating agent...")
agent = Agent(
instructions="You are a helpful assistant"
)
print("Starting agent...")
result = agent.start("Why is the sky blue?")
print("Agent completed successfully!")
print(f"Result: {result}")
agent = Agent(instructions="You are a helpful assistant")
result = agent.start("Why is the sky blue?")
@@ -1,6 +1,6 @@
# PraisonAI Agents - Real Execution Benchmark
**Generated:** 2025-12-18 14:43:52
**Generated:** 2026-01-13 06:07:28
**Model:** gpt-4o-mini
**Iterations:** 3
**Prompt:** "What is 2+2? Reply with just the number."
@@ -9,18 +9,18 @@
| Framework | Method | Avg Time | Relative |
|-----------|--------|----------|----------|
| **PraisonAI** | `agent.start()` | **0.45s** | **1.00x (fastest)** |
| PraisonAI (LiteLLM) | `agent.start()` | 0.55s | 1.22x |
| CrewAI | `crew.kickoff()` | 0.58s | 1.28x |
| Agno | `agent.run()` | 0.92s | 2.05x |
| **PraisonAI (LiteLLM)** | `agent.start()` | **0.55s** | **1.00x (fastest)** |
| PraisonAI | `agent.start()` | 0.64s | 1.15x |
| CrewAI | `crew.kickoff()` | 0.95s | 1.71x |
| Agno | `agent.run()` | 1.05s | 1.90x |
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| CrewAI | 1.6.1 |
| PraisonAI | 0.11.7 |
| Agno | 2.3.25 |
| CrewAI | 1.8.0 |
## How to Reproduce
@@ -1,6 +1,6 @@
# PraisonAI Agents - Real-World Execution Benchmark
**Generated:** 2025-12-18 14:33:49
**Generated:** 2026-01-13 06:05:09
**Model:** gpt-4o-mini
**Iterations:** 3 per test
@@ -8,60 +8,60 @@
| Test | PraisonAI (avg) | Agno (avg) | Difference |
|------|-----------------|------------|------------|
| Single Agent | 2.04s | 3.18s | PraisonAI 1.56x faster |
| 2 Agents | 2.49s | 20.49s | PraisonAI 8.21x faster |
| 3 Agents | 6.52s | 13.97s | PraisonAI 2.14x faster |
| Single Agent | 2.09s | 3.25s | PraisonAI 1.55x faster |
| 2 Agents | 2.50s | 9.73s | PraisonAI 3.89x faster |
| 3 Agents | 6.25s | 13.56s | PraisonAI 2.17x faster |
## Detailed Results
### praisonai_single
- Average: 2.04s
- Min: 1.95s
- Max: 2.19s
- Std Dev: 0.13s
- Times: ['2.19s', '1.95s', '2.00s']
- Average: 2.09s
- Min: 1.64s
- Max: 2.84s
- Std Dev: 0.66s
- Times: ['2.84s', '1.79s', '1.64s']
### agno_single
- Average: 3.18s
- Min: 3.00s
- Max: 3.51s
- Std Dev: 0.28s
- Times: ['3.51s', '3.00s', '3.04s']
- Average: 3.25s
- Min: 3.14s
- Max: 3.35s
- Std Dev: 0.10s
- Times: ['3.35s', '3.25s', '3.14s']
### praisonai_two_agents
- Average: 2.49s
- Min: 2.22s
- Max: 2.65s
- Std Dev: 0.24s
- Times: ['2.65s', '2.22s', '2.62s']
- Average: 2.50s
- Min: 2.38s
- Max: 2.75s
- Std Dev: 0.21s
- Times: ['2.38s', '2.75s', '2.38s']
### agno_two_agents
- Average: 20.49s
- Min: 9.96s
- Max: 35.05s
- Std Dev: 13.02s
- Times: ['16.46s', '9.96s', '35.05s']
- Average: 9.73s
- Min: 9.34s
- Max: 10.53s
- Std Dev: 0.69s
- Times: ['10.53s', '9.34s', '9.34s']
### praisonai_three_agents
- Average: 6.52s
- Min: 5.62s
- Max: 7.39s
- Std Dev: 0.88s
- Times: ['5.62s', '6.54s', '7.39s']
- Average: 6.25s
- Min: 5.86s
- Max: 6.80s
- Std Dev: 0.49s
- Times: ['6.80s', '6.09s', '5.86s']
### agno_three_agents
- Average: 13.97s
- Min: 11.69s
- Max: 15.32s
- Std Dev: 1.99s
- Times: ['14.91s', '11.69s', '15.32s']
- Average: 13.56s
- Min: 12.26s
- Max: 14.90s
- Std Dev: 1.32s
- Times: ['14.90s', '13.51s', '12.26s']
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| PraisonAI | 0.11.7 |
| Agno | 2.3.25 |
## How to Reproduce
@@ -52,12 +52,11 @@ def benchmark_praisonai(model, iterations, prompt):
agent = Agent(
name="Calculator",
instructions="You are a helpful assistant. Be very brief.",
llm=model,
verbose=False
llm=model
)
start = time.perf_counter()
response = agent.start(prompt)
response = agent.start(prompt, output="silent")
elapsed = time.perf_counter() - start
times.append(elapsed)
@@ -80,12 +79,11 @@ def benchmark_praisonai_litellm(model, iterations, prompt):
agent = Agent(
name="Calculator",
instructions="You are a helpful assistant. Be very brief.",
llm=f"openai/{model}",
verbose=False
llm=f"openai/{model}"
)
start = time.perf_counter()
response = agent.start(prompt)
response = agent.start(prompt, output="silent")
elapsed = time.perf_counter() - start
times.append(elapsed)
@@ -103,12 +103,11 @@ def benchmark_praisonai_single(iterations: int = 3) -> ExecutionResult:
name="Assistant",
instructions="You are a helpful assistant. Be very brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
tools=[get_weather]
)
start = time.perf_counter()
response = agent.start("What's the weather in NYC? Reply in one sentence.")
response = agent.start("What's the weather in NYC? Reply in one sentence.", output="silent")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
@@ -132,8 +131,7 @@ def benchmark_praisonai_two_agents(iterations: int = 3) -> ExecutionResult:
goal="Gather weather information",
instructions="Get weather data and pass to reporter. Be brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
tools=[get_weather]
)
reporter = Agent(
@@ -141,18 +139,16 @@ def benchmark_praisonai_two_agents(iterations: int = 3) -> ExecutionResult:
role="News Reporter",
goal="Create weather report",
instructions="Create a one-sentence weather summary.",
llm="gpt-4o-mini",
verbose=False
llm="gpt-4o-mini"
)
agents = Agents(
agents=[researcher, reporter],
process="sequential",
verbose=False
process="sequential"
)
start = time.perf_counter()
response = agents.start("Get NYC weather and create a brief report.")
response = agents.start("Get NYC weather and create a brief report.", output="silent")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
@@ -176,8 +172,7 @@ def benchmark_praisonai_three_agents(iterations: int = 3) -> ExecutionResult:
goal="Get weather data",
instructions="Fetch weather information. Be brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
tools=[get_weather]
)
time_agent = Agent(
@@ -186,8 +181,7 @@ def benchmark_praisonai_three_agents(iterations: int = 3) -> ExecutionResult:
goal="Get time information",
instructions="Fetch timezone information. Be brief.",
llm="gpt-4o-mini",
tools=[get_time],
verbose=False
tools=[get_time]
)
summarizer = Agent(
@@ -195,18 +189,16 @@ def benchmark_praisonai_three_agents(iterations: int = 3) -> ExecutionResult:
role="Summary Writer",
goal="Combine information",
instructions="Create a one-sentence summary combining weather and time.",
llm="gpt-4o-mini",
verbose=False
llm="gpt-4o-mini"
)
agents = Agents(
agents=[weather_agent, time_agent, summarizer],
process="sequential",
verbose=False
process="sequential"
)
start = time.perf_counter()
response = agents.start("Get NYC weather and EST time, then summarize.")
response = agents.start("Get NYC weather and EST time, then summarize.", output="silent")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
+1 -1
View File
@@ -1,5 +1,5 @@
from praisonaiagents import ContextAgent
agent = ContextAgent(llm="gpt-5-nano", auto_analyze=False)
agent = ContextAgent(llm="gpt-4o-mini", auto_analyze=False)
agent.start("https://github.com/MervinPraison/PraisonAI/ Need to add Authentication")
+1 -1
View File
@@ -14,7 +14,7 @@ def test_agent_direct():
agent = Agent(
instructions="""You are a helpful assistant that can break down complex problems.
Use the available tools when relevant to perform step-by-step analysis.""",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP("npx -y @modelcontextprotocol/server-sequential-thinking")
)
@@ -14,7 +14,7 @@ llm_config = {
agent = Agent(
instructions="You are a helpful assistant. Be concise and clear.",
llm=llm_config,
verbose=True,
output="verbose",
self_reflect=True,
max_reflect=2,
min_reflect=1
@@ -7,8 +7,7 @@ def validate_content(data):
agent = Agent(
instructions="You are a writer",
guardrail=validate_content,
max_guardrail_retries=1
guardrails=validate_content
)
agent.start("Write a welcome message with 5 words")
+1 -1
View File
@@ -32,7 +32,7 @@ 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
verbose=True, # Enable detailed output
output="verbose", # Enable detailed output
markdown=True, # Format responses in markdown
self_reflect=True, # Enable self-reflection
max_reflect=3, # Maximum reflection iterations
+1 -1
View File
@@ -3,7 +3,7 @@ from praisonaiagents import Agent, MCP
agent = Agent(
instructions="""You are a helpful assistant that can check stock prices and perform other tasks.
Use the available tools when relevant to answer user questions.""",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools = MCP("/Users/praison/miniconda3/envs/mcp/bin/python /Users/praison/stockprice/app.py")
)
@@ -71,7 +71,7 @@ def main():
tools=[mongodb_tools],
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# 2. Knowledge Curator Agent - manages knowledge base
@@ -85,7 +85,7 @@ def main():
knowledge_config=mongodb_knowledge_config,
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# 3. Business Analyst Agent - analyzes data and provides insights
@@ -99,7 +99,7 @@ def main():
tools=[mongodb_tools],
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# 4. Customer Service Agent - handles customer interactions
@@ -114,7 +114,7 @@ def main():
tools=[mongodb_tools],
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create comprehensive business tasks
@@ -55,11 +55,9 @@ def main():
retrieve information from a comprehensive MongoDB knowledge base. You excel
at finding relevant information, synthesizing knowledge from multiple sources,
and providing accurate, context-aware responses.""",
knowledge_config=mongodb_knowledge_config,
knowledge=[os.path.join(os.path.dirname(__file__), "llms.md")],
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create a research assistant agent
@@ -72,8 +70,7 @@ def main():
knowledge base. You ensure information is accurate, well-structured,
and properly categorized.""",
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create tasks for knowledge management
@@ -128,7 +125,7 @@ def main():
agents=[research_agent, knowledge_agent],
tasks=knowledge_tasks,
memory=True,
verbose=True
output="verbose"
)
# Execute the knowledge management pipeline
@@ -59,7 +59,7 @@ def main():
knowledge=[os.path.join(os.path.dirname(__file__), "llms.md")],
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create a research assistant agent
@@ -73,7 +73,7 @@ def main():
and properly categorized.""",
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create tasks for knowledge management
+2 -2
View File
@@ -3,7 +3,7 @@ from praisonaiagents.memory import Memory
from praisonaiagents.agent import ContextAgent
import pymongo
context_agent = ContextAgent(llm="gpt-5-nano", auto_analyze=False)
context_agent = ContextAgent(llm="gpt-4o-mini", auto_analyze=False)
context_output = context_agent.start("https://github.com/MervinPraison/PraisonAI/ Need to add Authentication")
@@ -25,7 +25,7 @@ implementation_agent = Agent(
goal="Implement authentication features based on project requirements",
backstory="Expert software implementer specializing in authentication systems, security features, and seamless integration with existing codebases",
memory=True,
llm="gpt-5-nano",
llm="gpt-4o-mini",
)
implementation_task = Task(
@@ -50,7 +50,7 @@ def main():
retrieve relevant information from your persistent memory store.""",
memory=True,
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create research tasks
@@ -38,7 +38,7 @@ def main():
for data management and retrieval.""",
tools=[mongodb_tools],
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create a Data Analysis Agent
@@ -51,7 +51,7 @@ def main():
identify patterns and trends in the stored information.""",
tools=[mongodb_tools],
verbose=True,
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# Create tasks for MongoDB operations
+7
View File
@@ -0,0 +1,7 @@
from praisonaiagents import Agent, Agents
research_agent = Agent(instructions="Research about AI")
summarise_agent = Agent(instructions="Summarise research agent's findings")
agents = Agents(agents=[research_agent, summarise_agent])
response = agents.start()
+1 -1
View File
@@ -2,6 +2,6 @@ from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
agent.start("Why sky is Blue?")
+1 -1
View File
@@ -2,7 +2,7 @@ from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
agent.start("Why sky is Blue?")
agent.start("What was my previous question?")
+1 -1
View File
@@ -3,7 +3,7 @@ import asyncio
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
agent.run("Why sea is Blue?")
+1 -1
View File
@@ -22,7 +22,7 @@ def multiply(a: int, b: int) -> int:
agent = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=[get_stock_price, multiply]
)
+1 -2
View File
@@ -14,8 +14,7 @@ task2 = Task(description="Write a short article about meditation benefits", agen
agents = Agents(
agents=[researcher, writer],
tasks=[task1, task2],
planning=True,
auto_approve_plan=True # Auto-approve the generated plan
planning=True
)
agents.start()
+1 -1
View File
@@ -22,7 +22,7 @@ def multiply(a: int, b: int) -> int:
agent = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=[get_stock_price, multiply]
)
+161 -165
View File
@@ -25,6 +25,14 @@ from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
from .db import db
from .obs import obs
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
from . import config
from . import tools
from . import memory
from . import workflows
# Note: knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
# Workflows - lightweight module
from .workflows import (
Workflow, WorkflowStep, WorkflowContext, StepResult,
@@ -442,6 +450,51 @@ def __getattr__(name):
_lazy_cache[name] = ContextManager
return ContextManager
# =========================================================================
# Sub-package fallback: delegate to sub-packages for additional symbols
# This enables: from praisonaiagents import duckduckgo, FileMemory, etc.
# =========================================================================
# Try tools sub-package (110+ tool functions)
try:
from . import tools as _tools_module
if hasattr(_tools_module, name):
result = getattr(_tools_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try memory sub-package (FileMemory, RulesManager, etc.)
try:
from . import memory as _memory_module
if hasattr(_memory_module, name):
result = getattr(_memory_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try config sub-package (already handled above, but catch any stragglers)
try:
from . import config as _config_module
if hasattr(_config_module, name):
result = getattr(_config_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try workflows sub-package (Pipeline, Route, etc. - already imported, catch aliases)
try:
from . import workflows as _workflows_module
if hasattr(_workflows_module, name):
result = getattr(_workflows_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -476,174 +529,117 @@ def _init_telemetry():
_init_telemetry()
def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
"""
Pre-import heavy dependencies to reduce first-call latency.
NOTE: For default OpenAI usage (llm="gpt-4o-mini"), warmup is NOT needed.
The default path uses the native OpenAI SDK which is fast (~100ms import).
Warmup is only beneficial when using LiteLLM backend, which is triggered by:
- Using "/" in model name (e.g., llm="openai/gpt-4o-mini")
- Passing a dict config (e.g., llm={"model": "gpt-4o-mini"})
- Using base_url parameter
Args:
include_litellm: Pre-import LiteLLM (~2-3s). Only needed for multi-provider support.
include_openai: Pre-import OpenAI SDK (~100ms). Default path, usually fast.
Returns:
dict: Timing information for each component warmed up
Example:
# For LiteLLM multi-provider usage:
from praisonaiagents import warmup
warmup(include_litellm=True) # Pre-load LiteLLM
agent = Agent(llm="anthropic/claude-3-sonnet") # Now faster
# For default OpenAI usage, no warmup needed:
agent = Agent(llm="gpt-4o-mini") # Already fast!
"""
import time
timings = {}
if include_openai:
start = time.perf_counter()
try:
import openai
timings['openai'] = (time.perf_counter() - start) * 1000
except ImportError:
timings['openai'] = -1 # Not available
if include_litellm:
start = time.perf_counter()
try:
import litellm
# Also configure litellm to avoid first-call overhead
litellm.telemetry = False
litellm.set_verbose = False
litellm.drop_params = True
litellm.modify_params = True
timings['litellm'] = (time.perf_counter() - start) * 1000
except ImportError:
timings['litellm'] = -1 # Not available
return timings
# ============================================================================
# PUBLIC API: __all__ (controls IDE autocomplete and `from X import *`)
# ============================================================================
# DESIGN: Keep __all__ minimal for clean IDE experience.
# All 186+ symbols are still accessible via __getattr__ for backwards compat.
# Organized imports available via sub-packages: config, tools, memory, workflows
# ============================================================================
__all__ = [
# Core classes (lazy loaded)
# Core classes - the essentials
'Agent',
'ImageAgent',
'ContextAgent',
'create_context_agent',
'PraisonAIAgents',
'Agents',
'Tools',
'Task',
# Plugin system
'BaseTool',
'ToolResult',
'ToolValidationError',
'validate_tool',
# Tool essentials
'tool',
'FunctionTool',
'ToolRegistry',
'get_registry',
'register_tool',
'get_tool',
'TaskOutput',
'ReflectionOutput',
'AutoAgents',
'AutoRagAgent',
'AutoRagConfig',
'RagRetrievalPolicy',
'Session',
'Memory',
'db',
'obs',
'display_interaction',
'display_self_reflection',
'display_instruction',
'display_tool_call',
'display_error',
'display_generating',
'clean_triple_backticks',
'error_logs',
'register_display_callback',
'sync_display_callbacks',
'async_display_callbacks',
'Knowledge',
'Chunking',
'GuardrailResult',
'LLMGuardrail',
'Handoff',
'handoff',
'handoff_filters',
'RECOMMENDED_PROMPT_PREFIX',
'prompt_with_handoff_instructions',
'HandoffConfig',
'HandoffResult',
'HandoffInputData',
'ContextPolicy',
'HandoffError',
'HandoffCycleError',
'HandoffDepthError',
'HandoffTimeoutError',
'get_telemetry',
'enable_telemetry',
'disable_telemetry',
'enable_performance_mode',
'disable_performance_mode',
'cleanup_telemetry_resources',
'MinimalTelemetry',
'TelemetryCollector',
'DeepResearchAgent',
'DeepResearchResponse',
'Citation',
'ReasoningStep',
'WebSearchCall',
'CodeExecutionStep',
'MCPCall',
'FileSearchCall',
'Provider',
'QueryRewriterAgent',
'RewriteStrategy',
'RewriteResult',
'PromptExpanderAgent',
'ExpandStrategy',
'ExpandResult',
# Workflows
'Workflow',
'WorkflowStep',
'WorkflowContext',
'StepResult',
'Route',
'Parallel',
'Loop',
'Repeat',
'route',
'parallel',
'loop',
'repeat',
'Pipeline',
# MCP
'MCP',
# Flow display
'FlowDisplay',
'track_workflow',
# FastContext
'FastContext',
'FastContextResult',
'FileMatch',
'LineRange',
# Agent Skills
'SkillManager',
'SkillProperties',
'SkillMetadata',
'SkillLoader',
# Planning
'Plan',
'PlanStep',
'TodoList',
'TodoItem',
'PlanStorage',
'PlanningAgent',
'ApprovalCallback',
'READ_ONLY_TOOLS',
'RESTRICTED_TOOLS',
# UI
'AGUI',
'A2A',
# RAG
'RAG',
'RAGConfig',
'RAGResult',
'RAGCitation',
# Feature Configs (agent-centric API)
'MemoryConfig',
'KnowledgeConfig',
'PlanningConfig',
'ReflectionConfig',
'GuardrailConfig',
'WebConfig',
'OutputConfig',
'ExecutionConfig',
'TemplateConfig',
'CachingConfig',
'HooksConfig',
'SkillsConfig',
'AutonomyConfig',
'MemoryBackend',
'ChunkingStrategy',
'GuardrailAction',
'WebSearchProvider',
'OutputPreset',
'ExecutionPreset',
'AutonomyLevel',
# Multi-Agent Feature Configs
'MultiAgentHooksConfig',
'MultiAgentOutputConfig',
'MultiAgentExecutionConfig',
'MultiAgentPlanningConfig',
'MultiAgentMemoryConfig',
# Context Management
'ManagerConfig',
'ContextManager',
# Parameter Resolution (agent-centric API)
'resolve',
'ArrayMode',
'resolve_guardrails',
'resolve_guardrail_policies',
# Presets (agent-centric API)
'GUARDRAIL_PRESETS',
'KNOWLEDGE_PRESETS',
# Parse Utilities
'is_policy_string',
'parse_policy_string',
'Tools',
# Sub-packages for organized imports
# Usage: import praisonaiagents as pa; pa.config.MemoryConfig
'config',
'tools',
'memory',
'workflows',
]
def __dir__():
"""
Return clean list for dir() - matches __all__ plus standard attributes.
This keeps IDE autocomplete clean while preserving full backwards
compatibility via __getattr__ for all 186+ legacy exports.
"""
return list(__all__) + [
# Standard module attributes
'__name__', '__doc__', '__file__', '__path__', '__package__',
'__loader__', '__spec__', '__cached__', '__builtins__',
]
# ============================================================================
# BACKWARDS COMPATIBILITY: Legacy __all__ items (for reference)
# ============================================================================
# All items below are still importable via __getattr__ but NOT in autocomplete:
# - ImageAgent, ContextAgent, create_context_agent, PraisonAIAgents
# - BaseTool, ToolResult, ToolValidationError, validate_tool, FunctionTool
# - ToolRegistry, get_registry, register_tool, get_tool
# - TaskOutput, ReflectionOutput, AutoAgents, AutoRagAgent, AutoRagConfig
# - Session, Memory, db, obs, Knowledge, Chunking
# - GuardrailResult, LLMGuardrail, Handoff, handoff, handoff_filters
# - MemoryConfig, KnowledgeConfig, PlanningConfig, OutputConfig, etc.
# - Workflow, WorkflowStep, Route, Parallel, Loop, Repeat, Pipeline, etc.
# - MCP, FlowDisplay, track_workflow, FastContext, etc.
# - Plan, PlanStep, TodoList, PlanningAgent, ApprovalCallback, etc.
# - RAG, RAGConfig, RAGResult, AGUI, A2A, etc.
# - All telemetry, display, and utility functions
# ============================================================================
@@ -44,7 +44,7 @@ _shared_apps = {} # Dict of port -> FastAPI app
if TYPE_CHECKING:
from ..task.task import Task
from ..main import TaskOutput
from ..handoff import Handoff
from .handoff import Handoff, HandoffConfig, HandoffResult
from ..rag.models import RAGResult, ContextPack
class Agent:
@@ -291,96 +291,87 @@ class Agent:
"""Initialize an Agent instance.
Args:
name (Optional[str], optional): Name of the agent used for identification and logging.
If None, defaults to "Agent". Defaults to None.
role (Optional[str], optional): Role or job title that defines the agent's expertise
and behavior patterns. Examples: "Data Analyst", "Content Writer". Defaults to None.
goal (Optional[str], optional): Primary objective or goal the agent aims to achieve.
Defines the agent's purpose and success criteria. Defaults to None.
backstory (Optional[str], optional): Background story or context that shapes the agent's
personality and decision-making approach. Defaults to None.
instructions (Optional[str], optional): Direct instructions that override role, goal,
and backstory when provided. Used for simple, task-specific agents. Defaults to None.
llm (Optional[Union[str, Any]], optional): Language model configuration. Can be a model
name string (e.g., "gpt-5-nano", "anthropic/claude-3-sonnet") or a configured LLM object.
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-5-nano".
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
available to the agent for task execution. Can include callables, tool objects,
or MCP instances. Defaults to None.
function_calling_llm (Optional[Any], optional): Dedicated language model for function
calling operations. If None, uses the main llm parameter. Defaults to None.
max_iter (int, optional): Maximum number of iterations the agent can perform during
task execution to prevent infinite loops. Defaults to 20.
max_rpm (Optional[int], optional): Maximum requests per minute to rate limit API calls
and prevent quota exhaustion. If None, no rate limiting is applied. Defaults to None.
max_execution_time (Optional[int], optional): Maximum execution time in seconds for
agent operations before timeout. If None, no time limit is enforced. Defaults to None.
memory (Optional[Any], optional): Memory system for storing and retrieving information
across conversations. Requires memory dependencies to be installed. Defaults to None.
verbose (bool, optional): Enable detailed logging and status updates during agent
execution for debugging and monitoring. Defaults to True.
allow_delegation (bool, optional): Allow the agent to delegate tasks to other agents
or sub-processes when appropriate. Defaults to False.
step_callback (Optional[Any], optional): Callback function called after each step
of agent execution for custom monitoring or intervention. Defaults to None.
cache (bool, optional): Enable caching of responses and computations to improve
performance and reduce API costs. Defaults to True.
system_template (Optional[str], optional): Custom template for system prompts that
overrides the default system prompt generation. Defaults to None.
prompt_template (Optional[str], optional): Template for formatting user prompts
before sending to the language model. Defaults to None.
response_template (Optional[str], optional): Template for formatting agent responses
before returning to the user. Defaults to None.
allow_code_execution (Optional[bool], optional): Enable the agent to execute code
snippets during task completion. Use with caution for security. Defaults to False.
max_retry_limit (int, optional): Maximum number of retry attempts for failed operations
before giving up. Helps handle transient errors. Defaults to 2.
code_execution_mode (Literal["safe", "unsafe"], optional): Safety mode for code execution.
"safe" restricts dangerous operations, "unsafe" allows full code execution. Defaults to "safe".
embedder_config (Optional[Dict[str, Any]], optional): Configuration dictionary for
text embedding models used in knowledge retrieval and similarity search. Defaults to None.
knowledge (Optional[List[str]], optional): List of knowledge sources (file paths, URLs,
or text content) to be processed and made available to the agent. Defaults to None.
knowledge_config (Optional[Dict[str, Any]], optional): Configuration for knowledge
processing and retrieval system including chunking and indexing parameters. Defaults to None.
use_system_prompt (Optional[bool], optional): Whether to include system prompts in
conversations to establish agent behavior and context. Defaults to True.
markdown (bool, optional): Enable markdown formatting in agent responses for better
readability and structure. Defaults to True.
stream (bool, optional): Enable streaming responses from the language model for real-time
output when using Agent.start() method. Defaults to False for backward compatibility.
metrics (bool, optional): Enable automatic token usage tracking and display summary
when tasks complete. Simplifies token monitoring for cost optimization. Defaults to False.
self_reflect (bool, optional): Enable self-reflection capabilities where the agent
evaluates and improves its own responses. Defaults to False.
max_reflect (int, optional): Maximum number of self-reflection iterations to prevent
excessive reflection loops. Defaults to 3.
min_reflect (int, optional): Minimum number of self-reflection iterations required
before accepting a response as satisfactory. Defaults to 1.
reflect_llm (Optional[str], optional): Dedicated language model for self-reflection
operations. If None, uses the main llm parameter. Defaults to None.
reflect_prompt (Optional[str], optional): Custom prompt template for self-reflection
that guides the agent's self-evaluation process. Defaults to None.
user_id (Optional[str], optional): Unique identifier for the user or session to
enable personalized responses and memory isolation. Defaults to "praison".
reasoning_steps (bool, optional): Enable step-by-step reasoning output to show the
agent's thought process during problem solving. Defaults to False.
guardrail (Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]], optional):
Safety mechanism to validate agent outputs. Can be a validation function or
description string for LLM-based validation. Defaults to None.
max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
validation fails before giving up. Defaults to 3.
handoffs (Optional[List[Union['Agent', 'Handoff']]], optional): List of agents or
handoff configurations that this agent can delegate tasks to. Enables agent-to-agent
collaboration and task specialization. Defaults to None.
base_url (Optional[str], optional): Base URL for custom LLM endpoints (e.g., Ollama).
If provided, automatically creates a custom LLM instance. Defaults to None.
api_key (Optional[str], optional): API key for LLM provider. If not provided,
falls back to environment variables. Defaults to None.
name: Agent name for identification and logging. Defaults to "Agent".
role: Role/job title defining expertise (e.g., "Data Analyst").
goal: Primary objective the agent aims to achieve.
backstory: Background context shaping personality and decisions.
instructions: Direct instructions (overrides role/goal/backstory). Recommended for simple agents.
llm: Model name string ("gpt-4o", "anthropic/claude-3-sonnet") or LLM object.
Defaults to OPENAI_MODEL_NAME env var or "gpt-4o-mini".
model: Alias for llm parameter.
function_calling_llm: Dedicated LLM for function calling. Deprecated: use llm=.
llm_config: LLM configuration dict. Deprecated: use llm=.
base_url: Custom LLM endpoint URL (e.g., for Ollama). Kept separate for auth.
api_key: API key for LLM provider. Kept separate for auth.
tools: List of tools, functions, callables, or MCP instances.
allow_delegation: Allow task delegation to other agents. Defaults to False.
allow_code_execution: Enable code execution during tasks. Defaults to False.
code_execution_mode: "safe" (restricted) or "unsafe" (full access). Defaults to "safe".
handoffs: List of Agent or Handoff objects for agent-to-agent collaboration.
auto_save: Session name for automatic session saving.
rate_limiter: Rate limiter instance for API call throttling.
memory: Memory system configuration. Accepts:
- bool: True enables defaults, False disables
- MemoryConfig: Custom configuration
- MemoryManager: Pre-configured instance
knowledge: Knowledge sources. Accepts:
- bool: True enables defaults
- List[str]: File paths, URLs, or text content
- KnowledgeConfig: Custom configuration
planning: Planning mode. Accepts:
- bool: True enables with defaults
- PlanningConfig: Custom configuration
reflection: Self-reflection. Accepts:
- bool: True enables with defaults
- ReflectionConfig: Custom configuration
guardrails: Output validation. Accepts:
- bool: True enables with defaults
- Callable: Validation function
- GuardrailConfig: Custom configuration
web: Web search/fetch. Accepts:
- bool: True enables with defaults
- WebConfig: Custom configuration
context: Context management. Accepts:
- bool: True enables with defaults
- ManagerConfig: Custom configuration
autonomy: Autonomy settings. Accepts:
- bool: True enables with defaults
- Dict: Configuration dict
- AutonomyConfig: Custom configuration
verification_hooks: List of VerificationHook instances for output verification.
output: Output configuration. Accepts:
- str: Preset name ("silent", "actions", "verbose", "json", "stream")
- OutputConfig: Custom configuration
Controls: verbose, markdown, stream, metrics, reasoning_steps
execution: Execution configuration. Accepts:
- str: Preset name ("fast", "balanced", "thorough")
- ExecutionConfig: Custom configuration
Controls: max_iter, max_rpm, max_execution_time, max_retry_limit
templates: Template configuration (TemplateConfig).
Controls: system_template, prompt_template, response_template
caching: Caching configuration. Accepts:
- bool: True enables with defaults
- CachingConfig: Custom configuration
hooks: Event hooks. Accepts:
- List: List of hook callables
- HooksConfig: Custom configuration
skills: Agent skills. Accepts:
- List[str]: Skill directory paths
- SkillsConfig: Custom configuration
Raises:
ValueError: If all of name, role, goal, backstory, and instructions are None.
ImportError: If memory or LLM features are requested but dependencies are not installed.
Note:
Many legacy parameters have been consolidated into config objects:
- verbose, markdown, stream, metrics, reasoning_steps → output=
- max_iter, max_rpm, max_execution_time, max_retry_limit → execution=
- self_reflect, max_reflect, min_reflect, reflect_llm → reflection=
- guardrail, max_guardrail_retries → guardrails=
- system_template, prompt_template, response_template → templates=
- cache, prompt_caching → caching=
- web_search, web_fetch → web=
"""
# Add check at start if memory is requested
if memory is not None:
@@ -447,14 +438,27 @@ class Agent:
# ─────────────────────────────────────────────────────────────────────
# Resolve OUTPUT param using unified resolver
# Supports: None, str preset, list [preset, overrides], OutputConfig
# DEFAULT: "silent" mode (zero overhead, fastest performance)
# Can be overridden by PRAISONAI_OUTPUT env var
# ─────────────────────────────────────────────────────────────────────
from ..config.presets import DEFAULT_OUTPUT_MODE
# Check for env var override
env_output = os.environ.get('PRAISONAI_OUTPUT', '').lower()
if env_output and env_output in OUTPUT_PRESETS and output is None:
output = env_output
# Use default output mode if not specified
if output is None:
output = DEFAULT_OUTPUT_MODE
_output_config = resolve(
value=output,
param_name="output",
config_class=OutputConfig,
presets=OUTPUT_PRESETS,
array_mode=ArrayMode.PRESET_OVERRIDE,
default=OutputConfig(),
default=OutputConfig(), # OutputConfig defaults to silent mode (zero overhead)
)
if _output_config:
verbose = _output_config.verbose
@@ -463,8 +467,24 @@ class Agent:
metrics = _output_config.metrics
reasoning_steps = _output_config.reasoning_steps
output_style = getattr(_output_config, 'style', None)
actions_trace = getattr(_output_config, 'actions_trace', False) # Default False (silent)
json_output = getattr(_output_config, 'json_output', False)
else:
verbose, markdown, stream, metrics, reasoning_steps = True, True, False, False, False
# Fallback defaults match silent mode (zero overhead)
verbose, markdown, stream, metrics, reasoning_steps = False, False, False, False, False
actions_trace = False # No callbacks by default
json_output = False
# Enable actions output mode if configured
# This registers callbacks to capture tool calls and final output
if actions_trace:
try:
from ..output.actions import enable_actions_mode, is_actions_mode_enabled
if not is_actions_mode_enabled():
output_format = "jsonl" if json_output else "text"
enable_actions_mode(redact=True, use_color=True, format=output_format)
except ImportError:
pass # Actions module not available
# ─────────────────────────────────────────────────────────────────────
# Resolve EXECUTION param using unified resolver
@@ -755,7 +775,15 @@ class Agent:
# If instructions are provided, use them to set role, goal, and backstory
if instructions:
self.name = name or "Agent"
# Only use explicitly provided name, don't auto-generate from instructions
# Auto-generation was producing confusing names like "You Are Agent" from
# instructions like "You are a helpful assistant"
if name:
self.name = name
else:
# Don't auto-generate - None signals "no explicit name provided"
# Display logic will skip Agent Info panel when name is None
self.name = None
self.role = role or "Assistant"
self.goal = goal or instructions
self.backstory = backstory or instructions
@@ -824,7 +852,7 @@ class Agent:
self.llm_instance = LLM(**llm_config)
else:
# Create LLM with model string and base_url
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
self.llm_instance = LLM(
model=model_name,
base_url=base_url,
@@ -887,7 +915,7 @@ class Agent:
) from e
# Otherwise, fall back to OpenAI environment/name
else:
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
# Handle tools parameter - ensure it's always a list
if callable(tools):
# If a single function/callable is passed, wrap it in a list
@@ -933,7 +961,7 @@ class Agent:
self.min_reflect = min_reflect
self.reflect_prompt = reflect_prompt
# Use the same model selection logic for reflect_llm
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
self._console = None # Lazy load console when needed
# Initialize system prompt
@@ -1065,6 +1093,16 @@ Your Goal: {self.goal}
self._context_param = context # Store raw param for lazy init
self._context_manager = None # Lazy initialized on first use
self._context_manager_initialized = False
# Action trace mode - handled via display callbacks, not separate emitter
self._actions_trace = actions_trace
# Telemetry
try:
from ..telemetry import get_telemetry
self._telemetry = get_telemetry()
except (ImportError, AttributeError):
self._telemetry = None
@property
def auto_memory(self):
@@ -1720,7 +1758,7 @@ Your Goal: {self.goal}
elif hasattr(self, 'llm') and self.llm:
model_name = self.llm
else:
model_name = "gpt-5-nano"
model_name = "gpt-4o-mini"
return supports_web_search(model_name)
@@ -1742,7 +1780,7 @@ Your Goal: {self.goal}
elif hasattr(self, 'llm') and self.llm:
model_name = self.llm
else:
model_name = "gpt-5-nano"
model_name = "gpt-4o-mini"
return supports_web_fetch(model_name)
@@ -1986,7 +2024,7 @@ Your Goal: {self.goal}
Returns:
The LLM model/instance being used by this agent.
- For standard models: returns the model string (e.g., "gpt-5-nano")
- For standard models: returns the model string (e.g., "gpt-4o-mini")
- For custom LLM instances: returns the LLM instance object
- For provider models: returns the LLM instance object
"""
@@ -1996,7 +2034,7 @@ Your Goal: {self.goal}
return self.llm
else:
# Default fallback
return "gpt-5-nano"
return "gpt-4o-mini"
def _ensure_knowledge_processed(self):
"""Ensure knowledge is initialized and processed when first accessed."""
@@ -2817,6 +2855,10 @@ Your Goal: {self.goal}"""
"""
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
# Notify callbacks that tool is being called (for dynamic status updates)
from ..main import execute_sync_callback
execute_sync_callback('tool_call', tool_name=function_name, tool_input=arguments)
# Set up injection context for tools with Injected parameters
from ..tools.injected import AgentState, with_injection_context
state = AgentState(
@@ -3696,7 +3738,7 @@ Your Goal: {self.goal}"""
if self.verbose:
logging.debug(f"Agent {self.name} final response: {response_text}")
# Return only reasoning content if reasoning_steps is True
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content'):
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content') and response.choices[0].message.reasoning_content:
# Apply guardrail to reasoning content
try:
validated_reasoning = self._apply_guardrail_with_retry(response.choices[0].message.reasoning_content, original_prompt, temperature, tools, task_name, task_description, task_id)
@@ -3848,6 +3890,10 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
async def achat(self, prompt: str, temperature=1.0, tools=None, output_json=None, output_pydantic=None, reasoning_steps=False, task_name=None, task_description=None, task_id=None):
"""Async version of chat method with self-reflection support."""
# Track execution via telemetry
if hasattr(self, '_telemetry') and self._telemetry:
self._telemetry.track_agent_execution(self.name, success=True)
# Reset the final display flag for each new conversation
self._final_display_shown = False
@@ -4221,7 +4267,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
final_response = process_stream_chunks(chunks)
# Return only reasoning content if reasoning_steps is True
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content'):
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content') and final_response.choices[0].message.reasoning_content:
return final_response.choices[0].message.reasoning_content
return final_response.choices[0].message.content if final_response else full_response_text
@@ -4235,16 +4281,94 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
return None
async def arun(self, prompt: str, **kwargs):
"""Async alias for astart() method"""
return await self.astart(prompt, **kwargs)
"""Async version of run() - silent, non-streaming, returns structured result.
Production-friendly async execution. Does not stream or display output.
Args:
prompt: The input prompt to process
**kwargs: Additional arguments passed to achat()
Returns:
The agent's response as a string
"""
# Remove stream from kwargs since achat() doesn't accept it
kwargs.pop('stream', None)
return await self.achat(prompt, **kwargs)
async def astart(self, prompt: str, **kwargs):
"""Async version of start method"""
"""Async version of start() - interactive, streaming-aware.
Beginner-friendly async execution. Streams by default when in TTY.
Args:
prompt: The input prompt to process
**kwargs: Additional arguments passed to achat()
Returns:
The agent's response as a string
"""
import sys
# Determine streaming behavior (same logic as start())
stream_requested = kwargs.get('stream')
if stream_requested is None:
if getattr(self, 'stream', None) is not None:
stream_requested = self.stream
else:
stream_requested = sys.stdout.isatty()
kwargs['stream'] = stream_requested
return await self.achat(prompt, **kwargs)
def run(self, prompt: str, **kwargs):
"""Alias for start() method"""
return self.start(prompt, **kwargs)
"""Execute agent silently and return structured result.
Production-friendly execution. Always uses silent mode with no streaming
or verbose display, regardless of TTY status. Use this for programmatic,
scripted, or automated usage where you want just the result.
Args:
prompt: The input prompt to process
**kwargs: Additional arguments:
- stream (bool): Force streaming if True. Default: False
- output (str): Output preset override (rarely needed)
Returns:
The agent's response as a string
Example:
```python
agent = Agent(instructions="You are helpful")
result = agent.run("What is 2+2?") # Silent, returns "4"
print(result)
```
Note:
Unlike .start() which enables verbose output in TTY for interactive
use, .run() is always silent. This makes it suitable for:
- Production pipelines
- Automated scripts
- Background processing
- API endpoints
"""
# Production defaults: no streaming, no display
if 'stream' not in kwargs:
kwargs['stream'] = False
# Load history context
self._load_history_context()
# Check if planning mode is enabled
if self.planning:
result = self._start_with_planning(prompt, **kwargs)
else:
result = self.chat(prompt, **kwargs)
# Auto-save session if enabled
self._auto_save_session()
return result
def _get_planning_agent(self):
"""Lazy load PlanningAgent for planning mode."""
@@ -4378,26 +4502,290 @@ Write the complete compiled report:"""
# Chat history is preserved in self.chat_history (no action needed)
def start(self, prompt: str, **kwargs):
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
# Load history from past sessions (now handled via context= param)
def start(self, prompt: str = None, **kwargs):
"""Start the agent interactively with verbose output.
Beginner-friendly execution. Defaults to verbose output with streaming
when running in a TTY. Use this for interactive/terminal usage where
you want to see output in real-time with rich formatting.
Args:
prompt: The input prompt to process. If not provided, uses the
agent's instructions as the task (useful when instructions
already describe what the agent should do).
**kwargs: Additional arguments:
- stream (bool | None): Override streaming. None = auto-detect TTY
- output (str): Output preset override (e.g., "silent", "verbose")
Returns:
- If streaming: Generator yielding response chunks
- If not streaming: The complete response as a string
Example:
```python
# Minimal usage - instructions IS the task
agent = Agent(instructions="Research AI trends and summarize")
result = agent.start() # Uses instructions as task
# With explicit prompt (overrides/adds to instructions)
agent = Agent(instructions="You are a helpful assistant")
result = agent.start("What is 2+2?") # Uses prompt as task
```
Note:
Unlike .run() which is always silent (production use), .start()
enables verbose output by default when in a TTY for beginner-friendly
interactive use. Use .run() for programmatic/scripted usage.
"""
import sys
# If no prompt provided, use instructions as the task
if prompt is None:
prompt = self.instructions or "Hello"
# Load history from past sessions
self._load_history_context()
# Check if planning mode is enabled
if self.planning:
result = self._start_with_planning(prompt, **kwargs)
elif kwargs.get('stream', getattr(self, 'stream', False)):
# Return a generator for streaming response
result = self._start_stream(prompt, **kwargs)
else:
# Return regular chat response for backward compatibility
kwargs['stream'] = False
result = self.chat(prompt, **kwargs)
# Determine if we're in an interactive TTY
is_tty = sys.stdout.isatty()
# Determine streaming behavior
# Priority: explicit kwarg > agent's stream attribute > TTY detection
stream_requested = kwargs.get('stream')
if stream_requested is None:
# Check agent's stream attribute first
if getattr(self, 'stream', None) is not None:
stream_requested = self.stream
else:
# Auto-detect: stream if stdout is a TTY (interactive terminal)
stream_requested = is_tty
# ─────────────────────────────────────────────────────────────────────
# Enable verbose output in TTY for beginner-friendly interactive use
# Priority: explicit output= kwarg > TTY auto-verbose > agent's default
# ─────────────────────────────────────────────────────────────────────
original_verbose = self.verbose
original_markdown = self.markdown
output_override = kwargs.pop('output', None) # Pop to prevent passing to chat()
try:
# If running in TTY and no explicit output override, enable verbose
if is_tty and output_override is None:
self.verbose = True
self.markdown = True
elif output_override:
# Apply explicit output preset for this call
from ..config.presets import OUTPUT_PRESETS
if output_override in OUTPUT_PRESETS:
preset = OUTPUT_PRESETS[output_override]
self.verbose = preset.get('verbose', False)
self.markdown = preset.get('markdown', False)
# Check if planning mode is enabled
if self.planning:
result = self._start_with_planning(prompt, **kwargs)
elif stream_requested:
# Return a generator for streaming response
kwargs['stream'] = True
result = self._start_stream(prompt, **kwargs)
else:
# Return regular chat response with animated working status
kwargs['stream'] = False
# Show animated status during LLM call if verbose
if self.verbose and is_tty:
from rich.live import Live
from rich.console import Group
from rich.status import Status
from ..main import PRAISON_COLORS, sync_display_callbacks
import threading
import time as time_module
console = Console()
start_time = time_module.time()
# ─────────────────────────────────────────────────────────────
# Shared state for dynamic status messages (thread-safe)
# Updated by callbacks during tool execution
# ─────────────────────────────────────────────────────────────
current_status = ["Analyzing query..."]
tools_called = []
# Register a temporary callback to track tool calls
def status_tool_callback(**kwargs):
tool_name = kwargs.get('tool_name', '')
if tool_name:
tools_called.append(tool_name)
current_status[0] = f"Calling tool: {tool_name}..."
# Store original callback and register ours
original_tool_callback = sync_display_callbacks.get('tool_call')
sync_display_callbacks['tool_call'] = status_tool_callback
# Animation state
result_holder = [None]
error_holder = [None]
# Temporarily disable verbose in chat to prevent duplicate output
original_verbose_chat = self.verbose
def run_chat():
try:
# Suppress verbose during animation - we'll display result ourselves
self.verbose = False
current_status[0] = "Sending to LLM..."
result_holder[0] = self.chat(prompt, **kwargs)
current_status[0] = "Finalizing response..."
except Exception as e:
error_holder[0] = e
finally:
self.verbose = original_verbose_chat
# Restore original callback
if original_tool_callback:
sync_display_callbacks['tool_call'] = original_tool_callback
elif 'tool_call' in sync_display_callbacks:
del sync_display_callbacks['tool_call']
# Start chat in background thread
chat_thread = threading.Thread(target=run_chat)
chat_thread.start()
from rich.panel import Panel
from rich.text import Text
from rich.markdown import Markdown
# ─────────────────────────────────────────────────────────────
# Smart Agent Info: Only show if user provided meaningful info
# Skip if using defaults ("Agent"/"Assistant") with single agent
# ─────────────────────────────────────────────────────────────
has_custom_name = self.name and self.name not in ("Agent", "agent", None, "")
has_custom_role = self.role and self.role not in ("Assistant", "AI Assistant", "assistant", None, "")
has_tools = bool(self.tools)
show_agent_info = has_custom_name or has_custom_role or has_tools
agent_panel = None
if show_agent_info:
agent_info_parts = []
if has_custom_name:
agent_info_parts.append(f"[bold {PRAISON_COLORS['task']}]👤 Agent:[/] [{PRAISON_COLORS['agent_text']}]{self.name}[/]")
if has_custom_role:
agent_info_parts.append(f"[bold {PRAISON_COLORS['metrics']}]Role:[/] [{PRAISON_COLORS['agent_text']}]{self.role}[/]")
if has_tools:
tools_list = [t.__name__ if hasattr(t, '__name__') else str(t) for t in self.tools][:5]
tools_str = ", ".join(f"[italic {PRAISON_COLORS['response']}]{tool}[/]" for tool in tools_list)
agent_info_parts.append(f"[bold {PRAISON_COLORS['agent']}]Tools:[/] {tools_str}")
agent_panel = Panel(
"\n".join(agent_info_parts),
border_style=PRAISON_COLORS["agent"],
title="[bold]Agent Info[/]",
title_align="left",
padding=(1, 2)
)
# Create task panel
task_panel = Panel.fit(
Markdown(prompt) if self.markdown else Text(prompt),
title="Task",
border_style=PRAISON_COLORS["task"]
)
# Show initial panels (agent info if applicable, then task)
if agent_panel:
console.print(agent_panel)
console.print(task_panel)
# ─────────────────────────────────────────────────────────────
# Animate with Rich.Status showing DYNAMIC status from callbacks
# ─────────────────────────────────────────────────────────────
with console.status(
f"[bold yellow]Working...[/] {current_status[0]}",
spinner="dots",
spinner_style="yellow"
) as status:
last_status = current_status[0]
while chat_thread.is_alive():
time_module.sleep(0.15) # More responsive updates
# Only update if status changed (reduces flicker)
if current_status[0] != last_status:
last_status = current_status[0]
status.update(f"[bold yellow]Working...[/] {current_status[0]}")
# Calculate elapsed time
elapsed = time_module.time() - start_time
# Re-raise any error from chat
if error_holder[0]:
raise error_holder[0]
result = result_holder[0]
# Display response panel
response_panel = Panel.fit(
Markdown(str(result)) if self.markdown else Text(str(result)),
title=f"Response ({elapsed:.1f}s)",
border_style=PRAISON_COLORS["response"]
)
console.print(response_panel)
# Show tool activity summary if tools were called
if tools_called:
tools_summary = ", ".join(tools_called)
console.print(f"[dim]🔧 Tools used: {tools_summary}[/dim]")
else:
result = self.chat(prompt, **kwargs)
# Auto-save session if enabled
self._auto_save_session()
return result
finally:
# Restore original output settings
self.verbose = original_verbose
self.markdown = original_markdown
def iter_stream(self, prompt: str, **kwargs):
"""Stream agent response as an iterator of chunks.
App-friendly streaming. Yields response chunks without terminal display.
Use this for building custom UIs or processing streams programmatically.
Args:
prompt: The input prompt to process
**kwargs: Additional arguments:
- display (bool): Show terminal output. Default: False
- output (str): Output preset override
Yields:
str: Response chunks as they are generated
Example:
```python
agent = Agent(instructions="You are helpful")
# Process stream programmatically
full_response = ""
for chunk in agent.iter_stream("Tell me a story"):
full_response += chunk
# Custom processing here
# Or collect all at once
response = "".join(agent.iter_stream("Hello"))
```
"""
# Load history context
self._load_history_context()
# Force streaming, no display by default (app-friendly)
kwargs['stream'] = True
# Use the internal streaming generator
for chunk in self._start_stream(prompt, **kwargs):
yield chunk
# Auto-save session if enabled
self._auto_save_session()
return result
def _load_history_context(self):
"""Load history from past sessions into context.
@@ -434,8 +434,7 @@ This report contains all agent interactions and outputs from a complete ContextA
Provide comprehensive analysis that follows the PRD template principles and enables
AI assistants to implement features that perfectly match existing codebase patterns.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
prompt = f"""Analyze this gitingest codebase digest following PRD template methodology:
@@ -515,8 +514,7 @@ codebase style and architecture following PRD template principles."""
role="Expert Manual Codebase Analysis Specialist",
goal="Perform comprehensive manual codebase analysis following PRD methodology",
instructions="""Analyze the codebase samples following PRD template methodology for complete understanding.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Format comprehensive sample
@@ -566,8 +564,7 @@ Analyze following PRD principles to extract patterns, conventions, and architect
6. Design pattern implementations
7. Code complexity metrics
8. API and interface patterns""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Perform AST analysis on Python files
@@ -624,8 +621,7 @@ Extract comprehensive patterns that follow PRD template principles for implement
For each pattern, provide the pattern name, where it's used, and how to replicate it
following PRD template principles.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Get representative code samples
@@ -674,8 +670,7 @@ patterns and best practices for first-try success."""
goal="Analyze testing patterns for comprehensive validation framework design",
instructions="""Analyze testing patterns to understand validation approaches and create
comprehensive test frameworks following PRD methodology.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Get test files
@@ -801,8 +796,7 @@ Extract testing patterns for validation framework creation following PRD princip
Confidence level for one-pass implementation
Generate PRPs following this EXACT structure for first-try implementation success.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
prompt = f"""Generate a comprehensive Product Requirements Prompt (PRP) following the EXACT PRD template structure:
@@ -850,8 +844,7 @@ on the first try following PRD template principles."""
6. CODE QUALITY: Complexity analysis, maintainability
7. DOCUMENTATION VALIDATION: Documentation completeness
8. DEPENDENCY VALIDATION: Dependency analysis and security""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Analyze existing validation patterns
@@ -897,8 +890,7 @@ following PRD template principles."""
instructions="""Compile all available documentation following PRD methodology including:
README files, API documentation, setup guides, architecture docs, and any other
relevant documentation that provides context for implementation.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Get documentation files
@@ -943,8 +935,7 @@ following PRD template principles."""
instructions="""Analyze integration points following PRD methodology including:
APIs, databases, external services, configuration points, and any other
integration requirements that affect implementation.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Get configuration and dependency files
@@ -1004,8 +995,7 @@ following PRD template principles."""
8. DOCUMENTATION UPDATES: Documentation to create/update
9. INTEGRATION STEPS: How to integrate with existing systems
10. VALIDATION CHECKPOINTS: Validation steps at each phase""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
prompt = f"""Create detailed implementation blueprint following PRD methodology:
@@ -1302,8 +1292,7 @@ Every agent interaction has been saved for full audit trail and reproducibility.
GOAL: [extracted implementation goal]
Be precise and extract only what is explicitly mentioned or clearly implied.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
prompt = f"""Parse this user input to extract codebase and goal:
@@ -1536,8 +1525,7 @@ Note: Detailed function/class metadata not available due to content access limit
5. Documentation topics
Make the output easy for a file selection agent to understand which files contain what functionality.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
structure_text = basic_structure.get('structure', '')
@@ -1770,8 +1758,7 @@ Focus on creating clear, structured metadata that will help with intelligent fil
["README.md", "src/auth/login.py", "config/settings.py", ...]
Maximum 50 files for efficient analysis.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
# Prepare enhanced structure information
@@ -1923,8 +1910,7 @@ Maximum 50 files.""".format(goal=goal)
8. EXAMPLES: Similar features that can guide {goal} implementation
Since these files were pre-selected for relevance, provide deep analysis of how each contributes to implementing: {goal}""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
prompt = f"""Analyze these carefully selected repository files for implementing: {goal}
@@ -2024,8 +2010,7 @@ Since these files were pre-selected for relevance, explain how each contributes
- Success criteria for {goal}
Focus everything on successfully implementing: {goal}""",
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
verbose=getattr(self, 'verbose', True)
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
)
chunk_info = ""
@@ -2303,13 +2288,13 @@ def create_context_agent(llm: Optional[Union[str, Any]] = None, **kwargs) -> Con
Factory function to create a ContextAgent following Context Engineering and PRD methodology.
Args:
llm: Language model to use (e.g., "gpt-5-nano", "claude-3-haiku")
llm: Language model to use (e.g., "gpt-4o-mini", "claude-3-haiku")
**kwargs: Additional arguments to pass to ContextAgent constructor
Returns:
ContextAgent: Configured ContextAgent for comprehensive context generation following PRD principles
"""
if llm is None:
llm = "gpt-5-nano"
llm = "gpt-4o-mini"
return ContextAgent(llm=llm, **kwargs)
@@ -44,7 +44,7 @@ class RouterAgent(Agent):
# Initialize model router
self.model_router = model_router or ModelRouter()
self.routing_strategy = routing_strategy
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
# Process models configuration
self.available_models = self._process_models_config(models)
@@ -363,7 +363,7 @@ class Agents:
self.variables = variables if variables else {}
# Check for manager_llm in environment variable if not provided
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
# Set logger level based on verbose
if _verbose >= 5:
@@ -466,10 +466,16 @@ class Agents:
if self.shared_memory:
for task in tasks:
if not task.memory:
task.memory = self.shared_memory
logger.info(f"Assigned shared memory to task {task.id}")
# Telemetry
try:
from ..telemetry import get_telemetry
self._telemetry = get_telemetry()
except (ImportError, AttributeError):
self._telemetry = None
def add_task(self, task):
task_id = self.task_id_counter
task.id = task_id
@@ -821,13 +827,17 @@ Context:
await loop.run_in_executor(None, self.run_task, task_id)
async def astart(self, content=None, return_dict=False, **kwargs):
"""Async version of start method
"""Async version of start method.
Args:
content: Optional content to add to all tasks' context
return_dict: If True, returns the full results dictionary instead of only the final response
**kwargs: Additional arguments
"""
# Track execution via telemetry
if hasattr(self, '_telemetry') and self._telemetry:
self._telemetry.track_agent_execution(self.name, success=True, async_mode=True)
if content:
# Add content to context of all tasks
for task in self.tasks.values():
@@ -1193,30 +1203,146 @@ Context:
return str(agent[0])
return None
def start(self, content=None, return_dict=False, **kwargs):
"""Start agent execution with optional content and config
def start(self, content=None, return_dict=False, output=None, **kwargs):
"""Start agent execution with verbose output (beginner-friendly).
Shows Rich panels with workflow progress when in TTY. Use .run() for
silent execution in production/scripts.
Args:
content: Optional content to add to all tasks' context
return_dict: If True, returns the full results dictionary instead of only the final response
return_dict: If True, returns the full results dictionary
output: Output preset - "silent", "verbose", "normal", etc.
Default in TTY: "verbose" (shows progress)
Default non-TTY: "silent"
**kwargs: Additional arguments
Example:
```python
# Interactive - shows Rich panels
agents = Agents(agents=[agent1, agent2])
result = agents.start() # Verbose output by default
# Force silent mode
result = agents.start(output="silent")
```
"""
# Track execution via telemetry
if hasattr(self, '_telemetry') and self._telemetry:
self._telemetry.track_agent_execution(self.name, success=True)
import sys
from ..main import PRAISON_COLORS
# Determine if we're in an interactive TTY
is_tty = sys.stdout.isatty()
# Resolve output mode (TTY-aware)
if output is None:
# Default: verbose in TTY (beginner-friendly), silent otherwise
# Note: Don't check self.verbose here - start() is for interactive use
show_verbose = is_tty
elif output == "silent":
show_verbose = False
elif output in ("verbose", "debug", "normal"):
show_verbose = True
else:
show_verbose = is_tty
# Add content to context if provided
if content:
# Add content to context of all tasks
for task in self.tasks.values():
if isinstance(content, (str, list)):
# If context is empty, initialize it
if not task.context:
task.context = []
# Add content to context
task.context.append(content)
# Planning Mode: Create plan and todo list before execution
if self.planning:
self._run_with_planning()
# ─────────────────────────────────────────────────────────────
# Verbose Mode: Show Rich panels for multi-agent workflow
# ─────────────────────────────────────────────────────────────
if show_verbose and is_tty:
from rich.panel import Panel
from rich.text import Text
console = Console()
import time as time_module
# Show workflow overview panel
agent_names = " → ".join([a.name for a in self.agents])
workflow_info = f"[bold {PRAISON_COLORS['metrics']}]Process:[/] {self.process}\n"
workflow_info += f"[bold {PRAISON_COLORS['metrics']}]Agents:[/] {agent_names}"
console.print(Panel(
workflow_info,
title="[bold]Multi-Agent Workflow[/]",
border_style=PRAISON_COLORS["agent"],
padding=(1, 2)
))
console.print()
# Execute tasks with verbose output
total_agents = len(self.agents)
workflow_start_time = time_module.time()
for idx, (task_id, task) in enumerate(self.tasks.items(), 1):
agent = task.agent
agent_name = agent.name if agent else "Unknown"
agent_model = getattr(agent, 'llm', 'gpt-4o-mini') if agent else "unknown"
# Show agent task panel with model info
task_desc = task.description[:100] + "..." if len(task.description) > 100 else task.description
panel_content = f"[bold {PRAISON_COLORS['task_text']}]📋 Task:[/] {task_desc}\n"
panel_content += f"[dim]🤖 Model: {agent_model}[/dim]"
console.print(Panel.fit(
panel_content,
title=f"[bold]Agent [{idx}/{total_agents}]: {agent_name}[/]",
border_style=PRAISON_COLORS["task"]
))
# Execute with timing and status
start_time = time_module.time()
# Show working spinner
with console.status(
f"[bold yellow]Working...[/] {agent_name} generating response...",
spinner="dots",
spinner_style="yellow"
):
# Run the task
if self.planning:
self._run_with_planning()
break # Planning mode handles all tasks
else:
self.run_task(task_id)
elapsed = time_module.time() - start_time
# Show response panel - FULL response, no truncation
result = self.get_task_result(task_id)
if result:
response_text = str(result.raw)
# No truncation - show full response in verbose mode
console.print(Panel(
Text(response_text),
title=f"[bold]Agent [{idx}/{total_agents}] Complete ({elapsed:.1f}s)[/]",
border_style=PRAISON_COLORS["response"],
padding=(1, 2)
))
console.print()
# Workflow summary panel
total_elapsed = time_module.time() - workflow_start_time
console.print(Panel.fit(
f"[bold green]Total Time:[/] {total_elapsed:.1f}s\n"
f"[bold green]Agents Run:[/] {total_agents}/{total_agents}",
title="[bold]✅ Workflow Complete[/]",
border_style="green"
))
console.print()
else:
# Run tasks as before
self.run_all_tasks()
# Silent mode: Run tasks without display
if self.planning:
self._run_with_planning()
else:
self.run_all_tasks()
# Auto-display token metrics if any agent has metrics=True
metrics_enabled = any(getattr(agent, 'metrics', False) for agent in self.agents)
@@ -1224,10 +1350,8 @@ Context:
try:
self.display_token_usage()
except (ImportError, AttributeError) as e:
# Token tracking not available or not properly configured
logging.debug(f"Could not auto-display token usage: {e}")
except Exception as e:
# Log unexpected errors for debugging
logging.debug(f"Unexpected error in token metrics display: {e}")
# Get results
@@ -1238,7 +1362,6 @@ Context:
# By default, return only the final agent's response
if not return_dict:
# Get the last task (assuming sequential processing)
task_ids = list(self.tasks.keys())
if task_ids:
last_task_id = task_ids[-1]
@@ -1246,12 +1369,21 @@ Context:
if last_result:
return last_result.raw
# Return full results dict if return_dict is True or if no final result was found
return results
def run(self, content=None, return_dict=False, **kwargs):
"""Alias for start() method to provide consistent API with Agent class"""
return self.start(content=content, return_dict=return_dict, **kwargs)
"""Run agents silently (production use).
Unlike .start() which shows verbose output, .run() executes silently
for programmatic/production use.
Args:
content: Optional content to add to all tasks' context
return_dict: If True, returns the full results dictionary
**kwargs: Additional arguments
"""
# Always run silently - no verbose output
return self.start(content=content, return_dict=return_dict, output="silent", **kwargs)
def set_state(self, key: str, value: Any) -> None:
"""Set a state value"""
@@ -103,7 +103,7 @@ class AutoAgents(Agents):
self.self_reflect = self_reflect
self.max_reflect = max_reflect
self.min_reflect = min_reflect
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
self.function_calling_llm = function_calling_llm
self.code_execution_mode = code_execution_mode
self.embedder_config = embedder_config
@@ -44,12 +44,79 @@ class MemoryBackend(str, Enum):
MONGODB = "mongodb"
class LearnScope(str, Enum):
"""Scope for learning data visibility.
PRIVATE: Learning data is private to this user/agent (default, safest)
SHARED: Learning data is shared with all agents
"""
PRIVATE = "private" # Private to this user/agent
SHARED = "shared" # Shared with all agents
@dataclass
class LearnConfig:
"""
Configuration for continuous learning within memory system.
Learning captures patterns, preferences, and insights from agent interactions
to improve future responses. All learning data is stored within the memory system.
Usage:
# Simple enable
Agent(memory=MemoryConfig(learn=True))
# With specific capabilities
Agent(memory=MemoryConfig(
learn=LearnConfig(
persona=True, # User preferences
insights=True, # Observations
patterns=True, # Reusable knowledge
)
))
"""
# Learning capabilities
persona: bool = True # User preferences and profile
insights: bool = True # Observations and learnings
thread: bool = True # Session/conversation context
patterns: bool = False # Reusable knowledge patterns
decisions: bool = False # Decision logging
feedback: bool = False # Outcome signals
improvements: bool = False # Self-improvement proposals
# Scope configuration
scope: Union[str, LearnScope] = LearnScope.PRIVATE
# Storage configuration
store_path: Optional[str] = None # Custom storage path
# Maintenance settings
auto_consolidate: bool = True # Auto-consolidate learnings
retention_days: Optional[int] = None # Days to retain (None = forever)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"persona": self.persona,
"insights": self.insights,
"thread": self.thread,
"patterns": self.patterns,
"decisions": self.decisions,
"feedback": self.feedback,
"improvements": self.improvements,
"scope": self.scope.value if isinstance(self.scope, LearnScope) else self.scope,
"store_path": self.store_path,
"auto_consolidate": self.auto_consolidate,
"retention_days": self.retention_days,
}
@dataclass
class MemoryConfig:
"""
Configuration for agent memory and session management.
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db, learn
Usage:
# Simple enable (uses FileMemory)
@@ -58,12 +125,22 @@ class MemoryConfig:
# With backend
Agent(memory=MemoryConfig(backend="redis"))
# Full config
# Full config with learning
Agent(memory=MemoryConfig(
backend="sqlite",
user_id="user123",
session_id="session456",
auto_memory=True,
learn=True, # Enable continuous learning
))
# With detailed learn config
Agent(memory=MemoryConfig(
learn=LearnConfig(
persona=True,
insights=True,
patterns=True,
)
))
"""
# Backend selection
@@ -85,8 +162,17 @@ class MemoryConfig:
# Memory configuration dict (for provider-specific settings)
config: Optional[Dict[str, Any]] = None
# Continuous learning configuration
learn: Optional[Union[bool, LearnConfig]] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
learn_dict = None
if self.learn is True:
learn_dict = LearnConfig().to_dict()
elif isinstance(self.learn, LearnConfig):
learn_dict = self.learn.to_dict()
return {
"backend": self.backend.value if isinstance(self.backend, MemoryBackend) else self.backend,
"user_id": self.user_id,
@@ -94,6 +180,7 @@ class MemoryConfig:
"auto_memory": self.auto_memory,
"claude_memory": self.claude_memory,
"config": self.config,
"learn": learn_dict,
}
@@ -391,10 +478,21 @@ class OutputConfig:
Consolidates: verbose, markdown, stream, metrics, reasoning_steps, output_style
DEFAULT: output="silent" (zero overhead, fastest performance)
Usage:
# Simple preset
# Default is silent mode (no output overhead, programmatic use)
Agent(instructions="...") # Uses output="silent"
# Actions mode (tool calls + final output trace)
Agent(output="actions")
# Verbose mode with Rich panels
Agent(output="verbose")
# JSON mode for piping
Agent(output="json")
# With config
Agent(output=OutputConfig(
verbose=True,
@@ -404,11 +502,11 @@ class OutputConfig:
reasoning_steps=True,
))
"""
# Verbosity
verbose: bool = True
# Verbosity - False by default for silent mode (fastest)
verbose: bool = False
# Formatting
markdown: bool = True
# Formatting - False by default for silent mode
markdown: bool = False
# Streaming
stream: bool = False
@@ -422,6 +520,14 @@ class OutputConfig:
# Output style (custom styling)
style: Optional[Any] = None
# Actions trace mode - shows tool calls, agent lifecycle, final output
# When True, registers callbacks and outputs to stderr
# DEFAULT: False for zero overhead (silent mode)
actions_trace: bool = False
# JSON output mode - emit JSONL events for piping
json_output: bool = False
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
@@ -430,6 +536,8 @@ class OutputConfig:
"stream": self.stream,
"metrics": self.metrics,
"reasoning_steps": self.reasoning_steps,
"actions_trace": self.actions_trace,
"json_output": self.json_output,
}
@@ -857,9 +965,259 @@ SkillsParam = Union[List[str], SkillsConfig]
AutonomyParam = Union[bool, Dict[str, Any], AutonomyConfig]
# =============================================================================
# PRECEDENCE LADDER RESOLVERS
# =============================================================================
# Precedence: Instance > Config > Array > Dict > String > Bool > Default
#
# These resolvers normalize various input types into the canonical Config form.
# This allows users to pass simplified inputs while maintaining type safety.
def resolve_memory(value: MemoryParam) -> Optional[MemoryConfig]:
"""
Resolve memory= parameter following precedence ladder.
Precedence: Instance > Config > Dict > String > Bool > Default
Args:
value: Memory parameter in any supported form
Returns:
MemoryConfig if enabled, None if disabled
Examples:
>>> resolve_memory(None) # Default: disabled
None
>>> resolve_memory(False) # Explicit disable
None
>>> resolve_memory(True) # Enable with defaults
MemoryConfig(backend='file')
>>> resolve_memory("redis") # String shorthand
MemoryConfig(backend='redis')
>>> resolve_memory({"backend": "sqlite", "user_id": "alice"}) # Dict
MemoryConfig(backend='sqlite', user_id='alice')
>>> resolve_memory(MemoryConfig(backend="postgres")) # Config passthrough
MemoryConfig(backend='postgres')
"""
# Default: disabled
if value is None:
return None
# Bool: False = disabled, True = defaults
if value is False:
return None
if value is True:
return MemoryConfig()
# String: backend shorthand
if isinstance(value, str):
try:
backend = MemoryBackend(value.lower())
except ValueError:
backend = value # Allow custom backend strings
return MemoryConfig(backend=backend)
# Dict: expand to config
if isinstance(value, dict):
# Handle backend enum conversion
backend = value.get("backend", MemoryBackend.FILE)
if isinstance(backend, str):
try:
backend = MemoryBackend(backend.lower())
except ValueError:
pass # Keep as string for custom backends
return MemoryConfig(
backend=backend,
user_id=value.get("user_id"),
session_id=value.get("session_id"),
auto_memory=value.get("auto_memory", False),
claude_memory=value.get("claude_memory", False),
db=value.get("db"),
config=value.get("config"),
)
# Config: passthrough
if isinstance(value, MemoryConfig):
return value
# Instance (MemoryManager): return as-is (caller handles)
# This is the highest precedence - user provided a pre-configured instance
return value
def resolve_knowledge(value: KnowledgeParam) -> Optional[KnowledgeConfig]:
"""
Resolve knowledge= parameter following precedence ladder.
Precedence: Instance > Config > Array > Dict > String > Bool > Default
Args:
value: Knowledge parameter in any supported form
Returns:
KnowledgeConfig if enabled, None if disabled
"""
# Default: disabled
if value is None:
return None
# Bool: False = disabled, True = defaults
if value is False:
return None
if value is True:
return KnowledgeConfig()
# String: single source
if isinstance(value, str):
return KnowledgeConfig(sources=[value])
# Array: list of sources
if isinstance(value, list):
return KnowledgeConfig(sources=value)
# Dict: expand to config
if isinstance(value, dict):
return KnowledgeConfig(
sources=value.get("sources", []),
embedder=value.get("embedder", "openai"),
embedder_config=value.get("embedder_config"),
chunking_strategy=value.get("chunking_strategy", ChunkingStrategy.SEMANTIC),
chunk_size=value.get("chunk_size", 1000),
chunk_overlap=value.get("chunk_overlap", 200),
retrieval_k=value.get("retrieval_k", 5),
retrieval_threshold=value.get("retrieval_threshold", 0.0),
rerank=value.get("rerank", False),
rerank_model=value.get("rerank_model"),
auto_retrieve=value.get("auto_retrieve", True),
)
# Config: passthrough
if isinstance(value, KnowledgeConfig):
return value
# Instance: return as-is
return value
def resolve_planning(value: PlanningParam) -> Optional[PlanningConfig]:
"""Resolve planning= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return PlanningConfig()
if isinstance(value, dict):
return PlanningConfig(**value)
if isinstance(value, PlanningConfig):
return value
return value
def resolve_reflection(value: ReflectionParam) -> Optional[ReflectionConfig]:
"""Resolve reflection= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return ReflectionConfig()
if isinstance(value, dict):
return ReflectionConfig(**value)
if isinstance(value, ReflectionConfig):
return value
return value
def resolve_guardrails(value: GuardrailParam) -> Optional[GuardrailConfig]:
"""Resolve guardrails= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return GuardrailConfig()
if callable(value):
return GuardrailConfig(validator=value)
if isinstance(value, dict):
return GuardrailConfig(**value)
if isinstance(value, GuardrailConfig):
return value
return value
def resolve_web(value: WebParam) -> Optional[WebConfig]:
"""Resolve web= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return WebConfig()
if isinstance(value, dict):
return WebConfig(**value)
if isinstance(value, WebConfig):
return value
return value
def resolve_output(value: OutputParam) -> Optional[OutputConfig]:
"""Resolve output= parameter following precedence ladder."""
if value is None:
return None
if isinstance(value, str):
try:
preset = OutputPreset(value.lower())
return OutputConfig.from_preset(preset)
except ValueError:
return None
if isinstance(value, dict):
return OutputConfig(**value)
if isinstance(value, OutputConfig):
return value
return value
def resolve_execution(value: ExecutionParam) -> Optional[ExecutionConfig]:
"""Resolve execution= parameter following precedence ladder."""
if value is None:
return None
if isinstance(value, str):
try:
preset = ExecutionPreset(value.lower())
return ExecutionConfig.from_preset(preset)
except ValueError:
return None
if isinstance(value, dict):
return ExecutionConfig(**value)
if isinstance(value, ExecutionConfig):
return value
return value
def resolve_caching(value: CachingParam) -> Optional[CachingConfig]:
"""Resolve caching= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return CachingConfig()
if isinstance(value, dict):
return CachingConfig(**value)
if isinstance(value, CachingConfig):
return value
return value
def resolve_autonomy(value: AutonomyParam) -> Optional[AutonomyConfig]:
"""Resolve autonomy= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return AutonomyConfig()
if isinstance(value, dict):
return AutonomyConfig(**value)
if isinstance(value, AutonomyConfig):
return value
return value
__all__ = [
# Enums
"MemoryBackend",
"LearnScope",
"ChunkingStrategy",
"GuardrailAction",
"WebSearchProvider",
@@ -867,6 +1225,7 @@ __all__ = [
"ExecutionPreset",
"AutonomyLevel",
# Config classes (Agent)
"LearnConfig",
"MemoryConfig",
"KnowledgeConfig",
"PlanningConfig",
@@ -900,4 +1259,15 @@ __all__ = [
"HooksParam",
"SkillsParam",
"AutonomyParam",
# Precedence ladder resolvers
"resolve_memory",
"resolve_knowledge",
"resolve_planning",
"resolve_reflection",
"resolve_guardrails",
"resolve_web",
"resolve_output",
"resolve_execution",
"resolve_caching",
"resolve_autonomy",
]
@@ -41,6 +41,37 @@ MEMORY_URL_SCHEMES: Dict[str, str] = {
# =============================================================================
OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
# Silent preset - DEFAULT for SDK
# Zero overhead, no callbacks, no I/O on hot path
# Fastest performance for programmatic use
"silent": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": False,
},
# Actions preset - opt-in observability
# Shows action trace (tool calls, agent lifecycle) + final output
# Registers callbacks, outputs to stderr
"actions": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True, # Special flag for action trace mode
},
# Plain preset - final output only, no action trace
"plain": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": False,
},
"minimal": {
"verbose": False,
"markdown": False,
@@ -69,13 +100,6 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"metrics": True,
"reasoning_steps": True,
},
"silent": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
},
# Streaming preset - enables streaming by default
"stream": {
"verbose": True,
@@ -84,8 +108,23 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"metrics": False,
"reasoning_steps": False,
},
# JSON preset - JSONL output for piping
"json": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True,
"json_output": True, # Output as JSONL
},
}
# Default output mode - can be overridden by PRAISONAI_OUTPUT env var
# "silent" = zero overhead, fastest performance (DEFAULT)
# "actions" = tool call trace + final output (opt-in observability)
DEFAULT_OUTPUT_MODE = "silent"
# =============================================================================
# Execution Presets
@@ -157,6 +157,16 @@ __all__ = [
"FastContextResult",
"FileMatch",
"LineRange",
# Artifacts (Dynamic Context Discovery)
"ArtifactRef",
"ArtifactMetadata",
"GrepMatch",
"QueueConfig",
"ArtifactStoreProtocol",
"HistoryStoreProtocol",
"TerminalLoggerProtocol",
"compute_checksum",
"generate_summary",
]
@@ -252,4 +262,11 @@ def __getattr__(name: str):
from praisonaiagents.context.fast.result import LineRange
return LineRange
# Artifacts (Dynamic Context Discovery)
if name in ("ArtifactRef", "ArtifactMetadata", "GrepMatch", "QueueConfig",
"ArtifactStoreProtocol", "HistoryStoreProtocol", "TerminalLoggerProtocol",
"compute_checksum", "generate_summary"):
from . import artifacts
return getattr(artifacts, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,580 @@
"""
Artifact Types and Protocols for Dynamic Context Discovery.
This module provides lightweight types and protocols for artifact-based
context management. Implementations live in the wrapper (praisonai).
Zero Performance Impact:
- Pure dataclasses and protocols
- No heavy imports
- No I/O at import time
Usage:
from praisonaiagents.context.artifacts import ArtifactRef, ArtifactStoreProtocol
# Create a reference to a stored artifact
ref = ArtifactRef(
path="/path/to/artifact.json",
summary="API response with 50,000 records",
size_bytes=1_200_000,
mime_type="application/json",
)
# Implement the protocol in wrapper
class FileSystemArtifactStore:
def store(self, content, metadata): ...
def load(self, ref): ...
"""
import time
import hashlib
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
@dataclass
class ArtifactRef:
"""
Reference to a stored artifact.
Used to replace large tool outputs in context with a lightweight pointer.
The agent can then use tail/grep/load operations to access content on demand.
Attributes:
path: Absolute path to the artifact file
summary: Brief description or first ~200 chars of content
size_bytes: Original content size in bytes
mime_type: Content type (e.g., "application/json", "text/plain")
checksum: SHA256 hash for integrity verification
created_at: Unix timestamp when artifact was created
artifact_id: Unique identifier for the artifact
agent_id: ID of the agent that created this artifact
run_id: ID of the run/session this artifact belongs to
tool_name: Name of the tool that produced this output (if applicable)
turn_id: Conversation turn number when artifact was created
"""
path: str
summary: str
size_bytes: int
mime_type: str = "application/octet-stream"
checksum: str = ""
created_at: float = field(default_factory=time.time)
artifact_id: str = ""
agent_id: str = ""
run_id: str = ""
tool_name: Optional[str] = None
turn_id: int = 0
def to_inline(self) -> str:
"""
Generate inline representation for context.
Returns a compact string that can be included in the conversation
context, allowing the agent to reference the artifact.
"""
size_str = self._format_size(self.size_bytes)
tool_info = f" from {self.tool_name}" if self.tool_name else ""
return f"[Artifact{tool_info}: {self.path} ({size_str}) - {self.summary}]"
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"path": self.path,
"summary": self.summary,
"size_bytes": self.size_bytes,
"mime_type": self.mime_type,
"checksum": self.checksum,
"created_at": self.created_at,
"artifact_id": self.artifact_id,
"agent_id": self.agent_id,
"run_id": self.run_id,
"tool_name": self.tool_name,
"turn_id": self.turn_id,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ArtifactRef":
"""Create from dictionary."""
return cls(
path=data.get("path", ""),
summary=data.get("summary", ""),
size_bytes=data.get("size_bytes", 0),
mime_type=data.get("mime_type", "application/octet-stream"),
checksum=data.get("checksum", ""),
created_at=data.get("created_at", time.time()),
artifact_id=data.get("artifact_id", ""),
agent_id=data.get("agent_id", ""),
run_id=data.get("run_id", ""),
tool_name=data.get("tool_name"),
turn_id=data.get("turn_id", 0),
)
@staticmethod
def _format_size(size_bytes: int) -> str:
"""Format size in human-readable form."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} MB"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
@dataclass
class ArtifactMetadata:
"""
Metadata for artifact creation and tracking.
Passed to ArtifactStore.store() to provide context about the artifact.
"""
agent_id: str = ""
run_id: str = ""
tool_name: Optional[str] = None
turn_id: int = 0
content_type: str = "auto" # auto, json, text, binary
tags: List[str] = field(default_factory=list)
extra: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"agent_id": self.agent_id,
"run_id": self.run_id,
"tool_name": self.tool_name,
"turn_id": self.turn_id,
"content_type": self.content_type,
"tags": self.tags,
"extra": self.extra,
}
@dataclass
class GrepMatch:
"""A single match from grep operation."""
line_number: int
line_content: str
context_before: List[str] = field(default_factory=list)
context_after: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"line_number": self.line_number,
"line_content": self.line_content,
"context_before": self.context_before,
"context_after": self.context_after,
}
@runtime_checkable
class ArtifactStoreProtocol(Protocol):
"""
Protocol for artifact storage backends.
Implementations should handle:
- Storing content to persistent storage
- Loading content back
- Efficient tail/grep operations for large files
- Checksum verification
- Secret redaction (if enabled)
Example implementation in wrapper:
class FileSystemArtifactStore:
def store(self, content, metadata):
# Write to ~/.praison/runs/{run_id}/artifacts/
...
"""
def store(
self,
content: Any,
metadata: ArtifactMetadata,
) -> ArtifactRef:
"""
Store content as an artifact.
Args:
content: The content to store (will be serialized)
metadata: Metadata about the artifact
Returns:
ArtifactRef pointing to the stored artifact
"""
...
def load(self, ref: ArtifactRef) -> Any:
"""
Load full content from an artifact.
Args:
ref: Reference to the artifact
Returns:
The deserialized content
"""
...
def tail(self, ref: ArtifactRef, lines: int = 50) -> str:
"""
Get the last N lines of an artifact.
Args:
ref: Reference to the artifact
lines: Number of lines to return
Returns:
String containing the last N lines
"""
...
def head(self, ref: ArtifactRef, lines: int = 50) -> str:
"""
Get the first N lines of an artifact.
Args:
ref: Reference to the artifact
lines: Number of lines to return
Returns:
String containing the first N lines
"""
...
def grep(
self,
ref: ArtifactRef,
pattern: str,
context_lines: int = 2,
max_matches: int = 50,
) -> List[GrepMatch]:
"""
Search for pattern in artifact content.
Args:
ref: Reference to the artifact
pattern: Regex pattern to search for
context_lines: Number of context lines before/after match
max_matches: Maximum number of matches to return
Returns:
List of GrepMatch objects
"""
...
def chunk(
self,
ref: ArtifactRef,
start_line: int = 1,
end_line: Optional[int] = None,
) -> str:
"""
Get a chunk of lines from an artifact.
Args:
ref: Reference to the artifact
start_line: Starting line number (1-indexed)
end_line: Ending line number (inclusive), None for end of file
Returns:
String containing the requested lines
"""
...
def delete(self, ref: ArtifactRef) -> bool:
"""
Delete an artifact.
Args:
ref: Reference to the artifact
Returns:
True if deleted successfully
"""
...
def list_artifacts(
self,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
tool_name: Optional[str] = None,
) -> List[ArtifactRef]:
"""
List artifacts matching filters.
Args:
run_id: Filter by run ID
agent_id: Filter by agent ID
tool_name: Filter by tool name
Returns:
List of matching ArtifactRef objects
"""
...
@runtime_checkable
class HistoryStoreProtocol(Protocol):
"""
Protocol for conversation history storage.
Enables loss recovery during summarization by persisting
full conversation history to artifacts.
"""
def append(
self,
message: Dict[str, Any],
agent_id: str,
run_id: str,
) -> None:
"""
Append a message to history.
Args:
message: The message dict (role, content, etc.)
agent_id: ID of the agent
run_id: ID of the run/session
"""
...
def get_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
"""
Get artifact reference for history file.
Args:
agent_id: ID of the agent
run_id: ID of the run/session
Returns:
ArtifactRef if history exists, None otherwise
"""
...
def search(
self,
query: str,
agent_id: str,
run_id: str,
max_results: int = 20,
) -> List[Dict[str, Any]]:
"""
Search history for matching messages.
Args:
query: Search query (supports regex)
agent_id: ID of the agent
run_id: ID of the run/session
max_results: Maximum results to return
Returns:
List of matching message dicts
"""
...
def get_messages(
self,
agent_id: str,
run_id: str,
start_turn: int = 0,
end_turn: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""
Get messages from history.
Args:
agent_id: ID of the agent
run_id: ID of the run/session
start_turn: Starting turn number
end_turn: Ending turn number (None for all)
Returns:
List of message dicts
"""
...
@runtime_checkable
class TerminalLoggerProtocol(Protocol):
"""
Protocol for terminal session logging.
Captures shell command outputs to artifacts for later retrieval.
"""
def log_command(
self,
command: str,
output: str,
exit_code: int,
agent_id: str,
run_id: str,
) -> ArtifactRef:
"""
Log a command execution.
Args:
command: The command that was executed
output: Combined stdout/stderr output
exit_code: Command exit code
agent_id: ID of the agent
run_id: ID of the run/session
Returns:
ArtifactRef to the logged output
"""
...
def get_session_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
"""
Get artifact reference for terminal session log.
Args:
agent_id: ID of the agent
run_id: ID of the run/session
Returns:
ArtifactRef if session exists, None otherwise
"""
...
def tail_session(
self,
agent_id: str,
run_id: str,
lines: int = 100,
) -> str:
"""
Get last N lines from terminal session.
Args:
agent_id: ID of the agent
run_id: ID of the run/session
lines: Number of lines to return
Returns:
String containing the last N lines
"""
...
def grep_session(
self,
pattern: str,
agent_id: str,
run_id: str,
max_matches: int = 50,
) -> List[GrepMatch]:
"""
Search terminal session for pattern.
Args:
pattern: Regex pattern to search for
agent_id: ID of the agent
run_id: ID of the run/session
max_matches: Maximum matches to return
Returns:
List of GrepMatch objects
"""
...
@dataclass
class QueueConfig:
"""
Configuration for tool output queuing.
Controls when and how large tool outputs are queued to artifacts
instead of being included inline in the context.
"""
enabled: bool = True
inline_max_bytes: int = 32 * 1024 # 32KB default
redact_secrets: bool = True
summary_max_chars: int = 200
checksum_algorithm: str = "sha256"
# Secret patterns to redact (regex)
secret_patterns: List[str] = field(default_factory=lambda: [
r'(?i)(api[_-]?key|apikey)["\']?\s*[:=]\s*["\']?[\w\-]+',
r'(?i)(secret|password|passwd|pwd)["\']?\s*[:=]\s*["\']?[\w\-]+',
r'(?i)(token|bearer)["\']?\s*[:=]\s*["\']?[\w\-]+',
r'(?i)(auth|authorization)["\']?\s*[:=]\s*["\']?[\w\-]+',
r'sk-[a-zA-Z0-9]{20,}', # OpenAI keys
r'ghp_[a-zA-Z0-9]{36}', # GitHub tokens
r'gho_[a-zA-Z0-9]{36}', # GitHub OAuth
r'xox[baprs]-[a-zA-Z0-9\-]+', # Slack tokens
])
def to_dict(self) -> Dict[str, Any]:
return {
"enabled": self.enabled,
"inline_max_bytes": self.inline_max_bytes,
"redact_secrets": self.redact_secrets,
"summary_max_chars": self.summary_max_chars,
"checksum_algorithm": self.checksum_algorithm,
"secret_patterns": self.secret_patterns,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "QueueConfig":
return cls(
enabled=data.get("enabled", True),
inline_max_bytes=data.get("inline_max_bytes", 32 * 1024),
redact_secrets=data.get("redact_secrets", True),
summary_max_chars=data.get("summary_max_chars", 200),
checksum_algorithm=data.get("checksum_algorithm", "sha256"),
secret_patterns=data.get("secret_patterns", cls().secret_patterns),
)
def compute_checksum(content: Union[str, bytes], algorithm: str = "sha256") -> str:
"""
Compute checksum of content.
Args:
content: String or bytes to hash
algorithm: Hash algorithm (sha256, md5, etc.)
Returns:
Hex digest of the hash
"""
if isinstance(content, str):
content = content.encode("utf-8")
hasher = hashlib.new(algorithm)
hasher.update(content)
return hasher.hexdigest()
def generate_summary(content: Any, max_chars: int = 200) -> str:
"""
Generate a brief summary of content.
Args:
content: Content to summarize
max_chars: Maximum characters in summary
Returns:
Brief summary string
"""
if isinstance(content, (dict, list)):
# For structured data, describe the structure
if isinstance(content, dict):
keys = list(content.keys())[:5]
key_str = ", ".join(str(k) for k in keys)
if len(content) > 5:
key_str += f", ... ({len(content)} total keys)"
return f"Dict with keys: {key_str}"[:max_chars]
else:
if len(content) > 0:
first_type = type(content[0]).__name__
return f"List of {len(content)} {first_type} items"[:max_chars]
return "Empty list"
# For strings, take first N chars
text = str(content)
if len(text) <= max_chars:
return text
return text[:max_chars - 3] + "..."
@@ -68,8 +68,8 @@ class LLM:
MODEL_WINDOWS = {
# OpenAI
"gpt-4": 6144, # 8,192 actual
"gpt-5-nano": 96000, # 128,000 actual
"gpt-5-nano": 96000, # 128,000 actual
"gpt-4o-mini": 96000, # 128,000 actual
"gpt-4o-mini": 96000, # 128,000 actual
"gpt-4-turbo": 96000, # 128,000 actual
"o1-preview": 96000, # 128,000 actual
"o1-mini": 96000, # 128,000 actual
@@ -1430,6 +1430,8 @@ Now provide your final answer using this result."""
task_id: Optional[str] = None,
execute_tool_fn: Optional[Callable] = None,
stream: bool = True,
stream_callback: Optional[Callable] = None,
emit_events: bool = False,
**kwargs
) -> str:
"""Enhanced get_response with all OpenAI-like features"""
@@ -1489,6 +1491,19 @@ Now provide your final answer using this result."""
# Disable litellm debug messages
litellm.set_verbose = False
# Setup StreamEvent emission if enabled
_emit = emit_events and stream_callback is not None
if _emit:
from ..streaming.events import StreamEvent, StreamEventType
_first_token_emitted = False
_last_content_time = None
_request_start_perf = time.perf_counter()
stream_callback(StreamEvent(
type=StreamEventType.REQUEST_START,
timestamp=_request_start_perf,
metadata={"model": self.model, "provider": "litellm"}
))
# Format tools if provided
formatted_tools = self._format_tools_for_litellm(tools)
@@ -1525,6 +1540,7 @@ Now provide your final answer using this result."""
max_iterations = 10 # Prevent infinite loops
iteration_count = 0
final_response_text = ""
response_text = "" # Initialize to prevent UnboundLocalError on API errors
stored_reasoning_content = None # Store reasoning content from tool execution
accumulated_tool_results = [] # Store all tool results across iterations
@@ -51,7 +51,7 @@ class ModelRouter:
DEFAULT_MODELS = [
# Lightweight/cheap models for simple tasks
ModelProfile(
name="gpt-5-nano",
name="gpt-4o-mini",
provider="openai",
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
cost_per_1k_tokens=0.00075, # Average of $0.00015 input, $0.0006 output
@@ -80,7 +80,7 @@ class ModelRouter:
# Mid-tier models for moderate complexity
ModelProfile(
name="gpt-5-nano",
name="gpt-4o-mini",
provider="openai",
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
cost_per_1k_tokens=0.0075, # Average of $0.0025 input, $0.01 output
@@ -145,7 +145,7 @@ class ModelRouter:
preferred_providers: List of preferred providers in order
"""
self.models = models or self.DEFAULT_MODELS
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
self.cost_threshold = cost_threshold
self.preferred_providers = preferred_providers or []
@@ -582,6 +582,8 @@ class OpenAIClient:
console: Optional[Console] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
stream_callback: Optional[Callable] = None,
emit_events: bool = False,
**kwargs
) -> Optional[ChatCompletion]:
"""
@@ -596,11 +598,18 @@ class OpenAIClient:
console: Console for output
display_fn: Display function for live updates
reasoning_steps: Whether to show reasoning steps
stream_callback: Optional callback for StreamEvent emission
emit_events: Whether to emit StreamEvents (requires stream_callback)
**kwargs: Additional parameters for the API
Returns:
ChatCompletion object or None if error
"""
# Lazy import StreamEvent types only when needed
_emit = emit_events and stream_callback is not None
if _emit:
from ..streaming.events import StreamEvent, StreamEventType
try:
# Default start time and console if not provided
if start_time is None:
@@ -608,6 +617,15 @@ class OpenAIClient:
if console is None:
console = self.console
# Emit REQUEST_START event
request_start_perf = time.perf_counter()
if _emit:
stream_callback(StreamEvent(
type=StreamEventType.REQUEST_START,
timestamp=request_start_perf,
metadata={"model": model, "message_count": len(messages)}
))
# Create the response stream
response_stream = self.sync_client.chat.completions.create(
model=model,
@@ -615,29 +633,81 @@ class OpenAIClient:
temperature=temperature,
tools=tools if tools else None,
stream=True,
stream_options={"include_usage": True} if _emit else None,
**kwargs
)
# Emit HEADERS_RECEIVED event (stream object created means headers received)
if _emit:
stream_callback(StreamEvent(
type=StreamEventType.HEADERS_RECEIVED,
timestamp=time.perf_counter()
))
full_response_text = ""
reasoning_content = ""
chunks = []
first_token_emitted = False
last_content_time = None
ttft_logged = False
# If display function provided, use Live display
if display_fn:
with Live(
display_fn("", start_time),
console=console,
refresh_per_second=4,
refresh_per_second=10, # Increased for more responsive streaming
transient=True,
vertical_overflow="ellipsis",
auto_refresh=True
) as live:
for chunk in response_stream:
chunks.append(chunk)
if chunk.choices[0].delta.content:
full_response_text += chunk.choices[0].delta.content
# Check for content delta
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response_text += content
last_content_time = time.perf_counter()
# Log TTFT on first token
if not ttft_logged:
ttft_ms = (last_content_time - request_start_perf) * 1000
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
ttft_logged = True
# Emit FIRST_TOKEN on first content
if _emit and not first_token_emitted:
stream_callback(StreamEvent(
type=StreamEventType.FIRST_TOKEN,
timestamp=last_content_time,
content=content
))
first_token_emitted = True
elif _emit:
# Emit DELTA_TEXT for subsequent tokens
stream_callback(StreamEvent(
type=StreamEventType.DELTA_TEXT,
timestamp=last_content_time,
content=content
))
live.update(display_fn(full_response_text, start_time))
# Handle tool calls streaming
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
for tc in chunk.choices[0].delta.tool_calls:
stream_callback(StreamEvent(
type=StreamEventType.DELTA_TOOL_CALL,
timestamp=time.perf_counter(),
tool_call={
"index": tc.index,
"id": getattr(tc, 'id', None),
"name": getattr(tc.function, 'name', None) if tc.function else None,
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
}
))
# Update live display with reasoning content if enabled
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
rc = chunk.choices[0].delta.reasoning_content
@@ -648,9 +718,61 @@ class OpenAIClient:
# Clear the last generating display with a blank line
console.print()
else:
# Just collect chunks without display
# Just collect chunks without display, but still emit events
for chunk in response_stream:
chunks.append(chunk)
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
last_content_time = time.perf_counter()
# Log TTFT on first token
if not ttft_logged:
ttft_ms = (last_content_time - request_start_perf) * 1000
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
ttft_logged = True
if _emit and not first_token_emitted:
stream_callback(StreamEvent(
type=StreamEventType.FIRST_TOKEN,
timestamp=last_content_time,
content=content
))
first_token_emitted = True
elif _emit:
# Emit DELTA_TEXT for subsequent tokens
stream_callback(StreamEvent(
type=StreamEventType.DELTA_TEXT,
timestamp=last_content_time,
content=content
))
# Handle tool calls streaming
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
for tc in chunk.choices[0].delta.tool_calls:
stream_callback(StreamEvent(
type=StreamEventType.DELTA_TOOL_CALL,
timestamp=time.perf_counter(),
tool_call={
"index": tc.index,
"id": getattr(tc, 'id', None),
"name": getattr(tc.function, 'name', None) if tc.function else None,
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
}
))
# Emit LAST_TOKEN and STREAM_END events
if _emit:
if last_content_time:
stream_callback(StreamEvent(
type=StreamEventType.LAST_TOKEN,
timestamp=last_content_time
))
stream_callback(StreamEvent(
type=StreamEventType.STREAM_END,
timestamp=time.perf_counter(),
metadata={"chunk_count": len(chunks)}
))
final_response = process_stream_chunks(chunks)
return final_response
@@ -669,6 +791,8 @@ class OpenAIClient:
console: Optional[Console] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
stream_callback: Optional[Callable] = None,
emit_events: bool = False,
**kwargs
) -> Optional[ChatCompletion]:
"""
@@ -683,11 +807,18 @@ class OpenAIClient:
console: Console for output
display_fn: Display function for live updates
reasoning_steps: Whether to show reasoning steps
stream_callback: Optional callback for StreamEvent emission (can be sync or async)
emit_events: Whether to emit StreamEvents (requires stream_callback)
**kwargs: Additional parameters for the API
Returns:
ChatCompletion object or None if error
"""
# Lazy import StreamEvent types only when needed
_emit = emit_events and stream_callback is not None
if _emit:
from ..streaming.events import StreamEvent, StreamEventType
try:
# Default start time and console if not provided
if start_time is None:
@@ -695,6 +826,19 @@ class OpenAIClient:
if console is None:
console = self.console
# Emit REQUEST_START event
request_start_perf = time.perf_counter()
if _emit:
event = StreamEvent(
type=StreamEventType.REQUEST_START,
timestamp=request_start_perf,
metadata={"model": model, "message_count": len(messages)}
)
if asyncio.iscoroutinefunction(stream_callback):
await stream_callback(event)
else:
stream_callback(event)
# Create the response stream
response_stream = await self.async_client.chat.completions.create(
model=model,
@@ -702,29 +846,81 @@ class OpenAIClient:
temperature=temperature,
tools=tools if tools else None,
stream=True,
stream_options={"include_usage": True} if _emit else None,
**kwargs
)
# Emit HEADERS_RECEIVED event
if _emit:
event = StreamEvent(
type=StreamEventType.HEADERS_RECEIVED,
timestamp=time.perf_counter()
)
if asyncio.iscoroutinefunction(stream_callback):
await stream_callback(event)
else:
stream_callback(event)
full_response_text = ""
reasoning_content = ""
chunks = []
first_token_emitted = False
last_content_time = None
async def emit_event(event: 'StreamEvent'):
if asyncio.iscoroutinefunction(stream_callback):
await stream_callback(event)
else:
stream_callback(event)
# If display function provided, use Live display
if display_fn:
with Live(
display_fn("", start_time),
console=console,
refresh_per_second=4,
refresh_per_second=10,
transient=True,
vertical_overflow="ellipsis",
auto_refresh=True
) as live:
async for chunk in response_stream:
chunks.append(chunk)
if chunk.choices[0].delta.content:
full_response_text += chunk.choices[0].delta.content
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response_text += content
last_content_time = time.perf_counter()
if _emit and not first_token_emitted:
await emit_event(StreamEvent(
type=StreamEventType.FIRST_TOKEN,
timestamp=last_content_time,
content=content
))
first_token_emitted = True
elif _emit:
await emit_event(StreamEvent(
type=StreamEventType.DELTA_TEXT,
timestamp=last_content_time,
content=content
))
live.update(display_fn(full_response_text, start_time))
# Handle tool calls streaming
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
for tc in chunk.choices[0].delta.tool_calls:
await emit_event(StreamEvent(
type=StreamEventType.DELTA_TOOL_CALL,
timestamp=time.perf_counter(),
tool_call={
"index": tc.index,
"id": getattr(tc, 'id', None),
"name": getattr(tc.function, 'name', None) if tc.function else None,
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
}
))
# Update live display with reasoning content if enabled
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
rc = chunk.choices[0].delta.reasoning_content
@@ -738,6 +934,50 @@ class OpenAIClient:
# Just collect chunks without display
async for chunk in response_stream:
chunks.append(chunk)
if _emit and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
last_content_time = time.perf_counter()
if not first_token_emitted:
await emit_event(StreamEvent(
type=StreamEventType.FIRST_TOKEN,
timestamp=last_content_time,
content=content
))
first_token_emitted = True
else:
await emit_event(StreamEvent(
type=StreamEventType.DELTA_TEXT,
timestamp=last_content_time,
content=content
))
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
for tc in chunk.choices[0].delta.tool_calls:
await emit_event(StreamEvent(
type=StreamEventType.DELTA_TOOL_CALL,
timestamp=time.perf_counter(),
tool_call={
"index": tc.index,
"id": getattr(tc, 'id', None),
"name": getattr(tc.function, 'name', None) if tc.function else None,
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
}
))
# Emit LAST_TOKEN and STREAM_END events
if _emit:
if last_content_time:
await emit_event(StreamEvent(
type=StreamEventType.LAST_TOKEN,
timestamp=last_content_time
))
await emit_event(StreamEvent(
type=StreamEventType.STREAM_END,
timestamp=time.perf_counter(),
metadata={"chunk_count": len(chunks)}
))
final_response = process_stream_chunks(chunks)
return final_response
@@ -749,7 +989,7 @@ class OpenAIClient:
def create_completion(
self,
messages: List[Dict[str, Any]],
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
stream: bool = False,
tools: Optional[List[Dict[str, Any]]] = None,
@@ -794,7 +1034,7 @@ class OpenAIClient:
async def acreate_completion(
self,
messages: List[Dict[str, Any]],
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
stream: bool = False,
tools: Optional[List[Dict[str, Any]]] = None,
@@ -839,7 +1079,7 @@ class OpenAIClient:
def chat_completion_with_tools(
self,
messages: List[Dict[str, Any]],
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
tools: Optional[List[Any]] = None,
execute_tool_fn: Optional[Callable] = None,
@@ -904,7 +1144,9 @@ class OpenAIClient:
if display_fn and console:
# When verbose (display_fn provided), use streaming for better UX
try:
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
request_start_perf = time.perf_counter()
ttft_logged = False
with Live(display_fn("", start_time), console=console, refresh_per_second=10, transient=True) as live:
# Use streaming when display_fn is provided for progressive display
response_stream = self.create_completion(
messages=messages,
@@ -922,6 +1164,11 @@ class OpenAIClient:
for chunk in response_stream:
chunks.append(chunk)
if chunk.choices[0].delta.content:
# Log TTFT on first token
if not ttft_logged:
ttft_ms = (time.perf_counter() - request_start_perf) * 1000
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
ttft_logged = True
full_response_text += chunk.choices[0].delta.content
live.update(display_fn(full_response_text, start_time))
@@ -1020,7 +1267,7 @@ class OpenAIClient:
async def achat_completion_with_tools(
self,
messages: List[Dict[str, Any]],
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
tools: Optional[List[Any]] = None,
execute_tool_fn: Optional[Callable] = None,
@@ -1204,7 +1451,7 @@ class OpenAIClient:
def chat_completion_with_tools_stream(
self,
messages: List[Dict[str, Any]],
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
tools: Optional[List[Any]] = None,
execute_tool_fn: Optional[Callable] = None,
@@ -1356,7 +1603,7 @@ class OpenAIClient:
self,
messages: List[Dict[str, Any]],
response_format: BaseModel,
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
**kwargs
) -> Any:
@@ -1390,7 +1637,7 @@ class OpenAIClient:
self,
messages: List[Dict[str, Any]],
response_format: BaseModel,
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
temperature: float = 1.0,
**kwargs
) -> Any:
+204 -23
View File
@@ -30,6 +30,53 @@ async_display_callbacks = {}
# Global approval callback registry
approval_callback = None
# ─────────────────────────────────────────────────────────────────────────────
# PraisonAI Unique Color Palette: "Elegant Intelligence"
# Creates a visual narrative flow: Agent → Task → Working → Response
# ─────────────────────────────────────────────────────────────────────────────
PRAISON_COLORS = {
# Agent identity - grounded, stable
"agent": "#86A789", # Soft Sage Green
"agent_text": "#D2E3C8", # Light sage for text
# Task/Question - input, attention-grabbing
"task": "#FF9B9B", # Warm Coral
"task_text": "#FFE5E5", # Light coral for text
# Working/Processing - action, energy
"working": "#FFB347", # Amber
"working_text": "#FFF3E0", # Light amber for text
# Response/Output - completion, calm
"response": "#4A90D9", # Ocean Blue
"response_text": "#E3F2FD", # Light blue for text
# Tool calls - special action
"tool": "#9B7EDE", # Violet Accent
"tool_text": "#EDE7F6", # Light violet for text
# Reasoning - thought process
"reasoning": "#78909C", # Blue Gray
"reasoning_text": "#ECEFF1", # Light gray for text
# Error/Warning - alert
"error": "#E57373", # Alert Red
"error_text": "#FFEBEE", # Light red for text
# Metrics - meta information
"metrics": "#B4B4B3", # Cool Gray
"metrics_text": "#FAFAFA", # Near white for text
}
# Status animation frames for "Working" indicator
WORKING_FRAMES = ["●○○", "○●○", "○○●", "○●○"]
WORKING_PHASES = [
"Analyzing query...",
"Processing context...",
"Generating response...",
"Finalizing output...",
]
# At the top of the file, add display_callbacks to __all__
__all__ = [
'error_logs',
@@ -39,7 +86,19 @@ __all__ = [
'async_display_callbacks',
'execute_callback',
'approval_callback',
# ... other exports
# Color palette and animation constants
'PRAISON_COLORS',
'WORKING_FRAMES',
'WORKING_PHASES',
# Display functions
'display_interaction',
'display_instruction',
'display_tool_call',
'display_error',
'display_generating',
'display_reasoning_steps',
'display_working_status',
'display_self_reflection',
]
def register_display_callback(display_type: str, callback_fn, is_async: bool = False):
@@ -149,10 +208,14 @@ def _clean_display_content(content: str, max_length: int = 20000) -> str:
return content.strip()
def display_interaction(message, response, markdown=True, generation_time=None, console=None, agent_name=None, agent_role=None, agent_tools=None, task_name=None, task_description=None, task_id=None):
def display_interaction(message, response, markdown=True, generation_time=None, console=None, agent_name=None, agent_role=None, agent_tools=None, task_name=None, task_description=None, task_id=None, metrics=None):
"""Synchronous version of display_interaction.
Displays the task/message and response in clean panels with response time in title.
Displays the task/message and response in clean panels with semantic colors
and optional metrics footer. Uses PraisonAI's unique color palette.
Args:
metrics: Optional dict with token_in, token_out, cost, model for footer display
"""
if console is None:
console = Console()
@@ -180,20 +243,40 @@ def display_interaction(message, response, markdown=True, generation_time=None,
agent_tools=agent_tools,
task_name=task_name,
task_description=task_description,
task_id=task_id
task_id=task_id,
metrics=metrics
)
# Build response title with time (Agno-style: "Response (1.2s)")
# Build response title with time
response_title = "Response"
if generation_time:
response_title = f"Response ({generation_time:.1f}s)"
# Build response content with optional metrics footer
response_content = response
if metrics and isinstance(metrics, dict):
# Add dashed separator and compact metrics line
tokens_in = metrics.get('tokens_in', 0)
tokens_out = metrics.get('tokens_out', 0)
cost = metrics.get('cost', 0)
model = metrics.get('model', '')
metrics_line = f"\n\n─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─\n📊 {tokens_in} tokens in · {tokens_out} out"
if cost > 0:
metrics_line += f" · ${cost:.4f}"
if model:
metrics_line += f" · {model}"
response_content = response + metrics_line
# Task is inline (less visual weight), Response keeps panel (it's the main content)
# Format: 📝 Task message
task_prefix = "[bold #FF9B9B]📝[/]"
if markdown:
console.print(Panel.fit(Markdown(message), title="Task", border_style="cyan"))
console.print(Panel.fit(Markdown(response), title=response_title, border_style="cyan"))
console.print(f"{task_prefix} {message}\n")
console.print(Panel.fit(Markdown(response_content), title=response_title, border_style=PRAISON_COLORS["response"]))
else:
console.print(Panel.fit(Text(message, style="bold green"), title="Task", border_style="cyan"))
console.print(Panel.fit(Text(response, style="bold blue"), title=response_title, border_style="cyan"))
console.print(f"{task_prefix} [bold {PRAISON_COLORS['task_text']}]{message}[/]\n")
console.print(Panel.fit(Text(response_content, style=f"bold {PRAISON_COLORS['response_text']}"), title=response_title, border_style=PRAISON_COLORS["response"]))
def display_self_reflection(message: str, console=None):
if not message or not message.strip():
@@ -231,8 +314,11 @@ def display_instruction(message: str, console=None, agent_name: str = None, agen
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
console.print(Panel.fit(Text(message, style="bold blue"), title="Instruction", border_style="cyan"))
def display_tool_call(message: str, console=None, tool_name: str = None, tool_input: dict = None, tool_output: str = None):
"""Display tool call information in a consolidated panel.
def display_tool_call(message: str, console=None, tool_name: str = None, tool_input: dict = None, tool_output: str = None, elapsed_time: float = None, success: bool = True):
"""Display tool call information in PraisonAI's unique timeline format.
Uses ▸ prefix, inline timing [X.Xs], and status icons ✓/✗ for a clean,
scannable tool activity display.
Args:
message: The tool call message (legacy format)
@@ -240,6 +326,8 @@ def display_tool_call(message: str, console=None, tool_name: str = None, tool_in
tool_name: Name of the tool being called
tool_input: Input arguments to the tool
tool_output: Output from the tool (if available)
elapsed_time: Time taken for tool execution in seconds
success: Whether the tool call succeeded
"""
logging.debug(f"display_tool_call called with message: {repr(message)}")
if not message or not message.strip():
@@ -251,19 +339,41 @@ def display_tool_call(message: str, console=None, tool_name: str = None, tool_in
# Execute synchronous callbacks (always, even when console is None)
execute_sync_callback('tool_call', message=message, tool_name=tool_name, tool_input=tool_input, tool_output=tool_output)
# Only print panel if console is provided (verbose mode)
# Only print if console is provided (verbose mode)
if console is not None:
# Build consolidated tool call display (Agno-style)
if tool_name and tool_input is not None:
# Format: tool_name(arg1=val1, arg2=val2)
args_str = ", ".join(f"{k}={v}" for k, v in tool_input.items()) if tool_input else ""
tool_display = f"• {tool_name}({args_str})"
# Build clean inline format - no panels, just prefixed lines
if tool_name:
# Format: ▸ tool_name(args) → result [X.Xs] ✓
args_str = ""
if tool_input:
# Truncate long values for display
args_parts = []
for k, v in tool_input.items():
v_str = str(v)
if len(v_str) > 50:
v_str = v_str[:47] + "..."
args_parts.append(f"{k}={repr(v_str) if isinstance(v, str) else v_str}")
args_str = ", ".join(args_parts)
# Build the inline entry
status_icon = "[green]✓[/]" if success else "[red]✗[/]"
time_str = f"[dim][{elapsed_time:.1f}s][/]" if elapsed_time else ""
# Base tool call line
tool_line = f"[bold #86A789]▸[/] [#B4D4FF]{tool_name}[/]({args_str})"
# Add output inline with arrow if available
if tool_output:
tool_display += f"\n → {tool_output}"
console.print(Panel.fit(Text(tool_display, style="bold cyan"), title="Tool Call", border_style="green"))
output_str = str(tool_output)
if len(output_str) > 80:
output_str = output_str[:77] + "..."
tool_line += f" [dim]→[/] [italic]{output_str}[/]"
tool_line += f" {time_str} {status_icon}"
console.print(tool_line)
else:
# Legacy format
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
# Legacy format - simple inline
console.print(f"[bold #86A789]▸[/] {message}")
def display_error(message: str, console=None):
if not message or not message.strip():
@@ -275,7 +385,12 @@ def display_error(message: str, console=None):
# Execute synchronous callbacks
execute_sync_callback('error', message=message)
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
# Use semantic error color
console.print(Panel.fit(
Text(message, style=f"bold {PRAISON_COLORS['error_text']}"),
title="⚠ Error",
border_style=PRAISON_COLORS["error"]
))
error_logs.append(message)
def display_generating(content: str = "", start_time: Optional[float] = None):
@@ -293,7 +408,73 @@ def display_generating(content: str = "", start_time: Optional[float] = None):
# Execute synchronous callbacks
execute_sync_callback('generating', content=content, elapsed_time=elapsed_str.strip() if elapsed_str else None)
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
# Use semantic response color
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style=PRAISON_COLORS["response"])
def display_reasoning_steps(steps: List[str], console=None):
"""Display reasoning steps with unique numbered circles.
Uses ①②③ numbered circles for a distinctive, scannable format
that shows the agent's thought process.
Args:
steps: List of reasoning step descriptions
console: Rich console for output
"""
if not steps:
return
if console is None:
console = Console()
# Circle number mapping (supports up to 20 steps)
circle_numbers = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
# Build reasoning display with numbered circles
reasoning_lines = []
for i, step in enumerate(steps):
circle = circle_numbers[i] if i < len(circle_numbers) else f"({i+1})"
step_text = _clean_display_content(str(step))
if len(step_text) > 80:
step_text = step_text[:77] + "..."
reasoning_lines.append(f"{circle} {step_text}")
reasoning_display = "\n".join(reasoning_lines)
console.print(Panel.fit(
Text(reasoning_display, style=f"italic {PRAISON_COLORS['reasoning_text']}"),
title="Reasoning",
border_style=PRAISON_COLORS["reasoning"]
))
def display_working_status(phase: int = 0, status_text: str = None, console=None):
"""Display animated working status with pulsing dots.
Shows a unique "Working ●○○" indicator with phase-specific status.
This is PraisonAI's distinctive approach to showing processing status.
Args:
phase: Current animation phase (0-3)
status_text: Optional status description
console: Rich console for output
Returns:
Panel object for use with Rich.Live
"""
if console is None:
console = Console()
# Get current frame and phase text
frame = WORKING_FRAMES[phase % len(WORKING_FRAMES)]
phase_text = status_text or WORKING_PHASES[phase % len(WORKING_PHASES)]
# Build working status display
working_display = f"Working {frame} {phase_text}"
return Panel.fit(
Text(working_display, style=f"bold {PRAISON_COLORS['working_text']}"),
title="Status",
border_style=PRAISON_COLORS["working"]
)
# Async versions with 'a' prefix
async def adisplay_interaction(message, response, markdown=True, generation_time=None, console=None, agent_name=None, agent_role=None, agent_tools=None, task_name=None, task_description=None, task_id=None):
@@ -162,7 +162,7 @@ class MCP:
# Method 1: Using command and args separately
agent = Agent(
instructions="You are a helpful assistant...",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP(
command="/path/to/python",
args=["/path/to/app.py"]
@@ -172,14 +172,14 @@ class MCP:
# Method 2: Using a single command string
agent = Agent(
instructions="You are a helpful assistant...",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP("/path/to/python /path/to/app.py")
)
# Method 3: Using an SSE endpoint
agent = Agent(
instructions="You are a helpful assistant...",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP("http://localhost:8080/sse")
)
@@ -548,7 +548,7 @@ class MCP:
"""Convert the MCP tool to an OpenAI-compatible tool definition.
This method is specifically invoked by the Agent class when using
provider/model format (e.g., "openai/gpt-5-nano").
provider/model format (e.g., "openai/gpt-4o-mini").
Returns:
dict or list: OpenAI-compatible tool definition(s)
@@ -110,6 +110,31 @@ def __getattr__(name):
if name == "create_hooks_manager":
from .hooks import create_hooks_manager
return create_hooks_manager
# Learn module
if name == "LearnManager":
from .learn import LearnManager
return LearnManager
if name == "PersonaStore":
from .learn import PersonaStore
return PersonaStore
if name == "InsightStore":
from .learn import InsightStore
return InsightStore
if name == "ThreadStore":
from .learn import ThreadStore
return ThreadStore
if name == "PatternStore":
from .learn import PatternStore
return PatternStore
if name == "DecisionStore":
from .learn import DecisionStore
return DecisionStore
if name == "FeedbackStore":
from .learn import FeedbackStore
return FeedbackStore
if name == "ImprovementStore":
from .learn import ImprovementStore
return ImprovementStore
# DocsManager and MCPConfigManager are already imported at module level
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -153,4 +178,13 @@ __all__ = [
# MCP config management
"MCPConfigManager",
"MCPConfig",
# Learn module
"LearnManager",
"PersonaStore",
"InsightStore",
"ThreadStore",
"PatternStore",
"DecisionStore",
"FeedbackStore",
"ImprovementStore",
]
@@ -0,0 +1,61 @@
"""
Learn Module - Continuous Learning within Memory System.
Provides learning capabilities for agents to capture patterns, preferences,
and insights from interactions to improve future responses.
Usage:
from praisonaiagents import Agent, MemoryConfig, LearnConfig
# Simple enable
agent = Agent(memory=MemoryConfig(learn=True))
# With specific capabilities
agent = Agent(memory=MemoryConfig(
learn=LearnConfig(
persona=True,
insights=True,
patterns=True,
)
))
"""
__all__ = [
"LearnManager",
"PersonaStore",
"InsightStore",
"ThreadStore",
"PatternStore",
"DecisionStore",
"FeedbackStore",
"ImprovementStore",
]
def __getattr__(name: str):
"""Lazy loading for learn module components."""
if name == "LearnManager":
from .manager import LearnManager
return LearnManager
if name == "PersonaStore":
from .stores import PersonaStore
return PersonaStore
if name == "InsightStore":
from .stores import InsightStore
return InsightStore
if name == "ThreadStore":
from .stores import ThreadStore
return ThreadStore
if name == "PatternStore":
from .stores import PatternStore
return PatternStore
if name == "DecisionStore":
from .stores import DecisionStore
return DecisionStore
if name == "FeedbackStore":
from .stores import FeedbackStore
return FeedbackStore
if name == "ImprovementStore":
from .stores import ImprovementStore
return ImprovementStore
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,241 @@
"""
LearnManager - Central manager for continuous learning capabilities.
Coordinates all learning stores and provides a unified interface for
capturing and retrieving learned information.
"""
from typing import Any, Dict, List, Optional
from ...config.feature_configs import LearnConfig, LearnScope
from .stores import (
PersonaStore,
InsightStore,
ThreadStore,
PatternStore,
DecisionStore,
FeedbackStore,
ImprovementStore,
LearnEntry,
)
class LearnManager:
"""
Central manager for continuous learning within the memory system.
Coordinates all learning stores based on LearnConfig settings.
Usage:
config = LearnConfig(persona=True, insights=True)
manager = LearnManager(config, user_id="alice")
# Capture learning
manager.capture_persona("User prefers concise responses")
manager.capture_insight("User works in data science")
# Retrieve learnings
context = manager.get_learning_context()
"""
def __init__(
self,
config: Optional[LearnConfig] = None,
user_id: Optional[str] = None,
store_path: Optional[str] = None,
):
self.config = config or LearnConfig()
self.user_id = user_id or "default"
self.store_path = store_path or self.config.store_path
scope = self.config.scope
if isinstance(scope, LearnScope):
scope = scope.value
self._stores: Dict[str, Any] = {}
if self.config.persona:
self._stores["persona"] = PersonaStore(
store_path=self._get_store_path("persona"),
user_id=self.user_id,
scope=scope,
)
if self.config.insights:
self._stores["insights"] = InsightStore(
store_path=self._get_store_path("insights"),
user_id=self.user_id,
scope=scope,
)
if self.config.thread:
self._stores["threads"] = ThreadStore(
store_path=self._get_store_path("threads"),
user_id=self.user_id,
scope=scope,
)
if self.config.patterns:
self._stores["patterns"] = PatternStore(
store_path=self._get_store_path("patterns"),
user_id=self.user_id,
scope=scope,
)
if self.config.decisions:
self._stores["decisions"] = DecisionStore(
store_path=self._get_store_path("decisions"),
user_id=self.user_id,
scope=scope,
)
if self.config.feedback:
self._stores["feedback"] = FeedbackStore(
store_path=self._get_store_path("feedback"),
user_id=self.user_id,
scope=scope,
)
if self.config.improvements:
self._stores["improvements"] = ImprovementStore(
store_path=self._get_store_path("improvements"),
user_id=self.user_id,
scope=scope,
)
def _get_store_path(self, store_name: str) -> Optional[str]:
"""Get path for a specific store."""
if self.store_path:
return f"{self.store_path}/{store_name}.json"
return None
def capture_persona(self, content: str, category: str = "general") -> Optional[LearnEntry]:
"""Capture user preference or profile information."""
if "persona" in self._stores:
return self._stores["persona"].add_preference(content, category)
return None
def capture_insight(self, content: str, source: str = "interaction") -> Optional[LearnEntry]:
"""Capture an observation or learning."""
if "insights" in self._stores:
return self._stores["insights"].add_insight(content, source)
return None
def capture_thread(self, summary: str, thread_id: str) -> Optional[LearnEntry]:
"""Capture thread/session context."""
if "threads" in self._stores:
return self._stores["threads"].add_thread_summary(summary, thread_id)
return None
def capture_pattern(self, pattern: str, pattern_type: str = "general") -> Optional[LearnEntry]:
"""Capture a reusable knowledge pattern."""
if "patterns" in self._stores:
return self._stores["patterns"].add_pattern(pattern, pattern_type)
return None
def capture_decision(
self,
decision: str,
reasoning: str = "",
outcome: str = "",
) -> Optional[LearnEntry]:
"""Capture a decision record."""
if "decisions" in self._stores:
return self._stores["decisions"].add_decision(decision, reasoning, outcome)
return None
def capture_feedback(
self,
feedback: str,
rating: Optional[int] = None,
source: str = "user",
) -> Optional[LearnEntry]:
"""Capture feedback."""
if "feedback" in self._stores:
return self._stores["feedback"].add_feedback(feedback, rating, source)
return None
def capture_improvement(
self,
proposal: str,
area: str = "general",
priority: str = "medium",
) -> Optional[LearnEntry]:
"""Capture an improvement proposal."""
if "improvements" in self._stores:
return self._stores["improvements"].add_improvement(proposal, area, priority)
return None
def get_learning_context(self, limit_per_store: int = 5) -> Dict[str, List[Dict[str, Any]]]:
"""
Get learning context from all enabled stores.
Returns a dictionary with entries from each store, suitable for
injection into agent context.
"""
context = {}
for store_name, store in self._stores.items():
entries = store.list_all(limit=limit_per_store)
context[store_name] = [e.to_dict() for e in entries]
return context
def search(self, query: str, limit: int = 10) -> Dict[str, List[Dict[str, Any]]]:
"""Search across all enabled stores."""
results = {}
for store_name, store in self._stores.items():
entries = store.search(query, limit=limit)
if entries:
results[store_name] = [e.to_dict() for e in entries]
return results
def get_persona_context(self, limit: int = 10) -> List[str]:
"""Get persona entries as context strings."""
if "persona" not in self._stores:
return []
entries = self._stores["persona"].list_all(limit=limit)
return [e.content for e in entries]
def get_insights_context(self, limit: int = 10) -> List[str]:
"""Get insight entries as context strings."""
if "insights" not in self._stores:
return []
entries = self._stores["insights"].list_all(limit=limit)
return [e.content for e in entries]
def clear_all(self) -> Dict[str, int]:
"""Clear all stores."""
cleared = {}
for store_name, store in self._stores.items():
cleared[store_name] = store.clear()
return cleared
def get_stats(self) -> Dict[str, int]:
"""Get entry counts for all stores."""
return {
store_name: len(store._entries)
for store_name, store in self._stores.items()
}
def to_system_prompt_context(self) -> str:
"""
Generate context suitable for injection into system prompt.
Returns a formatted string with relevant learnings.
"""
parts = []
if "persona" in self._stores:
personas = self.get_persona_context(limit=5)
if personas:
parts.append("User Preferences:")
for p in personas:
parts.append(f"- {p}")
if "insights" in self._stores:
insights = self.get_insights_context(limit=5)
if insights:
parts.append("\nLearned Insights:")
for i in insights:
parts.append(f"- {i}")
return "\n".join(parts) if parts else ""
@@ -0,0 +1,279 @@
"""
Learning Stores - Storage backends for different learning capabilities.
Each store handles a specific type of learning data:
- PersonaStore: User preferences and profile
- InsightStore: Observations and learnings
- ThreadStore: Session/conversation context
- PatternStore: Reusable knowledge patterns
- DecisionStore: Decision logging
- FeedbackStore: Outcome signals
- ImprovementStore: Self-improvement proposals
"""
import json
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
@dataclass
class LearnEntry:
"""Base entry for all learning stores."""
id: str
content: str
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
updated_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"content": self.content,
"metadata": self.metadata,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "LearnEntry":
return cls(
id=data["id"],
content=data["content"],
metadata=data.get("metadata", {}),
created_at=data.get("created_at", datetime.utcnow().isoformat()),
updated_at=data.get("updated_at", datetime.utcnow().isoformat()),
)
class BaseStore(ABC):
"""Abstract base class for learning stores."""
def __init__(
self,
store_path: Optional[str] = None,
user_id: Optional[str] = None,
scope: str = "private",
):
self.user_id = user_id or "default"
self.scope = scope
self.store_path = store_path or self._default_path()
self._entries: Dict[str, LearnEntry] = {}
self._load()
@property
@abstractmethod
def store_name(self) -> str:
"""Name of the store (used for file naming)."""
pass
def _default_path(self) -> str:
"""Default storage path."""
base = Path.home() / ".praison" / "learn" / self.scope / self.user_id
base.mkdir(parents=True, exist_ok=True)
return str(base / f"{self.store_name}.json")
def _load(self) -> None:
"""Load entries from storage."""
if os.path.exists(self.store_path):
try:
with open(self.store_path, "r") as f:
data = json.load(f)
self._entries = {
k: LearnEntry.from_dict(v) for k, v in data.items()
}
except (json.JSONDecodeError, IOError):
self._entries = {}
def _save(self) -> None:
"""Save entries to storage."""
os.makedirs(os.path.dirname(self.store_path), exist_ok=True)
with open(self.store_path, "w") as f:
json.dump(
{k: v.to_dict() for k, v in self._entries.items()},
f,
indent=2,
)
def add(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> LearnEntry:
"""Add a new entry."""
entry_id = f"{self.store_name}_{len(self._entries)}_{datetime.utcnow().timestamp()}"
entry = LearnEntry(
id=entry_id,
content=content,
metadata=metadata or {},
)
self._entries[entry_id] = entry
self._save()
return entry
def get(self, entry_id: str) -> Optional[LearnEntry]:
"""Get entry by ID."""
return self._entries.get(entry_id)
def search(self, query: str, limit: int = 10) -> List[LearnEntry]:
"""Simple text search (can be overridden for semantic search)."""
query_lower = query.lower()
results = [
entry for entry in self._entries.values()
if query_lower in entry.content.lower()
]
return results[:limit]
def list_all(self, limit: int = 100) -> List[LearnEntry]:
"""List all entries."""
entries = list(self._entries.values())
entries.sort(key=lambda x: x.updated_at, reverse=True)
return entries[:limit]
def update(self, entry_id: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[LearnEntry]:
"""Update an existing entry."""
if entry_id not in self._entries:
return None
entry = self._entries[entry_id]
entry.content = content
if metadata:
entry.metadata.update(metadata)
entry.updated_at = datetime.utcnow().isoformat()
self._save()
return entry
def delete(self, entry_id: str) -> bool:
"""Delete an entry."""
if entry_id in self._entries:
del self._entries[entry_id]
self._save()
return True
return False
def clear(self) -> int:
"""Clear all entries."""
count = len(self._entries)
self._entries = {}
self._save()
return count
class PersonaStore(BaseStore):
"""Store for user preferences and profile information."""
@property
def store_name(self) -> str:
return "persona"
def add_preference(self, preference: str, category: str = "general") -> LearnEntry:
"""Add a user preference."""
return self.add(preference, {"category": category, "type": "preference"})
def add_profile(self, profile_data: str, aspect: str = "general") -> LearnEntry:
"""Add profile information."""
return self.add(profile_data, {"aspect": aspect, "type": "profile"})
class InsightStore(BaseStore):
"""Store for observations and learnings."""
@property
def store_name(self) -> str:
return "insights"
def add_insight(self, insight: str, source: str = "interaction") -> LearnEntry:
"""Add an insight."""
return self.add(insight, {"source": source, "type": "insight"})
def add_observation(self, observation: str, context: str = "") -> LearnEntry:
"""Add an observation."""
return self.add(observation, {"context": context, "type": "observation"})
class ThreadStore(BaseStore):
"""Store for session/conversation context."""
@property
def store_name(self) -> str:
return "threads"
def add_thread_summary(self, summary: str, thread_id: str) -> LearnEntry:
"""Add a thread summary."""
return self.add(summary, {"thread_id": thread_id, "type": "summary"})
def add_context(self, context: str, thread_id: str) -> LearnEntry:
"""Add thread context."""
return self.add(context, {"thread_id": thread_id, "type": "context"})
class PatternStore(BaseStore):
"""Store for reusable knowledge patterns."""
@property
def store_name(self) -> str:
return "patterns"
def add_pattern(self, pattern: str, pattern_type: str = "general") -> LearnEntry:
"""Add a knowledge pattern."""
return self.add(pattern, {"pattern_type": pattern_type, "type": "pattern"})
class DecisionStore(BaseStore):
"""Store for decision logging."""
@property
def store_name(self) -> str:
return "decisions"
def add_decision(
self,
decision: str,
reasoning: str = "",
outcome: str = "",
) -> LearnEntry:
"""Add a decision record."""
return self.add(decision, {
"reasoning": reasoning,
"outcome": outcome,
"type": "decision",
})
class FeedbackStore(BaseStore):
"""Store for outcome signals and feedback."""
@property
def store_name(self) -> str:
return "feedback"
def add_feedback(
self,
feedback: str,
rating: Optional[int] = None,
source: str = "user",
) -> LearnEntry:
"""Add feedback."""
metadata = {"source": source, "type": "feedback"}
if rating is not None:
metadata["rating"] = rating
return self.add(feedback, metadata)
class ImprovementStore(BaseStore):
"""Store for self-improvement proposals."""
@property
def store_name(self) -> str:
return "improvements"
def add_improvement(
self,
proposal: str,
area: str = "general",
priority: str = "medium",
) -> LearnEntry:
"""Add an improvement proposal."""
return self.add(proposal, {
"area": area,
"priority": priority,
"type": "improvement",
})
@@ -109,7 +109,7 @@ class Memory:
},
"llm": {
"provider": "openai",
"config": {"model": "gpt-5-nano", "api_key": "..."}
"config": {"model": "gpt-4o-mini", "api_key": "..."}
},
"embedder": {
"provider": "openai",
@@ -146,6 +146,8 @@ class Memory:
self.use_rag = (self.provider.lower() == "rag") and CHROMADB_AVAILABLE and self.cfg.get("use_embedding", False)
self.use_mongodb = (self.provider.lower() == "mongodb") and PYMONGO_AVAILABLE
self.graph_enabled = False # Initialize graph support flag
self._learn_manager = None # Lazy-loaded LearnManager
self._learn_config = self.cfg.get("learn", None) # Learn configuration
# Extract embedding model from config
self.embedder_config = self.cfg.get("embedder", {})
@@ -1442,7 +1444,7 @@ class Memory:
import litellm
# Convert model name if it's in litellm format
model_name = llm or "gpt-5-nano"
model_name = llm or "gpt-4o-mini"
response = litellm.completion(
model=model_name,
@@ -1459,7 +1461,7 @@ class Memory:
client = OpenAI()
response = client.chat.completions.create(
model=llm or "gpt-5-nano",
model=llm or "gpt-4o-mini",
messages=[{
"role": "user",
"content": custom_prompt or default_prompt
@@ -1599,3 +1601,50 @@ class Memory:
except Exception as e:
self._log_verbose(f"Error getting all memories: {e}", logging.ERROR)
return []
# -------------------------------------------------------------------------
# Learn Integration
# -------------------------------------------------------------------------
@property
def learn(self):
"""
Get the LearnManager for continuous learning capabilities.
Returns None if learn is not enabled in config.
Usage:
memory = Memory({"learn": True})
memory.learn.capture_persona("User prefers concise responses")
memory.learn.capture_insight("User works in data science")
"""
if self._learn_manager is not None:
return self._learn_manager
if self._learn_config is None or self._learn_config is False:
return None
from .learn import LearnManager
from ..config.feature_configs import LearnConfig
if self._learn_config is True:
config = LearnConfig()
elif isinstance(self._learn_config, dict):
config = LearnConfig(**self._learn_config)
elif isinstance(self._learn_config, LearnConfig):
config = self._learn_config
else:
return None
user_id = self.cfg.get("user_id", "default")
self._learn_manager = LearnManager(config=config, user_id=user_id)
return self._learn_manager
def get_learn_context(self) -> str:
"""
Get learning context suitable for injection into system prompt.
Returns empty string if learn is not enabled.
"""
if self.learn is None:
return ""
return self.learn.to_system_prompt_context()
@@ -0,0 +1,479 @@
"""
PraisonAI Agents Core Profiling Module
Lightweight, protocol-based profiling for the core SDK.
Zero overhead when disabled. Opt-in only.
Features:
- Pre-LLM latency breakdown
- LLM client initialization timing
- Tool resolution timing
- Message serialization timing
- Streaming TTFT tracking
- Import timing
- Async-safe
Usage:
from praisonaiagents.profiling import Profiler, profile_block
# Enable profiling
Profiler.enable()
# Profile a block
with profile_block("my_operation"):
do_something()
# Get report
Profiler.report()
"""
import time
import os
import threading
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional, Any, Callable
from contextlib import contextmanager
import functools
import json
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class TimingRecord:
"""Record of a single timing measurement."""
name: str
duration_ms: float
category: str = "block"
parent: str = ""
timestamp: float = field(default_factory=time.time)
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class StreamingRecord:
"""Record of streaming operation."""
name: str
ttft_ms: float # Time to first token
total_ms: float
chunk_count: int = 0
timestamp: float = field(default_factory=time.time)
@dataclass
class PreLLMBreakdown:
"""Breakdown of pre-LLM latency phases."""
agent_init_ms: float = 0.0
llm_client_init_ms: float = 0.0
tool_resolution_ms: float = 0.0
message_build_ms: float = 0.0
context_management_ms: float = 0.0
knowledge_retrieval_ms: float = 0.0
guardrail_check_ms: float = 0.0
total_pre_llm_ms: float = 0.0
def to_dict(self) -> Dict[str, float]:
return asdict(self)
# ============================================================================
# Streaming Tracker
# ============================================================================
class StreamingTracker:
"""Track streaming operations for TTFT metrics."""
def __init__(self, name: str):
self.name = name
self._start_time: Optional[float] = None
self._first_token_time: Optional[float] = None
self._end_time: Optional[float] = None
self._chunk_count: int = 0
def start(self) -> None:
self._start_time = time.perf_counter()
def first_token(self) -> None:
if self._first_token_time is None:
self._first_token_time = time.perf_counter()
def chunk(self) -> None:
self._chunk_count += 1
def end(self) -> None:
self._end_time = time.perf_counter()
if self._start_time is not None:
ttft_ms = 0.0
if self._first_token_time is not None:
ttft_ms = (self._first_token_time - self._start_time) * 1000
total_ms = (self._end_time - self._start_time) * 1000
Profiler.record_streaming(
name=self.name,
ttft_ms=ttft_ms,
total_ms=total_ms,
chunk_count=self._chunk_count
)
@property
def ttft_ms(self) -> float:
if self._start_time and self._first_token_time:
return (self._first_token_time - self._start_time) * 1000
return 0.0
@property
def elapsed_ms(self) -> float:
if self._start_time:
end = self._end_time or time.perf_counter()
return (end - self._start_time) * 1000
return 0.0
# ============================================================================
# Core Profiler
# ============================================================================
class Profiler:
"""
Lightweight profiler for PraisonAI Agents core SDK.
Thread-safe singleton with zero overhead when disabled.
"""
_lock = threading.Lock()
_enabled: bool = False
_timings: List[TimingRecord] = []
_streaming: List[StreamingRecord] = []
_pre_llm_breakdown: Optional[PreLLMBreakdown] = None
_current_phases: Dict[str, float] = {} # Track in-progress phases
_phase_stack: List[str] = [] # Track nested phases
@classmethod
def enable(cls) -> None:
"""Enable profiling."""
cls._enabled = True
@classmethod
def disable(cls) -> None:
"""Disable profiling."""
cls._enabled = False
@classmethod
def is_enabled(cls) -> bool:
"""Check if profiling is enabled."""
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
@classmethod
def clear(cls) -> None:
"""Clear all profiling data."""
with cls._lock:
cls._timings.clear()
cls._streaming.clear()
cls._pre_llm_breakdown = None
cls._current_phases.clear()
cls._phase_stack.clear()
@classmethod
def record_timing(cls, name: str, duration_ms: float, category: str = "block",
parent: str = "", metadata: Optional[Dict[str, Any]] = None) -> None:
"""Record a timing measurement."""
if not cls.is_enabled():
return
with cls._lock:
cls._timings.append(TimingRecord(
name=name,
duration_ms=duration_ms,
category=category,
parent=parent,
metadata=metadata or {}
))
@classmethod
def record_streaming(cls, name: str, ttft_ms: float, total_ms: float,
chunk_count: int = 0) -> None:
"""Record streaming metrics."""
if not cls.is_enabled():
return
with cls._lock:
cls._streaming.append(StreamingRecord(
name=name,
ttft_ms=ttft_ms,
total_ms=total_ms,
chunk_count=chunk_count
))
@classmethod
def start_phase(cls, phase_name: str) -> None:
"""Start timing a phase (for pre-LLM breakdown)."""
if not cls.is_enabled():
return
with cls._lock:
cls._current_phases[phase_name] = time.perf_counter()
cls._phase_stack.append(phase_name)
@classmethod
def end_phase(cls, phase_name: str) -> float:
"""End timing a phase and return duration in ms."""
if not cls.is_enabled():
return 0.0
with cls._lock:
start = cls._current_phases.pop(phase_name, None)
if phase_name in cls._phase_stack:
cls._phase_stack.remove(phase_name)
if start is None:
return 0.0
duration_ms = (time.perf_counter() - start) * 1000
# Record as timing
parent = cls._phase_stack[-1] if cls._phase_stack else ""
cls._timings.append(TimingRecord(
name=phase_name,
duration_ms=duration_ms,
category="phase",
parent=parent
))
return duration_ms
@classmethod
@contextmanager
def phase(cls, name: str, category: str = "phase"):
"""Context manager for timing a phase."""
if not cls.is_enabled():
yield
return
start = time.perf_counter()
parent = ""
with cls._lock:
parent = cls._phase_stack[-1] if cls._phase_stack else ""
cls._phase_stack.append(name)
try:
yield
finally:
duration_ms = (time.perf_counter() - start) * 1000
with cls._lock:
if name in cls._phase_stack:
cls._phase_stack.remove(name)
cls._timings.append(TimingRecord(
name=name,
duration_ms=duration_ms,
category=category,
parent=parent
))
@classmethod
@contextmanager
def streaming(cls, name: str):
"""Context manager for streaming profiling."""
tracker = StreamingTracker(name)
tracker.start()
try:
yield tracker
finally:
tracker.end()
@classmethod
def set_pre_llm_breakdown(cls, breakdown: PreLLMBreakdown) -> None:
"""Set the pre-LLM breakdown data."""
if not cls.is_enabled():
return
with cls._lock:
cls._pre_llm_breakdown = breakdown
@classmethod
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
"""Get timing records."""
with cls._lock:
if category:
return [t for t in cls._timings if t.category == category]
return cls._timings.copy()
@classmethod
def get_streaming_records(cls) -> List[StreamingRecord]:
"""Get streaming records."""
with cls._lock:
return cls._streaming.copy()
@classmethod
def get_pre_llm_breakdown(cls) -> Optional[PreLLMBreakdown]:
"""Get pre-LLM breakdown."""
with cls._lock:
return cls._pre_llm_breakdown
@classmethod
def get_summary(cls) -> Dict[str, Any]:
"""Get profiling summary."""
with cls._lock:
total_time = sum(t.duration_ms for t in cls._timings)
# Group by category
by_category: Dict[str, float] = {}
for t in cls._timings:
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
# Top slowest
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:15]
# Pre-LLM breakdown
pre_llm = cls._pre_llm_breakdown.to_dict() if cls._pre_llm_breakdown else {}
# Streaming stats
streaming_stats = []
for s in cls._streaming:
streaming_stats.append({
'name': s.name,
'ttft_ms': s.ttft_ms,
'total_ms': s.total_ms,
'chunk_count': s.chunk_count
})
return {
'total_time_ms': total_time,
'timing_count': len(cls._timings),
'by_category': by_category,
'slowest_operations': [(s.name, s.duration_ms, s.category) for s in slowest],
'pre_llm_breakdown': pre_llm,
'streaming': streaming_stats
}
@classmethod
def report(cls, output: str = "console") -> str:
"""Generate and output profiling report."""
summary = cls.get_summary()
lines = [
"=" * 70,
"PraisonAI Agents Core Profiling Report",
"=" * 70,
"",
f"Total Time: {summary['total_time_ms']:.2f}ms",
f"Timing Records: {summary['timing_count']}",
"",
]
# Pre-LLM Breakdown (key for debugging the 7s delay)
if summary['pre_llm_breakdown']:
lines.append("## Pre-LLM Latency Breakdown")
lines.append("-" * 50)
for phase, ms in summary['pre_llm_breakdown'].items():
if ms > 0:
lines.append(f" {phase:<30}: {ms:>10.2f} ms")
lines.append("")
# By Category
lines.append("## By Category")
lines.append("-" * 50)
for cat, time_ms in summary['by_category'].items():
lines.append(f" {cat}: {time_ms:.2f}ms")
lines.append("")
# Slowest Operations
lines.append("## Slowest Operations")
lines.append("-" * 50)
for name, time_ms, cat in summary['slowest_operations']:
lines.append(f" {name:<40}: {time_ms:>10.2f}ms [{cat}]")
lines.append("")
# Streaming Stats
if summary['streaming']:
lines.append("## Streaming Metrics")
lines.append("-" * 50)
for s in summary['streaming']:
lines.append(f" {s['name']}: TTFT={s['ttft_ms']:.2f}ms, Total={s['total_ms']:.2f}ms, Chunks={s['chunk_count']}")
lines.append("")
lines.append("=" * 70)
report_text = "\n".join(lines)
if output == "console":
print(report_text)
return report_text
@classmethod
def export_json(cls) -> str:
"""Export profiling data as JSON."""
summary = cls.get_summary()
with cls._lock:
data = {
'summary': summary,
'timings': [asdict(t) for t in cls._timings],
'streaming': [asdict(s) for s in cls._streaming],
}
return json.dumps(data, indent=2, default=str)
# ============================================================================
# Decorators
# ============================================================================
def profile_func(func: Optional[Callable] = None, *, category: str = "function"):
"""Decorator to profile a function."""
def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args, **kwargs):
if not Profiler.is_enabled():
return fn(*args, **kwargs)
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
duration_ms = (time.perf_counter() - start) * 1000
Profiler.record_timing(fn.__name__, duration_ms, category)
return wrapper
if func is not None:
return decorator(func)
return decorator
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
"""Decorator to profile an async function."""
def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
if not Profiler.is_enabled():
return await fn(*args, **kwargs)
start = time.perf_counter()
try:
return await fn(*args, **kwargs)
finally:
duration_ms = (time.perf_counter() - start) * 1000
Profiler.record_timing(fn.__name__, duration_ms, category)
return wrapper
if func is not None:
return decorator(func)
return decorator
@contextmanager
def profile_block(name: str, category: str = "block"):
"""Context manager for profiling a block of code."""
if not Profiler.is_enabled():
yield
return
start = time.perf_counter()
try:
yield
finally:
duration_ms = (time.perf_counter() - start) * 1000
Profiler.record_timing(name, duration_ms, category)
# ============================================================================
# Exports
# ============================================================================
__all__ = [
'Profiler',
'StreamingTracker',
'TimingRecord',
'StreamingRecord',
'PreLLMBreakdown',
'profile_func',
'profile_async',
'profile_block',
]
@@ -162,11 +162,13 @@ class Session:
from ..agent import Agent as AgentClass
# Filter out parameters that are not valid for Agent.__init__
valid_kwargs = {k: v for k, v in kwargs.items() if k not in ('verbose', 'user_id')}
agent_kwargs = {
"name": name,
"role": role,
"user_id": self.user_id,
**kwargs
**valid_kwargs
}
if instructions:
@@ -177,7 +179,6 @@ class Session:
agent_kwargs["memory"] = self.memory
if knowledge:
agent_kwargs["knowledge"] = knowledge
agent_kwargs["knowledge_config"] = self.knowledge_config
agent = AgentClass(**agent_kwargs)
@@ -0,0 +1,75 @@
"""
PraisonAI Streaming Module.
Provides the StreamEvent protocol for pass-through streaming from LLM providers.
This module is lazily loaded to ensure zero performance impact when streaming
features are not used.
Usage:
from praisonaiagents.streaming import (
StreamEvent,
StreamEventType,
StreamMetrics,
StreamEventEmitter,
create_text_printer_callback,
)
"""
__all__ = [
"StreamEvent",
"StreamEventType",
"StreamMetrics",
"StreamEventEmitter",
"StreamCallback",
"AsyncStreamCallback",
"create_text_printer_callback",
"create_metrics_callback",
"StreamLogger",
"create_logging_callback",
]
def __getattr__(name: str):
"""Lazy loading of streaming components."""
if name in __all__:
# Events module exports
if name in (
"StreamEvent", "StreamEventType", "StreamMetrics",
"StreamEventEmitter", "StreamCallback", "AsyncStreamCallback",
"create_text_printer_callback", "create_metrics_callback"
):
from .events import (
StreamEvent,
StreamEventType,
StreamMetrics,
StreamEventEmitter,
StreamCallback,
AsyncStreamCallback,
create_text_printer_callback,
create_metrics_callback,
)
_events_exports = {
"StreamEvent": StreamEvent,
"StreamEventType": StreamEventType,
"StreamMetrics": StreamMetrics,
"StreamEventEmitter": StreamEventEmitter,
"StreamCallback": StreamCallback,
"AsyncStreamCallback": AsyncStreamCallback,
"create_text_printer_callback": create_text_printer_callback,
"create_metrics_callback": create_metrics_callback,
}
return _events_exports.get(name)
# Logging module exports
if name in ("StreamLogger", "create_logging_callback"):
from .logging import StreamLogger, create_logging_callback
_logging_exports = {
"StreamLogger": StreamLogger,
"create_logging_callback": create_logging_callback,
}
return _logging_exports.get(name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,300 @@
"""
StreamEvent protocol for PraisonAI streaming.
This module defines the minimal streaming event protocol that both OpenAI SDK
and LiteLLM streaming paths map their native chunks/events into.
Design principles:
- Zero overhead when not used (lazy imports, no global state)
- Multi-agent safe (per-agent callbacks, no shared mutable state)
- Async-safe (separate sync/async callback interfaces)
- Provider-agnostic (same events for OpenAI, LiteLLM, etc.)
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Protocol
import time
class StreamEventType(Enum):
"""Types of streaming events emitted during LLM response streaming."""
REQUEST_START = "request_start" # Before API call is made
HEADERS_RECEIVED = "headers_received" # When HTTP 200 headers arrive
FIRST_TOKEN = "first_token" # First content delta received (TTFT marker)
DELTA_TEXT = "delta_text" # Text content delta
DELTA_TOOL_CALL = "delta_tool_call" # Tool call delta (function name, args chunk)
TOOL_CALL_END = "tool_call_end" # Tool call complete
LAST_TOKEN = "last_token" # Final content delta
STREAM_END = "stream_end" # Stream completed successfully
ERROR = "error" # Error during streaming
@dataclass
class StreamEvent:
"""
A single streaming event emitted during LLM response streaming.
Attributes:
type: The type of event (see StreamEventType)
timestamp: High-precision timestamp (time.perf_counter())
content: Text content for DELTA_TEXT events
tool_call: Tool call data for DELTA_TOOL_CALL events
metadata: Additional event-specific metadata
error: Error message for ERROR events
"""
type: StreamEventType
timestamp: float = field(default_factory=time.perf_counter)
content: Optional[str] = None
tool_call: Optional[Dict[str, Any]] = None
metadata: Optional[Dict[str, Any]] = None
error: Optional[str] = None
def __repr__(self) -> str:
parts = [f"StreamEvent({self.type.value}"]
if self.content:
preview = self.content[:20] + "..." if len(self.content) > 20 else self.content
parts.append(f", content={preview!r}")
if self.tool_call:
parts.append(f", tool_call={self.tool_call.get('name', 'unknown')}")
if self.error:
parts.append(f", error={self.error!r}")
parts.append(")")
return "".join(parts)
class StreamCallback(Protocol):
"""Protocol for synchronous stream event callbacks."""
def __call__(self, event: StreamEvent) -> None:
"""Called when a stream event is emitted."""
...
class AsyncStreamCallback(Protocol):
"""Protocol for asynchronous stream event callbacks."""
async def __call__(self, event: StreamEvent) -> None:
"""Called when a stream event is emitted (async)."""
...
@dataclass
class StreamMetrics:
"""
Timing metrics for a streaming response.
Tracks key timestamps to measure:
- TTFT (Time To First Token): How long until first token arrives
- Stream Duration: How long the token streaming takes
- Total Time: End-to-end request time
All times are in seconds, measured with time.perf_counter() for precision.
"""
request_start: float = 0.0
headers_received: float = 0.0
first_token: float = 0.0
last_token: float = 0.0
stream_end: float = 0.0
token_count: int = 0
@property
def ttft(self) -> float:
"""Time To First Token in seconds (first_token - request_start)."""
if self.first_token and self.request_start:
return self.first_token - self.request_start
return 0.0
@property
def stream_duration(self) -> float:
"""Duration of token streaming in seconds (last_token - first_token)."""
if self.last_token and self.first_token:
return self.last_token - self.first_token
return 0.0
@property
def total_time(self) -> float:
"""Total request time in seconds (stream_end - request_start)."""
if self.stream_end and self.request_start:
return self.stream_end - self.request_start
return 0.0
@property
def tokens_per_second(self) -> float:
"""Token generation rate (tokens / stream_duration)."""
if self.stream_duration > 0 and self.token_count > 0:
return self.token_count / self.stream_duration
return 0.0
def update_from_event(self, event: StreamEvent) -> None:
"""Update metrics from a StreamEvent."""
if event.type == StreamEventType.REQUEST_START:
self.request_start = event.timestamp
elif event.type == StreamEventType.HEADERS_RECEIVED:
self.headers_received = event.timestamp
elif event.type == StreamEventType.FIRST_TOKEN:
self.first_token = event.timestamp
self.token_count = 1
elif event.type == StreamEventType.DELTA_TEXT:
self.token_count += 1
elif event.type == StreamEventType.LAST_TOKEN:
self.last_token = event.timestamp
elif event.type == StreamEventType.STREAM_END:
self.stream_end = event.timestamp
def to_dict(self) -> Dict[str, Any]:
"""Convert metrics to dictionary for logging/display."""
return {
"ttft_ms": round(self.ttft * 1000, 1),
"stream_duration_ms": round(self.stream_duration * 1000, 1),
"total_time_ms": round(self.total_time * 1000, 1),
"token_count": self.token_count,
"tokens_per_second": round(self.tokens_per_second, 1),
}
def format_summary(self) -> str:
"""Format metrics as a human-readable summary string."""
return (
f"TTFT: {self.ttft*1000:.0f}ms | "
f"Stream: {self.stream_duration*1000:.0f}ms | "
f"Total: {self.total_time*1000:.0f}ms | "
f"Tokens: {self.token_count} ({self.tokens_per_second:.1f}/s)"
)
class StreamEventEmitter:
"""
Manages stream event callbacks for an agent or streaming session.
Thread-safe and multi-agent safe - each agent has its own emitter instance.
Supports both sync and async callbacks.
Usage:
emitter = StreamEventEmitter()
emitter.add_callback(my_callback)
# During streaming:
emitter.emit(StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello"))
"""
def __init__(self) -> None:
self._callbacks: List[StreamCallback] = []
self._async_callbacks: List[AsyncStreamCallback] = []
self._metrics: Optional[StreamMetrics] = None
self._collect_metrics: bool = False
def add_callback(self, callback: StreamCallback) -> None:
"""Add a synchronous callback to receive stream events."""
self._callbacks.append(callback)
def remove_callback(self, callback: StreamCallback) -> None:
"""Remove a synchronous callback."""
if callback in self._callbacks:
self._callbacks.remove(callback)
def add_async_callback(self, callback: AsyncStreamCallback) -> None:
"""Add an asynchronous callback to receive stream events."""
self._async_callbacks.append(callback)
def remove_async_callback(self, callback: AsyncStreamCallback) -> None:
"""Remove an asynchronous callback."""
if callback in self._async_callbacks:
self._async_callbacks.remove(callback)
def enable_metrics(self) -> None:
"""Enable metrics collection for this streaming session."""
self._collect_metrics = True
self._metrics = StreamMetrics()
def get_metrics(self) -> Optional[StreamMetrics]:
"""Get collected metrics (None if metrics not enabled)."""
return self._metrics
def reset_metrics(self) -> None:
"""Reset metrics for a new streaming session."""
if self._collect_metrics:
self._metrics = StreamMetrics()
def emit(self, event: StreamEvent) -> None:
"""
Emit a stream event to all registered callbacks.
This is the synchronous emission path. For async streaming,
use emit_async().
"""
# Update metrics if enabled
if self._collect_metrics and self._metrics:
self._metrics.update_from_event(event)
# Call all sync callbacks
for callback in self._callbacks:
try:
callback(event)
except Exception:
# Don't let callback errors break streaming
pass
async def emit_async(self, event: StreamEvent) -> None:
"""
Emit a stream event to all registered callbacks (async version).
Calls both sync and async callbacks.
"""
# Update metrics if enabled
if self._collect_metrics and self._metrics:
self._metrics.update_from_event(event)
# Call sync callbacks
for callback in self._callbacks:
try:
callback(event)
except Exception:
pass
# Call async callbacks
for callback in self._async_callbacks:
try:
await callback(event)
except Exception:
pass
@property
def has_callbacks(self) -> bool:
"""Check if any callbacks are registered."""
return bool(self._callbacks or self._async_callbacks)
def create_text_printer_callback(flush: bool = True) -> StreamCallback:
"""
Create a simple callback that prints text deltas to stdout.
Args:
flush: Whether to flush stdout after each print (default True)
Returns:
A StreamCallback that prints DELTA_TEXT content
"""
def printer(event: StreamEvent) -> None:
if event.type == StreamEventType.DELTA_TEXT and event.content:
print(event.content, end="", flush=flush)
elif event.type == StreamEventType.STREAM_END:
print() # Newline at end
return printer
def create_metrics_callback(metrics: StreamMetrics) -> StreamCallback:
"""
Create a callback that updates a StreamMetrics instance.
Args:
metrics: The StreamMetrics instance to update
Returns:
A StreamCallback that updates the metrics
"""
def updater(event: StreamEvent) -> None:
metrics.update_from_event(event)
return updater
@@ -0,0 +1,207 @@
"""
Streaming logging utilities for PraisonAI.
Provides guarded logging for streaming events with clear timestamps
for request_start, headers_received, first_token, last_token, and stream_end.
Usage:
from praisonaiagents.streaming.logging import StreamLogger
logger = StreamLogger(verbose=True, metrics=True)
logger.log_request_start(model="gpt-4o-mini")
# ... streaming ...
logger.log_first_token()
# ... more tokens ...
logger.log_stream_end()
print(logger.get_metrics_summary())
"""
import logging
import time
from typing import Optional, Dict, Any
from .events import StreamMetrics, StreamEvent, StreamEventType
class StreamLogger:
"""
Guarded logging for streaming events with timing metrics.
Only logs when verbose=True or metrics=True to ensure zero overhead
when logging is disabled.
Attributes:
verbose: Whether to log detailed streaming events
metrics: Whether to collect and display timing metrics
"""
def __init__(
self,
verbose: bool = False,
metrics: bool = False,
logger: Optional[logging.Logger] = None
):
self.verbose = verbose
self.metrics = metrics
self._logger = logger or logging.getLogger(__name__)
self._metrics = StreamMetrics() if metrics else None
self._enabled = verbose or metrics
def log_request_start(
self,
model: Optional[str] = None,
message_count: Optional[int] = None
) -> None:
"""Log when the streaming request is initiated."""
if not self._enabled:
return
timestamp = time.perf_counter()
if self._metrics:
self._metrics.request_start = timestamp
if self.verbose:
extra = f" model={model}" if model else ""
extra += f" messages={message_count}" if message_count else ""
self._logger.info(f"[STREAM] REQUEST_START{extra}")
def log_headers_received(self) -> None:
"""Log when HTTP response headers are received."""
if not self._enabled:
return
timestamp = time.perf_counter()
if self._metrics:
self._metrics.headers_received = timestamp
if self.verbose:
self._logger.info("[STREAM] HEADERS_RECEIVED")
def log_first_token(self, content: Optional[str] = None) -> None:
"""Log when the first content token is received (TTFT marker)."""
if not self._enabled:
return
timestamp = time.perf_counter()
if self._metrics:
self._metrics.first_token = timestamp
self._metrics.token_count = 1
if self.verbose:
preview = content[:20] + "..." if content and len(content) > 20 else content
self._logger.info(f"[STREAM] FIRST_TOKEN content={preview!r}")
def log_delta_text(self, content: Optional[str] = None) -> None:
"""Log a text delta (only when verbose, not for every token)."""
if not self._enabled:
return
if self._metrics:
self._metrics.token_count += 1
# Don't log every delta to avoid noise - just track metrics
def log_last_token(self) -> None:
"""Log when the last content token is received."""
if not self._enabled:
return
timestamp = time.perf_counter()
if self._metrics:
self._metrics.last_token = timestamp
if self.verbose:
self._logger.info("[STREAM] LAST_TOKEN")
def log_stream_end(self, chunk_count: Optional[int] = None) -> None:
"""Log when the stream ends."""
if not self._enabled:
return
timestamp = time.perf_counter()
if self._metrics:
self._metrics.stream_end = timestamp
if self.verbose:
extra = f" chunks={chunk_count}" if chunk_count else ""
self._logger.info(f"[STREAM] STREAM_END{extra}")
def log_error(self, error: str) -> None:
"""Log a streaming error."""
if self.verbose:
self._logger.error(f"[STREAM] ERROR: {error}")
def get_metrics(self) -> Optional[StreamMetrics]:
"""Get the collected metrics (None if metrics not enabled)."""
return self._metrics
def get_metrics_summary(self) -> str:
"""Get a formatted summary of streaming metrics."""
if not self._metrics:
return "Metrics not enabled"
return self._metrics.format_summary()
def get_metrics_dict(self) -> Optional[Dict[str, Any]]:
"""Get metrics as a dictionary."""
if not self._metrics:
return None
return self._metrics.to_dict()
def update_from_event(self, event: StreamEvent) -> None:
"""Update logger state from a StreamEvent."""
if not self._enabled:
return
if self._metrics:
self._metrics.update_from_event(event)
if self.verbose:
if event.type == StreamEventType.REQUEST_START:
self.log_request_start()
elif event.type == StreamEventType.HEADERS_RECEIVED:
self.log_headers_received()
elif event.type == StreamEventType.FIRST_TOKEN:
self.log_first_token(event.content)
elif event.type == StreamEventType.LAST_TOKEN:
self.log_last_token()
elif event.type == StreamEventType.STREAM_END:
self.log_stream_end()
elif event.type == StreamEventType.ERROR:
self.log_error(event.error or "Unknown error")
def create_logging_callback(
verbose: bool = False,
metrics: bool = False,
logger: Optional[logging.Logger] = None
) -> tuple:
"""
Create a StreamLogger and a callback function for use with streaming.
Args:
verbose: Whether to log detailed events
metrics: Whether to collect timing metrics
logger: Optional custom logger
Returns:
Tuple of (StreamLogger, callback_function)
Usage:
stream_logger, callback = create_logging_callback(verbose=True, metrics=True)
# Pass callback to streaming method
response = client.process_stream_response(
...,
stream_callback=callback,
emit_events=True
)
# After streaming, get metrics
print(stream_logger.get_metrics_summary())
"""
stream_logger = StreamLogger(verbose=verbose, metrics=metrics, logger=logger)
def callback(event: StreamEvent) -> None:
stream_logger.update_from_event(event)
return stream_logger, callback
@@ -326,7 +326,7 @@ class Task:
if hasattr(self.agent.llm_instance, 'model'):
llm_model = self.agent.llm_instance.model
else:
llm_model = "gpt-5-nano" # Default fallback
llm_model = "gpt-4o-mini" # Default fallback
elif hasattr(self.agent, 'llm') and self.agent.llm:
# For standard model strings
llm_model = self.agent.llm
@@ -26,16 +26,41 @@ _telemetry_executor = None
_telemetry_queue = None
_queue_processor_running = False
_queue_lock = threading.Lock()
_atexit_registered = False
def _atexit_cleanup():
"""Cleanup handler registered with atexit to prevent hanging on exit."""
global _queue_processor_running, _telemetry_executor, _telemetry_queue
# Signal queue processor to stop
_queue_processor_running = False
# Shutdown executor without waiting (to prevent hang)
if _telemetry_executor is not None:
try:
_telemetry_executor.shutdown(wait=False)
except:
pass
_telemetry_executor = None
_telemetry_queue = None
def _get_telemetry_executor():
"""Get or create the shared telemetry thread pool executor."""
global _telemetry_executor
global _telemetry_executor, _atexit_registered
if _telemetry_executor is None:
# Use a small thread pool to avoid resource overhead
_telemetry_executor = concurrent.futures.ThreadPoolExecutor(
max_workers=2,
thread_name_prefix="telemetry"
)
# Register atexit cleanup to prevent hanging on exit
if not _atexit_registered:
import atexit
atexit.register(_atexit_cleanup)
_atexit_registered = True
return _telemetry_executor
def _get_telemetry_queue():
@@ -48,8 +73,13 @@ def _get_telemetry_queue():
with _queue_lock:
if not _queue_processor_running:
_queue_processor_running = True
executor = _get_telemetry_executor()
executor.submit(_process_telemetry_queue)
# Use a daemon thread directly instead of executor to prevent hang
processor_thread = threading.Thread(
target=_process_telemetry_queue,
name="telemetry-queue-processor",
daemon=True # Daemon thread will be killed on exit
)
processor_thread.start()
return _telemetry_queue
@@ -61,12 +91,22 @@ def _process_telemetry_queue():
try:
while _queue_processor_running:
# Check if interpreter is shutting down
try:
import sys
if hasattr(sys, 'is_finalizing') and sys.is_finalizing():
break
except:
break
events = []
deadline = time.time() + batch_timeout
# Collect events until batch size or timeout
while len(events) < batch_size and time.time() < deadline:
try:
if _telemetry_queue is None:
break
event = _telemetry_queue.get(timeout=0.1)
events.append(event)
_telemetry_queue.task_done()
@@ -381,6 +381,10 @@ class PerformanceMonitor:
Returns:
List of flow events showing function execution order and timing
"""
# If monitoring is disabled, return empty results
if self._monitoring_disabled:
return []
with self._lock:
flow = []
for event in self._function_flow.copy():
@@ -400,6 +404,10 @@ class PerformanceMonitor:
def get_active_calls(self) -> Dict[str, Any]:
"""Get information about currently executing functions."""
# If monitoring is disabled, return empty results
if self._monitoring_disabled:
return {}
with self._lock:
active = {}
current_time = time.time()
@@ -417,6 +425,10 @@ class PerformanceMonitor:
def get_slowest_functions(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get the slowest performing functions."""
# If monitoring is disabled, return empty results
if self._monitoring_disabled:
return []
with self._lock:
functions = []
for name, stats in self._function_stats.items():
@@ -435,6 +447,10 @@ class PerformanceMonitor:
def get_slowest_api_calls(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get the slowest performing API calls."""
# If monitoring is disabled, return empty results
if self._monitoring_disabled:
return []
with self._lock:
apis = []
for name, stats in self._api_calls.items():
@@ -539,6 +555,10 @@ class PerformanceMonitor:
def clear_statistics(self):
"""Clear all performance statistics."""
# If monitoring is disabled, nothing to clear
if self._monitoring_disabled:
return
with self._lock:
self._function_stats.clear()
self._api_calls.clear()
@@ -433,15 +433,24 @@ class MinimalTelemetry:
if posthog_client:
try:
# Include captured metrics in the event properties
event_properties = {
'version': self._environment.get('framework_version', 'unknown'),
'os': platform.system(),
'python_version': platform.python_version(),
'$process_person_profile': False,
'$geoip_disable': True,
'session_id': self.session_id
}
# Add all metrics to properties
if metrics:
event_properties.update(metrics)
posthog_client.capture(
distinct_id='anonymous',
event='sdk_used',
properties={
'version': self._environment['framework_version'],
'os': platform.system(),
'$process_person_profile': False,
'$geoip_disable': True
}
properties=event_properties
)
# Don't flush here - let PostHog handle it asynchronously
except:
@@ -691,7 +700,7 @@ def force_shutdown_telemetry():
def enable_telemetry():
"""Programmatically enable telemetry (if not disabled by environment)."""
global _telemetry_instance
if not _TELEMETRY_DISABLED:
if not _is_telemetry_disabled():
if _telemetry_instance:
_telemetry_instance.enabled = True
else:
@@ -5,6 +5,12 @@ from typing import Any
# Export Injected type for tool state injection
from .injected import Injected
# Export core tool items for organized imports
from .base import BaseTool, ToolResult, ToolValidationError, validate_tool
from .decorator import tool, FunctionTool
from .registry import get_registry, register_tool, get_tool, ToolRegistry
from .tools import Tools
# Map of function names to their module and class (if any)
TOOL_MAPPINGS = {
# Direct functions
@@ -27,7 +27,7 @@ class GenerateCOT:
def __init__(
self,
qa_pairs: Optional[Dict[str, str]] = None,
model: str = "gpt-5-nano",
model: str = "gpt-4o-mini",
api_key: Optional[str] = None,
max_attempts: int = 3,
verbose: bool = True,
@@ -0,0 +1,68 @@
"""
Trace Module for PraisonAI Agents.
Provides lightweight action tracing for the `output="actions"` mode.
Shows agent lifecycle events, tool calls, and final output without
verbose internal details.
Zero Performance Impact:
- All imports are lazy loaded via __getattr__
- NoOpSink is the default (zero overhead when not used)
- Disabled emitter has near-zero overhead
Usage:
from praisonaiagents import Agent
# Simple usage - shows action trace
agent = Agent(instructions="...", output="actions")
agent.start("Do something")
# Advanced - capture to file
from praisonaiagents.trace import ActionTraceConfig
agent = Agent(
instructions="...",
output="actions",
trace=ActionTraceConfig(
sink_type="jsonl",
file_path="trace.jsonl",
)
)
"""
__all__ = [
# Core types
"ActionEvent",
"ActionEventType",
"ActionTraceConfig",
# Sinks
"TraceSink",
"NoOpSink",
"ListSink",
# Emitter
"TraceEmitter",
# Redaction
"redact_dict",
"REDACT_KEYS",
]
def __getattr__(name: str):
"""Lazy load module components to avoid import overhead."""
if name in ("ActionEvent", "ActionEventType", "ActionTraceConfig"):
from .protocol import ActionEvent, ActionEventType, ActionTraceConfig
return locals()[name]
if name in ("TraceSink", "NoOpSink", "ListSink"):
from .protocol import TraceSink, NoOpSink, ListSink
return locals()[name]
if name == "TraceEmitter":
from .protocol import TraceEmitter
return TraceEmitter
if name in ("redact_dict", "REDACT_KEYS"):
from .redact import redact_dict, REDACT_KEYS
return locals()[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,398 @@
"""
Action Trace Protocol for PraisonAI Agents.
Lightweight protocol for tracing agent actions. Uses only stdlib
to ensure zero dependencies and minimal overhead.
Schema Version: 1.0
"""
import json
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
# Schema version for forward compatibility
SCHEMA_VERSION = "1.0"
class ActionEventType(str, Enum):
"""Types of action events."""
AGENT_START = "agent_start"
AGENT_END = "agent_end"
TOOL_START = "tool_start"
TOOL_END = "tool_end"
ERROR = "error"
OUTPUT = "output"
@dataclass
class ActionEvent:
"""
Lightweight action trace event.
Represents a single action in the agent execution trace.
Designed for minimal memory footprint and fast serialization.
"""
event_type: str
timestamp: float
schema_version: str = SCHEMA_VERSION
agent_id: Optional[str] = None
agent_name: Optional[str] = None
tool_name: Optional[str] = None
tool_args: Optional[Dict[str, Any]] = None
tool_result_summary: Optional[str] = None
duration_ms: Optional[float] = None
status: Optional[str] = None # ok, error
error_message: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary, excluding None values."""
result = {
"schema_version": self.schema_version,
"event_type": self.event_type,
"timestamp": self.timestamp,
}
# Add optional fields only if set
if self.agent_id is not None:
result["agent_id"] = self.agent_id
if self.agent_name is not None:
result["agent_name"] = self.agent_name
if self.tool_name is not None:
result["tool_name"] = self.tool_name
if self.tool_args is not None:
result["tool_args"] = self.tool_args
if self.tool_result_summary is not None:
result["tool_result_summary"] = self.tool_result_summary
if self.duration_ms is not None:
result["duration_ms"] = self.duration_ms
if self.status is not None:
result["status"] = self.status
if self.error_message is not None:
result["error_message"] = self.error_message
if self.metadata:
result["metadata"] = self.metadata
return result
def to_json(self) -> str:
"""Convert to JSON string."""
return json.dumps(self.to_dict(), default=str)
@runtime_checkable
class TraceSink(Protocol):
"""
Protocol for trace event sinks.
Implementations receive action events and handle them
(e.g., print to stdout, write to file, send to server).
"""
def emit(self, event: ActionEvent) -> None:
"""Emit an action event."""
...
def flush(self) -> None:
"""Flush any buffered events."""
...
def close(self) -> None:
"""Close the sink and release resources."""
...
class NoOpSink:
"""
No-operation sink that discards all events.
Used as the default sink when tracing is disabled.
Has near-zero overhead.
"""
__slots__ = ()
def emit(self, event: ActionEvent) -> None:
"""Discard the event."""
pass
def flush(self) -> None:
"""No-op."""
pass
def close(self) -> None:
"""No-op."""
pass
class ListSink:
"""
Sink that collects events in a list.
Useful for testing and programmatic access to trace events.
"""
__slots__ = ("_events",)
def __init__(self):
self._events: List[ActionEvent] = []
def emit(self, event: ActionEvent) -> None:
"""Add event to the list."""
self._events.append(event)
def flush(self) -> None:
"""No-op for list sink."""
pass
def close(self) -> None:
"""No-op for list sink."""
pass
def get_events(self) -> List[ActionEvent]:
"""Get all collected events."""
return self._events.copy()
def clear(self) -> None:
"""Clear all collected events."""
self._events.clear()
@dataclass
class ActionTraceConfig:
"""
Configuration for action tracing.
Usage:
# Default (disabled)
config = ActionTraceConfig()
# Enable with stdout
config = ActionTraceConfig(enabled=True, sink_type="stdout")
# Enable with JSONL file
config = ActionTraceConfig(
enabled=True,
sink_type="jsonl",
file_path="trace.jsonl",
)
"""
enabled: bool = True
redact: bool = True
compact: bool = False
sink_type: str = "noop" # noop, stdout, jsonl, file
file_path: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"enabled": self.enabled,
"redact": self.redact,
"compact": self.compact,
"sink_type": self.sink_type,
"file_path": self.file_path,
}
class TraceEmitter:
"""
Emitter for action trace events.
Provides a high-level API for emitting trace events.
Handles redaction and sink management.
Usage:
sink = ListSink()
emitter = TraceEmitter(sink=sink)
emitter.agent_start("researcher")
emitter.tool_start("search", {"query": "AI"})
emitter.tool_end("search", duration_ms=100, status="ok")
emitter.agent_end("researcher", duration_ms=500)
"""
__slots__ = ("_sink", "_enabled", "_redact", "_agent_starts")
def __init__(
self,
sink: Optional[TraceSink] = None,
enabled: bool = True,
redact: bool = True,
):
self._sink = sink or NoOpSink()
self._enabled = enabled
self._redact = redact
self._agent_starts: Dict[str, float] = {} # Track agent start times
def _emit(self, event: ActionEvent) -> None:
"""Internal emit with enabled check."""
if not self._enabled:
return
self._sink.emit(event)
def _redact_args(self, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Redact sensitive data from args if enabled."""
if args is None or not self._redact:
return args
from .redact import redact_dict
return redact_dict(args)
def agent_start(
self,
agent_name: str,
agent_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit agent start event."""
ts = time.time()
self._agent_starts[agent_name] = ts
self._emit(ActionEvent(
event_type=ActionEventType.AGENT_START.value,
timestamp=ts,
agent_name=agent_name,
agent_id=agent_id,
metadata=metadata or {},
))
def agent_end(
self,
agent_name: str,
agent_id: Optional[str] = None,
duration_ms: Optional[float] = None,
status: str = "ok",
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit agent end event."""
ts = time.time()
# Calculate duration if not provided
if duration_ms is None and agent_name in self._agent_starts:
start_ts = self._agent_starts.pop(agent_name, None)
if start_ts:
duration_ms = (ts - start_ts) * 1000
self._emit(ActionEvent(
event_type=ActionEventType.AGENT_END.value,
timestamp=ts,
agent_name=agent_name,
agent_id=agent_id,
duration_ms=duration_ms,
status=status,
metadata=metadata or {},
))
def tool_start(
self,
tool_name: str,
tool_args: Optional[Dict[str, Any]] = None,
agent_name: Optional[str] = None,
agent_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit tool start event."""
self._emit(ActionEvent(
event_type=ActionEventType.TOOL_START.value,
timestamp=time.time(),
tool_name=tool_name,
tool_args=self._redact_args(tool_args),
agent_name=agent_name,
agent_id=agent_id,
metadata=metadata or {},
))
def tool_end(
self,
tool_name: str,
duration_ms: float,
status: str = "ok",
result_summary: Optional[str] = None,
agent_name: Optional[str] = None,
agent_id: Optional[str] = None,
error_message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit tool end event."""
self._emit(ActionEvent(
event_type=ActionEventType.TOOL_END.value,
timestamp=time.time(),
tool_name=tool_name,
duration_ms=duration_ms,
status=status,
tool_result_summary=result_summary,
agent_name=agent_name,
agent_id=agent_id,
error_message=error_message,
metadata=metadata or {},
))
def error(
self,
message: str,
tool_name: Optional[str] = None,
agent_name: Optional[str] = None,
agent_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit error event."""
self._emit(ActionEvent(
event_type=ActionEventType.ERROR.value,
timestamp=time.time(),
error_message=message,
tool_name=tool_name,
agent_name=agent_name,
agent_id=agent_id,
status="error",
metadata=metadata or {},
))
def output(
self,
content: str,
agent_name: Optional[str] = None,
agent_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Emit final output event."""
# Truncate long output for summary
summary = content[:500] + "..." if len(content) > 500 else content
self._emit(ActionEvent(
event_type=ActionEventType.OUTPUT.value,
timestamp=time.time(),
tool_result_summary=summary,
agent_name=agent_name,
agent_id=agent_id,
metadata=metadata or {},
))
def flush(self) -> None:
"""Flush the sink."""
self._sink.flush()
def close(self) -> None:
"""Close the sink."""
self._sink.close()
# Global default emitter (NoOp by default)
_default_emitter: Optional[TraceEmitter] = None
def get_default_emitter() -> TraceEmitter:
"""Get the default trace emitter."""
global _default_emitter
if _default_emitter is None:
_default_emitter = TraceEmitter(sink=NoOpSink(), enabled=False)
return _default_emitter
def set_default_emitter(emitter: TraceEmitter) -> None:
"""Set the default trace emitter."""
global _default_emitter
_default_emitter = emitter
@@ -0,0 +1,149 @@
"""
Redaction utilities for action trace.
Provides deterministic redaction of sensitive data in tool arguments
and results. Uses only stdlib for zero dependencies.
"""
from typing import Any, Dict, Set
import re
# Keys that should always be redacted (case-insensitive matching)
REDACT_KEYS: Set[str] = {
# API keys and tokens
"api_key",
"apikey",
"api-key",
"secret",
"secret_key",
"secretkey",
"private_key",
"privatekey",
# Authentication
"password",
"passwd",
"pwd",
"token",
"bearer",
"authorization",
"auth",
"auth_token",
"access_token",
"refresh_token",
# Session and cookies
"cookie",
"session",
"session_id",
"sessionid",
"credential",
"credentials",
# Database
"connection_string",
"connectionstring",
"database_url",
"db_url",
"dburl",
"db_password",
"db_pass",
# Cloud provider specific
"aws_secret",
"aws_secret_access_key",
"azure_key",
"gcp_key",
"openai_api_key",
"anthropic_api_key",
}
# Compiled regex for case-insensitive matching
_REDACT_PATTERN = re.compile(
r"(" + "|".join(re.escape(k) for k in REDACT_KEYS) + r")",
re.IGNORECASE
)
REDACTED_VALUE = "[REDACTED]"
def _should_redact(key: str) -> bool:
"""Check if a key should be redacted."""
# Normalize key for comparison
normalized = key.lower().replace("-", "_").replace(" ", "_")
# Check exact match first
if normalized in REDACT_KEYS:
return True
# Check if any redact key is contained in the normalized key
for redact_key in REDACT_KEYS:
if redact_key in normalized:
return True
return False
def redact_dict(data: Dict[str, Any], enabled: bool = True) -> Dict[str, Any]:
"""
Redact sensitive values from a dictionary.
Args:
data: Dictionary to redact
enabled: If False, returns data unchanged
Returns:
New dictionary with sensitive values redacted
"""
if not enabled:
return data
if not data:
return {}
return _redact_value(data)
def _redact_value(value: Any) -> Any:
"""Recursively redact sensitive values."""
if isinstance(value, dict):
return {k: _redact_key_value(k, v) for k, v in value.items()}
elif isinstance(value, list):
return [_redact_value(item) for item in value]
else:
return value
def _redact_key_value(key: str, value: Any) -> Any:
"""Redact a value if its key is sensitive."""
if _should_redact(key):
return REDACTED_VALUE
# Recursively process nested structures
return _redact_value(value)
def redact_string(text: str, enabled: bool = True) -> str:
"""
Redact potential secrets from a string.
This is a best-effort function that looks for common patterns
like "api_key=xxx" or "password: xxx" in strings.
Args:
text: String to redact
enabled: If False, returns text unchanged
Returns:
String with potential secrets redacted
"""
if not enabled or not text:
return text
# Pattern for key=value or key: value
patterns = [
(r'(["\']?)(' + '|'.join(re.escape(k) for k in REDACT_KEYS) + r')(["\']?\s*[:=]\s*["\']?)([^"\'\s,}\]]+)',
r'\1\2\3[REDACTED]'),
]
result = text
for pattern, replacement in patterns:
result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
return result
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""
Detailed profiler to identify the exact source of streaming delay.
Traces every step from agent.start() to HTTP request.
"""
import os
import sys
import time
import functools
os.environ['LOGLEVEL'] = 'DEBUG'
# Microsecond precision timestamps
def ts():
return time.perf_counter()
start_time = ts()
def log(msg):
elapsed = (ts() - start_time) * 1000
print(f"[{elapsed:8.2f}ms] {msg}")
log("Script starting...")
# ============================================================
# PATCH 1: OpenAI client initialization
# ============================================================
import openai
original_openai_init = openai.OpenAI.__init__
@functools.wraps(original_openai_init)
def traced_openai_init(self, *args, **kwargs):
t0 = ts()
result = original_openai_init(self, *args, **kwargs)
log(f"OpenAI.__init__() took {(ts()-t0)*1000:.2f}ms")
return result
openai.OpenAI.__init__ = traced_openai_init
# ============================================================
# PATCH 2: OpenAI completions.create
# ============================================================
original_create = openai.resources.chat.completions.Completions.create
@functools.wraps(original_create)
def traced_create(self, *args, **kwargs):
log(f">>> OpenAI.create() CALLED - stream={kwargs.get('stream', False)}")
t0 = ts()
result = original_create(self, *args, **kwargs)
log(f"<<< OpenAI.create() RETURNED in {(ts()-t0)*1000:.2f}ms")
return result
openai.resources.chat.completions.Completions.create = traced_create
# ============================================================
# PATCH 3: httpx.Client.send (actual HTTP request)
# ============================================================
import httpx
original_send = httpx.Client.send
@functools.wraps(original_send)
def traced_send(self, request, *args, **kwargs):
log(f">>> HTTPX.send() {request.method} {request.url}")
t0 = ts()
result = original_send(self, request, *args, **kwargs)
log(f"<<< HTTPX.send() returned in {(ts()-t0)*1000:.2f}ms - status={result.status_code}")
return result
httpx.Client.send = traced_send
# ============================================================
# PATCH 4: Rich Live display
# ============================================================
from rich.live import Live
original_live_init = Live.__init__
original_live_enter = Live.__enter__
original_live_exit = Live.__exit__
@functools.wraps(original_live_init)
def traced_live_init(self, *args, **kwargs):
log(f">>> Live.__init__() called")
t0 = ts()
result = original_live_init(self, *args, **kwargs)
log(f"<<< Live.__init__() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_live_enter)
def traced_live_enter(self):
log(f">>> Live.__enter__() called")
t0 = ts()
result = original_live_enter(self)
log(f"<<< Live.__enter__() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_live_exit)
def traced_live_exit(self, *args):
log(f">>> Live.__exit__() called")
t0 = ts()
result = original_live_exit(self, *args)
log(f"<<< Live.__exit__() took {(ts()-t0)*1000:.2f}ms")
return result
Live.__init__ = traced_live_init
Live.__enter__ = traced_live_enter
Live.__exit__ = traced_live_exit
log("Patches applied, importing praisonaiagents...")
# ============================================================
# Import and trace Agent
# ============================================================
t0 = ts()
from praisonaiagents import Agent
log(f"Agent import took {(ts()-t0)*1000:.2f}ms")
# ============================================================
# PATCH 5: Agent methods
# ============================================================
from praisonaiagents.agent.agent import Agent as AgentClass
original_chat_completion = AgentClass._chat_completion
original_format_tools = AgentClass._format_tools_for_completion
@functools.wraps(original_chat_completion)
def traced_chat_completion(self, messages, temperature=1.0, tools=None, stream=True,
reasoning_steps=False, task_name=None, task_description=None, task_id=None):
log(f">>> Agent._chat_completion() stream={stream}, _using_custom_llm={self._using_custom_llm}")
t0 = ts()
result = original_chat_completion(self, messages, temperature, tools, stream,
reasoning_steps, task_name, task_description, task_id)
log(f"<<< Agent._chat_completion() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_format_tools)
def traced_format_tools(self, tools):
log(f">>> Agent._format_tools_for_completion() tools={tools}")
t0 = ts()
result = original_format_tools(self, tools)
log(f"<<< Agent._format_tools_for_completion() took {(ts()-t0)*1000:.2f}ms")
return result
AgentClass._chat_completion = traced_chat_completion
AgentClass._format_tools_for_completion = traced_format_tools
# ============================================================
# PATCH 6: OpenAIClient methods
# ============================================================
from praisonaiagents.llm.openai_client import OpenAIClient
original_chat_with_tools = OpenAIClient.chat_completion_with_tools
original_process_stream = OpenAIClient.process_stream_response
original_create_completion = OpenAIClient.create_completion
original_format_tools_client = OpenAIClient.format_tools
@functools.wraps(original_chat_with_tools)
def traced_chat_with_tools(self, *args, **kwargs):
log(f">>> OpenAIClient.chat_completion_with_tools() stream={kwargs.get('stream', True)}")
t0 = ts()
result = original_chat_with_tools(self, *args, **kwargs)
log(f"<<< OpenAIClient.chat_completion_with_tools() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_process_stream)
def traced_process_stream(self, *args, **kwargs):
log(f">>> OpenAIClient.process_stream_response()")
t0 = ts()
result = original_process_stream(self, *args, **kwargs)
log(f"<<< OpenAIClient.process_stream_response() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_create_completion)
def traced_create_completion(self, *args, **kwargs):
log(f">>> OpenAIClient.create_completion() stream={kwargs.get('stream', False)}")
t0 = ts()
result = original_create_completion(self, *args, **kwargs)
log(f"<<< OpenAIClient.create_completion() took {(ts()-t0)*1000:.2f}ms")
return result
@functools.wraps(original_format_tools_client)
def traced_format_tools_client(self, tools):
log(f">>> OpenAIClient.format_tools() tools count={len(tools) if tools else 0}")
t0 = ts()
result = original_format_tools_client(self, tools)
log(f"<<< OpenAIClient.format_tools() took {(ts()-t0)*1000:.2f}ms")
return result
OpenAIClient.chat_completion_with_tools = traced_chat_with_tools
OpenAIClient.process_stream_response = traced_process_stream
OpenAIClient.create_completion = traced_create_completion
OpenAIClient.format_tools = traced_format_tools_client
# ============================================================
# PATCH 7: sync_client property (lazy init)
# ============================================================
original_sync_client_getter = OpenAIClient.sync_client.fget
def traced_sync_client_getter(self):
log(f">>> OpenAIClient.sync_client property accessed")
t0 = ts()
result = original_sync_client_getter(self)
log(f"<<< OpenAIClient.sync_client took {(ts()-t0)*1000:.2f}ms")
return result
OpenAIClient.sync_client = property(traced_sync_client_getter)
log("All patches applied. Creating agent...")
# ============================================================
# RUN THE TEST
# ============================================================
t0 = ts()
agent = Agent(
instructions="You are a helpful assistant"
)
log(f"Agent created in {(ts()-t0)*1000:.2f}ms")
log(f"Agent.stream={agent.stream}, Agent.verbose={agent.verbose}")
log("=" * 60)
log("Starting agent.start()...")
log("=" * 60)
t0 = ts()
result = agent.start("Say hello in one word")
log(f"agent.start() completed in {(ts()-t0)*1000:.2f}ms")
log("=" * 60)
log("DONE")
log("=" * 60)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.11.1"
version = "0.11.19"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -4,7 +4,7 @@ import os
sequential_agent = Agent(
instructions="""You are a helpful assistant that can break down complex problems.
Use the available tools when relevant to perform step-by-step analysis.""",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP("npx -y @modelcontextprotocol/server-sequential-thinking")
)
@@ -0,0 +1,24 @@
"""
Streaming Example with TTFT Metrics
This example demonstrates true pass-through streaming with timing metrics.
When verbose output is enabled, tokens are displayed as they arrive from the provider.
Key timing metrics:
- TTFT (Time To First Token): Time from request to first token received
- Stream Duration: Time from first to last token
- Total Time: End-to-end request time
"""
from praisonaiagents import Agent
from praisonaiagents.config import OutputConfig
# Create agent with verbose output (shows streaming progress)
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-4o-mini",
output=OutputConfig(verbose=True) # Show generation progress
)
# Run the agent - verbose mode shows streaming progress via Rich Live display
result = agent.start("Write a short paragraph about the history of computing")
print(f"\n--- Response ---\n{result}")
-11
View File
@@ -1,11 +0,0 @@
from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-4o-mini"
)
# Use chat method with stream=True for streaming responses
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
print(chunk, end="", flush=True)
print() # Final newline
+1 -1
View File
@@ -13,7 +13,7 @@ allowed_dirs = [
filesystem_agent = Agent(
instructions="""You are a helpful assistant that can interact with the filesystem.
Use the available tools when relevant to manage files and directories.""",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem"] + allowed_dirs
+1 -1
View File
@@ -5,7 +5,7 @@ from praisonaiagents import Agent
print("Creating agent...")
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
print("Starting agent...")
@@ -40,7 +40,7 @@ def test_agent_lazy_loading():
role="Assistant",
goal="Help users",
backstory="I am a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
init_time = time.time() - start
print(f" Agent initialization: {init_time:.4f}s")
@@ -67,7 +67,7 @@ def test_agent_system_prompt_cache():
goal="Test caching",
backstory="I test caches",
tools=[calculator],
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# First build - no cache
@@ -98,7 +98,7 @@ def test_agent_tool_formatting_cache():
goal="Use tools",
backstory="I use tools",
tools=[calculator, sample_tool],
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
# First format - no cache
@@ -412,7 +412,7 @@ def test_console_lazy_loading():
print("\n13. Testing console lazy loading across all classes...")
# Test Agent console
agent = Agent(instructions="Test", llm="gpt-5-nano")
agent = Agent(instructions="Test", llm="gpt-4o-mini")
assert agent._console is None, "Agent console should not be initialized"
console1 = agent.console
assert agent._console is not None, "Agent console should be initialized"
@@ -448,7 +448,7 @@ def test_with_openai_basic():
start = time.time()
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
agent_init = time.time() - start
print(f" Agent initialization time: {agent_init:.4f}s")
@@ -484,7 +484,7 @@ def benchmark_agent_creation(num_agents=5):
goal="Help users",
backstory="I am a helpful assistant",
tools=[calculator] if i % 2 == 0 else None, # Half with tools
llm="gpt-5-nano"
llm="gpt-4o-mini"
)
creation_time = time.time() - start
times.append(creation_time)
+1 -1
View File
@@ -11,7 +11,7 @@ def test_agent_direct():
agent = Agent(
instructions="You are a helpful assistant that can break down complex problems.",
llm="gpt-5-nano",
llm="gpt-4o-mini",
tools=MCP("npx -y @modelcontextprotocol/server-sequential-thinking")
)
+1 -1
View File
@@ -34,7 +34,7 @@ agent = Agent(
role="Assistant",
goal="Help with tasks",
backstory="I am a helpful assistant",
llm="gpt-5-nano", # Using a simple model for testing
llm="gpt-4o-mini", # Using a simple model for testing
verbose=False
)
@@ -43,7 +43,7 @@ def test_single_display_no_tools():
MagicMock(choices=[MagicMock(delta=MagicMock(content="!"))])
]
llm = LLM(model="gpt-5-nano", verbose=False)
llm = LLM(model="gpt-4o-mini", verbose=False)
response = llm.get_response(
prompt="Test prompt",
verbose=True,
@@ -139,7 +139,7 @@ def test_single_display_with_self_reflection():
mock_completion.side_effect = mock_streaming
llm = LLM(model="gpt-5-nano", verbose=False, self_reflect=True, min_reflect=1, max_reflect=2)
llm = LLM(model="gpt-4o-mini", verbose=False, self_reflect=True, min_reflect=1, max_reflect=2)
response = llm.get_response(
prompt="Test prompt",
verbose=True,
@@ -179,7 +179,7 @@ def test_async_single_display():
mock_acompletion.return_value = async_generator()
llm = LLM(model="gpt-5-nano", verbose=False)
llm = LLM(model="gpt-4o-mini", verbose=False)
response = await llm.get_response_async(
prompt="Test async",
verbose=True,
+1 -1
View File
@@ -14,7 +14,7 @@ def test_self_reflection_fix():
goal="Solve math problems accurately",
backstory="You are a helpful math assistant",
self_reflect=True,
llm="gpt-5-nano", # Use a more widely available model
llm="gpt-4o-mini", # Use a more widely available model
verbose=True,
tools=[evaluate],
min_reflect=1,
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff. Show more