Compare commits

...
48 Commits
Author SHA1 Message Date
Mervin Praison 657fd93743 Release v3.9.2 2026-01-16 04:37:53 +00:00
github-actions[bot] f0a2936b7f docs: auto-update api.md [skip ci] 2026-01-15 20:08:49 +00:00
Mervin Praison 58778492ca Release v3.9.1 2026-01-15 20:08:35 +00:00
Mervin Praison 08c444f3a8 Release v3.9.0 2026-01-15 16:05:47 +00:00
Mervin Praison 34c1b39e95 Release v3.8.22 2026-01-15 13:01:10 +00:00
Mervin Praison 5e4c58a9ac fix(ci): correct CLI commands - session list, queue list (not ls) 2026-01-15 11:06:32 +00:00
Mervin Praison 19a30ff3d9 fix(ci): fix Handoff test - use correct parameters 2026-01-15 11:00:32 +00:00
Mervin Praison 50f12c7682 fix(ci): correct PlanningConfig params (reasoning, auto_approve) 2026-01-15 10:57:50 +00:00
Mervin Praison 5a70b8abb9 fix(ci): correct ExecutionConfig params (max_iter, max_retry_limit) 2026-01-15 10:55:33 +00:00
Mervin Praison b7d4d219b9 fix(ci): correct MemoryConfig test to use backend param 2026-01-15 10:52:56 +00:00
Mervin Praison ff2d83ffdf Release v3.8.21 2026-01-15 10:50:27 +00:00
Mervin Praison 2d58caa741 fix(ci): correct import paths for Windows tests
- MemoryConfig from praisonaiagents.config (not memory)
- FileMemory from praisonaiagents.memory
- OutputConfig, ExecutionConfig, PlanningConfig from config
2026-01-15 10:50:25 +00:00
Mervin Praison d31cf379e5 feat(ci): expand Windows tests with Memory, Tools, Config, Handoff, async
Added tests for:
- Memory: FileMemory, MemoryConfig
- Tool decorator (@tool)
- Config classes: OutputConfig, ExecutionConfig, PlanningConfig
- Handoff between agents
- Async features (arun, astart availability)
- Multi-agent real API test
2026-01-15 10:47:11 +00:00
Mervin Praison 719ef04e7c fix(memory): make fcntl import conditional for Windows compatibility
fcntl is Unix-only; on Windows, skip file locking.
This fixes ModuleNotFoundError: No module named 'fcntl' on Windows.

- Add _HAS_FCNTL flag based on sys.platform
- Update _read_json and _write_json to conditionally use fcntl.flock
- File locking is acceptable to skip for single-process usage
2026-01-15 10:38:45 +00:00
Mervin Praison 4fb25a99d5 fix(ci): fix Windows workflow YAML syntax and secrets condition 2026-01-15 10:35:18 +00:00
Mervin Praison 0fdacff2cf feat(ci): add Windows CI test workflow
Tests key PraisonAI functionality on Windows:
- Core imports: Agent, Agents, Workflow, Task, WorkflowStep
- Object instantiation
- CLI commands: --help, session ls, queue ls
- Real API test with OPENAI_API_KEY

Timeout: 5 minutes
Runs on: windows-latest
Triggers: push/PR to main/develop + manual dispatch
2026-01-15 10:33:56 +00:00
Mervin Praison 6a1fa19c6e Release v3.8.20 2026-01-15 09:45:45 +00:00
Mervin Praison a4725a027a Release v3.8.19 2026-01-15 07:29:27 +00:00
Mervin Praison dc9a8f3443 Release v3.8.18 2026-01-15 04:41:16 +00:00
Mervin Praison 9421e28c8a Release v3.8.17 2026-01-13 18:29:06 +00:00
Mervin Praison c3d60c82ca feat: add debug output with token/cost metrics
- Added llm_end callback to openai_client.py and llm.py with:
  - tokens_in, tokens_out from usage
  - cost from litellm.completion_cost()
  - latency_ms timing
- Updated trace.py and status.py to display metrics
- Added show_metrics param to enable debug mode metrics display
- Agent.py passes metrics flag to enable_status_output

Debug output now shows:
  [17:18:22] Calling LLM (gpt-4o-mini)...
  [17:18:24]   │ 📊 gpt-4o-mini: 104→14 tokens [1.5s]
  [17:18:24] ▸ get_weather(city='Tokyo')
  [17:18:24] Calling LLM (gpt-4o-mini)...
  [17:18:25]   │ 📊 gpt-4o-mini: 133→8 tokens [0.7s]
  Response: The weather in Tokyo is sunny.
2026-01-13 17:20:06 +00:00
Mervin Praison 8c509b439f Release v3.8.16 2026-01-13 16:55:40 +00:00
Mervin Praison 3fbf1a353e fix: consistent status output for all LLM paths (OpenAI + LiteLLM)
- Fixed _execute_callback_and_display to always trigger callback
  (removed incorrect 'if not _using_custom_llm' guard)
- Added tool_call callback to llm.py for LiteLLM path
- Removed duplicate llm_start callback from llm.py method entry
- Reset AI call counter between agent runs
- Both sync LLM paths now show identical output

Verified output for both paths:
  ▸ AI → thinking...
    │ → get_weather(city="Tokyo") → sunny
  ▸ AI → responding...
  Final Output: ...
2026-01-13 16:45:08 +00:00
Mervin Praison e0f3a49e41 improve: status output UX - cleaner, user-friendly format
- Changed 'LLM' to 'AI' for user-friendliness
- Added context: 'thinking' (1st call) / 'responding' (2nd call)
- Tool calls now show inline with result: │ → tool(args) → result
- Fixed openai_client to pass structured params to callbacks

Output now looks like:
  ▸ AI → thinking...
    │ → get_weather(city="Tokyo") → sunny
  ▸ AI → responding...
  Final Output: ...
2026-01-13 16:36:51 +00:00
Mervin Praison 987b43877f fix: show all LLM calls in status output (including tool follow-ups)
- Added llm_start callback in openai_client.py while loop
- Each LLM API call now triggers callback for status display
- Removed duplicate callback from agent._chat_completion

Status output now correctly shows:
  ▸ Calling LLM (gpt-4o-mini)...       # 1st: decides to call tool
  │ → get_weather(city="Tokyo")
  ▸ Calling LLM (gpt-4o-mini)...       # 2nd: generates response
  Final Output: ...
2026-01-13 16:18:53 +00:00
Mervin Praison ebb383208e feat: add LLM call display to status/trace output
- Added llm_start callback in agent._chat_completion()
- Registered on_llm_start callback in enable_status_output()
- StatusOutput.llm_start() displays '▸ Calling LLM (model)...'
- Works for both LiteLLM and OpenAI client paths

Status output now shows:
  ▸ Calling LLM (openai/gpt-4o-mini)...
  │ → get_weather(city="Tokyo")
  Final Output: ...
2026-01-13 16:10:46 +00:00
Mervin Praison c9e4f0738a refactor: rename output modules for user-friendliness
- Rename actions.py → status.py (handles status preset, no timestamps)
- Rename old status.py → trace.py (handles trace preset, with timestamps)
- ActionsSink → StatusOutput
- StatusSink → TraceOutput
- enable_actions_mode → enable_status_output
- enable_status_mode → enable_trace_output

This makes internal code match user-facing preset names:
  output='status' → StatusOutput (▸ tool → result)
  output='trace' → TraceOutput ([HH:MM:SS] ▸ tool [0.2s])
2026-01-13 15:59:42 +00:00
Mervin Praison db0a24555d fix: status preset correctly hides timestamps, add LLM call methods
- Added show_timestamps param to ActionsSink (default True)
- Agent passes show_timestamps=False for status preset (simple_output=True)
- Added llm_start/llm_end methods to ActionsSink for future LLM tracking
- status: │ → tool() (no timestamps)
- trace: [HH:MM:SS] ▸ tool [0.2s] ✓ (with timestamps)
2026-01-13 15:51:51 +00:00
Mervin Praison e3fe35e2b6 fix: status preset now hides timestamps correctly
- Added show_timestamps param to ActionsSink
- Agent passes show_timestamps=False when simple_output=True
- status: │ → tool() (no timestamps)
- trace: [HH:MM:SS] ▸ tool [0.2s] ✓ (with timestamps)
2026-01-13 15:46:57 +00:00
Mervin Praison 74a7322b83 refactor: rename output presets for clarity
- Rename 'text' → 'status' (no timestamps, simple progress)
- Rename old 'status' → 'trace' (with timestamps, full trace)
- Change 'debug' = trace + metrics (no boxes, developer-focused)
- Keep 'verbose' = rich panels (interactive use)
- Add 'text' as backward compat alias → status

New hierarchy (most intuitive):
  silent → status → trace → debug → verbose → stream → json
2026-01-13 15:10:05 +00:00
Mervin Praison 7ceb7f8cb9 Release v3.8.14 2026-01-13 14:46:59 +00:00
Mervin Praison 84e5b6c693 fix: Agents panels now render Markdown (bold, headers, lists)
Changed Text() to Markdown() in agents.start() verbose output so
**bold**, # headers, - lists render properly in multi-agent panels.
2026-01-13 14:44:17 +00:00
Mervin Praison 5f00ddbc7c Release v3.8.13 2026-01-13 14:43:54 +00:00
Mervin Praison db551c5d65 feat: add Markdown rendering for status output mode
- Added use_markdown=True parameter to StatusSink
- response() now renders content with rich.markdown.Markdown
- Lazy import for performance, graceful fallback
2026-01-13 14:39:53 +00:00
Mervin Praison aed6027256 Release v3.8.12 2026-01-13 14:33:31 +00:00
Mervin Praison a9d709645a fix: respect explicit output config, don't override with TTY auto-verbose
- Added _has_explicit_output_config flag to track when user sets output=
- Modified agent.start() to skip TTY auto-verbose if user explicitly configured output
- Fixes issue where output='status' was overridden by TTY auto-verbose showing panels

Before: output='status' in TTY showed panels (wrong)
After: output='status' in TTY shows clean inline status (correct)
2026-01-13 14:30:56 +00:00
Mervin Praison a430e1b873 Release v3.8.11 2026-01-13 14:21:02 +00:00
Mervin Praison 8e0a40db56 Release v3.8.10 2026-01-13 13:55:31 +00:00
Mervin Praison 8d1e4637d4 feat: add status output mode for clean inline progress
- Add new 'status' preset showing [HH:MM:SS] ▸ tool → result ✓
- Create output/status.py with StatusSink class
- Add status_trace field to OutputConfig
- Update presets.py with full preset hierarchy
- Consolidate presets: text, status, verbose, debug, stream, json
- Add backward compatible aliases (plain→silent, minimal→silent, etc.)

Fixes confusing verbose output by providing clean alternative
2026-01-13 13:52:05 +00:00
Mervin Praison e11d62dc89 Release v3.8.9 2026-01-13 13:14:04 +00:00
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
313 changed files with 15437 additions and 7913 deletions

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
name: Windows Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
jobs:
test-windows:
runs-on: windows-latest
timeout-minutes: 5
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install praisonaiagents
run: |
python -m pip install --upgrade pip
cd src/praisonai-agents
pip install .
shell: bash
- name: Install praisonai wrapper
run: |
cd src/praisonai
pip install .[dev]
shell: bash
# ==================== CORE IMPORTS ====================
- name: Test core imports
run: |
python -c "from praisonaiagents import Agent, Agents, Workflow; print('Core imports OK')"
python -c "from praisonaiagents import tool, Tools; print('Tool imports OK')"
python -c "from praisonaiagents import Task, WorkflowStep; print('Task/Step imports OK')"
# ==================== AGENT FEATURES ====================
- name: Test Agent instantiation
run: |
python -c "
from praisonaiagents import Agent
a = Agent(name='Test', instructions='You are helpful')
print('Agent created:', a.name)
a2 = Agent(name='ToolAgent', instructions='Use tools', tools=[])
print('Agent with tools:', a2.name)
"
- name: Test Agents orchestration
run: |
python -c "
from praisonaiagents import Agent, Agents, Task
a1 = Agent(name='Agent1', instructions='First agent')
a2 = Agent(name='Agent2', instructions='Second agent')
t = Task(description='Do something', agent=a1)
agents = Agents(agents=[a1, a2], tasks=[t])
print('Agents:', len(agents.agents), 'Tasks:', len(agents.tasks))
"
# ==================== WORKFLOW FEATURES ====================
- name: Test Workflow and WorkflowStep
run: |
python -c "
from praisonaiagents import Workflow, WorkflowStep
from praisonaiagents.workflows import WorkflowOutputConfig
w = Workflow(name='test')
s1 = WorkflowStep(name='step1')
s2 = WorkflowStep(name='step2', context=['step1'])
print('Workflow:', w.name)
print('Steps:', s1.name, s2.name)
cfg = WorkflowOutputConfig(verbose=True, stream=False)
w2 = Workflow(name='configured', output=cfg)
print('Configured workflow verbose:', w2.verbose)
"
# ==================== MEMORY FEATURES ====================
- name: Test Memory classes
run: |
python -c "
from praisonaiagents.memory import FileMemory
from praisonaiagents.config import MemoryConfig
cfg = MemoryConfig(backend='file', user_id='test_user')
print('MemoryConfig backend:', cfg.backend)
mem = FileMemory(user_id='test_user', base_path='.praison/test_memory')
print('FileMemory user:', mem.user_id)
"
# ==================== TOOL DECORATOR ====================
- name: Test tool decorator
run: |
python -c "
from praisonaiagents import tool
@tool
def my_tool(query: str) -> str:
'''A test tool'''
return f'Echo: {query}'
result = my_tool('hello')
print('Tool result:', result)
"
# ==================== CONFIG CLASSES ====================
- name: Test config classes
run: |
python -c "
from praisonaiagents.config import OutputConfig, ExecutionConfig, PlanningConfig
out_cfg = OutputConfig(verbose=True, stream=True)
print('OutputConfig verbose:', out_cfg.verbose)
exec_cfg = ExecutionConfig(max_iter=50, max_retry_limit=5)
print('ExecutionConfig max_iter:', exec_cfg.max_iter)
plan_cfg = PlanningConfig(reasoning=True, auto_approve=False)
print('PlanningConfig reasoning:', plan_cfg.reasoning)
"
# ==================== HANDOFF ====================
- name: Test Handoff
run: |
python -c "
from praisonaiagents import Handoff, handoff, Agent
a1 = Agent(name='Agent1', instructions='First')
a2 = Agent(name='Agent2', instructions='Second')
# Using Handoff class directly
h = Handoff(agent=a2)
print('Handoff tool name:', h.tool_name)
# Using handoff function
h2 = handoff(a1, tool_name_override='custom_handoff')
print('Custom handoff name:', h2.tool_name)
"
# ==================== ASYNC FEATURES ====================
- name: Test async imports
run: |
python -c "
import asyncio
from praisonaiagents import Agent
async def test_async():
agent = Agent(name='AsyncAgent', instructions='Test')
assert hasattr(agent, 'arun'), 'Missing arun'
assert hasattr(agent, 'astart'), 'Missing astart'
print('Async methods available')
asyncio.run(test_async())
"
# ==================== CLI COMMANDS ====================
- name: Test CLI help
run: praisonai --help
- name: Test CLI session list
run: praisonai session list
continue-on-error: true
- name: Test CLI queue list
run: praisonai queue list
continue-on-error: true
# ==================== REAL API TESTS ====================
- name: Real API test - Agent run
if: env.OPENAI_API_KEY != ''
run: |
python -c "
from praisonaiagents import Agent
agent = Agent(name='TestAgent', instructions='Reply with exactly: WINDOWS_OK', llm='gpt-4o-mini')
result = agent.run('Say WINDOWS_OK')
print('Result:', result)
print('Real API test passed')
"
- name: Real API test - Multi-agent
if: env.OPENAI_API_KEY != ''
run: |
python -c "
from praisonaiagents import Agent, Agents, Task
researcher = Agent(name='Researcher', instructions='You research topics', llm='gpt-4o-mini')
writer = Agent(name='Writer', instructions='You write content', llm='gpt-4o-mini')
task = Task(description='Say hello world', agent=researcher)
agents = Agents(agents=[researcher, writer], tasks=[task])
result = agents.start()
print('Multi-agent result type:', type(result))
print('Multi-agent test passed')
"
+1 -1
View File
@@ -23,7 +23,7 @@
</div>
PraisonAI is a production-ready Multi-AI Agents framework with self-reflection, designed to create AI Agents to automate and solve problems ranging from simple tasks to complex challenges. By integrating PraisonAI Agents, AG2 (Formerly AutoGen), and CrewAI into a low-code solution, it streamlines the building and management of multi-agent LLM systems, emphasising simplicity, customisation, and effective human-agent collaboration.
PraisonAI is a production-ready Multi-AI Agents framework with self-reflection, designed to create AI Agents to automate and solve problems ranging from simple tasks to complex challenges. It provides a low-code solution to streamline the building and management of multi-agent LLM systems, emphasising simplicity, customisation, and effective human-agent collaboration.
<div align="center">
<a href="https://docs.praison.ai">
+8 -448
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:
@@ -77,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>
@@ -117,462 +101,35 @@ 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">get_learn_context</a>() -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</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, warmup
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>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">store_in_memory</a>(content: str, agent_name: str = None, task_id: str = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/__init__.py">warmup</a>(include_litellm: bool = False, include_openai: bool = True) -> dict</code>
# Wrapper (praisonai)
@@ -642,6 +199,7 @@ Methods:
* <code title="cli">praisonai docs run-all <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai doctor cleanup <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor config <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor db <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor doctor-callback <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
@@ -651,6 +209,7 @@ Methods:
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor tools <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor troubleshoot <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai endpoints list <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai endpoints test <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai environment check <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
@@ -681,6 +240,7 @@ Methods:
* <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 run <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>
+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.8.2" \
"praisonai>=3.9.2" \
"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.8.2" \
"praisonai>=3.9.2" \
"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.8.2" \
"praisonai>=3.9.2" \
"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:
+148
View File
@@ -0,0 +1,148 @@
"""
Multi-Server MCP Example.
This example demonstrates how to use multiple MCP servers with PraisonAI Agents,
including mixing MCP tools with regular Python function tools.
Requirements:
- Node.js and npx installed (or uvx for Python-based servers)
- pip install praisonaiagents[mcp]
- OPENAI_API_KEY environment variable set
Usage:
python multi_server.py
"""
from praisonaiagents import Agent, MCP
import os
def calculate(expression: str) -> str:
"""
A simple calculator tool that evaluates mathematical expressions.
Args:
expression: A mathematical expression to evaluate (e.g., "2 + 2")
Returns:
The result of the calculation
"""
try:
# Safe evaluation of mathematical expressions
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "Error: Invalid characters in expression"
result = eval(expression)
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
def main():
# Check for API key
if not os.environ.get("OPENAI_API_KEY"):
print("Please set OPENAI_API_KEY environment variable")
return
print("=" * 60)
print("Multi-Server MCP Example")
print("=" * 60)
# Create MCP instance with Time server
print("\nInitializing MCP servers...")
try:
time_mcp = MCP("uvx mcp-server-time", timeout=30)
print("✓ Time MCP server initialized")
except Exception as e:
print(f"✗ Time MCP server failed: {e}")
return
# Create an agent with multiple tool sources
agent = Agent(
name="MultiToolAgent",
instructions="""You are a helpful assistant with access to multiple tools:
1. Time tools - for getting current time in different timezones
2. Calculator - for mathematical calculations
Use the appropriate tool based on the user's request.""",
llm="openai/gpt-4o-mini",
tools=[time_mcp, calculate] # Mix MCP and regular tools
)
# Show available tools
print("\nAvailable tools:")
formatted_tools = agent._format_tools_for_completion(agent.tools)
for tool in formatted_tools:
print(f" - {tool['function']['name']}: {tool['function']['description'][:50]}...")
# Example queries
queries = [
"What time is it in Tokyo?",
"Calculate 15 * 7 + 23",
"What's the current time in New York and London?"
]
print("\n" + "=" * 60)
print("Running queries...")
print("=" * 60)
for i, query in enumerate(queries, 1):
print(f"\n--- Query {i} ---")
print(f"Question: {query}\n")
try:
response = agent.chat(query)
print(f"Response:\n{response}")
except Exception as e:
print(f"Error: {e}")
print("-" * 40)
# Clean up
time_mcp.shutdown()
print("\nDone!")
def demo_multiple_mcp_servers():
"""
Demonstrate using multiple MCP servers simultaneously.
This function shows how to create agents with multiple MCP servers,
each providing different capabilities.
"""
print("\n" + "=" * 60)
print("Multiple MCP Servers Demo")
print("=" * 60)
# Initialize multiple MCP servers
servers = {}
try:
servers['time'] = MCP("uvx mcp-server-time", timeout=30)
print("✓ Time server initialized")
except Exception as e:
print(f"✗ Time server failed: {e}")
# Create agent with all available servers
available_tools = list(servers.values())
if not available_tools:
print("No MCP servers available")
return
agent = Agent(
name="MultiServerAgent",
instructions="You have access to multiple MCP servers. Use the appropriate tools.",
llm="openai/gpt-4o-mini",
tools=available_tools
)
print(f"\nAgent created with {len(available_tools)} MCP server(s)")
# Clean up
for server in servers.values():
server.shutdown()
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
"""
Sequential Thinking MCP Example.
This example demonstrates how to use the Sequential Thinking MCP server
with PraisonAI Agents to break down complex problems step by step.
Requirements:
- Node.js and npx installed
- pip install praisonaiagents[mcp]
- OPENAI_API_KEY environment variable set
Usage:
python sequential_thinking.py
"""
from praisonaiagents import Agent, MCP
import os
def main():
# Check for API key
if not os.environ.get("OPENAI_API_KEY"):
print("Please set OPENAI_API_KEY environment variable")
return
# Create MCP instance with Sequential Thinking server
# The server provides tools for step-by-step problem analysis
sequential_mcp = MCP(
"npx -y @modelcontextprotocol/server-sequential-thinking",
timeout=60
)
# Create an agent with the Sequential Thinking tools
agent = Agent(
name="ProblemSolver",
instructions="""You are an expert problem solver who breaks down complex problems
into manageable steps. When given a problem:
1. Use the sequential thinking tool to analyze it step by step
2. Provide clear, actionable steps
3. Explain your reasoning at each step""",
llm="openai/gpt-4o-mini",
tools=sequential_mcp
)
# Example problems to solve
problems = [
"Break down the process of making a cup of tea",
"How would you plan a weekend trip to a new city?",
"What steps would you take to learn a new programming language?"
]
print("=" * 60)
print("Sequential Thinking MCP Example")
print("=" * 60)
for i, problem in enumerate(problems, 1):
print(f"\n--- Problem {i} ---")
print(f"Question: {problem}\n")
response = agent.chat(problem)
print(f"Response:\n{response}")
print("-" * 40)
# Clean up
sequential_mcp.shutdown()
print("\nDone!")
if __name__ == "__main__":
main()
@@ -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")
-279
View File
@@ -1,279 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
PraisonAI Agents is a hierarchical AI agent framework for completing complex tasks with self-reflection capabilities. It supports multi-agent collaboration, tool integration, and various execution patterns (sequential, hierarchical, parallel).
## Development Commands
### Installation and Setup
```bash
# Install core package
pip install -e .
# Install with specific features
pip install -e .[all] # All features
pip install -e .[memory] # Memory capabilities
pip install -e .[knowledge] # Document processing
pip install -e .[mcp] # MCP server support
pip install -e .[llm] # Extended LLM support
pip install -e .[api] # API server capabilities
```
### Testing
```bash
# Run individual test examples (no formal test runner configured)
python tests/basic-agents.py
python tests/async_example.py
python tests/knowledge-agents.py
# Test specific features
python tests/mcp-agents.py # MCP integration
python tests/memory_example.py # Memory functionality
python tests/tools_example.py # Tool system
```
### Running Examples
```bash
# Basic agent usage
python tests/single-agent.py
# Multi-agent workflows
python tests/multi-agents-api.py
# Async operations
python tests/async_example_full.py
# MCP server examples
python tests/mcp-sse-direct-server.py # Start MCP server
python tests/mcp-sse-direct-client.py # Connect to server
```
## Core Architecture
### Agent System (`praisonaiagents/agent/`)
- **Agent**: Core agent class with LLM integration, tool calling, and self-reflection
- **ImageAgent**: Specialized multimodal agent for image processing
- Self-reflection with configurable min/max iterations (default: 1-3)
- Delegation support for hierarchical agent structures
### Multi-Agent Orchestration (`praisonaiagents/agents/`)
- **PraisonAIAgents**: Main orchestrator for managing multiple agents and tasks
- **AutoAgents**: Automatic agent creation and management
- Process types: `sequential`, `hierarchical`, `parallel`
- Context passing between agents and task dependency management
### Task System (`praisonaiagents/task/`)
- **Task**: Core task definition with context, callbacks, and output specifications
- Supports file output, JSON/Pydantic structured output, async execution
- Conditional logic with `condition` parameter for task flow control
- Context passing via `context` parameter for task dependencies
- **Guardrails**: Built-in validation and safety mechanisms for task outputs
- Function-based guardrails for custom validation logic
- LLM-based guardrails using natural language descriptions
- Automatic retry with configurable `max_retries` parameter
- Compatible with CrewAI guardrail patterns
### LLM Integration (`praisonaiagents/llm/`)
- Unified wrapper for multiple LLM providers via LiteLLM
- Supports OpenAI, Anthropic, Gemini, DeepSeek, local models (Ollama)
- Context length management and tool calling capabilities
- Set via `llm` parameter on agents or global `OPENAI_API_KEY`/`ANTHROPIC_API_KEY`
### Tool System (`praisonaiagents/tools/`)
Two implementation patterns:
1. **Function-based**: Simple tools using `@tool` decorator
2. **Class-based**: Complex tools inheriting from `BaseTool`
Built-in tools include: DuckDuckGo search, file operations, calculator, Wikipedia, arXiv, data analysis tools, shell execution.
### Memory & Knowledge Systems
- **Memory** (`praisonaiagents/memory/`): Multi-layered memory with RAG support
- Types: short-term, long-term, entity, user memory
- Providers: ChromaDB, Mem0, custom implementations
- **Knowledge** (`praisonaiagents/knowledge/`): Document processing with chunking
- Chunking strategies via `chonkie` library
- Embedding and retrieval capabilities
### MCP (Model Context Protocol) Integration
- **MCP Server**: Server-side tool protocol for distributed execution
- **SSE Support**: Server-sent events for real-time communication
- Tool discovery and dynamic registration
## Development Patterns
### Agent Creation
```python
agent = Agent(
name="Agent Name",
role="Agent Role",
goal="Agent Goal",
backstory="Agent Background",
llm="gpt-5-nano", # or other LLM
self_reflect=True, # Enable self-reflection
min_reflect=1, # Minimum reflection iterations
max_reflect=3, # Maximum reflection iterations
tools=[tool1, tool2], # Optional tools
guardrail=validate_function, # Agent-level guardrail (function or string)
max_guardrail_retries=3 # Retry limit for guardrail failures
)
```
### Task Definition
```python
task = Task(
name="task_name",
description="Task description",
expected_output="Expected output format",
agent=agent,
context=[previous_task], # Task dependencies
output_pydantic=ResponseModel, # Structured output
condition="condition_function" # Conditional execution
)
```
### Guardrails Usage
#### Task-Level Guardrails
```python
from typing import Tuple, Any
from praisonaiagents import GuardrailResult
# Function-based guardrail (Option 1: Using GuardrailResult)
def validate_output(task_output: TaskOutput) -> GuardrailResult:
"""Custom validation function returning GuardrailResult."""
if "error" in task_output.raw.lower():
return GuardrailResult(
success=False,
result=None,
error="Output contains errors"
)
if len(task_output.raw) < 10:
return GuardrailResult(
success=False,
result=None,
error="Output is too short"
)
return GuardrailResult(
success=True,
result=task_output,
error=""
)
# Function-based guardrail (Option 2: Using Tuple[bool, Any])
def validate_output_tuple(task_output: TaskOutput) -> Tuple[bool, Any]:
"""Custom validation function returning tuple."""
if "error" in task_output.raw.lower():
return False, "Output contains errors"
if len(task_output.raw) < 10:
return False, "Output is too short"
return True, task_output
task = Task(
description="Write a professional email",
expected_output="A well-formatted email",
agent=agent,
guardrail=validate_output, # Function-based guardrail
max_retries=3 # Retry up to 3 times if guardrail fails
)
# LLM-based guardrail
task = Task(
description="Generate marketing copy",
expected_output="Professional marketing content",
agent=agent,
guardrail="Ensure the content is professional, engaging, and free of errors", # String description
max_retries=2
)
```
#### Agent-Level Guardrails
```python
# Agent guardrails apply to ALL outputs from that agent
# Option 1: Using GuardrailResult
def validate_professional_tone(task_output: TaskOutput) -> GuardrailResult:
"""Ensure professional tone in all agent responses."""
content = task_output.raw.lower()
casual_words = ['yo', 'dude', 'awesome', 'cool']
for word in casual_words:
if word in content:
return GuardrailResult(
success=False,
result=None,
error=f"Unprofessional language detected: {word}"
)
return GuardrailResult(
success=True,
result=task_output,
error=""
)
# Option 2: Using Tuple[bool, Any]
def validate_professional_tone_tuple(task_output: TaskOutput) -> Tuple[bool, Any]:
"""Ensure professional tone in all agent responses."""
content = task_output.raw.lower()
casual_words = ['yo', 'dude', 'awesome', 'cool']
for word in casual_words:
if word in content:
return False, f"Unprofessional language detected: {word}"
return True, task_output
# Agent with function-based guardrail
agent = Agent(
name="BusinessWriter",
instructions="You are a professional business writer",
guardrail=validate_professional_tone, # Function guardrail
max_guardrail_retries=3
)
# Agent with LLM-based guardrail
agent = Agent(
name="ContentWriter",
instructions="You are a content writer",
guardrail="Ensure all responses are professional, accurate, and appropriate for business use", # String guardrail
max_guardrail_retries=2
)
```
### Multi-Agent Workflow
```python
workflow = PraisonAIAgents(
agents=[agent1, agent2],
tasks=[task1, task2],
process="sequential", # or "hierarchical", "parallel"
verbose=True,
manager_agent=manager_agent # For hierarchical process
)
result = workflow.start()
```
### Async Support
All major components support async execution:
```python
result = await workflow.astart()
result = await agent.aexecute(task)
```
## Key Dependencies
- **Core**: `pydantic`, `rich`, `openai`, `mcp`
- **Memory**: `chromadb`, `mem0ai`
- **Knowledge**: `markitdown`, `chonkie`
- **LLM**: `litellm` for unified provider access
- **API**: `fastapi`, `uvicorn` for server capabilities
## Error Handling
- Global error logging via `error_logs` list
- Callback system for real-time error reporting
- Context length exception handling with automatic retry
- Graceful degradation for optional dependencies
## Testing Strategy
The project uses example-driven testing with 100+ test files in `tests/` directory. Each test file demonstrates specific usage patterns and serves as both test and documentation. Run individual examples to test functionality rather than using a formal test runner.
Use conda activate praisonai-agents to activate the environment.
@@ -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
@@ -52,7 +52,7 @@ agents = AutoAgents(
tools=[evaluate, get_stock_price],
process="sequential",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
self_reflect=False,
reflection=False,
verbose=False,
max_agents=3 # Maximum number of agents to create
)
+3 -2
View File
@@ -5,8 +5,9 @@ def get_weather(city: str) -> str:
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-5-nano",
tools=[get_weather]
llm="gpt-4o-mini",
tools=[get_weather],
output="status"
)
agent.start("What is the weather in Tokyo?")
+1 -2
View File
@@ -1,4 +1,3 @@
from praisonaiagents import Agent
agent = Agent(instructions="You are a helpful assistant")
result = agent.start("Why is the sky blue?")
print(result)
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 14:42:59
**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.46s | 2.92s | PraisonAI 1.19x faster |
| 2 Agents | 3.21s | 19.39s | PraisonAI 6.05x faster |
| 3 Agents | 8.26s | 15.01s | PraisonAI 1.82x 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.46s
- Min: 1.93s
- Max: 2.90s
- Std Dev: 0.49s
- Times: ['1.93s', '2.55s', '2.90s']
### agno_single
- Average: 3.18s
- Min: 3.00s
- Max: 3.51s
- Std Dev: 0.28s
- Times: ['3.51s', '3.00s', '3.04s']
- Average: 2.92s
- Min: 2.85s
- Max: 2.97s
- Std Dev: 0.06s
- Times: ['2.97s', '2.85s', '2.94s']
### praisonai_two_agents
- Average: 2.49s
- Min: 2.22s
- Max: 2.65s
- Std Dev: 0.24s
- Times: ['2.65s', '2.22s', '2.62s']
- Average: 3.21s
- Min: 2.80s
- Max: 3.67s
- Std Dev: 0.44s
- Times: ['3.67s', '2.80s', '3.15s']
### agno_two_agents
- Average: 20.49s
- Min: 9.96s
- Max: 35.05s
- Std Dev: 13.02s
- Times: ['16.46s', '9.96s', '35.05s']
- Average: 19.39s
- Min: 10.62s
- Max: 26.06s
- Std Dev: 7.93s
- Times: ['26.06s', '10.62s', '21.50s']
### praisonai_three_agents
- Average: 6.52s
- Min: 5.62s
- Max: 7.39s
- Std Dev: 0.88s
- Times: ['5.62s', '6.54s', '7.39s']
- Average: 8.26s
- Min: 6.44s
- Max: 11.88s
- Std Dev: 3.13s
- Times: ['6.47s', '6.44s', '11.88s']
### agno_three_agents
- Average: 13.97s
- Min: 11.69s
- Max: 15.32s
- Std Dev: 1.99s
- Times: ['14.91s', '11.69s', '15.32s']
- Average: 15.01s
- Min: 14.46s
- Max: 15.90s
- Std Dev: 0.78s
- Times: ['15.90s', '14.46s', '14.68s']
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| PraisonAI | 0.11.22 |
| 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,8 +14,8 @@ llm_config = {
agent = Agent(
instructions="You are a helpful assistant. Be concise and clear.",
llm=llm_config,
verbose=True,
self_reflect=True,
output="verbose",
reflection=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")
@@ -12,7 +12,7 @@ agent = Agent(
task = Task(
description="Write a welcome message",
guardrail=validate_content,
guardrails=validate_content,
agent=agent
)
+2 -2
View File
@@ -62,7 +62,7 @@ def main():
description="Write a professional email to a client about project completion",
expected_output="A well-formatted professional email",
agent=agent,
guardrail=email_validator, # Function-based guardrail
guardrails=email_validator, # Function-based guardrail
max_retries=2
)
@@ -110,7 +110,7 @@ Project Team""",
description="Write a marketing email for a new product launch",
expected_output="Engaging marketing content",
agent=agent,
guardrail="Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors",
guardrails="Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors",
max_retries=3
)
+1 -1
View File
@@ -9,7 +9,7 @@ agent = Agent(
goal="Search for information from multiple sources",
backstory="I am an AI assistant that can search YouTube and Wikipedia.",
tools=[YouTubeSearchTool, WikipediaAPIWrapper],
self_reflect=False
reflection=False
)
# Create tasks to demonstrate both tools
+2 -2
View File
@@ -32,9 +32,9 @@ 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
reflection=True, # Enable self-reflection
max_reflect=3, # Maximum reflection iterations
min_reflect=1 # Minimum 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]
)
+140 -181
View File
@@ -5,6 +5,29 @@ This module uses lazy loading to minimize import time and memory usage.
Heavy dependencies like litellm are only loaded when actually needed.
"""
# =============================================================================
# NAMESPACE PACKAGE GUARD
# =============================================================================
# Detect if we're loaded as a namespace package (which indicates stale artifacts
# in site-packages). This happens when there's a praisonaiagents/ directory in
# site-packages without an __init__.py file.
#
# Root cause: Partial uninstall or version mismatch leaves directories that
# Python's PathFinder treats as namespace packages, shadowing the real package.
# =============================================================================
if __file__ is None:
import warnings as _ns_warnings
_ns_warnings.warn(
"praisonaiagents is loaded as a namespace package, which indicates "
"stale artifacts in site-packages. This will cause import errors. "
"Fix: Remove the stale directory with:\n"
" rm -rf $(python -c \"import site; print(site.getsitepackages()[0])\")/praisonaiagents/\n"
"Then reinstall: pip install praisonaiagents",
ImportWarning,
stacklevel=1
)
del _ns_warnings
# Apply warning patch BEFORE any imports to intercept warnings at the source
from . import _warning_patch # noqa: F401
@@ -25,6 +48,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,
@@ -68,7 +99,18 @@ from .main import (
)
# Module-level caches for lazy-loaded classes
_lazy_cache = {}
# Using threading.local() ensures thread-safe access to cached values
import threading
_lazy_cache_local = threading.local()
def _get_lazy_cache():
"""Get thread-local lazy cache dict. Thread-safe for concurrent access."""
if not hasattr(_lazy_cache_local, 'cache'):
_lazy_cache_local.cache = {}
return _lazy_cache_local.cache
# Backward-compatible alias (module-level access still works, now thread-safe)
_lazy_cache = {} # Kept for compatibility, but prefer _get_lazy_cache()
# Flags for optional modules
_mcp_available = None
@@ -442,6 +484,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}")
@@ -532,189 +619,61 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
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',
# Performance utilities
'warmup',
# Cortex - Continuous Improvement System
'CortexConfig',
'CaptureStore',
'CaptureMode',
'CaptureScope',
'PersonaConfig',
'InsightConfig',
'ThreadConfig',
'EntityConfig',
'PatternConfig',
'DecisionConfig',
'FeedbackConfig',
'ImprovementConfig',
'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
# ============================================================================
@@ -30,6 +30,13 @@ from .prompt_expander_agent import (
ExpandStrategy,
ExpandResult
)
from .protocols import (
AgentProtocol,
RunnableAgentProtocol,
ToolAwareAgentProtocol,
MemoryAwareAgentProtocol,
FullAgentProtocol,
)
__all__ = [
'Agent',
@@ -64,5 +71,11 @@ __all__ = [
'RewriteResult',
'PromptExpanderAgent',
'ExpandStrategy',
'ExpandResult'
'ExpandResult',
# Protocols
'AgentProtocol',
'RunnableAgentProtocol',
'ToolAwareAgentProtocol',
'MemoryAwareAgentProtocol',
'FullAgentProtocol',
]
@@ -449,8 +449,12 @@ class Agent:
output = env_output
# Use default output mode if not specified
# Track if user explicitly configured output (for respecting in start())
if output is None:
output = DEFAULT_OUTPUT_MODE
_has_explicit_output = False
else:
_has_explicit_output = True
_output_config = resolve(
value=output,
@@ -469,22 +473,43 @@ class Agent:
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)
status_trace = getattr(_output_config, 'status_trace', False) # New: clean inline status
simple_output = getattr(_output_config, 'simple_output', False) # status preset: no timestamps
else:
# 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
status_trace = False
simple_output = False
# Enable actions output mode if configured
# This registers callbacks to capture tool calls and final output
if actions_trace:
# Enable trace output mode if configured (takes priority)
# This provides timestamped inline status with duration
if status_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)
from ..output.trace import enable_trace_output, is_trace_output_enabled
if not is_trace_output_enabled():
enable_trace_output(use_color=True, show_timestamps=True)
except ImportError:
pass # Actions module not available
pass # Trace module not available
# Enable status output mode if configured (simple progress, no timestamps)
# This registers callbacks to capture tool calls and final output
elif actions_trace:
try:
from ..output.status import enable_status_output, is_status_output_enabled
if not is_status_output_enabled():
output_format = "jsonl" if json_output else "text"
# simple_output=True means status preset (no timestamps)
# metrics=True means debug preset (show token/cost info)
enable_status_output(
redact=True,
use_color=True,
format=output_format,
show_timestamps=not simple_output,
show_metrics=metrics
)
except ImportError:
pass # Status module not available
# ─────────────────────────────────────────────────────────────────────
# Resolve EXECUTION param using unified resolver
@@ -775,7 +800,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
@@ -931,6 +964,7 @@ class Agent:
self._memory_instance = None
self._init_memory(memory, user_id)
self.verbose = verbose
self._has_explicit_output_config = _has_explicit_output # Track if user set output mode
self.allow_delegation = allow_delegation
self.step_callback = step_callback
self.cache = cache
@@ -1089,6 +1123,13 @@ Your Goal: {self.goal}
# 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):
"""AutoMemory instance for automatic memory extraction."""
@@ -1207,7 +1248,11 @@ Your Goal: {self.goal}
@property
def console(self):
"""Lazily initialize Rich Console only when needed."""
"""Lazily initialize Rich Console only when needed AND verbose is True."""
# Only return console if verbose mode is enabled
# This prevents panels from being shown in status/silent modes
if not self.verbose:
return None
if self._console is None:
from rich.console import Console
self._console = Console()
@@ -2840,6 +2885,9 @@ Your Goal: {self.goal}"""
"""
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
# NOTE: tool_call callback is triggered by display_tool_call in openai_client.py
# Do NOT call it here to avoid duplicate output
# Set up injection context for tools with Injected parameters
from ..tools.injected import AgentState, with_injection_context
state = AgentState(
@@ -3154,21 +3202,20 @@ Your Goal: {self.goal}"""
This centralizes the logic for callback execution and display to avoid duplication.
"""
# Always execute callbacks regardless of verbose setting (only when not using custom LLM)
if not self._using_custom_llm:
execute_sync_callback(
'interaction',
message=prompt,
response=response,
markdown=self.markdown,
generation_time=generation_time,
agent_name=self.name,
agent_role=self.role,
agent_tools=[t.__name__ for t in self.tools] if self.tools else None,
task_name=task_name,
task_description=task_description,
task_id=task_id
)
# Always execute callbacks for status/trace output (regardless of LLM backend)
execute_sync_callback(
'interaction',
message=prompt,
response=response,
markdown=self.markdown,
generation_time=generation_time,
agent_name=self.name,
agent_role=self.role,
agent_tools=[t.__name__ for t in self.tools] if self.tools else None,
task_name=task_name,
task_description=task_description,
task_id=task_id
)
# Always display final interaction when verbose is True to ensure consistent formatting
# This ensures both OpenAI and custom LLM providers (like Gemini) show formatted output
if self.verbose and not self._final_display_shown:
@@ -3583,7 +3630,7 @@ Your Goal: {self.goal}"""
output_pydantic=output_pydantic,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
reflection=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
console=self.console,
@@ -3871,6 +3918,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
@@ -3937,7 +3988,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
output_pydantic=output_pydantic,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
reflection=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
console=self.console,
@@ -4269,8 +4320,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
Returns:
The agent's response as a string
"""
# Force non-streaming, non-display for production use
kwargs['stream'] = False
# 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):
@@ -4301,15 +4352,15 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
def run(self, prompt: str, **kwargs):
"""Execute agent silently and return structured result.
Production-friendly execution. Does not stream or display output by default.
Use this for programmatic/scripted usage where you want the result only.
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
- display (bool): Force terminal display if True. Default: False
- output (str): Output preset override
- output (str): Output preset override (rarely needed)
Returns:
The agent's response as a string
@@ -4320,6 +4371,14 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
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:
@@ -4471,18 +4530,20 @@ 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 interactively with streaming output.
def start(self, prompt: str = None, **kwargs):
"""Start the agent interactively with verbose output.
Beginner-friendly execution. Streams output by default when running in a TTY.
Use this for interactive/terminal usage where you want to see output in real-time.
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
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
- display (bool | None): Override display. None = auto (True if streaming)
- output (str): Output preset override
- output (str): Output preset override (e.g., "silent", "verbose")
Returns:
- If streaming: Generator yielding response chunks
@@ -4490,21 +4551,32 @@ Write the complete compiled report:"""
Example:
```python
agent = Agent(instructions="You are helpful")
# Minimal usage - instructions IS the task
agent = Agent(instructions="Research AI trends and summarize")
result = agent.start() # Uses instructions as task
# Interactive use - streams by default in terminal
for chunk in agent.start("Tell me a story"):
print(chunk, end="", flush=True)
# Or let it handle display automatically
result = agent.start("What is 2+2?") # Streams if TTY
# 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()
# 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')
@@ -4514,24 +4586,199 @@ Write the complete compiled report:"""
stream_requested = self.stream
else:
# Auto-detect: stream if stdout is a TTY (interactive terminal)
stream_requested = sys.stdout.isatty()
stream_requested = is_tty
# 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
kwargs['stream'] = False
result = self.chat(prompt, **kwargs)
# ─────────────────────────────────────────────────────────────────────
# Enable verbose output in TTY for beginner-friendly interactive use
# Priority: agent's explicit output config > start() override > TTY auto
# ─────────────────────────────────────────────────────────────────────
original_verbose = self.verbose
original_markdown = self.markdown
output_override = kwargs.pop('output', None) # Pop to prevent passing to chat()
# Auto-save session if enabled
self._auto_save_session()
# Check if agent was configured with explicit output mode (not default)
# If so, respect it and don't auto-enable verbose for TTY
has_explicit_output = getattr(self, '_has_explicit_output_config', False)
return result
try:
# Apply output override from start() call if provided
if 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)
# Only auto-enable verbose for TTY if NO explicit output was configured
elif is_tty and not has_explicit_output:
self.verbose = True
self.markdown = True
# 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 (deduplicated)
if tools_called:
# Use dict.fromkeys to preserve order while removing duplicates
unique_tools = list(dict.fromkeys(tools_called))
tools_summary = ", ".join(unique_tools)
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.
@@ -435,7 +435,6 @@ 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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
prompt = f"""Analyze this gitingest codebase digest following PRD template methodology:
@@ -516,7 +515,6 @@ codebase style and architecture following PRD template principles."""
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Format comprehensive sample
@@ -567,7 +565,6 @@ Analyze following PRD principles to extract patterns, conventions, and architect
7. Code complexity metrics
8. API and interface patterns""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Perform AST analysis on Python files
@@ -625,7 +622,6 @@ 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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Get representative code samples
@@ -675,7 +671,6 @@ patterns and best practices for first-try success."""
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Get test files
@@ -802,7 +797,6 @@ Extract testing patterns for validation framework creation following PRD princip
Generate PRPs following this EXACT structure for first-try implementation success.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
prompt = f"""Generate a comprehensive Product Requirements Prompt (PRP) following the EXACT PRD template structure:
@@ -851,7 +845,6 @@ on the first try following PRD template principles."""
7. DOCUMENTATION VALIDATION: Documentation completeness
8. DEPENDENCY VALIDATION: Dependency analysis and security""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Analyze existing validation patterns
@@ -898,7 +891,6 @@ following PRD template principles."""
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Get documentation files
@@ -944,7 +936,6 @@ following PRD template principles."""
APIs, databases, external services, configuration points, and any other
integration requirements that affect implementation.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Get configuration and dependency files
@@ -1005,7 +996,6 @@ following PRD template principles."""
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
prompt = f"""Create detailed implementation blueprint following PRD methodology:
@@ -1303,7 +1293,6 @@ Every agent interaction has been saved for full audit trail and reproducibility.
Be precise and extract only what is explicitly mentioned or clearly implied.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
prompt = f"""Parse this user input to extract codebase and goal:
@@ -1537,7 +1526,6 @@ Note: Detailed function/class metadata not available due to content access limit
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
structure_text = basic_structure.get('structure', '')
@@ -1771,7 +1759,6 @@ Focus on creating clear, structured metadata that will help with intelligent fil
Maximum 50 files for efficient analysis.""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
# Prepare enhanced structure information
@@ -1924,7 +1911,6 @@ Maximum 50 files.""".format(goal=goal)
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-4o-mini",
verbose=getattr(self, 'verbose', True)
)
prompt = f"""Analyze these carefully selected repository files for implementing: {goal}
@@ -2025,7 +2011,6 @@ Since these files were pre-selected for relevance, explain how each contributes
Focus everything on successfully implementing: {goal}""",
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
verbose=getattr(self, 'verbose', True)
)
chunk_info = ""
@@ -0,0 +1,151 @@
"""
Agent Protocol Definitions.
Provides Protocol interfaces that define the minimal contract for agent implementations.
This enables:
- Mocking agents in tests without real LLM calls
- Creating custom agent implementations
- Type checking with static analyzers
These protocols are lightweight and have zero performance impact.
"""
from typing import Protocol, runtime_checkable, Optional, Any, Dict, List
@runtime_checkable
class AgentProtocol(Protocol):
"""
Minimal Protocol for agent implementations.
This defines the essential interface that any agent must provide.
It enables proper mocking and testing without real LLM dependencies.
Example:
```python
# Create a mock agent for testing
class MockAgent:
@property
def name(self) -> str:
return "TestAgent"
def chat(self, prompt: str, **kwargs) -> str:
return "Mock response"
async def achat(self, prompt: str, **kwargs) -> str:
return "Async mock response"
# Use in tests
agent: AgentProtocol = MockAgent()
result = agent.chat("Hello")
```
"""
@property
def name(self) -> str:
"""Agent's name identifier."""
...
def chat(self, prompt: str, **kwargs) -> str:
"""
Synchronous chat with the agent.
Args:
prompt: The user prompt or message
**kwargs: Additional optional parameters (temperature, tools, etc.)
Returns:
The agent's response as a string
"""
...
async def achat(self, prompt: str, **kwargs) -> str:
"""
Asynchronous chat with the agent.
Args:
prompt: The user prompt or message
**kwargs: Additional optional parameters (temperature, tools, etc.)
Returns:
The agent's response as a string
"""
...
@runtime_checkable
class RunnableAgentProtocol(AgentProtocol, Protocol):
"""
Extended Protocol for agents that support run/start methods.
This extends AgentProtocol with the run/start interface used
by the Agent class for task execution.
"""
def run(self, prompt: str, **kwargs) -> str:
"""Run the agent with a prompt (alias for chat in most cases)."""
...
def start(self, prompt: str, **kwargs) -> str:
"""Start the agent with a prompt."""
...
async def arun(self, prompt: str, **kwargs) -> str:
"""Async run the agent with a prompt."""
...
async def astart(self, prompt: str, **kwargs) -> str:
"""Async start the agent with a prompt."""
...
@runtime_checkable
class ToolAwareAgentProtocol(AgentProtocol, Protocol):
"""
Protocol for agents that support tools.
This extends AgentProtocol with tool-related properties.
"""
@property
def tools(self) -> List[Any]:
"""List of tools available to the agent."""
...
@runtime_checkable
class MemoryAwareAgentProtocol(AgentProtocol, Protocol):
"""
Protocol for agents that support memory.
This extends AgentProtocol with memory-related properties.
"""
@property
def chat_history(self) -> List[Dict[str, Any]]:
"""The agent's conversation history."""
...
def clear_history(self) -> None:
"""Clear the agent's conversation history."""
...
# Composite protocol for fully-featured agents
@runtime_checkable
class FullAgentProtocol(RunnableAgentProtocol, ToolAwareAgentProtocol, MemoryAwareAgentProtocol, Protocol):
"""
Complete Protocol for full-featured agents.
This combines all agent protocol features for agents that support
the full interface (chat, run, tools, memory).
"""
pass
__all__ = [
'AgentProtocol',
'RunnableAgentProtocol',
'ToolAwareAgentProtocol',
'MemoryAwareAgentProtocol',
'FullAgentProtocol',
]
@@ -65,6 +65,52 @@ def process_video(video_path: str, seconds_per_frame=2):
video.release()
return base64_frames
def get_multimodal_message(text_prompt: str, images: list) -> list:
"""
Build multimodal message content for LLM with text and images.
DRY helper - replaces duplicate _get_multimodal_message in aexecute_task/execute_task.
Args:
text_prompt: The text content of the message
images: List of image paths (local or URL)
Returns:
List of content items for multimodal LLM message
"""
content = [{"type": "text", "text": text_prompt}]
for img in images:
# If local file path for a valid image
if os.path.exists(img):
ext = os.path.splitext(img)[1].lower()
# If it's a .mp4, convert to frames
if ext == ".mp4":
frames = process_video(img, seconds_per_frame=1)
content.append({"type": "text", "text": "These are frames from the video."})
for f in frames:
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpg;base64,{f}"}
})
else:
encoded = encode_file_to_base64(img)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/{ext.lstrip('.')};base64,{encoded}"
}
})
else:
# Treat as a remote URL
content.append({
"type": "image_url",
"image_url": {"url": img}
})
return content
def process_task_context(context_item, verbose=0, user_id=None):
"""
Process a single context item for task execution.
@@ -466,10 +512,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
@@ -603,40 +655,9 @@ Context:
logger.debug(f"Starting execution of task {task_id} with prompt:\n{task_prompt}")
if task.images:
def _get_multimodal_message(text_prompt, images):
content = [{"type": "text", "text": text_prompt}]
for img in images:
# If local file path for a valid image
if os.path.exists(img):
ext = os.path.splitext(img)[1].lower()
# If it's a .mp4, convert to frames
if ext == ".mp4":
frames = process_video(img, seconds_per_frame=1)
content.append({"type": "text", "text": "These are frames from the video."})
for f in frames:
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpg;base64,{f}"}
})
else:
encoded = encode_file_to_base64(img)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/{ext.lstrip('.')};base64,{encoded}"
}
})
else:
# Treat as a remote URL
content.append({
"type": "image_url",
"image_url": {"url": img}
})
return content
# Use shared multimodal helper (DRY - defined at module level)
agent_output = await executor_agent.achat(
_get_multimodal_message(task_prompt, task.images),
get_multimodal_message(task_prompt, task.images),
tools=tools,
output_json=task.output_json,
output_pydantic=task.output_pydantic,
@@ -821,13 +842,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():
@@ -961,40 +986,9 @@ Context:
logger.debug(f"Starting execution of task {task_id} with prompt:\n{task_prompt}")
if task.images:
def _get_multimodal_message(text_prompt, images):
content = [{"type": "text", "text": text_prompt}]
for img in images:
# If local file path for a valid image
if os.path.exists(img):
ext = os.path.splitext(img)[1].lower()
# If it's a .mp4, convert to frames
if ext == ".mp4":
frames = process_video(img, seconds_per_frame=1)
content.append({"type": "text", "text": "These are frames from the video."})
for f in frames:
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpg;base64,{f}"}
})
else:
encoded = encode_file_to_base64(img)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/{ext.lstrip('.')};base64,{encoded}"
}
})
else:
# Treat as a remote URL
content.append({
"type": "image_url",
"image_url": {"url": img}
})
return content
# Use shared multimodal helper (DRY - defined at module level)
agent_output = executor_agent.chat(
_get_multimodal_message(task_prompt, task.images),
get_multimodal_message(task_prompt, task.images),
tools=task.tools,
output_json=task.output_json,
output_pydantic=task.output_pydantic,
@@ -1193,30 +1187,147 @@ 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
from rich.markdown import Markdown
console.print(Panel(
Markdown(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 +1335,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 +1347,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 +1354,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"""
@@ -2191,7 +2308,7 @@ Context:
# Inherit from original task if available
memory=original_task.memory if original_task else None,
callback=original_task.callback if original_task else None,
guardrail=original_task.guardrail if original_task else None,
guardrails=original_task.guardrail if original_task else None,
max_retries=original_task.max_retries if original_task else 3,
output_json=original_task.output_json if original_task else None,
output_pydantic=original_task.output_pydantic if original_task else None,
@@ -13,10 +13,35 @@ from typing import Dict, Set, Optional, Callable, Any, Literal, List
from functools import wraps
from contextvars import ContextVar
from dataclasses import dataclass, field
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.prompt import Confirm
# Lazy import for rich components
_rich_console = None
_rich_panel = None
_rich_confirm = None
def _get_rich_console():
"""Lazy import rich Console."""
global _rich_console
if _rich_console is None:
from rich.console import Console
_rich_console = Console
return _rich_console
def _get_rich_panel():
"""Lazy import rich Panel."""
global _rich_panel
if _rich_panel is None:
from rich.panel import Panel
_rich_panel = Panel
return _rich_panel
def _get_rich_confirm():
"""Lazy import rich Confirm."""
global _rich_confirm
if _rich_confirm is None:
from rich.prompt import Confirm
_rich_confirm = Confirm
return _rich_confirm
# Global registries for approval requirements
APPROVAL_REQUIRED_TOOLS: Set[str] = set()
@@ -308,6 +333,10 @@ def console_approval_callback(function_name: str, arguments: Dict[str, Any], ris
Displays tool information and prompts user for approval via console.
"""
Console = _get_rich_console()
Panel = _get_rich_panel()
Confirm = _get_rich_confirm()
console = Console()
# Create risk level styling
@@ -528,6 +528,16 @@ class OutputConfig:
# JSON output mode - emit JSONL events for piping
json_output: bool = False
# Simple output mode - just print response without panels
simple_output: bool = False
# Show LLM parameters (for debug mode)
show_parameters: bool = False
# Status trace mode - clean inline status updates
# Shows: [timestamp] Calling LLM..., Executing tool..., Response: ...
status_trace: bool = False
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
@@ -538,6 +548,9 @@ class OutputConfig:
"reasoning_steps": self.reasoning_steps,
"actions_trace": self.actions_trace,
"json_output": self.json_output,
"simple_output": self.simple_output,
"show_parameters": self.show_parameters,
"status_trace": self.status_trace,
}
@@ -39,6 +39,16 @@ MEMORY_URL_SCHEMES: Dict[str, str] = {
# =============================================================================
# Output Presets
# =============================================================================
#
# Preset Hierarchy (from least to most output):
# silent → Nothing (default for SDK, max performance)
# text → Response only, simple format
# verbose → Task + Tools inline + Response panel
# debug → verbose + metrics footer (tokens, cost, model)
# stream → Real-time token streaming
# json → Machine-readable JSONL events
#
# =============================================================================
OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
# Silent preset - DEFAULT for SDK
@@ -52,53 +62,51 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"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": {
# Status preset - Shows tool calls and response without timestamps
# Simple progress indicator: ▸ tool → result ✓
"status": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True, # Special flag for action trace mode
"actions_trace": True,
"simple_output": True, # Simple format without timestamps
},
# Plain preset - final output only, no action trace
"plain": {
# Trace preset - Full execution trace with timestamps
# Shows: [HH:MM:SS] ▸ tool → result [0.2s] ✓
# Ideal for debugging and monitoring
"trace": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": False,
},
"minimal": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
},
"normal": {
"verbose": True,
"markdown": True,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True,
"status_trace": True, # Enable timestamped status output
},
# Verbose preset - Full interactive output with panels
# Shows: Task prompt, Tool calls (inline), Response panel
# Best for interactive terminal use
"verbose": {
"verbose": True,
"markdown": True,
"stream": False,
"metrics": True,
"reasoning_steps": True,
"metrics": False,
"reasoning_steps": False,
},
# Debug preset - trace + metrics (NO boxes)
# Shows: [timestamp] tool calls, metrics footer, reasoning steps
# Best for developers debugging agent behavior
"debug": {
"verbose": True,
"markdown": True,
"verbose": False, # No boxes
"markdown": False,
"stream": False,
"metrics": True,
"metrics": True, # Shows metrics footer
"reasoning_steps": True,
"actions_trace": True,
"status_trace": True, # Timestamps like trace
"show_parameters": True,
},
# Streaming preset - enables streaming by default
"stream": {
@@ -108,7 +116,7 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"metrics": False,
"reasoning_steps": False,
},
# JSON preset - JSONL output for piping
# JSON preset - JSONL output for piping/scripting
"json": {
"verbose": False,
"markdown": False,
@@ -118,11 +126,61 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"actions_trace": True,
"json_output": True, # Output as JSONL
},
# ==========================================================================
# ALIASES (for backward compatibility)
# ==========================================================================
# plain → silent (identical behavior)
"plain": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": False,
},
# minimal → silent (identical behavior)
"minimal": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
},
# normal → verbose (consolidated)
"normal": {
"verbose": True,
"markdown": True,
"stream": False,
"metrics": False,
"reasoning_steps": False,
},
# actions → status (renamed for clarity)
"actions": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True,
"simple_output": True,
},
# text → status (old name, kept for backward compat)
"text": {
"verbose": False,
"markdown": False,
"stream": False,
"metrics": False,
"reasoning_steps": False,
"actions_trace": True,
"simple_output": True,
},
}
# 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)
# "verbose" = full interactive display
# "debug" = verbose + metrics
DEFAULT_OUTPUT_MODE = "silent"
@@ -7,19 +7,35 @@ Visual display with agents in center and tools on sides.
from typing import Dict, List, Set, Tuple
from collections import defaultdict
import threading
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.align import Align
from rich.columns import Columns
from rich.table import Table
from rich import box
# Lazy imports for rich components
_rich_cache = {}
def _get_rich_components():
"""Lazy import all rich components."""
if not _rich_cache:
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.align import Align
from rich.columns import Columns
from rich.table import Table
from rich import box
_rich_cache['Console'] = Console
_rich_cache['Panel'] = Panel
_rich_cache['Text'] = Text
_rich_cache['Align'] = Align
_rich_cache['Columns'] = Columns
_rich_cache['Table'] = Table
_rich_cache['box'] = box
return _rich_cache
class FlowDisplay:
"""Displays agent workflow with agents centered and tools on sides."""
def __init__(self):
self.console = Console()
rich = _get_rich_components()
self.console = rich['Console']()
self.agents = [] # List of agents in order
self.agent_tools = defaultdict(list) # agent -> [tools]
self.tracking = False
@@ -98,6 +114,13 @@ class FlowDisplay:
def _display_agent_with_tools(self, agent: str):
"""Display agent with tools on the sides."""
rich = _get_rich_components()
Table = rich['Table']
Panel = rich['Panel']
Text = rich['Text']
Align = rich['Align']
box = rich['box']
tools = self.agent_tools.get(agent, [])
if not tools:
@@ -149,8 +172,13 @@ class FlowDisplay:
self.console.print(Align.center(Text("".join(arrow_parts))))
def _create_tools_panel(self, tools: List[str]) -> Panel:
def _create_tools_panel(self, tools: List[str]):
"""Create a panel for tools."""
rich = _get_rich_components()
Panel = rich['Panel']
Text = rich['Text']
box = rich['box']
if not tools:
return ""
@@ -173,6 +201,12 @@ class FlowDisplay:
def _display_centered_node(self, label: str, color: str):
"""Display a centered node."""
rich = _get_rich_components()
Panel = rich['Panel']
Text = rich['Text']
Align = rich['Align']
box = rich['box']
panel = Panel(
Text(label, style=f"white on {color}", justify="center"),
style=f"white on {color}",
@@ -183,6 +217,9 @@ class FlowDisplay:
def _display_arrow_down(self):
"""Display a downward arrow."""
rich = _get_rich_components()
Align = rich['Align']
self.console.print()
self.console.print(Align.center("↓"))
self.console.print()
+79 -19
View File
@@ -379,7 +379,7 @@ Respond with ONLY a valid JSON tool call in this format:
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
reflection=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps,
@@ -1458,7 +1458,7 @@ Now provide your final answer using this result."""
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
reflection=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps
@@ -1476,7 +1476,7 @@ Now provide your final answer using this result."""
output_pydantic=output_pydantic,
verbose=verbose,
markdown=markdown,
self_reflect=self_reflect,
reflection=self_reflect,
max_reflect=max_reflect,
min_reflect=min_reflect,
agent_name=agent_name,
@@ -1540,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
@@ -1547,6 +1548,10 @@ Now provide your final answer using this result."""
try:
# Get response from LiteLLM
current_time = time.time()
# Trigger LLM callback for subsequent iterations (after tool calls)
# Initial callback is in agent._chat_completion, so this triggers for all loop iterations
execute_sync_callback('llm_start', model=self.model, agent_name=agent_name)
# If reasoning_steps is True, do a single non-streaming call
if reasoning_steps:
@@ -1569,6 +1574,39 @@ Now provide your final answer using this result."""
if self.metrics:
self._track_token_usage(final_response, self.model)
# Trigger llm_end callback with metrics for debug output
llm_latency_ms = (time.time() - current_time) * 1000
# Extract usage - handle both dict and ModelResponse object
tokens_in = 0
tokens_out = 0
if isinstance(final_response, dict):
usage = final_response.get("usage", {})
tokens_in = usage.get("prompt_tokens", 0)
tokens_out = usage.get("completion_tokens", 0)
else:
# ModelResponse object
usage = getattr(final_response, 'usage', None)
if usage:
tokens_in = getattr(usage, 'prompt_tokens', 0) or 0
tokens_out = getattr(usage, 'completion_tokens', 0) or 0
# Calculate cost if available
llm_cost = None
try:
llm_cost = litellm.completion_cost(completion_response=final_response)
except Exception:
pass
execute_sync_callback(
'llm_end',
model=self.model,
tokens_in=tokens_in,
tokens_out=tokens_out,
cost=llm_cost,
latency_ms=llm_latency_ms
)
# Execute callbacks and display based on verbose setting
generation_time_val = time.time() - current_time
response_content = f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}" if reasoning_content else response_text
@@ -2154,6 +2192,16 @@ Now provide your final answer using this result."""
logging.debug(f"[TOOL_EXEC_DEBUG] About to display tool call with message: {display_message}")
display_tool_call(display_message, console=self.console)
# Always trigger callback for status output (even when verbose=False)
result_str = json.dumps(tool_result) if tool_result else "empty"
execute_sync_callback(
'tool_call',
message=f"Calling function: {function_name}",
tool_name=function_name,
tool_input=arguments,
tool_output=result_str[:200] if result_str else None
)
# Check if this is Ollama provider
if self._is_ollama_provider():
@@ -2932,7 +2980,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
reflection=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps
@@ -2950,7 +2998,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
output_pydantic=output_pydantic,
verbose=verbose,
markdown=markdown,
self_reflect=self_reflect,
reflection=self_reflect,
max_reflect=max_reflect,
min_reflect=min_reflect,
agent_name=agent_name,
@@ -3636,12 +3684,14 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
self.session_token_metrics = self.session_token_metrics + metrics
# Track in global collector
# Extract provider from model string (e.g., 'openai' from 'openai/gpt-4o-mini')
provider = model.split('/')[0] if '/' in model else 'litellm'
_token_collector.track_tokens(
model=model,
agent=self.current_agent_name,
metrics=metrics,
metadata={
"provider": self.provider,
"provider": provider,
"stream": False
}
)
@@ -3706,9 +3756,19 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
output_json = override_params.get('output_json')
output_pydantic = override_params.get('output_pydantic')
# Always remove these from params as they're not native litellm parameters
params.pop('output_json', None)
params.pop('output_pydantic', None)
# Always remove internal PraisonAI parameters that are not native litellm parameters
# These are used internally by PraisonAI but should not be passed to the LLM API
internal_params = [
'output_json', 'output_pydantic', # Structured output handling
'reflection', 'self_reflect', 'max_reflect', 'min_reflect', 'reflect_llm', # Self-reflection
'verbose', 'markdown', # Display options
'agent_name', 'agent_role', 'agent_tools', # Agent metadata
'task_name', 'task_description', 'task_id', # Task metadata
'execute_tool_fn', 'stream_callback', 'emit_events', # Callbacks
'console', # Rich console
]
for param in internal_params:
params.pop(param, None)
if output_json or output_pydantic:
@@ -3727,18 +3787,18 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
logging.debug(f"Using Gemini native structured output with schema: {json.dumps(schema, indent=2)}")
# Add tool_choice="auto" when tools are provided (unless already specified)
# This encourages the model to use tools when appropriate
if 'tools' in params and params['tools'] and 'tool_choice' not in params:
# For Gemini models, use tool_choice to encourage tool usage
if self._is_gemini_model():
try:
import litellm
# Check if model supports function calling before setting tool_choice
if litellm.supports_function_calling(model=self.model):
params['tool_choice'] = 'auto'
except Exception as e:
# If check fails, still set tool_choice for known Gemini models
logging.debug(f"Could not verify function calling support: {e}. Setting tool_choice anyway.")
try:
import litellm
# Check if model supports function calling before setting tool_choice
if litellm.supports_function_calling(model=self.model):
params['tool_choice'] = 'auto'
logging.debug(f"Set tool_choice='auto' for model {self.model}")
except Exception as e:
# If check fails, still set tool_choice to encourage tool usage
logging.debug(f"Could not verify function calling support: {e}. Setting tool_choice='auto' anyway.")
params['tool_choice'] = 'auto'
# Add web_search_options if web_search is enabled and model supports it
if self.web_search and self._supports_web_search():
@@ -10,15 +10,52 @@ import logging
import time
import json
import asyncio
from typing import Any, Dict, List, Optional, Union, AsyncIterator, Iterator, Callable, Tuple
from openai import OpenAI, AsyncOpenAI
from openai.types.chat import ChatCompletionChunk
from typing import Any, Dict, List, Optional, Union, AsyncIterator, Iterator, Callable, Tuple, TYPE_CHECKING
from pydantic import BaseModel
from dataclasses import dataclass
from rich.console import Console
from rich.live import Live
import inspect
# Lazy imports for optional dependencies
_openai_module = None
_rich_console = None
_rich_live = None
def _get_openai():
"""Lazy import openai module."""
global _openai_module
if _openai_module is None:
try:
import openai as _openai
_openai_module = _openai
except ImportError:
raise ImportError(
"openai is required for OpenAI client. "
"Install with: pip install openai"
)
return _openai_module
def _get_openai_classes():
"""Get OpenAI and AsyncOpenAI classes lazily."""
openai = _get_openai()
return openai.OpenAI, openai.AsyncOpenAI
def _get_rich_console():
"""Lazy import rich Console."""
global _rich_console
if _rich_console is None:
from rich.console import Console
_rich_console = Console
return _rich_console
def _get_rich_live():
"""Lazy import rich Live."""
global _rich_live
if _rich_live is None:
from rich.live import Live
_rich_live = Live
return _rich_live
# Import display_tool_call for callback support (lazy import to avoid circular imports)
_display_tool_call = None
def _get_display_tool_call():
@@ -258,21 +295,23 @@ class OpenAIClient:
def console(self):
"""Lazily initialize Rich Console only when needed."""
if self._console is None:
from rich.console import Console
Console = _get_rich_console()
self._console = Console()
return self._console
@property
def sync_client(self) -> OpenAI:
def sync_client(self):
"""Get the synchronous OpenAI client (lazy initialization)."""
if self._sync_client is None:
OpenAI, _ = _get_openai_classes()
self._sync_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
return self._sync_client
@property
def async_client(self) -> AsyncOpenAI:
def async_client(self):
"""Get the asynchronous OpenAI client (lazy initialization)."""
if self._async_client is None:
_, AsyncOpenAI = _get_openai_classes()
self._async_client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
return self._async_client
@@ -579,7 +618,7 @@ class OpenAIClient:
temperature: float = 1.0,
tools: Optional[List[Dict]] = None,
start_time: Optional[float] = None,
console: Optional[Console] = None,
console: Optional[Any] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
stream_callback: Optional[Callable] = None,
@@ -653,6 +692,7 @@ class OpenAIClient:
# If display function provided, use Live display
if display_fn:
Live = _get_rich_live()
with Live(
display_fn("", start_time),
console=console,
@@ -739,7 +779,8 @@ class OpenAIClient:
content=content
))
first_token_emitted = True
else:
elif _emit:
# Emit DELTA_TEXT for subsequent tokens
stream_callback(StreamEvent(
type=StreamEventType.DELTA_TEXT,
timestamp=last_content_time,
@@ -787,7 +828,7 @@ class OpenAIClient:
temperature: float = 1.0,
tools: Optional[List[Dict]] = None,
start_time: Optional[float] = None,
console: Optional[Console] = None,
console: Optional[Any] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
stream_callback: Optional[Callable] = None,
@@ -874,6 +915,7 @@ class OpenAIClient:
# If display function provided, use Live display
if display_fn:
Live = _get_rich_live()
with Live(
display_fn("", start_time),
console=console,
@@ -994,7 +1036,7 @@ class OpenAIClient:
tools: Optional[List[Dict[str, Any]]] = None,
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
**kwargs
) -> Union[Any, Iterator[ChatCompletionChunk]]:
) -> Union[Any, Iterator[Any]]:
"""
Create a chat completion using the synchronous client.
@@ -1039,7 +1081,7 @@ class OpenAIClient:
tools: Optional[List[Dict[str, Any]]] = None,
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
**kwargs
) -> Union[Any, AsyncIterator[ChatCompletionChunk]]:
) -> Union[Any, AsyncIterator[Any]]:
"""
Create a chat completion using the asynchronous client.
@@ -1083,7 +1125,7 @@ class OpenAIClient:
tools: Optional[List[Any]] = None,
execute_tool_fn: Optional[Callable] = None,
stream: bool = True,
console: Optional[Console] = None,
console: Optional[Any] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
verbose: bool = True,
@@ -1125,6 +1167,10 @@ class OpenAIClient:
iteration_count = 0
while iteration_count < max_iterations:
# Trigger LLM callback for status/trace output
from ..main import execute_sync_callback
execute_sync_callback('llm_start', model=model, agent_name=None)
if stream:
# Process as streaming response with formatted tools
final_response = self.process_stream_response(
@@ -1145,6 +1191,7 @@ class OpenAIClient:
try:
request_start_perf = time.perf_counter()
ttft_logged = False
Live = _get_rich_live()
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(
@@ -1200,6 +1247,35 @@ class OpenAIClient:
if not final_response:
return None
# Trigger llm_end callback with metrics for debug output
llm_end_time = time.perf_counter()
llm_latency_ms = (llm_end_time - start_time) * 1000
# Extract usage info if available
usage = getattr(final_response, 'usage', None)
tokens_in = getattr(usage, 'prompt_tokens', 0) if usage else 0
tokens_out = getattr(usage, 'completion_tokens', 0) if usage else 0
# Calculate cost if litellm available
cost = None
try:
import litellm
if hasattr(final_response, 'model_dump'):
cost = litellm.completion_cost(completion_response=final_response.model_dump())
elif isinstance(final_response, dict):
cost = litellm.completion_cost(completion_response=final_response)
except Exception:
pass # Cost calculation is optional
execute_sync_callback(
'llm_end',
model=model,
tokens_in=tokens_in,
tokens_out=tokens_out,
cost=cost,
latency_ms=llm_latency_ms
)
# Check for tool calls
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
@@ -1235,17 +1311,19 @@ class OpenAIClient:
# Always trigger callback for tool call tracking (even when verbose=False)
display_tool_call_fn = _get_display_tool_call()
display_tool_call_fn(f"Calling function: {function_name}", console=console if verbose else None)
if verbose and console:
console.print(f"[dim]Arguments:[/dim] {arguments}")
# Execute the tool
tool_result = execute_tool_fn(function_name, arguments)
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
# Trigger callback with result
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=console if verbose else None)
# Trigger callback with structured parameters for status output
display_tool_call_fn(
f"Calling function: {function_name}",
console=console if verbose else None,
tool_name=function_name,
tool_input=arguments,
tool_output=results_str[:200] if results_str else None
)
messages.append({
"role": "tool",
@@ -1271,7 +1349,7 @@ class OpenAIClient:
tools: Optional[List[Any]] = None,
execute_tool_fn: Optional[Callable] = None,
stream: bool = True,
console: Optional[Console] = None,
console: Optional[Any] = None,
display_fn: Optional[Callable] = None,
reasoning_steps: bool = False,
verbose: bool = True,
@@ -1325,6 +1403,7 @@ class OpenAIClient:
if display_fn and console:
# When verbose (display_fn provided), use streaming for better UX
try:
Live = _get_rich_live()
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
# Use streaming when display_fn is provided for progressive display
response_stream = await self.acreate_completion(
+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):
@@ -45,6 +45,15 @@ def __getattr__(name):
elif name == "python_type_to_json_schema":
from .mcp_utils import python_type_to_json_schema
return python_type_to_json_schema
elif name == "fix_array_schemas":
from .mcp_schema_utils import fix_array_schemas
return fix_array_schemas
elif name == "ThreadLocalEventLoop":
from .mcp_schema_utils import ThreadLocalEventLoop
return ThreadLocalEventLoop
elif name == "get_thread_local_event_loop":
from .mcp_schema_utils import get_thread_local_event_loop
return get_thread_local_event_loop
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
@@ -57,6 +66,10 @@ __all__ = [
"function_to_mcp_schema",
"get_tool_metadata",
"python_type_to_json_schema",
# Schema utilities (lazy loaded)
"fix_array_schemas",
"ThreadLocalEventLoop",
"get_thread_local_event_loop",
# Transport components (lazy loaded)
"WebSocketMCPClient",
"SessionManager",
@@ -507,6 +507,26 @@ class MCP:
"""
return iter(self._tools)
def get_tools(self) -> List[Callable]:
"""
Get the list of tool functions from this MCP instance.
This method provides explicit access to the tools list, which is useful
when you need to inspect or manipulate the tools programmatically.
Returns:
List[Callable]: List of tool functions that can be called
Example:
```python
mcp = MCP("npx -y @modelcontextprotocol/server-time")
tools = mcp.get_tools()
for tool in tools:
print(f"Tool: {tool.__name__}")
```
"""
return self._tools
def _fix_array_schemas(self, schema):
"""
Fix array schemas by adding missing 'items' attribute required by OpenAI.
@@ -30,20 +30,19 @@ except ImportError:
logger = logging.getLogger("mcp-http-stream")
# Global event loop for async operations
_event_loop = None
# Import shared utilities for thread-safe event loop and schema fixing
from .mcp_schema_utils import ThreadLocalEventLoop, fix_array_schemas
# Thread-local event loop for async operations (thread-safe)
_event_loop_manager = ThreadLocalEventLoop()
# Global registry of active clients for cleanup
_active_clients = weakref.WeakSet()
_cleanup_registered = False
def get_event_loop():
"""Get or create a global event loop."""
global _event_loop
if _event_loop is None or _event_loop.is_closed():
_event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(_event_loop)
return _event_loop
"""Get or create a thread-local event loop."""
return _event_loop_manager.get_loop()
def _cleanup_all_clients():
@@ -148,47 +147,10 @@ class HTTPStreamMCPTool:
logger.error(f"Error in _async_call for {self.name}: {e}")
raise
def _fix_array_schemas(self, schema):
"""
Fix array schemas by adding missing 'items' attribute required by OpenAI.
This ensures compatibility with OpenAI's function calling format which
requires array types to specify the type of items they contain.
Args:
schema: The schema dictionary to fix
Returns:
dict: The fixed schema
"""
if not isinstance(schema, dict):
return schema
# Create a copy to avoid modifying the original
fixed_schema = schema.copy()
# Fix array types at the current level
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
# Add a default items schema for arrays without it
fixed_schema["items"] = {"type": "string"}
# Recursively fix nested schemas
if "properties" in fixed_schema:
fixed_properties = {}
for prop_name, prop_schema in fixed_schema["properties"].items():
fixed_properties[prop_name] = self._fix_array_schemas(prop_schema)
fixed_schema["properties"] = fixed_properties
# Fix items schema if it exists
if "items" in fixed_schema:
fixed_schema["items"] = self._fix_array_schemas(fixed_schema["items"])
return fixed_schema
def to_openai_tool(self):
"""Convert the tool to OpenAI format."""
# Fix array schemas to include 'items' attribute
fixed_schema = self._fix_array_schemas(self.input_schema)
# Fix array schemas to include 'items' attribute (using shared utility)
fixed_schema = fix_array_schemas(self.input_schema)
return {
"type": "function",
@@ -0,0 +1,189 @@
"""
MCP Schema Utilities.
This module provides shared schema manipulation utilities for MCP transports.
These utilities ensure compatibility with OpenAI's function calling format
and other LLM providers.
DRY: This module centralizes schema fixing logic that was previously
duplicated across mcp.py, mcp_sse.py, mcp_http_stream.py, and mcp_websocket.py.
"""
import threading
from typing import Any, Dict, Optional
def fix_array_schemas(schema: Any) -> Any:
"""
Fix array schemas by adding missing 'items' attribute required by OpenAI.
This ensures compatibility with OpenAI's function calling format which
requires array types to specify the type of items they contain.
Args:
schema: The schema dictionary to fix
Returns:
dict: The fixed schema with 'items' added to array types
Example:
>>> schema = {"type": "array"}
>>> fixed = fix_array_schemas(schema)
>>> fixed
{'type': 'array', 'items': {'type': 'string'}}
"""
if not isinstance(schema, dict):
return schema
# Create a copy to avoid modifying the original
fixed_schema = schema.copy()
# Fix array types at the current level
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
# Add a default items schema for arrays without it
fixed_schema["items"] = {"type": "string"}
# Recursively fix nested schemas
if "properties" in fixed_schema:
fixed_properties = {}
for prop_name, prop_schema in fixed_schema["properties"].items():
fixed_properties[prop_name] = fix_array_schemas(prop_schema)
fixed_schema["properties"] = fixed_properties
# Fix items schema if it exists
if "items" in fixed_schema:
fixed_schema["items"] = fix_array_schemas(fixed_schema["items"])
# Fix additionalProperties if it's a schema
if isinstance(fixed_schema.get("additionalProperties"), dict):
fixed_schema["additionalProperties"] = fix_array_schemas(
fixed_schema["additionalProperties"]
)
# Fix anyOf/oneOf/allOf schemas
for key in ("anyOf", "oneOf", "allOf"):
if key in fixed_schema and isinstance(fixed_schema[key], list):
fixed_schema[key] = [fix_array_schemas(s) for s in fixed_schema[key]]
return fixed_schema
class ThreadLocalEventLoop:
"""
Thread-local event loop storage for MCP transports.
This class provides thread-safe event loop management, ensuring that
multiple MCP instances don't share the same event loop, which could
cause race conditions.
Usage:
event_loop_manager = ThreadLocalEventLoop()
loop = event_loop_manager.get_loop()
# Use loop for async operations
event_loop_manager.cleanup()
"""
def __init__(self):
"""Initialize thread-local storage."""
self._local = threading.local()
self._lock = threading.Lock()
def get_loop(self):
"""
Get or create an event loop for the current thread.
Returns:
asyncio.AbstractEventLoop: Event loop for current thread
"""
import asyncio
# Check if we have a loop for this thread
loop = getattr(self._local, 'loop', None)
if loop is None or loop.is_closed():
with self._lock:
# Double-check after acquiring lock
loop = getattr(self._local, 'loop', None)
if loop is None or loop.is_closed():
loop = asyncio.new_event_loop()
self._local.loop = loop
return loop
def set_loop(self, loop):
"""
Set the event loop for the current thread.
Args:
loop: The event loop to set
"""
self._local.loop = loop
def cleanup(self):
"""Clean up the event loop for the current thread."""
loop = getattr(self._local, 'loop', None)
if loop is not None and not loop.is_closed():
try:
loop.close()
except Exception:
pass
self._local.loop = None
# Singleton instance for backward compatibility with existing code
# that uses get_event_loop() function pattern
_default_event_loop_manager = ThreadLocalEventLoop()
def get_thread_local_event_loop():
"""
Get or create a thread-local event loop.
This is a drop-in replacement for the global get_event_loop() functions
in the transport modules, providing thread safety.
Returns:
asyncio.AbstractEventLoop: Event loop for current thread
"""
return _default_event_loop_manager.get_loop()
def json_type_to_python_type(json_type: str) -> type:
"""
Convert JSON Schema type to Python type annotation.
Args:
json_type: JSON Schema type string
Returns:
Python type corresponding to the JSON type
"""
type_map = {
'string': str,
'integer': int,
'number': float,
'boolean': bool,
'array': list,
'object': dict,
'null': type(None),
}
return type_map.get(json_type, str)
def extract_schema_properties(input_schema: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""
Extract properties and required fields from an input schema.
Args:
input_schema: JSON Schema for tool input
Returns:
Dictionary with 'properties' and 'required' keys
"""
if not input_schema:
return {'properties': {}, 'required': []}
return {
'properties': input_schema.get('properties', {}),
'required': input_schema.get('required', [])
}
@@ -22,16 +22,15 @@ except ImportError:
logger = logging.getLogger("mcp-sse")
# Global event loop for async operations
_event_loop = None
# Import shared utilities for thread-safe event loop and schema fixing
from .mcp_schema_utils import ThreadLocalEventLoop, fix_array_schemas
# Thread-local event loop for async operations (thread-safe)
_event_loop_manager = ThreadLocalEventLoop()
def get_event_loop():
"""Get or create a global event loop."""
global _event_loop
if _event_loop is None or _event_loop.is_closed():
_event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(_event_loop)
return _event_loop
"""Get or create a thread-local event loop."""
return _event_loop_manager.get_loop()
class SSEMCPTool:
@@ -111,47 +110,10 @@ class SSEMCPTool:
logger.error(f"Error in _async_call for {self.name}: {e}")
raise
def _fix_array_schemas(self, schema):
"""
Fix array schemas by adding missing 'items' attribute required by OpenAI.
This ensures compatibility with OpenAI's function calling format which
requires array types to specify the type of items they contain.
Args:
schema: The schema dictionary to fix
Returns:
dict: The fixed schema
"""
if not isinstance(schema, dict):
return schema
# Create a copy to avoid modifying the original
fixed_schema = schema.copy()
# Fix array types at the current level
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
# Add a default items schema for arrays without it
fixed_schema["items"] = {"type": "string"}
# Recursively fix nested schemas
if "properties" in fixed_schema:
fixed_properties = {}
for prop_name, prop_schema in fixed_schema["properties"].items():
fixed_properties[prop_name] = self._fix_array_schemas(prop_schema)
fixed_schema["properties"] = fixed_properties
# Fix items schema if it exists
if "items" in fixed_schema:
fixed_schema["items"] = self._fix_array_schemas(fixed_schema["items"])
return fixed_schema
def to_openai_tool(self):
"""Convert the tool to OpenAI format."""
# Fix array schemas to include 'items' attribute
fixed_schema = self._fix_array_schemas(self.input_schema)
# Fix array schemas to include 'items' attribute (using shared utility)
fixed_schema = fix_array_schemas(self.input_schema)
return {
"type": "function",
@@ -26,6 +26,9 @@ from typing import Any, Dict, Optional, List
logger = logging.getLogger("mcp-websocket")
# Import shared utilities for thread-safe event loop and schema fixing
from .mcp_schema_utils import ThreadLocalEventLoop, fix_array_schemas
def is_websocket_url(url: str) -> bool:
"""
@@ -58,18 +61,13 @@ def calculate_backoff(attempt: int, base_delay: float = 1.0, max_delay: float =
return min(delay, max_delay)
# Global event loop for WebSocket operations (similar to other MCP transports)
_event_loop = None
_loop_lock = threading.Lock()
# Thread-local event loop for WebSocket operations (thread-safe)
_event_loop_manager = ThreadLocalEventLoop()
def get_event_loop():
"""Get or create a global event loop for WebSocket operations."""
global _event_loop
with _loop_lock:
if _event_loop is None or _event_loop.is_closed():
_event_loop = asyncio.new_event_loop()
return _event_loop
"""Get or create a thread-local event loop for WebSocket operations."""
return _event_loop_manager.get_loop()
class WebSocketTransport:
@@ -359,36 +357,10 @@ class WebSocketMCPTool:
logger.error(f"Error in _async_call for {self.name}: {e}")
raise
def _fix_array_schemas(self, schema: Dict[str, Any]) -> Dict[str, Any]:
"""
Fix array schemas by adding missing 'items' attribute.
OpenAI requires array types to specify item types.
"""
if not isinstance(schema, dict):
return schema
fixed_schema = schema.copy()
# Fix array types at current level
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
fixed_schema["items"] = {"type": "string"}
# Recursively fix nested schemas
if "properties" in fixed_schema:
fixed_properties = {}
for prop_name, prop_schema in fixed_schema["properties"].items():
fixed_properties[prop_name] = self._fix_array_schemas(prop_schema)
fixed_schema["properties"] = fixed_properties
if "items" in fixed_schema:
fixed_schema["items"] = self._fix_array_schemas(fixed_schema["items"])
return fixed_schema
def to_openai_tool(self) -> Dict[str, Any]:
"""Convert the tool to OpenAI function calling format."""
fixed_schema = self._fix_array_schemas(self.input_schema)
# Fix array schemas to include 'items' attribute (using shared utility)
fixed_schema = fix_array_schemas(self.input_schema)
return {
"type": "function",
@@ -18,6 +18,11 @@ This module provides memory management capabilities including:
- Docs management (like Cursor docs)
- MCP config management (like Cursor .cursor/mcp/)
Protocols:
- MemoryProtocol: Minimal interface for memory implementations
- AsyncMemoryProtocol: Async interface for memory
- ResettableMemoryProtocol: Interface with reset methods
Memory Providers:
- FileMemory: Zero-dependency JSON file-based storage (default)
- Memory: Full-featured with SQLite, ChromaDB, Mem0, MongoDB support
@@ -33,6 +38,13 @@ from .file_memory import FileMemory, create_memory
from .rules_manager import RulesManager, Rule, create_rules_manager
from .docs_manager import DocsManager, Doc
from .mcp_config import MCPConfigManager, MCPConfig
from .protocols import (
MemoryProtocol,
AsyncMemoryProtocol,
ResettableMemoryProtocol,
EntityMemoryProtocol,
)
# Lazy imports for optional modules to avoid dependency issues and improve startup time
def __getattr__(name):
@@ -187,4 +199,9 @@ __all__ = [
"DecisionStore",
"FeedbackStore",
"ImprovementStore",
# Protocols
"MemoryProtocol",
"AsyncMemoryProtocol",
"ResettableMemoryProtocol",
"EntityMemoryProtocol",
]
@@ -18,7 +18,7 @@ Storage Structure:
import json
import time
import fcntl
import sys
import hashlib
import logging
from pathlib import Path
@@ -26,9 +26,17 @@ from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, asdict, field
# fcntl is Unix-only; on Windows, skip file locking (acceptable for single-process usage)
if sys.platform != 'win32':
import fcntl
_HAS_FCNTL = True
else:
_HAS_FCNTL = False
logger = logging.getLogger(__name__)
@dataclass
class MemoryItem:
"""A single memory item."""
@@ -178,35 +186,41 @@ class FileMemory:
# -------------------------------------------------------------------------
def _read_json(self, filepath: Path, default: Any = None) -> Any:
"""Read JSON file with file locking."""
"""Read JSON file with file locking (Unix only)."""
if not filepath.exists():
return default if default is not None else []
try:
with open(filepath, 'r', encoding='utf-8') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
if _HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
try:
data = json.load(f)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
if _HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
return data
except (json.JSONDecodeError, IOError) as e:
self._log(f"Error reading {filepath}: {e}", logging.WARNING)
return default if default is not None else []
def _write_json(self, filepath: Path, data: Any) -> bool:
"""Write JSON file with file locking."""
"""Write JSON file with file locking (Unix only)."""
try:
with open(filepath, 'w', encoding='utf-8') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
if _HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
json.dump(data, f, indent=2, ensure_ascii=False)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
if _HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
return True
except IOError as e:
self._log(f"Error writing {filepath}: {e}", logging.ERROR)
return False
def _load_config(self, overrides: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Load or create configuration."""
@@ -23,39 +23,102 @@ def trace(self, message, *args, **kwargs):
logging.Logger.trace = trace
try:
import chromadb
from chromadb.config import Settings as ChromaSettings
CHROMADB_AVAILABLE = True
except ImportError:
CHROMADB_AVAILABLE = False
pass
# Lazy availability flags and cached imports
_chromadb_cache = {"available": None, "module": None, "settings": None}
_mem0_cache = {"available": None, "module": None}
_openai_cache = {"available": None, "module": None}
_litellm_cache = {"available": None, "module": None}
_pymongo_cache = {"available": None, "module": None, "client": None}
try:
import mem0
MEM0_AVAILABLE = True
except ImportError:
MEM0_AVAILABLE = False
def _check_chromadb():
"""Lazily check chromadb availability and cache."""
if _chromadb_cache["available"] is None:
try:
import chromadb
from chromadb.config import Settings as ChromaSettings
_chromadb_cache["available"] = True
_chromadb_cache["module"] = chromadb
_chromadb_cache["settings"] = ChromaSettings
except ImportError:
_chromadb_cache["available"] = False
return _chromadb_cache["available"]
try:
import openai
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
def _get_chromadb():
"""Get chromadb module (lazy load)."""
if not _check_chromadb():
raise ImportError("chromadb is required. Install with: pip install chromadb")
return _chromadb_cache["module"], _chromadb_cache["settings"]
try:
import litellm
litellm.telemetry = False # Disable telemetry
LITELLM_AVAILABLE = True
except ImportError:
LITELLM_AVAILABLE = False
def _check_mem0():
"""Lazily check mem0 availability."""
if _mem0_cache["available"] is None:
try:
import mem0
_mem0_cache["available"] = True
_mem0_cache["module"] = mem0
except ImportError:
_mem0_cache["available"] = False
return _mem0_cache["available"]
try:
import pymongo
from pymongo import MongoClient
PYMONGO_AVAILABLE = True
except ImportError:
PYMONGO_AVAILABLE = False
def _get_mem0():
"""Get mem0 module (lazy load)."""
if not _check_mem0():
raise ImportError("mem0 is required. Install with: pip install mem0ai")
return _mem0_cache["module"]
def _check_openai():
"""Lazily check openai availability."""
if _openai_cache["available"] is None:
try:
import openai
_openai_cache["available"] = True
_openai_cache["module"] = openai
except ImportError:
_openai_cache["available"] = False
return _openai_cache["available"]
def _get_openai():
"""Get openai module (lazy load)."""
if not _check_openai():
raise ImportError("openai is required. Install with: pip install openai")
return _openai_cache["module"]
def _check_litellm():
"""Lazily check litellm availability."""
if _litellm_cache["available"] is None:
try:
import litellm
litellm.telemetry = False
_litellm_cache["available"] = True
_litellm_cache["module"] = litellm
except ImportError:
_litellm_cache["available"] = False
return _litellm_cache["available"]
def _get_litellm():
"""Get litellm module (lazy load)."""
if not _check_litellm():
raise ImportError("litellm is required. Install with: pip install litellm")
return _litellm_cache["module"]
def _check_pymongo():
"""Lazily check pymongo availability."""
if _pymongo_cache["available"] is None:
try:
import pymongo
from pymongo import MongoClient
_pymongo_cache["available"] = True
_pymongo_cache["module"] = pymongo
_pymongo_cache["client"] = MongoClient
except ImportError:
_pymongo_cache["available"] = False
return _pymongo_cache["available"]
def _get_pymongo():
"""Get pymongo module and MongoClient (lazy load)."""
if not _check_pymongo():
raise ImportError("pymongo is required. Install with: pip install pymongo")
return _pymongo_cache["module"], _pymongo_cache["client"]
@@ -142,9 +205,9 @@ class Memory:
logging.getLogger('litellm.utils').setLevel(logging.WARNING)
self.provider = self.cfg.get("provider", "rag")
self.use_mem0 = (self.provider.lower() == "mem0") and MEM0_AVAILABLE
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.use_mem0 = (self.provider.lower() == "mem0") and _check_mem0()
self.use_rag = (self.provider.lower() == "rag") and _check_chromadb() and self.cfg.get("use_embedding", False)
self.use_mongodb = (self.provider.lower() == "mongodb") and _check_pymongo()
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
@@ -274,6 +337,9 @@ class Memory:
rag_path = self.cfg.get("rag_db_path", "chroma_db")
os.makedirs(rag_path, exist_ok=True)
# Get chromadb lazily
chromadb, ChromaSettings = _get_chromadb()
# Initialize ChromaDB with persistent storage
self.chroma_client = chromadb.PersistentClient(
path=rag_path,
@@ -399,7 +465,7 @@ class Memory:
def _get_embedding(self, text: str) -> List[float]:
"""Get embedding for text using available embedding services."""
try:
if LITELLM_AVAILABLE:
if _check_litellm():
# Use LiteLLM for consistency with the rest of the codebase
import litellm
@@ -408,7 +474,7 @@ class Memory:
input=text
)
return response.data[0]["embedding"]
elif OPENAI_AVAILABLE:
elif _check_openai():
# Fallback to OpenAI client
from openai import OpenAI
client = OpenAI()
@@ -624,7 +690,7 @@ class Memory:
elif self.use_rag and hasattr(self, "chroma_col"):
try:
if LITELLM_AVAILABLE:
if _check_litellm():
# Use LiteLLM for consistency with the rest of the codebase
import litellm
@@ -633,7 +699,7 @@ class Memory:
input=query
)
query_embedding = response.data[0]["embedding"]
elif OPENAI_AVAILABLE:
elif _check_openai():
# Fallback to OpenAI client
from openai import OpenAI
client = OpenAI()
@@ -787,7 +853,7 @@ class Memory:
# Store in vector database if enabled
if self.use_rag and hasattr(self, "chroma_col"):
try:
if LITELLM_AVAILABLE:
if _check_litellm():
# Use LiteLLM for consistency with the rest of the codebase
import litellm
@@ -802,7 +868,7 @@ class Memory:
logger.info("Successfully got embeddings from LiteLLM")
logger.trace(f"Received embedding of length: {len(embedding)}")
elif OPENAI_AVAILABLE:
elif _check_openai():
# Fallback to OpenAI client
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
@@ -940,7 +1006,7 @@ class Memory:
elif self.use_rag and hasattr(self, "chroma_col"):
try:
if LITELLM_AVAILABLE:
if _check_litellm():
# Use LiteLLM for consistency with the rest of the codebase
import litellm
@@ -949,7 +1015,7 @@ class Memory:
input=query
)
query_embedding = response.data[0]["embedding"]
elif OPENAI_AVAILABLE:
elif _check_openai():
# Fallback to OpenAI client
from openai import OpenAI
client = OpenAI()
@@ -1439,7 +1505,7 @@ class Memory:
"""
try:
if LITELLM_AVAILABLE:
if _check_litellm():
# Use LiteLLM for consistency with the rest of the codebase
import litellm
@@ -1455,7 +1521,7 @@ class Memory:
response_format={"type": "json_object"},
temperature=0.3
)
elif OPENAI_AVAILABLE:
elif _check_openai():
# Fallback to OpenAI client
from openai import OpenAI
client = OpenAI()
@@ -0,0 +1,212 @@
"""
Memory Protocol Definitions.
Provides Protocol interfaces that define the minimal contract for memory implementations.
This enables:
- Mocking memory in tests without real database dependencies
- Creating custom memory backends (Redis, PostgreSQL, etc.)
- Type checking with static analyzers
These protocols are lightweight and have zero performance impact.
"""
from typing import Protocol, runtime_checkable, Optional, Any, Dict, List
@runtime_checkable
class MemoryProtocol(Protocol):
"""
Minimal Protocol for memory implementations.
This defines the essential interface that any memory backend must provide.
It enables proper mocking and testing without real database dependencies.
Example:
```python
# Create a simple in-memory store for testing
class InMemoryStore:
def __init__(self):
self._data = []
def store_short_term(self, text: str, metadata=None, **kwargs) -> str:
self._data.append({"text": text, "type": "short"})
return str(len(self._data))
def search_short_term(self, query: str, limit: int = 5, **kwargs):
return self._data[:limit]
def store_long_term(self, text: str, metadata=None, **kwargs) -> str:
self._data.append({"text": text, "type": "long"})
return str(len(self._data))
def search_long_term(self, query: str, limit: int = 5, **kwargs):
return self._data[:limit]
# Use in agent
memory: MemoryProtocol = InMemoryStore()
```
"""
def store_short_term(
self,
text: str,
metadata: Optional[Dict[str, Any]] = None,
**kwargs
) -> str:
"""
Store content in short-term memory.
Args:
text: The content to store
metadata: Optional metadata dictionary
**kwargs: Additional backend-specific parameters
Returns:
An identifier for the stored content
"""
...
def search_short_term(
self,
query: str,
limit: int = 5,
**kwargs
) -> List[Dict[str, Any]]:
"""
Search short-term memory.
Args:
query: The search query
limit: Maximum number of results
**kwargs: Additional backend-specific parameters
Returns:
List of matching memory entries
"""
...
def store_long_term(
self,
text: str,
metadata: Optional[Dict[str, Any]] = None,
**kwargs
) -> str:
"""
Store content in long-term memory.
Args:
text: The content to store
metadata: Optional metadata dictionary
**kwargs: Additional backend-specific parameters
Returns:
An identifier for the stored content
"""
...
def search_long_term(
self,
query: str,
limit: int = 5,
**kwargs
) -> List[Dict[str, Any]]:
"""
Search long-term memory.
Args:
query: The search query
limit: Maximum number of results
**kwargs: Additional backend-specific parameters
Returns:
List of matching memory entries
"""
...
@runtime_checkable
class ResettableMemoryProtocol(MemoryProtocol, Protocol):
"""
Extended Protocol for memory that can be reset/cleared.
"""
def reset_short_term(self) -> None:
"""Clear all short-term memory."""
...
def reset_long_term(self) -> None:
"""Clear all long-term memory."""
...
@runtime_checkable
class AsyncMemoryProtocol(Protocol):
"""
Async Protocol for memory implementations.
For memory backends that support async operations.
"""
async def astore_short_term(
self,
text: str,
metadata: Optional[Dict[str, Any]] = None,
**kwargs
) -> str:
"""Async store in short-term memory."""
...
async def asearch_short_term(
self,
query: str,
limit: int = 5,
**kwargs
) -> List[Dict[str, Any]]:
"""Async search short-term memory."""
...
async def astore_long_term(
self,
text: str,
metadata: Optional[Dict[str, Any]] = None,
**kwargs
) -> str:
"""Async store in long-term memory."""
...
async def asearch_long_term(
self,
query: str,
limit: int = 5,
**kwargs
) -> List[Dict[str, Any]]:
"""Async search long-term memory."""
...
@runtime_checkable
class EntityMemoryProtocol(Protocol):
"""
Protocol for memory that supports entity storage.
"""
def store_entity(
self,
name: str,
type_: str,
desc: str,
relations: str = ""
) -> str:
"""Store an entity in memory."""
...
def get_entity(self, name: str) -> Optional[Dict[str, Any]]:
"""Retrieve an entity by name."""
...
__all__ = [
'MemoryProtocol',
'ResettableMemoryProtocol',
'AsyncMemoryProtocol',
'EntityMemoryProtocol',
]
@@ -0,0 +1,115 @@
"""
Output Styles Module for PraisonAI Agents.
Provides configurable output formatting:
- Predefined styles (concise, detailed, technical, etc.)
- Custom formatting rules
- Markdown/plain text/JSON output
- Response length control
Zero Performance Impact:
- All imports are lazy loaded via __getattr__
- Styles only applied when configured
- No overhead when not in use
Usage:
from praisonaiagents.output import OutputStyle, OutputFormatter
# Use predefined style
style = OutputStyle.concise()
# Apply to agent
agent = Agent(
instructions="...",
output_style=style
)
# Or format manually
formatter = OutputFormatter(style)
formatted = formatter.format(response)
"""
__all__ = [
# Core classes
"OutputStyle",
"OutputFormatter",
# Style presets
"StylePreset",
# Configuration
"OutputConfig",
# Status output (for status preset - no timestamps)
"StatusOutput",
"enable_status_output",
"disable_status_output",
"is_status_output_enabled",
"get_status_output",
# Trace output (for trace preset - with timestamps)
"TraceOutput",
"enable_trace_output",
"disable_trace_output",
"is_trace_output_enabled",
"get_trace_output",
]
def __getattr__(name: str):
"""Lazy load module components to avoid import overhead."""
if name == "OutputStyle":
from .style import OutputStyle
return OutputStyle
if name == "OutputFormatter":
from .formatter import OutputFormatter
return OutputFormatter
if name == "StylePreset":
from .style import StylePreset
return StylePreset
if name == "OutputConfig":
from .config import OutputConfig
return OutputConfig
# Status output (for status preset - no timestamps)
if name == "StatusOutput":
from .status import StatusOutput
return StatusOutput
if name == "enable_status_output":
from .status import enable_status_output
return enable_status_output
if name == "disable_status_output":
from .status import disable_status_output
return disable_status_output
if name == "is_status_output_enabled":
from .status import is_status_output_enabled
return is_status_output_enabled
if name == "get_status_output":
from .status import get_status_output
return get_status_output
# Trace output (for trace preset - with timestamps)
if name == "TraceOutput":
from .trace import TraceOutput
return TraceOutput
if name == "enable_trace_output":
from .trace import enable_trace_output
return enable_trace_output
if name == "disable_trace_output":
from .trace import disable_trace_output
return disable_trace_output
if name == "is_trace_output_enabled":
from .trace import is_trace_output_enabled
return is_trace_output_enabled
if name == "get_trace_output":
from .trace import get_trace_output
return get_trace_output
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,16 @@
"""
Output Configuration for PraisonAI Agents.
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class OutputConfig:
"""Configuration for output formatting."""
enabled: bool = True
default_format: str = "markdown" # markdown, plain, json
max_length: Optional[int] = None
truncate_long_responses: bool = False
include_metadata: bool = False
@@ -0,0 +1,129 @@
"""
Output Formatter for PraisonAI Agents.
Formats output according to style settings.
"""
import re
import json
from typing import Optional, Dict, Any
from .style import OutputStyle
class OutputFormatter:
"""
Formats agent output according to style settings.
Example:
style = OutputStyle.concise()
formatter = OutputFormatter(style)
formatted = formatter.format(response)
"""
def __init__(self, style: Optional[OutputStyle] = None):
"""
Initialize the formatter.
Args:
style: Output style to use
"""
self.style = style or OutputStyle()
def format(self, text: str) -> str:
"""
Format text according to style.
Args:
text: Text to format
Returns:
Formatted text
"""
if not text:
return text
result = text
# Apply length limits
if self.style.max_length and len(result) > self.style.max_length:
result = self._truncate(result, self.style.max_length)
# Apply format conversion
if self.style.format == "plain":
result = self._to_plain(result)
elif self.style.format == "json":
result = self._to_json(result)
return result
def _truncate(self, text: str, max_length: int) -> str:
"""Truncate text to max length."""
if len(text) <= max_length:
return text
# Try to truncate at sentence boundary
truncated = text[:max_length]
# Find last sentence end
for end_char in ['. ', '! ', '? ', '\n']:
last_end = truncated.rfind(end_char)
if last_end > max_length * 0.7: # At least 70% of max
return truncated[:last_end + 1].strip() + "..."
# Fall back to word boundary
last_space = truncated.rfind(' ')
if last_space > max_length * 0.8:
return truncated[:last_space].strip() + "..."
return truncated.strip() + "..."
def _to_plain(self, text: str) -> str:
"""Convert markdown to plain text."""
result = text
# Remove headers
result = re.sub(r'^#{1,6}\s+', '', result, flags=re.MULTILINE)
# Remove bold/italic
result = re.sub(r'\*\*([^*]+)\*\*', r'\1', result)
result = re.sub(r'\*([^*]+)\*', r'\1', result)
result = re.sub(r'__([^_]+)__', r'\1', result)
result = re.sub(r'_([^_]+)_', r'\1', result)
# Remove code blocks (keep content)
result = re.sub(r'```[a-z]*\n?', '', result)
# Remove inline code
result = re.sub(r'`([^`]+)`', r'\1', result)
# Remove links (keep text)
result = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', result)
# Remove horizontal rules
result = re.sub(r'^---+$', '', result, flags=re.MULTILINE)
# Clean up extra whitespace
result = re.sub(r'\n{3,}', '\n\n', result)
return result.strip()
def _to_json(self, text: str) -> str:
"""Wrap text in JSON structure."""
return json.dumps({
"response": text,
"format": "text"
}, indent=2)
def get_word_count(self, text: str) -> int:
"""Get word count of text."""
return len(text.split())
def get_char_count(self, text: str) -> int:
"""Get character count of text."""
return len(text)
def estimate_tokens(self, text: str) -> int:
"""Estimate token count (rough approximation)."""
# Rough estimate: ~4 chars per token
return len(text) // 4
@@ -0,0 +1,440 @@
"""
Actions Output Mode for PraisonAI Agents.
Provides a filtered output mode that shows only:
1. Agent lifecycle events (start/end)
2. Tool calls (name, args, duration, status)
3. Final agent output
This module integrates with the existing display callback system
and uses the existing redaction utilities.
Usage:
from praisonaiagents import Agent
# Enable via preset
agent = Agent(instructions="...", output="actions")
agent.start("Do something")
# Or via OutputConfig
from praisonaiagents.config import OutputConfig
agent = Agent(
instructions="...",
output=OutputConfig(actions_trace=True)
)
"""
import sys
import time
import threading
from datetime import datetime
from typing import Any, Dict, Optional, TextIO
# Global state for actions mode
_status_output_enabled = False
_status_output: Optional['StatusOutput'] = None
_agent_start_times: Dict[str, float] = {}
_output_lock = threading.Lock() # Multi-agent safe output lock
_ai_call_count = 0 # Track AI call count for context display
def _format_timestamp(ts: float) -> str:
"""Format timestamp as HH:MM:SS.mmm."""
dt = datetime.fromtimestamp(ts)
return dt.strftime("%H:%M:%S.") + f"{int(dt.microsecond / 1000):03d}"
def _format_duration(duration_ms: Optional[float]) -> str:
"""Format duration in human-readable form."""
if duration_ms is None:
return ""
if duration_ms < 1000:
return f"{duration_ms:.0f}ms"
return f"{duration_ms / 1000:.1f}s"
def _truncate(text: str, max_len: int = 50) -> str:
"""Truncate text with ellipsis."""
if not text:
return ""
if len(text) <= max_len:
return text
return text[:max_len - 3] + "..."
class StatusOutput:
"""
Sink for actions-only output.
Formats and outputs action events in a clean, readable format.
Supports both human-readable text and JSONL output.
Thread-safe for multi-agent concurrent execution.
"""
def __init__(
self,
file: TextIO = None,
format: str = "text", # "text" or "jsonl"
redact: bool = True,
use_color: bool = True,
show_timestamps: bool = True, # NEW: control timestamp display
show_metrics: bool = False, # Enable metrics display for debug mode
):
self._file = file or sys.stderr # Use stderr to not interfere with agent output
self._format = format
self._redact = redact
self._use_color = use_color
self._show_timestamps = show_timestamps
self._show_metrics = show_metrics
self._console = None
self._tool_start_times: Dict[str, float] = {}
self._lock = threading.Lock() # Per-sink lock for thread safety
def _get_console(self):
"""Get Rich console for colored output."""
if self._console is None and self._use_color:
try:
from rich.console import Console
self._console = Console(file=self._file, force_terminal=True)
except ImportError:
self._console = None
return self._console
def _redact_args(self, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Redact sensitive data from args."""
if args is None or not self._redact:
return args
try:
from ..trace.redact import redact_dict
return redact_dict(args)
except ImportError:
return args
def _format_args(self, args: Optional[Dict[str, Any]]) -> str:
"""Format tool arguments for display."""
if not args:
return ""
redacted = self._redact_args(args)
parts = []
for k, v in (redacted or {}).items():
if isinstance(v, str):
v_str = f'"{_truncate(v, 30)}"'
else:
v_str = _truncate(str(v), 30)
parts.append(f"{k}={v_str}")
return _truncate(", ".join(parts), 80)
def agent_start(self, agent_name: str) -> None:
"""Record agent start."""
ts = time.time()
_agent_start_times[agent_name] = ts
if self._format == "jsonl":
self._emit_jsonl("agent_start", agent_name=agent_name, timestamp=ts)
else:
self._emit_text(f"▶ Agent:{agent_name} started", ts, "green")
def agent_end(self, agent_name: str, duration_ms: Optional[float] = None) -> None:
"""Record agent end."""
ts = time.time()
# Calculate duration if not provided
if duration_ms is None and agent_name in _agent_start_times:
start_ts = _agent_start_times.pop(agent_name, None)
if start_ts:
duration_ms = (ts - start_ts) * 1000
duration_str = f" [{_format_duration(duration_ms)}]" if duration_ms else ""
if self._format == "jsonl":
self._emit_jsonl("agent_end", agent_name=agent_name, timestamp=ts, duration_ms=duration_ms)
else:
self._emit_text(f"◀ Agent:{agent_name} completed{duration_str}", ts, "green")
def llm_start(self, model: str = None, agent_name: Optional[str] = None) -> None:
"""Record LLM call start."""
global _ai_call_count
ts = time.time()
self._llm_start_time = ts
_ai_call_count += 1
# Context based on call sequence
if _ai_call_count == 1:
context = "thinking"
else:
context = "responding"
if self._format == "jsonl":
self._emit_jsonl("llm_start", model=model, agent_name=agent_name, timestamp=ts)
else:
self._emit_text(f"▸ AI → {context}...", ts, "yellow")
def llm_end(
self,
duration_ms: Optional[float] = None,
agent_name: Optional[str] = None,
model: Optional[str] = None,
tokens_in: int = 0,
tokens_out: int = 0,
cost: Optional[float] = None,
latency_ms: Optional[float] = None,
) -> None:
"""Record LLM call end with optional metrics for debug mode."""
ts = time.time()
# Use latency_ms if provided, otherwise calculate from start time
if latency_ms is not None:
duration_ms = latency_ms
elif duration_ms is None and hasattr(self, '_llm_start_time'):
start_ts = self._llm_start_time
if start_ts:
duration_ms = (ts - start_ts) * 1000
# Track session totals for summary
if not hasattr(self, '_session_tokens_in'):
self._session_tokens_in = 0
self._session_tokens_out = 0
self._session_cost = 0.0
self._session_llm_calls = 0
self._session_tokens_in += tokens_in
self._session_tokens_out += tokens_out
if cost:
self._session_cost += cost
self._session_llm_calls += 1
# Only show metrics line in debug mode (when show_metrics is enabled)
show_metrics = getattr(self, '_show_metrics', False)
if self._format == "jsonl":
self._emit_jsonl("llm_end", agent_name=agent_name, timestamp=ts,
duration_ms=duration_ms, model=model,
tokens_in=tokens_in, tokens_out=tokens_out, cost=cost)
elif show_metrics and (tokens_in > 0 or tokens_out > 0):
# Debug mode: show metrics line
duration_str = f" [{_format_duration(duration_ms)}]" if duration_ms else ""
model_str = model.split('/')[-1] if model else "?" # Short model name
cost_str = f" (~${cost:.4f})" if cost else ""
metrics_line = f" │ 📊 {model_str}: {tokens_in}→{tokens_out} tokens{cost_str}{duration_str}"
self._emit_text(metrics_line, ts, "dim", show_timestamp=False)
def tool_start(self, tool_name: str, tool_args: Optional[Dict[str, Any]] = None, agent_name: Optional[str] = None) -> None:
"""Record tool start - stores info for inline display with result."""
ts = time.time()
self._tool_start_times[tool_name] = ts
self._pending_tool_args = tool_args # Store for display with result
self._pending_tool_name = tool_name
# Don't emit anything here - wait for result to show inline
def tool_end(
self,
tool_name: str,
duration_ms: Optional[float] = None,
status: str = "ok",
result_summary: Optional[str] = None,
agent_name: Optional[str] = None,
error_message: Optional[str] = None,
) -> None:
"""Record tool end with inline display showing tool call and result."""
ts = time.time()
# Calculate duration if not provided
if duration_ms is None and tool_name in self._tool_start_times:
start_ts = self._tool_start_times.pop(tool_name, None)
if start_ts:
duration_ms = (ts - start_ts) * 1000
if self._format == "jsonl":
self._emit_jsonl(
"tool_end",
tool_name=tool_name,
duration_ms=duration_ms,
status=status,
result_summary=_truncate(result_summary, 200) if result_summary else None,
agent_name=agent_name,
error_message=error_message,
timestamp=ts,
)
else:
# Build inline tool call with result: │ → tool(args) → result
args_str = ""
if hasattr(self, '_pending_tool_args') and self._pending_tool_args:
args_str = self._format_args(self._pending_tool_args)
result_str = ""
if result_summary:
result_str = f" → {_truncate(result_summary, 50)}"
elif error_message:
result_str = f" → ✗ {_truncate(error_message, 50)}"
color = "cyan" if status == "ok" else "red"
self._emit_text(f" │ → {tool_name}({args_str}){result_str}", ts, color)
def output(self, content: str, agent_name: Optional[str] = None) -> None:
"""Record final output."""
ts = time.time()
if self._format == "jsonl":
self._emit_jsonl("output", content=_truncate(content, 500), agent_name=agent_name, timestamp=ts)
else:
separator = "─" * 50
self._emit_text(separator, ts, "dim", show_timestamp=False)
self._emit_text("Final Output:", ts, "bold", show_timestamp=False)
# Print actual content without truncation for final output
with self._lock: # Thread-safe output
print(content, file=self._file)
def _emit_text(self, message: str, ts: float, style: str = None, show_timestamp: bool = True) -> None:
"""Emit a text line. Thread-safe for multi-agent execution."""
# Respect both instance-level and per-call timestamp settings
should_show_ts = show_timestamp and self._show_timestamps
ts_str = f"[{_format_timestamp(ts)}] " if should_show_ts else ""
with self._lock: # Thread-safe output
console = self._get_console()
if console and self._use_color:
if style:
console.print(f"[dim]{ts_str}[/dim][{style}]{message}[/{style}]")
else:
console.print(f"[dim]{ts_str}[/dim]{message}")
else:
print(f"{ts_str}{message}", file=self._file)
def _emit_jsonl(self, event_type: str, **kwargs) -> None:
"""Emit a JSONL line. Thread-safe for multi-agent execution."""
import json
data = {"event": event_type, **{k: v for k, v in kwargs.items() if v is not None}}
with self._lock: # Thread-safe output
print(json.dumps(data, default=str), file=self._file)
def enable_status_output(
file: TextIO = None,
format: str = "text",
redact: bool = True,
use_color: bool = True,
show_timestamps: bool = True,
show_metrics: bool = False, # Enable metrics for debug mode
) -> StatusOutput:
"""
Enable actions output mode globally.
This registers callbacks with the display system to capture
tool calls and agent lifecycle events.
Args:
file: Output file (default: stderr)
format: Output format ("text" or "jsonl")
redact: Whether to redact sensitive data (default: True)
use_color: Whether to use colored output (default: True)
show_timestamps: Whether to show timestamps (default: True)
show_metrics: Whether to show token/cost metrics (default: False)
Returns:
StatusOutput instance for programmatic access
"""
global _status_output_enabled, _status_output, _ai_call_count
# Reset AI call counter for new agent run
_ai_call_count = 0
_status_output = StatusOutput(
file=file,
format=format,
redact=redact,
use_color=use_color,
show_timestamps=show_timestamps,
show_metrics=show_metrics,
)
_status_output_enabled = True
# Register callbacks with the display system
from ..main import register_display_callback
def on_tool_call(message: str = None, tool_name: str = None, tool_input: dict = None, tool_output: str = None, **kwargs):
"""Callback for tool calls."""
if not _status_output_enabled or _status_output is None:
return
if tool_name:
# Emit tool_start when we see a tool call
_status_output.tool_start(tool_name, tool_input)
# Emit tool_end immediately since we have the output
if tool_output is not None:
_status_output.tool_end(
tool_name,
status="ok",
result_summary=str(tool_output)[:200] if tool_output else None,
)
def on_interaction(message: str = None, response: str = None, agent_name: str = None, generation_time: float = None, **kwargs):
"""Callback for agent interactions (final output)."""
if not _status_output_enabled or _status_output is None:
return
# Only emit output for final responses
if response:
_status_output.output(response, agent_name)
def on_error(message: str = None, **kwargs):
"""Callback for errors."""
if not _status_output_enabled or _status_output is None:
return
if message:
ts = time.time()
_status_output._emit_text(f"✗ Error: {message}", ts, "red")
def on_llm_start(model: str = None, agent_name: str = None, **kwargs):
"""Callback for LLM calls."""
if not _status_output_enabled or _status_output is None:
return
_status_output.llm_start(model=model, agent_name=agent_name)
def on_llm_end(model: str = None, tokens_in: int = 0, tokens_out: int = 0, cost: float = None, latency_ms: float = None, **kwargs):
"""Callback for LLM call completion with metrics."""
if not _status_output_enabled or _status_output is None:
return
_status_output.llm_end(model=model, tokens_in=tokens_in, tokens_out=tokens_out, cost=cost, latency_ms=latency_ms)
# Register the callbacks
register_display_callback('tool_call', on_tool_call)
register_display_callback('interaction', on_interaction)
register_display_callback('error', on_error)
register_display_callback('llm_start', on_llm_start)
register_display_callback('llm_end', on_llm_end)
return _status_output
def disable_status_output() -> None:
"""Disable actions output mode."""
global _status_output_enabled, _status_output
_status_output_enabled = False
_status_output = None
def is_status_output_enabled() -> bool:
"""Check if actions mode is enabled."""
return _status_output_enabled
def get_status_output() -> Optional[StatusOutput]:
"""Get the current actions sink."""
return _status_output
__all__ = [
"StatusOutput",
"enable_status_output",
"disable_status_output",
"is_status_output_enabled",
"get_status_output",
]
@@ -0,0 +1,209 @@
"""
Output Style for PraisonAI Agents.
Defines output style configurations.
"""
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from enum import Enum
class StylePreset(str, Enum):
"""Predefined output styles."""
CONCISE = "concise"
DETAILED = "detailed"
TECHNICAL = "technical"
CONVERSATIONAL = "conversational"
STRUCTURED = "structured"
MINIMAL = "minimal"
@dataclass
class OutputStyle:
"""
Output style configuration.
Controls how agent responses are formatted.
"""
name: str = "default"
preset: Optional[StylePreset] = None
# Length control
max_length: Optional[int] = None
target_length: Optional[int] = None # Soft target
# Format settings
format: str = "markdown" # markdown, plain, json, html
use_headers: bool = True
use_lists: bool = True
use_code_blocks: bool = True
# Tone settings
tone: str = "professional" # professional, casual, technical, friendly
verbosity: str = "normal" # minimal, normal, verbose
# Content settings
include_examples: bool = True
include_explanations: bool = True
include_caveats: bool = True
# Custom instructions
custom_instructions: Optional[str] = None
def __post_init__(self):
if self.preset:
self._apply_preset(self.preset)
def _apply_preset(self, preset: StylePreset):
"""Apply preset settings."""
if preset == StylePreset.CONCISE:
self.verbosity = "minimal"
self.include_examples = False
self.include_caveats = False
self.target_length = 500
elif preset == StylePreset.DETAILED:
self.verbosity = "verbose"
self.include_examples = True
self.include_explanations = True
self.include_caveats = True
elif preset == StylePreset.TECHNICAL:
self.tone = "technical"
self.use_code_blocks = True
self.include_examples = True
elif preset == StylePreset.CONVERSATIONAL:
self.tone = "friendly"
self.use_headers = False
self.format = "plain"
elif preset == StylePreset.STRUCTURED:
self.use_headers = True
self.use_lists = True
self.format = "markdown"
elif preset == StylePreset.MINIMAL:
self.verbosity = "minimal"
self.use_headers = False
self.use_lists = False
self.include_examples = False
self.include_explanations = False
self.include_caveats = False
self.target_length = 200
@classmethod
def concise(cls) -> "OutputStyle":
"""Create a concise style."""
return cls(name="concise", preset=StylePreset.CONCISE)
@classmethod
def detailed(cls) -> "OutputStyle":
"""Create a detailed style."""
return cls(name="detailed", preset=StylePreset.DETAILED)
@classmethod
def technical(cls) -> "OutputStyle":
"""Create a technical style."""
return cls(name="technical", preset=StylePreset.TECHNICAL)
@classmethod
def conversational(cls) -> "OutputStyle":
"""Create a conversational style."""
return cls(name="conversational", preset=StylePreset.CONVERSATIONAL)
@classmethod
def structured(cls) -> "OutputStyle":
"""Create a structured style."""
return cls(name="structured", preset=StylePreset.STRUCTURED)
@classmethod
def minimal(cls) -> "OutputStyle":
"""Create a minimal style."""
return cls(name="minimal", preset=StylePreset.MINIMAL)
def get_system_prompt_addition(self) -> str:
"""Generate system prompt addition for this style."""
parts = []
# Verbosity
if self.verbosity == "minimal":
parts.append("Be concise and direct. Avoid unnecessary elaboration.")
elif self.verbosity == "verbose":
parts.append("Provide detailed explanations with thorough coverage.")
# Tone
if self.tone == "technical":
parts.append("Use technical language appropriate for developers.")
elif self.tone == "friendly":
parts.append("Use a friendly, conversational tone.")
elif self.tone == "casual":
parts.append("Keep the tone casual and approachable.")
# Format
if self.format == "markdown":
parts.append("Format responses using Markdown.")
elif self.format == "plain":
parts.append("Use plain text without special formatting.")
elif self.format == "json":
parts.append("Return responses as JSON when appropriate.")
# Length
if self.target_length:
parts.append(f"Target response length: ~{self.target_length} characters.")
if self.max_length:
parts.append(f"Maximum response length: {self.max_length} characters.")
# Content
if not self.include_examples:
parts.append("Skip examples unless specifically requested.")
if not self.include_caveats:
parts.append("Skip caveats and warnings unless critical.")
# Custom
if self.custom_instructions:
parts.append(self.custom_instructions)
return " ".join(parts)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"name": self.name,
"preset": self.preset.value if self.preset else None,
"max_length": self.max_length,
"target_length": self.target_length,
"format": self.format,
"tone": self.tone,
"verbosity": self.verbosity,
"use_headers": self.use_headers,
"use_lists": self.use_lists,
"use_code_blocks": self.use_code_blocks,
"include_examples": self.include_examples,
"include_explanations": self.include_explanations,
"include_caveats": self.include_caveats
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "OutputStyle":
"""Create from dictionary."""
preset = None
if data.get("preset"):
preset = StylePreset(data["preset"])
return cls(
name=data.get("name", "default"),
preset=preset,
max_length=data.get("max_length"),
target_length=data.get("target_length"),
format=data.get("format", "markdown"),
tone=data.get("tone", "professional"),
verbosity=data.get("verbosity", "normal"),
use_headers=data.get("use_headers", True),
use_lists=data.get("use_lists", True),
use_code_blocks=data.get("use_code_blocks", True),
include_examples=data.get("include_examples", True),
include_explanations=data.get("include_explanations", True),
include_caveats=data.get("include_caveats", True)
)
@@ -0,0 +1,345 @@
"""
Status Output Mode for PraisonAI Agents.
Provides a clean, simple output mode that shows:
1. LLM call status (calling, completed)
2. Tool execution status (name, duration, result)
3. Final response (no boxes, just text)
This is ideal for:
- CLI users who want minimal but informative output
- External apps that parse status updates
- Logging and monitoring
Usage:
from praisonaiagents import Agent
# Enable via preset
agent = Agent(instructions="...", output="status")
agent.start("Do something")
# Output:
# [13:45:02] Calling LLM (gpt-4o-mini)...
# [13:45:03] Executing tool: get_weather
# [13:45:03] Tool completed (0.1s)
# Response: The weather in Paris is sunny.
"""
import sys
import time
import threading
from datetime import datetime
from typing import Any, Dict, Optional, TextIO
# Global state for status mode
_trace_output_enabled = False
_trace_output: Optional['TraceOutput'] = None
_output_lock = threading.Lock() # Multi-agent safe output lock
def _format_timestamp() -> str:
"""Format current time as HH:MM:SS."""
return datetime.now().strftime("%H:%M:%S")
class TraceOutput:
"""
Sink for clean status output.
Formats and outputs status events in a simple, readable format.
No boxes, no panels, just timestamped status lines.
Thread-safe for multi-agent concurrent execution.
"""
def __init__(
self,
file: TextIO = None,
use_color: bool = True,
show_timestamps: bool = True,
use_markdown: bool = True,
):
self._file = file or sys.stderr
self._use_color = use_color
self._show_timestamps = show_timestamps
self._use_markdown = use_markdown
self._console = None
self._lock = threading.Lock()
self._tool_start_times: Dict[str, float] = {}
self._llm_start_time: Optional[float] = None
def _get_console(self):
"""Get Rich console for colored output."""
if self._console is None and self._use_color:
try:
from rich.console import Console
self._console = Console(file=self._file, force_terminal=True)
except ImportError:
self._console = None
return self._console
def _emit(self, message: str, style: str = None) -> None:
"""Emit a status line. Thread-safe."""
ts_str = f"[{_format_timestamp()}] " if self._show_timestamps else ""
with self._lock:
console = self._get_console()
if console and self._use_color:
if style:
console.print(f"[dim]{ts_str}[/dim][{style}]{message}[/{style}]")
else:
console.print(f"[dim]{ts_str}[/dim]{message}")
else:
print(f"{ts_str}{message}", file=self._file)
def llm_start(self, model: str = None) -> None:
"""Record LLM call start."""
self._llm_start_time = time.time()
model_str = f" ({model})" if model else ""
self._emit(f"Calling LLM{model_str}...", "cyan")
def llm_end(
self,
duration_ms: float = None,
model: str = None,
tokens_in: int = 0,
tokens_out: int = 0,
cost: float = None,
) -> None:
"""Record LLM call end with optional metrics."""
# If duration_ms was passed and is positive, use it directly
# Otherwise calculate from internal tracking
if duration_ms is not None and duration_ms > 0:
pass # Use the passed value
elif self._llm_start_time:
duration_ms = (time.time() - self._llm_start_time) * 1000
duration_str = f" [{duration_ms/1000:.1f}s]" if duration_ms and duration_ms > 0 else ""
# Show metrics if tokens are available
if tokens_in > 0 or tokens_out > 0:
model_str = model.split('/')[-1] if model else "?"
cost_str = f" (~${cost:.4f})" if cost and cost > 0 else ""
metrics_line = f" │ 📊 {model_str}: {tokens_in}→{tokens_out} tokens{cost_str}{duration_str}"
self._emit(metrics_line, "dim")
else:
self._emit(f"LLM responded{duration_str}", "green")
self._llm_start_time = None
def tool_start(self, tool_name: str, tool_args: Dict[str, Any] = None) -> None:
"""Record tool execution start."""
self._tool_start_times[tool_name] = time.time()
# Format args compactly
args_str = ""
if tool_args:
parts = []
for k, v in tool_args.items():
v_str = str(v)
if len(v_str) > 30:
v_str = v_str[:27] + "..."
parts.append(f"{k}={repr(v_str) if isinstance(v, str) else v_str}")
args_str = f"({', '.join(parts)})"
self._emit(f"▸ {tool_name}{args_str}", "blue")
def tool_end(
self,
tool_name: str,
duration_ms: float = None,
result: str = None,
success: bool = True,
) -> None:
"""Record tool execution end."""
# Calculate duration if not provided
if duration_ms is None and tool_name in self._tool_start_times:
start_ts = self._tool_start_times.pop(tool_name, None)
if start_ts:
duration_ms = (time.time() - start_ts) * 1000
duration_str = f" [{duration_ms/1000:.1f}s]" if duration_ms else ""
status_icon = "✓" if success else "✗"
# Show result inline if short
result_str = ""
if result:
result_preview = str(result)
if len(result_preview) > 50:
result_preview = result_preview[:47] + "..."
result_str = f" → {result_preview}"
color = "green" if success else "red"
self._emit(f" └─ {tool_name}{result_str}{duration_str} {status_icon}", color)
def response(self, content: str, agent_name: str = None) -> None:
"""Output final response with optional Markdown rendering."""
with self._lock:
console = self._get_console()
# Separator line
if console and self._use_color:
console.print("")
console.print("[bold]Response:[/bold]")
# Render as Markdown if enabled
if self._use_markdown:
try:
from rich.markdown import Markdown
console.print(Markdown(content))
except ImportError:
# Fallback if markdown-it not available
console.print(content)
else:
console.print(content)
else:
print("", file=self._file)
print("Response:", file=self._file)
print(content, file=self._file)
def error(self, message: str) -> None:
"""Output error message."""
self._emit(f"✗ Error: {message}", "red")
def enable_trace_output(
file: TextIO = None,
use_color: bool = True,
show_timestamps: bool = True,
use_markdown: bool = True,
) -> TraceOutput:
"""
Enable status output mode globally.
This registers callbacks with the display system to capture
tool calls and output clean status updates.
Args:
file: Output file (default: stderr)
use_color: Whether to use colored output (default: True)
show_timestamps: Whether to show timestamps (default: True)
use_markdown: Whether to render response as Markdown (default: True)
Returns:
TraceOutput instance
"""
global _trace_output_enabled, _trace_output
_trace_output = TraceOutput(
file=file,
use_color=use_color,
show_timestamps=show_timestamps,
use_markdown=use_markdown,
)
_trace_output_enabled = True
# Register callbacks with the display system
from ..main import register_display_callback
def on_tool_call(
message: str = None,
tool_name: str = None,
tool_input: dict = None,
tool_output: str = None,
**kwargs
):
"""Callback for tool calls."""
if not _trace_output_enabled or _trace_output is None:
return
# Handle structured tool call (when tool_name is set)
if tool_name:
_trace_output.tool_start(tool_name, tool_input)
# If we have output, call tool_end immediately for inline display
if tool_output is not None:
_trace_output.tool_end(tool_name, result=str(tool_output)[:200], success=True)
return
# Handle legacy message format (for backward compatibility)
if message:
# Check for "Function X returned: Y" pattern
if "returned:" in message.lower():
import re
match = re.match(r'Function (\w+) returned: (.+)', message, re.IGNORECASE)
if match:
func_name = match.group(1)
result = match.group(2).strip('"')
_trace_output.tool_end(func_name, result=result, success=True)
# "Calling function" message is already shown via tool_start, skip
def on_interaction(
message: str = None,
response: str = None,
agent_name: str = None,
generation_time: float = None,
**kwargs
):
"""Callback for agent interactions (final output)."""
if not _trace_output_enabled or _trace_output is None:
return
# Output final response
if response:
_trace_output.response(response, agent_name)
def on_error(message: str = None, **kwargs):
"""Callback for errors."""
if not _trace_output_enabled or _trace_output is None:
return
if message:
_trace_output.error(message)
def on_llm_start(model: str = None, agent_name: str = None, **kwargs):
"""Callback for LLM call start."""
if not _trace_output_enabled or _trace_output is None:
return
_trace_output.llm_start(model=model)
def on_llm_end(model: str = None, tokens_in: int = 0, tokens_out: int = 0, cost: float = None, latency_ms: float = None, **kwargs):
"""Callback for LLM call completion with optional metrics."""
if not _trace_output_enabled or _trace_output is None:
return
_trace_output.llm_end(
duration_ms=latency_ms,
model=model,
tokens_in=tokens_in,
tokens_out=tokens_out,
cost=cost
)
# Register the callbacks
register_display_callback('tool_call', on_tool_call)
register_display_callback('interaction', on_interaction)
register_display_callback('error', on_error)
register_display_callback('llm_start', on_llm_start)
register_display_callback('llm_end', on_llm_end)
return _trace_output
def disable_trace_output() -> None:
"""Disable status output mode."""
global _trace_output_enabled, _trace_output
_trace_output_enabled = False
_trace_output = None
def is_trace_output_enabled() -> bool:
"""Check if status mode is enabled."""
return _trace_output_enabled
def get_trace_output() -> Optional[TraceOutput]:
"""Get the current status sink."""
return _trace_output
__all__ = [
"TraceOutput",
"enable_trace_output",
"disable_trace_output",
"is_trace_output_enabled",
"get_trace_output",
]
@@ -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)
@@ -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
@@ -13,27 +19,6 @@ TOOL_MAPPINGS = {
'searxng_search': ('.searxng_tools', None),
'searxng': ('.searxng_tools', None),
# arXiv Tools
'search_arxiv': ('.arxiv_tools', None),
'get_arxiv_paper': ('.arxiv_tools', None),
'get_papers_by_author': ('.arxiv_tools', None),
'get_papers_by_category': ('.arxiv_tools', None),
'arxiv_tools': ('.arxiv_tools', None),
# Wikipedia Tools
'wiki_search': ('.wikipedia_tools', None),
'wiki_summary': ('.wikipedia_tools', None),
'wiki_page': ('.wikipedia_tools', None),
'wiki_random': ('.wikipedia_tools', None),
'wiki_language': ('.wikipedia_tools', None),
'wikipedia_tools': ('.wikipedia_tools', None),
# Newspaper Tools
'get_article': ('.newspaper_tools', None),
'get_news_sources': ('.newspaper_tools', None),
'get_articles_from_source': ('.newspaper_tools', None),
'get_trending_topics': ('.newspaper_tools', None),
'newspaper_tools': ('.newspaper_tools', None),
# Spider Tools
'scrape_page': ('.spider_tools', None),
@@ -43,29 +28,6 @@ TOOL_MAPPINGS = {
'spider_tools': ('.spider_tools', None),
# DuckDB Tools
'query': ('.duckdb_tools', None),
'create_table': ('.duckdb_tools', None),
'load_data': ('.duckdb_tools', None),
'export_data': ('.duckdb_tools', None),
'get_table_info': ('.duckdb_tools', None),
'analyze_data': ('.duckdb_tools', None),
'duckdb_tools': ('.duckdb_tools', None),
# MongoDB Tools
'insert_document': ('.mongodb_tools', None),
'insert_documents': ('.mongodb_tools', None),
'find_documents': ('.mongodb_tools', None),
'update_document': ('.mongodb_tools', None),
'delete_document': ('.mongodb_tools', None),
'create_vector_index': ('.mongodb_tools', None),
'vector_search': ('.mongodb_tools', None),
'store_with_embedding': ('.mongodb_tools', None),
'text_search': ('.mongodb_tools', None),
'get_stats': ('.mongodb_tools', None),
'connect_mongodb': ('.mongodb_tools', None),
'mongodb_tools': ('.mongodb_tools', None),
# Shell Tools
'execute_command': ('.shell_tools', None),
'list_processes': ('.shell_tools', None),
@@ -73,19 +35,6 @@ TOOL_MAPPINGS = {
'get_system_info': ('.shell_tools', None),
'shell_tools': ('.shell_tools', None),
# Calculator Tools
'evaluate': ('.calculator_tools', None),
'solve_equation': ('.calculator_tools', None),
'convert_units': ('.calculator_tools', None),
'calculate_statistics': ('.calculator_tools', None),
'calculate_financial': ('.calculator_tools', None),
'calculator_tools': ('.calculator_tools', None),
# Class methods from YFinance
'get_stock_price': ('.yfinance_tools', 'YFinanceTools'),
'get_stock_info': ('.yfinance_tools', 'YFinanceTools'),
'get_historical_data': ('.yfinance_tools', 'YFinanceTools'),
'yfinance': ('.yfinance_tools', 'YFinanceTools'),
# File Tools
'read_file': ('.file_tools', 'FileTools'),
@@ -97,49 +46,6 @@ TOOL_MAPPINGS = {
'delete_file': ('.file_tools', 'FileTools'),
'file_tools': ('.file_tools', 'FileTools'),
# CSV Tools
'read_csv': ('.csv_tools', 'CSVTools'),
'write_csv': ('.csv_tools', 'CSVTools'),
'merge_csv': ('.csv_tools', 'CSVTools'),
'analyze_csv': ('.csv_tools', 'CSVTools'),
'split_csv': ('.csv_tools', 'CSVTools'),
'csv_tools': ('.csv_tools', 'CSVTools'),
# JSON Tools
'read_json': ('.json_tools', 'JSONTools'),
'write_json': ('.json_tools', 'JSONTools'),
'merge_json': ('.json_tools', 'JSONTools'),
'validate_json': ('.json_tools', 'JSONTools'),
'analyze_json': ('.json_tools', 'JSONTools'),
'transform_json': ('.json_tools', 'JSONTools'),
'json_tools': ('.json_tools', 'JSONTools'),
# Excel Tools
'read_excel': ('.excel_tools', 'ExcelTools'),
'write_excel': ('.excel_tools', 'ExcelTools'),
'merge_excel': ('.excel_tools', 'ExcelTools'),
'create_chart': ('.excel_tools', 'ExcelTools'),
'add_chart_to_sheet': ('.excel_tools', 'ExcelTools'),
'excel_tools': ('.excel_tools', 'ExcelTools'),
# XML Tools
'read_xml': ('.xml_tools', 'XMLTools'),
'write_xml': ('.xml_tools', 'XMLTools'),
'transform_xml': ('.xml_tools', 'XMLTools'),
'validate_xml': ('.xml_tools', 'XMLTools'),
'xml_to_dict': ('.xml_tools', 'XMLTools'),
'dict_to_xml': ('.xml_tools', 'XMLTools'),
'xpath_query': ('.xml_tools', 'XMLTools'),
'xml_tools': ('.xml_tools', 'XMLTools'),
# YAML Tools
'read_yaml': ('.yaml_tools', 'YAMLTools'),
'write_yaml': ('.yaml_tools', 'YAMLTools'),
'merge_yaml': ('.yaml_tools', 'YAMLTools'),
'validate_yaml': ('.yaml_tools', 'YAMLTools'),
'analyze_yaml': ('.yaml_tools', 'YAMLTools'),
'transform_yaml': ('.yaml_tools', 'YAMLTools'),
'yaml_tools': ('.yaml_tools', 'YAMLTools'),
# Python Tools
'execute_code': ('.python_tools', 'PythonTools'),
@@ -149,12 +55,6 @@ TOOL_MAPPINGS = {
'disassemble_code': ('.python_tools', 'PythonTools'),
'python_tools': ('.python_tools', 'PythonTools'),
# Pandas Tools
'filter_data': ('.pandas_tools', 'PandasTools'),
'get_summary': ('.pandas_tools', 'PandasTools'),
'group_by': ('.pandas_tools', 'PandasTools'),
'pivot_table': ('.pandas_tools', 'PandasTools'),
'pandas_tools': ('.pandas_tools', 'PandasTools'),
# Chain of Thought Training Tools
'cot_run': ('.train.data.generatecot', 'GenerateCOT'), # Orchestrates text solution
@@ -177,8 +77,6 @@ TOOL_MAPPINGS = {
'cot_upload_to_huggingface': ('.train.data.generatecot', 'GenerateCOT'), # Upload dataset to HuggingFace
'cot_tools': ('.train.data.generatecot', 'GenerateCOT'), # Full toolkit access
# Claude Memory Tool (Anthropic Beta)
'ClaudeMemoryTool': ('.claude_memory_tool', 'ClaudeMemoryTool'),
# Tavily Tools (requires TAVILY_API_KEY)
'tavily': ('.tavily_tools', None),
@@ -244,8 +142,8 @@ def __getattr__(name: str) -> Any:
module_path, class_name = TOOL_MAPPINGS[name]
# Return class itself (not instance) for ClaudeMemoryTool, TavilyTools, YouTools, ExaTools, Crawl4AITools
if name in ('ClaudeMemoryTool', 'TavilyTools', 'YouTools', 'ExaTools', 'Crawl4AITools'):
# Return class itself (not instance) for TavilyTools, YouTools, ExaTools, Crawl4AITools
if name in ('TavilyTools', 'YouTools', 'ExaTools', 'Crawl4AITools'):
module = import_module(module_path, __package__)
return getattr(module, class_name)
@@ -254,15 +152,8 @@ def __getattr__(name: str) -> Any:
module = import_module(module_path, __package__)
if name in [
'duckduckgo', 'internet_search', 'searxng_search', 'searxng',
'search_arxiv', 'get_arxiv_paper', 'get_papers_by_author', 'get_papers_by_category',
'wiki_search', 'wiki_summary', 'wiki_page', 'wiki_random', 'wiki_language',
'get_article', 'get_news_sources', 'get_articles_from_source', 'get_trending_topics',
'scrape_page', 'extract_links', 'crawl', 'extract_text',
'query', 'create_table', 'load_data', 'export_data', 'get_table_info', 'analyze_data',
'insert_document', 'insert_documents', 'find_documents', 'update_document', 'delete_document',
'create_vector_index', 'vector_search', 'store_with_embedding', 'text_search', 'get_stats', 'connect_mongodb',
'execute_command', 'list_processes', 'kill_process', 'get_system_info',
'evaluate', 'solve_equation', 'convert_units', 'calculate_statistics', 'calculate_financial',
'tavily', 'tavily_search', 'tavily_extract', 'tavily_crawl', 'tavily_map',
'tavily_search_async', 'tavily_extract_async',
'ydc', 'ydc_search', 'ydc_contents', 'ydc_news', 'ydc_images',
@@ -274,8 +165,7 @@ def __getattr__(name: str) -> Any:
'run_skill_script', 'read_skill_file', 'list_skill_scripts', 'create_skill_tools'
]:
return getattr(module, name)
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools', 'skill_tools']:
if name in ['file_tools', 'spider_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools', 'skill_tools']:
return module # Returns the callable module
return getattr(module, name)
else:
@@ -289,4 +179,10 @@ def __getattr__(name: str) -> Any:
method = getattr(_instances[class_name], name)
return method
__all__ = list(TOOL_MAPPINGS.keys()) + ['Injected']
__all__ = list(TOOL_MAPPINGS.keys()) + [
'Injected',
'BaseTool', 'ToolResult', 'ToolValidationError', 'validate_tool',
'tool', 'FunctionTool',
'get_registry', 'register_tool', 'get_tool', 'ToolRegistry',
'Tools'
]
@@ -1,292 +0,0 @@
"""Tools for searching and retrieving papers from arXiv.
Usage:
from praisonaiagents.tools import arxiv_tools
papers = arxiv_tools.search("quantum computing")
paper = arxiv_tools.get_paper("2401.00123")
or
from praisonaiagents.tools import search_arxiv, get_arxiv_paper
papers = search_arxiv("quantum computing")
"""
import logging
from typing import List, Dict, Union, Optional, Any
from importlib import util
import json
# Map sort criteria to arxiv.SortCriterion
SORT_CRITERIA = {
"relevance": "Relevance",
"lastupdateddate": "LastUpdatedDate",
"submitteddate": "SubmittedDate"
}
# Map sort order to arxiv.SortOrder
SORT_ORDER = {
"ascending": "Ascending",
"descending": "Descending"
}
class ArxivTools:
"""Tools for searching and retrieving papers from arXiv."""
def __init__(self):
"""Initialize ArxivTools and check for arxiv package."""
self._check_arxiv()
def _check_arxiv(self):
"""Check if arxiv package is installed."""
if util.find_spec("arxiv") is None:
raise ImportError("arxiv package is not available. Please install it using: pip install arxiv")
global arxiv
import arxiv
def search(
self,
query: str,
max_results: int = 10,
sort_by: str = "relevance",
sort_order: str = "descending",
include_fields: Optional[List[str]] = None
) -> Union[List[Dict[str, Any]], Dict[str, str]]:
"""
Search arXiv for papers matching the query.
Args:
query: Search query (e.g., "quantum computing", "author:Einstein")
max_results: Maximum number of results to return
sort_by: Sort results by ("relevance", "lastUpdatedDate", "submittedDate")
sort_order: Sort order ("ascending" or "descending")
include_fields: List of fields to include in results. If None, includes all.
Available fields: ["title", "authors", "summary", "comment", "journal_ref",
"doi", "primary_category", "categories", "links"]
Returns:
List[Dict] or Dict: List of papers or error dict
"""
try:
import arxiv
# Configure search client
client = arxiv.Client()
# Map sort criteria
sort_by_enum = getattr(arxiv.SortCriterion, SORT_CRITERIA[sort_by.lower()])
sort_order_enum = getattr(arxiv.SortOrder, SORT_ORDER[sort_order.lower()])
# Build search query
search = arxiv.Search(
query=query,
max_results=max_results,
sort_by=sort_by_enum,
sort_order=sort_order_enum
)
# Execute search
results = []
for result in client.results(search):
# Convert to dict with selected fields
paper = self._result_to_dict(result, include_fields)
results.append(paper)
return results
except Exception as e:
error_msg = f"Error searching arXiv: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def get_paper(
self,
paper_id: str,
include_fields: Optional[List[str]] = None
) -> Union[Dict[str, Any], Dict[str, str]]:
"""
Get details of a specific paper by its arXiv ID.
Args:
paper_id: arXiv paper ID (e.g., "2401.00123")
include_fields: List of fields to include in results. If None, includes all.
Available fields: ["title", "authors", "summary", "comment", "journal_ref",
"doi", "primary_category", "categories", "links"]
Returns:
Dict: Paper details or error dict
"""
try:
import arxiv
# Configure client
client = arxiv.Client()
# Get paper by ID
search = arxiv.Search(id_list=[paper_id])
results = list(client.results(search))
if not results:
return {"error": f"Paper with ID {paper_id} not found"}
# Convert to dict with selected fields
paper = self._result_to_dict(results[0], include_fields)
return paper
except Exception as e:
error_msg = f"Error getting paper {paper_id}: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def get_papers_by_author(
self,
author: str,
max_results: int = 10,
sort_by: str = "submittedDate",
sort_order: str = "descending",
include_fields: Optional[List[str]] = None
) -> Union[List[Dict[str, Any]], Dict[str, str]]:
"""
Get papers by a specific author.
Args:
author: Author name (e.g., "Einstein")
max_results: Maximum number of results to return
sort_by: Sort results by ("relevance", "lastUpdatedDate", "submittedDate")
sort_order: Sort order ("ascending" or "descending")
include_fields: List of fields to include in results
Returns:
List[Dict] or Dict: List of papers or error dict
"""
query = f'au:"{author}"'
return self.search(query, max_results, sort_by, sort_order, include_fields)
def get_papers_by_category(
self,
category: str,
max_results: int = 10,
sort_by: str = "submittedDate",
sort_order: str = "descending",
include_fields: Optional[List[str]] = None
) -> Union[List[Dict[str, Any]], Dict[str, str]]:
"""
Get papers from a specific category.
Args:
category: arXiv category (e.g., "cs.AI", "physics.gen-ph")
max_results: Maximum number of results to return
sort_by: Sort results by ("relevance", "lastUpdatedDate", "submittedDate")
sort_order: Sort order ("ascending" or "descending")
include_fields: List of fields to include in results
Returns:
List[Dict] or Dict: List of papers or error dict
"""
query = f'cat:{category}'
return self.search(query, max_results, sort_by, sort_order, include_fields)
def _result_to_dict(
self,
result: Any,
include_fields: Optional[List[str]] = None
) -> Dict[str, Any]:
"""Convert arxiv.Result to dictionary with selected fields."""
# Default fields to include
if include_fields is None:
include_fields = [
"title", "authors", "summary", "comment", "journal_ref",
"doi", "primary_category", "categories", "links"
]
# Build paper dict with selected fields
paper = {}
# Always include these basic fields
paper["arxiv_id"] = result.entry_id.split("/")[-1]
paper["updated"] = result.updated.isoformat() if result.updated else None
paper["published"] = result.published.isoformat() if result.published else None
# Add requested fields
if "title" in include_fields:
paper["title"] = result.title
if "authors" in include_fields:
paper["authors"] = [str(author) for author in result.authors]
if "summary" in include_fields:
paper["summary"] = result.summary
if "comment" in include_fields:
paper["comment"] = result.comment
if "journal_ref" in include_fields:
paper["journal_ref"] = result.journal_ref
if "doi" in include_fields:
paper["doi"] = result.doi
if "primary_category" in include_fields:
paper["primary_category"] = result.primary_category
if "categories" in include_fields:
paper["categories"] = result.categories
if "links" in include_fields:
paper["pdf_url"] = result.pdf_url
paper["abstract_url"] = f"https://arxiv.org/abs/{paper['arxiv_id']}"
return paper
# Create instance for direct function access
_arxiv_tools = ArxivTools()
search_arxiv = _arxiv_tools.search
get_arxiv_paper = _arxiv_tools.get_paper
get_papers_by_author = _arxiv_tools.get_papers_by_author
get_papers_by_category = _arxiv_tools.get_papers_by_category
if __name__ == "__main__":
# Example usage
print("\n==================================================")
print("ArxivTools Demonstration")
print("==================================================\n")
# 1. Search for papers
print("1. Searching for Papers")
print("------------------------------")
query = "quantum computing"
papers = search_arxiv(query, max_results=3)
print(f"Papers about {query}:")
if isinstance(papers, list):
print(json.dumps(papers, indent=2))
else:
print(papers) # Show error
print()
# 2. Get specific paper
print("2. Getting Specific Paper")
print("------------------------------")
if isinstance(papers, list) and papers:
paper_id = papers[0]["arxiv_id"]
paper = get_arxiv_paper(paper_id)
print(f"Paper {paper_id}:")
if "error" not in paper:
print(json.dumps(paper, indent=2))
else:
print(paper) # Show error
print()
# 3. Get papers by author
print("3. Getting Papers by Author")
print("------------------------------")
author = "Yoshua Bengio"
author_papers = get_papers_by_author(author, max_results=3)
print(f"Papers by {author}:")
if isinstance(author_papers, list):
print(json.dumps(author_papers, indent=2))
else:
print(author_papers) # Show error
print()
# 4. Get papers by category
print("4. Getting Papers by Category")
print("------------------------------")
category = "cs.AI"
category_papers = get_papers_by_category(category, max_results=3)
print(f"Papers in category {category}:")
if isinstance(category_papers, list):
print(json.dumps(category_papers, indent=2))
else:
print(category_papers) # Show error
print("\n==================================================")
print("Demonstration Complete")
print("==================================================")
@@ -1,278 +0,0 @@
"""Tools for performing calculations.
Usage:
from praisonaiagents.tools import calculator_tools
result = calculator_tools.evaluate("2 + 2 * 3")
or
from praisonaiagents.tools import evaluate, solve_equation, convert_units
result = evaluate("2 + 2 * 3")
"""
import logging
from typing import List, Dict, Optional, Any
from importlib import util
import math
class CalculatorTools:
"""Tools for performing calculations."""
def __init__(self):
"""Initialize CalculatorTools."""
self._check_dependencies()
def _check_dependencies(self):
"""Check if required packages are installed."""
missing = []
for package in ['sympy', 'pint']:
if util.find_spec(package) is None:
missing.append(package)
if missing:
raise ImportError(
f"Required packages not available. Please install: {', '.join(missing)}\n"
f"Run: pip install {' '.join(missing)}"
)
def evaluate(
self,
expression: str,
variables: Optional[Dict[str, float]] = None,
precision: int = 10
) -> Dict[str, Any]:
"""Evaluate a mathematical expression."""
try:
if util.find_spec('sympy') is None:
error_msg = "sympy package is not available. Please install it using: pip install sympy"
logging.error(error_msg)
return {"error": error_msg}
# Import sympy only when needed
import sympy
# Replace common mathematical functions
expression = expression.lower()
replacements = {
'sin(': 'math.sin(',
'cos(': 'math.cos(',
'tan(': 'math.tan(',
'sqrt(': 'math.sqrt(',
'log(': 'math.log(',
'exp(': 'math.exp(',
'pi': str(math.pi),
'e': str(math.e)
}
for old, new in replacements.items():
expression = expression.replace(old, new)
# Create safe namespace
safe_dict = {
'math': math,
'__builtins__': None
}
if variables:
safe_dict.update(variables)
# Evaluate expression
result = eval(expression, safe_dict)
# Round to specified precision
if isinstance(result, (int, float)):
return {"result": round(float(result), precision)}
return {"error": "Invalid expression"}
except Exception as e:
error_msg = f"Error evaluating expression: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def solve_equation(
self,
equation: str,
variable: str = 'x'
) -> Dict[str, Any]:
"""Solve a mathematical equation."""
try:
if util.find_spec('sympy') is None:
error_msg = "sympy package is not available. Please install it using: pip install sympy"
logging.error(error_msg)
return {"error": error_msg}
# Import sympy only when needed
import sympy
# Parse equation
if '=' in equation:
left, right = equation.split('=')
equation = f"({left}) - ({right})"
# Convert to SymPy expression
x = sympy.Symbol(variable)
expr = sympy.sympify(equation)
# Solve equation
solutions = sympy.solve(expr, x)
# Convert complex solutions to real if possible
real_solutions = []
for sol in solutions:
if sol.is_real:
real_solutions.append(float(sol))
return {"solutions": real_solutions} if real_solutions else {"error": "No real solutions found"}
except Exception as e:
error_msg = f"Error solving equation: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def convert_units(
self,
value: float,
from_unit: str,
to_unit: str
) -> Dict[str, Any]:
"""Convert between different units."""
try:
if util.find_spec('pint') is None:
error_msg = "pint package is not available. Please install it using: pip install pint"
logging.error(error_msg)
return {"error": error_msg}
# Import pint only when needed
import pint
# Create quantity with source unit
ureg = pint.UnitRegistry()
quantity = value * ureg(from_unit)
# Convert to target unit
result = quantity.to(to_unit)
return {"result": float(result.magnitude)}
except Exception as e:
error_msg = f"Error converting units: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def calculate_statistics(
self,
numbers: List[float]
) -> Dict[str, Any]:
"""Calculate basic statistics for a list of numbers."""
try:
if not numbers:
return {"error": "No numbers provided"}
n = len(numbers)
mean = sum(numbers) / n
# Calculate variance and std dev
squared_diff_sum = sum((x - mean) ** 2 for x in numbers)
variance = squared_diff_sum / (n - 1) if n > 1 else 0
std_dev = math.sqrt(variance)
# Calculate median
sorted_nums = sorted(numbers)
if n % 2 == 0:
median = (sorted_nums[n//2 - 1] + sorted_nums[n//2]) / 2
else:
median = sorted_nums[n//2]
return {
"mean": mean,
"median": median,
"std_dev": std_dev,
"variance": variance,
"min": min(numbers),
"max": max(numbers),
"range": max(numbers) - min(numbers),
"count": n
}
except Exception as e:
error_msg = f"Error calculating statistics: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def calculate_financial(
self,
principal: float,
rate: float,
time: float,
frequency: str = 'yearly'
) -> Dict[str, Any]:
"""Calculate financial metrics like compound interest."""
try:
# Convert rate to decimal
rate = rate / 100
# Get compounding frequency
freq_map = {
'yearly': 1,
'semi-annual': 2,
'quarterly': 4,
'monthly': 12,
'daily': 365
}
n = freq_map.get(frequency.lower(), 1)
# Calculate compound interest
amount = principal * (1 + rate/n)**(n*time)
interest = amount - principal
# Calculate simple interest
simple_interest = principal * rate * time
return {
"final_amount": amount,
"compound_interest": interest,
"simple_interest": simple_interest,
"difference": interest - simple_interest
}
except Exception as e:
error_msg = f"Error calculating financial metrics: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
# Create instance for direct function access
_calculator_tools = CalculatorTools()
evaluate = _calculator_tools.evaluate
solve_equation = _calculator_tools.solve_equation
convert_units = _calculator_tools.convert_units
calculate_statistics = _calculator_tools.calculate_statistics
calculate_financial = _calculator_tools.calculate_financial
if __name__ == "__main__":
print("\n==================================================")
print("CalculatorTools Demonstration")
print("==================================================\n")
print("1. Basic Arithmetic")
print("------------------------------")
print("2 + 2 =", evaluate("2 + 2")["result"])
print("5 * 6 =", evaluate("5 * 6")["result"])
print("10 / 3 =", evaluate("10 / 3")["result"])
print()
print("2. Complex Expressions")
print("------------------------------")
print("(2 + 3) * 4 =", evaluate("(2 + 3) * 4")["result"])
print("2^3 + 4 =", evaluate("2^3 + 4")["result"])
print("sqrt(16) + 2 =", evaluate("sqrt(16) + 2")["result"])
print()
print("3. Solving Equations")
print("------------------------------")
result = solve_equation("x^2 - 5*x + 6 = 0")
print("x^2 - 5*x + 6 = 0")
print("Solutions:", result["solutions"])
print()
print("4. Unit Conversions")
print("------------------------------")
print("100 meters to kilometers:", convert_units(100, "meters", "kilometers")["result"])
print("1 hour to minutes:", convert_units(1, "hours", "minutes")["result"])
print("32 fahrenheit to celsius:", convert_units(32, "fahrenheit", "celsius")["result"])
print()
print("==================================================")
print("Demonstration Complete")
print("==================================================")
@@ -1,529 +0,0 @@
"""
Claude Memory Tool - File-based persistent memory for Claude models.
This module implements Anthropic's Memory Tool API (beta) which enables Claude
to store and retrieve information across conversations through a memory file directory.
The memory tool operates client-side - you control where and how the data is stored.
Beta Header: context-management-2025-06-27
Tool Type: memory_20250818
Supported Models:
- Claude Sonnet 4.5 (claude-sonnet-4-5-20250929)
- Claude Sonnet 4 (claude-sonnet-4-20250514)
- Claude Haiku 4.5 (claude-haiku-4-5-20251001)
- Claude Opus 4.5 (claude-opus-4-5-20251101)
- Claude Opus 4.1 (claude-opus-4-1-20250805)
- Claude Opus 4 (claude-opus-4-20250514)
Reference: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/memory-tool
"""
import shutil
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Memory system prompt that Claude uses to manage memory
MEMORY_SYSTEM_PROMPT = """
IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.
MEMORY PROTOCOL:
1. Use the `view` command of your `memory` tool to check for earlier progress.
2. ... (work on the task) ...
- As you make progress, record status / progress / thoughts etc in your memory.
ASSUME INTERRUPTION: Your context window might be reset at any moment, so you risk losing any progress that is not recorded in your memory directory.
When editing your memory folder, always try to keep its content up-to-date, coherent and organized. You can rename or delete files that are no longer relevant. Do not create new files unless necessary.
"""
class ClaudeMemoryTool:
"""
File-based memory tool for Claude models.
Implements Anthropic's Memory Tool API (beta). Claude autonomously decides
what to store and retrieve from the memory directory.
Storage: {base_path}/memories/
Commands:
- view: Show directory contents or file contents with optional line ranges
- create: Create or overwrite a file
- str_replace: Replace text in a file
- insert: Insert text at a specific line
- delete: Delete a file or directory
- rename: Rename or move a file/directory
Example:
```python
from praisonaiagents import Agent
from praisonaiagents.tools import ClaudeMemoryTool
# Use default storage
agent = Agent(
name="Assistant",
llm="anthropic/claude-sonnet-4-20250514",
claude_memory=True
)
# Or with custom storage path
memory = ClaudeMemoryTool(base_path="./my_project/memory")
agent = Agent(
name="Assistant",
llm="anthropic/claude-sonnet-4-20250514",
claude_memory=memory
)
```
Security:
- All paths are validated to prevent directory traversal attacks
- Paths must start with /memories
- Symlinks are not followed outside the memory directory
"""
# Beta header required for memory tool
BETA_HEADER = "context-management-2025-06-27"
# Tool definition for API requests
TOOL_DEFINITION = {
"type": "memory_20250818",
"name": "memory"
}
def __init__(
self,
base_path: Optional[str] = None,
user_id: Optional[str] = None,
max_file_size: int = 1024 * 1024, # 1MB default
max_files: int = 100
):
"""
Initialize the Claude Memory Tool.
Args:
base_path: Base directory for memory storage.
Defaults to .praison/claude_memory/{user_id}
user_id: User identifier for user-specific memory.
Defaults to "default"
max_file_size: Maximum size per file in bytes (default 1MB)
max_files: Maximum number of files allowed (default 100)
"""
self.user_id = user_id or "default"
if base_path:
self.base_path = Path(base_path)
else:
self.base_path = Path(f".praison/claude_memory/{self.user_id}")
self.memory_root = self.base_path / "memories"
self.max_file_size = max_file_size
self.max_files = max_files
# Create memory directory
self.memory_root.mkdir(parents=True, exist_ok=True)
logger.debug(f"ClaudeMemoryTool initialized at {self.memory_root}")
def _validate_path(self, path: str) -> Path:
"""
Validate and resolve memory paths securely.
Args:
path: Path string starting with /memories
Returns:
Resolved Path object within memory directory
Raises:
ValueError: If path is invalid or attempts directory traversal
"""
if not path:
raise ValueError("Path cannot be empty")
if not path.startswith("/memories"):
raise ValueError(f"Path must start with /memories, got: {path}")
# Remove /memories prefix and leading slashes
relative_path = path[len("/memories"):].lstrip("/")
# Resolve the full path
if relative_path:
full_path = self.memory_root / relative_path
else:
full_path = self.memory_root
# Security check: ensure resolved path is within memory_root
try:
resolved = full_path.resolve()
resolved.relative_to(self.memory_root.resolve())
except ValueError as e:
raise ValueError(f"Path {path} would escape /memories directory") from e
# Check for suspicious patterns
if ".." in path or "~" in path:
raise ValueError(f"Invalid path pattern in: {path}")
return full_path
def execute(self, command: str, **kwargs) -> str:
"""
Execute a memory command.
Args:
command: Command name (view, create, str_replace, insert, delete, rename)
**kwargs: Command-specific arguments
Returns:
Result string from the command
Raises:
ValueError: If command is unknown
"""
commands = {
"view": self.view,
"create": self.create,
"str_replace": self.str_replace,
"insert": self.insert,
"delete": self.delete,
"rename": self.rename
}
if command not in commands:
raise ValueError(f"Unknown command: {command}. Valid commands: {list(commands.keys())}")
return commands[command](**kwargs)
def view(self, path: str, view_range: Optional[List[int]] = None) -> str:
"""
Show directory contents or file contents with optional line ranges.
Args:
path: Path to view (e.g., "/memories" or "/memories/notes.txt")
view_range: Optional [start_line, end_line] for file viewing.
Use -1 for end_line to read to end of file.
Returns:
Directory listing or file contents with line numbers
Raises:
RuntimeError: If path cannot be read
"""
full_path = self._validate_path(path)
if full_path.is_dir():
# List directory contents
items: List[str] = []
try:
for item in sorted(full_path.iterdir()):
# Skip hidden files
if item.name.startswith("."):
continue
# Add trailing slash for directories
items.append(f"{item.name}/" if item.is_dir() else item.name)
result = f"Directory: {path}\n"
if items:
result += "\n".join([f"- {item}" for item in items])
else:
result += "(empty directory)"
return result
except Exception as e:
raise RuntimeError(f"Cannot read directory {path}: {e}") from e
elif full_path.is_file():
# Read file contents
try:
content = full_path.read_text(encoding="utf-8")
lines = content.splitlines()
# Apply view range if specified
if view_range:
start_line = max(1, view_range[0]) - 1 # Convert to 0-indexed
end_line = len(lines) if view_range[1] == -1 else view_range[1]
lines = lines[start_line:end_line]
start_num = start_line + 1
else:
start_num = 1
# Add line numbers
numbered_lines = [
f"{i + start_num:4d}: {line}"
for i, line in enumerate(lines)
]
return "\n".join(numbered_lines) if numbered_lines else "(empty file)"
except Exception as e:
raise RuntimeError(f"Cannot read file {path}: {e}") from e
else:
raise RuntimeError(f"Path not found: {path}")
def create(self, path: str, file_text: str) -> str:
"""
Create or overwrite a file.
Args:
path: Path for the new file (e.g., "/memories/notes.txt")
file_text: Content to write to the file
Returns:
Success message
Raises:
ValueError: If file would exceed size limit or max files reached
"""
full_path = self._validate_path(path)
# Check file size limit
if len(file_text.encode('utf-8')) > self.max_file_size:
raise ValueError(
f"File content exceeds maximum size of {self.max_file_size} bytes"
)
# Check max files limit (only for new files)
if not full_path.exists():
current_files = sum(1 for _ in self.memory_root.rglob("*") if _.is_file())
if current_files >= self.max_files:
raise ValueError(
f"Maximum number of files ({self.max_files}) reached. "
"Delete some files before creating new ones."
)
# Create parent directories if needed
full_path.parent.mkdir(parents=True, exist_ok=True)
# Write file
full_path.write_text(file_text, encoding="utf-8")
return f"File created successfully at {path}"
def str_replace(self, path: str, old_str: str, new_str: str) -> str:
"""
Replace text in a file.
Args:
path: Path to the file
old_str: Text to find (must be unique in file)
new_str: Replacement text
Returns:
Success message
Raises:
FileNotFoundError: If file doesn't exist
ValueError: If old_str not found or appears multiple times
"""
full_path = self._validate_path(path)
if not full_path.is_file():
raise FileNotFoundError(f"File not found: {path}")
content = full_path.read_text(encoding="utf-8")
# Count occurrences
count = content.count(old_str)
if count == 0:
raise ValueError(f"Text not found in {path}")
elif count > 1:
raise ValueError(
f"Text appears {count} times in {path}. Must be unique for replacement."
)
# Replace and write
new_content = content.replace(old_str, new_str)
# Check size limit
if len(new_content.encode('utf-8')) > self.max_file_size:
raise ValueError(
f"Resulting file would exceed maximum size of {self.max_file_size} bytes"
)
full_path.write_text(new_content, encoding="utf-8")
return f"File {path} has been edited"
def insert(self, path: str, insert_line: int, insert_text: str) -> str:
"""
Insert text at a specific line.
Args:
path: Path to the file
insert_line: Line number to insert at (0-indexed)
insert_text: Text to insert
Returns:
Success message
Raises:
FileNotFoundError: If file doesn't exist
ValueError: If line number is invalid
"""
full_path = self._validate_path(path)
if not full_path.is_file():
raise FileNotFoundError(f"File not found: {path}")
lines = full_path.read_text(encoding="utf-8").splitlines()
# Validate line number
if insert_line < 0 or insert_line > len(lines):
raise ValueError(
f"Invalid insert_line {insert_line}. Must be 0-{len(lines)}"
)
# Insert the text (strip trailing newline to avoid double newlines)
lines.insert(insert_line, insert_text.rstrip("\n"))
# Reconstruct content
new_content = "\n".join(lines) + "\n"
# Check size limit
if len(new_content.encode('utf-8')) > self.max_file_size:
raise ValueError(
f"Resulting file would exceed maximum size of {self.max_file_size} bytes"
)
full_path.write_text(new_content, encoding="utf-8")
return f"Text inserted at line {insert_line} in {path}"
def delete(self, path: str) -> str:
"""
Delete a file or directory.
Args:
path: Path to delete
Returns:
Success message
Raises:
ValueError: If trying to delete /memories root
FileNotFoundError: If path doesn't exist
"""
# Prevent deleting the root memories directory
if path == "/memories" or path == "/memories/":
raise ValueError("Cannot delete the /memories directory itself")
full_path = self._validate_path(path)
if full_path.is_file():
full_path.unlink()
return f"File deleted: {path}"
elif full_path.is_dir():
shutil.rmtree(full_path)
return f"Directory deleted: {path}"
else:
raise FileNotFoundError(f"Path not found: {path}")
def rename(self, old_path: str, new_path: str) -> str:
"""
Rename or move a file/directory.
Args:
old_path: Current path
new_path: New path
Returns:
Success message
Raises:
FileNotFoundError: If source doesn't exist
ValueError: If destination already exists
"""
old_full_path = self._validate_path(old_path)
new_full_path = self._validate_path(new_path)
if not old_full_path.exists():
raise FileNotFoundError(f"Source path not found: {old_path}")
if new_full_path.exists():
raise ValueError(f"Destination already exists: {new_path}")
# Create parent directories if needed
new_full_path.parent.mkdir(parents=True, exist_ok=True)
# Rename/move
old_full_path.rename(new_full_path)
return f"Renamed {old_path} to {new_path}"
def clear_all(self) -> str:
"""
Clear all memory files.
Returns:
Success message
"""
if self.memory_root.exists():
shutil.rmtree(self.memory_root)
self.memory_root.mkdir(parents=True, exist_ok=True)
return "All memory cleared"
def get_tool_definition(self) -> Dict[str, Any]:
"""
Get the tool definition for API requests.
Returns:
Tool definition dict for Anthropic API
"""
return self.TOOL_DEFINITION.copy()
def get_beta_header(self) -> str:
"""
Get the beta header value for API requests.
Returns:
Beta header string
"""
return self.BETA_HEADER
def get_system_prompt(self) -> str:
"""
Get the memory system prompt to append to agent instructions.
Returns:
Memory protocol system prompt
"""
return MEMORY_SYSTEM_PROMPT
def process_tool_call(self, tool_input: Dict[str, Any]) -> str:
"""
Process a memory tool call from Claude.
Args:
tool_input: The input dict from Claude's tool_use block
Contains 'command' and command-specific parameters
Returns:
Result string to return to Claude
Example:
```python
# Claude sends:
# {"command": "view", "path": "/memories"}
result = memory_tool.process_tool_call(tool_input)
```
"""
command = tool_input.get("command")
if not command:
return "Error: No command specified in tool input"
try:
# Extract command-specific parameters
params = {k: v for k, v in tool_input.items() if k != "command"}
return self.execute(command, **params)
except Exception as e:
return f"Error: {str(e)}"
def __repr__(self) -> str:
return f"ClaudeMemoryTool(base_path='{self.base_path}', user_id='{self.user_id}')"
@@ -1,297 +0,0 @@
"""Tools for working with CSV files.
Usage:
from praisonaiagents.tools import csv_tools
df = csv_tools.read_csv("data.csv")
or
from praisonaiagents.tools import read_csv, write_csv, merge_csv
df = read_csv("data.csv")
"""
import logging
from typing import List, Dict, Union, Optional, Any, TYPE_CHECKING
from importlib import util
import json
import csv
from pathlib import Path
if TYPE_CHECKING:
import pandas as pd
class CSVTools:
"""Tools for working with CSV files."""
def __init__(self):
"""Initialize CSVTools."""
pass
def _get_pandas(self) -> Optional['pd']:
"""Get pandas module, installing if needed"""
if util.find_spec('pandas') is None:
error_msg = "pandas package is not available. Please install it using: pip install pandas"
logging.error(error_msg)
return None
import pandas as pd
return pd
def read_csv(
self,
filepath: str,
encoding: str = 'utf-8',
delimiter: str = ',',
header: Union[int, List[int], None] = 0,
usecols: Optional[List[str]] = None,
dtype: Optional[Dict[str, str]] = None,
parse_dates: Optional[List[str]] = None,
na_values: Optional[List[str]] = None,
nrows: Optional[int] = None
) -> List[Dict[str, Any]]:
"""Read a CSV file with advanced options.
Args:
filepath: Path to CSV file
encoding: File encoding
delimiter: Column delimiter
header: Row number(s) to use as column names
usecols: Columns to read
dtype: Dict of column dtypes
parse_dates: List of columns to parse as dates
na_values: Additional strings to recognize as NA/NaN
nrows: Number of rows to read
Returns:
List of row dicts
"""
try:
pd = self._get_pandas()
if pd is None:
return {"error": "pandas package not available"}
df = pd.read_csv(
filepath,
encoding=encoding,
delimiter=delimiter,
header=header,
usecols=usecols,
dtype=dtype,
parse_dates=parse_dates,
na_values=na_values,
nrows=nrows
)
return df.to_dict('records')
except Exception as e:
error_msg = f"Error reading CSV file {filepath}: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def write_csv(
self,
filepath: str,
data: Union[List[Dict[str, Any]], str],
encoding: str = 'utf-8',
delimiter: str = ',',
index: bool = False,
header: bool = True,
float_format: Optional[str] = None,
date_format: Optional[str] = None,
mode: str = 'w'
) -> bool:
"""Write data to a CSV file.
Args:
filepath: Path to CSV file
data: Either a list of dictionaries or a string containing CSV data
If string, each line should be comma-separated values
encoding: File encoding (default: 'utf-8')
delimiter: Column delimiter (default: ',')
index: Whether to write row indices (default: False)
header: Whether to write column headers (default: True)
float_format: Format string for float values (default: None)
date_format: Format string for date values (default: None)
mode: Write mode - 'w' for write, 'a' for append (default: 'w')
Returns:
bool: True if successful, False otherwise
"""
try:
pd = self._get_pandas()
if pd is None:
return False
# Handle string input
if isinstance(data, str):
# Convert string to list of dicts
rows = []
if delimiter in data:
# Get existing columns if file exists and in append mode
existing_cols = []
if mode == 'a' and Path(filepath).exists():
try:
existing_df = pd.read_csv(filepath, nrows=1)
existing_cols = existing_df.columns.tolist()
except:
pass
values = [v.strip() for v in data.split(delimiter)]
if existing_cols:
# Use existing column names
row_dict = dict(zip(existing_cols, values))
else:
# Create generic column names
row_dict = {f'col{i}': val for i, val in enumerate(values)}
rows.append(row_dict)
data = rows
df = pd.DataFrame(data)
# Handle append mode properly
write_header = header if mode == 'w' else (header and not Path(filepath).exists())
df.to_csv(
filepath,
encoding=encoding,
sep=delimiter,
index=index,
header=write_header,
float_format=float_format,
date_format=date_format,
mode=mode
)
return True
except Exception as e:
error_msg = f"Error writing CSV file {filepath}: {str(e)}"
logging.error(error_msg)
return False
def merge_csv(
self,
files: List[str],
output_file: str,
how: str = 'inner',
on: Optional[Union[str, List[str]]] = None,
suffixes: Optional[tuple] = None
) -> bool:
"""Merge multiple CSV files.
Args:
files: List of CSV files to merge
output_file: Output file path
how: Merge method ('inner', 'outer', 'left', 'right')
on: Column(s) to merge on
suffixes: Suffixes for overlapping columns
Returns:
bool: Success status
"""
try:
if len(files) < 2:
error_msg = "At least two files are required for merging"
logging.error(error_msg)
return False
pd = self._get_pandas()
if pd is None:
return False
# Read first file
result = pd.read_csv(files[0])
# Merge with remaining files
for file in files[1:]:
df = pd.read_csv(file)
result = pd.merge(
result,
df,
how=how,
on=on,
suffixes=suffixes or ('_1', '_2')
)
# Write merged result
result.to_csv(output_file, index=False)
return True
except Exception as e:
error_msg = f"Error merging CSV files: {str(e)}"
logging.error(error_msg)
return False
# Create instance for direct function access
_csv_tools = CSVTools()
read_csv = _csv_tools.read_csv
write_csv = _csv_tools.write_csv
merge_csv = _csv_tools.merge_csv
if __name__ == "__main__":
print("\n==================================================")
print("CSVTools Demonstration")
print("==================================================\n")
# Create a temporary file for testing
import tempfile
import os
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as temp:
temp_file = temp.name
print("1. Writing CSV File")
print("------------------------------")
data = [
{"name": "Alice", "age": 25, "city": "New York"},
{"name": "Bob", "age": 30, "city": "San Francisco"},
{"name": "Charlie", "age": 35, "city": "Chicago"}
]
result = write_csv(temp_file, data)
print(f"Data written successfully: {result}")
print()
print("2. Reading CSV File")
print("------------------------------")
read_data = read_csv(temp_file)
print("Contents of CSV file:")
for row in read_data:
print(row)
print()
print("3. Merging CSV Files")
print("------------------------------")
# Create a second file for merging
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as temp2:
temp_file2 = temp2.name
data2 = [
{"name": "Alice", "salary": 75000},
{"name": "Bob", "salary": 85000},
{"name": "Charlie", "salary": 95000}
]
write_csv(temp_file2, data2)
# Merge files
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as temp3:
temp_file3 = temp3.name
result = merge_csv(
[temp_file, temp_file2],
temp_file3,
how='inner',
on='name'
)
print(f"Files merged successfully: {result}")
if result:
merged_data = read_csv(temp_file3)
print("\nMerged contents:")
for row in merged_data:
print(row)
print()
# Clean up temporary files
os.unlink(temp_file)
os.unlink(temp_file2)
os.unlink(temp_file3)
print("==================================================")
print("Demonstration Complete")
print("==================================================\n")
@@ -1,299 +0,0 @@
"""Tools for working with DuckDB databases.
Usage:
from praisonaiagents.tools import duckdb_tools
df = duckdb_tools.query("SELECT * FROM my_table")
or
from praisonaiagents.tools import query_db, create_table, load_data
df = query_db("SELECT * FROM my_table")
"""
import logging
from typing import List, Dict, Any, Optional, Union, TYPE_CHECKING
from importlib import util
import json
import re
if TYPE_CHECKING:
import duckdb
import pandas as pd
class DuckDBTools:
"""Tools for working with DuckDB databases."""
def __init__(self, database: str = ':memory:'):
"""Initialize DuckDBTools.
Args:
database: Path to database file or ':memory:' for in-memory database
"""
self.database = database
self._conn = None
def _validate_identifier(self, identifier: str) -> str:
"""Validate and quote a SQL identifier to prevent injection.
Args:
identifier: Table or column name to validate
Returns:
Quoted identifier safe for SQL
Raises:
ValueError: If identifier contains invalid characters
"""
# Only allow alphanumeric characters, underscores, and dots
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$', identifier):
raise ValueError(f"Invalid identifier: {identifier}")
# Quote the identifier to handle reserved words
return f'"{identifier}"'
def _get_duckdb(self) -> Optional['duckdb']:
"""Get duckdb module, installing if needed"""
if util.find_spec('duckdb') is None:
error_msg = "duckdb package is not available. Please install it using: pip install duckdb"
logging.error(error_msg)
return None
import duckdb
return duckdb
def _get_pandas(self) -> Optional['pd']:
"""Get pandas module, installing if needed"""
if util.find_spec('pandas') is None:
error_msg = "pandas package is not available. Please install it using: pip install pandas"
logging.error(error_msg)
return None
import pandas as pd
return pd
def _get_connection(self) -> Optional['duckdb.DuckDBPyConnection']:
"""Get or create database connection"""
if self._conn is None:
duckdb = self._get_duckdb()
if duckdb is None:
return None
try:
self._conn = duckdb.connect(self.database)
except Exception as e:
error_msg = f"Error connecting to database {self.database}: {str(e)}"
logging.error(error_msg)
return None
return self._conn
def execute_query(
self,
query: str,
params: Optional[Union[tuple, dict]] = None,
return_df: bool = True
) -> Union[List[Dict[str, Any]], Dict[str, str]]:
"""Execute a SQL query.
Args:
query: SQL query to execute
params: Query parameters
return_df: If True, return results as DataFrame records
Returns:
Query results as list of dicts, or error dict
"""
try:
conn = self._get_connection()
if conn is None:
return {"error": "Could not connect to database"}
if params:
result = conn.execute(query, params)
else:
result = conn.execute(query)
if return_df:
pd = self._get_pandas()
if pd is None:
return {"error": "pandas package not available"}
df = result.df()
return df.to_dict('records')
else:
return [dict(row) for row in result.fetchall()]
except Exception as e:
error_msg = f"Error executing query: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def load_csv(
self,
table_name: str,
filepath: str,
schema: Optional[Dict[str, str]] = None,
if_exists: str = 'replace'
) -> bool:
"""Load a CSV file into a table.
Args:
table_name: Name of table to create
filepath: Path to CSV file
schema: Optional column definitions
if_exists: What to do if table exists ('fail', 'replace', 'append')
Returns:
bool: Success status
"""
try:
conn = self._get_connection()
if conn is None:
return False
# Check if table exists using parameterized query
exists = conn.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name=?
""", [table_name]).fetchone() is not None
if exists:
if if_exists == 'fail':
raise ValueError(f"Table {table_name} already exists")
elif if_exists == 'replace':
# Validate and quote table name to prevent injection
safe_table = self._validate_identifier(table_name)
conn.execute(f"DROP TABLE IF EXISTS {safe_table}")
elif if_exists != 'append':
raise ValueError("if_exists must be 'fail', 'replace', or 'append'")
# Create table if needed
if not exists or if_exists == 'replace':
safe_table = self._validate_identifier(table_name)
if schema:
# Create table with schema - validate column names
column_defs = []
for col_name, col_type in schema.items():
safe_col = self._validate_identifier(col_name)
# Validate column type to prevent injection
if not re.match(r'^[A-Z][A-Z0-9_]*(\([0-9,]+\))?$', col_type.upper()):
raise ValueError(f"Invalid column type: {col_type}")
column_defs.append(f"{safe_col} {col_type}")
columns = ', '.join(column_defs)
conn.execute(f"CREATE TABLE {safe_table} ({columns})")
else:
# Infer schema from CSV - use parameterized query for filepath
conn.execute(f"""
CREATE TABLE {safe_table} AS
SELECT * FROM read_csv_auto(?)
WHERE 1=0
""", [filepath])
# Load data - use validated table name and parameterized filepath
safe_table = self._validate_identifier(table_name)
conn.execute(f"""
INSERT INTO {safe_table}
SELECT * FROM read_csv_auto(?)
""", [filepath])
return True
except Exception as e:
error_msg = f"Error loading CSV file {filepath}: {str(e)}"
logging.error(error_msg)
return False
def export_csv(
self,
query: str,
filepath: str,
params: Optional[Union[tuple, dict]] = None
) -> bool:
"""Export query results to CSV.
Args:
query: SQL query to execute
filepath: Output file path
params: Optional query parameters
Returns:
bool: Success status
"""
try:
# Execute query and get results as DataFrame
results = self.execute_query(query, params)
if isinstance(results, dict) and 'error' in results:
return False
pd = self._get_pandas()
if pd is None:
return False
# Convert to DataFrame and save
df = pd.DataFrame(results)
df.to_csv(filepath, index=False)
return True
except Exception as e:
error_msg = f"Error exporting to CSV file {filepath}: {str(e)}"
logging.error(error_msg)
return False
def close(self):
"""Close database connection."""
if self._conn:
self._conn.close()
self._conn = None
# Create instance for direct function access
_duckdb_tools = DuckDBTools()
execute_query = _duckdb_tools.execute_query
load_csv = _duckdb_tools.load_csv
export_csv = _duckdb_tools.export_csv
if __name__ == "__main__":
print("\n==================================================")
print("DuckDBTools Demonstration")
print("==================================================\n")
# Create a temporary file for testing
import tempfile
import os
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as temp:
temp_file = temp.name
# Create sample data
with open(temp_file, 'w') as f:
f.write("name,age,city\n")
f.write("Alice,25,New York\n")
f.write("Bob,30,San Francisco\n")
f.write("Charlie,35,Chicago\n")
print("1. Loading CSV File")
print("------------------------------")
result = load_csv('users', temp_file)
print(f"CSV loaded successfully: {result}")
print()
print("2. Executing Query")
print("------------------------------")
query = "SELECT * FROM users WHERE age > 25"
results = execute_query(query)
print("Query results:")
for row in results:
print(row)
print()
print("3. Exporting Query Results")
print("------------------------------")
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as temp2:
temp_file2 = temp2.name
result = export_csv(query, temp_file2)
print(f"Results exported successfully: {result}")
if result:
print("\nExported file contents:")
with open(temp_file2) as f:
print(f.read())
# Clean up temporary files
os.unlink(temp_file)
os.unlink(temp_file2)
print("==================================================")
print("Demonstration Complete")
print("==================================================\n")
@@ -1,244 +0,0 @@
"""Example plugin tools demonstrating the BaseTool and @tool patterns.
This module shows how to create tools using the new plugin system.
External developers can use these patterns to create pip-installable plugins.
Usage:
from praisonaiagents import BaseTool, tool, Agent
# Method 1: Class-based tool
class MyTool(BaseTool):
name = "my_tool"
description = "Does something useful"
def run(self, param: str) -> str:
return f"Result: {param}"
# Method 2: Decorator-based tool
@tool
def my_function(param: str) -> str:
'''Does something useful.'''
return f"Result: {param}"
# Use with Agent
agent = Agent(tools=[MyTool(), my_function])
"""
from typing import Dict, Any, List
from .base import BaseTool
from .decorator import tool
# =============================================================================
# Example 1: Simple Class-Based Tool
# =============================================================================
class EchoTool(BaseTool):
"""A simple tool that echoes input back.
This demonstrates the minimal BaseTool implementation.
"""
name = "echo"
description = "Echo the input message back"
def run(self, message: str) -> str:
"""Echo the message.
Args:
message: The message to echo
Returns:
The same message
"""
return message
# =============================================================================
# Example 2: Tool with Multiple Parameters
# =============================================================================
class MathTool(BaseTool):
"""A tool for basic math operations.
Demonstrates:
- Multiple parameters with types
- Default parameter values
- Returning structured data
"""
name = "basic_math"
description = "Perform basic math operations (add, subtract, multiply, divide)"
def run(
self,
a: float,
b: float,
operation: str = "add"
) -> Dict[str, Any]:
"""Perform a math operation.
Args:
a: First number
b: Second number
operation: One of 'add', 'subtract', 'multiply', 'divide'
Returns:
Dict with result and operation details
"""
operations = {
"add": lambda x, y: x + y,
"subtract": lambda x, y: x - y,
"multiply": lambda x, y: x * y,
"divide": lambda x, y: x / y if y != 0 else float('inf')
}
if operation not in operations:
return {"error": f"Unknown operation: {operation}"}
result = operations[operation](a, b)
return {
"result": result,
"operation": operation,
"expression": f"{a} {operation} {b} = {result}"
}
# =============================================================================
# Example 3: Tool with State (Instance Variables)
# =============================================================================
class CounterTool(BaseTool):
"""A tool that maintains state between calls.
Demonstrates:
- Instance variables for state
- Custom __init__ with super() call
"""
name = "counter"
description = "A counter that increments each time it's called"
def __init__(self, start: int = 0):
self._count = start
super().__init__() # Important: call parent init
def run(self, increment: int = 1) -> Dict[str, int]:
"""Increment the counter.
Args:
increment: Amount to increment by (default: 1)
Returns:
Dict with previous and current count
"""
previous = self._count
self._count += increment
return {
"previous": previous,
"current": self._count,
"increment": increment
}
# =============================================================================
# Example 4: Decorator-Based Tools
# =============================================================================
@tool
def greet(name: str, greeting: str = "Hello") -> str:
"""Generate a greeting message.
Args:
name: Name of the person to greet
greeting: The greeting word (default: "Hello")
Returns:
A greeting string
"""
return f"{greeting}, {name}!"
@tool(name="reverse_text", description="Reverse a string of text")
def reverse_string(text: str) -> str:
"""Reverse the input text."""
return text[::-1]
@tool
def list_numbers(start: int, end: int, step: int = 1) -> List[int]:
"""Generate a list of numbers.
Args:
start: Starting number (inclusive)
end: Ending number (exclusive)
step: Step between numbers (default: 1)
Returns:
List of integers
"""
return list(range(start, end, step))
# =============================================================================
# Example 5: Tool with Error Handling
# =============================================================================
class SafeDivideTool(BaseTool):
"""A division tool with proper error handling.
Demonstrates:
- Error handling in run()
- Returning error information
"""
name = "safe_divide"
description = "Safely divide two numbers with error handling"
def run(self, numerator: float, denominator: float) -> Dict[str, Any]:
"""Divide numerator by denominator.
Args:
numerator: The number to divide
denominator: The number to divide by
Returns:
Dict with result or error message
"""
if denominator == 0:
return {
"success": False,
"error": "Division by zero is not allowed"
}
result = numerator / denominator
return {
"success": True,
"result": result
}
# =============================================================================
# Convenience exports
# =============================================================================
# These can be imported directly:
# from praisonaiagents.tools.example_plugin import echo_tool, math_tool, greet
echo_tool = EchoTool()
math_tool = MathTool()
counter_tool = CounterTool()
safe_divide_tool = SafeDivideTool()
__all__ = [
# Classes
'EchoTool',
'MathTool',
'CounterTool',
'SafeDivideTool',
# Instances
'echo_tool',
'math_tool',
'counter_tool',
'safe_divide_tool',
# Decorated functions
'greet',
'reverse_string',
'list_numbers',
]
@@ -1,310 +0,0 @@
"""Tools for working with Excel files.
Usage:
from praisonaiagents.tools import excel_tools
df = excel_tools.read_excel("data.xlsx")
or
from praisonaiagents.tools import read_excel, write_excel, merge_excel
df = read_excel("data.xlsx")
"""
import logging
from typing import List, Dict, Union, Optional, Any, TYPE_CHECKING, Tuple
from importlib import util
import json
from pathlib import Path
import tempfile
import os
if TYPE_CHECKING:
import pandas as pd
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.chart import BarChart, LineChart, PieChart, Reference
from openpyxl.chart.label import DataLabelList
class ExcelTools:
"""Tools for working with Excel files."""
def __init__(self):
"""Initialize ExcelTools."""
pass
def _get_pandas(self) -> Optional['pd']:
"""Get pandas module, installing if needed"""
if util.find_spec('pandas') is None:
error_msg = "pandas package is not available. Please install it using: pip install pandas"
logging.error(error_msg)
return None
if util.find_spec('openpyxl') is None:
error_msg = "openpyxl package is not available. Please install it using: pip install openpyxl"
logging.error(error_msg)
return None
import pandas as pd
return pd
def read_excel(
self,
filepath: str,
sheet_name: Optional[Union[str, int, List[Union[str, int]]]] = 0,
header: Optional[int] = 0,
usecols: Optional[List[str]] = None,
skiprows: Optional[Union[int, List[int]]] = None,
na_values: Optional[List[str]] = None,
dtype: Optional[Dict[str, str]] = None
) -> Union[Dict[str, List[Dict[str, Any]]], List[Dict[str, Any]]]:
"""Read an Excel file with advanced options.
Args:
filepath: Path to Excel file
sheet_name: Sheet name(s) or index(es)
header: Row number(s) to use as column names
usecols: Columns to read
skiprows: Line numbers to skip
na_values: Additional strings to recognize as NA/NaN
dtype: Dict of column dtypes
Returns:
Dict of sheet names to data if multiple sheets, else list of row dicts
"""
try:
pd = self._get_pandas()
if pd is None:
return {"error": "Required packages not available"}
# Read Excel file
df = pd.read_excel(
filepath,
sheet_name=sheet_name,
header=header,
usecols=usecols,
skiprows=skiprows,
na_values=na_values,
dtype=dtype,
engine='openpyxl'
)
# Convert to dict format
if isinstance(df, dict):
return {
name: df[name].to_dict('records')
for name in df.keys()
}
else:
return df.to_dict('records')
except Exception as e:
error_msg = f"Error reading Excel file {filepath}: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def write_excel(
self,
filepath: str,
data: Union[Dict[str, List[Dict[str, Any]]], List[Dict[str, Any]]],
sheet_name: Optional[str] = None,
index: bool = False,
header: bool = True,
mode: str = 'w'
) -> bool:
"""Write data to an Excel file.
Args:
filepath: Path to Excel file
data: Data to write (dict of sheet names to data or list of row dicts)
sheet_name: Sheet name if data is a list
index: Whether to write row indices
header: Whether to write column headers
mode: Write mode ('w' for write, 'a' for append)
Returns:
bool: Success status
"""
try:
pd = self._get_pandas()
if pd is None:
return False
# Convert data to DataFrame(s)
if isinstance(data, dict):
if mode == 'a' and os.path.exists(filepath):
book = pd.ExcelFile(filepath)
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
# Copy existing sheets
for sheet in book.sheet_names:
pd.read_excel(filepath, sheet_name=sheet).to_excel(
writer, sheet_name=sheet, index=index, header=header
)
# Add new sheets
for name, sheet_data in data.items():
df = pd.DataFrame(sheet_data)
df.to_excel(
writer, sheet_name=name, index=index, header=header
)
else:
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
for name, sheet_data in data.items():
df = pd.DataFrame(sheet_data)
df.to_excel(
writer, sheet_name=name, index=index, header=header
)
else:
if mode == 'a' and os.path.exists(filepath):
book = pd.ExcelFile(filepath)
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
# Copy existing sheets
for sheet in book.sheet_names:
pd.read_excel(filepath, sheet_name=sheet).to_excel(
writer, sheet_name=sheet, index=index, header=header
)
# Add new sheet
df = pd.DataFrame(data)
df.to_excel(
writer,
sheet_name=sheet_name or 'Sheet1',
index=index,
header=header
)
else:
df = pd.DataFrame(data)
df.to_excel(
filepath,
sheet_name=sheet_name or 'Sheet1',
index=index,
header=header,
engine='openpyxl'
)
return True
except Exception as e:
error_msg = f"Error writing Excel file {filepath}: {str(e)}"
logging.error(error_msg)
return False
def merge_excel(
self,
files: List[str],
output_file: str,
how: str = 'inner',
on: Optional[Union[str, List[str]]] = None,
suffixes: Optional[Tuple[str, str]] = None
) -> bool:
"""Merge multiple Excel files.
Args:
files: List of Excel files to merge
output_file: Output file path
how: Merge method ('inner', 'outer', 'left', 'right')
on: Column(s) to merge on
suffixes: Suffixes for overlapping columns
Returns:
bool: Success status
"""
try:
if len(files) < 2:
error_msg = "At least two files are required for merging"
logging.error(error_msg)
return False
# Read first file
result = self.read_excel(files[0])
# Merge with remaining files
for file in files[1:]:
df = self.read_excel(file)
if isinstance(df, dict):
df = next(iter(df.values()))
pd = self._get_pandas()
if pd is None:
return False
result = pd.merge(
pd.DataFrame(result),
pd.DataFrame(df),
how=how,
on=on,
suffixes=suffixes or ('_1', '_2')
).to_dict('records')
# Write merged result
return self.write_excel(output_file, result)
except Exception as e:
error_msg = f"Error merging Excel files: {str(e)}"
logging.error(error_msg)
return False
# Create instance for direct function access
_excel_tools = ExcelTools()
read_excel = _excel_tools.read_excel
write_excel = _excel_tools.write_excel
merge_excel = _excel_tools.merge_excel
if __name__ == "__main__":
print("\n==================================================")
print("ExcelTools Demonstration")
print("==================================================\n")
# Create a temporary file for testing
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp:
temp_file = temp.name
print("1. Writing Excel File")
print("------------------------------")
data = [
{"name": "Alice", "age": 25, "city": "New York"},
{"name": "Bob", "age": 30, "city": "San Francisco"},
{"name": "Charlie", "age": 35, "city": "Chicago"}
]
result = write_excel(temp_file, data, "People")
print(f"Data written successfully: {result}")
print()
print("2. Reading Excel File")
print("------------------------------")
read_data = read_excel(temp_file)
print("Contents of Excel file:")
for row in read_data:
print(row)
print()
print("3. Merging Excel Files")
print("------------------------------")
# Create a second file for merging
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp2:
temp_file2 = temp2.name
data2 = [
{"name": "Alice", "salary": 75000},
{"name": "Bob", "salary": 85000},
{"name": "Charlie", "salary": 95000}
]
write_excel(temp_file2, data2, "Salaries")
# Merge files
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp3:
temp_file3 = temp3.name
result = merge_excel(
[temp_file, temp_file2],
temp_file3,
how='inner',
on='name'
)
print(f"Files merged successfully: {result}")
if result:
merged_data = read_excel(temp_file3)
print("\nMerged contents:")
for row in merged_data:
print(row)
print()
# Clean up temporary files
os.unlink(temp_file)
os.unlink(temp_file2)
os.unlink(temp_file3)
print("==================================================")
print("Demonstration Complete")
print("==================================================\n")
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff. Show more