mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df3e32640 | ||
|
|
fb36e1c50c | ||
|
|
ac5454cb00 | ||
|
|
bbe95795d1 | ||
|
|
bdaf5340f9 | ||
|
|
5579614001 | ||
|
|
2c4dd0e2d4 | ||
|
|
fd23d62841 | ||
|
|
5f3bcb78ac | ||
|
|
3c40c2278e | ||
|
|
1921b03aab | ||
|
|
f33bf57662 | ||
|
|
df21c419ea | ||
|
|
fe997d525c | ||
|
|
ca26988774 | ||
|
|
3600594316 | ||
|
|
097cd42890 | ||
|
|
595add2991 | ||
|
|
03a3aa420b | ||
|
|
9533abd8a4 | ||
|
|
39c9e50875 |
No files matched your search
@@ -0,0 +1,57 @@
|
||||
name: Update API Reference
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/praisonai-agents/praisonaiagents/**/*.py'
|
||||
- 'src/praisonai/praisonai/**/*.py'
|
||||
- 'src/praisonai-ts/src/**/*.ts'
|
||||
- '.github/workflows/update-api-md.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-api-md:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Generate api.md
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 src/praisonai/scripts/generate_api_md.py
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --quiet api.md; then
|
||||
echo "changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes to api.md"
|
||||
else
|
||||
echo "changes=true" >> $GITHUB_OUTPUT
|
||||
echo "api.md has been updated"
|
||||
git diff api.md | head -50
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check_changes.outputs.changes == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "docs: auto-update api.md [skip ci]"
|
||||
file_pattern: 'api.md'
|
||||
commit_user_name: "github-actions[bot]"
|
||||
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
+2
-1
@@ -83,4 +83,5 @@ issue_details.json
|
||||
# MCP Registry tokens
|
||||
.mcpregistry_*
|
||||
|
||||
*.log
|
||||
*.log
|
||||
.internal
|
||||
@@ -0,0 +1,881 @@
|
||||
# PraisonAI API Reference
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">achat</a>(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)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aexecute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">arun</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">astart</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clear_history</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_generating</a>(content: str, start_time: float)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool</a>(function_name, arguments)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool_async</a>(function_name: str, arguments: Dict[str, Any]) -> Any</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">generate_task</a>() -> 'Task'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_available_tools</a>() -> List[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_memory_context</a>(query: Optional[str] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_rules_context</a>(file_path: Optional[str] = None, include_manual: Optional[List[str]] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">iter_stream</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">launch</a>(path: str = '/', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
|
||||
* <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">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>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<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 Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<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 Agents">Agents.<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 Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <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">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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
|
||||
```
|
||||
|
||||
# CLI
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark agent <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark benchmark-callback <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark cli <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark compare <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark litellm <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
|
||||
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
|
||||
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context export <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context grep <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context list <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug interactive <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug lsp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug trace <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy aws <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy azure <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy docker <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy gcp <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag diag-callback <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag export <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs api-md <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs generate <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs list <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs report <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs run <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <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 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>
|
||||
* <code title="cli">praisonai doctor env <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <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 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>
|
||||
* <code title="cli">praisonai environment doctor <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment view <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval accuracy <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval performance <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples info <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples list <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples report <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge add <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp lsp-callback <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp tools <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory add <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory clear <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile query <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile snapshot <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile startup <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile suite <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag chat <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag eval <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag index <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag query <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag serve <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai realtime realtime-main <a href="./src/praisonai/praisonai/cli/commands/realtime.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe install <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe list <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe run <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry list <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry serve <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai research research-main <a href="./src/praisonai/praisonai/cli/commands/research.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval index <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval query <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval search <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules add <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules clear <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule logs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule restart <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule schedule-callback <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stop <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve serve-callback <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve start <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve status <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve stop <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai session delete <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session export <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session import <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates create <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates list <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai test info <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test interactive <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test run <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo add <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo done <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo list <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools info <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools list <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools test <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces disable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces enable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces list <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces status <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai train train-main <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui chat <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui code <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui gradio <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui ui-main <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow create <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow list <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow run <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
|
||||
# TypeScript
|
||||
|
||||
Types/Exports:
|
||||
```ts
|
||||
export { Agent, Agents, PraisonAIAgents, Router } from "./agent";
|
||||
export type { PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig } from "./agent";
|
||||
export { ContextAgent, createContextAgent } from "./agent/context";
|
||||
export { Handoff, handoff, handoffFilters } from "./agent/handoff";
|
||||
export { ImageAgent, createImageAgent } from "./agent/image";
|
||||
export { PromptExpanderAgent, createPromptExpanderAgent } from "./agent/prompt-expander";
|
||||
export { QueryRewriterAgent, createQueryRewriterAgent } from "./agent/query-rewriter";
|
||||
export { DeepResearchAgent, createDeepResearchAgent } from "./agent/research";
|
||||
export { RouterAgent, createRouter, routeConditions } from "./agent/router";
|
||||
export { // Agent loop
|
||||
createAgentLoop, // DevTools
|
||||
enableDevTools, // MCP
|
||||
createMCP, // Middleware (renamed to avoid conflicts)
|
||||
createCachingMiddleware, // Models
|
||||
createModel, // Multimodal
|
||||
createImagePart, // Next.js
|
||||
createRouteHandler, // OAuth for MCP
|
||||
type OAuthClientProvider, // Server adapters
|
||||
createHttpHandler, // Speech & Transcription
|
||||
generateSpeech, // Telemetry (AI SDK v6 parity)
|
||||
configureTelemetry, // Tool Approval (AI SDK v6 parity)
|
||||
ApprovalManager, // Tools
|
||||
defineTool, // UI Message (AI SDK v6 parity)
|
||||
convertToModelMessages, AIAgentStep, AIEmbedManyResult, AIEmbedOptions, AIEmbedResult, AIFilePart, AIGenerateImageOptions, AIGenerateImageResult, AIGenerateObjectOptions, AIGenerateObjectResult, AIGenerateTextOptions, AIGenerateTextResult, AIImagePart, AIMiddleware, AIMiddlewareConfig, AIModelMessage, AISpan, AISpanKind, AISpanOptions, AISpanStatus, AIStreamObjectOptions, AIStreamObjectResult, AIStreamTextOptions, AIStreamTextResult, AITelemetryEvent, AITelemetrySettings, AITextPart, AIToolDefinition, AITracer, AgentLoop, DANGEROUS_PATTERNS, MODEL_ALIASES, SPEECH_MODELS, TRANSCRIPTION_MODELS, ToolApprovalDeniedError, ToolApprovalTimeoutError, aiEmbed, aiEmbedMany, aiGenerateImage, aiGenerateObject, aiGenerateText, aiStreamObject, aiStreamText, applyMiddleware, autoEnableDevTools, base64ToUint8Array, clearAICache, clearEvents, closeAllMCPClients, closeMCPClient, convertToUIMessages, createAILoggingMiddleware, createAISpan, createApprovalResponse, createDangerousPatternChecker, createDevToolsMiddleware, createExpressHandler, createFastifyHandler, createFilePart, createHonoHandler, createMultimodalMessage, createNestHandler, createPagesHandler, createPdfPart, createSystemMessage, createTelemetryMiddleware, createTelemetrySettings, createTextMessage, createTextPart, createToolSet, disableAITelemetry, disableDevTools, enableAITelemetry, functionToTool, getAICacheStats, getApprovalManager, getDevToolsState, getDevToolsUrl, getEvents, getMCPClient, getModel, getTelemetrySettings, getToolsNeedingApproval, getTracer, hasModelAlias, hasPendingApprovals, initOpenTelemetry, isDangerous, isDataUrl, isDevToolsEnabled, isTelemetryEnabled, isUrl, listModelAliases, mcpToolsToAITools, parseModel, pipeUIMessageStreamToResponse, recordEvent, resolveModelAlias, safeValidateUIMessages, setApprovalManager, stopAfterSteps, stopWhen, stopWhenNoToolCalls, toMessageContent, toUIMessageStreamResponse, transcribe, uint8ArrayToBase64, validateUIMessages, withApproval, withSpan, wrapModel } from "./ai";
|
||||
export { AutoAgents, createAutoAgents } from "./auto";
|
||||
export { BaseCache, FileCache, MemoryCache, createFileCache, createMemoryCache } from "./cache";
|
||||
export { CLI_SPEC_VERSION, executeCommand, parseArgs } from "./cli";
|
||||
export { // Autonomy Mode
|
||||
AutonomyManager, // Background Jobs
|
||||
JobQueue, // Checkpoints
|
||||
CheckpointManager, // Cost Tracker
|
||||
CostTracker, // External Agents
|
||||
BaseExternalAgent, // Fast Context
|
||||
FastContext, // Flow Display
|
||||
FlowDisplay, // Git Integration
|
||||
GitManager, // Interactive TUI
|
||||
InteractiveTUI, // N8N Integration
|
||||
N8NIntegration, // Repo Map
|
||||
RepoMap, // Sandbox Executor
|
||||
SandboxExecutor, // Scheduler
|
||||
Scheduler, // Slash Commands
|
||||
SlashCommandHandler, AiderAgent, ClaudeCodeAgent, CodexCliAgent, CommandValidator, CostTokenUsage, DEFAULT_BLOCKED_COMMANDS, DEFAULT_BLOCKED_PATHS, DEFAULT_IGNORE_PATTERNS, DiffViewer, FileCheckpointStorage, FileJobStorage, GeminiCliAgent, GenericExternalAgent, HistoryManager, MODEL_PRICING, MODE_POLICIES, MemoryCheckpointStorage, MemoryJobStorage, StatusDisplay, cliApprovalPrompt, createAutonomyManager, createCheckpointManager, createCostTracker, createDiffViewer, createExternalAgent, createFastContext, createFileCheckpointStorage, createFileJobStorage, createFlowDisplay, createGitManager, createHistoryManager, createInteractiveTUI, createJobQueue, createN8NIntegration, createRepoMap, createSandboxExecutor, createScheduler, createSlashCommandHandler, createStatusDisplay, cronExpressions, estimateTokens, executeSlashCommand, externalAgentAsTool, formatCost, getExternalAgentRegistry, getQuickContext, getRepoTree, isSlashCommand, parseSlashCommand, registerCommand, renderWorkflow, sandboxExec, triggerN8NWebhook } from "./cli/features";
|
||||
export { createDbAdapter, db, getDefaultDbAdapter, setDefaultDbAdapter } from "./db";
|
||||
export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from "./db";
|
||||
export { MemoryPostgresAdapter, NeonPostgresAdapter, PostgresSessionStorage, createMemoryPostgres, createNeonPostgres, createPostgresSessionStorage } from "./db/postgres";
|
||||
export { MemoryRedisAdapter, UpstashRedisAdapter, createMemoryRedis, createUpstashRedis } from "./db/redis";
|
||||
export { SQLiteAdapter, createSQLiteAdapter } from "./db/sqlite";
|
||||
export { EvalSuite, accuracyEval, performanceEval, reliabilityEval } from "./eval";
|
||||
export { AgentEventBus, AgentEvents, EventEmitterPubSub, PubSub, createEventBus, createPubSub } from "./events";
|
||||
export { LLMGuardrail, createLLMGuardrail } from "./guardrails/llm-guardrail";
|
||||
export { // Computer Use
|
||||
createComputerUse, ComputerUseClient, createCLIApprovalPrompt, createComputerUseAgent } from "./integrations/computer-use";
|
||||
export { BaseObservabilityProvider, ConsoleObservabilityProvider, LangfuseObservabilityProvider, MemoryObservabilityProvider, ObservabilityTraceContext, createConsoleObservability, createLangfuseObservability, createMemoryObservability } from "./integrations/observability";
|
||||
export { // Natural Language Postgres
|
||||
createNLPostgres, NLPostgresClient, NLPostgresConfig, createPostgresTool } from "./integrations/postgres";
|
||||
export { // Slack
|
||||
createSlackBot, SlackBot, parseSlackMessage, verifySlackSignature } from "./integrations/slack";
|
||||
export { BaseVectorStore, ChromaVectorStore, MemoryVectorStore, PineconeVectorStore, QdrantVectorStore, VectorQueryResult, WeaviateVectorStore, createChromaStore, createMemoryVectorStore, createPineconeStore, createQdrantStore, createWeaviateStore } from "./integrations/vector";
|
||||
export { BaseVoiceProvider, ElevenLabsVoiceProvider, OpenAIVoiceProvider, createElevenLabsVoice, createOpenAIVoice } from "./integrations/voice";
|
||||
export { GraphRAG, GraphStore, createGraphRAG } from "./knowledge/graph-rag";
|
||||
export { BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker, createCohereReranker, createCrossEncoderReranker, createLLMReranker } from "./knowledge/reranker";
|
||||
export { // Provider classes
|
||||
OpenAIProvider, // Provider factory and utilities
|
||||
createProvider, // Provider registry (extensibility API)
|
||||
ProviderRegistry, // Types
|
||||
type LLMProvider, AnthropicProvider, BaseProvider, GoogleProvider, ProviderMessage, ProviderToolDefinition, createProviderRegistry, getAvailableProviders, getDefaultProvider, getDefaultRegistry, hasProvider, isProviderAvailable, listProviders, parseModelString, registerBuiltinProviders, registerProvider, unregisterProvider } from "./llm/providers";
|
||||
export { ADAPTERS, AISDK_PROVIDERS, COMMUNITY_PROVIDERS, PROVIDER_ALIASES } from "./llm/providers/ai-sdk/types";
|
||||
export { AutoMemory, AutoMemoryKnowledgeBase, AutoMemoryVectorStore, DEFAULT_POLICIES, createAutoMemory, createLLMSummarizer } from "./memory/auto-memory";
|
||||
export { FileMemory, createFileMemory } from "./memory/file-memory";
|
||||
export { Memory, createMemory } from "./memory/memory";
|
||||
export type { MemoryConfig, MemoryEntry } from "./memory/memory";
|
||||
export { // Adapters
|
||||
NoopObservabilityAdapter, // Constants
|
||||
OBSERVABILITY_TOOLS, // Global adapter management
|
||||
setObservabilityAdapter, // Types
|
||||
type SpanKind, ConsoleObservabilityAdapter, MemoryObservabilityAdapter, clearAdapterCache, createConsoleAdapter, createMemoryAdapter, createObservabilityAdapter, getObservabilityAdapter, getObservabilityToolInfo, hasObservabilityToolEnvVar, listObservabilityTools, noopAdapter, resetObservabilityAdapter, trace } from "./observability";
|
||||
export { Plan, PlanStep, PlanStorage, PlanningAgent, TaskAgent, TodoItem, TodoList, createPlan, createPlanStorage, createPlanningAgent, createTaskAgent, createTodoList } from "./planning";
|
||||
export { SkillManager, createSkillManager, parseSkillFile } from "./skills";
|
||||
export { AgentTelemetry, TelemetryCollector, cleanupTelemetry, createAgentTelemetry, disableTelemetry, enableTelemetry, getTelemetry } from "./telemetry";
|
||||
export { BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, resetToolsRegistry } from "./tools/registry";
|
||||
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
|
||||
export { registerBuiltinTools, tools } from "./tools/tools";
|
||||
export { Workflow, loop, parallel, repeat, route } from "./workflows";
|
||||
export type { StepResult, WorkflowContext, WorkflowStep } from "./workflows";
|
||||
export { createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
|
||||
```
|
||||
|
||||
# Optional Plugins
|
||||
|
||||
External tools are available via `praisonai-tools` package:
|
||||
|
||||
```bash
|
||||
pip install praisonai-tools
|
||||
```
|
||||
|
||||
See [PraisonAI-tools](https://github.com/MervinPraison/PraisonAI-tools) for available tools.
|
||||
@@ -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.6.2" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.6.2" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.6.2" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Consolidated Params Examples
|
||||
|
||||
Examples demonstrating the **Agent-Centric API** with consolidated parameters.
|
||||
|
||||
## Precedence Ladder
|
||||
|
||||
```
|
||||
Instance > Config > Array > Dict > String > Bool > Default
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Feature | Presets | Example |
|
||||
|---------|---------|---------|
|
||||
| `memory` | file, sqlite, redis, postgres, mem0, mongodb | `memory="sqlite"` |
|
||||
| `output` | minimal, normal, verbose, debug, silent | `output="verbose"` |
|
||||
| `execution` | fast, balanced, thorough, unlimited | `execution="balanced"` |
|
||||
| `planning` | reasoning, read_only, auto | `planning="reasoning"` |
|
||||
| `reflection` | minimal, standard, thorough | `reflection="standard"` |
|
||||
| `guardrails` | strict, permissive, safety | `guardrails="strict"` |
|
||||
| `web` | duckduckgo, tavily, google, bing, serper | `web="duckduckgo"` |
|
||||
| `context` | sliding_window, summarize, truncate | `context="sliding_window"` |
|
||||
| `autonomy` | suggest, auto_edit, full_auto | `autonomy="suggest"` |
|
||||
| `caching` | enabled, disabled, prompt | `caching="prompt"` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Examples (Single Feature)
|
||||
|
||||
| File | Feature | Description |
|
||||
|------|---------|-------------|
|
||||
| `basic_agent.py` | Agent | Minimal agent with memory |
|
||||
| `basic_memory.py` | memory | SQLite memory preset |
|
||||
| `basic_output.py` | output | Verbose output preset |
|
||||
| `basic_execution.py` | execution | Execution control |
|
||||
| `basic_guardrails.py` | guardrails | Callable validator |
|
||||
| `basic_reflection.py` | reflection | Self-reflection preset |
|
||||
| `basic_web.py` | web | Web search preset |
|
||||
| `basic_planning.py` | planning | Planning preset |
|
||||
| `basic_autonomy.py` | autonomy | Autonomy preset |
|
||||
| `basic_caching.py` | caching | Caching preset |
|
||||
| `basic_knowledge.py` | knowledge | RAG with sources |
|
||||
| `basic_context.py` | context | Context management |
|
||||
| `basic_hooks.py` | hooks | Lifecycle callbacks |
|
||||
|
||||
### Multi-Agent Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_agents.py` | Multi-agent with memory and planning |
|
||||
|
||||
### Workflow Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_workflow.py` | Simple workflow with output preset |
|
||||
| `basic_workflow_agentlike.py` | Workflow with agent-like params |
|
||||
| `basic_step_override.py` | Step-level param overrides |
|
||||
| `advanced_workflow_full_features.py` | All consolidated params |
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `advanced_output_execution.py` | Combined output + execution |
|
||||
|
||||
## Usage Forms
|
||||
|
||||
Each consolidated param supports multiple input forms:
|
||||
|
||||
```python
|
||||
# Bool - enable with defaults
|
||||
memory=True
|
||||
|
||||
# String preset
|
||||
memory="sqlite"
|
||||
|
||||
# Dict config
|
||||
memory={"backend": "sqlite", "user_id": "user123"}
|
||||
|
||||
# Config instance
|
||||
memory=MemoryConfig(backend="sqlite", user_id="user123")
|
||||
|
||||
# Array with overrides (for some params)
|
||||
guardrails=["strict", {"max_retries": 10}]
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Run any example
|
||||
python basic_agent.py
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Autonomy Example - Agent-Centric API
|
||||
|
||||
Demonstrates autonomy with consolidated params.
|
||||
Presets: suggest, auto_edit, full_auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable autonomy with preset
|
||||
agent = Agent(
|
||||
instructions="You are an autonomous assistant.",
|
||||
autonomy="suggest", # Presets: suggest, auto_edit, full_auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Help me organize my project files.")
|
||||
print(response)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Basic Caching Example - Agent-Centric API
|
||||
|
||||
Demonstrates caching with consolidated params.
|
||||
Presets: enabled, disabled, prompt
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable caching with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with caching enabled.",
|
||||
caching="prompt", # Presets: enabled, disabled, prompt
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# First call - will cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
|
||||
# Second call - may use cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Context Example - Agent-Centric API
|
||||
|
||||
Demonstrates context management with consolidated params.
|
||||
Presets: sliding_window, summarize, truncate
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable context management with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with context management.",
|
||||
context="sliding_window", # Presets: sliding_window, summarize, truncate
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Tell me a long story about a dragon.")
|
||||
print(response)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Basic Hooks Example - Agent-Centric API
|
||||
|
||||
Demonstrates lifecycle hooks with consolidated params.
|
||||
Valid keys: on_step, on_tool_call, middleware
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define hook callbacks
|
||||
def on_step_callback(step_info):
|
||||
print(f"Step: {step_info}")
|
||||
|
||||
def on_tool_call_callback(tool_info):
|
||||
print(f"Tool called: {tool_info}")
|
||||
|
||||
# Basic: Enable hooks with dict
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
hooks={
|
||||
"on_step": on_step_callback,
|
||||
"on_tool_call": on_tool_call_callback,
|
||||
},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What is 2 + 2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Knowledge (RAG) Example - Agent-Centric API
|
||||
|
||||
Demonstrates knowledge with consolidated params.
|
||||
Supports: bool, list of sources, dict config, KnowledgeConfig
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable knowledge with list of sources
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with document knowledge.",
|
||||
knowledge=["docs/"], # List of file/folder paths
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What does the documentation say about installation?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Planning Example - Agent-Centric API
|
||||
|
||||
Demonstrates planning with consolidated params.
|
||||
Presets: reasoning, read_only, auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable planning with preset
|
||||
agent = Agent(
|
||||
instructions="You are a strategic planner.",
|
||||
planning="reasoning", # Presets: reasoning, read_only, auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Plan a 3-day trip to Tokyo.")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Reflection Example - Agent-Centric API
|
||||
|
||||
Demonstrates reflection (self-reflect) with consolidated params.
|
||||
Presets: minimal, standard, thorough
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable reflection with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant that reflects on your answers.",
|
||||
reflection="standard", # Presets: minimal, standard, thorough
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Explain quantum computing in simple terms.")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Web Search Example - Agent-Centric API
|
||||
|
||||
Demonstrates web search with consolidated params.
|
||||
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable web search with preset
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What are the latest AI developments in 2024?")
|
||||
print(response)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Streaming Example with Profiling
|
||||
|
||||
Key Points:
|
||||
1. output="stream" preset enables streaming by default
|
||||
2. stream=True in start() is redundant when using output="stream" (but doesn't hurt)
|
||||
3. Profiler.enable() MUST be called before profiling works
|
||||
4. Use Profiler.streaming() context manager for TTFT metrics
|
||||
"""
|
||||
from praisonai.profiler import Profiler, profile, profile_imports
|
||||
|
||||
# IMPORTANT: Enable profiler BEFORE any profiling operations
|
||||
Profiler.enable()
|
||||
|
||||
# Profile imports
|
||||
with profile_imports():
|
||||
from praisonaiagents import Agent
|
||||
|
||||
@profile
|
||||
def main():
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
output="stream" # This already enables streaming
|
||||
)
|
||||
|
||||
print("\nStreaming response:")
|
||||
print("-" * 40)
|
||||
|
||||
# Use Profiler.streaming() to track TTFT (Time To First Token)
|
||||
with Profiler.streaming("llm_stream") as tracker:
|
||||
first_chunk = True
|
||||
for chunk in agent.start("Write a short paragraph about the history of computing"):
|
||||
if first_chunk:
|
||||
tracker.first_token() # Mark TTFT
|
||||
first_chunk = False
|
||||
tracker.chunk() # Count chunks
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
print("\n" + "-" * 40)
|
||||
|
||||
# Run with profiling blocks
|
||||
with Profiler.block("full_workflow"):
|
||||
with Profiler.block("setup"):
|
||||
pass
|
||||
|
||||
with Profiler.block("execution"):
|
||||
result = main()
|
||||
|
||||
with Profiler.block("cleanup"):
|
||||
pass
|
||||
|
||||
# Generate report
|
||||
Profiler.report()
|
||||
|
||||
# Show streaming-specific stats
|
||||
streaming_records = Profiler.get_streaming_records()
|
||||
if streaming_records:
|
||||
print("\nStreaming Metrics:")
|
||||
for record in streaming_records:
|
||||
print(f" {record.name}: TTFT={record.ttft_ms:.2f}ms, Total={record.total_ms:.2f}ms, Chunks={record.chunk_count}")
|
||||
|
||||
stats = Profiler.get_statistics()
|
||||
print(f"\nMedian execution time: {stats['p50']:.2f}ms")
|
||||
@@ -1,14 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("Creating agent...")
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant"
|
||||
)
|
||||
|
||||
print("Starting agent...")
|
||||
agent = Agent(instructions="You are a helpful assistant")
|
||||
result = agent.start("Why is the sky blue?")
|
||||
|
||||
print("Agent completed successfully!")
|
||||
print(f"Result: {result}")
|
||||
print(result)
|
||||
@@ -476,6 +476,62 @@ def _init_telemetry():
|
||||
_init_telemetry()
|
||||
|
||||
|
||||
def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
|
||||
"""
|
||||
Pre-import heavy dependencies to reduce first-call latency.
|
||||
|
||||
NOTE: For default OpenAI usage (llm="gpt-4o-mini"), warmup is NOT needed.
|
||||
The default path uses the native OpenAI SDK which is fast (~100ms import).
|
||||
|
||||
Warmup is only beneficial when using LiteLLM backend, which is triggered by:
|
||||
- Using "/" in model name (e.g., llm="openai/gpt-4o-mini")
|
||||
- Passing a dict config (e.g., llm={"model": "gpt-4o-mini"})
|
||||
- Using base_url parameter
|
||||
|
||||
Args:
|
||||
include_litellm: Pre-import LiteLLM (~2-3s). Only needed for multi-provider support.
|
||||
include_openai: Pre-import OpenAI SDK (~100ms). Default path, usually fast.
|
||||
|
||||
Returns:
|
||||
dict: Timing information for each component warmed up
|
||||
|
||||
Example:
|
||||
# For LiteLLM multi-provider usage:
|
||||
from praisonaiagents import warmup
|
||||
warmup(include_litellm=True) # Pre-load LiteLLM
|
||||
|
||||
agent = Agent(llm="anthropic/claude-3-sonnet") # Now faster
|
||||
|
||||
# For default OpenAI usage, no warmup needed:
|
||||
agent = Agent(llm="gpt-4o-mini") # Already fast!
|
||||
"""
|
||||
import time
|
||||
timings = {}
|
||||
|
||||
if include_openai:
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import openai
|
||||
timings['openai'] = (time.perf_counter() - start) * 1000
|
||||
except ImportError:
|
||||
timings['openai'] = -1 # Not available
|
||||
|
||||
if include_litellm:
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import litellm
|
||||
# Also configure litellm to avoid first-call overhead
|
||||
litellm.telemetry = False
|
||||
litellm.set_verbose = False
|
||||
litellm.drop_params = True
|
||||
litellm.modify_params = True
|
||||
timings['litellm'] = (time.perf_counter() - start) * 1000
|
||||
except ImportError:
|
||||
timings['litellm'] = -1 # Not available
|
||||
|
||||
return timings
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Core classes (lazy loaded)
|
||||
'Agent',
|
||||
@@ -646,4 +702,19 @@ __all__ = [
|
||||
# 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',
|
||||
]
|
||||
@@ -44,7 +44,7 @@ _shared_apps = {} # Dict of port -> FastAPI app
|
||||
if TYPE_CHECKING:
|
||||
from ..task.task import Task
|
||||
from ..main import TaskOutput
|
||||
from ..handoff import Handoff
|
||||
from .handoff import Handoff, HandoffConfig, HandoffResult
|
||||
from ..rag.models import RAGResult, ContextPack
|
||||
|
||||
class Agent:
|
||||
@@ -254,10 +254,11 @@ class Agent:
|
||||
instructions: Optional[str] = None,
|
||||
# LLM configuration
|
||||
llm: Optional[Union[str, Any]] = None,
|
||||
function_calling_llm: Optional[Any] = None,
|
||||
llm_config: Optional[Dict[str, Any]] = None,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
model: Optional[Union[str, Any]] = None, # Alias for llm=
|
||||
function_calling_llm: Optional[Any] = None, # DEPRECATED: use llm= with LLMConfig
|
||||
llm_config: Optional[Dict[str, Any]] = None, # DEPRECATED: use llm= with LLMConfig
|
||||
base_url: Optional[str] = None, # Kept separate (connection/auth)
|
||||
api_key: Optional[str] = None, # Kept separate (connection/auth)
|
||||
# Tools
|
||||
tools: Optional[List[Any]] = None,
|
||||
allow_delegation: bool = False,
|
||||
@@ -290,96 +291,87 @@ class Agent:
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
Args:
|
||||
name (Optional[str], optional): Name of the agent used for identification and logging.
|
||||
If None, defaults to "Agent". Defaults to None.
|
||||
role (Optional[str], optional): Role or job title that defines the agent's expertise
|
||||
and behavior patterns. Examples: "Data Analyst", "Content Writer". Defaults to None.
|
||||
goal (Optional[str], optional): Primary objective or goal the agent aims to achieve.
|
||||
Defines the agent's purpose and success criteria. Defaults to None.
|
||||
backstory (Optional[str], optional): Background story or context that shapes the agent's
|
||||
personality and decision-making approach. Defaults to None.
|
||||
instructions (Optional[str], optional): Direct instructions that override role, goal,
|
||||
and backstory when provided. Used for simple, task-specific agents. Defaults to None.
|
||||
llm (Optional[Union[str, Any]], optional): Language model configuration. Can be a model
|
||||
name string (e.g., "gpt-5-nano", "anthropic/claude-3-sonnet") or a configured LLM object.
|
||||
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-5-nano".
|
||||
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
|
||||
available to the agent for task execution. Can include callables, tool objects,
|
||||
or MCP instances. Defaults to None.
|
||||
function_calling_llm (Optional[Any], optional): Dedicated language model for function
|
||||
calling operations. If None, uses the main llm parameter. Defaults to None.
|
||||
max_iter (int, optional): Maximum number of iterations the agent can perform during
|
||||
task execution to prevent infinite loops. Defaults to 20.
|
||||
max_rpm (Optional[int], optional): Maximum requests per minute to rate limit API calls
|
||||
and prevent quota exhaustion. If None, no rate limiting is applied. Defaults to None.
|
||||
max_execution_time (Optional[int], optional): Maximum execution time in seconds for
|
||||
agent operations before timeout. If None, no time limit is enforced. Defaults to None.
|
||||
memory (Optional[Any], optional): Memory system for storing and retrieving information
|
||||
across conversations. Requires memory dependencies to be installed. Defaults to None.
|
||||
verbose (bool, optional): Enable detailed logging and status updates during agent
|
||||
execution for debugging and monitoring. Defaults to True.
|
||||
allow_delegation (bool, optional): Allow the agent to delegate tasks to other agents
|
||||
or sub-processes when appropriate. Defaults to False.
|
||||
step_callback (Optional[Any], optional): Callback function called after each step
|
||||
of agent execution for custom monitoring or intervention. Defaults to None.
|
||||
cache (bool, optional): Enable caching of responses and computations to improve
|
||||
performance and reduce API costs. Defaults to True.
|
||||
system_template (Optional[str], optional): Custom template for system prompts that
|
||||
overrides the default system prompt generation. Defaults to None.
|
||||
prompt_template (Optional[str], optional): Template for formatting user prompts
|
||||
before sending to the language model. Defaults to None.
|
||||
response_template (Optional[str], optional): Template for formatting agent responses
|
||||
before returning to the user. Defaults to None.
|
||||
allow_code_execution (Optional[bool], optional): Enable the agent to execute code
|
||||
snippets during task completion. Use with caution for security. Defaults to False.
|
||||
max_retry_limit (int, optional): Maximum number of retry attempts for failed operations
|
||||
before giving up. Helps handle transient errors. Defaults to 2.
|
||||
code_execution_mode (Literal["safe", "unsafe"], optional): Safety mode for code execution.
|
||||
"safe" restricts dangerous operations, "unsafe" allows full code execution. Defaults to "safe".
|
||||
embedder_config (Optional[Dict[str, Any]], optional): Configuration dictionary for
|
||||
text embedding models used in knowledge retrieval and similarity search. Defaults to None.
|
||||
knowledge (Optional[List[str]], optional): List of knowledge sources (file paths, URLs,
|
||||
or text content) to be processed and made available to the agent. Defaults to None.
|
||||
knowledge_config (Optional[Dict[str, Any]], optional): Configuration for knowledge
|
||||
processing and retrieval system including chunking and indexing parameters. Defaults to None.
|
||||
use_system_prompt (Optional[bool], optional): Whether to include system prompts in
|
||||
conversations to establish agent behavior and context. Defaults to True.
|
||||
markdown (bool, optional): Enable markdown formatting in agent responses for better
|
||||
readability and structure. Defaults to True.
|
||||
stream (bool, optional): Enable streaming responses from the language model for real-time
|
||||
output when using Agent.start() method. Defaults to False for backward compatibility.
|
||||
metrics (bool, optional): Enable automatic token usage tracking and display summary
|
||||
when tasks complete. Simplifies token monitoring for cost optimization. Defaults to False.
|
||||
self_reflect (bool, optional): Enable self-reflection capabilities where the agent
|
||||
evaluates and improves its own responses. Defaults to False.
|
||||
max_reflect (int, optional): Maximum number of self-reflection iterations to prevent
|
||||
excessive reflection loops. Defaults to 3.
|
||||
min_reflect (int, optional): Minimum number of self-reflection iterations required
|
||||
before accepting a response as satisfactory. Defaults to 1.
|
||||
reflect_llm (Optional[str], optional): Dedicated language model for self-reflection
|
||||
operations. If None, uses the main llm parameter. Defaults to None.
|
||||
reflect_prompt (Optional[str], optional): Custom prompt template for self-reflection
|
||||
that guides the agent's self-evaluation process. Defaults to None.
|
||||
user_id (Optional[str], optional): Unique identifier for the user or session to
|
||||
enable personalized responses and memory isolation. Defaults to "praison".
|
||||
reasoning_steps (bool, optional): Enable step-by-step reasoning output to show the
|
||||
agent's thought process during problem solving. Defaults to False.
|
||||
guardrail (Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]], optional):
|
||||
Safety mechanism to validate agent outputs. Can be a validation function or
|
||||
description string for LLM-based validation. Defaults to None.
|
||||
max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
|
||||
validation fails before giving up. Defaults to 3.
|
||||
handoffs (Optional[List[Union['Agent', 'Handoff']]], optional): List of agents or
|
||||
handoff configurations that this agent can delegate tasks to. Enables agent-to-agent
|
||||
collaboration and task specialization. Defaults to None.
|
||||
base_url (Optional[str], optional): Base URL for custom LLM endpoints (e.g., Ollama).
|
||||
If provided, automatically creates a custom LLM instance. Defaults to None.
|
||||
api_key (Optional[str], optional): API key for LLM provider. If not provided,
|
||||
falls back to environment variables. Defaults to None.
|
||||
name: Agent name for identification and logging. Defaults to "Agent".
|
||||
role: Role/job title defining expertise (e.g., "Data Analyst").
|
||||
goal: Primary objective the agent aims to achieve.
|
||||
backstory: Background context shaping personality and decisions.
|
||||
instructions: Direct instructions (overrides role/goal/backstory). Recommended for simple agents.
|
||||
llm: Model name string ("gpt-4o", "anthropic/claude-3-sonnet") or LLM object.
|
||||
Defaults to OPENAI_MODEL_NAME env var or "gpt-4o-mini".
|
||||
model: Alias for llm parameter.
|
||||
function_calling_llm: Dedicated LLM for function calling. Deprecated: use llm=.
|
||||
llm_config: LLM configuration dict. Deprecated: use llm=.
|
||||
base_url: Custom LLM endpoint URL (e.g., for Ollama). Kept separate for auth.
|
||||
api_key: API key for LLM provider. Kept separate for auth.
|
||||
tools: List of tools, functions, callables, or MCP instances.
|
||||
allow_delegation: Allow task delegation to other agents. Defaults to False.
|
||||
allow_code_execution: Enable code execution during tasks. Defaults to False.
|
||||
code_execution_mode: "safe" (restricted) or "unsafe" (full access). Defaults to "safe".
|
||||
handoffs: List of Agent or Handoff objects for agent-to-agent collaboration.
|
||||
auto_save: Session name for automatic session saving.
|
||||
rate_limiter: Rate limiter instance for API call throttling.
|
||||
memory: Memory system configuration. Accepts:
|
||||
- bool: True enables defaults, False disables
|
||||
- MemoryConfig: Custom configuration
|
||||
- MemoryManager: Pre-configured instance
|
||||
knowledge: Knowledge sources. Accepts:
|
||||
- bool: True enables defaults
|
||||
- List[str]: File paths, URLs, or text content
|
||||
- KnowledgeConfig: Custom configuration
|
||||
planning: Planning mode. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- PlanningConfig: Custom configuration
|
||||
reflection: Self-reflection. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- ReflectionConfig: Custom configuration
|
||||
guardrails: Output validation. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- Callable: Validation function
|
||||
- GuardrailConfig: Custom configuration
|
||||
web: Web search/fetch. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- WebConfig: Custom configuration
|
||||
context: Context management. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- ManagerConfig: Custom configuration
|
||||
autonomy: Autonomy settings. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- Dict: Configuration dict
|
||||
- AutonomyConfig: Custom configuration
|
||||
verification_hooks: List of VerificationHook instances for output verification.
|
||||
output: Output configuration. Accepts:
|
||||
- str: Preset name ("silent", "actions", "verbose", "json", "stream")
|
||||
- OutputConfig: Custom configuration
|
||||
Controls: verbose, markdown, stream, metrics, reasoning_steps
|
||||
execution: Execution configuration. Accepts:
|
||||
- str: Preset name ("fast", "balanced", "thorough")
|
||||
- ExecutionConfig: Custom configuration
|
||||
Controls: max_iter, max_rpm, max_execution_time, max_retry_limit
|
||||
templates: Template configuration (TemplateConfig).
|
||||
Controls: system_template, prompt_template, response_template
|
||||
caching: Caching configuration. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- CachingConfig: Custom configuration
|
||||
hooks: Event hooks. Accepts:
|
||||
- List: List of hook callables
|
||||
- HooksConfig: Custom configuration
|
||||
skills: Agent skills. Accepts:
|
||||
- List[str]: Skill directory paths
|
||||
- SkillsConfig: Custom configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If all of name, role, goal, backstory, and instructions are None.
|
||||
ImportError: If memory or LLM features are requested but dependencies are not installed.
|
||||
|
||||
Note:
|
||||
Many legacy parameters have been consolidated into config objects:
|
||||
- verbose, markdown, stream, metrics, reasoning_steps → output=
|
||||
- max_iter, max_rpm, max_execution_time, max_retry_limit → execution=
|
||||
- self_reflect, max_reflect, min_reflect, reflect_llm → reflection=
|
||||
- guardrail, max_guardrail_retries → guardrails=
|
||||
- system_template, prompt_template, response_template → templates=
|
||||
- cache, prompt_caching → caching=
|
||||
- web_search, web_fetch → web=
|
||||
"""
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -446,14 +438,27 @@ class Agent:
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve OUTPUT param using unified resolver
|
||||
# Supports: None, str preset, list [preset, overrides], OutputConfig
|
||||
# DEFAULT: "silent" mode (zero overhead, fastest performance)
|
||||
# Can be overridden by PRAISONAI_OUTPUT env var
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
from ..config.presets import DEFAULT_OUTPUT_MODE
|
||||
|
||||
# Check for env var override
|
||||
env_output = os.environ.get('PRAISONAI_OUTPUT', '').lower()
|
||||
if env_output and env_output in OUTPUT_PRESETS and output is None:
|
||||
output = env_output
|
||||
|
||||
# Use default output mode if not specified
|
||||
if output is None:
|
||||
output = DEFAULT_OUTPUT_MODE
|
||||
|
||||
_output_config = resolve(
|
||||
value=output,
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=OutputConfig(),
|
||||
default=OutputConfig(), # OutputConfig defaults to silent mode (zero overhead)
|
||||
)
|
||||
if _output_config:
|
||||
verbose = _output_config.verbose
|
||||
@@ -462,8 +467,24 @@ class Agent:
|
||||
metrics = _output_config.metrics
|
||||
reasoning_steps = _output_config.reasoning_steps
|
||||
output_style = getattr(_output_config, 'style', None)
|
||||
actions_trace = getattr(_output_config, 'actions_trace', False) # Default False (silent)
|
||||
json_output = getattr(_output_config, 'json_output', False)
|
||||
else:
|
||||
verbose, markdown, stream, metrics, reasoning_steps = True, True, False, False, False
|
||||
# Fallback defaults match silent mode (zero overhead)
|
||||
verbose, markdown, stream, metrics, reasoning_steps = False, False, False, False, False
|
||||
actions_trace = False # No callbacks by default
|
||||
json_output = False
|
||||
|
||||
# Enable actions output mode if configured
|
||||
# This registers callbacks to capture tool calls and final output
|
||||
if actions_trace:
|
||||
try:
|
||||
from ..output.actions import enable_actions_mode, is_actions_mode_enabled
|
||||
if not is_actions_mode_enabled():
|
||||
output_format = "jsonl" if json_output else "text"
|
||||
enable_actions_mode(redact=True, use_color=True, format=output_format)
|
||||
except ImportError:
|
||||
pass # Actions module not available
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve EXECUTION param using unified resolver
|
||||
@@ -783,13 +804,30 @@ class Agent:
|
||||
self._llm_config = llm_config or {}
|
||||
self._llm_configurable = self._llm_config.get('configurable', False)
|
||||
|
||||
# ============================================================
|
||||
# LLM CONSOLIDATION: Handle model= alias and deprecation warnings
|
||||
# Precedence: llm= > model= > default
|
||||
# ============================================================
|
||||
import warnings
|
||||
|
||||
# Handle model= alias for llm= (NO warnings - both are valid)
|
||||
if llm is None and model is not None:
|
||||
llm = model # model= is an alias for llm=
|
||||
|
||||
# Legacy params (llm_config, function_calling_llm) are accepted silently
|
||||
# for backward compatibility - NO deprecation warnings per policy
|
||||
|
||||
# Store rate limiter (optional, zero overhead when None)
|
||||
self._rate_limiter = rate_limiter
|
||||
|
||||
# Store OpenAI client parameters for lazy initialization
|
||||
# Store OpenAI client parameters for lazy initialization (kept separate)
|
||||
self._openai_api_key = api_key
|
||||
self._openai_base_url = base_url
|
||||
self.__openai_client = None
|
||||
|
||||
# Expose base_url and api_key as properties for tests
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
|
||||
# If base_url is provided, always create a custom LLM instance
|
||||
if base_url:
|
||||
@@ -806,7 +844,7 @@ class Agent:
|
||||
self.llm_instance = LLM(**llm_config)
|
||||
else:
|
||||
# Create LLM with model string and base_url
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.llm_instance = LLM(
|
||||
model=model_name,
|
||||
base_url=base_url,
|
||||
@@ -869,7 +907,7 @@ class Agent:
|
||||
) from e
|
||||
# Otherwise, fall back to OpenAI environment/name
|
||||
else:
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
# Handle tools parameter - ensure it's always a list
|
||||
if callable(tools):
|
||||
# If a single function/callable is passed, wrap it in a list
|
||||
@@ -915,7 +953,7 @@ class Agent:
|
||||
self.min_reflect = min_reflect
|
||||
self.reflect_prompt = reflect_prompt
|
||||
# Use the same model selection logic for reflect_llm
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self._console = None # Lazy load console when needed
|
||||
|
||||
# Initialize system prompt
|
||||
@@ -957,9 +995,14 @@ Your Goal: {self.goal}
|
||||
self.tools.append(internet_search)
|
||||
logging.info("Model does not support native web search. Added DuckDuckGo fallback tool.")
|
||||
|
||||
# Log warning if web_fetch is enabled but model doesn't support it
|
||||
# Raise error if web_fetch is explicitly enabled but model doesn't support it
|
||||
# Web fetch is only supported by Anthropic/Claude models
|
||||
if web_fetch and not self._model_supports_web_fetch():
|
||||
logging.warning(f"Model '{self.llm}' does not support native web fetch. Web fetch will be ignored.")
|
||||
raise ValueError(
|
||||
f"web_fetch is only supported on Anthropic/Claude models. "
|
||||
f"Model '{self.llm}' does not support web fetch. "
|
||||
f"Either use a Claude model (e.g., 'anthropic/claude-sonnet-4') or disable fetch with web=WebConfig(fetch=False)."
|
||||
)
|
||||
|
||||
# Initialize guardrail settings
|
||||
self.guardrail = guardrail
|
||||
@@ -1042,6 +1085,9 @@ Your Goal: {self.goal}
|
||||
self._context_param = context # Store raw param for lazy init
|
||||
self._context_manager = None # Lazy initialized on first use
|
||||
self._context_manager_initialized = False
|
||||
|
||||
# Action trace mode - handled via display callbacks, not separate emitter
|
||||
self._actions_trace = actions_trace
|
||||
|
||||
@property
|
||||
def auto_memory(self):
|
||||
@@ -1697,7 +1743,7 @@ Your Goal: {self.goal}
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
model_name = "gpt-4o-mini"
|
||||
|
||||
return supports_web_search(model_name)
|
||||
|
||||
@@ -1719,7 +1765,7 @@ Your Goal: {self.goal}
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
model_name = "gpt-4o-mini"
|
||||
|
||||
return supports_web_fetch(model_name)
|
||||
|
||||
@@ -1963,7 +2009,7 @@ Your Goal: {self.goal}
|
||||
|
||||
Returns:
|
||||
The LLM model/instance being used by this agent.
|
||||
- For standard models: returns the model string (e.g., "gpt-5-nano")
|
||||
- For standard models: returns the model string (e.g., "gpt-4o-mini")
|
||||
- For custom LLM instances: returns the LLM instance object
|
||||
- For provider models: returns the LLM instance object
|
||||
"""
|
||||
@@ -1973,7 +2019,7 @@ Your Goal: {self.goal}
|
||||
return self.llm
|
||||
else:
|
||||
# Default fallback
|
||||
return "gpt-5-nano"
|
||||
return "gpt-4o-mini"
|
||||
|
||||
def _ensure_knowledge_processed(self):
|
||||
"""Ensure knowledge is initialized and processed when first accessed."""
|
||||
@@ -3673,7 +3719,7 @@ Your Goal: {self.goal}"""
|
||||
if self.verbose:
|
||||
logging.debug(f"Agent {self.name} final response: {response_text}")
|
||||
# Return only reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content'):
|
||||
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content') and response.choices[0].message.reasoning_content:
|
||||
# Apply guardrail to reasoning content
|
||||
try:
|
||||
validated_reasoning = self._apply_guardrail_with_retry(response.choices[0].message.reasoning_content, original_prompt, temperature, tools, task_name, task_description, task_id)
|
||||
@@ -4198,7 +4244,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
# Return only reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content'):
|
||||
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content') and final_response.choices[0].message.reasoning_content:
|
||||
return final_response.choices[0].message.reasoning_content
|
||||
return final_response.choices[0].message.content if final_response else full_response_text
|
||||
|
||||
@@ -4212,16 +4258,86 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
return None
|
||||
|
||||
async def arun(self, prompt: str, **kwargs):
|
||||
"""Async alias for astart() method"""
|
||||
return await self.astart(prompt, **kwargs)
|
||||
"""Async version of run() - silent, non-streaming, returns structured result.
|
||||
|
||||
Production-friendly async execution. Does not stream or display output.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments passed to achat()
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
"""
|
||||
# Force non-streaming, non-display for production use
|
||||
kwargs['stream'] = False
|
||||
return await self.achat(prompt, **kwargs)
|
||||
|
||||
async def astart(self, prompt: str, **kwargs):
|
||||
"""Async version of start method"""
|
||||
"""Async version of start() - interactive, streaming-aware.
|
||||
|
||||
Beginner-friendly async execution. Streams by default when in TTY.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments passed to achat()
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
"""
|
||||
import sys
|
||||
|
||||
# Determine streaming behavior (same logic as start())
|
||||
stream_requested = kwargs.get('stream')
|
||||
if stream_requested is None:
|
||||
if getattr(self, 'stream', None) is not None:
|
||||
stream_requested = self.stream
|
||||
else:
|
||||
stream_requested = sys.stdout.isatty()
|
||||
|
||||
kwargs['stream'] = stream_requested
|
||||
return await self.achat(prompt, **kwargs)
|
||||
|
||||
def run(self, prompt: str, **kwargs):
|
||||
"""Alias for start() method"""
|
||||
return self.start(prompt, **kwargs)
|
||||
"""Execute agent silently and return structured result.
|
||||
|
||||
Production-friendly execution. Does not stream or display output by default.
|
||||
Use this for programmatic/scripted usage where you want the result only.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
result = agent.run("What is 2+2?") # Silent, returns "4"
|
||||
print(result)
|
||||
```
|
||||
"""
|
||||
# Production defaults: no streaming, no display
|
||||
if 'stream' not in kwargs:
|
||||
kwargs['stream'] = False
|
||||
|
||||
# Load history context
|
||||
self._load_history_context()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
result = self._start_with_planning(prompt, **kwargs)
|
||||
else:
|
||||
result = self.chat(prompt, **kwargs)
|
||||
|
||||
# Auto-save session if enabled
|
||||
self._auto_save_session()
|
||||
|
||||
return result
|
||||
|
||||
def _get_planning_agent(self):
|
||||
"""Lazy load PlanningAgent for planning mode."""
|
||||
@@ -4356,18 +4472,59 @@ Write the complete compiled report:"""
|
||||
# Chat history is preserved in self.chat_history (no action needed)
|
||||
|
||||
def start(self, prompt: str, **kwargs):
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
# Load history from past sessions (now handled via context= param)
|
||||
"""Start the agent interactively with streaming 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.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**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
|
||||
|
||||
Returns:
|
||||
- If streaming: Generator yielding response chunks
|
||||
- If not streaming: The complete response as a string
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
|
||||
# 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
|
||||
```
|
||||
"""
|
||||
import sys
|
||||
|
||||
# Load history from past sessions
|
||||
self._load_history_context()
|
||||
|
||||
# Determine streaming behavior
|
||||
# Priority: explicit kwarg > agent's stream attribute > TTY detection
|
||||
stream_requested = kwargs.get('stream')
|
||||
if stream_requested is None:
|
||||
# Check agent's stream attribute first
|
||||
if getattr(self, 'stream', None) is not None:
|
||||
stream_requested = self.stream
|
||||
else:
|
||||
# Auto-detect: stream if stdout is a TTY (interactive terminal)
|
||||
stream_requested = sys.stdout.isatty()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
result = self._start_with_planning(prompt, **kwargs)
|
||||
elif kwargs.get('stream', getattr(self, 'stream', False)):
|
||||
elif stream_requested:
|
||||
# Return a generator for streaming response
|
||||
kwargs['stream'] = True
|
||||
result = self._start_stream(prompt, **kwargs)
|
||||
else:
|
||||
# Return regular chat response for backward compatibility
|
||||
# Return regular chat response
|
||||
kwargs['stream'] = False
|
||||
result = self.chat(prompt, **kwargs)
|
||||
|
||||
@@ -4376,6 +4533,48 @@ Write the complete compiled report:"""
|
||||
|
||||
return result
|
||||
|
||||
def iter_stream(self, prompt: str, **kwargs):
|
||||
"""Stream agent response as an iterator of chunks.
|
||||
|
||||
App-friendly streaming. Yields response chunks without terminal display.
|
||||
Use this for building custom UIs or processing streams programmatically.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments:
|
||||
- display (bool): Show terminal output. Default: False
|
||||
- output (str): Output preset override
|
||||
|
||||
Yields:
|
||||
str: Response chunks as they are generated
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
|
||||
# Process stream programmatically
|
||||
full_response = ""
|
||||
for chunk in agent.iter_stream("Tell me a story"):
|
||||
full_response += chunk
|
||||
# Custom processing here
|
||||
|
||||
# Or collect all at once
|
||||
response = "".join(agent.iter_stream("Hello"))
|
||||
```
|
||||
"""
|
||||
# Load history context
|
||||
self._load_history_context()
|
||||
|
||||
# Force streaming, no display by default (app-friendly)
|
||||
kwargs['stream'] = True
|
||||
|
||||
# Use the internal streaming generator
|
||||
for chunk in self._start_stream(prompt, **kwargs):
|
||||
yield chunk
|
||||
|
||||
# Auto-save session if enabled
|
||||
self._auto_save_session()
|
||||
|
||||
def _load_history_context(self):
|
||||
"""Load history from past sessions into context.
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ This report contains all agent interactions and outputs from a complete ContextA
|
||||
|
||||
Provide comprehensive analysis that follows the PRD template principles and enables
|
||||
AI assistants to implement features that perfectly match existing codebase patterns.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -515,7 +515,7 @@ codebase style and architecture following PRD template principles."""
|
||||
role="Expert Manual Codebase Analysis Specialist",
|
||||
goal="Perform comprehensive manual codebase analysis following PRD methodology",
|
||||
instructions="""Analyze the codebase samples following PRD template methodology for complete understanding.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -566,7 +566,7 @@ Analyze following PRD principles to extract patterns, conventions, and architect
|
||||
6. Design pattern implementations
|
||||
7. Code complexity metrics
|
||||
8. API and interface patterns""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -624,7 +624,7 @@ Extract comprehensive patterns that follow PRD template principles for implement
|
||||
|
||||
For each pattern, provide the pattern name, where it's used, and how to replicate it
|
||||
following PRD template principles.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -674,7 +674,7 @@ patterns and best practices for first-try success."""
|
||||
goal="Analyze testing patterns for comprehensive validation framework design",
|
||||
instructions="""Analyze testing patterns to understand validation approaches and create
|
||||
comprehensive test frameworks following PRD methodology.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -801,7 +801,7 @@ Extract testing patterns for validation framework creation following PRD princip
|
||||
Confidence level for one-pass implementation
|
||||
|
||||
Generate PRPs following this EXACT structure for first-try implementation success.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -850,7 +850,7 @@ on the first try following PRD template principles."""
|
||||
6. CODE QUALITY: Complexity analysis, maintainability
|
||||
7. DOCUMENTATION VALIDATION: Documentation completeness
|
||||
8. DEPENDENCY VALIDATION: Dependency analysis and security""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -897,7 +897,7 @@ following PRD template principles."""
|
||||
instructions="""Compile all available documentation following PRD methodology including:
|
||||
README files, API documentation, setup guides, architecture docs, and any other
|
||||
relevant documentation that provides context for implementation.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -943,7 +943,7 @@ following PRD template principles."""
|
||||
instructions="""Analyze integration points following PRD methodology including:
|
||||
APIs, databases, external services, configuration points, and any other
|
||||
integration requirements that affect implementation.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1004,7 +1004,7 @@ following PRD template principles."""
|
||||
8. DOCUMENTATION UPDATES: Documentation to create/update
|
||||
9. INTEGRATION STEPS: How to integrate with existing systems
|
||||
10. VALIDATION CHECKPOINTS: Validation steps at each phase""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1302,7 +1302,7 @@ Every agent interaction has been saved for full audit trail and reproducibility.
|
||||
GOAL: [extracted implementation goal]
|
||||
|
||||
Be precise and extract only what is explicitly mentioned or clearly implied.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1536,7 +1536,7 @@ Note: Detailed function/class metadata not available due to content access limit
|
||||
5. Documentation topics
|
||||
|
||||
Make the output easy for a file selection agent to understand which files contain what functionality.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1770,7 +1770,7 @@ Focus on creating clear, structured metadata that will help with intelligent fil
|
||||
["README.md", "src/auth/login.py", "config/settings.py", ...]
|
||||
|
||||
Maximum 50 files for efficient analysis.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1923,7 +1923,7 @@ Maximum 50 files.""".format(goal=goal)
|
||||
8. EXAMPLES: Similar features that can guide {goal} implementation
|
||||
|
||||
Since these files were pre-selected for relevance, provide deep analysis of how each contributes to implementing: {goal}""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -2024,7 +2024,7 @@ Since these files were pre-selected for relevance, explain how each contributes
|
||||
- Success criteria for {goal}
|
||||
|
||||
Focus everything on successfully implementing: {goal}""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -2303,13 +2303,13 @@ def create_context_agent(llm: Optional[Union[str, Any]] = None, **kwargs) -> Con
|
||||
Factory function to create a ContextAgent following Context Engineering and PRD methodology.
|
||||
|
||||
Args:
|
||||
llm: Language model to use (e.g., "gpt-5-nano", "claude-3-haiku")
|
||||
llm: Language model to use (e.g., "gpt-4o-mini", "claude-3-haiku")
|
||||
**kwargs: Additional arguments to pass to ContextAgent constructor
|
||||
|
||||
Returns:
|
||||
ContextAgent: Configured ContextAgent for comprehensive context generation following PRD principles
|
||||
"""
|
||||
if llm is None:
|
||||
llm = "gpt-5-nano"
|
||||
llm = "gpt-4o-mini"
|
||||
|
||||
return ContextAgent(llm=llm, **kwargs)
|
||||
@@ -17,7 +17,7 @@ Supported Expansion Strategies:
|
||||
Example:
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent(verbose=True)
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
# Basic expansion
|
||||
result = agent.expand("write a movie script in 3 lines")
|
||||
@@ -29,7 +29,7 @@ Example:
|
||||
|
||||
# With tools for context gathering
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = PromptExpanderAgent(tools=[internet_search], verbose=True)
|
||||
agent = PromptExpanderAgent(tools=[internet_search])
|
||||
result = agent.expand("latest AI developments")
|
||||
"""
|
||||
|
||||
@@ -73,13 +73,12 @@ class PromptExpanderAgent:
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for expansion
|
||||
verbose: Whether to print detailed logs
|
||||
model: LLM model to use for expansion (alias for llm=)
|
||||
temperature: Temperature for LLM generation (higher = more creative)
|
||||
tools: Optional tools for context gathering
|
||||
|
||||
Example:
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini", verbose=True)
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
result = agent.expand("write a poem", strategy=ExpandStrategy.CREATIVE)
|
||||
print(result.expanded_prompt)
|
||||
"""
|
||||
@@ -230,8 +229,7 @@ Expanded prompt:"""
|
||||
backstory="You are an expert at understanding user intent and transforming brief requests into comprehensive, detailed prompts.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
output={"verbose": self.verbose, "markdown": False}
|
||||
)
|
||||
return self._agent
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Supported Rewriting Strategies:
|
||||
Example:
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent(verbose=True)
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
# Basic rewriting
|
||||
result = agent.rewrite("AI trends")
|
||||
@@ -34,7 +34,7 @@ Example:
|
||||
|
||||
# With search tools for context-aware rewriting
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = QueryRewriterAgent(tools=[internet_search], verbose=True)
|
||||
agent = QueryRewriterAgent(tools=[internet_search])
|
||||
result = agent.rewrite("latest AI developments") # Searches first, then rewrites
|
||||
"""
|
||||
|
||||
@@ -96,13 +96,12 @@ class QueryRewriterAgent:
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for rewriting
|
||||
verbose: Whether to print detailed logs
|
||||
model: LLM model to use for rewriting (alias for llm=)
|
||||
max_queries: Maximum number of queries to generate for multi-query strategy
|
||||
abbreviations: Dictionary of common abbreviations to expand
|
||||
|
||||
Example:
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini", verbose=True)
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
result = agent.rewrite("ML best practices", strategy=RewriteStrategy.MULTI_QUERY)
|
||||
for query in result.rewritten_queries:
|
||||
print(query)
|
||||
@@ -288,8 +287,7 @@ Improved version:"""
|
||||
backstory="You are an expert at understanding user intent and transforming queries for optimal information retrieval.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
output={"verbose": self.verbose, "markdown": False}
|
||||
)
|
||||
return self._agent
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class RouterAgent(Agent):
|
||||
# Initialize model router
|
||||
self.model_router = model_router or ModelRouter()
|
||||
self.routing_strategy = routing_strategy
|
||||
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
# Process models configuration
|
||||
self.available_models = self._process_models_config(models)
|
||||
|
||||
@@ -148,6 +148,7 @@ class Agents:
|
||||
guardrails: Optional[Any] = None, # Union[bool, Callable, GuardrailConfig] - validation
|
||||
web: Optional[Any] = None, # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None, # Union[bool, ReflectionConfig] - self-reflection
|
||||
caching: Optional[Any] = None, # Union[bool, CachingConfig] - caching
|
||||
):
|
||||
"""
|
||||
Initialize PraisonAIAgents with consolidated feature parameters.
|
||||
@@ -177,6 +178,7 @@ class Agents:
|
||||
self._guardrails = guardrails
|
||||
self._web = web
|
||||
self._reflection = reflection
|
||||
self._caching = caching
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Extract values from consolidated params using UNIFIED CANONICAL resolver
|
||||
# Precedence: Instance > Config > Dict > Array > String > Bool > Default
|
||||
@@ -361,7 +363,7 @@ class Agents:
|
||||
self.variables = variables if variables else {}
|
||||
|
||||
# Check for manager_llm in environment variable if not provided
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
# Set logger level based on verbose
|
||||
if _verbose >= 5:
|
||||
|
||||
@@ -74,6 +74,17 @@ class AutoAgents(Agents):
|
||||
api_key: Optional[str] = None,
|
||||
max_agents: int = 3, # Maximum number of agents
|
||||
autonomy: Optional[Any] = None, # Union[bool, dict, AutonomyConfig] - Enable autonomy for all agents
|
||||
# ============================================================
|
||||
# CONSOLIDATED PARAMS (new SDK contract - take precedence)
|
||||
# ============================================================
|
||||
output: Optional[Any] = None, # Union[str, OutputConfig] - overrides verbose/markdown
|
||||
reflection: Optional[Any] = None, # Union[bool, ReflectionConfig] - overrides self_reflect/max_reflect/min_reflect/reflect_llm
|
||||
caching: Optional[Any] = None, # Union[bool, CachingConfig] - overrides cache
|
||||
knowledge: Optional[Any] = None, # Union[bool, List[str], KnowledgeConfig] - overrides knowledge_sources/embedder_config
|
||||
execution: Optional[Any] = None, # Union[str, ExecutionConfig] - overrides max_iter
|
||||
guardrails: Optional[Any] = None, # Union[bool, Callable, GuardrailConfig]
|
||||
web: Optional[Any] = None, # Union[bool, WebConfig]
|
||||
hooks: Optional[Any] = None, # HooksConfig
|
||||
):
|
||||
"""Initialize AutoAgents with configuration for automatic agent and task creation."""
|
||||
if max_agents < 1:
|
||||
@@ -92,7 +103,7 @@ class AutoAgents(Agents):
|
||||
self.self_reflect = self_reflect
|
||||
self.max_reflect = max_reflect
|
||||
self.min_reflect = min_reflect
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.code_execution_mode = code_execution_mode
|
||||
self.embedder_config = embedder_config
|
||||
@@ -112,6 +123,16 @@ class AutoAgents(Agents):
|
||||
self.api_key = api_key
|
||||
self.autonomy = autonomy # Store autonomy config for agents
|
||||
|
||||
# Store consolidated params (new SDK contract - take precedence over legacy)
|
||||
self._output = output
|
||||
self._reflection = reflection
|
||||
self._caching = caching
|
||||
self._knowledge = knowledge
|
||||
self._execution = execution
|
||||
self._guardrails = guardrails
|
||||
self._web = web
|
||||
self._hooks = hooks
|
||||
|
||||
# Display initial instruction
|
||||
if self.verbose:
|
||||
display_instruction(f"🎯 Main Task: {self.instructions}")
|
||||
@@ -129,14 +150,21 @@ class AutoAgents(Agents):
|
||||
self._display_agents_and_tasks(agents, tasks)
|
||||
|
||||
# Initialize parent class with generated agents and tasks
|
||||
# Use consolidated output= instead of legacy verbose=
|
||||
super().__init__(
|
||||
agents=agents,
|
||||
tasks=tasks,
|
||||
verbose=verbose,
|
||||
completion_checker=completion_checker,
|
||||
max_retries=max_retries,
|
||||
process=process,
|
||||
manager_llm=manager_llm or self.llm
|
||||
manager_llm=manager_llm or self.llm,
|
||||
output=self._output, # Consolidated output param
|
||||
execution=self._execution,
|
||||
hooks=self._hooks,
|
||||
memory=self.memory,
|
||||
reflection=self._reflection,
|
||||
caching=self._caching,
|
||||
guardrails=self._guardrails,
|
||||
web=self._web,
|
||||
autonomy=self.autonomy,
|
||||
)
|
||||
|
||||
def _display_agents_and_tasks(self, agents: List[Agent], tasks: List[Task]):
|
||||
@@ -431,38 +459,33 @@ DO NOT use strings for tasks. Each task MUST be a complete object with all four
|
||||
agent_tools = self._assign_tools_to_agent(agent_config)
|
||||
|
||||
# Create the agent with all parameters
|
||||
# Consolidated params take precedence over legacy params
|
||||
# Use consolidated knowledge if provided, else fall back to legacy knowledge_sources
|
||||
effective_knowledge = self._knowledge if self._knowledge is not None else self.knowledge_sources
|
||||
|
||||
agent = Agent(
|
||||
name=agent_config.name,
|
||||
role=agent_config.role,
|
||||
goal=agent_config.goal,
|
||||
backstory=agent_config.backstory,
|
||||
tools=agent_tools, # Use assigned tools
|
||||
verbose=self.verbose >= 1,
|
||||
allow_code_execution=self.allow_code_execution,
|
||||
memory=self.memory,
|
||||
markdown=self.markdown,
|
||||
self_reflect=self.self_reflect,
|
||||
max_reflect=self.max_reflect,
|
||||
min_reflect=self.min_reflect,
|
||||
llm=self.llm,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
llm=self.llm, # Consolidated LLM param
|
||||
code_execution_mode=self.code_execution_mode,
|
||||
embedder_config=self.embedder_config,
|
||||
knowledge=self.knowledge_sources,
|
||||
use_system_prompt=self.use_system_prompt,
|
||||
cache=self.cache,
|
||||
allow_delegation=self.allow_delegation,
|
||||
step_callback=self.step_callback,
|
||||
system_template=self.system_template,
|
||||
prompt_template=self.prompt_template,
|
||||
response_template=self.response_template,
|
||||
max_rpm=self.max_rpm,
|
||||
max_execution_time=self.max_execution_time,
|
||||
max_iter=self.max_iter,
|
||||
reflect_llm=self.reflect_llm,
|
||||
base_url=self.base_url,
|
||||
api_key=self.api_key,
|
||||
autonomy=self.autonomy # Pass autonomy config to agents
|
||||
# Consolidated params (new SDK contract)
|
||||
output=self._output, # Overrides verbose/markdown
|
||||
reflection=self._reflection, # Overrides self_reflect/max_reflect/min_reflect/reflect_llm
|
||||
caching=self._caching, # Overrides cache
|
||||
knowledge=effective_knowledge, # Overrides knowledge_sources/embedder_config
|
||||
execution=self._execution, # Overrides max_iter
|
||||
guardrails=self._guardrails,
|
||||
web=self._web,
|
||||
hooks=self._hooks,
|
||||
autonomy=self.autonomy,
|
||||
)
|
||||
agents.append(agent)
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ class SubagentDelegator:
|
||||
return result
|
||||
|
||||
async def _create_agent(self, profile: AgentProfile, task: DelegationTask) -> Any:
|
||||
"""Create an agent from profile."""
|
||||
"""Create an agent from profile using consolidated params."""
|
||||
if self.agent_factory:
|
||||
return self.agent_factory(profile.name)
|
||||
|
||||
@@ -387,14 +387,17 @@ class SubagentDelegator:
|
||||
# Filter tools based on profile
|
||||
tools = self._get_tools_for_profile(profile)
|
||||
|
||||
# Use consolidated params (protocol-compliant)
|
||||
# output='silent' replaces verbose=False
|
||||
# execution={max_iter: N} replaces max_iter=N
|
||||
agent = Agent(
|
||||
name=profile.name,
|
||||
role=profile.name.title(),
|
||||
goal=task.objective,
|
||||
backstory=profile.system_prompt,
|
||||
tools=tools,
|
||||
verbose=False,
|
||||
max_iter=min(task.max_steps, profile.max_steps),
|
||||
output='silent',
|
||||
execution={'max_iter': min(task.max_steps, profile.max_steps)},
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
@@ -44,12 +44,79 @@ class MemoryBackend(str, Enum):
|
||||
MONGODB = "mongodb"
|
||||
|
||||
|
||||
class LearnScope(str, Enum):
|
||||
"""Scope for learning data visibility.
|
||||
|
||||
PRIVATE: Learning data is private to this user/agent (default, safest)
|
||||
SHARED: Learning data is shared with all agents
|
||||
"""
|
||||
PRIVATE = "private" # Private to this user/agent
|
||||
SHARED = "shared" # Shared with all agents
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearnConfig:
|
||||
"""
|
||||
Configuration for continuous learning within memory system.
|
||||
|
||||
Learning captures patterns, preferences, and insights from agent interactions
|
||||
to improve future responses. All learning data is stored within the memory system.
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(memory=MemoryConfig(learn=True))
|
||||
|
||||
# With specific capabilities
|
||||
Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True, # User preferences
|
||||
insights=True, # Observations
|
||||
patterns=True, # Reusable knowledge
|
||||
)
|
||||
))
|
||||
"""
|
||||
# Learning capabilities
|
||||
persona: bool = True # User preferences and profile
|
||||
insights: bool = True # Observations and learnings
|
||||
thread: bool = True # Session/conversation context
|
||||
patterns: bool = False # Reusable knowledge patterns
|
||||
decisions: bool = False # Decision logging
|
||||
feedback: bool = False # Outcome signals
|
||||
improvements: bool = False # Self-improvement proposals
|
||||
|
||||
# Scope configuration
|
||||
scope: Union[str, LearnScope] = LearnScope.PRIVATE
|
||||
|
||||
# Storage configuration
|
||||
store_path: Optional[str] = None # Custom storage path
|
||||
|
||||
# Maintenance settings
|
||||
auto_consolidate: bool = True # Auto-consolidate learnings
|
||||
retention_days: Optional[int] = None # Days to retain (None = forever)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"persona": self.persona,
|
||||
"insights": self.insights,
|
||||
"thread": self.thread,
|
||||
"patterns": self.patterns,
|
||||
"decisions": self.decisions,
|
||||
"feedback": self.feedback,
|
||||
"improvements": self.improvements,
|
||||
"scope": self.scope.value if isinstance(self.scope, LearnScope) else self.scope,
|
||||
"store_path": self.store_path,
|
||||
"auto_consolidate": self.auto_consolidate,
|
||||
"retention_days": self.retention_days,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryConfig:
|
||||
"""
|
||||
Configuration for agent memory and session management.
|
||||
|
||||
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db
|
||||
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db, learn
|
||||
|
||||
Usage:
|
||||
# Simple enable (uses FileMemory)
|
||||
@@ -58,12 +125,22 @@ class MemoryConfig:
|
||||
# With backend
|
||||
Agent(memory=MemoryConfig(backend="redis"))
|
||||
|
||||
# Full config
|
||||
# Full config with learning
|
||||
Agent(memory=MemoryConfig(
|
||||
backend="sqlite",
|
||||
user_id="user123",
|
||||
session_id="session456",
|
||||
auto_memory=True,
|
||||
learn=True, # Enable continuous learning
|
||||
))
|
||||
|
||||
# With detailed learn config
|
||||
Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
)
|
||||
))
|
||||
"""
|
||||
# Backend selection
|
||||
@@ -85,8 +162,17 @@ class MemoryConfig:
|
||||
# Memory configuration dict (for provider-specific settings)
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Continuous learning configuration
|
||||
learn: Optional[Union[bool, LearnConfig]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
learn_dict = None
|
||||
if self.learn is True:
|
||||
learn_dict = LearnConfig().to_dict()
|
||||
elif isinstance(self.learn, LearnConfig):
|
||||
learn_dict = self.learn.to_dict()
|
||||
|
||||
return {
|
||||
"backend": self.backend.value if isinstance(self.backend, MemoryBackend) else self.backend,
|
||||
"user_id": self.user_id,
|
||||
@@ -94,6 +180,7 @@ class MemoryConfig:
|
||||
"auto_memory": self.auto_memory,
|
||||
"claude_memory": self.claude_memory,
|
||||
"config": self.config,
|
||||
"learn": learn_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -391,10 +478,21 @@ class OutputConfig:
|
||||
|
||||
Consolidates: verbose, markdown, stream, metrics, reasoning_steps, output_style
|
||||
|
||||
DEFAULT: output="silent" (zero overhead, fastest performance)
|
||||
|
||||
Usage:
|
||||
# Simple preset
|
||||
# Default is silent mode (no output overhead, programmatic use)
|
||||
Agent(instructions="...") # Uses output="silent"
|
||||
|
||||
# Actions mode (tool calls + final output trace)
|
||||
Agent(output="actions")
|
||||
|
||||
# Verbose mode with Rich panels
|
||||
Agent(output="verbose")
|
||||
|
||||
# JSON mode for piping
|
||||
Agent(output="json")
|
||||
|
||||
# With config
|
||||
Agent(output=OutputConfig(
|
||||
verbose=True,
|
||||
@@ -404,11 +502,11 @@ class OutputConfig:
|
||||
reasoning_steps=True,
|
||||
))
|
||||
"""
|
||||
# Verbosity
|
||||
verbose: bool = True
|
||||
# Verbosity - False by default for silent mode (fastest)
|
||||
verbose: bool = False
|
||||
|
||||
# Formatting
|
||||
markdown: bool = True
|
||||
# Formatting - False by default for silent mode
|
||||
markdown: bool = False
|
||||
|
||||
# Streaming
|
||||
stream: bool = False
|
||||
@@ -422,6 +520,14 @@ class OutputConfig:
|
||||
# Output style (custom styling)
|
||||
style: Optional[Any] = None
|
||||
|
||||
# Actions trace mode - shows tool calls, agent lifecycle, final output
|
||||
# When True, registers callbacks and outputs to stderr
|
||||
# DEFAULT: False for zero overhead (silent mode)
|
||||
actions_trace: bool = False
|
||||
|
||||
# JSON output mode - emit JSONL events for piping
|
||||
json_output: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
@@ -430,6 +536,8 @@ class OutputConfig:
|
||||
"stream": self.stream,
|
||||
"metrics": self.metrics,
|
||||
"reasoning_steps": self.reasoning_steps,
|
||||
"actions_trace": self.actions_trace,
|
||||
"json_output": self.json_output,
|
||||
}
|
||||
|
||||
|
||||
@@ -857,9 +965,259 @@ SkillsParam = Union[List[str], SkillsConfig]
|
||||
AutonomyParam = Union[bool, Dict[str, Any], AutonomyConfig]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRECEDENCE LADDER RESOLVERS
|
||||
# =============================================================================
|
||||
# Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
#
|
||||
# These resolvers normalize various input types into the canonical Config form.
|
||||
# This allows users to pass simplified inputs while maintaining type safety.
|
||||
|
||||
|
||||
def resolve_memory(value: MemoryParam) -> Optional[MemoryConfig]:
|
||||
"""
|
||||
Resolve memory= parameter following precedence ladder.
|
||||
|
||||
Precedence: Instance > Config > Dict > String > Bool > Default
|
||||
|
||||
Args:
|
||||
value: Memory parameter in any supported form
|
||||
|
||||
Returns:
|
||||
MemoryConfig if enabled, None if disabled
|
||||
|
||||
Examples:
|
||||
>>> resolve_memory(None) # Default: disabled
|
||||
None
|
||||
>>> resolve_memory(False) # Explicit disable
|
||||
None
|
||||
>>> resolve_memory(True) # Enable with defaults
|
||||
MemoryConfig(backend='file')
|
||||
>>> resolve_memory("redis") # String shorthand
|
||||
MemoryConfig(backend='redis')
|
||||
>>> resolve_memory({"backend": "sqlite", "user_id": "alice"}) # Dict
|
||||
MemoryConfig(backend='sqlite', user_id='alice')
|
||||
>>> resolve_memory(MemoryConfig(backend="postgres")) # Config passthrough
|
||||
MemoryConfig(backend='postgres')
|
||||
"""
|
||||
# Default: disabled
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Bool: False = disabled, True = defaults
|
||||
if value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return MemoryConfig()
|
||||
|
||||
# String: backend shorthand
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
backend = MemoryBackend(value.lower())
|
||||
except ValueError:
|
||||
backend = value # Allow custom backend strings
|
||||
return MemoryConfig(backend=backend)
|
||||
|
||||
# Dict: expand to config
|
||||
if isinstance(value, dict):
|
||||
# Handle backend enum conversion
|
||||
backend = value.get("backend", MemoryBackend.FILE)
|
||||
if isinstance(backend, str):
|
||||
try:
|
||||
backend = MemoryBackend(backend.lower())
|
||||
except ValueError:
|
||||
pass # Keep as string for custom backends
|
||||
return MemoryConfig(
|
||||
backend=backend,
|
||||
user_id=value.get("user_id"),
|
||||
session_id=value.get("session_id"),
|
||||
auto_memory=value.get("auto_memory", False),
|
||||
claude_memory=value.get("claude_memory", False),
|
||||
db=value.get("db"),
|
||||
config=value.get("config"),
|
||||
)
|
||||
|
||||
# Config: passthrough
|
||||
if isinstance(value, MemoryConfig):
|
||||
return value
|
||||
|
||||
# Instance (MemoryManager): return as-is (caller handles)
|
||||
# This is the highest precedence - user provided a pre-configured instance
|
||||
return value
|
||||
|
||||
|
||||
def resolve_knowledge(value: KnowledgeParam) -> Optional[KnowledgeConfig]:
|
||||
"""
|
||||
Resolve knowledge= parameter following precedence ladder.
|
||||
|
||||
Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
|
||||
Args:
|
||||
value: Knowledge parameter in any supported form
|
||||
|
||||
Returns:
|
||||
KnowledgeConfig if enabled, None if disabled
|
||||
"""
|
||||
# Default: disabled
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Bool: False = disabled, True = defaults
|
||||
if value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return KnowledgeConfig()
|
||||
|
||||
# String: single source
|
||||
if isinstance(value, str):
|
||||
return KnowledgeConfig(sources=[value])
|
||||
|
||||
# Array: list of sources
|
||||
if isinstance(value, list):
|
||||
return KnowledgeConfig(sources=value)
|
||||
|
||||
# Dict: expand to config
|
||||
if isinstance(value, dict):
|
||||
return KnowledgeConfig(
|
||||
sources=value.get("sources", []),
|
||||
embedder=value.get("embedder", "openai"),
|
||||
embedder_config=value.get("embedder_config"),
|
||||
chunking_strategy=value.get("chunking_strategy", ChunkingStrategy.SEMANTIC),
|
||||
chunk_size=value.get("chunk_size", 1000),
|
||||
chunk_overlap=value.get("chunk_overlap", 200),
|
||||
retrieval_k=value.get("retrieval_k", 5),
|
||||
retrieval_threshold=value.get("retrieval_threshold", 0.0),
|
||||
rerank=value.get("rerank", False),
|
||||
rerank_model=value.get("rerank_model"),
|
||||
auto_retrieve=value.get("auto_retrieve", True),
|
||||
)
|
||||
|
||||
# Config: passthrough
|
||||
if isinstance(value, KnowledgeConfig):
|
||||
return value
|
||||
|
||||
# Instance: return as-is
|
||||
return value
|
||||
|
||||
|
||||
def resolve_planning(value: PlanningParam) -> Optional[PlanningConfig]:
|
||||
"""Resolve planning= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return PlanningConfig()
|
||||
if isinstance(value, dict):
|
||||
return PlanningConfig(**value)
|
||||
if isinstance(value, PlanningConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_reflection(value: ReflectionParam) -> Optional[ReflectionConfig]:
|
||||
"""Resolve reflection= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return ReflectionConfig()
|
||||
if isinstance(value, dict):
|
||||
return ReflectionConfig(**value)
|
||||
if isinstance(value, ReflectionConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_guardrails(value: GuardrailParam) -> Optional[GuardrailConfig]:
|
||||
"""Resolve guardrails= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return GuardrailConfig()
|
||||
if callable(value):
|
||||
return GuardrailConfig(validator=value)
|
||||
if isinstance(value, dict):
|
||||
return GuardrailConfig(**value)
|
||||
if isinstance(value, GuardrailConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_web(value: WebParam) -> Optional[WebConfig]:
|
||||
"""Resolve web= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return WebConfig()
|
||||
if isinstance(value, dict):
|
||||
return WebConfig(**value)
|
||||
if isinstance(value, WebConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_output(value: OutputParam) -> Optional[OutputConfig]:
|
||||
"""Resolve output= parameter following precedence ladder."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
preset = OutputPreset(value.lower())
|
||||
return OutputConfig.from_preset(preset)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return OutputConfig(**value)
|
||||
if isinstance(value, OutputConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_execution(value: ExecutionParam) -> Optional[ExecutionConfig]:
|
||||
"""Resolve execution= parameter following precedence ladder."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
preset = ExecutionPreset(value.lower())
|
||||
return ExecutionConfig.from_preset(preset)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return ExecutionConfig(**value)
|
||||
if isinstance(value, ExecutionConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_caching(value: CachingParam) -> Optional[CachingConfig]:
|
||||
"""Resolve caching= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return CachingConfig()
|
||||
if isinstance(value, dict):
|
||||
return CachingConfig(**value)
|
||||
if isinstance(value, CachingConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_autonomy(value: AutonomyParam) -> Optional[AutonomyConfig]:
|
||||
"""Resolve autonomy= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return AutonomyConfig()
|
||||
if isinstance(value, dict):
|
||||
return AutonomyConfig(**value)
|
||||
if isinstance(value, AutonomyConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"MemoryBackend",
|
||||
"LearnScope",
|
||||
"ChunkingStrategy",
|
||||
"GuardrailAction",
|
||||
"WebSearchProvider",
|
||||
@@ -867,6 +1225,7 @@ __all__ = [
|
||||
"ExecutionPreset",
|
||||
"AutonomyLevel",
|
||||
# Config classes (Agent)
|
||||
"LearnConfig",
|
||||
"MemoryConfig",
|
||||
"KnowledgeConfig",
|
||||
"PlanningConfig",
|
||||
@@ -900,4 +1259,15 @@ __all__ = [
|
||||
"HooksParam",
|
||||
"SkillsParam",
|
||||
"AutonomyParam",
|
||||
# Precedence ladder resolvers
|
||||
"resolve_memory",
|
||||
"resolve_knowledge",
|
||||
"resolve_planning",
|
||||
"resolve_reflection",
|
||||
"resolve_guardrails",
|
||||
"resolve_web",
|
||||
"resolve_output",
|
||||
"resolve_execution",
|
||||
"resolve_caching",
|
||||
"resolve_autonomy",
|
||||
]
|
||||
@@ -41,6 +41,37 @@ MEMORY_URL_SCHEMES: Dict[str, str] = {
|
||||
# =============================================================================
|
||||
|
||||
OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
|
||||
# Silent preset - DEFAULT for SDK
|
||||
# Zero overhead, no callbacks, no I/O on hot path
|
||||
# Fastest performance for programmatic use
|
||||
"silent": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": False,
|
||||
},
|
||||
# Actions preset - opt-in observability
|
||||
# Shows action trace (tool calls, agent lifecycle) + final output
|
||||
# Registers callbacks, outputs to stderr
|
||||
"actions": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": True, # Special flag for action trace mode
|
||||
},
|
||||
# Plain preset - final output only, no action trace
|
||||
"plain": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": False,
|
||||
},
|
||||
"minimal": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
@@ -69,15 +100,31 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
|
||||
"metrics": True,
|
||||
"reasoning_steps": True,
|
||||
},
|
||||
"silent": {
|
||||
# Streaming preset - enables streaming by default
|
||||
"stream": {
|
||||
"verbose": True,
|
||||
"markdown": True,
|
||||
"stream": True,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
},
|
||||
# JSON preset - JSONL output for piping
|
||||
"json": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": True,
|
||||
"json_output": True, # Output as JSONL
|
||||
},
|
||||
}
|
||||
|
||||
# Default output mode - can be overridden by PRAISONAI_OUTPUT env var
|
||||
# "silent" = zero overhead, fastest performance (DEFAULT)
|
||||
# "actions" = tool call trace + final output (opt-in observability)
|
||||
DEFAULT_OUTPUT_MODE = "silent"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Execution Presets
|
||||
|
||||
@@ -157,6 +157,16 @@ __all__ = [
|
||||
"FastContextResult",
|
||||
"FileMatch",
|
||||
"LineRange",
|
||||
# Artifacts (Dynamic Context Discovery)
|
||||
"ArtifactRef",
|
||||
"ArtifactMetadata",
|
||||
"GrepMatch",
|
||||
"QueueConfig",
|
||||
"ArtifactStoreProtocol",
|
||||
"HistoryStoreProtocol",
|
||||
"TerminalLoggerProtocol",
|
||||
"compute_checksum",
|
||||
"generate_summary",
|
||||
]
|
||||
|
||||
|
||||
@@ -252,4 +262,11 @@ def __getattr__(name: str):
|
||||
from praisonaiagents.context.fast.result import LineRange
|
||||
return LineRange
|
||||
|
||||
# Artifacts (Dynamic Context Discovery)
|
||||
if name in ("ArtifactRef", "ArtifactMetadata", "GrepMatch", "QueueConfig",
|
||||
"ArtifactStoreProtocol", "HistoryStoreProtocol", "TerminalLoggerProtocol",
|
||||
"compute_checksum", "generate_summary"):
|
||||
from . import artifacts
|
||||
return getattr(artifacts, name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,580 @@
|
||||
"""
|
||||
Artifact Types and Protocols for Dynamic Context Discovery.
|
||||
|
||||
This module provides lightweight types and protocols for artifact-based
|
||||
context management. Implementations live in the wrapper (praisonai).
|
||||
|
||||
Zero Performance Impact:
|
||||
- Pure dataclasses and protocols
|
||||
- No heavy imports
|
||||
- No I/O at import time
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.context.artifacts import ArtifactRef, ArtifactStoreProtocol
|
||||
|
||||
# Create a reference to a stored artifact
|
||||
ref = ArtifactRef(
|
||||
path="/path/to/artifact.json",
|
||||
summary="API response with 50,000 records",
|
||||
size_bytes=1_200_000,
|
||||
mime_type="application/json",
|
||||
)
|
||||
|
||||
# Implement the protocol in wrapper
|
||||
class FileSystemArtifactStore:
|
||||
def store(self, content, metadata): ...
|
||||
def load(self, ref): ...
|
||||
"""
|
||||
|
||||
import time
|
||||
import hashlib
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtifactRef:
|
||||
"""
|
||||
Reference to a stored artifact.
|
||||
|
||||
Used to replace large tool outputs in context with a lightweight pointer.
|
||||
The agent can then use tail/grep/load operations to access content on demand.
|
||||
|
||||
Attributes:
|
||||
path: Absolute path to the artifact file
|
||||
summary: Brief description or first ~200 chars of content
|
||||
size_bytes: Original content size in bytes
|
||||
mime_type: Content type (e.g., "application/json", "text/plain")
|
||||
checksum: SHA256 hash for integrity verification
|
||||
created_at: Unix timestamp when artifact was created
|
||||
artifact_id: Unique identifier for the artifact
|
||||
agent_id: ID of the agent that created this artifact
|
||||
run_id: ID of the run/session this artifact belongs to
|
||||
tool_name: Name of the tool that produced this output (if applicable)
|
||||
turn_id: Conversation turn number when artifact was created
|
||||
"""
|
||||
path: str
|
||||
summary: str
|
||||
size_bytes: int
|
||||
mime_type: str = "application/octet-stream"
|
||||
checksum: str = ""
|
||||
created_at: float = field(default_factory=time.time)
|
||||
artifact_id: str = ""
|
||||
agent_id: str = ""
|
||||
run_id: str = ""
|
||||
tool_name: Optional[str] = None
|
||||
turn_id: int = 0
|
||||
|
||||
def to_inline(self) -> str:
|
||||
"""
|
||||
Generate inline representation for context.
|
||||
|
||||
Returns a compact string that can be included in the conversation
|
||||
context, allowing the agent to reference the artifact.
|
||||
"""
|
||||
size_str = self._format_size(self.size_bytes)
|
||||
tool_info = f" from {self.tool_name}" if self.tool_name else ""
|
||||
return f"[Artifact{tool_info}: {self.path} ({size_str}) - {self.summary}]"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"path": self.path,
|
||||
"summary": self.summary,
|
||||
"size_bytes": self.size_bytes,
|
||||
"mime_type": self.mime_type,
|
||||
"checksum": self.checksum,
|
||||
"created_at": self.created_at,
|
||||
"artifact_id": self.artifact_id,
|
||||
"agent_id": self.agent_id,
|
||||
"run_id": self.run_id,
|
||||
"tool_name": self.tool_name,
|
||||
"turn_id": self.turn_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ArtifactRef":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
path=data.get("path", ""),
|
||||
summary=data.get("summary", ""),
|
||||
size_bytes=data.get("size_bytes", 0),
|
||||
mime_type=data.get("mime_type", "application/octet-stream"),
|
||||
checksum=data.get("checksum", ""),
|
||||
created_at=data.get("created_at", time.time()),
|
||||
artifact_id=data.get("artifact_id", ""),
|
||||
agent_id=data.get("agent_id", ""),
|
||||
run_id=data.get("run_id", ""),
|
||||
tool_name=data.get("tool_name"),
|
||||
turn_id=data.get("turn_id", 0),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes: int) -> str:
|
||||
"""Format size in human-readable form."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtifactMetadata:
|
||||
"""
|
||||
Metadata for artifact creation and tracking.
|
||||
|
||||
Passed to ArtifactStore.store() to provide context about the artifact.
|
||||
"""
|
||||
agent_id: str = ""
|
||||
run_id: str = ""
|
||||
tool_name: Optional[str] = None
|
||||
turn_id: int = 0
|
||||
content_type: str = "auto" # auto, json, text, binary
|
||||
tags: List[str] = field(default_factory=list)
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"run_id": self.run_id,
|
||||
"tool_name": self.tool_name,
|
||||
"turn_id": self.turn_id,
|
||||
"content_type": self.content_type,
|
||||
"tags": self.tags,
|
||||
"extra": self.extra,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrepMatch:
|
||||
"""A single match from grep operation."""
|
||||
line_number: int
|
||||
line_content: str
|
||||
context_before: List[str] = field(default_factory=list)
|
||||
context_after: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"line_number": self.line_number,
|
||||
"line_content": self.line_content,
|
||||
"context_before": self.context_before,
|
||||
"context_after": self.context_after,
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ArtifactStoreProtocol(Protocol):
|
||||
"""
|
||||
Protocol for artifact storage backends.
|
||||
|
||||
Implementations should handle:
|
||||
- Storing content to persistent storage
|
||||
- Loading content back
|
||||
- Efficient tail/grep operations for large files
|
||||
- Checksum verification
|
||||
- Secret redaction (if enabled)
|
||||
|
||||
Example implementation in wrapper:
|
||||
class FileSystemArtifactStore:
|
||||
def store(self, content, metadata):
|
||||
# Write to ~/.praison/runs/{run_id}/artifacts/
|
||||
...
|
||||
"""
|
||||
|
||||
def store(
|
||||
self,
|
||||
content: Any,
|
||||
metadata: ArtifactMetadata,
|
||||
) -> ArtifactRef:
|
||||
"""
|
||||
Store content as an artifact.
|
||||
|
||||
Args:
|
||||
content: The content to store (will be serialized)
|
||||
metadata: Metadata about the artifact
|
||||
|
||||
Returns:
|
||||
ArtifactRef pointing to the stored artifact
|
||||
"""
|
||||
...
|
||||
|
||||
def load(self, ref: ArtifactRef) -> Any:
|
||||
"""
|
||||
Load full content from an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
|
||||
Returns:
|
||||
The deserialized content
|
||||
"""
|
||||
...
|
||||
|
||||
def tail(self, ref: ArtifactRef, lines: int = 50) -> str:
|
||||
"""
|
||||
Get the last N lines of an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the last N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def head(self, ref: ArtifactRef, lines: int = 50) -> str:
|
||||
"""
|
||||
Get the first N lines of an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the first N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def grep(
|
||||
self,
|
||||
ref: ArtifactRef,
|
||||
pattern: str,
|
||||
context_lines: int = 2,
|
||||
max_matches: int = 50,
|
||||
) -> List[GrepMatch]:
|
||||
"""
|
||||
Search for pattern in artifact content.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
pattern: Regex pattern to search for
|
||||
context_lines: Number of context lines before/after match
|
||||
max_matches: Maximum number of matches to return
|
||||
|
||||
Returns:
|
||||
List of GrepMatch objects
|
||||
"""
|
||||
...
|
||||
|
||||
def chunk(
|
||||
self,
|
||||
ref: ArtifactRef,
|
||||
start_line: int = 1,
|
||||
end_line: Optional[int] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get a chunk of lines from an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
start_line: Starting line number (1-indexed)
|
||||
end_line: Ending line number (inclusive), None for end of file
|
||||
|
||||
Returns:
|
||||
String containing the requested lines
|
||||
"""
|
||||
...
|
||||
|
||||
def delete(self, ref: ArtifactRef) -> bool:
|
||||
"""
|
||||
Delete an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
|
||||
Returns:
|
||||
True if deleted successfully
|
||||
"""
|
||||
...
|
||||
|
||||
def list_artifacts(
|
||||
self,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
tool_name: Optional[str] = None,
|
||||
) -> List[ArtifactRef]:
|
||||
"""
|
||||
List artifacts matching filters.
|
||||
|
||||
Args:
|
||||
run_id: Filter by run ID
|
||||
agent_id: Filter by agent ID
|
||||
tool_name: Filter by tool name
|
||||
|
||||
Returns:
|
||||
List of matching ArtifactRef objects
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class HistoryStoreProtocol(Protocol):
|
||||
"""
|
||||
Protocol for conversation history storage.
|
||||
|
||||
Enables loss recovery during summarization by persisting
|
||||
full conversation history to artifacts.
|
||||
"""
|
||||
|
||||
def append(
|
||||
self,
|
||||
message: Dict[str, Any],
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Append a message to history.
|
||||
|
||||
Args:
|
||||
message: The message dict (role, content, etc.)
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
"""
|
||||
...
|
||||
|
||||
def get_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
|
||||
"""
|
||||
Get artifact reference for history file.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef if history exists, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
max_results: int = 20,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search history for matching messages.
|
||||
|
||||
Args:
|
||||
query: Search query (supports regex)
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
max_results: Maximum results to return
|
||||
|
||||
Returns:
|
||||
List of matching message dicts
|
||||
"""
|
||||
...
|
||||
|
||||
def get_messages(
|
||||
self,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
start_turn: int = 0,
|
||||
end_turn: Optional[int] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get messages from history.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
start_turn: Starting turn number
|
||||
end_turn: Ending turn number (None for all)
|
||||
|
||||
Returns:
|
||||
List of message dicts
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TerminalLoggerProtocol(Protocol):
|
||||
"""
|
||||
Protocol for terminal session logging.
|
||||
|
||||
Captures shell command outputs to artifacts for later retrieval.
|
||||
"""
|
||||
|
||||
def log_command(
|
||||
self,
|
||||
command: str,
|
||||
output: str,
|
||||
exit_code: int,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
) -> ArtifactRef:
|
||||
"""
|
||||
Log a command execution.
|
||||
|
||||
Args:
|
||||
command: The command that was executed
|
||||
output: Combined stdout/stderr output
|
||||
exit_code: Command exit code
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef to the logged output
|
||||
"""
|
||||
...
|
||||
|
||||
def get_session_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
|
||||
"""
|
||||
Get artifact reference for terminal session log.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef if session exists, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def tail_session(
|
||||
self,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
lines: int = 100,
|
||||
) -> str:
|
||||
"""
|
||||
Get last N lines from terminal session.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the last N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def grep_session(
|
||||
self,
|
||||
pattern: str,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
max_matches: int = 50,
|
||||
) -> List[GrepMatch]:
|
||||
"""
|
||||
Search terminal session for pattern.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern to search for
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
max_matches: Maximum matches to return
|
||||
|
||||
Returns:
|
||||
List of GrepMatch objects
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueConfig:
|
||||
"""
|
||||
Configuration for tool output queuing.
|
||||
|
||||
Controls when and how large tool outputs are queued to artifacts
|
||||
instead of being included inline in the context.
|
||||
"""
|
||||
enabled: bool = True
|
||||
inline_max_bytes: int = 32 * 1024 # 32KB default
|
||||
redact_secrets: bool = True
|
||||
summary_max_chars: int = 200
|
||||
checksum_algorithm: str = "sha256"
|
||||
|
||||
# Secret patterns to redact (regex)
|
||||
secret_patterns: List[str] = field(default_factory=lambda: [
|
||||
r'(?i)(api[_-]?key|apikey)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(secret|password|passwd|pwd)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(token|bearer)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(auth|authorization)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'sk-[a-zA-Z0-9]{20,}', # OpenAI keys
|
||||
r'ghp_[a-zA-Z0-9]{36}', # GitHub tokens
|
||||
r'gho_[a-zA-Z0-9]{36}', # GitHub OAuth
|
||||
r'xox[baprs]-[a-zA-Z0-9\-]+', # Slack tokens
|
||||
])
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"inline_max_bytes": self.inline_max_bytes,
|
||||
"redact_secrets": self.redact_secrets,
|
||||
"summary_max_chars": self.summary_max_chars,
|
||||
"checksum_algorithm": self.checksum_algorithm,
|
||||
"secret_patterns": self.secret_patterns,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "QueueConfig":
|
||||
return cls(
|
||||
enabled=data.get("enabled", True),
|
||||
inline_max_bytes=data.get("inline_max_bytes", 32 * 1024),
|
||||
redact_secrets=data.get("redact_secrets", True),
|
||||
summary_max_chars=data.get("summary_max_chars", 200),
|
||||
checksum_algorithm=data.get("checksum_algorithm", "sha256"),
|
||||
secret_patterns=data.get("secret_patterns", cls().secret_patterns),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def compute_checksum(content: Union[str, bytes], algorithm: str = "sha256") -> str:
|
||||
"""
|
||||
Compute checksum of content.
|
||||
|
||||
Args:
|
||||
content: String or bytes to hash
|
||||
algorithm: Hash algorithm (sha256, md5, etc.)
|
||||
|
||||
Returns:
|
||||
Hex digest of the hash
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
||||
hasher = hashlib.new(algorithm)
|
||||
hasher.update(content)
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def generate_summary(content: Any, max_chars: int = 200) -> str:
|
||||
"""
|
||||
Generate a brief summary of content.
|
||||
|
||||
Args:
|
||||
content: Content to summarize
|
||||
max_chars: Maximum characters in summary
|
||||
|
||||
Returns:
|
||||
Brief summary string
|
||||
"""
|
||||
if isinstance(content, (dict, list)):
|
||||
# For structured data, describe the structure
|
||||
if isinstance(content, dict):
|
||||
keys = list(content.keys())[:5]
|
||||
key_str = ", ".join(str(k) for k in keys)
|
||||
if len(content) > 5:
|
||||
key_str += f", ... ({len(content)} total keys)"
|
||||
return f"Dict with keys: {key_str}"[:max_chars]
|
||||
else:
|
||||
if len(content) > 0:
|
||||
first_type = type(content[0]).__name__
|
||||
return f"List of {len(content)} {first_type} items"[:max_chars]
|
||||
return "Empty list"
|
||||
|
||||
# For strings, take first N chars
|
||||
text = str(content)
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[:max_chars - 3] + "..."
|
||||
@@ -68,8 +68,8 @@ class LLM:
|
||||
MODEL_WINDOWS = {
|
||||
# OpenAI
|
||||
"gpt-4": 6144, # 8,192 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-4o-mini": 96000, # 128,000 actual
|
||||
"gpt-4o-mini": 96000, # 128,000 actual
|
||||
"gpt-4-turbo": 96000, # 128,000 actual
|
||||
"o1-preview": 96000, # 128,000 actual
|
||||
"o1-mini": 96000, # 128,000 actual
|
||||
@@ -1430,6 +1430,8 @@ Now provide your final answer using this result."""
|
||||
task_id: Optional[str] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
stream: bool = True,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""Enhanced get_response with all OpenAI-like features"""
|
||||
@@ -1489,6 +1491,19 @@ Now provide your final answer using this result."""
|
||||
# Disable litellm debug messages
|
||||
litellm.set_verbose = False
|
||||
|
||||
# Setup StreamEvent emission if enabled
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
_first_token_emitted = False
|
||||
_last_content_time = None
|
||||
_request_start_perf = time.perf_counter()
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=_request_start_perf,
|
||||
metadata={"model": self.model, "provider": "litellm"}
|
||||
))
|
||||
|
||||
# Format tools if provided
|
||||
formatted_tools = self._format_tools_for_litellm(tools)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class ModelRouter:
|
||||
DEFAULT_MODELS = [
|
||||
# Lightweight/cheap models for simple tasks
|
||||
ModelProfile(
|
||||
name="gpt-5-nano",
|
||||
name="gpt-4o-mini",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
|
||||
cost_per_1k_tokens=0.00075, # Average of $0.00015 input, $0.0006 output
|
||||
@@ -80,7 +80,7 @@ class ModelRouter:
|
||||
|
||||
# Mid-tier models for moderate complexity
|
||||
ModelProfile(
|
||||
name="gpt-5-nano",
|
||||
name="gpt-4o-mini",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
|
||||
cost_per_1k_tokens=0.0075, # Average of $0.0025 input, $0.01 output
|
||||
@@ -145,7 +145,7 @@ class ModelRouter:
|
||||
preferred_providers: List of preferred providers in order
|
||||
"""
|
||||
self.models = models or self.DEFAULT_MODELS
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.cost_threshold = cost_threshold
|
||||
self.preferred_providers = preferred_providers or []
|
||||
|
||||
|
||||
@@ -582,6 +582,8 @@ class OpenAIClient:
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
@@ -596,11 +598,18 @@ class OpenAIClient:
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
stream_callback: Optional callback for StreamEvent emission
|
||||
emit_events: Whether to emit StreamEvents (requires stream_callback)
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
# Lazy import StreamEvent types only when needed
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
@@ -608,6 +617,15 @@ class OpenAIClient:
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Emit REQUEST_START event
|
||||
request_start_perf = time.perf_counter()
|
||||
if _emit:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=request_start_perf,
|
||||
metadata={"model": model, "message_count": len(messages)}
|
||||
))
|
||||
|
||||
# Create the response stream
|
||||
response_stream = self.sync_client.chat.completions.create(
|
||||
model=model,
|
||||
@@ -615,29 +633,81 @@ class OpenAIClient:
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
stream_options={"include_usage": True} if _emit else None,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Emit HEADERS_RECEIVED event (stream object created means headers received)
|
||||
if _emit:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.HEADERS_RECEIVED,
|
||||
timestamp=time.perf_counter()
|
||||
))
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
first_token_emitted = False
|
||||
last_content_time = None
|
||||
ttft_logged = False
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
refresh_per_second=10, # Increased for more responsive streaming
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
|
||||
# Check for content delta
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
full_response_text += content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (last_content_time - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
|
||||
# Emit FIRST_TOKEN on first content
|
||||
if _emit and not first_token_emitted:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
elif _emit:
|
||||
# Emit DELTA_TEXT for subsequent tokens
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
@@ -648,9 +718,60 @@ class OpenAIClient:
|
||||
# Clear the last generating display with a blank line
|
||||
console.print()
|
||||
else:
|
||||
# Just collect chunks without display
|
||||
# Just collect chunks without display, but still emit events
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (last_content_time - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
|
||||
if _emit and not first_token_emitted:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
else:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Emit LAST_TOKEN and STREAM_END events
|
||||
if _emit:
|
||||
if last_content_time:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.LAST_TOKEN,
|
||||
timestamp=last_content_time
|
||||
))
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.STREAM_END,
|
||||
timestamp=time.perf_counter(),
|
||||
metadata={"chunk_count": len(chunks)}
|
||||
))
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
@@ -669,6 +790,8 @@ class OpenAIClient:
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
@@ -683,11 +806,18 @@ class OpenAIClient:
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
stream_callback: Optional callback for StreamEvent emission (can be sync or async)
|
||||
emit_events: Whether to emit StreamEvents (requires stream_callback)
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
# Lazy import StreamEvent types only when needed
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
@@ -695,6 +825,19 @@ class OpenAIClient:
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Emit REQUEST_START event
|
||||
request_start_perf = time.perf_counter()
|
||||
if _emit:
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=request_start_perf,
|
||||
metadata={"model": model, "message_count": len(messages)}
|
||||
)
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
# Create the response stream
|
||||
response_stream = await self.async_client.chat.completions.create(
|
||||
model=model,
|
||||
@@ -702,29 +845,81 @@ class OpenAIClient:
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
stream_options={"include_usage": True} if _emit else None,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Emit HEADERS_RECEIVED event
|
||||
if _emit:
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.HEADERS_RECEIVED,
|
||||
timestamp=time.perf_counter()
|
||||
)
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
first_token_emitted = False
|
||||
last_content_time = None
|
||||
|
||||
async def emit_event(event: 'StreamEvent'):
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
refresh_per_second=10,
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
full_response_text += content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
if _emit and not first_token_emitted:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
elif _emit:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
@@ -738,6 +933,50 @@ class OpenAIClient:
|
||||
# Just collect chunks without display
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
if _emit and chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
if not first_token_emitted:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
else:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Emit LAST_TOKEN and STREAM_END events
|
||||
if _emit:
|
||||
if last_content_time:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.LAST_TOKEN,
|
||||
timestamp=last_content_time
|
||||
))
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.STREAM_END,
|
||||
timestamp=time.perf_counter(),
|
||||
metadata={"chunk_count": len(chunks)}
|
||||
))
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
@@ -749,7 +988,7 @@ class OpenAIClient:
|
||||
def create_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -794,7 +1033,7 @@ class OpenAIClient:
|
||||
async def acreate_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -839,7 +1078,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -904,7 +1143,9 @@ class OpenAIClient:
|
||||
if display_fn and console:
|
||||
# When verbose (display_fn provided), use streaming for better UX
|
||||
try:
|
||||
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
|
||||
request_start_perf = time.perf_counter()
|
||||
ttft_logged = False
|
||||
with Live(display_fn("", start_time), console=console, refresh_per_second=10, transient=True) as live:
|
||||
# Use streaming when display_fn is provided for progressive display
|
||||
response_stream = self.create_completion(
|
||||
messages=messages,
|
||||
@@ -922,6 +1163,11 @@ class OpenAIClient:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (time.perf_counter() - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
@@ -1020,7 +1266,7 @@ class OpenAIClient:
|
||||
async def achat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1204,7 +1450,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools_stream(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1356,7 +1602,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
@@ -1390,7 +1636,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
|
||||
@@ -162,7 +162,7 @@ class MCP:
|
||||
# Method 1: Using command and args separately
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP(
|
||||
command="/path/to/python",
|
||||
args=["/path/to/app.py"]
|
||||
@@ -172,14 +172,14 @@ class MCP:
|
||||
# Method 2: Using a single command string
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("/path/to/python /path/to/app.py")
|
||||
)
|
||||
|
||||
# Method 3: Using an SSE endpoint
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("http://localhost:8080/sse")
|
||||
)
|
||||
|
||||
@@ -548,7 +548,7 @@ class MCP:
|
||||
"""Convert the MCP tool to an OpenAI-compatible tool definition.
|
||||
|
||||
This method is specifically invoked by the Agent class when using
|
||||
provider/model format (e.g., "openai/gpt-5-nano").
|
||||
provider/model format (e.g., "openai/gpt-4o-mini").
|
||||
|
||||
Returns:
|
||||
dict or list: OpenAI-compatible tool definition(s)
|
||||
|
||||
@@ -110,6 +110,31 @@ def __getattr__(name):
|
||||
if name == "create_hooks_manager":
|
||||
from .hooks import create_hooks_manager
|
||||
return create_hooks_manager
|
||||
# Learn module
|
||||
if name == "LearnManager":
|
||||
from .learn import LearnManager
|
||||
return LearnManager
|
||||
if name == "PersonaStore":
|
||||
from .learn import PersonaStore
|
||||
return PersonaStore
|
||||
if name == "InsightStore":
|
||||
from .learn import InsightStore
|
||||
return InsightStore
|
||||
if name == "ThreadStore":
|
||||
from .learn import ThreadStore
|
||||
return ThreadStore
|
||||
if name == "PatternStore":
|
||||
from .learn import PatternStore
|
||||
return PatternStore
|
||||
if name == "DecisionStore":
|
||||
from .learn import DecisionStore
|
||||
return DecisionStore
|
||||
if name == "FeedbackStore":
|
||||
from .learn import FeedbackStore
|
||||
return FeedbackStore
|
||||
if name == "ImprovementStore":
|
||||
from .learn import ImprovementStore
|
||||
return ImprovementStore
|
||||
# DocsManager and MCPConfigManager are already imported at module level
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -153,4 +178,13 @@ __all__ = [
|
||||
# MCP config management
|
||||
"MCPConfigManager",
|
||||
"MCPConfig",
|
||||
# Learn module
|
||||
"LearnManager",
|
||||
"PersonaStore",
|
||||
"InsightStore",
|
||||
"ThreadStore",
|
||||
"PatternStore",
|
||||
"DecisionStore",
|
||||
"FeedbackStore",
|
||||
"ImprovementStore",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Learn Module - Continuous Learning within Memory System.
|
||||
|
||||
Provides learning capabilities for agents to capture patterns, preferences,
|
||||
and insights from interactions to improve future responses.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent, MemoryConfig, LearnConfig
|
||||
|
||||
# Simple enable
|
||||
agent = Agent(memory=MemoryConfig(learn=True))
|
||||
|
||||
# With specific capabilities
|
||||
agent = Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
)
|
||||
))
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"LearnManager",
|
||||
"PersonaStore",
|
||||
"InsightStore",
|
||||
"ThreadStore",
|
||||
"PatternStore",
|
||||
"DecisionStore",
|
||||
"FeedbackStore",
|
||||
"ImprovementStore",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy loading for learn module components."""
|
||||
if name == "LearnManager":
|
||||
from .manager import LearnManager
|
||||
return LearnManager
|
||||
if name == "PersonaStore":
|
||||
from .stores import PersonaStore
|
||||
return PersonaStore
|
||||
if name == "InsightStore":
|
||||
from .stores import InsightStore
|
||||
return InsightStore
|
||||
if name == "ThreadStore":
|
||||
from .stores import ThreadStore
|
||||
return ThreadStore
|
||||
if name == "PatternStore":
|
||||
from .stores import PatternStore
|
||||
return PatternStore
|
||||
if name == "DecisionStore":
|
||||
from .stores import DecisionStore
|
||||
return DecisionStore
|
||||
if name == "FeedbackStore":
|
||||
from .stores import FeedbackStore
|
||||
return FeedbackStore
|
||||
if name == "ImprovementStore":
|
||||
from .stores import ImprovementStore
|
||||
return ImprovementStore
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
LearnManager - Central manager for continuous learning capabilities.
|
||||
|
||||
Coordinates all learning stores and provides a unified interface for
|
||||
capturing and retrieving learned information.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ...config.feature_configs import LearnConfig, LearnScope
|
||||
from .stores import (
|
||||
PersonaStore,
|
||||
InsightStore,
|
||||
ThreadStore,
|
||||
PatternStore,
|
||||
DecisionStore,
|
||||
FeedbackStore,
|
||||
ImprovementStore,
|
||||
LearnEntry,
|
||||
)
|
||||
|
||||
|
||||
class LearnManager:
|
||||
"""
|
||||
Central manager for continuous learning within the memory system.
|
||||
|
||||
Coordinates all learning stores based on LearnConfig settings.
|
||||
|
||||
Usage:
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config, user_id="alice")
|
||||
|
||||
# Capture learning
|
||||
manager.capture_persona("User prefers concise responses")
|
||||
manager.capture_insight("User works in data science")
|
||||
|
||||
# Retrieve learnings
|
||||
context = manager.get_learning_context()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[LearnConfig] = None,
|
||||
user_id: Optional[str] = None,
|
||||
store_path: Optional[str] = None,
|
||||
):
|
||||
self.config = config or LearnConfig()
|
||||
self.user_id = user_id or "default"
|
||||
self.store_path = store_path or self.config.store_path
|
||||
|
||||
scope = self.config.scope
|
||||
if isinstance(scope, LearnScope):
|
||||
scope = scope.value
|
||||
|
||||
self._stores: Dict[str, Any] = {}
|
||||
|
||||
if self.config.persona:
|
||||
self._stores["persona"] = PersonaStore(
|
||||
store_path=self._get_store_path("persona"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.insights:
|
||||
self._stores["insights"] = InsightStore(
|
||||
store_path=self._get_store_path("insights"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.thread:
|
||||
self._stores["threads"] = ThreadStore(
|
||||
store_path=self._get_store_path("threads"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.patterns:
|
||||
self._stores["patterns"] = PatternStore(
|
||||
store_path=self._get_store_path("patterns"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.decisions:
|
||||
self._stores["decisions"] = DecisionStore(
|
||||
store_path=self._get_store_path("decisions"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.feedback:
|
||||
self._stores["feedback"] = FeedbackStore(
|
||||
store_path=self._get_store_path("feedback"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.improvements:
|
||||
self._stores["improvements"] = ImprovementStore(
|
||||
store_path=self._get_store_path("improvements"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
def _get_store_path(self, store_name: str) -> Optional[str]:
|
||||
"""Get path for a specific store."""
|
||||
if self.store_path:
|
||||
return f"{self.store_path}/{store_name}.json"
|
||||
return None
|
||||
|
||||
def capture_persona(self, content: str, category: str = "general") -> Optional[LearnEntry]:
|
||||
"""Capture user preference or profile information."""
|
||||
if "persona" in self._stores:
|
||||
return self._stores["persona"].add_preference(content, category)
|
||||
return None
|
||||
|
||||
def capture_insight(self, content: str, source: str = "interaction") -> Optional[LearnEntry]:
|
||||
"""Capture an observation or learning."""
|
||||
if "insights" in self._stores:
|
||||
return self._stores["insights"].add_insight(content, source)
|
||||
return None
|
||||
|
||||
def capture_thread(self, summary: str, thread_id: str) -> Optional[LearnEntry]:
|
||||
"""Capture thread/session context."""
|
||||
if "threads" in self._stores:
|
||||
return self._stores["threads"].add_thread_summary(summary, thread_id)
|
||||
return None
|
||||
|
||||
def capture_pattern(self, pattern: str, pattern_type: str = "general") -> Optional[LearnEntry]:
|
||||
"""Capture a reusable knowledge pattern."""
|
||||
if "patterns" in self._stores:
|
||||
return self._stores["patterns"].add_pattern(pattern, pattern_type)
|
||||
return None
|
||||
|
||||
def capture_decision(
|
||||
self,
|
||||
decision: str,
|
||||
reasoning: str = "",
|
||||
outcome: str = "",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture a decision record."""
|
||||
if "decisions" in self._stores:
|
||||
return self._stores["decisions"].add_decision(decision, reasoning, outcome)
|
||||
return None
|
||||
|
||||
def capture_feedback(
|
||||
self,
|
||||
feedback: str,
|
||||
rating: Optional[int] = None,
|
||||
source: str = "user",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture feedback."""
|
||||
if "feedback" in self._stores:
|
||||
return self._stores["feedback"].add_feedback(feedback, rating, source)
|
||||
return None
|
||||
|
||||
def capture_improvement(
|
||||
self,
|
||||
proposal: str,
|
||||
area: str = "general",
|
||||
priority: str = "medium",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture an improvement proposal."""
|
||||
if "improvements" in self._stores:
|
||||
return self._stores["improvements"].add_improvement(proposal, area, priority)
|
||||
return None
|
||||
|
||||
def get_learning_context(self, limit_per_store: int = 5) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Get learning context from all enabled stores.
|
||||
|
||||
Returns a dictionary with entries from each store, suitable for
|
||||
injection into agent context.
|
||||
"""
|
||||
context = {}
|
||||
for store_name, store in self._stores.items():
|
||||
entries = store.list_all(limit=limit_per_store)
|
||||
context[store_name] = [e.to_dict() for e in entries]
|
||||
return context
|
||||
|
||||
def search(self, query: str, limit: int = 10) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Search across all enabled stores."""
|
||||
results = {}
|
||||
for store_name, store in self._stores.items():
|
||||
entries = store.search(query, limit=limit)
|
||||
if entries:
|
||||
results[store_name] = [e.to_dict() for e in entries]
|
||||
return results
|
||||
|
||||
def get_persona_context(self, limit: int = 10) -> List[str]:
|
||||
"""Get persona entries as context strings."""
|
||||
if "persona" not in self._stores:
|
||||
return []
|
||||
entries = self._stores["persona"].list_all(limit=limit)
|
||||
return [e.content for e in entries]
|
||||
|
||||
def get_insights_context(self, limit: int = 10) -> List[str]:
|
||||
"""Get insight entries as context strings."""
|
||||
if "insights" not in self._stores:
|
||||
return []
|
||||
entries = self._stores["insights"].list_all(limit=limit)
|
||||
return [e.content for e in entries]
|
||||
|
||||
def clear_all(self) -> Dict[str, int]:
|
||||
"""Clear all stores."""
|
||||
cleared = {}
|
||||
for store_name, store in self._stores.items():
|
||||
cleared[store_name] = store.clear()
|
||||
return cleared
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
"""Get entry counts for all stores."""
|
||||
return {
|
||||
store_name: len(store._entries)
|
||||
for store_name, store in self._stores.items()
|
||||
}
|
||||
|
||||
def to_system_prompt_context(self) -> str:
|
||||
"""
|
||||
Generate context suitable for injection into system prompt.
|
||||
|
||||
Returns a formatted string with relevant learnings.
|
||||
"""
|
||||
parts = []
|
||||
|
||||
if "persona" in self._stores:
|
||||
personas = self.get_persona_context(limit=5)
|
||||
if personas:
|
||||
parts.append("User Preferences:")
|
||||
for p in personas:
|
||||
parts.append(f"- {p}")
|
||||
|
||||
if "insights" in self._stores:
|
||||
insights = self.get_insights_context(limit=5)
|
||||
if insights:
|
||||
parts.append("\nLearned Insights:")
|
||||
for i in insights:
|
||||
parts.append(f"- {i}")
|
||||
|
||||
return "\n".join(parts) if parts else ""
|
||||
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Learning Stores - Storage backends for different learning capabilities.
|
||||
|
||||
Each store handles a specific type of learning data:
|
||||
- PersonaStore: User preferences and profile
|
||||
- InsightStore: Observations and learnings
|
||||
- ThreadStore: Session/conversation context
|
||||
- PatternStore: Reusable knowledge patterns
|
||||
- DecisionStore: Decision logging
|
||||
- FeedbackStore: Outcome signals
|
||||
- ImprovementStore: Self-improvement proposals
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearnEntry:
|
||||
"""Base entry for all learning stores."""
|
||||
id: str
|
||||
content: str
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"content": self.content,
|
||||
"metadata": self.metadata,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "LearnEntry":
|
||||
return cls(
|
||||
id=data["id"],
|
||||
content=data["content"],
|
||||
metadata=data.get("metadata", {}),
|
||||
created_at=data.get("created_at", datetime.utcnow().isoformat()),
|
||||
updated_at=data.get("updated_at", datetime.utcnow().isoformat()),
|
||||
)
|
||||
|
||||
|
||||
class BaseStore(ABC):
|
||||
"""Abstract base class for learning stores."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store_path: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
scope: str = "private",
|
||||
):
|
||||
self.user_id = user_id or "default"
|
||||
self.scope = scope
|
||||
self.store_path = store_path or self._default_path()
|
||||
self._entries: Dict[str, LearnEntry] = {}
|
||||
self._load()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def store_name(self) -> str:
|
||||
"""Name of the store (used for file naming)."""
|
||||
pass
|
||||
|
||||
def _default_path(self) -> str:
|
||||
"""Default storage path."""
|
||||
base = Path.home() / ".praison" / "learn" / self.scope / self.user_id
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return str(base / f"{self.store_name}.json")
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Load entries from storage."""
|
||||
if os.path.exists(self.store_path):
|
||||
try:
|
||||
with open(self.store_path, "r") as f:
|
||||
data = json.load(f)
|
||||
self._entries = {
|
||||
k: LearnEntry.from_dict(v) for k, v in data.items()
|
||||
}
|
||||
except (json.JSONDecodeError, IOError):
|
||||
self._entries = {}
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Save entries to storage."""
|
||||
os.makedirs(os.path.dirname(self.store_path), exist_ok=True)
|
||||
with open(self.store_path, "w") as f:
|
||||
json.dump(
|
||||
{k: v.to_dict() for k, v in self._entries.items()},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
def add(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> LearnEntry:
|
||||
"""Add a new entry."""
|
||||
entry_id = f"{self.store_name}_{len(self._entries)}_{datetime.utcnow().timestamp()}"
|
||||
entry = LearnEntry(
|
||||
id=entry_id,
|
||||
content=content,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
self._entries[entry_id] = entry
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def get(self, entry_id: str) -> Optional[LearnEntry]:
|
||||
"""Get entry by ID."""
|
||||
return self._entries.get(entry_id)
|
||||
|
||||
def search(self, query: str, limit: int = 10) -> List[LearnEntry]:
|
||||
"""Simple text search (can be overridden for semantic search)."""
|
||||
query_lower = query.lower()
|
||||
results = [
|
||||
entry for entry in self._entries.values()
|
||||
if query_lower in entry.content.lower()
|
||||
]
|
||||
return results[:limit]
|
||||
|
||||
def list_all(self, limit: int = 100) -> List[LearnEntry]:
|
||||
"""List all entries."""
|
||||
entries = list(self._entries.values())
|
||||
entries.sort(key=lambda x: x.updated_at, reverse=True)
|
||||
return entries[:limit]
|
||||
|
||||
def update(self, entry_id: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[LearnEntry]:
|
||||
"""Update an existing entry."""
|
||||
if entry_id not in self._entries:
|
||||
return None
|
||||
entry = self._entries[entry_id]
|
||||
entry.content = content
|
||||
if metadata:
|
||||
entry.metadata.update(metadata)
|
||||
entry.updated_at = datetime.utcnow().isoformat()
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def delete(self, entry_id: str) -> bool:
|
||||
"""Delete an entry."""
|
||||
if entry_id in self._entries:
|
||||
del self._entries[entry_id]
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> int:
|
||||
"""Clear all entries."""
|
||||
count = len(self._entries)
|
||||
self._entries = {}
|
||||
self._save()
|
||||
return count
|
||||
|
||||
|
||||
class PersonaStore(BaseStore):
|
||||
"""Store for user preferences and profile information."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "persona"
|
||||
|
||||
def add_preference(self, preference: str, category: str = "general") -> LearnEntry:
|
||||
"""Add a user preference."""
|
||||
return self.add(preference, {"category": category, "type": "preference"})
|
||||
|
||||
def add_profile(self, profile_data: str, aspect: str = "general") -> LearnEntry:
|
||||
"""Add profile information."""
|
||||
return self.add(profile_data, {"aspect": aspect, "type": "profile"})
|
||||
|
||||
|
||||
class InsightStore(BaseStore):
|
||||
"""Store for observations and learnings."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "insights"
|
||||
|
||||
def add_insight(self, insight: str, source: str = "interaction") -> LearnEntry:
|
||||
"""Add an insight."""
|
||||
return self.add(insight, {"source": source, "type": "insight"})
|
||||
|
||||
def add_observation(self, observation: str, context: str = "") -> LearnEntry:
|
||||
"""Add an observation."""
|
||||
return self.add(observation, {"context": context, "type": "observation"})
|
||||
|
||||
|
||||
class ThreadStore(BaseStore):
|
||||
"""Store for session/conversation context."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "threads"
|
||||
|
||||
def add_thread_summary(self, summary: str, thread_id: str) -> LearnEntry:
|
||||
"""Add a thread summary."""
|
||||
return self.add(summary, {"thread_id": thread_id, "type": "summary"})
|
||||
|
||||
def add_context(self, context: str, thread_id: str) -> LearnEntry:
|
||||
"""Add thread context."""
|
||||
return self.add(context, {"thread_id": thread_id, "type": "context"})
|
||||
|
||||
|
||||
class PatternStore(BaseStore):
|
||||
"""Store for reusable knowledge patterns."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "patterns"
|
||||
|
||||
def add_pattern(self, pattern: str, pattern_type: str = "general") -> LearnEntry:
|
||||
"""Add a knowledge pattern."""
|
||||
return self.add(pattern, {"pattern_type": pattern_type, "type": "pattern"})
|
||||
|
||||
|
||||
class DecisionStore(BaseStore):
|
||||
"""Store for decision logging."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "decisions"
|
||||
|
||||
def add_decision(
|
||||
self,
|
||||
decision: str,
|
||||
reasoning: str = "",
|
||||
outcome: str = "",
|
||||
) -> LearnEntry:
|
||||
"""Add a decision record."""
|
||||
return self.add(decision, {
|
||||
"reasoning": reasoning,
|
||||
"outcome": outcome,
|
||||
"type": "decision",
|
||||
})
|
||||
|
||||
|
||||
class FeedbackStore(BaseStore):
|
||||
"""Store for outcome signals and feedback."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "feedback"
|
||||
|
||||
def add_feedback(
|
||||
self,
|
||||
feedback: str,
|
||||
rating: Optional[int] = None,
|
||||
source: str = "user",
|
||||
) -> LearnEntry:
|
||||
"""Add feedback."""
|
||||
metadata = {"source": source, "type": "feedback"}
|
||||
if rating is not None:
|
||||
metadata["rating"] = rating
|
||||
return self.add(feedback, metadata)
|
||||
|
||||
|
||||
class ImprovementStore(BaseStore):
|
||||
"""Store for self-improvement proposals."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "improvements"
|
||||
|
||||
def add_improvement(
|
||||
self,
|
||||
proposal: str,
|
||||
area: str = "general",
|
||||
priority: str = "medium",
|
||||
) -> LearnEntry:
|
||||
"""Add an improvement proposal."""
|
||||
return self.add(proposal, {
|
||||
"area": area,
|
||||
"priority": priority,
|
||||
"type": "improvement",
|
||||
})
|
||||
@@ -109,7 +109,7 @@ class Memory:
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {"model": "gpt-5-nano", "api_key": "..."}
|
||||
"config": {"model": "gpt-4o-mini", "api_key": "..."}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
@@ -146,6 +146,8 @@ class Memory:
|
||||
self.use_rag = (self.provider.lower() == "rag") and CHROMADB_AVAILABLE and self.cfg.get("use_embedding", False)
|
||||
self.use_mongodb = (self.provider.lower() == "mongodb") and PYMONGO_AVAILABLE
|
||||
self.graph_enabled = False # Initialize graph support flag
|
||||
self._learn_manager = None # Lazy-loaded LearnManager
|
||||
self._learn_config = self.cfg.get("learn", None) # Learn configuration
|
||||
|
||||
# Extract embedding model from config
|
||||
self.embedder_config = self.cfg.get("embedder", {})
|
||||
@@ -1442,7 +1444,7 @@ class Memory:
|
||||
import litellm
|
||||
|
||||
# Convert model name if it's in litellm format
|
||||
model_name = llm or "gpt-5-nano"
|
||||
model_name = llm or "gpt-4o-mini"
|
||||
|
||||
response = litellm.completion(
|
||||
model=model_name,
|
||||
@@ -1459,7 +1461,7 @@ class Memory:
|
||||
client = OpenAI()
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=llm or "gpt-5-nano",
|
||||
model=llm or "gpt-4o-mini",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": custom_prompt or default_prompt
|
||||
@@ -1599,3 +1601,50 @@ class Memory:
|
||||
except Exception as e:
|
||||
self._log_verbose(f"Error getting all memories: {e}", logging.ERROR)
|
||||
return []
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Learn Integration
|
||||
# -------------------------------------------------------------------------
|
||||
@property
|
||||
def learn(self):
|
||||
"""
|
||||
Get the LearnManager for continuous learning capabilities.
|
||||
|
||||
Returns None if learn is not enabled in config.
|
||||
|
||||
Usage:
|
||||
memory = Memory({"learn": True})
|
||||
memory.learn.capture_persona("User prefers concise responses")
|
||||
memory.learn.capture_insight("User works in data science")
|
||||
"""
|
||||
if self._learn_manager is not None:
|
||||
return self._learn_manager
|
||||
|
||||
if self._learn_config is None or self._learn_config is False:
|
||||
return None
|
||||
|
||||
from .learn import LearnManager
|
||||
from ..config.feature_configs import LearnConfig
|
||||
|
||||
if self._learn_config is True:
|
||||
config = LearnConfig()
|
||||
elif isinstance(self._learn_config, dict):
|
||||
config = LearnConfig(**self._learn_config)
|
||||
elif isinstance(self._learn_config, LearnConfig):
|
||||
config = self._learn_config
|
||||
else:
|
||||
return None
|
||||
|
||||
user_id = self.cfg.get("user_id", "default")
|
||||
self._learn_manager = LearnManager(config=config, user_id=user_id)
|
||||
return self._learn_manager
|
||||
|
||||
def get_learn_context(self) -> str:
|
||||
"""
|
||||
Get learning context suitable for injection into system prompt.
|
||||
|
||||
Returns empty string if learn is not enabled.
|
||||
"""
|
||||
if self.learn is None:
|
||||
return ""
|
||||
return self.learn.to_system_prompt_context()
|
||||
@@ -722,9 +722,8 @@ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
|
||||
goal="Manage the entire flow of tasks and delegate them to the right agent",
|
||||
backstory="Expert project manager to coordinate tasks among agents",
|
||||
llm=self.manager_llm,
|
||||
verbose=self.verbose,
|
||||
markdown=True,
|
||||
self_reflect=False
|
||||
output={"verbose": self.verbose, "markdown": True},
|
||||
reflection=False
|
||||
)
|
||||
|
||||
class ManagerInstructions(BaseModel):
|
||||
@@ -1384,9 +1383,8 @@ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
|
||||
goal="Manage the entire flow of tasks and delegate them to the right agent",
|
||||
backstory="Expert project manager to coordinate tasks among agents",
|
||||
llm=self.manager_llm,
|
||||
verbose=self.verbose,
|
||||
markdown=True,
|
||||
self_reflect=False
|
||||
output={"verbose": self.verbose, "markdown": True},
|
||||
reflection=False
|
||||
)
|
||||
|
||||
class ManagerInstructions(BaseModel):
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
"""
|
||||
PraisonAI Agents Core Profiling Module
|
||||
|
||||
Lightweight, protocol-based profiling for the core SDK.
|
||||
Zero overhead when disabled. Opt-in only.
|
||||
|
||||
Features:
|
||||
- Pre-LLM latency breakdown
|
||||
- LLM client initialization timing
|
||||
- Tool resolution timing
|
||||
- Message serialization timing
|
||||
- Streaming TTFT tracking
|
||||
- Import timing
|
||||
- Async-safe
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
# Enable profiling
|
||||
Profiler.enable()
|
||||
|
||||
# Profile a block
|
||||
with profile_block("my_operation"):
|
||||
do_something()
|
||||
|
||||
# Get report
|
||||
Profiler.report()
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
import threading
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Dict, List, Optional, Any, Callable
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import json
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Classes
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class TimingRecord:
|
||||
"""Record of a single timing measurement."""
|
||||
name: str
|
||||
duration_ms: float
|
||||
category: str = "block"
|
||||
parent: str = ""
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamingRecord:
|
||||
"""Record of streaming operation."""
|
||||
name: str
|
||||
ttft_ms: float # Time to first token
|
||||
total_ms: float
|
||||
chunk_count: int = 0
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreLLMBreakdown:
|
||||
"""Breakdown of pre-LLM latency phases."""
|
||||
agent_init_ms: float = 0.0
|
||||
llm_client_init_ms: float = 0.0
|
||||
tool_resolution_ms: float = 0.0
|
||||
message_build_ms: float = 0.0
|
||||
context_management_ms: float = 0.0
|
||||
knowledge_retrieval_ms: float = 0.0
|
||||
guardrail_check_ms: float = 0.0
|
||||
total_pre_llm_ms: float = 0.0
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Streaming Tracker
|
||||
# ============================================================================
|
||||
|
||||
class StreamingTracker:
|
||||
"""Track streaming operations for TTFT metrics."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self._start_time: Optional[float] = None
|
||||
self._first_token_time: Optional[float] = None
|
||||
self._end_time: Optional[float] = None
|
||||
self._chunk_count: int = 0
|
||||
|
||||
def start(self) -> None:
|
||||
self._start_time = time.perf_counter()
|
||||
|
||||
def first_token(self) -> None:
|
||||
if self._first_token_time is None:
|
||||
self._first_token_time = time.perf_counter()
|
||||
|
||||
def chunk(self) -> None:
|
||||
self._chunk_count += 1
|
||||
|
||||
def end(self) -> None:
|
||||
self._end_time = time.perf_counter()
|
||||
if self._start_time is not None:
|
||||
ttft_ms = 0.0
|
||||
if self._first_token_time is not None:
|
||||
ttft_ms = (self._first_token_time - self._start_time) * 1000
|
||||
total_ms = (self._end_time - self._start_time) * 1000
|
||||
Profiler.record_streaming(
|
||||
name=self.name,
|
||||
ttft_ms=ttft_ms,
|
||||
total_ms=total_ms,
|
||||
chunk_count=self._chunk_count
|
||||
)
|
||||
|
||||
@property
|
||||
def ttft_ms(self) -> float:
|
||||
if self._start_time and self._first_token_time:
|
||||
return (self._first_token_time - self._start_time) * 1000
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def elapsed_ms(self) -> float:
|
||||
if self._start_time:
|
||||
end = self._end_time or time.perf_counter()
|
||||
return (end - self._start_time) * 1000
|
||||
return 0.0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Core Profiler
|
||||
# ============================================================================
|
||||
|
||||
class Profiler:
|
||||
"""
|
||||
Lightweight profiler for PraisonAI Agents core SDK.
|
||||
|
||||
Thread-safe singleton with zero overhead when disabled.
|
||||
"""
|
||||
|
||||
_lock = threading.Lock()
|
||||
_enabled: bool = False
|
||||
_timings: List[TimingRecord] = []
|
||||
_streaming: List[StreamingRecord] = []
|
||||
_pre_llm_breakdown: Optional[PreLLMBreakdown] = None
|
||||
_current_phases: Dict[str, float] = {} # Track in-progress phases
|
||||
_phase_stack: List[str] = [] # Track nested phases
|
||||
|
||||
@classmethod
|
||||
def enable(cls) -> None:
|
||||
"""Enable profiling."""
|
||||
cls._enabled = True
|
||||
|
||||
@classmethod
|
||||
def disable(cls) -> None:
|
||||
"""Disable profiling."""
|
||||
cls._enabled = False
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
"""Check if profiling is enabled."""
|
||||
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
"""Clear all profiling data."""
|
||||
with cls._lock:
|
||||
cls._timings.clear()
|
||||
cls._streaming.clear()
|
||||
cls._pre_llm_breakdown = None
|
||||
cls._current_phases.clear()
|
||||
cls._phase_stack.clear()
|
||||
|
||||
@classmethod
|
||||
def record_timing(cls, name: str, duration_ms: float, category: str = "block",
|
||||
parent: str = "", metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Record a timing measurement."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._timings.append(TimingRecord(
|
||||
name=name,
|
||||
duration_ms=duration_ms,
|
||||
category=category,
|
||||
parent=parent,
|
||||
metadata=metadata or {}
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def record_streaming(cls, name: str, ttft_ms: float, total_ms: float,
|
||||
chunk_count: int = 0) -> None:
|
||||
"""Record streaming metrics."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._streaming.append(StreamingRecord(
|
||||
name=name,
|
||||
ttft_ms=ttft_ms,
|
||||
total_ms=total_ms,
|
||||
chunk_count=chunk_count
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def start_phase(cls, phase_name: str) -> None:
|
||||
"""Start timing a phase (for pre-LLM breakdown)."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._current_phases[phase_name] = time.perf_counter()
|
||||
cls._phase_stack.append(phase_name)
|
||||
|
||||
@classmethod
|
||||
def end_phase(cls, phase_name: str) -> float:
|
||||
"""End timing a phase and return duration in ms."""
|
||||
if not cls.is_enabled():
|
||||
return 0.0
|
||||
with cls._lock:
|
||||
start = cls._current_phases.pop(phase_name, None)
|
||||
if phase_name in cls._phase_stack:
|
||||
cls._phase_stack.remove(phase_name)
|
||||
if start is None:
|
||||
return 0.0
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
# Record as timing
|
||||
parent = cls._phase_stack[-1] if cls._phase_stack else ""
|
||||
cls._timings.append(TimingRecord(
|
||||
name=phase_name,
|
||||
duration_ms=duration_ms,
|
||||
category="phase",
|
||||
parent=parent
|
||||
))
|
||||
return duration_ms
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def phase(cls, name: str, category: str = "phase"):
|
||||
"""Context manager for timing a phase."""
|
||||
if not cls.is_enabled():
|
||||
yield
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
parent = ""
|
||||
with cls._lock:
|
||||
parent = cls._phase_stack[-1] if cls._phase_stack else ""
|
||||
cls._phase_stack.append(name)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
with cls._lock:
|
||||
if name in cls._phase_stack:
|
||||
cls._phase_stack.remove(name)
|
||||
cls._timings.append(TimingRecord(
|
||||
name=name,
|
||||
duration_ms=duration_ms,
|
||||
category=category,
|
||||
parent=parent
|
||||
))
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def streaming(cls, name: str):
|
||||
"""Context manager for streaming profiling."""
|
||||
tracker = StreamingTracker(name)
|
||||
tracker.start()
|
||||
try:
|
||||
yield tracker
|
||||
finally:
|
||||
tracker.end()
|
||||
|
||||
@classmethod
|
||||
def set_pre_llm_breakdown(cls, breakdown: PreLLMBreakdown) -> None:
|
||||
"""Set the pre-LLM breakdown data."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._pre_llm_breakdown = breakdown
|
||||
|
||||
@classmethod
|
||||
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
|
||||
"""Get timing records."""
|
||||
with cls._lock:
|
||||
if category:
|
||||
return [t for t in cls._timings if t.category == category]
|
||||
return cls._timings.copy()
|
||||
|
||||
@classmethod
|
||||
def get_streaming_records(cls) -> List[StreamingRecord]:
|
||||
"""Get streaming records."""
|
||||
with cls._lock:
|
||||
return cls._streaming.copy()
|
||||
|
||||
@classmethod
|
||||
def get_pre_llm_breakdown(cls) -> Optional[PreLLMBreakdown]:
|
||||
"""Get pre-LLM breakdown."""
|
||||
with cls._lock:
|
||||
return cls._pre_llm_breakdown
|
||||
|
||||
@classmethod
|
||||
def get_summary(cls) -> Dict[str, Any]:
|
||||
"""Get profiling summary."""
|
||||
with cls._lock:
|
||||
total_time = sum(t.duration_ms for t in cls._timings)
|
||||
|
||||
# Group by category
|
||||
by_category: Dict[str, float] = {}
|
||||
for t in cls._timings:
|
||||
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
|
||||
|
||||
# Top slowest
|
||||
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:15]
|
||||
|
||||
# Pre-LLM breakdown
|
||||
pre_llm = cls._pre_llm_breakdown.to_dict() if cls._pre_llm_breakdown else {}
|
||||
|
||||
# Streaming stats
|
||||
streaming_stats = []
|
||||
for s in cls._streaming:
|
||||
streaming_stats.append({
|
||||
'name': s.name,
|
||||
'ttft_ms': s.ttft_ms,
|
||||
'total_ms': s.total_ms,
|
||||
'chunk_count': s.chunk_count
|
||||
})
|
||||
|
||||
return {
|
||||
'total_time_ms': total_time,
|
||||
'timing_count': len(cls._timings),
|
||||
'by_category': by_category,
|
||||
'slowest_operations': [(s.name, s.duration_ms, s.category) for s in slowest],
|
||||
'pre_llm_breakdown': pre_llm,
|
||||
'streaming': streaming_stats
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def report(cls, output: str = "console") -> str:
|
||||
"""Generate and output profiling report."""
|
||||
summary = cls.get_summary()
|
||||
|
||||
lines = [
|
||||
"=" * 70,
|
||||
"PraisonAI Agents Core Profiling Report",
|
||||
"=" * 70,
|
||||
"",
|
||||
f"Total Time: {summary['total_time_ms']:.2f}ms",
|
||||
f"Timing Records: {summary['timing_count']}",
|
||||
"",
|
||||
]
|
||||
|
||||
# Pre-LLM Breakdown (key for debugging the 7s delay)
|
||||
if summary['pre_llm_breakdown']:
|
||||
lines.append("## Pre-LLM Latency Breakdown")
|
||||
lines.append("-" * 50)
|
||||
for phase, ms in summary['pre_llm_breakdown'].items():
|
||||
if ms > 0:
|
||||
lines.append(f" {phase:<30}: {ms:>10.2f} ms")
|
||||
lines.append("")
|
||||
|
||||
# By Category
|
||||
lines.append("## By Category")
|
||||
lines.append("-" * 50)
|
||||
for cat, time_ms in summary['by_category'].items():
|
||||
lines.append(f" {cat}: {time_ms:.2f}ms")
|
||||
lines.append("")
|
||||
|
||||
# Slowest Operations
|
||||
lines.append("## Slowest Operations")
|
||||
lines.append("-" * 50)
|
||||
for name, time_ms, cat in summary['slowest_operations']:
|
||||
lines.append(f" {name:<40}: {time_ms:>10.2f}ms [{cat}]")
|
||||
lines.append("")
|
||||
|
||||
# Streaming Stats
|
||||
if summary['streaming']:
|
||||
lines.append("## Streaming Metrics")
|
||||
lines.append("-" * 50)
|
||||
for s in summary['streaming']:
|
||||
lines.append(f" {s['name']}: TTFT={s['ttft_ms']:.2f}ms, Total={s['total_ms']:.2f}ms, Chunks={s['chunk_count']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 70)
|
||||
|
||||
report_text = "\n".join(lines)
|
||||
|
||||
if output == "console":
|
||||
print(report_text)
|
||||
|
||||
return report_text
|
||||
|
||||
@classmethod
|
||||
def export_json(cls) -> str:
|
||||
"""Export profiling data as JSON."""
|
||||
summary = cls.get_summary()
|
||||
with cls._lock:
|
||||
data = {
|
||||
'summary': summary,
|
||||
'timings': [asdict(t) for t in cls._timings],
|
||||
'streaming': [asdict(s) for s in cls._streaming],
|
||||
}
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Decorators
|
||||
# ============================================================================
|
||||
|
||||
def profile_func(func: Optional[Callable] = None, *, category: str = "function"):
|
||||
"""Decorator to profile a function."""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return fn(*args, **kwargs)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category)
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
|
||||
"""Decorator to profile an async function."""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return await fn(*args, **kwargs)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category)
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
@contextmanager
|
||||
def profile_block(name: str, category: str = "block"):
|
||||
"""Context manager for profiling a block of code."""
|
||||
if not Profiler.is_enabled():
|
||||
yield
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(name, duration_ms, category)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
'Profiler',
|
||||
'StreamingTracker',
|
||||
'TimingRecord',
|
||||
'StreamingRecord',
|
||||
'PreLLMBreakdown',
|
||||
'profile_func',
|
||||
'profile_async',
|
||||
'profile_block',
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
PraisonAI Streaming Module.
|
||||
|
||||
Provides the StreamEvent protocol for pass-through streaming from LLM providers.
|
||||
|
||||
This module is lazily loaded to ensure zero performance impact when streaming
|
||||
features are not used.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.streaming import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
create_text_printer_callback,
|
||||
)
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"StreamEvent",
|
||||
"StreamEventType",
|
||||
"StreamMetrics",
|
||||
"StreamEventEmitter",
|
||||
"StreamCallback",
|
||||
"AsyncStreamCallback",
|
||||
"create_text_printer_callback",
|
||||
"create_metrics_callback",
|
||||
"StreamLogger",
|
||||
"create_logging_callback",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy loading of streaming components."""
|
||||
if name in __all__:
|
||||
# Events module exports
|
||||
if name in (
|
||||
"StreamEvent", "StreamEventType", "StreamMetrics",
|
||||
"StreamEventEmitter", "StreamCallback", "AsyncStreamCallback",
|
||||
"create_text_printer_callback", "create_metrics_callback"
|
||||
):
|
||||
from .events import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
StreamCallback,
|
||||
AsyncStreamCallback,
|
||||
create_text_printer_callback,
|
||||
create_metrics_callback,
|
||||
)
|
||||
|
||||
_events_exports = {
|
||||
"StreamEvent": StreamEvent,
|
||||
"StreamEventType": StreamEventType,
|
||||
"StreamMetrics": StreamMetrics,
|
||||
"StreamEventEmitter": StreamEventEmitter,
|
||||
"StreamCallback": StreamCallback,
|
||||
"AsyncStreamCallback": AsyncStreamCallback,
|
||||
"create_text_printer_callback": create_text_printer_callback,
|
||||
"create_metrics_callback": create_metrics_callback,
|
||||
}
|
||||
return _events_exports.get(name)
|
||||
|
||||
# Logging module exports
|
||||
if name in ("StreamLogger", "create_logging_callback"):
|
||||
from .logging import StreamLogger, create_logging_callback
|
||||
|
||||
_logging_exports = {
|
||||
"StreamLogger": StreamLogger,
|
||||
"create_logging_callback": create_logging_callback,
|
||||
}
|
||||
return _logging_exports.get(name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
StreamEvent protocol for PraisonAI streaming.
|
||||
|
||||
This module defines the minimal streaming event protocol that both OpenAI SDK
|
||||
and LiteLLM streaming paths map their native chunks/events into.
|
||||
|
||||
Design principles:
|
||||
- Zero overhead when not used (lazy imports, no global state)
|
||||
- Multi-agent safe (per-agent callbacks, no shared mutable state)
|
||||
- Async-safe (separate sync/async callback interfaces)
|
||||
- Provider-agnostic (same events for OpenAI, LiteLLM, etc.)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Protocol
|
||||
import time
|
||||
|
||||
|
||||
class StreamEventType(Enum):
|
||||
"""Types of streaming events emitted during LLM response streaming."""
|
||||
|
||||
REQUEST_START = "request_start" # Before API call is made
|
||||
HEADERS_RECEIVED = "headers_received" # When HTTP 200 headers arrive
|
||||
FIRST_TOKEN = "first_token" # First content delta received (TTFT marker)
|
||||
DELTA_TEXT = "delta_text" # Text content delta
|
||||
DELTA_TOOL_CALL = "delta_tool_call" # Tool call delta (function name, args chunk)
|
||||
TOOL_CALL_END = "tool_call_end" # Tool call complete
|
||||
LAST_TOKEN = "last_token" # Final content delta
|
||||
STREAM_END = "stream_end" # Stream completed successfully
|
||||
ERROR = "error" # Error during streaming
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamEvent:
|
||||
"""
|
||||
A single streaming event emitted during LLM response streaming.
|
||||
|
||||
Attributes:
|
||||
type: The type of event (see StreamEventType)
|
||||
timestamp: High-precision timestamp (time.perf_counter())
|
||||
content: Text content for DELTA_TEXT events
|
||||
tool_call: Tool call data for DELTA_TOOL_CALL events
|
||||
metadata: Additional event-specific metadata
|
||||
error: Error message for ERROR events
|
||||
"""
|
||||
type: StreamEventType
|
||||
timestamp: float = field(default_factory=time.perf_counter)
|
||||
content: Optional[str] = None
|
||||
tool_call: Optional[Dict[str, Any]] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
parts = [f"StreamEvent({self.type.value}"]
|
||||
if self.content:
|
||||
preview = self.content[:20] + "..." if len(self.content) > 20 else self.content
|
||||
parts.append(f", content={preview!r}")
|
||||
if self.tool_call:
|
||||
parts.append(f", tool_call={self.tool_call.get('name', 'unknown')}")
|
||||
if self.error:
|
||||
parts.append(f", error={self.error!r}")
|
||||
parts.append(")")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
class StreamCallback(Protocol):
|
||||
"""Protocol for synchronous stream event callbacks."""
|
||||
|
||||
def __call__(self, event: StreamEvent) -> None:
|
||||
"""Called when a stream event is emitted."""
|
||||
...
|
||||
|
||||
|
||||
class AsyncStreamCallback(Protocol):
|
||||
"""Protocol for asynchronous stream event callbacks."""
|
||||
|
||||
async def __call__(self, event: StreamEvent) -> None:
|
||||
"""Called when a stream event is emitted (async)."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamMetrics:
|
||||
"""
|
||||
Timing metrics for a streaming response.
|
||||
|
||||
Tracks key timestamps to measure:
|
||||
- TTFT (Time To First Token): How long until first token arrives
|
||||
- Stream Duration: How long the token streaming takes
|
||||
- Total Time: End-to-end request time
|
||||
|
||||
All times are in seconds, measured with time.perf_counter() for precision.
|
||||
"""
|
||||
request_start: float = 0.0
|
||||
headers_received: float = 0.0
|
||||
first_token: float = 0.0
|
||||
last_token: float = 0.0
|
||||
stream_end: float = 0.0
|
||||
token_count: int = 0
|
||||
|
||||
@property
|
||||
def ttft(self) -> float:
|
||||
"""Time To First Token in seconds (first_token - request_start)."""
|
||||
if self.first_token and self.request_start:
|
||||
return self.first_token - self.request_start
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def stream_duration(self) -> float:
|
||||
"""Duration of token streaming in seconds (last_token - first_token)."""
|
||||
if self.last_token and self.first_token:
|
||||
return self.last_token - self.first_token
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def total_time(self) -> float:
|
||||
"""Total request time in seconds (stream_end - request_start)."""
|
||||
if self.stream_end and self.request_start:
|
||||
return self.stream_end - self.request_start
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def tokens_per_second(self) -> float:
|
||||
"""Token generation rate (tokens / stream_duration)."""
|
||||
if self.stream_duration > 0 and self.token_count > 0:
|
||||
return self.token_count / self.stream_duration
|
||||
return 0.0
|
||||
|
||||
def update_from_event(self, event: StreamEvent) -> None:
|
||||
"""Update metrics from a StreamEvent."""
|
||||
if event.type == StreamEventType.REQUEST_START:
|
||||
self.request_start = event.timestamp
|
||||
elif event.type == StreamEventType.HEADERS_RECEIVED:
|
||||
self.headers_received = event.timestamp
|
||||
elif event.type == StreamEventType.FIRST_TOKEN:
|
||||
self.first_token = event.timestamp
|
||||
self.token_count = 1
|
||||
elif event.type == StreamEventType.DELTA_TEXT:
|
||||
self.token_count += 1
|
||||
elif event.type == StreamEventType.LAST_TOKEN:
|
||||
self.last_token = event.timestamp
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
self.stream_end = event.timestamp
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert metrics to dictionary for logging/display."""
|
||||
return {
|
||||
"ttft_ms": round(self.ttft * 1000, 1),
|
||||
"stream_duration_ms": round(self.stream_duration * 1000, 1),
|
||||
"total_time_ms": round(self.total_time * 1000, 1),
|
||||
"token_count": self.token_count,
|
||||
"tokens_per_second": round(self.tokens_per_second, 1),
|
||||
}
|
||||
|
||||
def format_summary(self) -> str:
|
||||
"""Format metrics as a human-readable summary string."""
|
||||
return (
|
||||
f"TTFT: {self.ttft*1000:.0f}ms | "
|
||||
f"Stream: {self.stream_duration*1000:.0f}ms | "
|
||||
f"Total: {self.total_time*1000:.0f}ms | "
|
||||
f"Tokens: {self.token_count} ({self.tokens_per_second:.1f}/s)"
|
||||
)
|
||||
|
||||
|
||||
class StreamEventEmitter:
|
||||
"""
|
||||
Manages stream event callbacks for an agent or streaming session.
|
||||
|
||||
Thread-safe and multi-agent safe - each agent has its own emitter instance.
|
||||
Supports both sync and async callbacks.
|
||||
|
||||
Usage:
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.add_callback(my_callback)
|
||||
|
||||
# During streaming:
|
||||
emitter.emit(StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello"))
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._callbacks: List[StreamCallback] = []
|
||||
self._async_callbacks: List[AsyncStreamCallback] = []
|
||||
self._metrics: Optional[StreamMetrics] = None
|
||||
self._collect_metrics: bool = False
|
||||
|
||||
def add_callback(self, callback: StreamCallback) -> None:
|
||||
"""Add a synchronous callback to receive stream events."""
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def remove_callback(self, callback: StreamCallback) -> None:
|
||||
"""Remove a synchronous callback."""
|
||||
if callback in self._callbacks:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
def add_async_callback(self, callback: AsyncStreamCallback) -> None:
|
||||
"""Add an asynchronous callback to receive stream events."""
|
||||
self._async_callbacks.append(callback)
|
||||
|
||||
def remove_async_callback(self, callback: AsyncStreamCallback) -> None:
|
||||
"""Remove an asynchronous callback."""
|
||||
if callback in self._async_callbacks:
|
||||
self._async_callbacks.remove(callback)
|
||||
|
||||
def enable_metrics(self) -> None:
|
||||
"""Enable metrics collection for this streaming session."""
|
||||
self._collect_metrics = True
|
||||
self._metrics = StreamMetrics()
|
||||
|
||||
def get_metrics(self) -> Optional[StreamMetrics]:
|
||||
"""Get collected metrics (None if metrics not enabled)."""
|
||||
return self._metrics
|
||||
|
||||
def reset_metrics(self) -> None:
|
||||
"""Reset metrics for a new streaming session."""
|
||||
if self._collect_metrics:
|
||||
self._metrics = StreamMetrics()
|
||||
|
||||
def emit(self, event: StreamEvent) -> None:
|
||||
"""
|
||||
Emit a stream event to all registered callbacks.
|
||||
|
||||
This is the synchronous emission path. For async streaming,
|
||||
use emit_async().
|
||||
"""
|
||||
# Update metrics if enabled
|
||||
if self._collect_metrics and self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
# Call all sync callbacks
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(event)
|
||||
except Exception:
|
||||
# Don't let callback errors break streaming
|
||||
pass
|
||||
|
||||
async def emit_async(self, event: StreamEvent) -> None:
|
||||
"""
|
||||
Emit a stream event to all registered callbacks (async version).
|
||||
|
||||
Calls both sync and async callbacks.
|
||||
"""
|
||||
# Update metrics if enabled
|
||||
if self._collect_metrics and self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
# Call sync callbacks
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(event)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Call async callbacks
|
||||
for callback in self._async_callbacks:
|
||||
try:
|
||||
await callback(event)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@property
|
||||
def has_callbacks(self) -> bool:
|
||||
"""Check if any callbacks are registered."""
|
||||
return bool(self._callbacks or self._async_callbacks)
|
||||
|
||||
|
||||
def create_text_printer_callback(flush: bool = True) -> StreamCallback:
|
||||
"""
|
||||
Create a simple callback that prints text deltas to stdout.
|
||||
|
||||
Args:
|
||||
flush: Whether to flush stdout after each print (default True)
|
||||
|
||||
Returns:
|
||||
A StreamCallback that prints DELTA_TEXT content
|
||||
"""
|
||||
def printer(event: StreamEvent) -> None:
|
||||
if event.type == StreamEventType.DELTA_TEXT and event.content:
|
||||
print(event.content, end="", flush=flush)
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
print() # Newline at end
|
||||
|
||||
return printer
|
||||
|
||||
|
||||
def create_metrics_callback(metrics: StreamMetrics) -> StreamCallback:
|
||||
"""
|
||||
Create a callback that updates a StreamMetrics instance.
|
||||
|
||||
Args:
|
||||
metrics: The StreamMetrics instance to update
|
||||
|
||||
Returns:
|
||||
A StreamCallback that updates the metrics
|
||||
"""
|
||||
def updater(event: StreamEvent) -> None:
|
||||
metrics.update_from_event(event)
|
||||
|
||||
return updater
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Streaming logging utilities for PraisonAI.
|
||||
|
||||
Provides guarded logging for streaming events with clear timestamps
|
||||
for request_start, headers_received, first_token, last_token, and stream_end.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.streaming.logging import StreamLogger
|
||||
|
||||
logger = StreamLogger(verbose=True, metrics=True)
|
||||
logger.log_request_start(model="gpt-4o-mini")
|
||||
# ... streaming ...
|
||||
logger.log_first_token()
|
||||
# ... more tokens ...
|
||||
logger.log_stream_end()
|
||||
print(logger.get_metrics_summary())
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from .events import StreamMetrics, StreamEvent, StreamEventType
|
||||
|
||||
|
||||
class StreamLogger:
|
||||
"""
|
||||
Guarded logging for streaming events with timing metrics.
|
||||
|
||||
Only logs when verbose=True or metrics=True to ensure zero overhead
|
||||
when logging is disabled.
|
||||
|
||||
Attributes:
|
||||
verbose: Whether to log detailed streaming events
|
||||
metrics: Whether to collect and display timing metrics
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose: bool = False,
|
||||
metrics: bool = False,
|
||||
logger: Optional[logging.Logger] = None
|
||||
):
|
||||
self.verbose = verbose
|
||||
self.metrics = metrics
|
||||
self._logger = logger or logging.getLogger(__name__)
|
||||
self._metrics = StreamMetrics() if metrics else None
|
||||
self._enabled = verbose or metrics
|
||||
|
||||
def log_request_start(
|
||||
self,
|
||||
model: Optional[str] = None,
|
||||
message_count: Optional[int] = None
|
||||
) -> None:
|
||||
"""Log when the streaming request is initiated."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.request_start = timestamp
|
||||
|
||||
if self.verbose:
|
||||
extra = f" model={model}" if model else ""
|
||||
extra += f" messages={message_count}" if message_count else ""
|
||||
self._logger.info(f"[STREAM] REQUEST_START{extra}")
|
||||
|
||||
def log_headers_received(self) -> None:
|
||||
"""Log when HTTP response headers are received."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.headers_received = timestamp
|
||||
|
||||
if self.verbose:
|
||||
self._logger.info("[STREAM] HEADERS_RECEIVED")
|
||||
|
||||
def log_first_token(self, content: Optional[str] = None) -> None:
|
||||
"""Log when the first content token is received (TTFT marker)."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.first_token = timestamp
|
||||
self._metrics.token_count = 1
|
||||
|
||||
if self.verbose:
|
||||
preview = content[:20] + "..." if content and len(content) > 20 else content
|
||||
self._logger.info(f"[STREAM] FIRST_TOKEN content={preview!r}")
|
||||
|
||||
def log_delta_text(self, content: Optional[str] = None) -> None:
|
||||
"""Log a text delta (only when verbose, not for every token)."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self._metrics:
|
||||
self._metrics.token_count += 1
|
||||
|
||||
# Don't log every delta to avoid noise - just track metrics
|
||||
|
||||
def log_last_token(self) -> None:
|
||||
"""Log when the last content token is received."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.last_token = timestamp
|
||||
|
||||
if self.verbose:
|
||||
self._logger.info("[STREAM] LAST_TOKEN")
|
||||
|
||||
def log_stream_end(self, chunk_count: Optional[int] = None) -> None:
|
||||
"""Log when the stream ends."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.stream_end = timestamp
|
||||
|
||||
if self.verbose:
|
||||
extra = f" chunks={chunk_count}" if chunk_count else ""
|
||||
self._logger.info(f"[STREAM] STREAM_END{extra}")
|
||||
|
||||
def log_error(self, error: str) -> None:
|
||||
"""Log a streaming error."""
|
||||
if self.verbose:
|
||||
self._logger.error(f"[STREAM] ERROR: {error}")
|
||||
|
||||
def get_metrics(self) -> Optional[StreamMetrics]:
|
||||
"""Get the collected metrics (None if metrics not enabled)."""
|
||||
return self._metrics
|
||||
|
||||
def get_metrics_summary(self) -> str:
|
||||
"""Get a formatted summary of streaming metrics."""
|
||||
if not self._metrics:
|
||||
return "Metrics not enabled"
|
||||
return self._metrics.format_summary()
|
||||
|
||||
def get_metrics_dict(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get metrics as a dictionary."""
|
||||
if not self._metrics:
|
||||
return None
|
||||
return self._metrics.to_dict()
|
||||
|
||||
def update_from_event(self, event: StreamEvent) -> None:
|
||||
"""Update logger state from a StreamEvent."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
if self.verbose:
|
||||
if event.type == StreamEventType.REQUEST_START:
|
||||
self.log_request_start()
|
||||
elif event.type == StreamEventType.HEADERS_RECEIVED:
|
||||
self.log_headers_received()
|
||||
elif event.type == StreamEventType.FIRST_TOKEN:
|
||||
self.log_first_token(event.content)
|
||||
elif event.type == StreamEventType.LAST_TOKEN:
|
||||
self.log_last_token()
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
self.log_stream_end()
|
||||
elif event.type == StreamEventType.ERROR:
|
||||
self.log_error(event.error or "Unknown error")
|
||||
|
||||
|
||||
def create_logging_callback(
|
||||
verbose: bool = False,
|
||||
metrics: bool = False,
|
||||
logger: Optional[logging.Logger] = None
|
||||
) -> tuple:
|
||||
"""
|
||||
Create a StreamLogger and a callback function for use with streaming.
|
||||
|
||||
Args:
|
||||
verbose: Whether to log detailed events
|
||||
metrics: Whether to collect timing metrics
|
||||
logger: Optional custom logger
|
||||
|
||||
Returns:
|
||||
Tuple of (StreamLogger, callback_function)
|
||||
|
||||
Usage:
|
||||
stream_logger, callback = create_logging_callback(verbose=True, metrics=True)
|
||||
|
||||
# Pass callback to streaming method
|
||||
response = client.process_stream_response(
|
||||
...,
|
||||
stream_callback=callback,
|
||||
emit_events=True
|
||||
)
|
||||
|
||||
# After streaming, get metrics
|
||||
print(stream_logger.get_metrics_summary())
|
||||
"""
|
||||
stream_logger = StreamLogger(verbose=verbose, metrics=metrics, logger=logger)
|
||||
|
||||
def callback(event: StreamEvent) -> None:
|
||||
stream_logger.update_from_event(event)
|
||||
|
||||
return stream_logger, callback
|
||||
@@ -326,7 +326,7 @@ class Task:
|
||||
if hasattr(self.agent.llm_instance, 'model'):
|
||||
llm_model = self.agent.llm_instance.model
|
||||
else:
|
||||
llm_model = "gpt-5-nano" # Default fallback
|
||||
llm_model = "gpt-4o-mini" # Default fallback
|
||||
elif hasattr(self.agent, 'llm') and self.agent.llm:
|
||||
# For standard model strings
|
||||
llm_model = self.agent.llm
|
||||
|
||||
@@ -27,7 +27,7 @@ class GenerateCOT:
|
||||
def __init__(
|
||||
self,
|
||||
qa_pairs: Optional[Dict[str, str]] = None,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
api_key: Optional[str] = None,
|
||||
max_attempts: int = 3,
|
||||
verbose: bool = True,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Trace Module for PraisonAI Agents.
|
||||
|
||||
Provides lightweight action tracing for the `output="actions"` mode.
|
||||
Shows agent lifecycle events, tool calls, and final output without
|
||||
verbose internal details.
|
||||
|
||||
Zero Performance Impact:
|
||||
- All imports are lazy loaded via __getattr__
|
||||
- NoOpSink is the default (zero overhead when not used)
|
||||
- Disabled emitter has near-zero overhead
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Simple usage - shows action trace
|
||||
agent = Agent(instructions="...", output="actions")
|
||||
agent.start("Do something")
|
||||
|
||||
# Advanced - capture to file
|
||||
from praisonaiagents.trace import ActionTraceConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="...",
|
||||
output="actions",
|
||||
trace=ActionTraceConfig(
|
||||
sink_type="jsonl",
|
||||
file_path="trace.jsonl",
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
# Core types
|
||||
"ActionEvent",
|
||||
"ActionEventType",
|
||||
"ActionTraceConfig",
|
||||
# Sinks
|
||||
"TraceSink",
|
||||
"NoOpSink",
|
||||
"ListSink",
|
||||
# Emitter
|
||||
"TraceEmitter",
|
||||
# Redaction
|
||||
"redact_dict",
|
||||
"REDACT_KEYS",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load module components to avoid import overhead."""
|
||||
if name in ("ActionEvent", "ActionEventType", "ActionTraceConfig"):
|
||||
from .protocol import ActionEvent, ActionEventType, ActionTraceConfig
|
||||
return locals()[name]
|
||||
|
||||
if name in ("TraceSink", "NoOpSink", "ListSink"):
|
||||
from .protocol import TraceSink, NoOpSink, ListSink
|
||||
return locals()[name]
|
||||
|
||||
if name == "TraceEmitter":
|
||||
from .protocol import TraceEmitter
|
||||
return TraceEmitter
|
||||
|
||||
if name in ("redact_dict", "REDACT_KEYS"):
|
||||
from .redact import redact_dict, REDACT_KEYS
|
||||
return locals()[name]
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
Action Trace Protocol for PraisonAI Agents.
|
||||
|
||||
Lightweight protocol for tracing agent actions. Uses only stdlib
|
||||
to ensure zero dependencies and minimal overhead.
|
||||
|
||||
Schema Version: 1.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
# Schema version for forward compatibility
|
||||
SCHEMA_VERSION = "1.0"
|
||||
|
||||
|
||||
class ActionEventType(str, Enum):
|
||||
"""Types of action events."""
|
||||
AGENT_START = "agent_start"
|
||||
AGENT_END = "agent_end"
|
||||
TOOL_START = "tool_start"
|
||||
TOOL_END = "tool_end"
|
||||
ERROR = "error"
|
||||
OUTPUT = "output"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionEvent:
|
||||
"""
|
||||
Lightweight action trace event.
|
||||
|
||||
Represents a single action in the agent execution trace.
|
||||
Designed for minimal memory footprint and fast serialization.
|
||||
"""
|
||||
event_type: str
|
||||
timestamp: float
|
||||
schema_version: str = SCHEMA_VERSION
|
||||
agent_id: Optional[str] = None
|
||||
agent_name: Optional[str] = None
|
||||
tool_name: Optional[str] = None
|
||||
tool_args: Optional[Dict[str, Any]] = None
|
||||
tool_result_summary: Optional[str] = None
|
||||
duration_ms: Optional[float] = None
|
||||
status: Optional[str] = None # ok, error
|
||||
error_message: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary, excluding None values."""
|
||||
result = {
|
||||
"schema_version": self.schema_version,
|
||||
"event_type": self.event_type,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
# Add optional fields only if set
|
||||
if self.agent_id is not None:
|
||||
result["agent_id"] = self.agent_id
|
||||
if self.agent_name is not None:
|
||||
result["agent_name"] = self.agent_name
|
||||
if self.tool_name is not None:
|
||||
result["tool_name"] = self.tool_name
|
||||
if self.tool_args is not None:
|
||||
result["tool_args"] = self.tool_args
|
||||
if self.tool_result_summary is not None:
|
||||
result["tool_result_summary"] = self.tool_result_summary
|
||||
if self.duration_ms is not None:
|
||||
result["duration_ms"] = self.duration_ms
|
||||
if self.status is not None:
|
||||
result["status"] = self.status
|
||||
if self.error_message is not None:
|
||||
result["error_message"] = self.error_message
|
||||
if self.metadata:
|
||||
result["metadata"] = self.metadata
|
||||
|
||||
return result
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string."""
|
||||
return json.dumps(self.to_dict(), default=str)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TraceSink(Protocol):
|
||||
"""
|
||||
Protocol for trace event sinks.
|
||||
|
||||
Implementations receive action events and handle them
|
||||
(e.g., print to stdout, write to file, send to server).
|
||||
"""
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Emit an action event."""
|
||||
...
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush any buffered events."""
|
||||
...
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the sink and release resources."""
|
||||
...
|
||||
|
||||
|
||||
class NoOpSink:
|
||||
"""
|
||||
No-operation sink that discards all events.
|
||||
|
||||
Used as the default sink when tracing is disabled.
|
||||
Has near-zero overhead.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Discard the event."""
|
||||
pass
|
||||
|
||||
def flush(self) -> None:
|
||||
"""No-op."""
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
"""No-op."""
|
||||
pass
|
||||
|
||||
|
||||
class ListSink:
|
||||
"""
|
||||
Sink that collects events in a list.
|
||||
|
||||
Useful for testing and programmatic access to trace events.
|
||||
"""
|
||||
|
||||
__slots__ = ("_events",)
|
||||
|
||||
def __init__(self):
|
||||
self._events: List[ActionEvent] = []
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Add event to the list."""
|
||||
self._events.append(event)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""No-op for list sink."""
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
"""No-op for list sink."""
|
||||
pass
|
||||
|
||||
def get_events(self) -> List[ActionEvent]:
|
||||
"""Get all collected events."""
|
||||
return self._events.copy()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all collected events."""
|
||||
self._events.clear()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionTraceConfig:
|
||||
"""
|
||||
Configuration for action tracing.
|
||||
|
||||
Usage:
|
||||
# Default (disabled)
|
||||
config = ActionTraceConfig()
|
||||
|
||||
# Enable with stdout
|
||||
config = ActionTraceConfig(enabled=True, sink_type="stdout")
|
||||
|
||||
# Enable with JSONL file
|
||||
config = ActionTraceConfig(
|
||||
enabled=True,
|
||||
sink_type="jsonl",
|
||||
file_path="trace.jsonl",
|
||||
)
|
||||
"""
|
||||
enabled: bool = True
|
||||
redact: bool = True
|
||||
compact: bool = False
|
||||
sink_type: str = "noop" # noop, stdout, jsonl, file
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"redact": self.redact,
|
||||
"compact": self.compact,
|
||||
"sink_type": self.sink_type,
|
||||
"file_path": self.file_path,
|
||||
}
|
||||
|
||||
|
||||
class TraceEmitter:
|
||||
"""
|
||||
Emitter for action trace events.
|
||||
|
||||
Provides a high-level API for emitting trace events.
|
||||
Handles redaction and sink management.
|
||||
|
||||
Usage:
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink)
|
||||
|
||||
emitter.agent_start("researcher")
|
||||
emitter.tool_start("search", {"query": "AI"})
|
||||
emitter.tool_end("search", duration_ms=100, status="ok")
|
||||
emitter.agent_end("researcher", duration_ms=500)
|
||||
"""
|
||||
|
||||
__slots__ = ("_sink", "_enabled", "_redact", "_agent_starts")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sink: Optional[TraceSink] = None,
|
||||
enabled: bool = True,
|
||||
redact: bool = True,
|
||||
):
|
||||
self._sink = sink or NoOpSink()
|
||||
self._enabled = enabled
|
||||
self._redact = redact
|
||||
self._agent_starts: Dict[str, float] = {} # Track agent start times
|
||||
|
||||
def _emit(self, event: ActionEvent) -> None:
|
||||
"""Internal emit with enabled check."""
|
||||
if not self._enabled:
|
||||
return
|
||||
self._sink.emit(event)
|
||||
|
||||
def _redact_args(self, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Redact sensitive data from args if enabled."""
|
||||
if args is None or not self._redact:
|
||||
return args
|
||||
|
||||
from .redact import redact_dict
|
||||
return redact_dict(args)
|
||||
|
||||
def agent_start(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit agent start event."""
|
||||
ts = time.time()
|
||||
self._agent_starts[agent_name] = ts
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.AGENT_START.value,
|
||||
timestamp=ts,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def agent_end(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_id: Optional[str] = None,
|
||||
duration_ms: Optional[float] = None,
|
||||
status: str = "ok",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit agent end event."""
|
||||
ts = time.time()
|
||||
|
||||
# Calculate duration if not provided
|
||||
if duration_ms is None and agent_name in self._agent_starts:
|
||||
start_ts = self._agent_starts.pop(agent_name, None)
|
||||
if start_ts:
|
||||
duration_ms = (ts - start_ts) * 1000
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.AGENT_END.value,
|
||||
timestamp=ts,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def tool_start(
|
||||
self,
|
||||
tool_name: str,
|
||||
tool_args: Optional[Dict[str, Any]] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit tool start event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.TOOL_START.value,
|
||||
timestamp=time.time(),
|
||||
tool_name=tool_name,
|
||||
tool_args=self._redact_args(tool_args),
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def tool_end(
|
||||
self,
|
||||
tool_name: str,
|
||||
duration_ms: float,
|
||||
status: str = "ok",
|
||||
result_summary: Optional[str] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
error_message: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit tool end event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.TOOL_END.value,
|
||||
timestamp=time.time(),
|
||||
tool_name=tool_name,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
tool_result_summary=result_summary,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
error_message=error_message,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def error(
|
||||
self,
|
||||
message: str,
|
||||
tool_name: Optional[str] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit error event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.ERROR.value,
|
||||
timestamp=time.time(),
|
||||
error_message=message,
|
||||
tool_name=tool_name,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
status="error",
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def output(
|
||||
self,
|
||||
content: str,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit final output event."""
|
||||
# Truncate long output for summary
|
||||
summary = content[:500] + "..." if len(content) > 500 else content
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.OUTPUT.value,
|
||||
timestamp=time.time(),
|
||||
tool_result_summary=summary,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the sink."""
|
||||
self._sink.flush()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the sink."""
|
||||
self._sink.close()
|
||||
|
||||
|
||||
# Global default emitter (NoOp by default)
|
||||
_default_emitter: Optional[TraceEmitter] = None
|
||||
|
||||
|
||||
def get_default_emitter() -> TraceEmitter:
|
||||
"""Get the default trace emitter."""
|
||||
global _default_emitter
|
||||
if _default_emitter is None:
|
||||
_default_emitter = TraceEmitter(sink=NoOpSink(), enabled=False)
|
||||
return _default_emitter
|
||||
|
||||
|
||||
def set_default_emitter(emitter: TraceEmitter) -> None:
|
||||
"""Set the default trace emitter."""
|
||||
global _default_emitter
|
||||
_default_emitter = emitter
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Redaction utilities for action trace.
|
||||
|
||||
Provides deterministic redaction of sensitive data in tool arguments
|
||||
and results. Uses only stdlib for zero dependencies.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Set
|
||||
import re
|
||||
|
||||
# Keys that should always be redacted (case-insensitive matching)
|
||||
REDACT_KEYS: Set[str] = {
|
||||
# API keys and tokens
|
||||
"api_key",
|
||||
"apikey",
|
||||
"api-key",
|
||||
"secret",
|
||||
"secret_key",
|
||||
"secretkey",
|
||||
"private_key",
|
||||
"privatekey",
|
||||
# Authentication
|
||||
"password",
|
||||
"passwd",
|
||||
"pwd",
|
||||
"token",
|
||||
"bearer",
|
||||
"authorization",
|
||||
"auth",
|
||||
"auth_token",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
# Session and cookies
|
||||
"cookie",
|
||||
"session",
|
||||
"session_id",
|
||||
"sessionid",
|
||||
"credential",
|
||||
"credentials",
|
||||
# Database
|
||||
"connection_string",
|
||||
"connectionstring",
|
||||
"database_url",
|
||||
"db_url",
|
||||
"dburl",
|
||||
"db_password",
|
||||
"db_pass",
|
||||
# Cloud provider specific
|
||||
"aws_secret",
|
||||
"aws_secret_access_key",
|
||||
"azure_key",
|
||||
"gcp_key",
|
||||
"openai_api_key",
|
||||
"anthropic_api_key",
|
||||
}
|
||||
|
||||
# Compiled regex for case-insensitive matching
|
||||
_REDACT_PATTERN = re.compile(
|
||||
r"(" + "|".join(re.escape(k) for k in REDACT_KEYS) + r")",
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
REDACTED_VALUE = "[REDACTED]"
|
||||
|
||||
|
||||
def _should_redact(key: str) -> bool:
|
||||
"""Check if a key should be redacted."""
|
||||
# Normalize key for comparison
|
||||
normalized = key.lower().replace("-", "_").replace(" ", "_")
|
||||
|
||||
# Check exact match first
|
||||
if normalized in REDACT_KEYS:
|
||||
return True
|
||||
|
||||
# Check if any redact key is contained in the normalized key
|
||||
for redact_key in REDACT_KEYS:
|
||||
if redact_key in normalized:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def redact_dict(data: Dict[str, Any], enabled: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Redact sensitive values from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary to redact
|
||||
enabled: If False, returns data unchanged
|
||||
|
||||
Returns:
|
||||
New dictionary with sensitive values redacted
|
||||
"""
|
||||
if not enabled:
|
||||
return data
|
||||
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
return _redact_value(data)
|
||||
|
||||
|
||||
def _redact_value(value: Any) -> Any:
|
||||
"""Recursively redact sensitive values."""
|
||||
if isinstance(value, dict):
|
||||
return {k: _redact_key_value(k, v) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [_redact_value(item) for item in value]
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def _redact_key_value(key: str, value: Any) -> Any:
|
||||
"""Redact a value if its key is sensitive."""
|
||||
if _should_redact(key):
|
||||
return REDACTED_VALUE
|
||||
|
||||
# Recursively process nested structures
|
||||
return _redact_value(value)
|
||||
|
||||
|
||||
def redact_string(text: str, enabled: bool = True) -> str:
|
||||
"""
|
||||
Redact potential secrets from a string.
|
||||
|
||||
This is a best-effort function that looks for common patterns
|
||||
like "api_key=xxx" or "password: xxx" in strings.
|
||||
|
||||
Args:
|
||||
text: String to redact
|
||||
enabled: If False, returns text unchanged
|
||||
|
||||
Returns:
|
||||
String with potential secrets redacted
|
||||
"""
|
||||
if not enabled or not text:
|
||||
return text
|
||||
|
||||
# Pattern for key=value or key: value
|
||||
patterns = [
|
||||
(r'(["\']?)(' + '|'.join(re.escape(k) for k in REDACT_KEYS) + r')(["\']?\s*[:=]\s*["\']?)([^"\'\s,}\]]+)',
|
||||
r'\1\2\3[REDACTED]'),
|
||||
]
|
||||
|
||||
result = text
|
||||
for pattern, replacement in patterns:
|
||||
result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
|
||||
|
||||
return result
|
||||
@@ -254,6 +254,10 @@ class WorkflowStep:
|
||||
knowledge: Optional[Any] = None # Union[bool, List[str], KnowledgeConfig] - RAG
|
||||
web: Optional[Any] = None # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None # Union[bool, ReflectionConfig] - self-reflection
|
||||
memory: Optional[Any] = None # Union[bool, MemoryConfig] - step-level memory
|
||||
planning: Optional[Any] = None # Union[bool, PlanningConfig] - step-level planning
|
||||
hooks: Optional[Any] = None # Union[List, HooksConfig] - step lifecycle hooks
|
||||
caching: Optional[Any] = None # Union[bool, CachingConfig] - step-level caching
|
||||
|
||||
# Status tracking
|
||||
status: str = "pending" # pending, running, completed, failed, skipped
|
||||
@@ -278,6 +282,10 @@ class WorkflowStep:
|
||||
_knowledge_config: Optional[Any] = field(default=None, repr=False)
|
||||
_web_config: Optional[Any] = field(default=None, repr=False)
|
||||
_reflection_config: Optional[Any] = field(default=None, repr=False)
|
||||
_memory_config: Optional[Any] = field(default=None, repr=False)
|
||||
_planning_config: Optional[Any] = field(default=None, repr=False)
|
||||
_hooks_config: Optional[Any] = field(default=None, repr=False)
|
||||
_caching_config: Optional[Any] = field(default=None, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve consolidated params to internal values."""
|
||||
@@ -370,6 +378,62 @@ class WorkflowStep:
|
||||
config_class=ReflectionConfig, presets=REFLECTION_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve memory (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import MemoryConfig
|
||||
from ..config.presets import MEMORY_PRESETS
|
||||
except ImportError:
|
||||
MemoryConfig = None
|
||||
MEMORY_PRESETS = {}
|
||||
if MemoryConfig and self.memory is not None:
|
||||
self._memory_config = resolve(
|
||||
value=self.memory, param_name="memory",
|
||||
config_class=MemoryConfig, presets=MEMORY_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve planning (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import PlanningConfig
|
||||
from ..config.presets import PLANNING_PRESETS
|
||||
except ImportError:
|
||||
PlanningConfig = None
|
||||
PLANNING_PRESETS = {}
|
||||
if PlanningConfig and self.planning is not None:
|
||||
self._planning_config = resolve(
|
||||
value=self.planning, param_name="planning",
|
||||
config_class=PlanningConfig, presets=PLANNING_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve hooks (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import HooksConfig
|
||||
except ImportError:
|
||||
HooksConfig = None
|
||||
if HooksConfig and self.hooks is not None:
|
||||
# Hooks don't have presets, just config class
|
||||
if isinstance(self.hooks, list):
|
||||
self._hooks_config = HooksConfig(callbacks=self.hooks)
|
||||
elif hasattr(self.hooks, 'callbacks'):
|
||||
self._hooks_config = self.hooks
|
||||
else:
|
||||
self._hooks_config = self.hooks
|
||||
|
||||
# Resolve caching (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import CachingConfig
|
||||
from ..config.presets import CACHING_PRESETS
|
||||
except ImportError:
|
||||
CachingConfig = None
|
||||
CACHING_PRESETS = {}
|
||||
if CachingConfig and self.caching is not None:
|
||||
self._caching_config = resolve(
|
||||
value=self.caching, param_name="caching",
|
||||
config_class=CachingConfig, presets=CACHING_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Property accessors for backward compatibility
|
||||
@property
|
||||
@@ -507,6 +571,8 @@ class Workflow:
|
||||
guardrails: Optional[Any] = None # Union[bool, Callable, GuardrailConfig] - validation
|
||||
web: Optional[Any] = None # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None # Union[bool, ReflectionConfig] - self-reflection
|
||||
execution: Optional[Any] = None # Union[str, ExecutionConfig] - execution control
|
||||
caching: Optional[Any] = None # Union[bool, CachingConfig] - caching
|
||||
|
||||
# Status tracking
|
||||
status: str = "not_started" # not_started, running, completed, failed
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Detailed profiler to identify the exact source of streaming delay.
|
||||
Traces every step from agent.start() to HTTP request.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import functools
|
||||
|
||||
os.environ['LOGLEVEL'] = 'DEBUG'
|
||||
|
||||
# Microsecond precision timestamps
|
||||
def ts():
|
||||
return time.perf_counter()
|
||||
|
||||
start_time = ts()
|
||||
|
||||
def log(msg):
|
||||
elapsed = (ts() - start_time) * 1000
|
||||
print(f"[{elapsed:8.2f}ms] {msg}")
|
||||
|
||||
log("Script starting...")
|
||||
|
||||
# ============================================================
|
||||
# PATCH 1: OpenAI client initialization
|
||||
# ============================================================
|
||||
import openai
|
||||
|
||||
original_openai_init = openai.OpenAI.__init__
|
||||
|
||||
@functools.wraps(original_openai_init)
|
||||
def traced_openai_init(self, *args, **kwargs):
|
||||
t0 = ts()
|
||||
result = original_openai_init(self, *args, **kwargs)
|
||||
log(f"OpenAI.__init__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
openai.OpenAI.__init__ = traced_openai_init
|
||||
|
||||
# ============================================================
|
||||
# PATCH 2: OpenAI completions.create
|
||||
# ============================================================
|
||||
original_create = openai.resources.chat.completions.Completions.create
|
||||
|
||||
@functools.wraps(original_create)
|
||||
def traced_create(self, *args, **kwargs):
|
||||
log(f">>> OpenAI.create() CALLED - stream={kwargs.get('stream', False)}")
|
||||
t0 = ts()
|
||||
result = original_create(self, *args, **kwargs)
|
||||
log(f"<<< OpenAI.create() RETURNED in {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
openai.resources.chat.completions.Completions.create = traced_create
|
||||
|
||||
# ============================================================
|
||||
# PATCH 3: httpx.Client.send (actual HTTP request)
|
||||
# ============================================================
|
||||
import httpx
|
||||
|
||||
original_send = httpx.Client.send
|
||||
|
||||
@functools.wraps(original_send)
|
||||
def traced_send(self, request, *args, **kwargs):
|
||||
log(f">>> HTTPX.send() {request.method} {request.url}")
|
||||
t0 = ts()
|
||||
result = original_send(self, request, *args, **kwargs)
|
||||
log(f"<<< HTTPX.send() returned in {(ts()-t0)*1000:.2f}ms - status={result.status_code}")
|
||||
return result
|
||||
|
||||
httpx.Client.send = traced_send
|
||||
|
||||
# ============================================================
|
||||
# PATCH 4: Rich Live display
|
||||
# ============================================================
|
||||
from rich.live import Live
|
||||
|
||||
original_live_init = Live.__init__
|
||||
original_live_enter = Live.__enter__
|
||||
original_live_exit = Live.__exit__
|
||||
|
||||
@functools.wraps(original_live_init)
|
||||
def traced_live_init(self, *args, **kwargs):
|
||||
log(f">>> Live.__init__() called")
|
||||
t0 = ts()
|
||||
result = original_live_init(self, *args, **kwargs)
|
||||
log(f"<<< Live.__init__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_live_enter)
|
||||
def traced_live_enter(self):
|
||||
log(f">>> Live.__enter__() called")
|
||||
t0 = ts()
|
||||
result = original_live_enter(self)
|
||||
log(f"<<< Live.__enter__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_live_exit)
|
||||
def traced_live_exit(self, *args):
|
||||
log(f">>> Live.__exit__() called")
|
||||
t0 = ts()
|
||||
result = original_live_exit(self, *args)
|
||||
log(f"<<< Live.__exit__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
Live.__init__ = traced_live_init
|
||||
Live.__enter__ = traced_live_enter
|
||||
Live.__exit__ = traced_live_exit
|
||||
|
||||
log("Patches applied, importing praisonaiagents...")
|
||||
|
||||
# ============================================================
|
||||
# Import and trace Agent
|
||||
# ============================================================
|
||||
t0 = ts()
|
||||
from praisonaiagents import Agent
|
||||
log(f"Agent import took {(ts()-t0)*1000:.2f}ms")
|
||||
|
||||
# ============================================================
|
||||
# PATCH 5: Agent methods
|
||||
# ============================================================
|
||||
from praisonaiagents.agent.agent import Agent as AgentClass
|
||||
|
||||
original_chat_completion = AgentClass._chat_completion
|
||||
original_format_tools = AgentClass._format_tools_for_completion
|
||||
|
||||
@functools.wraps(original_chat_completion)
|
||||
def traced_chat_completion(self, messages, temperature=1.0, tools=None, stream=True,
|
||||
reasoning_steps=False, task_name=None, task_description=None, task_id=None):
|
||||
log(f">>> Agent._chat_completion() stream={stream}, _using_custom_llm={self._using_custom_llm}")
|
||||
t0 = ts()
|
||||
result = original_chat_completion(self, messages, temperature, tools, stream,
|
||||
reasoning_steps, task_name, task_description, task_id)
|
||||
log(f"<<< Agent._chat_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_format_tools)
|
||||
def traced_format_tools(self, tools):
|
||||
log(f">>> Agent._format_tools_for_completion() tools={tools}")
|
||||
t0 = ts()
|
||||
result = original_format_tools(self, tools)
|
||||
log(f"<<< Agent._format_tools_for_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
AgentClass._chat_completion = traced_chat_completion
|
||||
AgentClass._format_tools_for_completion = traced_format_tools
|
||||
|
||||
# ============================================================
|
||||
# PATCH 6: OpenAIClient methods
|
||||
# ============================================================
|
||||
from praisonaiagents.llm.openai_client import OpenAIClient
|
||||
|
||||
original_chat_with_tools = OpenAIClient.chat_completion_with_tools
|
||||
original_process_stream = OpenAIClient.process_stream_response
|
||||
original_create_completion = OpenAIClient.create_completion
|
||||
original_format_tools_client = OpenAIClient.format_tools
|
||||
|
||||
@functools.wraps(original_chat_with_tools)
|
||||
def traced_chat_with_tools(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.chat_completion_with_tools() stream={kwargs.get('stream', True)}")
|
||||
t0 = ts()
|
||||
result = original_chat_with_tools(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.chat_completion_with_tools() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_process_stream)
|
||||
def traced_process_stream(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.process_stream_response()")
|
||||
t0 = ts()
|
||||
result = original_process_stream(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.process_stream_response() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_create_completion)
|
||||
def traced_create_completion(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.create_completion() stream={kwargs.get('stream', False)}")
|
||||
t0 = ts()
|
||||
result = original_create_completion(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.create_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_format_tools_client)
|
||||
def traced_format_tools_client(self, tools):
|
||||
log(f">>> OpenAIClient.format_tools() tools count={len(tools) if tools else 0}")
|
||||
t0 = ts()
|
||||
result = original_format_tools_client(self, tools)
|
||||
log(f"<<< OpenAIClient.format_tools() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
OpenAIClient.chat_completion_with_tools = traced_chat_with_tools
|
||||
OpenAIClient.process_stream_response = traced_process_stream
|
||||
OpenAIClient.create_completion = traced_create_completion
|
||||
OpenAIClient.format_tools = traced_format_tools_client
|
||||
|
||||
# ============================================================
|
||||
# PATCH 7: sync_client property (lazy init)
|
||||
# ============================================================
|
||||
original_sync_client_getter = OpenAIClient.sync_client.fget
|
||||
|
||||
def traced_sync_client_getter(self):
|
||||
log(f">>> OpenAIClient.sync_client property accessed")
|
||||
t0 = ts()
|
||||
result = original_sync_client_getter(self)
|
||||
log(f"<<< OpenAIClient.sync_client took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
OpenAIClient.sync_client = property(traced_sync_client_getter)
|
||||
|
||||
log("All patches applied. Creating agent...")
|
||||
|
||||
# ============================================================
|
||||
# RUN THE TEST
|
||||
# ============================================================
|
||||
t0 = ts()
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant"
|
||||
)
|
||||
log(f"Agent created in {(ts()-t0)*1000:.2f}ms")
|
||||
log(f"Agent.stream={agent.stream}, Agent.verbose={agent.verbose}")
|
||||
|
||||
log("=" * 60)
|
||||
log("Starting agent.start()...")
|
||||
log("=" * 60)
|
||||
|
||||
t0 = ts()
|
||||
result = agent.start("Say hello in one word")
|
||||
log(f"agent.start() completed in {(ts()-t0)*1000:.2f}ms")
|
||||
|
||||
log("=" * 60)
|
||||
log("DONE")
|
||||
log("=" * 60)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.10"
|
||||
version = "0.11.8"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Streaming Example with TTFT Metrics
|
||||
|
||||
This example demonstrates true pass-through streaming with timing metrics.
|
||||
When verbose output is enabled, tokens are displayed as they arrive from the provider.
|
||||
|
||||
Key timing metrics:
|
||||
- TTFT (Time To First Token): Time from request to first token received
|
||||
- Stream Duration: Time from first to last token
|
||||
- Total Time: End-to-end request time
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config import OutputConfig
|
||||
|
||||
# Create agent with verbose output (shows streaming progress)
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-4o-mini",
|
||||
output=OutputConfig(verbose=True) # Show generation progress
|
||||
)
|
||||
|
||||
# Run the agent - verbose mode shows streaming progress via Rich Live display
|
||||
result = agent.start("Write a short paragraph about the history of computing")
|
||||
print(f"\n--- Response ---\n{result}")
|
||||
@@ -1,11 +0,0 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Use chat method with stream=True for streaming responses
|
||||
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
|
||||
print(chunk, end="", flush=True)
|
||||
print() # Final newline
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Micro-benchmark for output mode overhead comparison.
|
||||
|
||||
Measures:
|
||||
1. Import-time cost (cold import)
|
||||
2. Agent initialization overhead per output mode
|
||||
3. Hot-path overhead (simulated callback execution)
|
||||
|
||||
Usage:
|
||||
python tests/benchmarks/output_mode_benchmark.py
|
||||
|
||||
All stdlib - no external dependencies required.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from statistics import mean, stdev
|
||||
|
||||
|
||||
def measure_import_time(mode: str, iterations: int = 5) -> dict:
|
||||
"""Measure cold import time for a specific output mode."""
|
||||
times = []
|
||||
|
||||
# Script that imports and creates an agent with specific output mode
|
||||
script = f'''
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
import os
|
||||
os.environ['PRAISONAI_OUTPUT'] = '{mode}'
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(name="BenchAgent", instructions="Test", output="{mode}")
|
||||
end = time.perf_counter()
|
||||
print(f"{{(end - start) * 1000:.3f}}")
|
||||
'''
|
||||
|
||||
for _ in range(iterations):
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-c', script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**dict(__import__('os').environ), 'PYTHONDONTWRITEBYTECODE': '1'}
|
||||
)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
times.append(float(result.stdout.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not times:
|
||||
return {"mode": mode, "mean_ms": -1, "stdev_ms": -1, "samples": 0}
|
||||
|
||||
return {
|
||||
"mode": mode,
|
||||
"mean_ms": mean(times),
|
||||
"stdev_ms": stdev(times) if len(times) > 1 else 0,
|
||||
"samples": len(times),
|
||||
}
|
||||
|
||||
|
||||
def measure_callback_overhead(iterations: int = 1000) -> dict:
|
||||
"""Measure hot-path overhead of callback execution vs no callbacks."""
|
||||
results = {}
|
||||
|
||||
# Silent mode - no callbacks
|
||||
silent_times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
# Simulate what happens in silent mode: nothing
|
||||
pass
|
||||
end = time.perf_counter()
|
||||
silent_times.append((end - start) * 1_000_000) # microseconds
|
||||
|
||||
results["silent"] = {
|
||||
"mean_us": mean(silent_times),
|
||||
"stdev_us": stdev(silent_times) if len(silent_times) > 1 else 0,
|
||||
}
|
||||
|
||||
# Actions mode - simulate callback overhead
|
||||
actions_times = []
|
||||
|
||||
# Simulate the work done by actions mode per tool call
|
||||
def simulate_actions_callback():
|
||||
import time as t
|
||||
# Simulate: time.time(), dict creation, string formatting, stderr write
|
||||
_ = t.time()
|
||||
event = {
|
||||
"type": "tool_call",
|
||||
"tool": "test_tool",
|
||||
"args": {"arg1": "value1", "arg2": "value2"},
|
||||
"timestamp": t.time(),
|
||||
}
|
||||
formatted = f"[tool_call] {event['tool']}({event['args']})"
|
||||
# Don't actually write to avoid polluting output
|
||||
_ = formatted.encode()
|
||||
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
simulate_actions_callback()
|
||||
end = time.perf_counter()
|
||||
actions_times.append((end - start) * 1_000_000) # microseconds
|
||||
|
||||
results["actions"] = {
|
||||
"mean_us": mean(actions_times),
|
||||
"stdev_us": stdev(actions_times) if len(actions_times) > 1 else 0,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def measure_agent_init_overhead() -> dict:
|
||||
"""Measure Agent.__init__ overhead for different output modes."""
|
||||
import os
|
||||
|
||||
# Clear any env var
|
||||
os.environ.pop('PRAISONAI_OUTPUT', None)
|
||||
|
||||
results = {}
|
||||
iterations = 10
|
||||
|
||||
for mode in ["silent", "actions", "verbose"]:
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
# Import fresh each time to avoid caching effects
|
||||
from praisonaiagents import Agent
|
||||
|
||||
start = time.perf_counter()
|
||||
agent = Agent(
|
||||
name="BenchAgent",
|
||||
instructions="Test agent",
|
||||
output=mode,
|
||||
)
|
||||
end = time.perf_counter()
|
||||
times.append((end - start) * 1000) # milliseconds
|
||||
del agent
|
||||
|
||||
results[mode] = {
|
||||
"mean_ms": mean(times),
|
||||
"stdev_ms": stdev(times) if len(times) > 1 else 0,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("OUTPUT MODE MICRO-BENCHMARK")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# 1. Import time comparison
|
||||
print("1. IMPORT + INIT TIME (cold start, subprocess)")
|
||||
print("-" * 40)
|
||||
|
||||
modes = ["silent", "actions", "verbose"]
|
||||
import_results = []
|
||||
|
||||
for mode in modes:
|
||||
print(f" Measuring {mode}...", end=" ", flush=True)
|
||||
result = measure_import_time(mode, iterations=3)
|
||||
import_results.append(result)
|
||||
print(f"{result['mean_ms']:.1f}ms (±{result['stdev_ms']:.1f})")
|
||||
|
||||
print()
|
||||
|
||||
# 2. Agent init overhead (warm)
|
||||
print("2. AGENT INIT OVERHEAD (warm, same process)")
|
||||
print("-" * 40)
|
||||
|
||||
init_results = measure_agent_init_overhead()
|
||||
for mode, data in init_results.items():
|
||||
print(f" {mode:12s}: {data['mean_ms']:.2f}ms (±{data['stdev_ms']:.2f})")
|
||||
|
||||
print()
|
||||
|
||||
# 3. Hot-path callback overhead
|
||||
print("3. HOT-PATH CALLBACK OVERHEAD (per call)")
|
||||
print("-" * 40)
|
||||
|
||||
callback_results = measure_callback_overhead(iterations=10000)
|
||||
for mode, data in callback_results.items():
|
||||
print(f" {mode:12s}: {data['mean_us']:.3f}µs (±{data['stdev_us']:.3f})")
|
||||
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
silent_init = init_results.get("silent", {}).get("mean_ms", 0)
|
||||
actions_init = init_results.get("actions", {}).get("mean_ms", 0)
|
||||
|
||||
silent_callback = callback_results.get("silent", {}).get("mean_us", 0)
|
||||
actions_callback = callback_results.get("actions", {}).get("mean_us", 0)
|
||||
|
||||
print(f" Init overhead (actions vs silent): +{actions_init - silent_init:.2f}ms")
|
||||
print(f" Per-call overhead (actions vs silent): +{actions_callback - silent_callback:.3f}µs")
|
||||
print()
|
||||
print(" DEFAULT MODE: silent (zero overhead)")
|
||||
print(" OPT-IN: output='actions' for observability")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -179,25 +179,27 @@ class TestAgentWebFetchParameter:
|
||||
"""Test the web_fetch parameter in Agent class."""
|
||||
|
||||
def test_agent_init_with_web_fetch_true(self):
|
||||
"""Test Agent initialization with web_fetch=True."""
|
||||
"""Test Agent initialization with web=WebConfig(fetch=True)."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.web_fetch is True
|
||||
|
||||
def test_agent_init_with_web_fetch_dict(self):
|
||||
"""Test Agent initialization with web_fetch as dict."""
|
||||
"""Test Agent initialization with web=WebConfig(fetch=True)."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
web_fetch_config = {"max_uses": 10, "allowed_domains": ["example.com"]}
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
# Now using consolidated web= param with WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=web_fetch_config
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.web_fetch == web_fetch_config
|
||||
assert agent.web_fetch is True
|
||||
|
||||
def test_agent_init_without_web_fetch(self):
|
||||
"""Test Agent initialization without web_fetch."""
|
||||
@@ -208,10 +210,11 @@ class TestAgentWebFetchParameter:
|
||||
def test_agent_passes_web_fetch_to_llm(self):
|
||||
"""Test that Agent passes web_fetch to LLM instance."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.llm_instance.web_fetch is True
|
||||
|
||||
@@ -270,12 +273,13 @@ class TestWebFetchIntegration:
|
||||
def test_web_fetch_with_anthropic(self):
|
||||
"""Test actual web fetch with Anthropic API."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
agent = Agent(
|
||||
name="Web Fetch Agent",
|
||||
instructions="You are a helpful assistant that can fetch web content.",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
|
||||
# This would make an actual API call
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Tests for Agent execution API semantics: start(), run(), stream()
|
||||
|
||||
TDD tests for the new execution API:
|
||||
- start() = interactive, streams by default in TTY
|
||||
- run() = production, silent by default
|
||||
- stream() = app-friendly iterator
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestAgentStartMethod:
|
||||
"""Tests for Agent.start() - interactive, streaming by default in TTY"""
|
||||
|
||||
def test_start_streams_by_default_in_tty(self):
|
||||
"""start() should stream by default when stdout is a TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create agent without stream attribute set (None)
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
# Mock TTY detection
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
# Mock chat to avoid actual LLM call
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " World"])
|
||||
with patch.object(agent, 'chat') as mock_chat:
|
||||
agent.start("Hello")
|
||||
# Should call _start_stream when TTY
|
||||
mock_stream.assert_called_once()
|
||||
mock_chat.assert_not_called()
|
||||
|
||||
def test_start_does_not_stream_when_not_tty(self):
|
||||
"""start() should not stream when stdout is not a TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Mock non-TTY
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
result = agent.start("Hello")
|
||||
# Should call chat, not _start_stream
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_start_respects_explicit_stream_true(self):
|
||||
"""start(stream=True) should always stream"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when not TTY, explicit stream=True should stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
result = agent.start("Hello", stream=True)
|
||||
mock_stream.assert_called_once()
|
||||
|
||||
def test_start_respects_explicit_stream_false(self):
|
||||
"""start(stream=False) should never stream"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when TTY, explicit stream=False should not stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.start("Hello", stream=False)
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_start_respects_agent_stream_attribute(self):
|
||||
"""start() should respect agent's stream attribute over TTY detection"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Agent with stream=True should always stream
|
||||
agent = Agent(name="Test", instructions="You are helpful", output="stream")
|
||||
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
# Agent's stream attribute should take precedence
|
||||
if agent.stream:
|
||||
result = agent.start("Hello")
|
||||
mock_stream.assert_called()
|
||||
|
||||
|
||||
class TestAgentRunMethod:
|
||||
"""Tests for Agent.run() - production, silent by default"""
|
||||
|
||||
def test_run_does_not_stream_by_default(self):
|
||||
"""run() should not stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.run("Hello")
|
||||
# Should call chat with stream=False
|
||||
mock_chat.assert_called_once()
|
||||
call_kwargs = mock_chat.call_args[1]
|
||||
assert call_kwargs.get('stream') == False
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_run_returns_result_directly(self):
|
||||
"""run() should return the result directly, not a generator"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
result = agent.run("Hello")
|
||||
assert result == "Hello World"
|
||||
assert not hasattr(result, '__iter__') or isinstance(result, str)
|
||||
|
||||
def test_run_ignores_tty_detection(self):
|
||||
"""run() should ignore TTY detection and never stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even in TTY, run() should not stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.run("Hello")
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_run_can_be_forced_to_stream(self):
|
||||
"""run(stream=True) should stream when explicitly requested"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
result = agent.run("Hello", stream=True)
|
||||
# When stream=True is passed, it should be passed to chat
|
||||
call_kwargs = mock_chat.call_args[1]
|
||||
assert call_kwargs.get('stream') == True
|
||||
|
||||
|
||||
class TestAgentIterStreamMethod:
|
||||
"""Tests for Agent.iter_stream() - app-friendly iterator"""
|
||||
|
||||
def test_iter_stream_returns_iterator(self):
|
||||
"""iter_stream() should return an iterator"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " ", "World"])
|
||||
result = agent.iter_stream("Hello")
|
||||
# Should be an iterator/generator
|
||||
assert hasattr(result, '__iter__')
|
||||
assert hasattr(result, '__next__')
|
||||
|
||||
def test_iter_stream_yields_chunks(self):
|
||||
"""iter_stream() should yield response chunks"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " ", "World"])
|
||||
chunks = list(agent.iter_stream("Hello"))
|
||||
assert chunks == ["Hello", " ", "World"]
|
||||
|
||||
def test_iter_stream_always_streams(self):
|
||||
"""iter_stream() should always stream regardless of TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when not TTY
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
result = agent.iter_stream("Hello")
|
||||
# Consume the generator
|
||||
list(result)
|
||||
mock_stream.assert_called_once()
|
||||
|
||||
|
||||
class TestAsyncMethods:
|
||||
"""Tests for async versions: arun(), astart()"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arun_does_not_stream_by_default(self):
|
||||
"""arun() should not stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'achat', return_value="Hello World") as mock_achat:
|
||||
result = await agent.arun("Hello")
|
||||
mock_achat.assert_called_once()
|
||||
call_kwargs = mock_achat.call_args[1]
|
||||
assert call_kwargs.get('stream') == False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astart_streams_in_tty(self):
|
||||
"""astart() should stream when in TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'achat', return_value="Hello") as mock_achat:
|
||||
await agent.astart("Hello")
|
||||
call_kwargs = mock_achat.call_args[1]
|
||||
assert call_kwargs.get('stream')
|
||||
|
||||
|
||||
class TestMethodSemanticDistinction:
|
||||
"""Tests to verify start() and run() have distinct semantics"""
|
||||
|
||||
def test_start_and_run_are_different(self):
|
||||
"""start() and run() should have different default behaviors"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
# In TTY context
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
|
||||
# start() should stream
|
||||
agent.start("Hello")
|
||||
start_streamed = mock_stream.called
|
||||
|
||||
mock_stream.reset_mock()
|
||||
mock_chat.reset_mock()
|
||||
|
||||
# run() should not stream
|
||||
agent.run("Hello")
|
||||
run_streamed = mock_stream.called
|
||||
|
||||
# They should behave differently
|
||||
assert start_streamed
|
||||
assert not run_streamed
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Unit tests for Agent output parameter with verbose preset.
|
||||
|
||||
Tests the fix for the bug where output="verbose" would return None
|
||||
because reasoning_steps=True in the verbose preset was causing the code
|
||||
to try to return reasoning_content, but this attribute exists as None
|
||||
for non-reasoning models.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
|
||||
class TestReasoningContentLogic:
|
||||
"""Test the reasoning_content check logic directly."""
|
||||
|
||||
def test_condition_with_none_reasoning_content(self):
|
||||
"""Verify the fix: condition should be False when reasoning_content is None."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = None
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Old buggy condition:
|
||||
# if reasoning_steps and hasattr(mock_message, 'reasoning_content'):
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition == True, "Old condition would be True (the bug)"
|
||||
|
||||
# Fixed condition:
|
||||
# if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
# The truthiness of the whole expression should be False
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Fixed condition should be falsy when reasoning_content is None"
|
||||
|
||||
def test_condition_with_empty_string_reasoning_content(self):
|
||||
"""Verify the fix: condition should be False when reasoning_content is empty string."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = ""
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Old condition would True (bug)
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition == True
|
||||
|
||||
# Fixed condition should be falsy
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Fixed condition should be falsy when reasoning_content is empty string"
|
||||
|
||||
def test_condition_with_actual_reasoning_content(self):
|
||||
"""Verify the fix: condition should be True when reasoning_content has content."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "This is my reasoning process..."
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Both old and new condition should be truthy
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition
|
||||
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert fixed_condition, "Fixed condition should be truthy when reasoning_content has content"
|
||||
|
||||
def test_condition_when_reasoning_steps_false(self):
|
||||
"""Verify that when reasoning_steps is False, condition is always False."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "This is my reasoning process..."
|
||||
|
||||
reasoning_steps = False
|
||||
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Condition should be falsy when reasoning_steps is False"
|
||||
|
||||
def test_if_statement_behavior_with_none(self):
|
||||
"""Verify that an if statement using the condition skips when reasoning_content is None."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = None
|
||||
|
||||
reasoning_steps = True
|
||||
execution_path = "not_taken"
|
||||
|
||||
# This is how the fixed code behaves
|
||||
if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
execution_path = "reasoning_path"
|
||||
else:
|
||||
execution_path = "regular_path"
|
||||
|
||||
assert execution_path == "regular_path", "Should take regular path when reasoning_content is None"
|
||||
|
||||
def test_if_statement_behavior_with_reasoning(self):
|
||||
"""Verify that an if statement using the condition enters when reasoning_content has value."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "Reasoning steps here..."
|
||||
|
||||
reasoning_steps = True
|
||||
execution_path = "not_taken"
|
||||
|
||||
if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
execution_path = "reasoning_path"
|
||||
else:
|
||||
execution_path = "regular_path"
|
||||
|
||||
assert execution_path == "reasoning_path", "Should take reasoning path when reasoning_content has value"
|
||||
|
||||
|
||||
class TestVerbosePresetConfig:
|
||||
"""Test that the verbose preset has the expected configuration."""
|
||||
|
||||
def test_verbose_preset_has_reasoning_steps_true(self):
|
||||
"""Verify that the verbose preset enables reasoning_steps."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset is not None, "verbose preset should exist"
|
||||
assert verbose_preset.get("reasoning_steps") == True, "verbose preset should have reasoning_steps=True"
|
||||
|
||||
def test_verbose_preset_has_verbose_true(self):
|
||||
"""Verify that the verbose preset enables verbose output."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset.get("verbose") == True, "verbose preset should have verbose=True"
|
||||
|
||||
def test_verbose_preset_has_stream_false(self):
|
||||
"""Verify that the verbose preset does not enable streaming by default."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset.get("stream") == False, "verbose preset should have stream=False"
|
||||
|
||||
|
||||
class TestChatCompletionMessageDataclass:
|
||||
"""Test the ChatCompletionMessage dataclass has reasoning_content attribute."""
|
||||
|
||||
def test_chat_completion_message_has_reasoning_content(self):
|
||||
"""Verify ChatCompletionMessage has reasoning_content attribute that defaults to None."""
|
||||
from praisonaiagents.llm.openai_client import ChatCompletionMessage
|
||||
|
||||
# Create without reasoning_content
|
||||
message = ChatCompletionMessage(content="Test", role="assistant")
|
||||
|
||||
# Should have the attribute
|
||||
assert hasattr(message, 'reasoning_content')
|
||||
# Should default to None
|
||||
assert message.reasoning_content is None
|
||||
|
||||
def test_chat_completion_message_with_reasoning(self):
|
||||
"""Verify ChatCompletionMessage can have reasoning_content set."""
|
||||
from praisonaiagents.llm.openai_client import ChatCompletionMessage
|
||||
|
||||
message = ChatCompletionMessage(
|
||||
content="Test",
|
||||
role="assistant",
|
||||
reasoning_content="This is my reasoning..."
|
||||
)
|
||||
|
||||
assert message.reasoning_content == "This is my reasoning..."
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Tests for wrapper agent modernization - ensuring legacy args are mapped to consolidated params.
|
||||
|
||||
TDD: These tests verify that wrapper classes (PromptExpanderAgent, QueryRewriterAgent)
|
||||
correctly use consolidated params (output=) instead of legacy args (verbose=, markdown=).
|
||||
"""
|
||||
|
||||
|
||||
class TestPromptExpanderAgentModernization:
|
||||
"""Test PromptExpanderAgent uses consolidated output param."""
|
||||
|
||||
def test_agent_uses_output_config_not_legacy_args(self):
|
||||
"""PromptExpanderAgent should pass output= to Agent, not verbose=/markdown=."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
# Create wrapper with verbose=True
|
||||
expander = PromptExpanderAgent(verbose=True)
|
||||
|
||||
# Access the lazy-initialized agent
|
||||
agent = expander.agent
|
||||
|
||||
# Verify: Agent should have output config set, not legacy verbose/markdown as direct args
|
||||
# The Agent stores resolved output in self.verbose and self.markdown after resolution
|
||||
# But the key is that the wrapper passed output= dict, not verbose=/markdown= directly
|
||||
assert hasattr(agent, 'verbose')
|
||||
assert hasattr(agent, 'markdown')
|
||||
# verbose should be True (from output config)
|
||||
assert agent.verbose == True
|
||||
# markdown should be False (from output config)
|
||||
assert agent.markdown == False
|
||||
|
||||
def test_agent_verbose_false_propagates(self):
|
||||
"""PromptExpanderAgent with verbose=False should propagate correctly."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
expander = PromptExpanderAgent(verbose=False)
|
||||
agent = expander.agent
|
||||
|
||||
assert agent.verbose == False
|
||||
assert agent.markdown == False
|
||||
|
||||
|
||||
class TestQueryRewriterAgentModernization:
|
||||
"""Test QueryRewriterAgent uses consolidated output param."""
|
||||
|
||||
def test_agent_uses_output_config_not_legacy_args(self):
|
||||
"""QueryRewriterAgent should pass output= to Agent, not verbose=/markdown=."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
# Create wrapper with verbose=True
|
||||
rewriter = QueryRewriterAgent(verbose=True)
|
||||
|
||||
# Access the lazy-initialized agent
|
||||
agent = rewriter.agent
|
||||
|
||||
# Verify output config was applied
|
||||
assert agent.verbose == True
|
||||
assert agent.markdown == False
|
||||
|
||||
def test_agent_verbose_false_propagates(self):
|
||||
"""QueryRewriterAgent with verbose=False should propagate correctly."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
rewriter = QueryRewriterAgent(verbose=False)
|
||||
agent = rewriter.agent
|
||||
|
||||
assert agent.verbose == False
|
||||
assert agent.markdown == False
|
||||
|
||||
|
||||
class TestProcessManagerAgentModernization:
|
||||
"""Test Process class manager agents use consolidated params."""
|
||||
|
||||
def test_manager_agent_creation_uses_consolidated_params(self):
|
||||
"""Process manager agent should use output= and reflection=, not legacy args."""
|
||||
# This is a structural test - we verify the code pattern
|
||||
import inspect
|
||||
from praisonaiagents.process.process import Process
|
||||
|
||||
# Get source of hierarchical method (sync version)
|
||||
source = inspect.getsource(Process.hierarchical)
|
||||
|
||||
# After fix: should contain output= and reflection=
|
||||
assert 'output=' in source, "Process.hierarchical should use output= consolidated param"
|
||||
assert 'reflection=' in source, "Process.hierarchical should use reflection= consolidated param"
|
||||
|
||||
# Should NOT contain legacy args passed to Agent
|
||||
assert 'self_reflect=' not in source, "Process.hierarchical should not use legacy self_reflect="
|
||||
|
||||
|
||||
class TestAutoAgentsConsolidatedParams:
|
||||
"""Test AutoAgents already uses consolidated params correctly."""
|
||||
|
||||
def test_autoagents_has_consolidated_params(self):
|
||||
"""AutoAgents should accept consolidated params."""
|
||||
import inspect
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
# Should have consolidated params
|
||||
assert 'output' in params
|
||||
assert 'reflection' in params
|
||||
assert 'caching' in params
|
||||
assert 'knowledge' in params
|
||||
assert 'execution' in params
|
||||
assert 'guardrails' in params
|
||||
assert 'web' in params
|
||||
assert 'hooks' in params
|
||||
|
||||
def test_autoagents_consolidated_takes_precedence(self):
|
||||
"""AutoAgents consolidated params should override legacy params."""
|
||||
import inspect
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
# Get source of _create_agents_and_tasks
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should pass consolidated params to Agent
|
||||
assert 'output=self._output' in source
|
||||
assert 'reflection=self._reflection' in source
|
||||
assert 'caching=self._caching' in source
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
TDD Tests for AutoAgents SDK Parity.
|
||||
|
||||
Tests that AutoAgents correctly maps legacy params to consolidated params
|
||||
and passes them through to Agent instances.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestAutoAgentsLegacyParamMapping:
|
||||
"""Test that AutoAgents maps legacy params to consolidated params."""
|
||||
|
||||
def test_autoagents_accepts_legacy_verbose_param(self):
|
||||
"""AutoAgents should accept verbose param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'verbose' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_markdown_param(self):
|
||||
"""AutoAgents should accept markdown param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'markdown' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_self_reflect_param(self):
|
||||
"""AutoAgents should accept self_reflect param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'self_reflect' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_cache_param(self):
|
||||
"""AutoAgents should accept cache param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'cache' in params
|
||||
|
||||
def test_autoagents_accepts_consolidated_output_param(self):
|
||||
"""AutoAgents should accept consolidated output param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have output param yet
|
||||
assert 'output' in params, "AutoAgents should accept consolidated 'output' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_reflection_param(self):
|
||||
"""AutoAgents should accept consolidated reflection param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have reflection param yet
|
||||
assert 'reflection' in params, "AutoAgents should accept consolidated 'reflection' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_caching_param(self):
|
||||
"""AutoAgents should accept consolidated caching param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have caching param yet
|
||||
assert 'caching' in params, "AutoAgents should accept consolidated 'caching' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_knowledge_param(self):
|
||||
"""AutoAgents should accept consolidated knowledge param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have knowledge param yet
|
||||
assert 'knowledge' in params, "AutoAgents should accept consolidated 'knowledge' param"
|
||||
|
||||
|
||||
class TestAutoAgentsParamPrecedence:
|
||||
"""Test that consolidated params take precedence over legacy params."""
|
||||
|
||||
def test_consolidated_output_overrides_legacy_verbose(self):
|
||||
"""Consolidated output param should override legacy verbose."""
|
||||
# This tests the mapping logic once implemented
|
||||
pass # Placeholder - will be implemented after AutoAgents is updated
|
||||
|
||||
def test_consolidated_reflection_overrides_legacy_self_reflect(self):
|
||||
"""Consolidated reflection param should override legacy self_reflect."""
|
||||
pass # Placeholder
|
||||
|
||||
def test_consolidated_caching_overrides_legacy_cache(self):
|
||||
"""Consolidated caching param should override legacy cache."""
|
||||
pass # Placeholder
|
||||
|
||||
|
||||
class TestAutoAgentsAgentCreation:
|
||||
"""Test that AutoAgents creates Agent instances with correct params."""
|
||||
|
||||
@patch('praisonaiagents.agents.autoagents.AutoAgents._generate_config')
|
||||
@patch('praisonaiagents.agents.autoagents.AutoAgents._create_agents_and_tasks')
|
||||
def test_agent_receives_consolidated_params(self, mock_create, mock_config):
|
||||
"""Agents created by AutoAgents should receive consolidated params."""
|
||||
# Setup mocks to avoid LLM calls
|
||||
mock_config.return_value = MagicMock()
|
||||
mock_create.return_value = ([], [])
|
||||
|
||||
# This test verifies the param passing logic
|
||||
# Will be more detailed once implementation is done
|
||||
pass
|
||||
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
TDD tests for delegator.py protocol compliance.
|
||||
|
||||
Tests that SubagentDelegator creates agents using consolidated params
|
||||
instead of legacy params like verbose=, max_iter=.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class TestDelegatorProtocolCompliance:
|
||||
"""Test that delegator uses consolidated params when creating agents."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_uses_output_not_verbose(self):
|
||||
"""Delegator should use output='silent' instead of verbose=False."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
# Create a delegator
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
# Create a profile and task
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="Test prompt",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-1",
|
||||
agent_name="test_agent",
|
||||
objective="Test task",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
# Mock Agent to capture how it's called (Agent is imported inside _create_agent)
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
MockAgent.return_value = Mock()
|
||||
|
||||
await delegator._create_agent(profile, task)
|
||||
|
||||
# Verify Agent was called
|
||||
MockAgent.assert_called_once()
|
||||
call_kwargs = MockAgent.call_args[1]
|
||||
|
||||
# Should NOT have verbose= param
|
||||
assert 'verbose' not in call_kwargs, "Should not use legacy verbose= param"
|
||||
|
||||
# Should have output= param with 'silent' preset
|
||||
assert 'output' in call_kwargs, "Should use consolidated output= param"
|
||||
assert call_kwargs['output'] == 'silent', "Should use output='silent' for quiet mode"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_uses_execution_not_max_iter(self):
|
||||
"""Delegator should use execution= instead of max_iter=."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="Test prompt",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-2",
|
||||
agent_name="test_agent",
|
||||
objective="Test task",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
MockAgent.return_value = Mock()
|
||||
|
||||
await delegator._create_agent(profile, task)
|
||||
|
||||
call_kwargs = MockAgent.call_args[1]
|
||||
|
||||
# Should NOT have max_iter= param
|
||||
assert 'max_iter' not in call_kwargs, "Should not use legacy max_iter= param"
|
||||
|
||||
# Should have execution= param with max_iter in config
|
||||
assert 'execution' in call_kwargs, "Should use consolidated execution= param"
|
||||
exec_config = call_kwargs['execution']
|
||||
assert isinstance(exec_config, dict), "execution should be a dict config"
|
||||
assert 'max_iter' in exec_config, "execution config should contain max_iter"
|
||||
assert exec_config['max_iter'] == 5, "max_iter should be min(task.max_steps, profile.max_steps)"
|
||||
|
||||
|
||||
class TestDelegatorBackwardCompatibility:
|
||||
"""Test that delegator still works correctly after modernization."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_returns_valid_agent(self):
|
||||
"""Created agent should be usable."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="You are a helpful assistant.",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-3",
|
||||
agent_name="test_agent",
|
||||
objective="Say hello",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.chat = Mock(return_value="Hello!")
|
||||
MockAgent.return_value = mock_agent
|
||||
|
||||
agent = await delegator._create_agent(profile, task)
|
||||
|
||||
# Agent should be returned
|
||||
assert agent is not None
|
||||
assert agent == mock_agent
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Tests for LearnScope enum - verifying PRIVATE/SHARED values only.
|
||||
|
||||
These tests verify that:
|
||||
1. LearnScope only has PRIVATE and SHARED values
|
||||
2. TEAM and ORG values do NOT exist
|
||||
3. LearnConfig defaults to PRIVATE scope
|
||||
4. BaseStore defaults to "private" scope
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestLearnScopeValues:
|
||||
"""Test LearnScope enum has exactly the right values."""
|
||||
|
||||
def test_learn_scope_has_private(self):
|
||||
"""LearnScope should have PRIVATE value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert hasattr(LearnScope, 'PRIVATE')
|
||||
assert LearnScope.PRIVATE.value == "private"
|
||||
|
||||
def test_learn_scope_has_shared(self):
|
||||
"""LearnScope should have SHARED value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert hasattr(LearnScope, 'SHARED')
|
||||
assert LearnScope.SHARED.value == "shared"
|
||||
|
||||
def test_learn_scope_does_not_have_team(self):
|
||||
"""LearnScope should NOT have TEAM value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'TEAM'), "TEAM should be removed from LearnScope"
|
||||
|
||||
def test_learn_scope_does_not_have_org(self):
|
||||
"""LearnScope should NOT have ORG value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'ORG'), "ORG should be removed from LearnScope"
|
||||
|
||||
def test_learn_scope_does_not_have_user(self):
|
||||
"""LearnScope should NOT have USER value (replaced by PRIVATE)."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'USER'), "USER should be replaced by PRIVATE"
|
||||
|
||||
def test_learn_scope_does_not_have_global(self):
|
||||
"""LearnScope should NOT have GLOBAL value (replaced by SHARED)."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'GLOBAL'), "GLOBAL should be replaced by SHARED"
|
||||
|
||||
def test_learn_scope_has_exactly_two_values(self):
|
||||
"""LearnScope should have exactly 2 values: PRIVATE and SHARED."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
all_values = list(LearnScope)
|
||||
assert len(all_values) == 2, f"Expected 2 values, got {len(all_values)}: {all_values}"
|
||||
|
||||
|
||||
class TestLearnConfigDefaults:
|
||||
"""Test LearnConfig uses correct default scope."""
|
||||
|
||||
def test_learn_config_default_scope_is_private(self):
|
||||
"""LearnConfig should default to PRIVATE scope."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig()
|
||||
assert config.scope == LearnScope.PRIVATE
|
||||
|
||||
def test_learn_config_scope_can_be_set_to_shared(self):
|
||||
"""LearnConfig scope can be set to SHARED."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig(scope=LearnScope.SHARED)
|
||||
assert config.scope == LearnScope.SHARED
|
||||
|
||||
def test_learn_config_to_dict_includes_scope(self):
|
||||
"""LearnConfig.to_dict() should include scope with correct value."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig()
|
||||
d = config.to_dict()
|
||||
assert d['scope'] == 'private'
|
||||
|
||||
config2 = LearnConfig(scope=LearnScope.SHARED)
|
||||
d2 = config2.to_dict()
|
||||
assert d2['scope'] == 'shared'
|
||||
|
||||
|
||||
class TestBaseStoreDefaults:
|
||||
"""Test BaseStore uses correct default scope."""
|
||||
|
||||
def test_base_store_default_scope_is_private(self):
|
||||
"""BaseStore should default to 'private' scope."""
|
||||
import inspect
|
||||
from praisonaiagents.memory.learn.stores import BaseStore
|
||||
|
||||
sig = inspect.signature(BaseStore.__init__)
|
||||
scope_param = sig.parameters.get('scope')
|
||||
assert scope_param is not None
|
||||
assert scope_param.default == 'private', f"Expected 'private', got '{scope_param.default}'"
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Tests for precedence ladder resolvers."""
|
||||
|
||||
import pytest
|
||||
from praisonaiagents.config.feature_configs import (
|
||||
MemoryConfig,
|
||||
MemoryBackend,
|
||||
KnowledgeConfig,
|
||||
PlanningConfig,
|
||||
ReflectionConfig,
|
||||
GuardrailConfig,
|
||||
WebConfig,
|
||||
OutputConfig,
|
||||
ExecutionConfig,
|
||||
CachingConfig,
|
||||
AutonomyConfig,
|
||||
resolve_memory,
|
||||
resolve_knowledge,
|
||||
resolve_planning,
|
||||
resolve_reflection,
|
||||
resolve_guardrails,
|
||||
resolve_web,
|
||||
resolve_output,
|
||||
resolve_execution,
|
||||
resolve_caching,
|
||||
resolve_autonomy,
|
||||
)
|
||||
|
||||
|
||||
class TestResolveMemory:
|
||||
"""Test resolve_memory precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
"""Default (None) returns None."""
|
||||
assert resolve_memory(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
"""False returns None (disabled)."""
|
||||
assert resolve_memory(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
"""True returns MemoryConfig with defaults."""
|
||||
result = resolve_memory(True)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.FILE
|
||||
|
||||
def test_string_backend(self):
|
||||
"""String returns MemoryConfig with that backend."""
|
||||
result = resolve_memory("redis")
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.REDIS
|
||||
|
||||
def test_string_custom_backend(self):
|
||||
"""Custom string backend is preserved."""
|
||||
result = resolve_memory("custom_backend")
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == "custom_backend"
|
||||
|
||||
def test_dict_expansion(self):
|
||||
"""Dict expands to MemoryConfig."""
|
||||
result = resolve_memory({
|
||||
"backend": "sqlite",
|
||||
"user_id": "alice",
|
||||
"auto_memory": True,
|
||||
})
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.SQLITE
|
||||
assert result.user_id == "alice"
|
||||
assert result.auto_memory is True
|
||||
|
||||
def test_config_passthrough(self):
|
||||
"""MemoryConfig passes through unchanged."""
|
||||
config = MemoryConfig(backend=MemoryBackend.POSTGRES, user_id="bob")
|
||||
result = resolve_memory(config)
|
||||
assert result is config
|
||||
|
||||
def test_instance_passthrough(self):
|
||||
"""Instance (non-config object) passes through."""
|
||||
class MockMemoryManager:
|
||||
pass
|
||||
instance = MockMemoryManager()
|
||||
result = resolve_memory(instance)
|
||||
assert result is instance
|
||||
|
||||
|
||||
class TestResolveKnowledge:
|
||||
"""Test resolve_knowledge precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_knowledge(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_knowledge(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_knowledge(True)
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
|
||||
def test_string_single_source(self):
|
||||
result = resolve_knowledge("docs/")
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/"]
|
||||
|
||||
def test_array_sources(self):
|
||||
result = resolve_knowledge(["docs/", "data.pdf"])
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/", "data.pdf"]
|
||||
|
||||
def test_dict_expansion(self):
|
||||
result = resolve_knowledge({
|
||||
"sources": ["docs/"],
|
||||
"rerank": True,
|
||||
})
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/"]
|
||||
assert result.rerank is True
|
||||
|
||||
def test_config_passthrough(self):
|
||||
config = KnowledgeConfig(sources=["test/"])
|
||||
result = resolve_knowledge(config)
|
||||
assert result is config
|
||||
|
||||
|
||||
class TestResolvePlanning:
|
||||
"""Test resolve_planning precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_planning(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_planning(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_planning(True)
|
||||
assert isinstance(result, PlanningConfig)
|
||||
|
||||
def test_config_passthrough(self):
|
||||
config = PlanningConfig()
|
||||
result = resolve_planning(config)
|
||||
assert result is config
|
||||
|
||||
|
||||
class TestResolveReflection:
|
||||
"""Test resolve_reflection precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_reflection(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_reflection(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_reflection(True)
|
||||
assert isinstance(result, ReflectionConfig)
|
||||
|
||||
|
||||
class TestResolveGuardrails:
|
||||
"""Test resolve_guardrails precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_guardrails(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_guardrails(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_guardrails(True)
|
||||
assert isinstance(result, GuardrailConfig)
|
||||
|
||||
def test_callable_validator(self):
|
||||
def my_validator(output):
|
||||
return True, output
|
||||
result = resolve_guardrails(my_validator)
|
||||
assert isinstance(result, GuardrailConfig)
|
||||
assert result.validator is my_validator
|
||||
|
||||
|
||||
class TestResolveWeb:
|
||||
"""Test resolve_web precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_web(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_web(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_web(True)
|
||||
assert isinstance(result, WebConfig)
|
||||
|
||||
|
||||
class TestResolveCaching:
|
||||
"""Test resolve_caching precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_caching(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_caching(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_caching(True)
|
||||
assert isinstance(result, CachingConfig)
|
||||
|
||||
|
||||
class TestResolveAutonomy:
|
||||
"""Test resolve_autonomy precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_autonomy(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_autonomy(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_autonomy(True)
|
||||
assert isinstance(result, AutonomyConfig)
|
||||
|
||||
|
||||
class TestNoCortexParam:
|
||||
"""Test that cortex parameter does not exist."""
|
||||
|
||||
def test_agent_has_no_cortex_param(self):
|
||||
"""Agent.__init__ should not have cortex parameter."""
|
||||
import inspect
|
||||
from praisonaiagents import Agent
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
param_names = list(sig.parameters.keys())
|
||||
|
||||
assert "cortex" not in param_names, "cortex param should be removed from Agent"
|
||||
|
||||
def test_no_cortex_module_in_agents(self):
|
||||
"""praisonaiagents should not have cortex module."""
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec("praisonaiagents.cortex")
|
||||
assert spec is None, "praisonaiagents.cortex module should not exist"
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Tests for the learn module - continuous learning within memory system."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope, MemoryConfig
|
||||
from praisonaiagents.memory.learn.stores import (
|
||||
PersonaStore,
|
||||
InsightStore,
|
||||
ThreadStore,
|
||||
PatternStore,
|
||||
DecisionStore,
|
||||
FeedbackStore,
|
||||
ImprovementStore,
|
||||
LearnEntry,
|
||||
)
|
||||
from praisonaiagents.memory.learn.manager import LearnManager
|
||||
|
||||
|
||||
class TestLearnConfig:
|
||||
"""Test LearnConfig dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default LearnConfig values."""
|
||||
config = LearnConfig()
|
||||
assert config.persona is True
|
||||
assert config.insights is True
|
||||
assert config.thread is True
|
||||
assert config.patterns is False
|
||||
assert config.decisions is False
|
||||
assert config.feedback is False
|
||||
assert config.improvements is False
|
||||
assert config.scope == LearnScope.USER
|
||||
assert config.auto_consolidate is True
|
||||
assert config.retention_days is None
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test LearnConfig with custom values."""
|
||||
config = LearnConfig(
|
||||
persona=False,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
scope=LearnScope.TEAM,
|
||||
retention_days=30,
|
||||
)
|
||||
assert config.persona is False
|
||||
assert config.patterns is True
|
||||
assert config.scope == LearnScope.TEAM
|
||||
assert config.retention_days == 30
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test LearnConfig to_dict method."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
d = config.to_dict()
|
||||
assert d["persona"] is True
|
||||
assert d["insights"] is True
|
||||
assert d["scope"] == "user"
|
||||
|
||||
|
||||
class TestMemoryConfigWithLearn:
|
||||
"""Test MemoryConfig with learn field."""
|
||||
|
||||
def test_learn_none_by_default(self):
|
||||
"""Test that learn is None by default."""
|
||||
config = MemoryConfig()
|
||||
assert config.learn is None
|
||||
|
||||
def test_learn_bool_true(self):
|
||||
"""Test learn=True."""
|
||||
config = MemoryConfig(learn=True)
|
||||
assert config.learn is True
|
||||
d = config.to_dict()
|
||||
assert d["learn"] is not None
|
||||
assert d["learn"]["persona"] is True
|
||||
|
||||
def test_learn_config_instance(self):
|
||||
"""Test learn=LearnConfig(...)."""
|
||||
learn_config = LearnConfig(persona=True, patterns=True)
|
||||
config = MemoryConfig(learn=learn_config)
|
||||
assert config.learn is learn_config
|
||||
d = config.to_dict()
|
||||
assert d["learn"]["patterns"] is True
|
||||
|
||||
|
||||
class TestLearnEntry:
|
||||
"""Test LearnEntry dataclass."""
|
||||
|
||||
def test_create_entry(self):
|
||||
"""Test creating a LearnEntry."""
|
||||
entry = LearnEntry(
|
||||
id="test_1",
|
||||
content="Test content",
|
||||
metadata={"key": "value"},
|
||||
)
|
||||
assert entry.id == "test_1"
|
||||
assert entry.content == "Test content"
|
||||
assert entry.metadata["key"] == "value"
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test LearnEntry to_dict."""
|
||||
entry = LearnEntry(id="test_1", content="Test")
|
||||
d = entry.to_dict()
|
||||
assert d["id"] == "test_1"
|
||||
assert d["content"] == "Test"
|
||||
assert "created_at" in d
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test LearnEntry from_dict."""
|
||||
data = {
|
||||
"id": "test_1",
|
||||
"content": "Test content",
|
||||
"metadata": {"key": "value"},
|
||||
}
|
||||
entry = LearnEntry.from_dict(data)
|
||||
assert entry.id == "test_1"
|
||||
assert entry.content == "Test content"
|
||||
|
||||
|
||||
class TestPersonaStore:
|
||||
"""Test PersonaStore."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.store_path = os.path.join(self.temp_dir, "persona.json")
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_add_preference(self):
|
||||
"""Test adding a preference."""
|
||||
store = PersonaStore(store_path=self.store_path, user_id="test_user")
|
||||
entry = store.add_preference("User prefers concise responses", "communication")
|
||||
assert entry is not None
|
||||
assert "concise" in entry.content
|
||||
assert entry.metadata["category"] == "communication"
|
||||
|
||||
def test_search(self):
|
||||
"""Test searching entries."""
|
||||
store = PersonaStore(store_path=self.store_path, user_id="test_user")
|
||||
store.add_preference("User likes Python", "programming")
|
||||
store.add_preference("User prefers dark mode", "ui")
|
||||
|
||||
results = store.search("Python")
|
||||
assert len(results) == 1
|
||||
assert "Python" in results[0].content
|
||||
|
||||
|
||||
class TestInsightStore:
|
||||
"""Test InsightStore."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.store_path = os.path.join(self.temp_dir, "insights.json")
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_add_insight(self):
|
||||
"""Test adding an insight."""
|
||||
store = InsightStore(store_path=self.store_path, user_id="test_user")
|
||||
entry = store.add_insight("User works in data science", "conversation")
|
||||
assert entry is not None
|
||||
assert "data science" in entry.content
|
||||
|
||||
|
||||
class TestLearnManager:
|
||||
"""Test LearnManager."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_create_manager_with_defaults(self):
|
||||
"""Test creating LearnManager with default config."""
|
||||
config = LearnConfig()
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
assert "persona" in manager._stores
|
||||
assert "insights" in manager._stores
|
||||
assert "threads" in manager._stores
|
||||
|
||||
def test_capture_persona(self):
|
||||
"""Test capturing persona."""
|
||||
config = LearnConfig(persona=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
entry = manager.capture_persona("User prefers detailed explanations")
|
||||
assert entry is not None
|
||||
assert "detailed" in entry.content
|
||||
|
||||
def test_capture_insight(self):
|
||||
"""Test capturing insight."""
|
||||
config = LearnConfig(insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
entry = manager.capture_insight("User is experienced with Python")
|
||||
assert entry is not None
|
||||
|
||||
def test_get_learning_context(self):
|
||||
"""Test getting learning context."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User likes concise code")
|
||||
manager.capture_insight("User knows Python")
|
||||
|
||||
context = manager.get_learning_context()
|
||||
assert "persona" in context
|
||||
assert "insights" in context
|
||||
assert len(context["persona"]) == 1
|
||||
assert len(context["insights"]) == 1
|
||||
|
||||
def test_search(self):
|
||||
"""Test searching across stores."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User prefers Python")
|
||||
manager.capture_insight("User knows Python well")
|
||||
|
||||
results = manager.search("Python")
|
||||
assert "persona" in results or "insights" in results
|
||||
|
||||
def test_clear_all(self):
|
||||
"""Test clearing all stores."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("Test 1")
|
||||
manager.capture_insight("Test 2")
|
||||
|
||||
cleared = manager.clear_all()
|
||||
assert cleared["persona"] == 1
|
||||
assert cleared["insights"] == 1
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test getting stats."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("Test 1")
|
||||
manager.capture_persona("Test 2")
|
||||
manager.capture_insight("Test 3")
|
||||
|
||||
stats = manager.get_stats()
|
||||
assert stats["persona"] == 2
|
||||
assert stats["insights"] == 1
|
||||
|
||||
def test_to_system_prompt_context(self):
|
||||
"""Test generating system prompt context."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User prefers concise responses")
|
||||
manager.capture_insight("User is a Python developer")
|
||||
|
||||
context = manager.to_system_prompt_context()
|
||||
assert "User Preferences" in context
|
||||
assert "concise" in context
|
||||
assert "Learned Insights" in context
|
||||
assert "Python" in context
|
||||
|
||||
|
||||
class TestNoCortexParam:
|
||||
"""Test that cortex parameter does not exist."""
|
||||
|
||||
def test_agent_has_no_cortex_param(self):
|
||||
"""Agent.__init__ should not have cortex parameter."""
|
||||
import inspect
|
||||
from praisonaiagents import Agent
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
param_names = list(sig.parameters.keys())
|
||||
|
||||
assert "cortex" not in param_names, "cortex param should be removed from Agent"
|
||||
|
||||
def test_no_cortex_module_in_agents(self):
|
||||
"""praisonaiagents should not have cortex module."""
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec("praisonaiagents.cortex")
|
||||
assert spec is None, "praisonaiagents.cortex module should not exist"
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for streaming module."""
|
||||
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
Unit tests for streaming events module.
|
||||
|
||||
Tests cover:
|
||||
- StreamEvent creation and representation
|
||||
- StreamEventType enum values
|
||||
- StreamMetrics calculations (TTFT, stream duration, total time)
|
||||
- StreamEventEmitter callback management
|
||||
- create_text_printer_callback and create_metrics_callback helpers
|
||||
"""
|
||||
|
||||
import time
|
||||
from praisonaiagents.streaming.events import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
create_text_printer_callback,
|
||||
create_metrics_callback,
|
||||
)
|
||||
|
||||
|
||||
class TestStreamEventType:
|
||||
"""Tests for StreamEventType enum."""
|
||||
|
||||
def test_event_types_exist(self):
|
||||
"""All required event types should exist."""
|
||||
assert StreamEventType.REQUEST_START.value == "request_start"
|
||||
assert StreamEventType.HEADERS_RECEIVED.value == "headers_received"
|
||||
assert StreamEventType.FIRST_TOKEN.value == "first_token"
|
||||
assert StreamEventType.DELTA_TEXT.value == "delta_text"
|
||||
assert StreamEventType.DELTA_TOOL_CALL.value == "delta_tool_call"
|
||||
assert StreamEventType.TOOL_CALL_END.value == "tool_call_end"
|
||||
assert StreamEventType.LAST_TOKEN.value == "last_token"
|
||||
assert StreamEventType.STREAM_END.value == "stream_end"
|
||||
assert StreamEventType.ERROR.value == "error"
|
||||
|
||||
|
||||
class TestStreamEvent:
|
||||
"""Tests for StreamEvent dataclass."""
|
||||
|
||||
def test_create_basic_event(self):
|
||||
"""Create a basic event with type only."""
|
||||
event = StreamEvent(type=StreamEventType.REQUEST_START)
|
||||
assert event.type == StreamEventType.REQUEST_START
|
||||
assert event.timestamp > 0
|
||||
assert event.content is None
|
||||
assert event.tool_call is None
|
||||
assert event.metadata is None
|
||||
assert event.error is None
|
||||
|
||||
def test_create_event_with_content(self):
|
||||
"""Create an event with content."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
content="Hello"
|
||||
)
|
||||
assert event.type == StreamEventType.DELTA_TEXT
|
||||
assert event.content == "Hello"
|
||||
|
||||
def test_create_event_with_tool_call(self):
|
||||
"""Create an event with tool call data."""
|
||||
tool_data = {"name": "calculator", "arguments": '{"x": 1}'}
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
tool_call=tool_data
|
||||
)
|
||||
assert event.type == StreamEventType.DELTA_TOOL_CALL
|
||||
assert event.tool_call == tool_data
|
||||
|
||||
def test_create_event_with_metadata(self):
|
||||
"""Create an event with metadata."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
metadata={"model": "gpt-4o-mini", "message_count": 2}
|
||||
)
|
||||
assert event.metadata["model"] == "gpt-4o-mini"
|
||||
assert event.metadata["message_count"] == 2
|
||||
|
||||
def test_create_error_event(self):
|
||||
"""Create an error event."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.ERROR,
|
||||
error="Connection timeout"
|
||||
)
|
||||
assert event.type == StreamEventType.ERROR
|
||||
assert event.error == "Connection timeout"
|
||||
|
||||
def test_event_repr(self):
|
||||
"""Event repr should be informative."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
content="Hello world"
|
||||
)
|
||||
repr_str = repr(event)
|
||||
assert "DELTA_TEXT" in repr_str or "delta_text" in repr_str
|
||||
assert "Hello world" in repr_str
|
||||
|
||||
|
||||
class TestStreamMetrics:
|
||||
"""Tests for StreamMetrics dataclass."""
|
||||
|
||||
def test_default_metrics(self):
|
||||
"""Default metrics should be zero."""
|
||||
metrics = StreamMetrics()
|
||||
assert metrics.request_start == 0.0
|
||||
assert metrics.headers_received == 0.0
|
||||
assert metrics.first_token == 0.0
|
||||
assert metrics.last_token == 0.0
|
||||
assert metrics.stream_end == 0.0
|
||||
assert metrics.token_count == 0
|
||||
|
||||
def test_ttft_calculation(self):
|
||||
"""TTFT should be first_token - request_start."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
assert metrics.ttft == 0.5
|
||||
|
||||
def test_ttft_zero_when_no_first_token(self):
|
||||
"""TTFT should be 0 when first_token not set."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
assert metrics.ttft == 0.0
|
||||
|
||||
def test_stream_duration_calculation(self):
|
||||
"""Stream duration should be last_token - first_token."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.first_token = 100.0
|
||||
metrics.last_token = 102.0
|
||||
assert metrics.stream_duration == 2.0
|
||||
|
||||
def test_total_time_calculation(self):
|
||||
"""Total time should be stream_end - request_start."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.stream_end = 103.0
|
||||
assert metrics.total_time == 3.0
|
||||
|
||||
def test_tokens_per_second(self):
|
||||
"""Tokens per second calculation."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.first_token = 100.0
|
||||
metrics.last_token = 102.0 # 2 second duration
|
||||
metrics.token_count = 100
|
||||
assert metrics.tokens_per_second == 50.0
|
||||
|
||||
def test_tokens_per_second_zero_duration(self):
|
||||
"""Tokens per second should be 0 when duration is 0."""
|
||||
metrics = StreamMetrics()
|
||||
assert metrics.tokens_per_second == 0.0
|
||||
|
||||
def test_update_from_event(self):
|
||||
"""Metrics should update from events."""
|
||||
metrics = StreamMetrics()
|
||||
|
||||
# Request start
|
||||
event1 = StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0)
|
||||
metrics.update_from_event(event1)
|
||||
assert metrics.request_start == 100.0
|
||||
|
||||
# Headers received
|
||||
event2 = StreamEvent(type=StreamEventType.HEADERS_RECEIVED, timestamp=100.2)
|
||||
metrics.update_from_event(event2)
|
||||
assert metrics.headers_received == 100.2
|
||||
|
||||
# First token
|
||||
event3 = StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5)
|
||||
metrics.update_from_event(event3)
|
||||
assert metrics.first_token == 100.5
|
||||
assert metrics.token_count == 1
|
||||
|
||||
# Delta text
|
||||
event4 = StreamEvent(type=StreamEventType.DELTA_TEXT, timestamp=100.6)
|
||||
metrics.update_from_event(event4)
|
||||
assert metrics.token_count == 2
|
||||
|
||||
# Last token
|
||||
event5 = StreamEvent(type=StreamEventType.LAST_TOKEN, timestamp=102.0)
|
||||
metrics.update_from_event(event5)
|
||||
assert metrics.last_token == 102.0
|
||||
|
||||
# Stream end
|
||||
event6 = StreamEvent(type=StreamEventType.STREAM_END, timestamp=102.1)
|
||||
metrics.update_from_event(event6)
|
||||
assert metrics.stream_end == 102.1
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Metrics should convert to dict."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
metrics.last_token = 102.0
|
||||
metrics.stream_end = 102.1
|
||||
metrics.token_count = 50
|
||||
|
||||
d = metrics.to_dict()
|
||||
assert "ttft_ms" in d
|
||||
assert "stream_duration_ms" in d
|
||||
assert "total_time_ms" in d
|
||||
assert "token_count" in d
|
||||
assert "tokens_per_second" in d
|
||||
|
||||
def test_format_summary(self):
|
||||
"""Metrics should format as summary string."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
metrics.last_token = 102.0
|
||||
metrics.stream_end = 102.1
|
||||
metrics.token_count = 50
|
||||
|
||||
summary = metrics.format_summary()
|
||||
assert "TTFT" in summary
|
||||
assert "Stream" in summary
|
||||
assert "Total" in summary
|
||||
assert "Tokens" in summary
|
||||
|
||||
|
||||
class TestStreamEventEmitter:
|
||||
"""Tests for StreamEventEmitter."""
|
||||
|
||||
def test_create_emitter(self):
|
||||
"""Create an emitter."""
|
||||
emitter = StreamEventEmitter()
|
||||
assert not emitter.has_callbacks
|
||||
|
||||
def test_add_callback(self):
|
||||
"""Add a callback."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(callback)
|
||||
assert emitter.has_callbacks
|
||||
|
||||
def test_remove_callback(self):
|
||||
"""Remove a callback."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(callback)
|
||||
emitter.remove_callback(callback)
|
||||
assert not emitter.has_callbacks
|
||||
|
||||
def test_emit_calls_callbacks(self):
|
||||
"""Emit should call all callbacks."""
|
||||
emitter = StreamEventEmitter()
|
||||
events1 = []
|
||||
events2 = []
|
||||
|
||||
emitter.add_callback(lambda e: events1.append(e))
|
||||
emitter.add_callback(lambda e: events2.append(e))
|
||||
|
||||
event = StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello")
|
||||
emitter.emit(event)
|
||||
|
||||
assert len(events1) == 1
|
||||
assert len(events2) == 1
|
||||
assert events1[0].content == "Hello"
|
||||
|
||||
def test_emit_with_metrics(self):
|
||||
"""Emit should update metrics when enabled."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5))
|
||||
emitter.emit(StreamEvent(type=StreamEventType.STREAM_END, timestamp=102.0))
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
assert metrics is not None
|
||||
assert metrics.request_start == 100.0
|
||||
assert metrics.first_token == 100.5
|
||||
assert metrics.stream_end == 102.0
|
||||
|
||||
def test_callback_error_does_not_break_emission(self):
|
||||
"""Callback errors should not break other callbacks."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def bad_callback(event):
|
||||
raise ValueError("Test error")
|
||||
|
||||
def good_callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(bad_callback)
|
||||
emitter.add_callback(good_callback)
|
||||
|
||||
event = StreamEvent(type=StreamEventType.DELTA_TEXT)
|
||||
emitter.emit(event) # Should not raise
|
||||
|
||||
assert len(events) == 1
|
||||
|
||||
def test_reset_metrics(self):
|
||||
"""Reset metrics should create new metrics."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
emitter.reset_metrics()
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
assert metrics.request_start == 0.0
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Tests for helper functions."""
|
||||
|
||||
def test_create_text_printer_callback(self, capsys):
|
||||
"""Text printer callback should print content."""
|
||||
callback = create_text_printer_callback(flush=True)
|
||||
|
||||
callback(StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello"))
|
||||
callback(StreamEvent(type=StreamEventType.DELTA_TEXT, content=" World"))
|
||||
callback(StreamEvent(type=StreamEventType.STREAM_END))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Hello World" in captured.out
|
||||
|
||||
def test_create_metrics_callback(self):
|
||||
"""Metrics callback should update metrics."""
|
||||
metrics = StreamMetrics()
|
||||
callback = create_metrics_callback(metrics)
|
||||
|
||||
callback(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
callback(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5))
|
||||
|
||||
assert metrics.request_start == 100.0
|
||||
assert metrics.first_token == 100.5
|
||||
|
||||
|
||||
class TestStreamingEventOrdering:
|
||||
"""Tests for event ordering and pass-through behavior."""
|
||||
|
||||
def test_events_emitted_immediately(self):
|
||||
"""Events should be emitted immediately, not buffered."""
|
||||
emitter = StreamEventEmitter()
|
||||
timestamps = []
|
||||
|
||||
def callback(event):
|
||||
timestamps.append((event.type, time.perf_counter()))
|
||||
|
||||
emitter.add_callback(callback)
|
||||
|
||||
# Simulate streaming with small delays
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START))
|
||||
time.sleep(0.01)
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN))
|
||||
time.sleep(0.01)
|
||||
emitter.emit(StreamEvent(type=StreamEventType.DELTA_TEXT))
|
||||
|
||||
# Verify events were captured in order
|
||||
assert len(timestamps) == 3
|
||||
assert timestamps[0][0] == StreamEventType.REQUEST_START
|
||||
assert timestamps[1][0] == StreamEventType.FIRST_TOKEN
|
||||
assert timestamps[2][0] == StreamEventType.DELTA_TEXT
|
||||
|
||||
# Verify timestamps are increasing (no buffering)
|
||||
assert timestamps[0][1] < timestamps[1][1] < timestamps[2][1]
|
||||
|
||||
def test_no_buffering_before_first_token(self):
|
||||
"""Events should not be buffered before first token."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
# Emit request start
|
||||
t1 = time.perf_counter()
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=t1))
|
||||
|
||||
# Small delay (simulating network)
|
||||
time.sleep(0.05)
|
||||
|
||||
# Emit first token
|
||||
t2 = time.perf_counter()
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=t2))
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
|
||||
# TTFT should reflect the actual delay
|
||||
assert metrics.ttft >= 0.04 # Allow some tolerance
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
Tests for Gap Closure - LLM Consolidation + Parity
|
||||
|
||||
TDD: These tests define the expected behavior for:
|
||||
1. GAP A: No deprecation warnings for model/llm alias
|
||||
2. GAP B: YAML parser supports consolidated params
|
||||
3. GAP C: Wrapper agents use output= (not verbose/markdown)
|
||||
4. GAP D: AutoAgents uses consolidated llm= (not function_calling_llm)
|
||||
5. GAP E: model= and llm= are aliases with NO warnings
|
||||
"""
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestNoDeprecationWarnings:
|
||||
"""GAP A + E: No deprecation warnings for model/llm or legacy params."""
|
||||
|
||||
def test_no_warning_for_model_alias(self):
|
||||
"""model= should work without any deprecation warning."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", model="gpt-4o-mini")
|
||||
|
||||
# Should NOT emit any deprecation warnings
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_llm_param(self):
|
||||
"""llm= should work without any deprecation warning."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", llm="gpt-4o-mini")
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_llm_config(self):
|
||||
"""llm_config= should work without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", llm_config={"temperature": 0.7})
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_function_calling_llm(self):
|
||||
"""function_calling_llm= should work without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", function_calling_llm="gpt-4o-mini")
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_model_and_llm_are_equivalent(self):
|
||||
"""model= and llm= should produce identical behavior."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent1 = Agent(instructions="Test", llm="gpt-4o")
|
||||
agent2 = Agent(instructions="Test", model="gpt-4o")
|
||||
|
||||
assert agent1.llm == agent2.llm == "gpt-4o"
|
||||
|
||||
|
||||
class TestAutoAgentsConsolidation:
|
||||
"""GAP D: AutoAgents uses consolidated llm= (not function_calling_llm)."""
|
||||
|
||||
def test_autoagents_does_not_pass_function_calling_llm_to_agent(self):
|
||||
"""AutoAgents should not pass function_calling_llm= to Agent()."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
|
||||
# Get the source code of the _create_agents_and_tasks method
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should NOT contain function_calling_llm= in Agent() call
|
||||
# This is a static analysis test
|
||||
assert "function_calling_llm=self.function_calling_llm" not in source, \
|
||||
"AutoAgents._create_agents_and_tasks still passes function_calling_llm= to Agent()"
|
||||
|
||||
def test_autoagents_accepts_llm_param(self):
|
||||
"""AutoAgents should accept llm= parameter."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
# Should not raise
|
||||
auto = AutoAgents(instructions="Test task", llm="gpt-4o-mini")
|
||||
assert auto.llm == "gpt-4o-mini"
|
||||
|
||||
def test_autoagents_no_deprecation_warnings(self):
|
||||
"""AutoAgents should not emit deprecation warnings."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
auto = AutoAgents(
|
||||
instructions="Test task",
|
||||
llm="gpt-4o-mini",
|
||||
function_calling_llm="gpt-4o-mini" # Legacy param
|
||||
)
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
|
||||
class TestYAMLParserConsolidation:
|
||||
"""GAP B: YAML parser supports consolidated params."""
|
||||
|
||||
def test_yaml_parser_maps_legacy_reflection_fields(self):
|
||||
"""YAML parser should map legacy reflect_llm/min_reflect/max_reflect to reflection config."""
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
yaml_content = """
|
||||
roles:
|
||||
researcher:
|
||||
role: Researcher
|
||||
goal: Research topics
|
||||
backstory: Expert researcher
|
||||
reflect_llm: gpt-4o
|
||||
min_reflect: 2
|
||||
max_reflect: 5
|
||||
tasks:
|
||||
- description: Research AI
|
||||
expected_output: Report
|
||||
"""
|
||||
|
||||
import yaml
|
||||
config = yaml.safe_load(yaml_content)
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
# The parser should handle legacy fields without error
|
||||
# and map them appropriately
|
||||
agent = parser._create_agent("researcher", config["roles"]["researcher"])
|
||||
|
||||
# Legacy fields should be stored for later use
|
||||
assert hasattr(agent, '_yaml_reflect_llm')
|
||||
assert hasattr(agent, '_yaml_min_reflect')
|
||||
assert hasattr(agent, '_yaml_max_reflect')
|
||||
|
||||
def test_yaml_parser_supports_new_reflection_structure(self):
|
||||
"""YAML parser should support new reflection: structure."""
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
yaml_content = """
|
||||
roles:
|
||||
researcher:
|
||||
role: Researcher
|
||||
goal: Research topics
|
||||
backstory: Expert researcher
|
||||
reflection:
|
||||
enabled: true
|
||||
max_iterations: 5
|
||||
min_iterations: 2
|
||||
tasks:
|
||||
- description: Research AI
|
||||
expected_output: Report
|
||||
"""
|
||||
|
||||
import yaml
|
||||
config = yaml.safe_load(yaml_content)
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
agent = parser._create_agent("researcher", config["roles"]["researcher"])
|
||||
|
||||
# Should handle new structure
|
||||
assert agent is not None
|
||||
|
||||
|
||||
class TestWrapperAgentsModernization:
|
||||
"""GAP C: Wrapper agents use output= (not verbose/markdown)."""
|
||||
|
||||
def test_prompt_expander_agent_uses_output_in_agent_call(self):
|
||||
"""PromptExpanderAgent should use output= when creating internal Agent."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
import inspect
|
||||
|
||||
# Get the source of the agent property
|
||||
source = inspect.getsource(PromptExpanderAgent.agent.fget)
|
||||
|
||||
# Should use output= not verbose= or markdown=
|
||||
assert "output=" in source, "PromptExpanderAgent.agent should use output="
|
||||
assert "verbose=" not in source or "output=" in source, \
|
||||
"PromptExpanderAgent.agent should not pass verbose= directly to Agent()"
|
||||
|
||||
def test_query_rewriter_agent_uses_output_in_agent_call(self):
|
||||
"""QueryRewriterAgent should use output= when creating internal Agent."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
import inspect
|
||||
|
||||
# Get the source of the agent property
|
||||
source = inspect.getsource(QueryRewriterAgent.agent.fget)
|
||||
|
||||
# Should use output= not verbose= or markdown=
|
||||
assert "output=" in source, "QueryRewriterAgent.agent should use output="
|
||||
assert "verbose=" not in source or "output=" in source, \
|
||||
"QueryRewriterAgent.agent should not pass verbose= directly to Agent()"
|
||||
|
||||
def test_prompt_expander_accepts_model_param(self):
|
||||
"""PromptExpanderAgent should accept model= parameter."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_query_rewriter_accepts_model_param(self):
|
||||
"""QueryRewriterAgent should accept model= parameter."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
|
||||
class TestAutoAgentsNoLegacyVerboseMarkdown:
|
||||
"""AutoAgents should not pass verbose/markdown to Agent when output= is set."""
|
||||
|
||||
def test_autoagents_uses_output_not_verbose_markdown(self):
|
||||
"""AutoAgents should use output= and not pass verbose/markdown when output is set."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should pass output= to Agent
|
||||
assert "output=self._output" in source or "output=" in source, \
|
||||
"AutoAgents should pass output= to Agent()"
|
||||
|
||||
# Should NOT pass verbose= or markdown= directly
|
||||
assert "verbose=self.verbose" not in source, \
|
||||
"AutoAgents should not pass verbose= to Agent()"
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Tests for LLM parameter consolidation.
|
||||
|
||||
TDD: These tests define the expected behavior for:
|
||||
1. Agent accepts `llm=` consolidated usage (new)
|
||||
2. Agent accepts `model=` alias (new)
|
||||
3. Agent still accepts `llm_config=` and `function_calling_llm=` but maps them internally (deprecated)
|
||||
4. base_url/api_key remain separate and unchanged
|
||||
5. Deprecation warnings emitted only when legacy params are used
|
||||
"""
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestLLMConsolidation:
|
||||
"""Tests for LLM parameter consolidation in Agent."""
|
||||
|
||||
def test_agent_accepts_llm_string(self):
|
||||
"""Agent accepts llm= as a string model name."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test agent",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
assert agent.llm == "gpt-4o-mini"
|
||||
|
||||
def test_agent_accepts_model_alias(self):
|
||||
"""Agent accepts model= as an alias for llm=."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test agent",
|
||||
model="gpt-4o-mini"
|
||||
)
|
||||
# model= should behave identically to llm=
|
||||
assert agent.llm == "gpt-4o-mini"
|
||||
|
||||
def test_model_and_llm_are_equivalent(self):
|
||||
"""model= and llm= produce identical agents."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent1 = Agent(instructions="Test", llm="gpt-4o")
|
||||
agent2 = Agent(instructions="Test", model="gpt-4o")
|
||||
|
||||
assert agent1.llm == agent2.llm
|
||||
|
||||
def test_llm_takes_precedence_over_model(self):
|
||||
"""If both llm= and model= are provided, llm= takes precedence."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
model="gpt-3.5-turbo" # Should be ignored
|
||||
)
|
||||
assert agent.llm == "gpt-4o"
|
||||
|
||||
def test_llm_config_still_works_without_warning(self):
|
||||
"""llm_config= still works without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm_config={"temperature": 0.7}
|
||||
)
|
||||
# Should NOT emit deprecation warning (per new policy)
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_function_calling_llm_still_works_without_warning(self):
|
||||
"""function_calling_llm= still works without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
function_calling_llm="gpt-4o-mini"
|
||||
)
|
||||
# Should NOT emit deprecation warning (per new policy)
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_base_url_remains_separate(self):
|
||||
"""base_url= remains a separate parameter, not consolidated."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
base_url="https://custom.api.com/v1"
|
||||
)
|
||||
assert agent.base_url == "https://custom.api.com/v1"
|
||||
|
||||
def test_api_key_remains_separate(self):
|
||||
"""api_key= remains a separate parameter, not consolidated."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
api_key="sk-test-key"
|
||||
)
|
||||
assert agent.api_key == "sk-test-key"
|
||||
|
||||
def test_no_deprecation_warning_on_normal_path(self):
|
||||
"""No deprecation warnings when using only new consolidated params."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
base_url="https://api.openai.com/v1",
|
||||
api_key="sk-test"
|
||||
)
|
||||
# Should NOT emit deprecation warnings for llm/base_url/api_key
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
llm_related = [x for x in deprecation_warnings
|
||||
if "llm_config" in str(x.message).lower()
|
||||
or "function_calling_llm" in str(x.message).lower()]
|
||||
assert len(llm_related) == 0
|
||||
|
||||
|
||||
class TestWebFetchGating:
|
||||
"""Tests for web_fetch gating (Claude-only)."""
|
||||
|
||||
def test_non_claude_model_with_web_fetch_raises_error(self):
|
||||
"""Non-Claude model + web fetch requested → error."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# This should raise an error or warning when fetch is explicitly requested
|
||||
with pytest.raises((ValueError, RuntimeError)) as exc_info:
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o", # OpenAI model - does NOT support fetch
|
||||
web=WebConfig(fetch=True) # Explicitly request fetch
|
||||
)
|
||||
# Force initialization to trigger the check
|
||||
_ = agent.web
|
||||
|
||||
assert "fetch" in str(exc_info.value).lower() or "claude" in str(exc_info.value).lower()
|
||||
|
||||
def test_claude_model_with_web_fetch_allowed(self):
|
||||
"""Claude model + web fetch requested → allowed."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# This should work without error
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="anthropic/claude-sonnet-4", # Claude model - supports fetch
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
# Should not raise
|
||||
assert agent is not None
|
||||
|
||||
def test_web_enabled_without_fetch_works_on_any_model(self):
|
||||
"""web enabled with search only (no fetch) works on any model."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# Web search (not fetch) should work on any model that supports it
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o-search-preview", # OpenAI search model
|
||||
web=WebConfig(search=True, fetch=False) # Explicitly disable fetch
|
||||
)
|
||||
# Should not raise - web search is allowed
|
||||
assert agent is not None
|
||||
|
||||
def test_web_fetch_false_on_non_claude_no_error(self):
|
||||
"""web=True with fetch=False on non-Claude should not error."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
web=WebConfig(search=True, fetch=False) # Explicitly disable fetch
|
||||
)
|
||||
assert agent is not None
|
||||
|
||||
|
||||
class TestWrapperAgentConsolidation:
|
||||
"""Tests for wrapper agents using consolidated params."""
|
||||
|
||||
def test_prompt_expander_accepts_model_param(self):
|
||||
"""PromptExpanderAgent accepts model= parameter."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
# PromptExpanderAgent stores model in self.model attribute
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_prompt_expander_model_is_used(self):
|
||||
"""PromptExpanderAgent uses the model parameter correctly."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
# Should accept model= parameter without error
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
assert agent is not None
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_query_rewriter_accepts_model_param(self):
|
||||
"""QueryRewriterAgent accepts model= parameter."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Unit tests for the core SDK profiling module.
|
||||
|
||||
Tests cover:
|
||||
- Profiler enable/disable
|
||||
- Timing recording
|
||||
- Streaming tracking
|
||||
- Phase tracking
|
||||
- Report generation
|
||||
- Zero overhead when disabled
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestProfilerBasics:
|
||||
"""Test basic Profiler functionality."""
|
||||
|
||||
def test_profiler_disabled_by_default(self):
|
||||
"""Profiler should be disabled by default."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
Profiler.disable() # Ensure clean state
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
def test_profiler_enable_disable(self):
|
||||
"""Test enable/disable functionality."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
Profiler.enable()
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
Profiler.disable()
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
def test_profiler_env_var_enable(self):
|
||||
"""Test enabling via environment variable."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': '1'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': 'true'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': 'yes'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
def test_profiler_clear(self):
|
||||
"""Test clearing profiling data."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.record_timing("test", 100.0)
|
||||
assert len(Profiler.get_timings()) > 0
|
||||
|
||||
Profiler.clear()
|
||||
assert len(Profiler.get_timings()) == 0
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestTimingRecording:
|
||||
"""Test timing recording functionality."""
|
||||
|
||||
def test_record_timing_when_enabled(self):
|
||||
"""Timing should be recorded when profiler is enabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 123.45, category="test")
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "test_op"
|
||||
assert timings[0].duration_ms == 123.45
|
||||
assert timings[0].category == "test"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_record_timing_when_disabled(self):
|
||||
"""Timing should NOT be recorded when profiler is disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 123.45)
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
def test_record_timing_with_metadata(self):
|
||||
"""Timing should include metadata."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 100.0, metadata={"key": "value"})
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert timings[0].metadata == {"key": "value"}
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestPhaseTracking:
|
||||
"""Test phase tracking functionality."""
|
||||
|
||||
def test_phase_context_manager(self):
|
||||
"""Phase context manager should record timing."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.phase("test_phase"):
|
||||
time.sleep(0.01) # 10ms
|
||||
|
||||
timings = Profiler.get_timings(category="phase")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "test_phase"
|
||||
assert timings[0].duration_ms >= 10 # At least 10ms
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_nested_phases(self):
|
||||
"""Nested phases should track parent."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.phase("outer"):
|
||||
with Profiler.phase("inner"):
|
||||
time.sleep(0.005)
|
||||
|
||||
timings = Profiler.get_timings(category="phase")
|
||||
assert len(timings) == 2
|
||||
|
||||
inner = next(t for t in timings if t.name == "inner")
|
||||
assert inner.parent == "outer"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestStreamingTracker:
|
||||
"""Test streaming tracker functionality."""
|
||||
|
||||
def test_streaming_ttft(self):
|
||||
"""Streaming tracker should measure TTFT."""
|
||||
from praisonaiagents.profiling import Profiler, StreamingTracker
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
tracker = StreamingTracker("test_stream")
|
||||
tracker.start()
|
||||
time.sleep(0.01) # 10ms to first token
|
||||
tracker.first_token()
|
||||
time.sleep(0.02) # 20ms more
|
||||
tracker.end()
|
||||
|
||||
assert tracker.ttft_ms >= 10
|
||||
assert tracker.elapsed_ms >= 30
|
||||
|
||||
records = Profiler.get_streaming_records()
|
||||
assert len(records) == 1
|
||||
assert records[0].name == "test_stream"
|
||||
assert records[0].ttft_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_streaming_context_manager(self):
|
||||
"""Streaming context manager should work."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.streaming("test_stream") as tracker:
|
||||
time.sleep(0.01)
|
||||
tracker.first_token()
|
||||
tracker.chunk()
|
||||
tracker.chunk()
|
||||
|
||||
records = Profiler.get_streaming_records()
|
||||
assert len(records) == 1
|
||||
assert records[0].chunk_count == 2
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestProfileDecorators:
|
||||
"""Test profiling decorators."""
|
||||
|
||||
def test_profile_func_decorator(self):
|
||||
"""profile_func decorator should record function timing."""
|
||||
from praisonaiagents.profiling import Profiler, profile_func
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile_func
|
||||
def slow_function():
|
||||
time.sleep(0.01)
|
||||
return "done"
|
||||
|
||||
result = slow_function()
|
||||
assert result == "done"
|
||||
|
||||
timings = Profiler.get_timings(category="function")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "slow_function"
|
||||
assert timings[0].duration_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_profile_func_disabled(self):
|
||||
"""profile_func should have zero overhead when disabled."""
|
||||
from praisonaiagents.profiling import Profiler, profile_func
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile_func
|
||||
def fast_function():
|
||||
return "done"
|
||||
|
||||
result = fast_function()
|
||||
assert result == "done"
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
|
||||
class TestProfileBlock:
|
||||
"""Test profile_block context manager."""
|
||||
|
||||
def test_profile_block(self):
|
||||
"""profile_block should record block timing."""
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with profile_block("my_block", category="custom"):
|
||||
time.sleep(0.01)
|
||||
|
||||
timings = Profiler.get_timings(category="custom")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "my_block"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_profile_block_disabled(self):
|
||||
"""profile_block should have zero overhead when disabled."""
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
with profile_block("my_block"):
|
||||
pass
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
|
||||
class TestReporting:
|
||||
"""Test report generation."""
|
||||
|
||||
def test_get_summary(self):
|
||||
"""get_summary should return structured data."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("op1", 100.0, category="cat1")
|
||||
Profiler.record_timing("op2", 200.0, category="cat2")
|
||||
|
||||
summary = Profiler.get_summary()
|
||||
|
||||
assert summary['total_time_ms'] == 300.0
|
||||
assert summary['timing_count'] == 2
|
||||
assert 'cat1' in summary['by_category']
|
||||
assert 'cat2' in summary['by_category']
|
||||
assert len(summary['slowest_operations']) == 2
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_export_json(self):
|
||||
"""export_json should return valid JSON."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
import json
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test", 100.0)
|
||||
|
||||
json_str = Profiler.export_json()
|
||||
data = json.loads(json_str)
|
||||
|
||||
assert 'summary' in data
|
||||
assert 'timings' in data
|
||||
assert len(data['timings']) == 1
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestWarmup:
|
||||
"""Test warmup function."""
|
||||
|
||||
def test_warmup_returns_timings(self):
|
||||
"""warmup should return timing information."""
|
||||
from praisonaiagents import warmup
|
||||
|
||||
result = warmup(include_litellm=False, include_openai=False)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_warmup_with_litellm(self):
|
||||
"""warmup with litellm should return litellm timing."""
|
||||
from praisonaiagents import warmup
|
||||
|
||||
result = warmup(include_litellm=True, include_openai=False)
|
||||
assert 'litellm' in result
|
||||
# Should be positive (imported) or -1 (not available)
|
||||
assert result['litellm'] > 0 or result['litellm'] == -1
|
||||
|
||||
|
||||
class TestZeroOverhead:
|
||||
"""Test that profiling has zero overhead when disabled."""
|
||||
|
||||
def test_record_timing_overhead(self):
|
||||
"""record_timing should be fast when disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(10000):
|
||||
Profiler.record_timing("test", 100.0)
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Should complete 10k calls in under 10ms when disabled
|
||||
assert elapsed < 10, f"record_timing took {elapsed}ms for 10k calls"
|
||||
|
||||
def test_phase_context_overhead(self):
|
||||
"""phase context manager should be fast when disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
with Profiler.phase("test"):
|
||||
pass
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Should complete 1k calls in under 10ms when disabled
|
||||
assert elapsed < 10, f"phase took {elapsed}ms for 1k calls"
|
||||
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
Tests for the 'stream' output preset and streaming behavior.
|
||||
|
||||
Verifies:
|
||||
1. output="stream" is a valid preset string
|
||||
2. Streaming path yields chunks without buffering
|
||||
3. LLM call count == 1 for simple streaming case
|
||||
4. Precedence: start(stream=...) overrides OutputConfig.stream
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestStreamPresetValidation:
|
||||
"""Test that 'stream' is a valid output preset."""
|
||||
|
||||
def test_stream_preset_exists_in_output_presets(self):
|
||||
"""'stream' should be a valid key in OUTPUT_PRESETS."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
assert "stream" in OUTPUT_PRESETS, "Missing 'stream' preset in OUTPUT_PRESETS"
|
||||
assert OUTPUT_PRESETS["stream"]["stream"] is True, "'stream' preset should have stream=True"
|
||||
|
||||
def test_stream_preset_resolves_correctly(self):
|
||||
"""output='stream' should resolve to OutputConfig with stream=True."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value="stream",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is True
|
||||
assert result.verbose is True # stream preset enables verbose
|
||||
|
||||
def test_all_output_presets_are_valid(self):
|
||||
"""All documented presets should be valid."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
expected_presets = {"minimal", "normal", "verbose", "debug", "silent", "stream"}
|
||||
actual_presets = set(OUTPUT_PRESETS.keys())
|
||||
|
||||
assert expected_presets.issubset(actual_presets), \
|
||||
f"Missing presets: {expected_presets - actual_presets}"
|
||||
|
||||
def test_invalid_preset_raises_helpful_error(self):
|
||||
"""Invalid preset should raise error listing valid options including 'stream'."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve(
|
||||
value="invalid_preset",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "invalid_preset" in error_msg
|
||||
assert "stream" in error_msg, "Error should list 'stream' as valid preset"
|
||||
|
||||
|
||||
class TestStreamPresetWithOverrides:
|
||||
"""Test array form with stream preset and overrides."""
|
||||
|
||||
def test_stream_preset_with_override_to_disable(self):
|
||||
"""['stream', {'stream': False}] should override stream to False."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value=["stream", {"stream": False}],
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is False # Override takes effect
|
||||
assert result.verbose is True # Base preset value preserved
|
||||
|
||||
def test_verbose_preset_with_stream_override(self):
|
||||
"""['verbose', {'stream': True}] should enable streaming."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value=["verbose", {"stream": True}],
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is True
|
||||
assert result.metrics is True # verbose preset has metrics=True
|
||||
|
||||
|
||||
class TestStreamPrecedence:
|
||||
"""Test precedence: start(stream=...) should override OutputConfig.stream."""
|
||||
|
||||
def test_start_stream_kwarg_overrides_output_config(self):
|
||||
"""start(stream=True) should enable streaming even if output='silent'."""
|
||||
# This is a behavioral test - we verify the logic in start() method
|
||||
# The actual streaming behavior is tested in integration tests
|
||||
|
||||
# Verify the start method checks kwargs['stream'] first
|
||||
import inspect
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
source = inspect.getsource(Agent.start)
|
||||
|
||||
# start() should check kwargs.get('stream', ...) OR getattr(self, 'stream', ...)
|
||||
assert "kwargs.get('stream'" in source or "stream" in source, \
|
||||
"start() should check stream kwarg"
|
||||
|
||||
|
||||
class TestNoDeprecationWarnings:
|
||||
"""Ensure no deprecation warnings for stream preset."""
|
||||
|
||||
def test_stream_preset_no_warnings(self):
|
||||
"""Using output='stream' should not emit any warnings."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
resolve(
|
||||
value="stream",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, \
|
||||
f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
|
||||
class TestOutputConfigStreamField:
|
||||
"""Test OutputConfig has stream field."""
|
||||
|
||||
def test_output_config_has_stream_field(self):
|
||||
"""OutputConfig should have a stream field."""
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
config = OutputConfig()
|
||||
assert hasattr(config, 'stream'), "OutputConfig missing 'stream' field"
|
||||
|
||||
def test_output_config_stream_default_is_false(self):
|
||||
"""OutputConfig.stream should default to False."""
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
config = OutputConfig()
|
||||
assert config.stream is False, "OutputConfig.stream should default to False"
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the trace module."""
|
||||
@@ -0,0 +1,450 @@
|
||||
"""
|
||||
Tests for the trace protocol module.
|
||||
|
||||
TDD tests for ActionEvent, TraceSink protocol, and redaction.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
class TestActionEvent:
|
||||
"""Tests for ActionEvent dataclass."""
|
||||
|
||||
def test_action_event_creation_minimal(self):
|
||||
"""Test creating ActionEvent with minimal fields."""
|
||||
from praisonaiagents.trace.protocol import ActionEvent
|
||||
|
||||
event = ActionEvent(
|
||||
event_type="agent_start",
|
||||
timestamp=1736665496.123,
|
||||
)
|
||||
|
||||
assert event.event_type == "agent_start"
|
||||
assert event.timestamp == 1736665496.123
|
||||
assert event.schema_version == "1.0"
|
||||
assert event.agent_id is None
|
||||
assert event.agent_name is None
|
||||
assert event.tool_name is None
|
||||
|
||||
def test_action_event_creation_full(self):
|
||||
"""Test creating ActionEvent with all fields."""
|
||||
from praisonaiagents.trace.protocol import ActionEvent
|
||||
|
||||
event = ActionEvent(
|
||||
event_type="tool_end",
|
||||
timestamp=1736665497.456,
|
||||
agent_id="agent-123",
|
||||
agent_name="researcher",
|
||||
tool_name="web_search",
|
||||
tool_args={"query": "AI trends"},
|
||||
tool_result_summary="Found 10 results",
|
||||
duration_ms=800.5,
|
||||
status="ok",
|
||||
error_message=None,
|
||||
metadata={"provider": "duckduckgo"},
|
||||
)
|
||||
|
||||
assert event.event_type == "tool_end"
|
||||
assert event.agent_name == "researcher"
|
||||
assert event.tool_name == "web_search"
|
||||
assert event.tool_args == {"query": "AI trends"}
|
||||
assert event.duration_ms == 800.5
|
||||
assert event.status == "ok"
|
||||
assert event.metadata == {"provider": "duckduckgo"}
|
||||
|
||||
def test_action_event_to_dict(self):
|
||||
"""Test ActionEvent.to_dict() method."""
|
||||
from praisonaiagents.trace.protocol import ActionEvent
|
||||
|
||||
event = ActionEvent(
|
||||
event_type="agent_start",
|
||||
timestamp=1736665496.123,
|
||||
agent_name="writer",
|
||||
)
|
||||
|
||||
d = event.to_dict()
|
||||
|
||||
assert d["schema_version"] == "1.0"
|
||||
assert d["event_type"] == "agent_start"
|
||||
assert d["timestamp"] == 1736665496.123
|
||||
assert d["agent_name"] == "writer"
|
||||
# None values should be excluded
|
||||
assert "tool_name" not in d or d["tool_name"] is None
|
||||
|
||||
def test_action_event_to_json(self):
|
||||
"""Test ActionEvent.to_json() method."""
|
||||
from praisonaiagents.trace.protocol import ActionEvent
|
||||
|
||||
event = ActionEvent(
|
||||
event_type="tool_start",
|
||||
timestamp=1736665497.0,
|
||||
tool_name="read_file",
|
||||
tool_args={"path": "/tmp/test.txt"},
|
||||
)
|
||||
|
||||
json_str = event.to_json()
|
||||
parsed = json.loads(json_str)
|
||||
|
||||
assert parsed["event_type"] == "tool_start"
|
||||
assert parsed["tool_name"] == "read_file"
|
||||
assert parsed["tool_args"] == {"path": "/tmp/test.txt"}
|
||||
|
||||
def test_action_event_types(self):
|
||||
"""Test all valid event types."""
|
||||
from praisonaiagents.trace.protocol import ActionEvent, ActionEventType
|
||||
|
||||
valid_types = [
|
||||
ActionEventType.AGENT_START,
|
||||
ActionEventType.AGENT_END,
|
||||
ActionEventType.TOOL_START,
|
||||
ActionEventType.TOOL_END,
|
||||
ActionEventType.ERROR,
|
||||
ActionEventType.OUTPUT,
|
||||
]
|
||||
|
||||
for event_type in valid_types:
|
||||
event = ActionEvent(
|
||||
event_type=event_type.value,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
assert event.event_type == event_type.value
|
||||
|
||||
|
||||
class TestRedaction:
|
||||
"""Tests for redaction functionality."""
|
||||
|
||||
def test_redact_api_key(self):
|
||||
"""Test redacting api_key field."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {"api_key": "sk-1234567890abcdef", "query": "hello"}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["api_key"] == "[REDACTED]"
|
||||
assert redacted["query"] == "hello"
|
||||
|
||||
def test_redact_multiple_sensitive_keys(self):
|
||||
"""Test redacting multiple sensitive keys."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {
|
||||
"api_key": "sk-secret",
|
||||
"password": "mypassword123",
|
||||
"token": "bearer-token",
|
||||
"authorization": "Bearer xyz",
|
||||
"secret": "top-secret",
|
||||
"normal_field": "visible",
|
||||
}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["api_key"] == "[REDACTED]"
|
||||
assert redacted["password"] == "[REDACTED]"
|
||||
assert redacted["token"] == "[REDACTED]"
|
||||
assert redacted["authorization"] == "[REDACTED]"
|
||||
assert redacted["secret"] == "[REDACTED]"
|
||||
assert redacted["normal_field"] == "visible"
|
||||
|
||||
def test_redact_nested_dict(self):
|
||||
"""Test redacting nested dictionaries."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {
|
||||
"config": {
|
||||
"api_key": "sk-nested",
|
||||
"endpoint": "https://api.example.com",
|
||||
},
|
||||
"name": "test",
|
||||
}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["config"]["api_key"] == "[REDACTED]"
|
||||
assert redacted["config"]["endpoint"] == "https://api.example.com"
|
||||
assert redacted["name"] == "test"
|
||||
|
||||
def test_redact_list_of_dicts(self):
|
||||
"""Test redacting list containing dictionaries."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {
|
||||
"items": [
|
||||
{"name": "item1", "secret": "hidden1"},
|
||||
{"name": "item2", "secret": "hidden2"},
|
||||
]
|
||||
}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["items"][0]["name"] == "item1"
|
||||
assert redacted["items"][0]["secret"] == "[REDACTED]"
|
||||
assert redacted["items"][1]["secret"] == "[REDACTED]"
|
||||
|
||||
def test_redact_case_insensitive(self):
|
||||
"""Test that redaction is case-insensitive."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {
|
||||
"API_KEY": "key1",
|
||||
"Api_Key": "key2",
|
||||
"apiKey": "key3",
|
||||
"APIKEY": "key4",
|
||||
}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["API_KEY"] == "[REDACTED]"
|
||||
assert redacted["Api_Key"] == "[REDACTED]"
|
||||
assert redacted["apiKey"] == "[REDACTED]"
|
||||
assert redacted["APIKEY"] == "[REDACTED]"
|
||||
|
||||
def test_redact_connection_strings(self):
|
||||
"""Test redacting database connection strings."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {
|
||||
"connection_string": "postgresql://user:pass@host/db",
|
||||
"database_url": "mysql://root:secret@localhost/mydb",
|
||||
"db_url": "redis://default:password@redis:6379",
|
||||
}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
assert redacted["connection_string"] == "[REDACTED]"
|
||||
assert redacted["database_url"] == "[REDACTED]"
|
||||
assert redacted["db_url"] == "[REDACTED]"
|
||||
|
||||
def test_redact_preserves_original(self):
|
||||
"""Test that redaction doesn't modify original dict."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
original = {"api_key": "secret", "name": "test"}
|
||||
redacted = redact_dict(original)
|
||||
|
||||
assert original["api_key"] == "secret"
|
||||
assert redacted["api_key"] == "[REDACTED]"
|
||||
|
||||
def test_redact_empty_dict(self):
|
||||
"""Test redacting empty dictionary."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
assert redact_dict({}) == {}
|
||||
|
||||
def test_redact_none_values(self):
|
||||
"""Test handling None values."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {"api_key": None, "name": "test"}
|
||||
redacted = redact_dict(data)
|
||||
|
||||
# None values for sensitive keys should still be redacted
|
||||
assert redacted["api_key"] == "[REDACTED]"
|
||||
assert redacted["name"] == "test"
|
||||
|
||||
def test_redact_disabled(self):
|
||||
"""Test disabling redaction."""
|
||||
from praisonaiagents.trace.redact import redact_dict
|
||||
|
||||
data = {"api_key": "visible", "name": "test"}
|
||||
result = redact_dict(data, enabled=False)
|
||||
|
||||
assert result["api_key"] == "visible"
|
||||
assert result["name"] == "test"
|
||||
|
||||
|
||||
class TestTraceSink:
|
||||
"""Tests for TraceSink protocol."""
|
||||
|
||||
def test_noop_sink(self):
|
||||
"""Test NoOpSink does nothing."""
|
||||
from praisonaiagents.trace.protocol import NoOpSink, ActionEvent
|
||||
|
||||
sink = NoOpSink()
|
||||
event = ActionEvent(event_type="agent_start", timestamp=time.time())
|
||||
|
||||
# Should not raise
|
||||
sink.emit(event)
|
||||
sink.flush()
|
||||
sink.close()
|
||||
|
||||
def test_list_sink(self):
|
||||
"""Test ListSink collects events."""
|
||||
from praisonaiagents.trace.protocol import ListSink, ActionEvent
|
||||
|
||||
sink = ListSink()
|
||||
|
||||
event1 = ActionEvent(event_type="agent_start", timestamp=1.0)
|
||||
event2 = ActionEvent(event_type="tool_start", timestamp=2.0)
|
||||
|
||||
sink.emit(event1)
|
||||
sink.emit(event2)
|
||||
|
||||
events = sink.get_events()
|
||||
assert len(events) == 2
|
||||
assert events[0].event_type == "agent_start"
|
||||
assert events[1].event_type == "tool_start"
|
||||
|
||||
def test_list_sink_clear(self):
|
||||
"""Test ListSink.clear() method."""
|
||||
from praisonaiagents.trace.protocol import ListSink, ActionEvent
|
||||
|
||||
sink = ListSink()
|
||||
sink.emit(ActionEvent(event_type="test", timestamp=1.0))
|
||||
|
||||
assert len(sink.get_events()) == 1
|
||||
|
||||
sink.clear()
|
||||
|
||||
assert len(sink.get_events()) == 0
|
||||
|
||||
|
||||
class TestTraceEmitter:
|
||||
"""Tests for TraceEmitter class."""
|
||||
|
||||
def test_emitter_with_noop_sink(self):
|
||||
"""Test TraceEmitter with NoOpSink."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, NoOpSink
|
||||
|
||||
emitter = TraceEmitter(sink=NoOpSink())
|
||||
|
||||
# Should not raise
|
||||
emitter.agent_start("test-agent", agent_id="123")
|
||||
emitter.tool_start("web_search", {"query": "test"})
|
||||
emitter.tool_end("web_search", duration_ms=100, status="ok")
|
||||
emitter.agent_end("test-agent", duration_ms=500)
|
||||
|
||||
def test_emitter_with_list_sink(self):
|
||||
"""Test TraceEmitter collects events."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink)
|
||||
|
||||
emitter.agent_start("researcher")
|
||||
emitter.tool_start("search", {"q": "AI"})
|
||||
emitter.tool_end("search", duration_ms=100, status="ok")
|
||||
emitter.agent_end("researcher", duration_ms=200)
|
||||
|
||||
events = sink.get_events()
|
||||
assert len(events) == 4
|
||||
assert events[0].event_type == "agent_start"
|
||||
assert events[1].event_type == "tool_start"
|
||||
assert events[2].event_type == "tool_end"
|
||||
assert events[3].event_type == "agent_end"
|
||||
|
||||
def test_emitter_redacts_by_default(self):
|
||||
"""Test TraceEmitter redacts sensitive data by default."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink, redact=True)
|
||||
|
||||
emitter.tool_start("api_call", {"api_key": "sk-secret", "query": "test"})
|
||||
|
||||
events = sink.get_events()
|
||||
assert events[0].tool_args["api_key"] == "[REDACTED]"
|
||||
assert events[0].tool_args["query"] == "test"
|
||||
|
||||
def test_emitter_disabled(self):
|
||||
"""Test TraceEmitter when disabled."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink, enabled=False)
|
||||
|
||||
emitter.agent_start("test")
|
||||
emitter.tool_start("tool", {})
|
||||
|
||||
# No events should be emitted
|
||||
assert len(sink.get_events()) == 0
|
||||
|
||||
def test_emitter_error_event(self):
|
||||
"""Test TraceEmitter error event."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink)
|
||||
|
||||
emitter.error("Something went wrong", tool_name="failing_tool")
|
||||
|
||||
events = sink.get_events()
|
||||
assert len(events) == 1
|
||||
assert events[0].event_type == "error"
|
||||
assert events[0].error_message == "Something went wrong"
|
||||
assert events[0].tool_name == "failing_tool"
|
||||
|
||||
def test_emitter_output_event(self):
|
||||
"""Test TraceEmitter output event for final agent output."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink)
|
||||
|
||||
emitter.output("The analysis shows three major trends...")
|
||||
|
||||
events = sink.get_events()
|
||||
assert len(events) == 1
|
||||
assert events[0].event_type == "output"
|
||||
assert "three major trends" in events[0].tool_result_summary
|
||||
|
||||
|
||||
class TestActionTraceConfig:
|
||||
"""Tests for ActionTraceConfig."""
|
||||
|
||||
def test_config_defaults(self):
|
||||
"""Test ActionTraceConfig default values."""
|
||||
from praisonaiagents.trace.protocol import ActionTraceConfig
|
||||
|
||||
config = ActionTraceConfig()
|
||||
|
||||
assert config.enabled is True
|
||||
assert config.redact is True
|
||||
assert config.compact is False
|
||||
assert config.sink_type == "noop"
|
||||
assert config.file_path is None
|
||||
|
||||
def test_config_custom(self):
|
||||
"""Test ActionTraceConfig custom values."""
|
||||
from praisonaiagents.trace.protocol import ActionTraceConfig
|
||||
|
||||
config = ActionTraceConfig(
|
||||
enabled=True,
|
||||
redact=False,
|
||||
compact=True,
|
||||
sink_type="jsonl",
|
||||
file_path="/tmp/trace.jsonl",
|
||||
)
|
||||
|
||||
assert config.redact is False
|
||||
assert config.compact is True
|
||||
assert config.sink_type == "jsonl"
|
||||
assert config.file_path == "/tmp/trace.jsonl"
|
||||
|
||||
|
||||
class TestPerformance:
|
||||
"""Performance tests for trace module."""
|
||||
|
||||
def test_noop_sink_overhead(self):
|
||||
"""Test that NoOpSink has minimal overhead."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, NoOpSink, ActionEvent
|
||||
|
||||
emitter = TraceEmitter(sink=NoOpSink())
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(10000):
|
||||
emitter.tool_start("test", {"arg": "value"})
|
||||
emitter.tool_end("test", duration_ms=1, status="ok")
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
# Should complete 10000 iterations in under 100ms
|
||||
assert elapsed < 0.1, f"NoOpSink too slow: {elapsed:.3f}s for 10000 iterations"
|
||||
|
||||
def test_disabled_emitter_overhead(self):
|
||||
"""Test that disabled emitter has near-zero overhead."""
|
||||
from praisonaiagents.trace.protocol import TraceEmitter, ListSink
|
||||
|
||||
emitter = TraceEmitter(sink=ListSink(), enabled=False)
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(10000):
|
||||
emitter.tool_start("test", {"arg": "value"})
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
# Should complete 10000 iterations in under 500ms (very conservative)
|
||||
assert elapsed < 0.5, f"Disabled emitter too slow: {elapsed:.3f}s"
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.10"
|
||||
version = "0.11.8"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
chainlit.md
|
||||
tests/fixtures
|
||||
reports
|
||||
reports
|
||||
.agent
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Dynamic Context Discovery Example.
|
||||
|
||||
This example demonstrates how to use the Dynamic Context Discovery feature
|
||||
to efficiently manage large tool outputs, conversation history, and terminal logs.
|
||||
|
||||
Features demonstrated:
|
||||
1. Tool output queuing - Large outputs automatically saved as artifacts
|
||||
2. Artifact tools - tail, grep, chunk operations on stored artifacts
|
||||
3. History persistence - Conversation history saved for loss recovery
|
||||
4. Terminal logging - Shell command outputs captured
|
||||
|
||||
Usage:
|
||||
python dynamic_context_example.py
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
|
||||
|
||||
def example_artifact_store():
|
||||
"""Demonstrate artifact storage and retrieval."""
|
||||
print("\n=== Artifact Store Example ===\n")
|
||||
|
||||
from praisonai.context import FileSystemArtifactStore
|
||||
from praisonaiagents.context.artifacts import ArtifactMetadata
|
||||
|
||||
# Create a temporary store
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = FileSystemArtifactStore(base_dir=tmpdir)
|
||||
|
||||
# Store a large JSON response (simulating API output)
|
||||
large_data = {
|
||||
"records": [{"id": i, "name": f"Item {i}"} for i in range(1000)],
|
||||
"metadata": {"total": 1000, "page": 1},
|
||||
}
|
||||
|
||||
metadata = ArtifactMetadata(
|
||||
agent_id="data_agent",
|
||||
run_id="run_001",
|
||||
tool_name="api_fetch",
|
||||
)
|
||||
|
||||
ref = store.store(large_data, metadata)
|
||||
print(f"Stored artifact: {ref.path}")
|
||||
print(f"Size: {ref._format_size(ref.size_bytes)}")
|
||||
print(f"Summary: {ref.summary}")
|
||||
print(f"Inline reference: {ref.to_inline()}")
|
||||
|
||||
# Demonstrate tail operation
|
||||
print("\n--- Tail (last 5 lines) ---")
|
||||
tail = store.tail(ref, lines=5)
|
||||
print(tail)
|
||||
|
||||
# Demonstrate grep operation
|
||||
print("\n--- Grep for 'Item 50' ---")
|
||||
matches = store.grep(ref, pattern="Item 50")
|
||||
for match in matches[:3]:
|
||||
print(f"Line {match.line_number}: {match.line_content[:60]}...")
|
||||
|
||||
# List all artifacts
|
||||
print("\n--- List artifacts ---")
|
||||
artifacts = store.list_artifacts(run_id="run_001")
|
||||
for art in artifacts:
|
||||
print(f" - {art.artifact_id}: {art.tool_name} ({art._format_size(art.size_bytes)})")
|
||||
|
||||
|
||||
def example_output_queue():
|
||||
"""Demonstrate automatic tool output queuing (previously called spooling)."""
|
||||
print("\n=== Output Queue Example ===\n")
|
||||
|
||||
from praisonai.context import OutputQueue
|
||||
from praisonaiagents.context.artifacts import ArtifactMetadata, ArtifactRef, QueueConfig
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Create output queue with 1KB threshold (for demo)
|
||||
config = QueueConfig(inline_max_bytes=1024)
|
||||
queue = OutputQueue(base_dir=tmpdir, config=config)
|
||||
|
||||
# Small output - stays inline (not queued to artifact)
|
||||
small_output = "Hello, world!"
|
||||
metadata = ArtifactMetadata(agent_id="agent1", run_id="run1")
|
||||
result = queue.process(small_output, metadata)
|
||||
print(f"Small output ({len(small_output)} bytes): {'Queued' if isinstance(result, ArtifactRef) else 'Inline'}")
|
||||
|
||||
# Large output - automatically queued to artifact
|
||||
large_output = "x" * 2000
|
||||
result = queue.process(large_output, metadata)
|
||||
print(f"Large output ({len(large_output)} bytes): {'Queued' if isinstance(result, ArtifactRef) else 'Inline'}")
|
||||
|
||||
if isinstance(result, ArtifactRef):
|
||||
print(f" Artifact path: {result.path}")
|
||||
|
||||
|
||||
def example_history_store():
|
||||
"""Demonstrate conversation history persistence."""
|
||||
print("\n=== History Store Example ===\n")
|
||||
|
||||
from praisonai.context import HistoryStore
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = HistoryStore(base_dir=tmpdir)
|
||||
|
||||
# Simulate a conversation
|
||||
messages = [
|
||||
{"role": "user", "content": "What is the capital of France?"},
|
||||
{"role": "assistant", "content": "The capital of France is Paris."},
|
||||
{"role": "user", "content": "What about Germany?"},
|
||||
{"role": "assistant", "content": "The capital of Germany is Berlin."},
|
||||
{"role": "user", "content": "And what about the error in my code?"},
|
||||
{"role": "assistant", "content": "I see an error on line 42. The variable is undefined."},
|
||||
]
|
||||
|
||||
for msg in messages:
|
||||
store.append(msg, agent_id="assistant", run_id="session_001")
|
||||
|
||||
# Get history reference
|
||||
ref = store.get_ref(agent_id="assistant", run_id="session_001")
|
||||
print(f"History stored: {ref.summary}")
|
||||
|
||||
# Search history
|
||||
print("\n--- Search for 'capital' ---")
|
||||
matches = store.search("capital", agent_id="assistant", run_id="session_001")
|
||||
for msg in matches:
|
||||
print(f" [{msg['role']}]: {msg['content'][:50]}...")
|
||||
|
||||
# Get last messages
|
||||
print("\n--- Last 2 messages ---")
|
||||
recent = store.get_last_messages(agent_id="assistant", run_id="session_001", count=2)
|
||||
for msg in recent:
|
||||
print(f" [{msg['role']}]: {msg['content'][:50]}...")
|
||||
|
||||
|
||||
def example_terminal_logger():
|
||||
"""Demonstrate terminal session logging."""
|
||||
print("\n=== Terminal Logger Example ===\n")
|
||||
|
||||
from praisonai.context import TerminalLogger
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
logger = TerminalLogger(base_dir=tmpdir)
|
||||
|
||||
# Log some commands
|
||||
commands = [
|
||||
("ls -la", "total 100\ndrwxr-xr-x 5 user user 160 Jan 1 00:00 .\n-rw-r--r-- 1 user user 1234 Jan 1 00:00 file.txt", 0),
|
||||
("cat file.txt", "Hello, world!\nThis is a test file.", 0),
|
||||
("grep error log.txt", "Error: Something went wrong\nError: Another issue", 0),
|
||||
("invalid_command", "command not found: invalid_command", 127),
|
||||
]
|
||||
|
||||
for cmd, output, exit_code in commands:
|
||||
ref = logger.log_command(
|
||||
command=cmd,
|
||||
output=output,
|
||||
exit_code=exit_code,
|
||||
agent_id="shell_agent",
|
||||
run_id="session_001",
|
||||
)
|
||||
|
||||
# Get session reference
|
||||
ref = logger.get_session_ref(agent_id="shell_agent", run_id="session_001")
|
||||
print(f"Terminal session: {ref.summary}")
|
||||
|
||||
# Tail the session
|
||||
print("\n--- Tail (last 10 lines) ---")
|
||||
tail = logger.tail_session(agent_id="shell_agent", run_id="session_001", lines=10)
|
||||
print(tail)
|
||||
|
||||
# Search for errors
|
||||
print("\n--- Grep for 'Error' ---")
|
||||
matches = logger.grep_session("Error", agent_id="shell_agent", run_id="session_001")
|
||||
for match in matches:
|
||||
print(f" Line {match.line_number}: {match.line_content}")
|
||||
|
||||
# Get command list
|
||||
print("\n--- Commands executed ---")
|
||||
cmds = logger.get_commands(agent_id="shell_agent", run_id="session_001")
|
||||
for cmd in cmds:
|
||||
status = "✓" if cmd["exit_code"] == 0 else "✗"
|
||||
print(f" {status} $ {cmd['command']}")
|
||||
|
||||
|
||||
def example_setup_dynamic_context():
|
||||
"""Demonstrate the convenience setup function."""
|
||||
print("\n=== Setup Dynamic Context Example ===\n")
|
||||
|
||||
from praisonai.context import setup_dynamic_context
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Set up dynamic context with all features
|
||||
ctx = setup_dynamic_context(
|
||||
base_dir=tmpdir,
|
||||
inline_max_kb=16, # 16KB threshold
|
||||
redact_secrets=True,
|
||||
history_enabled=True,
|
||||
terminal_logging=True,
|
||||
)
|
||||
|
||||
print(f"Run ID: {ctx.run_id}")
|
||||
print(f"Base directory: {ctx.config.base_dir}")
|
||||
|
||||
# Get tools for agent
|
||||
tools = ctx.get_tools()
|
||||
print(f"\nAvailable tools ({len(tools)}):")
|
||||
for tool in tools:
|
||||
print(f" - {tool.__name__}: {tool.__doc__.split(chr(10))[0] if tool.__doc__ else 'No description'}")
|
||||
|
||||
# Get middleware
|
||||
middleware = ctx.get_middleware()
|
||||
print(f"\nMiddleware: {middleware.__name__}")
|
||||
|
||||
|
||||
def example_agent_integration():
|
||||
"""Show how to integrate with Agent (conceptual)."""
|
||||
print("\n=== Agent Integration Example (Conceptual) ===\n")
|
||||
|
||||
print("""
|
||||
# To integrate Dynamic Context Discovery with an Agent:
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.context import setup_dynamic_context
|
||||
|
||||
# Set up dynamic context
|
||||
ctx = setup_dynamic_context(
|
||||
inline_max_kb=32,
|
||||
history_enabled=True,
|
||||
)
|
||||
|
||||
# Create agent with dynamic context tools and middleware
|
||||
agent = Agent(
|
||||
name="DataAnalyst",
|
||||
instructions="You are a data analyst. Use artifact tools to explore large outputs.",
|
||||
tools=ctx.get_tools(), # Adds artifact_tail, artifact_grep, etc.
|
||||
hooks=[ctx.get_middleware()], # Auto-queues large tool outputs
|
||||
)
|
||||
|
||||
# Now when the agent uses tools that produce large outputs,
|
||||
# they will be automatically queued to artifacts.
|
||||
# The agent can then use artifact_tail, artifact_grep, etc.
|
||||
# to explore the data without loading it all into context.
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Dynamic Context Discovery Examples")
|
||||
print("=" * 60)
|
||||
|
||||
example_artifact_store()
|
||||
example_output_queue()
|
||||
example_history_store()
|
||||
example_terminal_logger()
|
||||
example_setup_dynamic_context()
|
||||
example_agent_integration()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed successfully!")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Guardrails - Advanced Example
|
||||
|
||||
Demonstrates advanced guardrails patterns including:
|
||||
- Multi-agent configuration
|
||||
- Custom callbacks/handlers
|
||||
- Error handling
|
||||
- All available options
|
||||
|
||||
Expected Output:
|
||||
Comprehensive demonstration of guardrails with multiple agents.
|
||||
"""
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# ============================================================
|
||||
# Section 1: Custom Configuration
|
||||
# ============================================================
|
||||
|
||||
# Define custom guardrails configuration
|
||||
# config = ...
|
||||
|
||||
# ============================================================
|
||||
# Section 2: Multi-Agent Setup
|
||||
# ============================================================
|
||||
|
||||
agent1 = Agent(
|
||||
name="Agent1",
|
||||
instructions="You are the first agent",
|
||||
# guardrails=config,
|
||||
)
|
||||
|
||||
agent2 = Agent(
|
||||
name="Agent2",
|
||||
instructions="You are the second agent",
|
||||
# guardrails=config,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Section 3: Task Definition
|
||||
# ============================================================
|
||||
|
||||
task = Task(
|
||||
description="Demonstrate guardrails with multiple agents",
|
||||
agent=agent1,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Section 4: Execution
|
||||
# ============================================================
|
||||
|
||||
agents = PraisonAIAgents(agents=[agent1, agent2], tasks=[task])
|
||||
result = agents.start()
|
||||
print(result)
|
||||
|
||||
# ============================================================
|
||||
# Section 5: All Options Reference
|
||||
# ============================================================
|
||||
"""
|
||||
Guardrails Options:
|
||||
──────────────────
|
||||
| Option | Type | Default | Description |
|
||||
|-----------------|----------|---------|----------------------|
|
||||
| option1 | str | None | Description here |
|
||||
| option2 | bool | False | Description here |
|
||||
"""
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Guardrails - Basic Example
|
||||
|
||||
Demonstrates minimal usage of guardrails with a single agent.
|
||||
|
||||
Expected Output:
|
||||
Agent response demonstrating guardrails functionality.
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic guardrails usage
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
# guardrails=... # Configure guardrails here
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
result = agent.start("Hello! Demonstrate guardrails functionality.")
|
||||
print(result)
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.6.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.6.2.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.8.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.8.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Development utilities for PraisonAI.
|
||||
|
||||
This module contains development-only tools that are NOT imported during normal runtime.
|
||||
"""
|
||||
|
||||
__all__ = ["generate_api_md", "ApiMdGenerator"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load development utilities."""
|
||||
if name in ("generate_api_md", "ApiMdGenerator"):
|
||||
from .api_md import generate_api_md, ApiMdGenerator
|
||||
if name == "generate_api_md":
|
||||
return generate_api_md
|
||||
return ApiMdGenerator
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,981 @@
|
||||
"""
|
||||
API Markdown Generator for PraisonAI.
|
||||
|
||||
Generates a comprehensive api.md file documenting all public API surfaces:
|
||||
- Python Core SDK (praisonaiagents)
|
||||
- Python Wrapper/Integrations (praisonai)
|
||||
- CLI commands
|
||||
- TypeScript package (praisonai-ts)
|
||||
|
||||
Uses AST-based static analysis to avoid importing heavy dependencies.
|
||||
|
||||
Example output format (OpenAI SDK style):
|
||||
|
||||
# Agents
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Agent, AgentConfig
|
||||
from praisonaiagents.types import RunResult
|
||||
```
|
||||
|
||||
Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent.py">start</a>(prompt: str, **kwargs) -> RunResult</code>
|
||||
|
||||
Usage:
|
||||
python -m praisonai._dev.api_md --write
|
||||
python -m praisonai._dev.api_md --check
|
||||
python -m praisonai._dev.api_md --stdout
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class SymbolInfo:
|
||||
"""Information about a discovered symbol."""
|
||||
name: str
|
||||
kind: str # 'class', 'function', 'type', 'constant'
|
||||
module_path: str # e.g., 'praisonaiagents.agent.agent'
|
||||
file_path: str # relative to repo root
|
||||
line_number: int
|
||||
signature: Optional[str] = None
|
||||
return_type: Optional[str] = None
|
||||
methods: List["MethodInfo"] = field(default_factory=list)
|
||||
docstring: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodInfo:
|
||||
"""Information about a class method."""
|
||||
name: str
|
||||
signature: str
|
||||
return_type: Optional[str]
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class CLICommand:
|
||||
"""Information about a CLI command."""
|
||||
command_path: str # e.g., 'praisonai docs api-md'
|
||||
help_text: Optional[str]
|
||||
file_path: str
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TSExport:
|
||||
"""Information about a TypeScript export."""
|
||||
name: str
|
||||
kind: str # 'class', 'function', 'type', 'const'
|
||||
source_file: str
|
||||
is_type: bool = False
|
||||
|
||||
|
||||
class ApiMdGenerator:
|
||||
"""
|
||||
Generates api.md from source code using AST analysis.
|
||||
|
||||
Design principles:
|
||||
- AST-first: Never import modules that may have heavy dependencies
|
||||
- Deterministic: Stable ordering for reproducible output
|
||||
- Comprehensive: Cover all public exports
|
||||
"""
|
||||
|
||||
# Section groupings for organizing exports
|
||||
SECTION_GROUPS = {
|
||||
'Shared Types': ['TaskOutput', 'ReflectionOutput', 'ToolResult', 'ToolValidationError',
|
||||
'GuardrailResult', 'StepResult', 'WorkflowContext', 'HandoffResult',
|
||||
'HandoffInputData', 'HandoffConfig', 'ContextPolicy', 'HandoffError',
|
||||
'HandoffCycleError', 'HandoffDepthError', 'HandoffTimeoutError'],
|
||||
'Agents': ['Agent', 'Agents', 'PraisonAIAgents', 'ImageAgent', 'ContextAgent',
|
||||
'DeepResearchAgent', 'QueryRewriterAgent', 'PromptExpanderAgent',
|
||||
'AutoAgents', 'AutoRagAgent', 'PlanningAgent', 'create_context_agent'],
|
||||
'Tools': ['Tools', 'BaseTool', 'tool', 'FunctionTool', 'ToolRegistry',
|
||||
'get_registry', 'register_tool', 'get_tool', 'validate_tool'],
|
||||
'Workflows': ['Workflow', 'WorkflowStep', 'Pipeline', 'Route', 'Parallel',
|
||||
'Loop', 'Repeat', 'route', 'parallel', 'loop', 'repeat'],
|
||||
'DB': ['db', 'DbAdapter', 'AsyncDbAdapter'],
|
||||
'Memory': ['Memory', 'MemoryConfig', 'MemoryBackend'],
|
||||
'Knowledge': ['Knowledge', 'Chunking', 'ChunkingStrategy', 'KnowledgeConfig'],
|
||||
'RAG': ['RAG', 'RAGConfig', 'RAGResult', 'RAGCitation', 'RetrievalConfig',
|
||||
'RetrievalPolicy', 'CitationsMode', 'ContextPack'],
|
||||
'Handoff': ['Handoff', 'handoff', 'handoff_filters', 'RECOMMENDED_PROMPT_PREFIX',
|
||||
'prompt_with_handoff_instructions'],
|
||||
'Guardrails': ['LLMGuardrail', 'GuardrailConfig', 'GuardrailAction', 'GUARDRAIL_PRESETS'],
|
||||
'Planning': ['Plan', 'PlanStep', 'TodoList', 'TodoItem', 'PlanStorage',
|
||||
'ApprovalCallback', 'READ_ONLY_TOOLS', 'RESTRICTED_TOOLS', 'PlanningConfig'],
|
||||
'Skills': ['SkillManager', 'SkillProperties', 'SkillMetadata', 'SkillLoader', 'SkillsConfig'],
|
||||
'Session': ['Session'],
|
||||
'MCP': ['MCP'],
|
||||
'Telemetry': ['get_telemetry', 'enable_telemetry', 'disable_telemetry',
|
||||
'enable_performance_mode', 'disable_performance_mode',
|
||||
'cleanup_telemetry_resources', 'MinimalTelemetry', 'TelemetryCollector'],
|
||||
'Observability': ['obs', 'FlowDisplay', 'track_workflow'],
|
||||
'Context': ['FastContext', 'FastContextResult', 'FileMatch', 'LineRange',
|
||||
'ContextManager', 'ManagerConfig'],
|
||||
'UI': ['AGUI', 'A2A'],
|
||||
'Config': ['MemoryConfig', 'KnowledgeConfig', 'PlanningConfig', 'ReflectionConfig',
|
||||
'GuardrailConfig', 'WebConfig', 'OutputConfig', 'ExecutionConfig',
|
||||
'TemplateConfig', 'CachingConfig', 'HooksConfig', 'SkillsConfig',
|
||||
'AutonomyConfig', 'OutputPreset', 'ExecutionPreset', 'AutonomyLevel',
|
||||
'WebSearchProvider', 'MultiAgentHooksConfig', 'MultiAgentOutputConfig',
|
||||
'MultiAgentExecutionConfig', 'MultiAgentPlanningConfig', 'MultiAgentMemoryConfig'],
|
||||
'Display': ['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'],
|
||||
'Utilities': ['resolve', 'ArrayMode', 'resolve_memory', 'resolve_knowledge',
|
||||
'resolve_output', 'resolve_execution', 'resolve_web', 'resolve_planning',
|
||||
'resolve_reflection', 'resolve_context', 'resolve_autonomy',
|
||||
'resolve_caching', 'resolve_hooks', 'resolve_skills', 'resolve_routing',
|
||||
'resolve_guardrails', 'resolve_guardrail_policies', 'is_policy_string',
|
||||
'parse_policy_string'],
|
||||
}
|
||||
|
||||
def __init__(self, repo_root: Optional[Path] = None):
|
||||
"""Initialize the generator with repository root path."""
|
||||
if repo_root is None:
|
||||
# Auto-detect repo root
|
||||
repo_root = self._find_repo_root()
|
||||
self.repo_root = Path(repo_root).resolve()
|
||||
|
||||
# Package paths
|
||||
self.agents_pkg = self.repo_root / "src" / "praisonai-agents" / "praisonaiagents"
|
||||
self.wrapper_pkg = self.repo_root / "src" / "praisonai" / "praisonai"
|
||||
self.ts_pkg = self.repo_root / "src" / "praisonai-ts"
|
||||
|
||||
# Discovered symbols
|
||||
self.agents_symbols: Dict[str, SymbolInfo] = {}
|
||||
self.wrapper_symbols: Dict[str, SymbolInfo] = {}
|
||||
self.cli_commands: List[CLICommand] = []
|
||||
self.ts_exports: List[TSExport] = []
|
||||
|
||||
def _find_repo_root(self) -> Path:
|
||||
"""Find repository root by looking for .git directory."""
|
||||
current = Path.cwd()
|
||||
while current != current.parent:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback to expected location
|
||||
return Path("/Users/praison/praisonai-package")
|
||||
|
||||
def discover_all(self) -> None:
|
||||
"""Discover all public API symbols."""
|
||||
self._discover_python_exports(self.agents_pkg, "praisonaiagents", self.agents_symbols)
|
||||
self._discover_python_exports(self.wrapper_pkg, "praisonai", self.wrapper_symbols)
|
||||
self._discover_cli_commands()
|
||||
self._discover_ts_exports()
|
||||
|
||||
def _discover_python_exports(
|
||||
self,
|
||||
pkg_path: Path,
|
||||
pkg_name: str,
|
||||
symbols: Dict[str, SymbolInfo]
|
||||
) -> None:
|
||||
"""Discover Python exports from __init__.py using AST."""
|
||||
init_file = pkg_path / "__init__.py"
|
||||
if not init_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(init_file, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
# Extract __all__ if present
|
||||
all_exports: Set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == '__all__':
|
||||
if isinstance(node.value, ast.List):
|
||||
for elt in node.value.elts:
|
||||
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
||||
all_exports.add(elt.value)
|
||||
|
||||
# Track imports to find source files
|
||||
import_sources: Dict[str, Tuple[str, str]] = {} # name -> (module, original_name)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
for alias in node.names:
|
||||
name = alias.asname if alias.asname else alias.name
|
||||
import_sources[name] = (node.module, alias.name)
|
||||
|
||||
# Parse __getattr__ for lazy-loaded symbols
|
||||
lazy_sources = self._parse_getattr_lazy_imports(tree)
|
||||
import_sources.update(lazy_sources)
|
||||
|
||||
# For each export, find its definition
|
||||
for export_name in sorted(all_exports):
|
||||
if export_name in import_sources:
|
||||
module_path, original_name = import_sources[export_name]
|
||||
# Resolve to file
|
||||
symbol_info = self._resolve_symbol(
|
||||
pkg_path, pkg_name, module_path, original_name, export_name
|
||||
)
|
||||
if symbol_info:
|
||||
symbols[export_name] = symbol_info
|
||||
else:
|
||||
# Defined in __init__.py itself
|
||||
symbol_info = self._find_symbol_in_file(
|
||||
init_file, export_name, pkg_name
|
||||
)
|
||||
if symbol_info:
|
||||
symbols[export_name] = symbol_info
|
||||
|
||||
def _parse_getattr_lazy_imports(self, tree: ast.Module) -> Dict[str, Tuple[str, str]]:
|
||||
"""
|
||||
Parse __getattr__ function to extract lazy-loaded imports.
|
||||
|
||||
Looks for patterns like:
|
||||
if name == "Agent":
|
||||
from .agent.agent import Agent
|
||||
return Agent
|
||||
"""
|
||||
lazy_sources: Dict[str, Tuple[str, str]] = {}
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef) and node.name == '__getattr__':
|
||||
# Walk through ALL nodes in the function to find if statements
|
||||
for stmt in ast.walk(node):
|
||||
if isinstance(stmt, ast.If):
|
||||
# Check if this is `if name == "SomeName":`
|
||||
symbol_name = self._extract_name_comparison(stmt.test)
|
||||
if symbol_name:
|
||||
# Look for import statement in the body
|
||||
for inner_stmt in stmt.body:
|
||||
if isinstance(inner_stmt, ast.ImportFrom) and inner_stmt.module:
|
||||
for alias in inner_stmt.names:
|
||||
imported_name = alias.name
|
||||
lazy_sources[symbol_name] = (inner_stmt.module, imported_name)
|
||||
break
|
||||
break
|
||||
|
||||
return lazy_sources
|
||||
|
||||
def _extract_name_comparison(self, test: ast.expr) -> Optional[str]:
|
||||
"""
|
||||
Extract the string value from comparisons like `name == "Agent"`.
|
||||
Also handles `name in ("A", "B", "C")` patterns.
|
||||
"""
|
||||
if isinstance(test, ast.Compare):
|
||||
# Handle: name == "Agent"
|
||||
if (len(test.ops) == 1 and
|
||||
isinstance(test.ops[0], ast.Eq) and
|
||||
isinstance(test.left, ast.Name) and test.left.id == 'name'):
|
||||
if len(test.comparators) == 1:
|
||||
comp = test.comparators[0]
|
||||
if isinstance(comp, ast.Constant) and isinstance(comp.value, str):
|
||||
return comp.value
|
||||
# Handle: name in ("A", "B", "C")
|
||||
if (len(test.ops) == 1 and
|
||||
isinstance(test.ops[0], ast.In) and
|
||||
isinstance(test.left, ast.Name) and test.left.id == 'name'):
|
||||
# Return first item for now (we'll get others via elif)
|
||||
if len(test.comparators) == 1:
|
||||
comp = test.comparators[0]
|
||||
if isinstance(comp, ast.Tuple):
|
||||
for elt in comp.elts:
|
||||
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
||||
return elt.value
|
||||
return None
|
||||
|
||||
def _resolve_symbol(
|
||||
self,
|
||||
pkg_path: Path,
|
||||
pkg_name: str,
|
||||
module_path: str,
|
||||
original_name: str,
|
||||
export_name: str
|
||||
) -> Optional[SymbolInfo]:
|
||||
"""Resolve a symbol to its definition file."""
|
||||
file_path = None
|
||||
|
||||
# Handle relative imports (starting with .)
|
||||
if module_path.startswith('.'):
|
||||
# Convert relative to absolute
|
||||
parts = module_path.lstrip('.').split('.')
|
||||
rel_depth = len(module_path) - len(module_path.lstrip('.'))
|
||||
if rel_depth == 1:
|
||||
# Same package
|
||||
file_path = pkg_path
|
||||
else:
|
||||
file_path = pkg_path.parent
|
||||
for _ in range(rel_depth - 1):
|
||||
file_path = file_path.parent
|
||||
for part in parts:
|
||||
if part:
|
||||
file_path = file_path / part
|
||||
# Handle absolute imports starting with package name
|
||||
elif module_path.startswith(pkg_name + '.') or module_path == pkg_name:
|
||||
rel_path = module_path[len(pkg_name):].lstrip('.')
|
||||
parts = rel_path.split('.') if rel_path else []
|
||||
file_path = pkg_path
|
||||
for part in parts:
|
||||
file_path = file_path / part
|
||||
else:
|
||||
# Treat as relative import within package (e.g., 'agent.agent' -> pkg_path/agent/agent.py)
|
||||
# This handles lazy imports in __getattr__ that use implicit relative paths
|
||||
parts = module_path.split('.')
|
||||
file_path = pkg_path
|
||||
for part in parts:
|
||||
file_path = file_path / part
|
||||
|
||||
if file_path is None:
|
||||
return None
|
||||
|
||||
# Try as directory with __init__.py or as .py file
|
||||
if file_path.is_dir():
|
||||
init_file = file_path / "__init__.py"
|
||||
if init_file.exists():
|
||||
return self._find_symbol_in_file(init_file, original_name, pkg_name, export_name)
|
||||
|
||||
py_file = file_path.with_suffix('.py')
|
||||
if py_file.exists():
|
||||
return self._find_symbol_in_file(py_file, original_name, pkg_name, export_name)
|
||||
|
||||
return None
|
||||
|
||||
def _find_symbol_in_file(
|
||||
self,
|
||||
file_path: Path,
|
||||
symbol_name: str,
|
||||
pkg_name: str,
|
||||
export_name: Optional[str] = None
|
||||
) -> Optional[SymbolInfo]:
|
||||
"""Find a symbol definition in a Python file."""
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return None
|
||||
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
module_path = self._file_to_module(file_path, pkg_name)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef) and node.name == symbol_name:
|
||||
methods = self._extract_methods(node)
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='class',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_class_signature(node),
|
||||
methods=methods,
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.FunctionDef) and node.name == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='function',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.AsyncFunctionDef) and node.name == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='function',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='constant',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno
|
||||
)
|
||||
|
||||
# Check for re-exports in __init__.py
|
||||
if file_path.name == "__init__.py":
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
for alias in node.names:
|
||||
name = alias.asname if alias.asname else alias.name
|
||||
if name == symbol_name:
|
||||
# Recursively resolve
|
||||
return self._resolve_symbol(
|
||||
file_path.parent, pkg_name,
|
||||
'.' + node.module if not node.module.startswith('.') else node.module,
|
||||
alias.name, export_name or symbol_name
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _extract_methods(self, class_node: ast.ClassDef) -> List[MethodInfo]:
|
||||
"""Extract public methods from a class."""
|
||||
methods = []
|
||||
for node in class_node.body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if not node.name.startswith('_') or node.name in ('__init__', '__call__'):
|
||||
methods.append(MethodInfo(
|
||||
name=node.name,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
line_number=node.lineno
|
||||
))
|
||||
return sorted(methods, key=lambda m: m.name)
|
||||
|
||||
def _extract_function_signature(self, node: ast.FunctionDef) -> str:
|
||||
"""Extract function signature as string."""
|
||||
args = []
|
||||
|
||||
# Regular args
|
||||
defaults_offset = len(node.args.args) - len(node.args.defaults)
|
||||
for i, arg in enumerate(node.args.args):
|
||||
if arg.arg == 'self' or arg.arg == 'cls':
|
||||
continue
|
||||
arg_str = arg.arg
|
||||
if arg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(arg.annotation)}"
|
||||
if i >= defaults_offset:
|
||||
default = node.args.defaults[i - defaults_offset]
|
||||
arg_str += f" = {self._default_to_str(default)}"
|
||||
args.append(arg_str)
|
||||
|
||||
# *args
|
||||
if node.args.vararg:
|
||||
arg_str = f"*{node.args.vararg.arg}"
|
||||
if node.args.vararg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(node.args.vararg.annotation)}"
|
||||
args.append(arg_str)
|
||||
|
||||
# **kwargs
|
||||
if node.args.kwarg:
|
||||
arg_str = f"**{node.args.kwarg.arg}"
|
||||
if node.args.kwarg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(node.args.kwarg.annotation)}"
|
||||
args.append(arg_str)
|
||||
|
||||
return f"({', '.join(args)})"
|
||||
|
||||
def _extract_class_signature(self, node: ast.ClassDef) -> str:
|
||||
"""Extract class __init__ signature."""
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef) and item.name == '__init__':
|
||||
return self._extract_function_signature(item)
|
||||
return "()"
|
||||
|
||||
def _extract_return_type(self, node: ast.FunctionDef) -> Optional[str]:
|
||||
"""Extract return type annotation."""
|
||||
if node.returns:
|
||||
return self._annotation_to_str(node.returns)
|
||||
return None
|
||||
|
||||
def _annotation_to_str(self, node: ast.expr) -> str:
|
||||
"""Convert AST annotation to string."""
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.Constant):
|
||||
return repr(node.value)
|
||||
elif isinstance(node, ast.Subscript):
|
||||
value = self._annotation_to_str(node.value)
|
||||
slice_val = self._annotation_to_str(node.slice)
|
||||
return f"{value}[{slice_val}]"
|
||||
elif isinstance(node, ast.Attribute):
|
||||
return f"{self._annotation_to_str(node.value)}.{node.attr}"
|
||||
elif isinstance(node, ast.Tuple):
|
||||
elts = ', '.join(self._annotation_to_str(e) for e in node.elts)
|
||||
return elts
|
||||
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
||||
left = self._annotation_to_str(node.left)
|
||||
right = self._annotation_to_str(node.right)
|
||||
return f"{left} | {right}"
|
||||
elif isinstance(node, ast.List):
|
||||
elts = ', '.join(self._annotation_to_str(e) for e in node.elts)
|
||||
return f"[{elts}]"
|
||||
return "Any"
|
||||
|
||||
def _default_to_str(self, node: ast.expr) -> str:
|
||||
"""Convert default value to string."""
|
||||
if isinstance(node, ast.Constant):
|
||||
return repr(node.value)
|
||||
elif isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.List):
|
||||
return "[]"
|
||||
elif isinstance(node, ast.Dict):
|
||||
return "{}"
|
||||
elif isinstance(node, ast.Call):
|
||||
return "..."
|
||||
return "..."
|
||||
|
||||
def _get_relative_path(self, file_path: Path) -> str:
|
||||
"""Get path relative to repo root."""
|
||||
try:
|
||||
return "./" + str(file_path.relative_to(self.repo_root))
|
||||
except ValueError:
|
||||
return str(file_path)
|
||||
|
||||
def _file_to_module(self, file_path: Path, pkg_name: str) -> str:
|
||||
"""Convert file path to module path."""
|
||||
try:
|
||||
if pkg_name == "praisonaiagents":
|
||||
rel = file_path.relative_to(self.agents_pkg.parent)
|
||||
else:
|
||||
rel = file_path.relative_to(self.wrapper_pkg.parent)
|
||||
parts = list(rel.parts)
|
||||
if parts[-1] == "__init__.py":
|
||||
parts = parts[:-1]
|
||||
elif parts[-1].endswith('.py'):
|
||||
parts[-1] = parts[-1][:-3]
|
||||
return '.'.join(parts)
|
||||
except ValueError:
|
||||
return pkg_name
|
||||
|
||||
def _discover_cli_commands(self) -> None:
|
||||
"""Discover CLI commands from Typer apps."""
|
||||
commands_dir = self.wrapper_pkg / "cli" / "commands"
|
||||
if not commands_dir.exists():
|
||||
return
|
||||
|
||||
for py_file in sorted(commands_dir.glob("*.py")):
|
||||
if py_file.name.startswith('_'):
|
||||
continue
|
||||
self._parse_typer_commands(py_file)
|
||||
|
||||
# Also check main.py for argparse commands
|
||||
main_py = self.wrapper_pkg / "cli" / "main.py"
|
||||
if main_py.exists():
|
||||
self._parse_main_commands(main_py)
|
||||
|
||||
def _parse_typer_commands(self, file_path: Path) -> None:
|
||||
"""Parse Typer command definitions from a file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
command_name = file_path.stem
|
||||
|
||||
# Find @app.command() decorators
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for decorator in node.decorator_list:
|
||||
if self._is_command_decorator(decorator):
|
||||
cmd_name = self._extract_command_name(decorator, node.name)
|
||||
help_text = ast.get_docstring(node)
|
||||
self.cli_commands.append(CLICommand(
|
||||
command_path=f"praisonai {command_name} {cmd_name}".strip(),
|
||||
help_text=help_text[:100] if help_text else None,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno
|
||||
))
|
||||
|
||||
def _is_command_decorator(self, decorator: ast.expr) -> bool:
|
||||
"""Check if decorator is @app.command() or similar."""
|
||||
if isinstance(decorator, ast.Call):
|
||||
if isinstance(decorator.func, ast.Attribute):
|
||||
return decorator.func.attr in ('command', 'callback')
|
||||
elif isinstance(decorator, ast.Attribute):
|
||||
return decorator.attr in ('command', 'callback')
|
||||
return False
|
||||
|
||||
def _extract_command_name(self, decorator: ast.expr, func_name: str) -> str:
|
||||
"""Extract command name from decorator or function name."""
|
||||
if isinstance(decorator, ast.Call):
|
||||
for arg in decorator.args:
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
return arg.value
|
||||
for kw in decorator.keywords:
|
||||
if kw.arg == 'name' and isinstance(kw.value, ast.Constant):
|
||||
return kw.value.value
|
||||
# Convert function name: some_command -> some-command
|
||||
return func_name.replace('_', '-')
|
||||
|
||||
def _parse_main_commands(self, file_path: Path) -> None:
|
||||
"""Parse main CLI commands from argparse in main.py."""
|
||||
# Add base commands that are handled in main.py
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
base_commands = [
|
||||
("praisonai", "Main CLI entry point"),
|
||||
("praisonai run", "Run agents from YAML file"),
|
||||
("praisonai chat", "Terminal chat interface"),
|
||||
("praisonai code", "Terminal code assistant"),
|
||||
]
|
||||
for cmd, help_text in base_commands:
|
||||
self.cli_commands.append(CLICommand(
|
||||
command_path=cmd,
|
||||
help_text=help_text,
|
||||
file_path=rel_path,
|
||||
line_number=1
|
||||
))
|
||||
|
||||
def _discover_ts_exports(self) -> None:
|
||||
"""Discover TypeScript exports from index.ts."""
|
||||
index_file = self.ts_pkg / "src" / "index.ts"
|
||||
if not index_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(index_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except (IOError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
# Parse export statements using regex
|
||||
# export { Name1, Name2 } from './path'
|
||||
export_pattern = r"export\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]"
|
||||
type_export_pattern = r"export\s+type\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]"
|
||||
|
||||
for match in re.finditer(export_pattern, content):
|
||||
names = match.group(1)
|
||||
source = match.group(2)
|
||||
for name in names.split(','):
|
||||
name = name.strip()
|
||||
if ' as ' in name:
|
||||
name = name.split(' as ')[1].strip()
|
||||
if name and not name.startswith('type '):
|
||||
self.ts_exports.append(TSExport(
|
||||
name=name,
|
||||
kind='export',
|
||||
source_file=source,
|
||||
is_type=False
|
||||
))
|
||||
|
||||
for match in re.finditer(type_export_pattern, content):
|
||||
names = match.group(1)
|
||||
source = match.group(2)
|
||||
for name in names.split(','):
|
||||
name = name.strip()
|
||||
if ' as ' in name:
|
||||
name = name.split(' as ')[1].strip()
|
||||
if name:
|
||||
self.ts_exports.append(TSExport(
|
||||
name=name,
|
||||
kind='type',
|
||||
source_file=source,
|
||||
is_type=True
|
||||
))
|
||||
|
||||
def generate(self) -> str:
|
||||
"""Generate the api.md content."""
|
||||
self.discover_all()
|
||||
|
||||
lines = [
|
||||
"# PraisonAI API Reference",
|
||||
"",
|
||||
"This file is auto-generated. Do not edit manually.",
|
||||
"Regenerate with: `praisonai docs api-md --write`",
|
||||
"",
|
||||
]
|
||||
|
||||
# Group symbols by section
|
||||
sections = self._group_symbols_by_section()
|
||||
|
||||
# Generate each section
|
||||
section_order = [
|
||||
'Shared Types', 'Agents', 'Tools', 'Workflows', 'DB', 'Memory',
|
||||
'Knowledge', 'RAG', 'Handoff', 'Guardrails', 'Planning', 'Skills',
|
||||
'Session', 'MCP', 'Telemetry', 'Observability', 'Context', 'UI',
|
||||
'Config', 'Display', 'Utilities'
|
||||
]
|
||||
|
||||
for section_name in section_order:
|
||||
if section_name in sections and sections[section_name]:
|
||||
lines.extend(self._generate_section(section_name, sections[section_name]))
|
||||
|
||||
# Handle remaining symbols not in predefined sections
|
||||
remaining = self._get_remaining_symbols(sections)
|
||||
if remaining:
|
||||
lines.extend(self._generate_section('Other', remaining))
|
||||
|
||||
# Wrapper package exports
|
||||
if self.wrapper_symbols:
|
||||
lines.extend(self._generate_wrapper_section())
|
||||
|
||||
# CLI section
|
||||
if self.cli_commands:
|
||||
lines.extend(self._generate_cli_section())
|
||||
|
||||
# TypeScript section
|
||||
if self.ts_exports:
|
||||
lines.extend(self._generate_ts_section())
|
||||
|
||||
# Optional Plugins section
|
||||
lines.extend(self._generate_plugins_section())
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _group_symbols_by_section(self) -> Dict[str, List[SymbolInfo]]:
|
||||
"""Group symbols by their section."""
|
||||
sections: Dict[str, List[SymbolInfo]] = {}
|
||||
|
||||
for section_name, symbol_names in self.SECTION_GROUPS.items():
|
||||
section_symbols = []
|
||||
for name in symbol_names:
|
||||
if name in self.agents_symbols:
|
||||
section_symbols.append(self.agents_symbols[name])
|
||||
if section_symbols:
|
||||
sections[section_name] = sorted(section_symbols, key=lambda s: s.name)
|
||||
|
||||
return sections
|
||||
|
||||
def _get_remaining_symbols(self, sections: Dict[str, List[SymbolInfo]]) -> List[SymbolInfo]:
|
||||
"""Get symbols not in any predefined section."""
|
||||
used_names = set()
|
||||
for symbol_list in sections.values():
|
||||
for symbol in symbol_list:
|
||||
used_names.add(symbol.name)
|
||||
|
||||
remaining = []
|
||||
for name, symbol in self.agents_symbols.items():
|
||||
if name not in used_names:
|
||||
remaining.append(symbol)
|
||||
|
||||
return sorted(remaining, key=lambda s: s.name)
|
||||
|
||||
def _generate_section(self, section_name: str, symbols: List[SymbolInfo]) -> List[str]:
|
||||
"""Generate a section of the API documentation."""
|
||||
lines = [
|
||||
f"# {section_name}",
|
||||
"",
|
||||
]
|
||||
|
||||
# Types block
|
||||
imports = []
|
||||
for symbol in symbols:
|
||||
if symbol.module_path.startswith('praisonaiagents'):
|
||||
imports.append(symbol.name)
|
||||
|
||||
if imports:
|
||||
lines.append("Types:")
|
||||
lines.append("```python")
|
||||
# Group imports by module
|
||||
lines.append(f"from praisonaiagents import {', '.join(sorted(imports))}")
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
# Methods block
|
||||
methods_lines = []
|
||||
for symbol in symbols:
|
||||
if symbol.kind == 'class' and symbol.methods:
|
||||
for method in symbol.methods:
|
||||
if method.name == '__init__':
|
||||
continue
|
||||
ret_type = f" -> {method.return_type}" if method.return_type else ""
|
||||
methods_lines.append(
|
||||
f'* <code title="class {symbol.name}">{symbol.name}.'
|
||||
f'<a href="{symbol.file_path}">{method.name}</a>'
|
||||
f'{method.signature}{ret_type}</code>'
|
||||
)
|
||||
elif symbol.kind == 'function':
|
||||
ret_type = f" -> {symbol.return_type}" if symbol.return_type else ""
|
||||
module_prefix = symbol.module_path.split('.')[0]
|
||||
methods_lines.append(
|
||||
f'* <code title="function">{module_prefix}.'
|
||||
f'<a href="{symbol.file_path}">{symbol.name}</a>'
|
||||
f'{symbol.signature or "()"}{ret_type}</code>'
|
||||
)
|
||||
|
||||
if methods_lines:
|
||||
lines.append("Methods:")
|
||||
lines.append("")
|
||||
lines.extend(methods_lines)
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_wrapper_section(self) -> List[str]:
|
||||
"""Generate section for praisonai wrapper package."""
|
||||
lines = [
|
||||
"# Wrapper (praisonai)",
|
||||
"",
|
||||
"Types:",
|
||||
"```python",
|
||||
]
|
||||
|
||||
imports = sorted(self.wrapper_symbols.keys())
|
||||
lines.append(f"from praisonai import {', '.join(imports)}")
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_cli_section(self) -> List[str]:
|
||||
"""Generate CLI commands section."""
|
||||
lines = [
|
||||
"# CLI",
|
||||
"",
|
||||
"Methods:",
|
||||
"",
|
||||
]
|
||||
|
||||
# Sort and deduplicate commands
|
||||
seen = set()
|
||||
for cmd in sorted(self.cli_commands, key=lambda c: c.command_path):
|
||||
if cmd.command_path in seen:
|
||||
continue
|
||||
seen.add(cmd.command_path)
|
||||
lines.append(
|
||||
f'* <code title="cli">{cmd.command_path} '
|
||||
f'<a href="{cmd.file_path}">--help</a></code>'
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def _generate_ts_section(self) -> List[str]:
|
||||
"""Generate TypeScript exports section."""
|
||||
lines = [
|
||||
"# TypeScript",
|
||||
"",
|
||||
"Types/Exports:",
|
||||
"```ts",
|
||||
]
|
||||
|
||||
# Group by source file
|
||||
by_source: Dict[str, List[TSExport]] = {}
|
||||
for export in self.ts_exports:
|
||||
if export.source_file not in by_source:
|
||||
by_source[export.source_file] = []
|
||||
by_source[export.source_file].append(export)
|
||||
|
||||
for source in sorted(by_source.keys()):
|
||||
exports = by_source[source]
|
||||
regular = [e.name for e in exports if not e.is_type]
|
||||
types = [e.name for e in exports if e.is_type]
|
||||
|
||||
if regular:
|
||||
lines.append(f'export {{ {", ".join(sorted(regular))} }} from "{source}";')
|
||||
if types:
|
||||
lines.append(f'export type {{ {", ".join(sorted(types))} }} from "{source}";')
|
||||
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def _generate_plugins_section(self) -> List[str]:
|
||||
"""Generate optional plugins section."""
|
||||
return [
|
||||
"# Optional Plugins",
|
||||
"",
|
||||
"External tools are available via `praisonai-tools` package:",
|
||||
"",
|
||||
"```bash",
|
||||
"pip install praisonai-tools",
|
||||
"```",
|
||||
"",
|
||||
"See [PraisonAI-tools](https://github.com/MervinPraison/PraisonAI-tools) for available tools.",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def generate_api_md(
|
||||
repo_root: Optional[Path] = None,
|
||||
output_path: Optional[Path] = None,
|
||||
check: bool = False,
|
||||
stdout: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Generate api.md file.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path (auto-detected if None)
|
||||
output_path: Output file path (defaults to repo_root/api.md)
|
||||
check: If True, check if existing file matches (exit 1 if different)
|
||||
stdout: If True, print to stdout instead of writing file
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for check failure)
|
||||
"""
|
||||
generator = ApiMdGenerator(repo_root)
|
||||
content = generator.generate()
|
||||
|
||||
if stdout:
|
||||
print(content)
|
||||
return 0
|
||||
|
||||
if output_path is None:
|
||||
output_path = generator.repo_root / "api.md"
|
||||
|
||||
if check:
|
||||
if output_path.exists():
|
||||
with open(output_path, 'r', encoding='utf-8') as f:
|
||||
existing = f.read()
|
||||
if existing == content:
|
||||
print(f"✓ {output_path} is up to date")
|
||||
return 0
|
||||
else:
|
||||
print(f"✗ {output_path} is out of date")
|
||||
print("Run `praisonai docs api-md --write` to regenerate")
|
||||
return 1
|
||||
else:
|
||||
print(f"✗ {output_path} does not exist")
|
||||
print("Run `praisonai docs api-md --write` to generate")
|
||||
return 1
|
||||
|
||||
# Write the file
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Generated {output_path}")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point for the generator."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate api.md for PraisonAI"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--write', action='store_true', default=True,
|
||||
help='Write api.md file (default)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--check', action='store_true',
|
||||
help='Check if api.md is up to date (exit 1 if not)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--stdout', action='store_true',
|
||||
help='Print to stdout instead of writing file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o', type=Path,
|
||||
help='Output file path (default: repo_root/api.md)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
exit_code = generate_api_md(
|
||||
output_path=args.output,
|
||||
check=args.check,
|
||||
stdout=args.stdout
|
||||
)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -141,11 +141,18 @@ def main_callback(
|
||||
)
|
||||
set_output_controller(state.output_controller)
|
||||
|
||||
# If no command provided, show help or handle legacy behavior
|
||||
# If no command provided, start interactive mode
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Check if there are remaining args (legacy behavior)
|
||||
# This is handled by the legacy adapter in main.py
|
||||
pass
|
||||
from praisonai.cli.interactive.async_tui import AsyncTUI, AsyncTUIConfig
|
||||
|
||||
tui_config = AsyncTUIConfig(
|
||||
model="gpt-4o-mini",
|
||||
show_logo=True,
|
||||
show_status_bar=state.output_format != OutputFormat.json,
|
||||
)
|
||||
|
||||
tui = AsyncTUI(config=tui_config)
|
||||
tui.run()
|
||||
|
||||
|
||||
def get_output_controller() -> OutputController:
|
||||
@@ -266,6 +273,115 @@ def register_commands():
|
||||
app.add_typer(test_app, name="test", help="Run test suite with tier and provider options")
|
||||
app.add_typer(examples_app, name="examples", help="Run and manage example files")
|
||||
|
||||
# Register standardise command
|
||||
try:
|
||||
standardise_app = typer.Typer(name="standardise", help="Documentation and examples standardisation (FDEP)")
|
||||
|
||||
@standardise_app.command("check")
|
||||
def standardise_check(
|
||||
path: str = typer.Option(".", "--path", "-p", help="Project root path"),
|
||||
feature: str = typer.Option(None, "--feature", help="Specific feature slug"),
|
||||
scope: str = typer.Option("all", "--scope", help="Scope: all, docs, examples, sdk, cli"),
|
||||
ci: bool = typer.Option(False, "--ci", help="CI mode"),
|
||||
):
|
||||
"""Check for standardisation issues."""
|
||||
from .commands.standardise import _run_check
|
||||
import argparse
|
||||
args = argparse.Namespace(path=path, feature=feature, scope=scope, ci=ci, dry_run=True)
|
||||
_run_check(args)
|
||||
|
||||
@standardise_app.command("report")
|
||||
def standardise_report(
|
||||
path: str = typer.Option(".", "--path", "-p", help="Project root path"),
|
||||
format: str = typer.Option("text", "--format", "-f", help="Format: text, json, markdown"),
|
||||
output: str = typer.Option(None, "--output", "-o", help="Output file"),
|
||||
ci: bool = typer.Option(False, "--ci", help="CI mode"),
|
||||
):
|
||||
"""Generate detailed report."""
|
||||
from .commands.standardise import _run_report
|
||||
import argparse
|
||||
args = argparse.Namespace(path=path, format=format, output=output, ci=ci, feature=None, scope="all", dry_run=True)
|
||||
_run_report(args)
|
||||
|
||||
@standardise_app.command("fix")
|
||||
def standardise_fix(
|
||||
path: str = typer.Option(".", "--path", "-p", help="Project root path"),
|
||||
feature: str = typer.Option(None, "--feature", help="Specific feature slug"),
|
||||
apply: bool = typer.Option(False, "--apply", help="Actually apply changes"),
|
||||
no_backup: bool = typer.Option(False, "--no-backup", help="Don't create backups"),
|
||||
):
|
||||
"""Fix standardisation issues."""
|
||||
from .commands.standardise import _run_fix
|
||||
import argparse
|
||||
args = argparse.Namespace(path=path, feature=feature, apply=apply, no_backup=no_backup, scope="all", ci=False, dry_run=not apply)
|
||||
_run_fix(args)
|
||||
|
||||
@standardise_app.command("init")
|
||||
def standardise_init(
|
||||
feature: str = typer.Argument(..., help="Feature slug to initialise"),
|
||||
path: str = typer.Option(".", "--path", "-p", help="Project root path"),
|
||||
apply: bool = typer.Option(False, "--apply", help="Actually create files"),
|
||||
):
|
||||
"""Initialise a new feature with all required artifacts."""
|
||||
from .commands.standardise import _run_init
|
||||
import argparse
|
||||
args = argparse.Namespace(feature=feature, path=path, apply=apply, scope="all", ci=False, dry_run=not apply)
|
||||
_run_init(args)
|
||||
|
||||
@standardise_app.command("ai")
|
||||
def standardise_ai(
|
||||
feature: str = typer.Argument(..., help="Feature slug to generate content for"),
|
||||
gen_type: str = typer.Option("all", "--type", "-t", help="Type: docs, examples, all"),
|
||||
apply: bool = typer.Option(False, "--apply", help="Actually create files"),
|
||||
verify: bool = typer.Option(False, "--verify", help="Verify with AI"),
|
||||
model: str = typer.Option("gpt-4o-mini", "--model", help="LLM model"),
|
||||
path: str = typer.Option(".", "--path", "-p", help="Project root path"),
|
||||
):
|
||||
"""AI-powered generation of docs/examples."""
|
||||
from .commands.standardise import _run_ai
|
||||
import argparse
|
||||
args = argparse.Namespace(feature=feature, type=gen_type, apply=apply, verify=verify, model=model, path=path, scope="all", ci=False, dry_run=not apply)
|
||||
_run_ai(args)
|
||||
|
||||
@standardise_app.command("checkpoint")
|
||||
def standardise_checkpoint(
|
||||
message: str = typer.Option(None, "--message", "-m", help="Checkpoint message"),
|
||||
path: str = typer.Option(".", "--path", "-p", help="Repository path"),
|
||||
):
|
||||
"""Create an undo checkpoint."""
|
||||
from .commands.standardise import _run_checkpoint
|
||||
import argparse
|
||||
args = argparse.Namespace(message=message, path=path)
|
||||
_run_checkpoint(args)
|
||||
|
||||
@standardise_app.command("undo")
|
||||
def standardise_undo(
|
||||
checkpoint: str = typer.Option(None, "--checkpoint", help="Checkpoint ID"),
|
||||
list_checkpoints: bool = typer.Option(False, "--list", help="List checkpoints"),
|
||||
path: str = typer.Option(".", "--path", "-p", help="Repository path"),
|
||||
):
|
||||
"""Undo to a previous checkpoint."""
|
||||
from .commands.standardise import _run_undo
|
||||
import argparse
|
||||
args = argparse.Namespace(checkpoint=checkpoint, list=list_checkpoints, path=path)
|
||||
_run_undo(args)
|
||||
|
||||
@standardise_app.command("redo")
|
||||
def standardise_redo(
|
||||
path: str = typer.Option(".", "--path", "-p", help="Repository path"),
|
||||
):
|
||||
"""Redo after an undo."""
|
||||
from .commands.standardise import _run_redo
|
||||
import argparse
|
||||
args = argparse.Namespace(path=path)
|
||||
_run_redo(args)
|
||||
|
||||
app.add_typer(standardise_app, name="standardise", help="Documentation and examples standardisation (FDEP)")
|
||||
# Also register as 'standardize' for US spelling
|
||||
app.add_typer(standardise_app, name="standardize", help="Documentation and examples standardisation (FDEP)")
|
||||
except Exception:
|
||||
pass # Graceful degradation if standardise module not available
|
||||
|
||||
# Register TUI and queue commands
|
||||
tui_app = create_tui_debug_app()
|
||||
queue_app = create_queue_app()
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
PraisonAI Branding - Single Source of Truth.
|
||||
|
||||
This module provides unified branding assets (logo, version, product name)
|
||||
for all interactive UIs. All frontends should import from here.
|
||||
|
||||
Usage:
|
||||
from praisonai.cli.branding import get_logo, get_version, PRODUCT_NAME
|
||||
"""
|
||||
|
||||
# Product name
|
||||
PRODUCT_NAME = "Praison AI"
|
||||
|
||||
# ASCII Art Logos - responsive to terminal width
|
||||
LOGO_LARGE = r"""
|
||||
██████╗ ██████╗ █████╗ ██╗███████╗ ██████╗ ███╗ ██╗ █████╗ ██╗
|
||||
██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██║
|
||||
██████╔╝██████╔╝███████║██║███████╗██║ ██║██╔██╗ ██║ ███████║██║
|
||||
██╔═══╝ ██╔══██╗██╔══██║██║╚════██║██║ ██║██║╚██╗██║ ██╔══██║██║
|
||||
██║ ██║ ██║██║ ██║██║███████║╚██████╔╝██║ ╚████║ ██║ ██║██║
|
||||
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝
|
||||
"""
|
||||
|
||||
LOGO_MEDIUM = r"""
|
||||
╔═╗┬─┐┌─┐┬┌─┐┌─┐┌┐┌ ╔═╗╦
|
||||
╠═╝├┬┘├─┤│└─┐│ ││││ ╠═╣║
|
||||
╩ ┴└─┴ ┴┴└─┘└─┘┘└┘ ╩ ╩╩
|
||||
"""
|
||||
|
||||
LOGO_SMALL = "▶ Praison AI"
|
||||
|
||||
LOGO_MINIMAL = "Praison AI"
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""Get PraisonAI version string."""
|
||||
try:
|
||||
from praisonai import __version__
|
||||
return __version__
|
||||
except Exception:
|
||||
return "1.0.0"
|
||||
|
||||
|
||||
def get_logo(width: int = 80) -> str:
|
||||
"""
|
||||
Get appropriate logo based on terminal width.
|
||||
|
||||
Args:
|
||||
width: Terminal width in characters
|
||||
|
||||
Returns:
|
||||
ASCII art logo string appropriate for the width
|
||||
"""
|
||||
if width >= 75:
|
||||
return LOGO_LARGE
|
||||
elif width >= 40:
|
||||
return LOGO_MEDIUM
|
||||
else:
|
||||
return LOGO_SMALL
|
||||
|
||||
|
||||
def get_banner(width: int = 80, show_version: bool = True, model: str = None) -> str:
|
||||
"""
|
||||
Get full banner with logo, version, and optional model info.
|
||||
|
||||
Args:
|
||||
width: Terminal width
|
||||
show_version: Whether to include version
|
||||
model: Optional model name to display
|
||||
|
||||
Returns:
|
||||
Complete banner string
|
||||
"""
|
||||
lines = []
|
||||
|
||||
# Logo
|
||||
logo = get_logo(width)
|
||||
lines.append(logo.strip())
|
||||
lines.append("")
|
||||
|
||||
# Version and model
|
||||
version_line = f" v{get_version()}"
|
||||
if model:
|
||||
version_line += f" · Model: {model}"
|
||||
lines.append(version_line)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_welcome_tips() -> str:
|
||||
"""Get welcome tips for new sessions."""
|
||||
return """ Type your message and press Enter. Use /help for commands.
|
||||
Use PageUp/PageDown or Ctrl+Up/Down to scroll."""
|
||||
@@ -67,6 +67,7 @@ def chat_main(
|
||||
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
|
||||
no_acp: bool = typer.Option(False, "--no-acp", help="Disable ACP tools"),
|
||||
no_lsp: bool = typer.Option(False, "--no-lsp", help="Disable LSP tools"),
|
||||
safe_mode: bool = typer.Option(False, "--safe", help="Safe mode: require approval for file writes and commands"),
|
||||
autonomy: bool = typer.Option(True, "--autonomy/--no-autonomy", help="Enable agent autonomy for complex tasks"),
|
||||
# NEW: Agent-like consolidated params for ALL GREEN consistency
|
||||
knowledge: Optional[str] = typer.Option(
|
||||
@@ -93,8 +94,46 @@ def chat_main(
|
||||
is_flag=False,
|
||||
flag_value="true",
|
||||
),
|
||||
# NEW: Additional consolidated params for ALL GREEN feature parity
|
||||
planning: Optional[str] = typer.Option(
|
||||
None, "--planning",
|
||||
help="Enable planning mode. Use --planning for default, --planning=thorough for preset",
|
||||
is_flag=False,
|
||||
flag_value="true",
|
||||
),
|
||||
context: Optional[str] = typer.Option(
|
||||
None, "--context",
|
||||
help="Enable context management. Use --context for default",
|
||||
is_flag=False,
|
||||
flag_value="true",
|
||||
),
|
||||
output: Optional[str] = typer.Option(
|
||||
None, "--output", "-o",
|
||||
help="Output mode (default: actions). Options: actions, plain, verbose, json, silent",
|
||||
),
|
||||
execution: Optional[str] = typer.Option(
|
||||
None, "--execution",
|
||||
help="Execution preset. Use --execution=fast, --execution=thorough, --execution=unlimited",
|
||||
),
|
||||
hooks: Optional[str] = typer.Option(
|
||||
None, "--hooks",
|
||||
help="Hooks config file path for lifecycle callbacks",
|
||||
),
|
||||
caching: Optional[str] = typer.Option(
|
||||
None, "--caching",
|
||||
help="Enable caching. Use --caching for default, --caching=redis for preset",
|
||||
is_flag=False,
|
||||
flag_value="true",
|
||||
),
|
||||
profile: bool = typer.Option(False, "--profile", help="Enable CLI profiling (timing breakdown)"),
|
||||
profile_deep: bool = typer.Option(False, "--profile-deep", help="Enable deep profiling (cProfile stats, higher overhead)"),
|
||||
debug: bool = typer.Option(False, "--debug", help="Enable debug logging to ~/.praisonai/async_tui_debug.log"),
|
||||
# UI backend selection
|
||||
ui_backend: str = typer.Option("auto", "--ui-backend", help="UI backend: auto, plain, rich, mg (middle-ground)"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON (forces plain backend)"),
|
||||
no_color: bool = typer.Option(False, "--no-color", help="Disable colors"),
|
||||
theme: str = typer.Option("default", "--theme", help="UI theme: default, dark, light, minimal"),
|
||||
compact: bool = typer.Option(False, "--compact", help="Compact output mode"),
|
||||
):
|
||||
"""
|
||||
Start terminal-native interactive chat mode.
|
||||
@@ -112,7 +151,6 @@ def chat_main(
|
||||
praisonai chat "Summarize this" --file README.md
|
||||
praisonai chat "What is 2+2?" --profile
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# Set workspace if provided
|
||||
@@ -135,55 +173,38 @@ def chat_main(
|
||||
typer.echo(" Use: praisonai chat \"your prompt\" --profile", err=True)
|
||||
|
||||
# Parse memory flag: --no-memory takes precedence, then --memory value
|
||||
memory_value = _parse_memory_flag(memory, no_memory)
|
||||
# TODO: Pass memory_value to TUI when memory support is added
|
||||
_parse_memory_flag(memory, no_memory)
|
||||
|
||||
# Try InteractiveCore first (preferred terminal-native implementation)
|
||||
try:
|
||||
from praisonai.cli.interactive import InteractiveCore, InteractiveConfig
|
||||
from praisonai.cli.interactive.frontends import RichFrontend
|
||||
|
||||
config = InteractiveConfig(
|
||||
model=model,
|
||||
session_id=session_id,
|
||||
continue_session=continue_session,
|
||||
workspace=workspace or None,
|
||||
verbose=verbose,
|
||||
memory=memory_value,
|
||||
files=list(file) if file else [],
|
||||
autonomy=autonomy,
|
||||
)
|
||||
|
||||
core = InteractiveCore(config=config)
|
||||
|
||||
if prompt:
|
||||
# Single prompt mode
|
||||
async def run_prompt():
|
||||
if continue_session:
|
||||
core.continue_session()
|
||||
response = await core.prompt(prompt)
|
||||
print(response)
|
||||
|
||||
asyncio.run(run_prompt())
|
||||
else:
|
||||
# Interactive REPL mode
|
||||
frontend = RichFrontend(core=core, config=config)
|
||||
asyncio.run(frontend.run())
|
||||
|
||||
except ImportError:
|
||||
# Fallback to legacy terminal-native interactive mode
|
||||
_run_legacy_terminal_chat(
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
verbose=verbose,
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
continue_session=continue_session,
|
||||
workspace=workspace,
|
||||
no_acp=no_acp,
|
||||
no_lsp=no_lsp,
|
||||
)
|
||||
# Set approval mode based on --safe flag
|
||||
import os
|
||||
if safe_mode:
|
||||
os.environ["PRAISON_APPROVAL_MODE"] = "prompt"
|
||||
else:
|
||||
os.environ["PRAISON_APPROVAL_MODE"] = "auto"
|
||||
|
||||
# Use the async TUI (non-blocking, scrollable output)
|
||||
from praisonai.cli.interactive.async_tui import AsyncTUI, AsyncTUIConfig
|
||||
|
||||
tui_config = AsyncTUIConfig(
|
||||
model=model or "gpt-4o-mini",
|
||||
show_logo=not compact,
|
||||
show_status_bar=not compact,
|
||||
session_id=session_id,
|
||||
workspace=workspace,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
tui = AsyncTUI(config=tui_config)
|
||||
|
||||
if prompt:
|
||||
# Single prompt mode - direct response, no streaming
|
||||
response = tui.run_single(prompt)
|
||||
if response:
|
||||
print(response)
|
||||
else:
|
||||
# Interactive split-pane TUI mode
|
||||
tui.run()
|
||||
|
||||
|
||||
def _run_profiled_chat(
|
||||
@@ -221,7 +242,6 @@ def _run_profiled_chat(
|
||||
"name": "ChatAgent",
|
||||
"role": "Assistant",
|
||||
"goal": "Help the user",
|
||||
"verbose": verbose,
|
||||
}
|
||||
if model:
|
||||
agent_config["llm"] = model
|
||||
|
||||
@@ -23,6 +23,7 @@ def code_main(
|
||||
file: Optional[List[str]] = typer.Option(None, "--file", "-f", help="Attach file(s) to context"),
|
||||
no_acp: bool = typer.Option(False, "--no-acp", help="Disable ACP tools (file operations)"),
|
||||
no_lsp: bool = typer.Option(False, "--no-lsp", help="Disable LSP tools (code intelligence)"),
|
||||
safe_mode: bool = typer.Option(False, "--safe", help="Safe mode: require approval for file writes and commands"),
|
||||
session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID to resume"),
|
||||
continue_session: bool = typer.Option(False, "--continue", "-c", help="Continue last session"),
|
||||
autonomy: bool = typer.Option(True, "--autonomy/--no-autonomy", help="Enable agent autonomy for complex tasks"),
|
||||
@@ -54,6 +55,12 @@ def code_main(
|
||||
# Enable code-specific environment
|
||||
os.environ["PRAISONAI_CODE_MODE"] = "true"
|
||||
|
||||
# Set approval mode based on --safe flag
|
||||
if safe_mode:
|
||||
os.environ["PRAISON_APPROVAL_MODE"] = "prompt"
|
||||
else:
|
||||
os.environ["PRAISON_APPROVAL_MODE"] = "auto"
|
||||
|
||||
# Handle profiling for single prompt mode
|
||||
if prompt and (profile or profile_deep):
|
||||
_run_profiled_code(
|
||||
|
||||
@@ -224,3 +224,191 @@ def context_export(
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: Context module not available: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Artifact Commands (Dynamic Context Discovery)
|
||||
# =============================================================================
|
||||
|
||||
artifacts_app = typer.Typer(help="Artifact management for dynamic context discovery")
|
||||
app.add_typer(artifacts_app, name="artifacts")
|
||||
|
||||
|
||||
@artifacts_app.command("list")
|
||||
def artifacts_list(
|
||||
run_id: str = typer.Option(None, "--run-id", "-r", help="Filter by run ID"),
|
||||
agent_id: str = typer.Option(None, "--agent-id", "-a", help="Filter by agent ID"),
|
||||
tool_name: str = typer.Option(None, "--tool", "-t", help="Filter by tool name"),
|
||||
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
||||
):
|
||||
"""List artifacts."""
|
||||
try:
|
||||
from praisonai.context import FileSystemArtifactStore
|
||||
import json as json_module
|
||||
|
||||
store = FileSystemArtifactStore()
|
||||
artifacts = store.list_artifacts(
|
||||
run_id=run_id,
|
||||
agent_id=agent_id,
|
||||
tool_name=tool_name,
|
||||
)
|
||||
|
||||
if json_output:
|
||||
data = [a.to_dict() for a in artifacts]
|
||||
print(json_module.dumps(data, indent=2))
|
||||
else:
|
||||
if not artifacts:
|
||||
typer.echo("No artifacts found.")
|
||||
return
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title=f"Artifacts ({len(artifacts)} found)")
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Tool", style="green")
|
||||
table.add_column("Size", justify="right")
|
||||
table.add_column("Summary")
|
||||
table.add_column("Path", style="dim")
|
||||
|
||||
for ref in artifacts:
|
||||
size_str = ref._format_size(ref.size_bytes)
|
||||
table.add_row(
|
||||
ref.artifact_id,
|
||||
ref.tool_name or "-",
|
||||
size_str,
|
||||
ref.summary[:40] + "..." if len(ref.summary) > 40 else ref.summary,
|
||||
ref.path,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@artifacts_app.command("show")
|
||||
def artifacts_show(
|
||||
artifact_path: str = typer.Argument(..., help="Path to artifact"),
|
||||
lines: int = typer.Option(None, "--lines", "-n", help="Number of lines to show"),
|
||||
):
|
||||
"""Show artifact content."""
|
||||
try:
|
||||
from praisonai.context import FileSystemArtifactStore
|
||||
from praisonaiagents.context.artifacts import ArtifactRef
|
||||
|
||||
store = FileSystemArtifactStore()
|
||||
ref = ArtifactRef(path=artifact_path, summary="", size_bytes=0)
|
||||
|
||||
if lines:
|
||||
content = store.head(ref, lines=lines)
|
||||
else:
|
||||
content = store.load(ref)
|
||||
if not isinstance(content, str):
|
||||
import json
|
||||
content = json.dumps(content, indent=2)
|
||||
|
||||
typer.echo(content)
|
||||
|
||||
except FileNotFoundError:
|
||||
typer.echo(f"Artifact not found: {artifact_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@artifacts_app.command("tail")
|
||||
def artifacts_tail(
|
||||
artifact_path: str = typer.Argument(..., help="Path to artifact"),
|
||||
lines: int = typer.Option(50, "--lines", "-n", help="Number of lines"),
|
||||
):
|
||||
"""Get last N lines of artifact."""
|
||||
try:
|
||||
from praisonai.context import FileSystemArtifactStore
|
||||
from praisonaiagents.context.artifacts import ArtifactRef
|
||||
|
||||
store = FileSystemArtifactStore()
|
||||
ref = ArtifactRef(path=artifact_path, summary="", size_bytes=0)
|
||||
content = store.tail(ref, lines=lines)
|
||||
typer.echo(content)
|
||||
|
||||
except FileNotFoundError:
|
||||
typer.echo(f"Artifact not found: {artifact_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@artifacts_app.command("grep")
|
||||
def artifacts_grep(
|
||||
artifact_path: str = typer.Argument(..., help="Path to artifact"),
|
||||
pattern: str = typer.Argument(..., help="Search pattern (regex)"),
|
||||
context_lines: int = typer.Option(2, "--context", "-C", help="Context lines"),
|
||||
max_matches: int = typer.Option(50, "--max", "-m", help="Max matches"),
|
||||
):
|
||||
"""Search for pattern in artifact."""
|
||||
try:
|
||||
from praisonai.context import FileSystemArtifactStore
|
||||
from praisonaiagents.context.artifacts import ArtifactRef
|
||||
|
||||
store = FileSystemArtifactStore()
|
||||
ref = ArtifactRef(path=artifact_path, summary="", size_bytes=0)
|
||||
matches = store.grep(ref, pattern=pattern, context_lines=context_lines, max_matches=max_matches)
|
||||
|
||||
if not matches:
|
||||
typer.echo(f"No matches found for: {pattern}")
|
||||
return
|
||||
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
||||
console.print(f"[bold]Found {len(matches)} matches:[/bold]")
|
||||
for match in matches:
|
||||
console.print(f"\n[cyan]--- Line {match.line_number} ---[/cyan]")
|
||||
for ctx in match.context_before:
|
||||
console.print(f" {ctx}", style="dim")
|
||||
console.print(f"[green]> {match.line_content}[/green]")
|
||||
for ctx in match.context_after:
|
||||
console.print(f" {ctx}", style="dim")
|
||||
|
||||
except FileNotFoundError:
|
||||
typer.echo(f"Artifact not found: {artifact_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
except ValueError as e:
|
||||
typer.echo(f"Invalid pattern: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@artifacts_app.command("export")
|
||||
def artifacts_export(
|
||||
artifact_path: str = typer.Argument(..., help="Path to artifact"),
|
||||
output: str = typer.Option(None, "--output", "-o", help="Output file path"),
|
||||
):
|
||||
"""Export artifact to file."""
|
||||
try:
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
src_path = Path(artifact_path)
|
||||
if not src_path.exists():
|
||||
typer.echo(f"Artifact not found: {artifact_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output:
|
||||
dst_path = Path(output)
|
||||
else:
|
||||
dst_path = Path.cwd() / src_path.name
|
||||
|
||||
shutil.copy2(src_path, dst_path)
|
||||
typer.echo(f"Exported to: {dst_path}")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -813,3 +813,47 @@ def docs_report(
|
||||
|
||||
typer.echo(output)
|
||||
raise typer.Exit(exit_code)
|
||||
|
||||
|
||||
@app.command("api-md")
|
||||
def docs_api_md(
|
||||
write: bool = typer.Option(
|
||||
True,
|
||||
"--write", "-w",
|
||||
help="Write api.md file (default)",
|
||||
),
|
||||
check: bool = typer.Option(
|
||||
False,
|
||||
"--check", "-c",
|
||||
help="Check if api.md is up to date (exit 1 if not)",
|
||||
),
|
||||
stdout: bool = typer.Option(
|
||||
False,
|
||||
"--stdout", "-s",
|
||||
help="Print to stdout instead of writing file",
|
||||
),
|
||||
output: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--output", "-o",
|
||||
help="Output file path (default: repo_root/api.md)",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Generate or check the api.md API reference file.
|
||||
|
||||
This command generates a comprehensive API reference document
|
||||
covering all public exports from praisonaiagents, praisonai, CLI, and TypeScript.
|
||||
|
||||
Examples:
|
||||
praisonai docs api-md # Generate api.md
|
||||
praisonai docs api-md --check # Check if api.md is up to date
|
||||
praisonai docs api-md --stdout # Print to stdout
|
||||
"""
|
||||
from praisonai._dev.api_md import generate_api_md
|
||||
|
||||
exit_code = generate_api_md(
|
||||
output_path=output,
|
||||
check=check,
|
||||
stdout=stdout
|
||||
)
|
||||
raise typer.Exit(exit_code)
|
||||
@@ -203,6 +203,164 @@ def mcp_test(
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("sync")
|
||||
def mcp_sync(
|
||||
server: Optional[str] = typer.Argument(None, help="Server name (None for all)"),
|
||||
):
|
||||
"""Sync tool schemas from MCP servers to local index."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from praisonai.mcp_server.tool_index import MCPToolIndex
|
||||
|
||||
index = MCPToolIndex()
|
||||
loader = get_config_loader()
|
||||
config = loader.load()
|
||||
|
||||
servers_to_sync = []
|
||||
if server:
|
||||
if server not in config.mcp.servers:
|
||||
output.print_error(f"Server not found: {server}")
|
||||
raise typer.Exit(1)
|
||||
servers_to_sync = [server]
|
||||
else:
|
||||
servers_to_sync = list(config.mcp.servers.keys())
|
||||
|
||||
if not servers_to_sync:
|
||||
output.print_info("No MCP servers configured")
|
||||
return
|
||||
|
||||
total_tools = 0
|
||||
for srv in servers_to_sync:
|
||||
output.print_info(f"Syncing {srv}...")
|
||||
# Note: This would need actual MCP connection to fetch tools
|
||||
# For now, we just create the index structure
|
||||
count = index.sync(srv, tools=[])
|
||||
total_tools += count
|
||||
output.print(f" Synced {count} tools")
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"synced_servers": servers_to_sync, "total_tools": total_tools})
|
||||
else:
|
||||
output.print_success(f"Synced {len(servers_to_sync)} servers, {total_tools} tools total")
|
||||
|
||||
except ImportError as e:
|
||||
output.print_error(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("tools")
|
||||
def mcp_tools(
|
||||
server: Optional[str] = typer.Argument(None, help="Server name (None for all)"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List indexed MCP tools."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from praisonai.mcp_server.tool_index import MCPToolIndex
|
||||
|
||||
index = MCPToolIndex()
|
||||
|
||||
if server:
|
||||
tools = index.list_tools(server)
|
||||
if not tools:
|
||||
output.print_info(f"No tools indexed for server: {server}")
|
||||
output.print("\nRun 'praisonai mcp sync' to index tools")
|
||||
return
|
||||
else:
|
||||
tools = index.get_all_tools()
|
||||
if not tools:
|
||||
output.print_info("No tools indexed")
|
||||
output.print("\nRun 'praisonai mcp sync' to index tools")
|
||||
return
|
||||
|
||||
if output.is_json_mode or json_output:
|
||||
output.print_json({"tools": [t.to_dict() for t in tools]})
|
||||
else:
|
||||
headers = ["Server", "Tool", "Description"]
|
||||
rows = [[t.server, t.name, t.hint[:50] + "..." if len(t.hint) > 50 else t.hint] for t in tools]
|
||||
output.print_table(headers, rows, title=f"MCP Tools ({len(tools)} total)")
|
||||
|
||||
except ImportError as e:
|
||||
output.print_error(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("describe")
|
||||
def mcp_describe(
|
||||
server: str = typer.Argument(..., help="Server name"),
|
||||
tool: str = typer.Argument(..., help="Tool name"),
|
||||
):
|
||||
"""Show full schema for an MCP tool."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from praisonai.mcp_server.tool_index import MCPToolIndex
|
||||
import json
|
||||
|
||||
index = MCPToolIndex()
|
||||
schema = index.describe(server, tool)
|
||||
|
||||
if not schema:
|
||||
output.print_error(f"Tool not found: {server}/{tool}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(schema)
|
||||
else:
|
||||
output.print(json.dumps(schema, indent=2))
|
||||
|
||||
except ImportError as e:
|
||||
output.print_error(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def mcp_status(
|
||||
server: Optional[str] = typer.Argument(None, help="Server name (None for all)"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show status of MCP servers."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from praisonai.mcp_server.tool_index import MCPToolIndex
|
||||
from datetime import datetime
|
||||
|
||||
index = MCPToolIndex()
|
||||
|
||||
servers = [server] if server else index.list_servers()
|
||||
|
||||
if not servers:
|
||||
output.print_info("No servers indexed")
|
||||
return
|
||||
|
||||
statuses = []
|
||||
for srv in servers:
|
||||
status = index.get_status(srv)
|
||||
if status:
|
||||
statuses.append(status)
|
||||
|
||||
if output.is_json_mode or json_output:
|
||||
output.print_json({"statuses": [s.to_dict() for s in statuses]})
|
||||
else:
|
||||
headers = ["Server", "Status", "Tools", "Last Sync"]
|
||||
rows = []
|
||||
for s in statuses:
|
||||
status_str = "✓ Available" if s.available else "✗ Unavailable"
|
||||
if s.auth_required:
|
||||
status_str += " (auth required)"
|
||||
last_sync = datetime.fromtimestamp(s.last_sync).strftime("%Y-%m-%d %H:%M") if s.last_sync else "Never"
|
||||
rows.append([s.server, status_str, str(s.tool_count), last_sync])
|
||||
|
||||
output.print_table(headers, rows, title="MCP Server Status")
|
||||
|
||||
except ImportError as e:
|
||||
output.print_error(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def mcp_callback(ctx: typer.Context):
|
||||
"""Show MCP help or list servers."""
|
||||
|
||||
@@ -107,3 +107,192 @@ def memory_clear(
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
# Learn subcommand group
|
||||
learn_app = typer.Typer(help="Continuous learning management")
|
||||
app.add_typer(learn_app, name="learn")
|
||||
|
||||
|
||||
@learn_app.command("status")
|
||||
def learn_status(
|
||||
user_id: str = typer.Option("default", "--user-id", "-u", help="User ID"),
|
||||
scope: str = typer.Option("private", "--scope", "-s", help="Scope (private/shared)"),
|
||||
):
|
||||
"""Show learning status and statistics."""
|
||||
from praisonaiagents.memory.learn import LearnManager
|
||||
from praisonaiagents.config.feature_configs import LearnConfig
|
||||
|
||||
config = LearnConfig()
|
||||
manager = LearnManager(config=config, user_id=user_id)
|
||||
stats = manager.get_stats()
|
||||
|
||||
typer.echo(f"\n📚 Learning Status for user: {user_id}")
|
||||
typer.echo(f" Scope: {scope}")
|
||||
typer.echo("-" * 40)
|
||||
|
||||
if not stats:
|
||||
typer.echo("No learning stores enabled.")
|
||||
return
|
||||
|
||||
total = 0
|
||||
for store_name, count in stats.items():
|
||||
typer.echo(f" {store_name}: {count} entries")
|
||||
total += count
|
||||
|
||||
typer.echo("-" * 40)
|
||||
typer.echo(f" Total: {total} entries")
|
||||
|
||||
|
||||
@learn_app.command("show")
|
||||
def learn_show(
|
||||
store: str = typer.Argument("all", help="Store to show (persona/insights/threads/patterns/decisions/feedback/improvements/all)"),
|
||||
user_id: str = typer.Option("default", "--user-id", "-u", help="User ID"),
|
||||
limit: int = typer.Option(10, "--limit", "-l", help="Number of entries to show"),
|
||||
):
|
||||
"""Show learned entries."""
|
||||
from praisonaiagents.memory.learn import LearnManager
|
||||
from praisonaiagents.config.feature_configs import LearnConfig
|
||||
|
||||
config = LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
thread=True,
|
||||
patterns=True,
|
||||
decisions=True,
|
||||
feedback=True,
|
||||
improvements=True,
|
||||
)
|
||||
manager = LearnManager(config=config, user_id=user_id)
|
||||
|
||||
if store == "all":
|
||||
context = manager.get_learning_context(limit_per_store=limit)
|
||||
for store_name, entries in context.items():
|
||||
if entries:
|
||||
typer.echo(f"\n📁 {store_name.upper()}")
|
||||
typer.echo("-" * 40)
|
||||
for entry in entries:
|
||||
typer.echo(f" [{entry['id'][:20]}...] {entry['content'][:60]}...")
|
||||
else:
|
||||
context = manager.get_learning_context(limit_per_store=limit)
|
||||
if store in context:
|
||||
entries = context[store]
|
||||
typer.echo(f"\n📁 {store.upper()}")
|
||||
typer.echo("-" * 40)
|
||||
for entry in entries:
|
||||
typer.echo(f" [{entry['id'][:20]}...] {entry['content']}")
|
||||
else:
|
||||
typer.echo(f"Store '{store}' not found or empty.")
|
||||
|
||||
|
||||
@learn_app.command("add")
|
||||
def learn_add(
|
||||
content: str = typer.Argument(..., help="Content to learn"),
|
||||
store: str = typer.Option("insights", "--store", "-s", help="Store type (persona/insights/patterns)"),
|
||||
user_id: str = typer.Option("default", "--user-id", "-u", help="User ID"),
|
||||
):
|
||||
"""Add a learning entry."""
|
||||
from praisonaiagents.memory.learn import LearnManager
|
||||
from praisonaiagents.config.feature_configs import LearnConfig
|
||||
|
||||
config = LearnConfig(
|
||||
persona=(store == "persona"),
|
||||
insights=(store == "insights"),
|
||||
patterns=(store == "patterns"),
|
||||
thread=(store == "threads"),
|
||||
decisions=(store == "decisions"),
|
||||
feedback=(store == "feedback"),
|
||||
improvements=(store == "improvements"),
|
||||
)
|
||||
manager = LearnManager(config=config, user_id=user_id)
|
||||
|
||||
entry = None
|
||||
if store == "persona":
|
||||
entry = manager.capture_persona(content)
|
||||
elif store == "insights":
|
||||
entry = manager.capture_insight(content)
|
||||
elif store == "patterns":
|
||||
entry = manager.capture_pattern(content)
|
||||
elif store == "decisions":
|
||||
entry = manager.capture_decision(content)
|
||||
elif store == "feedback":
|
||||
entry = manager.capture_feedback(content)
|
||||
elif store == "improvements":
|
||||
entry = manager.capture_improvement(content)
|
||||
else:
|
||||
typer.echo(f"Unknown store: {store}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if entry:
|
||||
typer.echo(f"✅ Added to {store}: {entry.id}")
|
||||
else:
|
||||
typer.echo(f"❌ Failed to add to {store}")
|
||||
|
||||
|
||||
@learn_app.command("search")
|
||||
def learn_search(
|
||||
query: str = typer.Argument(..., help="Search query"),
|
||||
user_id: str = typer.Option("default", "--user-id", "-u", help="User ID"),
|
||||
limit: int = typer.Option(10, "--limit", "-l", help="Max results per store"),
|
||||
):
|
||||
"""Search across all learning stores."""
|
||||
from praisonaiagents.memory.learn import LearnManager
|
||||
from praisonaiagents.config.feature_configs import LearnConfig
|
||||
|
||||
config = LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
thread=True,
|
||||
patterns=True,
|
||||
decisions=True,
|
||||
feedback=True,
|
||||
improvements=True,
|
||||
)
|
||||
manager = LearnManager(config=config, user_id=user_id)
|
||||
|
||||
results = manager.search(query, limit=limit)
|
||||
|
||||
if not results:
|
||||
typer.echo(f"No results found for: {query}")
|
||||
return
|
||||
|
||||
typer.echo(f"\n🔍 Search results for: {query}")
|
||||
for store_name, entries in results.items():
|
||||
typer.echo(f"\n📁 {store_name.upper()}")
|
||||
typer.echo("-" * 40)
|
||||
for entry in entries:
|
||||
typer.echo(f" {entry['content'][:80]}...")
|
||||
|
||||
|
||||
@learn_app.command("clear")
|
||||
def learn_clear(
|
||||
store: str = typer.Argument("all", help="Store to clear (or 'all')"),
|
||||
user_id: str = typer.Option("default", "--user-id", "-u", help="User ID"),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
||||
):
|
||||
"""Clear learning entries."""
|
||||
from praisonaiagents.memory.learn import LearnManager
|
||||
from praisonaiagents.config.feature_configs import LearnConfig
|
||||
|
||||
if not force:
|
||||
confirm = typer.confirm(f"Clear {store} learning data for user {user_id}?")
|
||||
if not confirm:
|
||||
typer.echo("Cancelled.")
|
||||
return
|
||||
|
||||
config = LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
thread=True,
|
||||
patterns=True,
|
||||
decisions=True,
|
||||
feedback=True,
|
||||
improvements=True,
|
||||
)
|
||||
manager = LearnManager(config=config, user_id=user_id)
|
||||
|
||||
cleared = manager.clear_all()
|
||||
|
||||
typer.echo("\n🗑️ Cleared learning data:")
|
||||
for store_name, count in cleared.items():
|
||||
typer.echo(f" {store_name}: {count} entries")
|
||||
@@ -22,13 +22,14 @@ def run_main(
|
||||
framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Framework: praisonai, crewai, autogen"),
|
||||
interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
stream: bool = typer.Option(True, "--stream/--no-stream", help="Stream output"),
|
||||
stream: bool = typer.Option(False, "--stream/--no-stream", help="Stream output (default: off for production use)"),
|
||||
trace: bool = typer.Option(False, "--trace", help="Enable tracing"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
|
||||
max_tokens: int = typer.Option(16000, "--max-tokens", help="Maximum output tokens"),
|
||||
profile: bool = typer.Option(False, "--profile", help="Enable CLI profiling (timing breakdown)"),
|
||||
profile_deep: bool = typer.Option(False, "--profile-deep", help="Enable deep profiling (cProfile stats, higher overhead)"),
|
||||
output_mode: Optional[str] = typer.Option(None, "--output", "-o", help="Output mode: silent (default), actions, verbose, json, stream"),
|
||||
):
|
||||
"""
|
||||
Run agents from a file or prompt.
|
||||
@@ -108,6 +109,7 @@ def run_main(
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
output_mode=output_mode,
|
||||
)
|
||||
else:
|
||||
# Run as prompt
|
||||
@@ -120,6 +122,7 @@ def run_main(
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
output_mode=output_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -134,6 +137,7 @@ def _run_from_file(
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
output_mode: Optional[str] = None,
|
||||
):
|
||||
"""Run agents from a YAML file."""
|
||||
output = get_output_controller()
|
||||
@@ -178,12 +182,37 @@ def _run_prompt(
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
output_mode: Optional[str] = None,
|
||||
):
|
||||
"""Run a direct prompt."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
# Use existing handle_direct_prompt
|
||||
# If output_mode is "actions", use direct Agent with actions preset
|
||||
if output_mode == "actions":
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent_config = {
|
||||
"name": "RunAgent",
|
||||
"role": "Assistant",
|
||||
"goal": "Complete the task",
|
||||
"output": "actions", # Use actions preset
|
||||
}
|
||||
if model:
|
||||
agent_config["llm"] = model
|
||||
|
||||
agent = Agent(**agent_config)
|
||||
result = agent.start(prompt)
|
||||
|
||||
output.emit_result(
|
||||
message="Prompt completed",
|
||||
data={"result": str(result) if result else None}
|
||||
)
|
||||
|
||||
# Don't print result again - actions mode already shows output
|
||||
return
|
||||
|
||||
# Use existing handle_direct_prompt for other modes
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
|
||||
@@ -97,3 +97,121 @@ def skills_install(
|
||||
pass
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@app.command("search")
|
||||
def skills_search(
|
||||
query: str = typer.Argument(..., help="Search query"),
|
||||
skill_dirs: str = typer.Option(None, "--dirs", "-d", help="Comma-separated skill directories"),
|
||||
):
|
||||
"""Search skills by name, description, or content."""
|
||||
try:
|
||||
from praisonaiagents.skills import discover_skills
|
||||
import re
|
||||
|
||||
dirs = skill_dirs.split(",") if skill_dirs else None
|
||||
skills = discover_skills(dirs, include_defaults=True)
|
||||
|
||||
pattern = re.compile(query, re.IGNORECASE)
|
||||
matches = []
|
||||
|
||||
for skill in skills:
|
||||
# Search in name and description
|
||||
if pattern.search(skill.name) or pattern.search(skill.description):
|
||||
matches.append(skill)
|
||||
continue
|
||||
|
||||
# Search in SKILL.md content if path available
|
||||
if skill.path:
|
||||
skill_md = skill.path / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
content = skill_md.read_text()
|
||||
if pattern.search(content):
|
||||
matches.append(skill)
|
||||
|
||||
if not matches:
|
||||
typer.echo(f"No skills found matching: {query}")
|
||||
return
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title=f"Skills matching '{query}' ({len(matches)} found)")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Description")
|
||||
table.add_column("Path", style="dim")
|
||||
|
||||
for skill in matches:
|
||||
table.add_row(
|
||||
skill.name,
|
||||
skill.description[:60] + "..." if len(skill.description) > 60 else skill.description,
|
||||
str(skill.path) if skill.path else "-",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("info")
|
||||
def skills_info(
|
||||
name: str = typer.Argument(..., help="Skill name or path"),
|
||||
):
|
||||
"""Show detailed information about a skill."""
|
||||
try:
|
||||
from praisonaiagents.skills import discover_skills, SkillLoader
|
||||
from pathlib import Path
|
||||
|
||||
# Try as path first
|
||||
path = Path(name).expanduser()
|
||||
if path.exists() and path.is_dir():
|
||||
loader = SkillLoader()
|
||||
skill = loader.load_metadata(str(path))
|
||||
if skill:
|
||||
loader.activate(skill)
|
||||
else:
|
||||
# Search by name
|
||||
skills = discover_skills(include_defaults=True)
|
||||
skill = None
|
||||
for s in skills:
|
||||
if s.name == name:
|
||||
loader = SkillLoader()
|
||||
skill = loader.load_metadata(str(s.path))
|
||||
if skill:
|
||||
loader.activate(skill)
|
||||
break
|
||||
|
||||
if not skill:
|
||||
typer.echo(f"Skill not found: {name}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.markdown import Markdown
|
||||
|
||||
console = Console()
|
||||
|
||||
# Show metadata
|
||||
console.print(f"\n[bold cyan]{skill.properties.name}[/bold cyan]")
|
||||
console.print(f"[dim]{skill.properties.description}[/dim]\n")
|
||||
|
||||
if skill.properties.license:
|
||||
console.print(f"License: {skill.properties.license}")
|
||||
if skill.properties.compatibility:
|
||||
console.print(f"Compatibility: {skill.properties.compatibility}")
|
||||
if skill.properties.allowed_tools:
|
||||
console.print(f"Allowed Tools: {skill.properties.allowed_tools}")
|
||||
if skill.properties.metadata:
|
||||
console.print(f"Metadata: {skill.properties.metadata}")
|
||||
|
||||
# Show instructions if loaded
|
||||
if skill.instructions:
|
||||
console.print("\n[bold]Instructions:[/bold]")
|
||||
console.print(Panel(Markdown(skill.instructions[:2000])))
|
||||
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -0,0 +1,694 @@
|
||||
"""
|
||||
CLI commands for the FDEP standardisation system.
|
||||
|
||||
Commands:
|
||||
- praisonai standardise check - Check for standardisation issues
|
||||
- praisonai standardise report - Generate detailed report
|
||||
- praisonai standardise fix - Fix issues (with --apply)
|
||||
- praisonai standardise init - Initialise a new feature
|
||||
- praisonai standardise ai - AI-powered generation
|
||||
- praisonai standardise checkpoint - Create undo checkpoint
|
||||
- praisonai standardise undo - Undo to previous checkpoint
|
||||
- praisonai standardise redo - Redo after undo
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _lazy_import_engine():
|
||||
"""Lazy import to avoid heavy imports at CLI startup."""
|
||||
from praisonai.standardise import StandardiseEngine, StandardiseConfig
|
||||
return StandardiseEngine, StandardiseConfig
|
||||
|
||||
|
||||
def standardise_command(args=None):
|
||||
"""Main entry point for standardise command."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="praisonai standardise",
|
||||
description="Standardise documentation and examples (FDEP)",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommands")
|
||||
|
||||
# check subcommand
|
||||
check_parser = subparsers.add_parser(
|
||||
"check",
|
||||
help="Check for standardisation issues (default)",
|
||||
)
|
||||
_add_common_args(check_parser)
|
||||
|
||||
# report subcommand
|
||||
report_parser = subparsers.add_parser(
|
||||
"report",
|
||||
help="Generate detailed report",
|
||||
)
|
||||
_add_common_args(report_parser)
|
||||
report_parser.add_argument(
|
||||
"--format", "-f",
|
||||
choices=["text", "json", "markdown"],
|
||||
default="text",
|
||||
help="Report format (default: text)",
|
||||
)
|
||||
report_parser.add_argument(
|
||||
"--output", "-o",
|
||||
type=str,
|
||||
help="Output file path (default: stdout)",
|
||||
)
|
||||
|
||||
# fix subcommand
|
||||
fix_parser = subparsers.add_parser(
|
||||
"fix",
|
||||
help="Fix standardisation issues",
|
||||
)
|
||||
_add_common_args(fix_parser)
|
||||
fix_parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Actually apply changes (default: dry-run)",
|
||||
)
|
||||
fix_parser.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Don't create backups before changes",
|
||||
)
|
||||
|
||||
# init subcommand
|
||||
init_parser = subparsers.add_parser(
|
||||
"init",
|
||||
help="Initialise a new feature with all required artifacts",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"feature",
|
||||
type=str,
|
||||
help="Feature slug to initialise",
|
||||
)
|
||||
_add_common_args(init_parser)
|
||||
init_parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Actually create files (default: dry-run)",
|
||||
)
|
||||
|
||||
# ai subcommand
|
||||
ai_parser = subparsers.add_parser(
|
||||
"ai",
|
||||
help="AI-powered generation of docs/examples",
|
||||
)
|
||||
ai_parser.add_argument(
|
||||
"feature",
|
||||
type=str,
|
||||
help="Feature slug to generate content for",
|
||||
)
|
||||
ai_parser.add_argument(
|
||||
"--type", "-t",
|
||||
choices=["docs", "examples", "all"],
|
||||
default="all",
|
||||
help="Type of content to generate",
|
||||
)
|
||||
ai_parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Actually create files (default: dry-run preview)",
|
||||
)
|
||||
ai_parser.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="Verify generated content with AI",
|
||||
)
|
||||
ai_parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
default="gpt-4o-mini",
|
||||
help="LLM model to use (default: gpt-4o-mini)",
|
||||
)
|
||||
_add_common_args(ai_parser)
|
||||
|
||||
# checkpoint subcommand
|
||||
checkpoint_parser = subparsers.add_parser(
|
||||
"checkpoint",
|
||||
help="Create an undo checkpoint",
|
||||
)
|
||||
checkpoint_parser.add_argument(
|
||||
"--message", "-m",
|
||||
type=str,
|
||||
help="Checkpoint message",
|
||||
)
|
||||
checkpoint_parser.add_argument(
|
||||
"--path", "-p",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Repository path",
|
||||
)
|
||||
|
||||
# undo subcommand
|
||||
undo_parser = subparsers.add_parser(
|
||||
"undo",
|
||||
help="Undo to a previous checkpoint",
|
||||
)
|
||||
undo_parser.add_argument(
|
||||
"--checkpoint",
|
||||
type=str,
|
||||
help="Specific checkpoint ID to restore",
|
||||
)
|
||||
undo_parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available checkpoints",
|
||||
)
|
||||
undo_parser.add_argument(
|
||||
"--path", "-p",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Repository path",
|
||||
)
|
||||
|
||||
# redo subcommand
|
||||
redo_parser = subparsers.add_parser(
|
||||
"redo",
|
||||
help="Redo after an undo",
|
||||
)
|
||||
redo_parser.add_argument(
|
||||
"--path", "-p",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Repository path",
|
||||
)
|
||||
|
||||
# Parse args
|
||||
parsed = parser.parse_args(args)
|
||||
|
||||
# Default to check if no subcommand
|
||||
if not parsed.subcommand:
|
||||
parsed.subcommand = "check"
|
||||
parsed = parser.parse_args(["check"] + (args or []))
|
||||
|
||||
# Execute subcommand
|
||||
if parsed.subcommand == "check":
|
||||
return _run_check(parsed)
|
||||
elif parsed.subcommand == "report":
|
||||
return _run_report(parsed)
|
||||
elif parsed.subcommand == "fix":
|
||||
return _run_fix(parsed)
|
||||
elif parsed.subcommand == "init":
|
||||
return _run_init(parsed)
|
||||
elif parsed.subcommand == "ai":
|
||||
return _run_ai(parsed)
|
||||
elif parsed.subcommand == "checkpoint":
|
||||
return _run_checkpoint(parsed)
|
||||
elif parsed.subcommand == "undo":
|
||||
return _run_undo(parsed)
|
||||
elif parsed.subcommand == "redo":
|
||||
return _run_redo(parsed)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _add_common_args(parser):
|
||||
"""Add common arguments to a parser."""
|
||||
parser.add_argument(
|
||||
"--path", "-p",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Project root path (default: current directory)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature",
|
||||
type=str,
|
||||
help="Specific feature slug to check",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scope",
|
||||
choices=["all", "docs", "examples", "sdk", "cli"],
|
||||
default="all",
|
||||
help="Scope of check (default: all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ci",
|
||||
action="store_true",
|
||||
help="CI mode (no prompts, machine-readable output)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Don't make changes (default: true)",
|
||||
)
|
||||
|
||||
|
||||
def _create_config(parsed):
|
||||
"""Create config from parsed arguments."""
|
||||
_, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = StandardiseConfig(
|
||||
project_root=Path(parsed.path).resolve() if hasattr(parsed, "path") else None,
|
||||
dry_run=getattr(parsed, "dry_run", True) and not getattr(parsed, "apply", False),
|
||||
backup=not getattr(parsed, "no_backup", False),
|
||||
scope=getattr(parsed, "scope", "all"),
|
||||
feature_filter=getattr(parsed, "feature", None),
|
||||
ci_mode=getattr(parsed, "ci", False),
|
||||
report_format=getattr(parsed, "format", "text"),
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _run_check(parsed) -> int:
|
||||
"""Run the check subcommand."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = _create_config(parsed)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
report = engine.check()
|
||||
|
||||
# Print report
|
||||
output = engine.reports.generate(report, "text")
|
||||
print(output)
|
||||
|
||||
# Return exit code
|
||||
if config.ci_mode:
|
||||
return engine.get_exit_code(report)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_report(parsed) -> int:
|
||||
"""Run the report subcommand."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = _create_config(parsed)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
report = engine.check()
|
||||
output = engine.reports.generate(report, parsed.format)
|
||||
|
||||
if parsed.output:
|
||||
Path(parsed.output).write_text(output, encoding="utf-8")
|
||||
print(f"Report saved to {parsed.output}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
if config.ci_mode:
|
||||
return engine.get_exit_code(report)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_fix(parsed) -> int:
|
||||
"""Run the fix subcommand."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = _create_config(parsed)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
actions = engine.fix(
|
||||
feature=parsed.feature,
|
||||
apply=parsed.apply,
|
||||
)
|
||||
|
||||
if not actions["planned"]:
|
||||
print("No fixes needed.")
|
||||
return 0
|
||||
|
||||
print(f"Planned actions ({len(actions['planned'])}):")
|
||||
for action in actions["planned"]:
|
||||
print(f" • {action}")
|
||||
|
||||
if parsed.apply:
|
||||
print(f"\nApplied actions ({len(actions['applied'])}):")
|
||||
for action in actions["applied"]:
|
||||
print(f" ✓ {action}")
|
||||
else:
|
||||
print("\nRun with --apply to execute these changes.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_init(parsed) -> int:
|
||||
"""Run the init subcommand."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = _create_config(parsed)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
try:
|
||||
created = engine.init(parsed.feature)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}")
|
||||
return 2
|
||||
|
||||
if not created:
|
||||
print(f"Feature '{parsed.feature}' already has all required artifacts.")
|
||||
return 0
|
||||
|
||||
print(f"{'Would create' if config.dry_run else 'Created'} artifacts for '{parsed.feature}':")
|
||||
for artifact_type, path in created.items():
|
||||
status = "📝" if config.dry_run else "✓"
|
||||
print(f" {status} {artifact_type}: {path}")
|
||||
|
||||
if config.dry_run:
|
||||
print("\nRun with --apply to create these files.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_ai(parsed) -> int:
|
||||
"""Run the AI generation subcommand with ACP/LSP runtime support."""
|
||||
import asyncio
|
||||
from praisonai.standardise.ai_generator import AIGenerator
|
||||
from praisonai.standardise.models import ArtifactType, FeatureSlug
|
||||
|
||||
config = _create_config(parsed)
|
||||
|
||||
slug = FeatureSlug.from_string(parsed.feature)
|
||||
if not slug.is_valid:
|
||||
print(f"Error: Invalid feature slug: {slug.validation_error}")
|
||||
return 2
|
||||
|
||||
# Start ACP/LSP runtime for context gathering
|
||||
runtime = None
|
||||
try:
|
||||
from praisonai.cli.features.interactive_runtime import create_runtime
|
||||
runtime = create_runtime(
|
||||
workspace=str(config.sdk_root or config.project_root or "."),
|
||||
lsp=True,
|
||||
acp=False # ACP not needed for generation
|
||||
)
|
||||
# Start runtime in background
|
||||
asyncio.get_event_loop().run_until_complete(runtime.start())
|
||||
if runtime.lsp_ready:
|
||||
print("🔧 LSP server ready for code intelligence")
|
||||
except Exception:
|
||||
# Runtime is optional, continue without it
|
||||
pass
|
||||
|
||||
generator = AIGenerator(
|
||||
model=parsed.model,
|
||||
sdk_root=config.sdk_root,
|
||||
docs_root=config.docs_root,
|
||||
examples_root=config.examples_root,
|
||||
use_fast_context=True,
|
||||
)
|
||||
|
||||
# Determine which artifacts to generate
|
||||
artifact_types = []
|
||||
gen_type = getattr(parsed, "type", "all")
|
||||
|
||||
if gen_type in ["docs", "all"]:
|
||||
artifact_types.extend([
|
||||
ArtifactType.DOCS_CONCEPT,
|
||||
ArtifactType.DOCS_FEATURE,
|
||||
ArtifactType.DOCS_CLI,
|
||||
ArtifactType.DOCS_SDK,
|
||||
])
|
||||
|
||||
if gen_type in ["examples", "all"]:
|
||||
artifact_types.extend([
|
||||
ArtifactType.EXAMPLE_BASIC,
|
||||
ArtifactType.EXAMPLE_ADVANCED,
|
||||
])
|
||||
|
||||
print(f"🤖 AI Generation for '{slug.normalised}'")
|
||||
print("=" * 50)
|
||||
|
||||
results = []
|
||||
for artifact_type in artifact_types:
|
||||
print(f"\n📝 Generating {artifact_type.value}...")
|
||||
|
||||
try:
|
||||
# Generate with execution verification for examples
|
||||
content, output_path, verification_info = generator.generate(
|
||||
slug, artifact_type,
|
||||
dry_run=not parsed.apply,
|
||||
verify_execution=True,
|
||||
)
|
||||
|
||||
# Show verification results for examples
|
||||
if verification_info:
|
||||
if verification_info.get("execution_passed"):
|
||||
print(" ✅ Code verified: runs successfully")
|
||||
elif verification_info.get("requires_external"):
|
||||
libs = ", ".join(verification_info.get("missing_libraries", []))
|
||||
print(f" ⚠️ Requires external libraries: {libs}")
|
||||
elif not verification_info.get("syntax_valid"):
|
||||
print(f" ❌ Syntax error: {verification_info.get('error', 'Unknown')[:100]}")
|
||||
else:
|
||||
print(f" ❌ Execution failed after {verification_info.get('attempt', 1)} attempts")
|
||||
if verification_info.get("error"):
|
||||
print(f" Error: {verification_info['error'][:150]}")
|
||||
|
||||
# AI content verification (optional)
|
||||
if parsed.verify:
|
||||
print(" 🔍 AI content review...")
|
||||
is_valid, summary, issues = generator.verify(content, artifact_type, slug)
|
||||
if is_valid:
|
||||
print(f" ✅ {summary}")
|
||||
else:
|
||||
print(f" ⚠️ {summary}")
|
||||
for issue in issues[:3]:
|
||||
print(f" - {issue}")
|
||||
|
||||
# Check if we should write
|
||||
can_write = True
|
||||
if verification_info and not verification_info.get("success"):
|
||||
if not verification_info.get("requires_external"):
|
||||
can_write = False
|
||||
print(" ⛔ Not writing: code doesn't run")
|
||||
|
||||
if parsed.apply and output_path and can_write:
|
||||
print(f" ✓ Created: {output_path}")
|
||||
results.append((artifact_type.value, True, output_path))
|
||||
elif not parsed.apply:
|
||||
print(f" 📋 Preview ({len(content)} chars)")
|
||||
# Show first few lines
|
||||
preview_lines = content.split("\n")[:10]
|
||||
for line in preview_lines:
|
||||
print(f" {line[:60]}")
|
||||
if len(content.split("\n")) > 10:
|
||||
print(" ...")
|
||||
results.append((artifact_type.value, True, None))
|
||||
else:
|
||||
results.append((artifact_type.value, False, "Verification failed"))
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
results.append((artifact_type.value, False, str(e)))
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Summary:")
|
||||
success_count = sum(1 for _, success, _ in results if success)
|
||||
print(f" Generated: {success_count}/{len(results)}")
|
||||
|
||||
if not parsed.apply:
|
||||
print("\nRun with --apply to create these files.")
|
||||
|
||||
# Cleanup runtime
|
||||
if runtime:
|
||||
try:
|
||||
asyncio.get_event_loop().run_until_complete(runtime.stop())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_checkpoint(parsed) -> int:
|
||||
"""Run the checkpoint subcommand."""
|
||||
from praisonai.standardise.undo_redo import UndoRedoManager
|
||||
|
||||
manager = UndoRedoManager(Path(parsed.path).resolve())
|
||||
|
||||
success, result = manager.create_checkpoint(parsed.message)
|
||||
|
||||
if success:
|
||||
print(f"✓ Checkpoint created: {result}")
|
||||
else:
|
||||
print(f"❌ Failed: {result}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_undo(parsed) -> int:
|
||||
"""Run the undo subcommand."""
|
||||
from praisonai.standardise.undo_redo import UndoRedoManager
|
||||
|
||||
manager = UndoRedoManager(Path(parsed.path).resolve())
|
||||
|
||||
# List checkpoints if requested
|
||||
if getattr(parsed, "list", False):
|
||||
checkpoints = manager.list_checkpoints()
|
||||
if not checkpoints:
|
||||
print("No checkpoints found.")
|
||||
return 0
|
||||
|
||||
print("Available checkpoints:")
|
||||
for cp_id, date, message in checkpoints:
|
||||
print(f" • {cp_id} ({date}): {message[:50]}")
|
||||
return 0
|
||||
|
||||
# Undo to checkpoint
|
||||
success, result = manager.undo(parsed.checkpoint)
|
||||
|
||||
if success:
|
||||
print(f"✓ {result}")
|
||||
else:
|
||||
print(f"❌ {result}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _run_redo(parsed) -> int:
|
||||
"""Run the redo subcommand."""
|
||||
from praisonai.standardise.undo_redo import UndoRedoManager
|
||||
|
||||
manager = UndoRedoManager(Path(parsed.path).resolve())
|
||||
|
||||
success, result = manager.redo()
|
||||
|
||||
if success:
|
||||
print(f"✓ {result}")
|
||||
else:
|
||||
print(f"❌ {result}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# Click-based alternative for integration with main CLI
|
||||
def register_click_commands(app):
|
||||
"""Register click commands with the main CLI app."""
|
||||
try:
|
||||
import click
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
@app.group(name="standardise")
|
||||
def standardise_group():
|
||||
"""Standardise documentation and examples (FDEP)."""
|
||||
pass
|
||||
|
||||
@standardise_group.command(name="check")
|
||||
@click.option("--path", "-p", default=".", help="Project root path")
|
||||
@click.option("--feature", help="Specific feature slug to check")
|
||||
@click.option("--scope", type=click.Choice(["all", "docs", "examples", "sdk", "cli"]), default="all")
|
||||
@click.option("--ci", is_flag=True, help="CI mode")
|
||||
def check_cmd(path, feature, scope, ci):
|
||||
"""Check for standardisation issues."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = StandardiseConfig(
|
||||
project_root=Path(path).resolve(),
|
||||
feature_filter=feature,
|
||||
scope=scope,
|
||||
ci_mode=ci,
|
||||
)
|
||||
engine = StandardiseEngine(config)
|
||||
report = engine.check()
|
||||
|
||||
output = engine.reports.generate(report, "text")
|
||||
click.echo(output)
|
||||
|
||||
if ci:
|
||||
sys.exit(engine.get_exit_code(report))
|
||||
|
||||
@standardise_group.command(name="report")
|
||||
@click.option("--path", "-p", default=".", help="Project root path")
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json", "markdown"]), default="text")
|
||||
@click.option("--output", "-o", help="Output file path")
|
||||
@click.option("--ci", is_flag=True, help="CI mode")
|
||||
def report_cmd(path, format, output, ci):
|
||||
"""Generate detailed report."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = StandardiseConfig(
|
||||
project_root=Path(path).resolve(),
|
||||
ci_mode=ci,
|
||||
)
|
||||
engine = StandardiseEngine(config)
|
||||
report = engine.check()
|
||||
|
||||
content = engine.reports.generate(report, format)
|
||||
|
||||
if output:
|
||||
Path(output).write_text(content, encoding="utf-8")
|
||||
click.echo(f"Report saved to {output}")
|
||||
else:
|
||||
click.echo(content)
|
||||
|
||||
@standardise_group.command(name="fix")
|
||||
@click.option("--path", "-p", default=".", help="Project root path")
|
||||
@click.option("--feature", help="Specific feature slug to fix")
|
||||
@click.option("--apply", is_flag=True, help="Actually apply changes")
|
||||
@click.option("--no-backup", is_flag=True, help="Don't create backups")
|
||||
def fix_cmd(path, feature, apply, no_backup):
|
||||
"""Fix standardisation issues."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = StandardiseConfig(
|
||||
project_root=Path(path).resolve(),
|
||||
feature_filter=feature,
|
||||
dry_run=not apply,
|
||||
backup=not no_backup,
|
||||
)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
actions = engine.fix(feature=feature, apply=apply)
|
||||
|
||||
if not actions["planned"]:
|
||||
click.echo("No fixes needed.")
|
||||
return
|
||||
|
||||
click.echo(f"Planned actions ({len(actions['planned'])}):")
|
||||
for action in actions["planned"]:
|
||||
click.echo(f" • {action}")
|
||||
|
||||
if apply:
|
||||
click.echo(f"\nApplied actions ({len(actions['applied'])}):")
|
||||
for action in actions["applied"]:
|
||||
click.echo(f" ✓ {action}")
|
||||
else:
|
||||
click.echo("\nRun with --apply to execute these changes.")
|
||||
|
||||
@standardise_group.command(name="init")
|
||||
@click.argument("feature")
|
||||
@click.option("--path", "-p", default=".", help="Project root path")
|
||||
@click.option("--apply", is_flag=True, help="Actually create files")
|
||||
def init_cmd(feature, path, apply):
|
||||
"""Initialise a new feature with all required artifacts."""
|
||||
StandardiseEngine, StandardiseConfig = _lazy_import_engine()
|
||||
|
||||
config = StandardiseConfig(
|
||||
project_root=Path(path).resolve(),
|
||||
dry_run=not apply,
|
||||
)
|
||||
engine = StandardiseEngine(config)
|
||||
|
||||
try:
|
||||
created = engine.init(feature)
|
||||
except ValueError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
if not created:
|
||||
click.echo(f"Feature '{feature}' already has all required artifacts.")
|
||||
return
|
||||
|
||||
action_word = "Would create" if config.dry_run else "Created"
|
||||
click.echo(f"{action_word} artifacts for '{feature}':")
|
||||
for artifact_type, artifact_path in created.items():
|
||||
status = "📝" if config.dry_run else "✓"
|
||||
click.echo(f" {status} {artifact_type}: {artifact_path}")
|
||||
|
||||
if config.dry_run:
|
||||
click.echo("\nRun with --apply to create these files.")
|
||||
|
||||
return standardise_group
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(standardise_command())
|
||||
@@ -10,6 +10,12 @@ Usage:
|
||||
praisonai test --provider openai # Run OpenAI provider tests
|
||||
praisonai test --live # Enable live API tests
|
||||
praisonai test --parallel auto # Run with parallelization
|
||||
|
||||
# Interactive mode tests
|
||||
praisonai test interactive --csv tests.csv
|
||||
praisonai test interactive --suite smoke
|
||||
praisonai test interactive --list
|
||||
praisonai test interactive --generate-template
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -239,6 +245,272 @@ def run(
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# Built-in interactive test suites
|
||||
BUILTIN_SUITES = {
|
||||
"smoke": "smoke.csv",
|
||||
"tools": "tools.csv",
|
||||
"refactor": "refactor.csv",
|
||||
"multi_agent": "multi_agent.csv",
|
||||
}
|
||||
|
||||
# Special suites that don't use CSV
|
||||
SPECIAL_SUITES = {
|
||||
"advanced": "Advanced Agent-Centric (5 complex autonomous scenarios)",
|
||||
}
|
||||
|
||||
|
||||
def _get_builtin_suite_path(suite: str):
|
||||
"""Get path to built-in test suite."""
|
||||
from pathlib import Path
|
||||
|
||||
if suite not in BUILTIN_SUITES:
|
||||
return None
|
||||
|
||||
# Find fixtures directory relative to package
|
||||
package_dir = Path(__file__).parent.parent.parent.parent # Up to src/praisonai
|
||||
fixtures_dir = package_dir / "tests" / "fixtures" / "interactive"
|
||||
|
||||
suite_path = fixtures_dir / BUILTIN_SUITES[suite]
|
||||
if suite_path.exists():
|
||||
return suite_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@app.command("interactive")
|
||||
def interactive(
|
||||
csv: Optional[str] = typer.Option(
|
||||
None, "--csv", "-c",
|
||||
help="Path to CSV file with test cases"
|
||||
),
|
||||
suite: Optional[str] = typer.Option(
|
||||
None, "--suite", "-s",
|
||||
help="Built-in test suite: smoke, tools, refactor, multi_agent"
|
||||
),
|
||||
model: str = typer.Option(
|
||||
"gpt-4o-mini", "--model", "-m",
|
||||
help="LLM model for agent"
|
||||
),
|
||||
judge_model: str = typer.Option(
|
||||
"gpt-4o-mini", "--judge-model",
|
||||
help="LLM model for judge"
|
||||
),
|
||||
workspace: Optional[str] = typer.Option(
|
||||
None, "--workspace", "-w",
|
||||
help="Workspace directory (default: temp)"
|
||||
),
|
||||
artifacts_dir: Optional[str] = typer.Option(
|
||||
None, "--artifacts-dir",
|
||||
help="Directory for test artifacts"
|
||||
),
|
||||
fail_fast: bool = typer.Option(
|
||||
False, "--fail-fast", "-x",
|
||||
help="Stop on first failure"
|
||||
),
|
||||
keep_artifacts: bool = typer.Option(
|
||||
False, "--keep-artifacts",
|
||||
help="Keep test artifacts after run"
|
||||
),
|
||||
no_judge: bool = typer.Option(
|
||||
False, "--no-judge",
|
||||
help="Skip judge evaluation even if rubric present"
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False, "--verbose", "-v",
|
||||
help="Verbose output"
|
||||
),
|
||||
list_suites: bool = typer.Option(
|
||||
False, "--list",
|
||||
help="List available built-in suites"
|
||||
),
|
||||
generate_template: bool = typer.Option(
|
||||
False, "--generate-template",
|
||||
help="Generate CSV template in current directory"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Run interactive mode tests from CSV.
|
||||
|
||||
Tests are executed through the headless interactive core, using the same
|
||||
pipeline as the interactive TUI (InteractiveRuntime, get_interactive_tools,
|
||||
ActionOrchestrator).
|
||||
|
||||
Examples:
|
||||
praisonai test interactive --csv tests.csv
|
||||
praisonai test interactive --suite smoke
|
||||
praisonai test interactive --suite tools --keep-artifacts
|
||||
praisonai test interactive --list
|
||||
praisonai test interactive --generate-template
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
# List suites
|
||||
if list_suites:
|
||||
typer.echo("📋 Available built-in test suites:")
|
||||
typer.echo("")
|
||||
typer.echo("CSV-based suites:")
|
||||
for name, filename in BUILTIN_SUITES.items():
|
||||
suite_path = _get_builtin_suite_path(name)
|
||||
status = "✅" if suite_path else "❌ (not found)"
|
||||
typer.echo(f" {name:15} {filename:20} {status}")
|
||||
typer.echo("")
|
||||
typer.echo("Special suites:")
|
||||
for name, description in SPECIAL_SUITES.items():
|
||||
typer.echo(f" {name:15} {description}")
|
||||
typer.echo("")
|
||||
typer.echo("Usage: praisonai test interactive --suite <name>")
|
||||
typer.echo(" PRAISONAI_LIVE_INTERACTIVE=1 praisonai test interactive --suite advanced")
|
||||
return
|
||||
|
||||
# Generate template
|
||||
if generate_template:
|
||||
# Lazy import
|
||||
from praisonai.cli.features.csv_test_runner import generate_csv_template
|
||||
|
||||
output_path = Path.cwd() / "interactive_tests_template.csv"
|
||||
generate_csv_template(output_path)
|
||||
typer.echo(f"✅ Generated CSV template: {output_path}")
|
||||
return
|
||||
|
||||
# Validate inputs
|
||||
if not csv and not suite:
|
||||
typer.echo("❌ Either --csv or --suite is required")
|
||||
typer.echo("")
|
||||
typer.echo("Examples:")
|
||||
typer.echo(" praisonai test interactive --csv tests.csv")
|
||||
typer.echo(" praisonai test interactive --suite smoke")
|
||||
typer.echo(" praisonai test interactive --suite advanced")
|
||||
typer.echo(" praisonai test interactive --list")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Handle special suites
|
||||
if suite and suite in SPECIAL_SUITES:
|
||||
if suite == "advanced":
|
||||
_run_advanced_suite(
|
||||
model=model,
|
||||
artifacts_dir=Path(artifacts_dir) if artifacts_dir else None,
|
||||
keep_artifacts=keep_artifacts,
|
||||
verbose=verbose,
|
||||
)
|
||||
return
|
||||
|
||||
# Get CSV path
|
||||
if csv:
|
||||
csv_path = Path(csv)
|
||||
if not csv_path.exists():
|
||||
typer.echo(f"❌ CSV file not found: {csv}")
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
csv_path = _get_builtin_suite_path(suite)
|
||||
if not csv_path:
|
||||
typer.echo(f"❌ Built-in suite not found: {suite}")
|
||||
typer.echo("Use --list to see available suites")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Lazy import runner
|
||||
from praisonai.cli.features.csv_test_runner import CSVTestRunner
|
||||
|
||||
typer.echo(f"🧪 Running interactive tests from: {csv_path}")
|
||||
typer.echo(f" Model: {model} | Judge: {judge_model}")
|
||||
typer.echo("")
|
||||
|
||||
# Create runner
|
||||
runner = CSVTestRunner(
|
||||
csv_path=csv_path,
|
||||
model=model,
|
||||
judge_model=judge_model,
|
||||
workspace=Path(workspace) if workspace else None,
|
||||
artifacts_dir=Path(artifacts_dir) if artifacts_dir else None,
|
||||
fail_fast=fail_fast,
|
||||
keep_artifacts=keep_artifacts,
|
||||
no_judge=no_judge,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
# Run tests
|
||||
summary = runner.run()
|
||||
|
||||
# Print summary
|
||||
typer.echo("")
|
||||
summary.print_summary()
|
||||
|
||||
# Save summary if keeping artifacts
|
||||
if keep_artifacts and runner.artifacts_dir:
|
||||
import json
|
||||
summary_path = runner.artifacts_dir / "summary.json"
|
||||
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(summary_path, "w") as f:
|
||||
json.dump(summary.to_dict(), f, indent=2, default=str)
|
||||
typer.echo(f"\n📁 Artifacts saved to: {runner.artifacts_dir}")
|
||||
|
||||
# Exit code based on results
|
||||
if summary.failed > 0 or summary.errors > 0:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _run_advanced_suite(
|
||||
model: str = "gpt-4o-mini",
|
||||
artifacts_dir = None,
|
||||
keep_artifacts: bool = True,
|
||||
verbose: bool = True,
|
||||
):
|
||||
"""Run the Advanced Agent-Centric test suite."""
|
||||
from pathlib import Path
|
||||
|
||||
# Check prerequisites
|
||||
typer.echo("🔍 Checking Advanced suite prerequisites...")
|
||||
|
||||
# Check if live interactive is enabled
|
||||
live_enabled = os.environ.get("PRAISONAI_LIVE_INTERACTIVE", "0") == "1"
|
||||
has_api_key = bool(os.environ.get("OPENAI_API_KEY"))
|
||||
|
||||
if not live_enabled:
|
||||
typer.echo("\n⚠️ Cannot run Advanced tests: PRAISONAI_LIVE_INTERACTIVE=1 not set")
|
||||
typer.echo("")
|
||||
typer.echo("Prerequisites:")
|
||||
typer.echo(" 1. Set PRAISONAI_LIVE_INTERACTIVE=1")
|
||||
typer.echo(" 2. Set OPENAI_API_KEY")
|
||||
typer.echo("")
|
||||
typer.echo("Example:")
|
||||
typer.echo(" PRAISONAI_LIVE_INTERACTIVE=1 praisonai test interactive --suite advanced")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not has_api_key:
|
||||
typer.echo("\n⚠️ Cannot run Advanced tests: OPENAI_API_KEY not set")
|
||||
raise typer.Exit(1)
|
||||
|
||||
typer.echo("✅ Live interactive mode enabled")
|
||||
typer.echo("✅ API key configured")
|
||||
typer.echo("")
|
||||
|
||||
# Lazy import runner and scenarios
|
||||
from tests.live.interactive.runner import LiveInteractiveRunner
|
||||
from tests.live.interactive.advanced.scenarios import ALL_ADVANCED_SCENARIOS
|
||||
|
||||
# Create runner
|
||||
runner = LiveInteractiveRunner(
|
||||
model=model,
|
||||
artifacts_dir=Path(artifacts_dir) if artifacts_dir else None,
|
||||
keep_artifacts=keep_artifacts,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
# Run tests
|
||||
summary = runner.run_all(ALL_ADVANCED_SCENARIOS)
|
||||
|
||||
# Print summary
|
||||
typer.echo("")
|
||||
summary.print_summary()
|
||||
|
||||
# Show artifacts location
|
||||
if keep_artifacts:
|
||||
typer.echo(f"\n📁 Artifacts saved to: {runner.artifacts_dir}")
|
||||
|
||||
# Exit code
|
||||
if summary.failed > 0:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def info():
|
||||
"""Show test configuration and available options."""
|
||||
|
||||
@@ -160,6 +160,8 @@ class ActionOrchestrator:
|
||||
|
||||
async def _create_fallback_plan(self, prompt: str, plan_id: str) -> ActionResult:
|
||||
"""Create a fallback plan when ACP is not available for planning."""
|
||||
import re # Import at function level for all pattern matching
|
||||
|
||||
# This is a simplified fallback - in production, this would use LLM
|
||||
steps = []
|
||||
|
||||
@@ -168,7 +170,6 @@ class ActionOrchestrator:
|
||||
|
||||
if "create" in prompt_lower or "new file" in prompt_lower:
|
||||
# Extract file path if mentioned
|
||||
import re
|
||||
file_match = re.search(r'(?:create|new)\s+(?:a\s+)?(?:file\s+)?(?:called\s+)?([^\s]+\.\w+)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else "new_file.py"
|
||||
|
||||
@@ -188,7 +189,8 @@ class ActionOrchestrator:
|
||||
))
|
||||
|
||||
elif "edit" in prompt_lower or "modify" in prompt_lower or "change" in prompt_lower:
|
||||
file_match = re.search(r'(?:edit|modify|change)\s+([^\s]+\.\w+)', prompt_lower)
|
||||
# Match patterns like "edit file X", "edit X", "modify X"
|
||||
file_match = re.search(r'(?:edit|modify|change)\s+(?:file\s+)?([^\s]+(?:\.\w+)?)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else ""
|
||||
|
||||
if target:
|
||||
@@ -197,11 +199,13 @@ class ActionOrchestrator:
|
||||
action_type=ActionType.FILE_EDIT,
|
||||
description=f"Edit file: {target}",
|
||||
target=target,
|
||||
params={"changes": prompt}
|
||||
params={"changes": prompt},
|
||||
status=ActionStatus.APPROVED # Auto-approve for fallback
|
||||
))
|
||||
|
||||
elif "delete" in prompt_lower or "remove" in prompt_lower:
|
||||
file_match = re.search(r'(?:delete|remove)\s+([^\s]+\.\w+)', prompt_lower)
|
||||
# Match patterns like "delete file X", "delete X", "remove X"
|
||||
file_match = re.search(r'(?:delete|remove)\s+(?:file\s+)?([^\s]+(?:\.\w+)?)', prompt_lower)
|
||||
target = file_match.group(1) if file_match else ""
|
||||
|
||||
if target:
|
||||
@@ -209,7 +213,8 @@ class ActionOrchestrator:
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.FILE_DELETE,
|
||||
description=f"Delete file: {target}",
|
||||
target=target
|
||||
target=target,
|
||||
status=ActionStatus.APPROVED # Auto-approve for fallback
|
||||
))
|
||||
|
||||
elif "rename" in prompt_lower:
|
||||
@@ -218,19 +223,22 @@ class ActionOrchestrator:
|
||||
action_type=ActionType.REFACTOR,
|
||||
description="Rename operation",
|
||||
target="",
|
||||
params={"prompt": prompt}
|
||||
params={"prompt": prompt},
|
||||
status=ActionStatus.APPROVED # Auto-approve for fallback
|
||||
))
|
||||
|
||||
elif "run" in prompt_lower or "execute" in prompt_lower:
|
||||
cmd_match = re.search(r'(?:run|execute)\s+[`"\']?([^`"\']+)[`"\']?', prompt_lower)
|
||||
command = cmd_match.group(1) if cmd_match else ""
|
||||
elif "run" in prompt_lower or "execute" in prompt_lower or "command" in prompt_lower:
|
||||
# Match patterns like "run command: X", "execute X", "run X"
|
||||
cmd_match = re.search(r'(?:run|execute)\s+(?:command[:\s]+)?[`"\']?(.+?)[`"\']?$', prompt_lower)
|
||||
command = cmd_match.group(1).strip() if cmd_match else ""
|
||||
|
||||
if command:
|
||||
steps.append(ActionStep(
|
||||
id=self._generate_step_id(plan_id, 0),
|
||||
action_type=ActionType.SHELL_COMMAND,
|
||||
description=f"Execute: {command}",
|
||||
target=command
|
||||
target=command,
|
||||
status=ActionStatus.APPROVED # Auto-approve for fallback
|
||||
))
|
||||
|
||||
if not steps:
|
||||
|
||||
@@ -596,20 +596,34 @@ class CodeIntelligenceRouter:
|
||||
|
||||
def _extract_symbol_from_query(self, query: str) -> Optional[str]:
|
||||
"""Extract symbol name from query."""
|
||||
# Common patterns for symbol extraction
|
||||
# Common patterns for symbol extraction (order matters - more specific first)
|
||||
patterns = [
|
||||
r'(?:definition\s+of|where\s+is|find|show)\s+["\']?(\w+)["\']?',
|
||||
# "find all references to X" - capture X after "to"
|
||||
r'references?\s+to\s+["\']?(\w+)["\']?',
|
||||
# "definition of X" - capture X after "of"
|
||||
r'definition\s+of\s+["\']?(\w+)["\']?',
|
||||
# "where is X defined/used"
|
||||
r'where\s+is\s+["\']?(\w+)["\']?\s+(?:defined|used)',
|
||||
# Quoted or backticked symbols
|
||||
r'["\'](\w+)["\']',
|
||||
r'`(\w+)`',
|
||||
# "X is defined/used"
|
||||
r'(\w+)\s+(?:is\s+)?(?:defined|used)',
|
||||
# Generic "find/show X" - but filter common words
|
||||
r'(?:find|show)\s+["\']?(\w+)["\']?',
|
||||
]
|
||||
|
||||
# Words to filter out
|
||||
filter_words = {'the', 'a', 'an', 'is', 'are', 'in', 'of', 'to', 'for',
|
||||
'all', 'any', 'some', 'this', 'that', 'where', 'what',
|
||||
'definition', 'references', 'reference', 'symbol', 'symbols'}
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
symbol = match.group(1)
|
||||
# Filter out common words
|
||||
if symbol.lower() not in ['the', 'a', 'an', 'is', 'are', 'in', 'of', 'to', 'for']:
|
||||
if symbol.lower() not in filter_words:
|
||||
return symbol
|
||||
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,736 @@
|
||||
"""
|
||||
CSV-driven Interactive Test Runner for PraisonAI.
|
||||
|
||||
Provides a CSV-driven test runner that:
|
||||
- Loads test cases from CSV with defined schema
|
||||
- Runs each test in isolated temp workspace
|
||||
- Executes prompts via headless interactive core
|
||||
- Validates tool calls, files, and responses
|
||||
- Integrates LLM-as-judge for response quality
|
||||
- Emits artifacts per-test
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# CSV Schema definition
|
||||
CSV_SCHEMA = {
|
||||
# Required columns
|
||||
"id": {"required": True, "type": str, "description": "Unique test identifier"},
|
||||
"name": {"required": True, "type": str, "description": "Test name"},
|
||||
"prompts": {"required": True, "type": str, "description": "Single prompt or JSON array"},
|
||||
# Optional columns
|
||||
"description": {"required": False, "type": str, "default": ""},
|
||||
"mode": {"required": False, "type": str, "default": "headless"},
|
||||
"workspace_fixture": {"required": False, "type": str, "default": "empty"},
|
||||
"expected_tools": {"required": False, "type": str, "default": ""},
|
||||
"forbidden_tools": {"required": False, "type": str, "default": ""},
|
||||
"expected_files": {"required": False, "type": str, "default": "{}"},
|
||||
"expected_response": {"required": False, "type": str, "default": ""},
|
||||
"judge_rubric": {"required": False, "type": str, "default": ""},
|
||||
"judge_threshold": {"required": False, "type": float, "default": 7.0},
|
||||
"judge_model": {"required": False, "type": str, "default": "gpt-4o-mini"},
|
||||
"timeout": {"required": False, "type": int, "default": 60},
|
||||
"retries": {"required": False, "type": int, "default": 0},
|
||||
"skip_if": {"required": False, "type": str, "default": ""},
|
||||
"agents": {"required": False, "type": str, "default": "[]"},
|
||||
"workflow": {"required": False, "type": str, "default": "{}"},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
"""A single test case from CSV."""
|
||||
id: str
|
||||
name: str
|
||||
prompts: List[str]
|
||||
description: str = ""
|
||||
mode: str = "headless"
|
||||
workspace_fixture: str = "empty"
|
||||
expected_tools: List[str] = field(default_factory=list)
|
||||
forbidden_tools: List[str] = field(default_factory=list)
|
||||
expected_files: Dict[str, str] = field(default_factory=dict)
|
||||
expected_response: str = ""
|
||||
judge_rubric: str = ""
|
||||
judge_threshold: float = 7.0
|
||||
judge_model: str = "gpt-4o-mini"
|
||||
timeout: int = 60
|
||||
retries: int = 0
|
||||
skip_if: str = ""
|
||||
agents: List[Dict[str, Any]] = field(default_factory=list)
|
||||
workflow: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def should_skip(self) -> Optional[str]:
|
||||
"""Check if test should be skipped."""
|
||||
if not self.skip_if:
|
||||
return None
|
||||
|
||||
conditions = [c.strip() for c in self.skip_if.split(",")]
|
||||
|
||||
for condition in conditions:
|
||||
if condition == "no_openai_key":
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
return "OPENAI_API_KEY not set"
|
||||
elif condition == "no_anthropic_key":
|
||||
if not os.environ.get("ANTHROPIC_API_KEY"):
|
||||
return "ANTHROPIC_API_KEY not set"
|
||||
elif condition == "no_lsp":
|
||||
# Could check if LSP is available
|
||||
pass
|
||||
elif condition == "no_network":
|
||||
if os.environ.get("PRAISONAI_NO_NETWORK"):
|
||||
return "Network disabled"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestResult:
|
||||
"""Result of a single test."""
|
||||
test_id: str
|
||||
test_name: str
|
||||
status: str # passed, failed, skipped, error
|
||||
duration: float
|
||||
tool_calls: List[str] = field(default_factory=list)
|
||||
response: str = ""
|
||||
judge_score: Optional[float] = None
|
||||
judge_passed: Optional[bool] = None
|
||||
judge_reasoning: str = ""
|
||||
artifacts_path: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
skip_reason: Optional[str] = None
|
||||
assertions: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"test_id": self.test_id,
|
||||
"test_name": self.test_name,
|
||||
"status": self.status,
|
||||
"duration": self.duration,
|
||||
"tool_calls": self.tool_calls,
|
||||
"response": self.response[:500] if self.response else "",
|
||||
"judge_score": self.judge_score,
|
||||
"judge_passed": self.judge_passed,
|
||||
"judge_reasoning": self.judge_reasoning,
|
||||
"artifacts_path": self.artifacts_path,
|
||||
"error": self.error,
|
||||
"skip_reason": self.skip_reason,
|
||||
"assertions": self.assertions,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestSummary:
|
||||
"""Summary of all test results."""
|
||||
total: int = 0
|
||||
passed: int = 0
|
||||
failed: int = 0
|
||||
skipped: int = 0
|
||||
errors: int = 0
|
||||
duration: float = 0.0
|
||||
judge_avg: Optional[float] = None
|
||||
results: List[TestResult] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"total": self.total,
|
||||
"passed": self.passed,
|
||||
"failed": self.failed,
|
||||
"skipped": self.skipped,
|
||||
"errors": self.errors,
|
||||
"duration": self.duration,
|
||||
"judge_avg": self.judge_avg,
|
||||
"pass_rate": self.passed / self.total if self.total > 0 else 0,
|
||||
"results": [r.to_dict() for r in self.results],
|
||||
}
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""Print summary to console."""
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
table = Table(title="Interactive Test Results")
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Name", style="white")
|
||||
table.add_column("Status")
|
||||
table.add_column("Duration", justify="right")
|
||||
table.add_column("Judge", justify="right")
|
||||
|
||||
for r in self.results:
|
||||
status_style = {
|
||||
"passed": "[green]✓ passed[/green]",
|
||||
"failed": "[red]✗ failed[/red]",
|
||||
"skipped": "[yellow]○ skipped[/yellow]",
|
||||
"error": "[red]! error[/red]",
|
||||
}.get(r.status, r.status)
|
||||
|
||||
judge_str = f"{r.judge_score:.1f}" if r.judge_score is not None else "-"
|
||||
|
||||
table.add_row(
|
||||
r.test_id,
|
||||
r.test_name[:30],
|
||||
status_style,
|
||||
f"{r.duration:.2f}s",
|
||||
judge_str,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
console.print(f"[bold]Total:[/bold] {self.total} | "
|
||||
f"[green]Passed:[/green] {self.passed} | "
|
||||
f"[red]Failed:[/red] {self.failed} | "
|
||||
f"[yellow]Skipped:[/yellow] {self.skipped}")
|
||||
if self.judge_avg is not None:
|
||||
console.print(f"[bold]Average Judge Score:[/bold] {self.judge_avg:.2f}")
|
||||
console.print(f"[bold]Total Duration:[/bold] {self.duration:.2f}s")
|
||||
|
||||
except ImportError:
|
||||
# Fallback to plain print
|
||||
print("\n=== Interactive Test Results ===")
|
||||
for r in self.results:
|
||||
status = {"passed": "✓", "failed": "✗", "skipped": "○", "error": "!"}.get(r.status, "?")
|
||||
print(f" {status} {r.test_id}: {r.test_name} ({r.duration:.2f}s)")
|
||||
print(f"\nTotal: {self.total} | Passed: {self.passed} | Failed: {self.failed} | Skipped: {self.skipped}")
|
||||
|
||||
|
||||
def parse_csv(csv_path: Path) -> List[TestCase]:
|
||||
"""
|
||||
Parse CSV file into TestCase objects.
|
||||
|
||||
Args:
|
||||
csv_path: Path to CSV file
|
||||
|
||||
Returns:
|
||||
List of TestCase objects
|
||||
"""
|
||||
test_cases = []
|
||||
|
||||
with open(csv_path, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
for row_num, row in enumerate(reader, start=2): # Start at 2 (header is 1)
|
||||
try:
|
||||
# Parse prompts (single string or JSON array)
|
||||
prompts_raw = row.get("prompts", "").strip()
|
||||
if prompts_raw.startswith("["):
|
||||
prompts = json.loads(prompts_raw)
|
||||
else:
|
||||
prompts = [prompts_raw] if prompts_raw else []
|
||||
|
||||
# Parse expected_tools
|
||||
expected_tools = []
|
||||
if row.get("expected_tools"):
|
||||
expected_tools = [t.strip() for t in row["expected_tools"].split(",") if t.strip()]
|
||||
|
||||
# Parse forbidden_tools
|
||||
forbidden_tools = []
|
||||
if row.get("forbidden_tools"):
|
||||
forbidden_tools = [t.strip() for t in row["forbidden_tools"].split(",") if t.strip()]
|
||||
|
||||
# Parse expected_files JSON
|
||||
expected_files = {}
|
||||
if row.get("expected_files"):
|
||||
try:
|
||||
expected_files = json.loads(row["expected_files"])
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Row {row_num}: Invalid JSON in expected_files")
|
||||
|
||||
# Parse agents JSON
|
||||
agents = []
|
||||
if row.get("agents"):
|
||||
try:
|
||||
agents = json.loads(row["agents"])
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Row {row_num}: Invalid JSON in agents")
|
||||
|
||||
# Parse workflow JSON
|
||||
workflow = {}
|
||||
if row.get("workflow"):
|
||||
try:
|
||||
workflow = json.loads(row["workflow"])
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Row {row_num}: Invalid JSON in workflow")
|
||||
|
||||
# Parse numeric fields
|
||||
judge_threshold = float(row.get("judge_threshold", 7.0) or 7.0)
|
||||
timeout = int(row.get("timeout", 60) or 60)
|
||||
retries = int(row.get("retries", 0) or 0)
|
||||
|
||||
test_case = TestCase(
|
||||
id=row.get("id", f"test_{row_num}"),
|
||||
name=row.get("name", f"Test {row_num}"),
|
||||
prompts=prompts,
|
||||
description=row.get("description", ""),
|
||||
mode=row.get("mode", "headless"),
|
||||
workspace_fixture=row.get("workspace_fixture", "empty"),
|
||||
expected_tools=expected_tools,
|
||||
forbidden_tools=forbidden_tools,
|
||||
expected_files=expected_files,
|
||||
expected_response=row.get("expected_response", ""),
|
||||
judge_rubric=row.get("judge_rubric", ""),
|
||||
judge_threshold=judge_threshold,
|
||||
judge_model=row.get("judge_model", "gpt-4o-mini"),
|
||||
timeout=timeout,
|
||||
retries=retries,
|
||||
skip_if=row.get("skip_if", ""),
|
||||
agents=agents,
|
||||
workflow=workflow,
|
||||
)
|
||||
|
||||
test_cases.append(test_case)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing row {row_num}: {e}")
|
||||
continue
|
||||
|
||||
return test_cases
|
||||
|
||||
|
||||
class CSVTestRunner:
|
||||
"""
|
||||
CSV-driven test runner for interactive mode.
|
||||
|
||||
Usage:
|
||||
runner = CSVTestRunner(csv_path="tests.csv")
|
||||
summary = runner.run()
|
||||
summary.print_summary()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
csv_path: Path,
|
||||
model: str = "gpt-4o-mini",
|
||||
judge_model: str = "gpt-4o-mini",
|
||||
workspace: Optional[Path] = None,
|
||||
artifacts_dir: Optional[Path] = None,
|
||||
fail_fast: bool = False,
|
||||
keep_artifacts: bool = False,
|
||||
no_judge: bool = False,
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize CSV test runner.
|
||||
|
||||
Args:
|
||||
csv_path: Path to CSV file with test cases
|
||||
model: LLM model for agent
|
||||
judge_model: LLM model for judge
|
||||
workspace: Base workspace directory
|
||||
artifacts_dir: Directory for artifacts
|
||||
fail_fast: Stop on first failure
|
||||
keep_artifacts: Keep artifacts after run
|
||||
no_judge: Skip judge even if rubric present
|
||||
verbose: Verbose output
|
||||
"""
|
||||
self.csv_path = Path(csv_path)
|
||||
self.model = model
|
||||
self.judge_model = judge_model
|
||||
self.workspace = workspace
|
||||
self.artifacts_dir = artifacts_dir
|
||||
self.fail_fast = fail_fast
|
||||
self.keep_artifacts = keep_artifacts
|
||||
self.no_judge = no_judge
|
||||
self.verbose = verbose
|
||||
self.results: List[TestResult] = []
|
||||
|
||||
def run(self) -> TestSummary:
|
||||
"""
|
||||
Run all tests from CSV.
|
||||
|
||||
Returns:
|
||||
TestSummary with all results
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# Parse CSV
|
||||
test_cases = parse_csv(self.csv_path)
|
||||
logger.info(f"Loaded {len(test_cases)} test cases from {self.csv_path}")
|
||||
|
||||
if self.verbose:
|
||||
print(f"Running {len(test_cases)} tests from {self.csv_path}")
|
||||
|
||||
# Run each test
|
||||
for i, test_case in enumerate(test_cases):
|
||||
if self.verbose:
|
||||
print(f"\n[{i+1}/{len(test_cases)}] Running: {test_case.name}")
|
||||
|
||||
# Check skip conditions
|
||||
skip_reason = test_case.should_skip()
|
||||
if skip_reason:
|
||||
result = TestResult(
|
||||
test_id=test_case.id,
|
||||
test_name=test_case.name,
|
||||
status="skipped",
|
||||
duration=0.0,
|
||||
skip_reason=skip_reason,
|
||||
)
|
||||
self.results.append(result)
|
||||
if self.verbose:
|
||||
print(f" Skipped: {skip_reason}")
|
||||
continue
|
||||
|
||||
# Run test
|
||||
result = self._run_single_test(test_case)
|
||||
self.results.append(result)
|
||||
|
||||
if self.verbose:
|
||||
status_icon = {"passed": "✓", "failed": "✗", "error": "!"}.get(result.status, "?")
|
||||
print(f" {status_icon} {result.status} ({result.duration:.2f}s)")
|
||||
if result.error:
|
||||
print(f" Error: {result.error}")
|
||||
|
||||
# Fail fast
|
||||
if self.fail_fast and result.status in ("failed", "error"):
|
||||
logger.info(f"Stopping due to fail-fast: {test_case.id}")
|
||||
break
|
||||
|
||||
# Create summary
|
||||
summary = self._create_summary(time.time() - start_time)
|
||||
|
||||
return summary
|
||||
|
||||
def _run_single_test(self, test_case: TestCase) -> TestResult:
|
||||
"""Run a single test case."""
|
||||
import tempfile
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Create workspace
|
||||
if self.workspace:
|
||||
test_workspace = self.workspace / test_case.id
|
||||
else:
|
||||
test_workspace = Path(tempfile.mkdtemp(prefix=f"praison_test_{test_case.id}_"))
|
||||
|
||||
# Create artifacts dir
|
||||
if self.artifacts_dir:
|
||||
test_artifacts = self.artifacts_dir / test_case.id
|
||||
else:
|
||||
test_artifacts = test_workspace / "artifacts"
|
||||
|
||||
try:
|
||||
# Import harness (lazy)
|
||||
from .interactive_test_harness import InteractiveTestHarness
|
||||
|
||||
# Create harness
|
||||
harness = InteractiveTestHarness(
|
||||
workspace=test_workspace,
|
||||
artifacts_dir=test_artifacts,
|
||||
keep_workspace=self.keep_artifacts,
|
||||
)
|
||||
|
||||
# Setup workspace
|
||||
harness.setup_workspace(test_case.workspace_fixture)
|
||||
|
||||
# Run prompts
|
||||
exec_result = harness.run(
|
||||
prompts=test_case.prompts,
|
||||
model=self.model,
|
||||
approval_mode="auto",
|
||||
agents=test_case.agents if test_case.agents else None,
|
||||
workflow=test_case.workflow if test_case.workflow else None,
|
||||
)
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Get response
|
||||
response = "\n".join(exec_result.responses) if exec_result.responses else ""
|
||||
|
||||
# Verify assertions
|
||||
assertions = {}
|
||||
|
||||
# Tool call assertions
|
||||
tool_result = harness.verify_tool_calls(
|
||||
expected_tools=test_case.expected_tools,
|
||||
forbidden_tools=test_case.forbidden_tools,
|
||||
)
|
||||
assertions["tools"] = tool_result["passed"]
|
||||
|
||||
# File assertions
|
||||
if test_case.expected_files:
|
||||
file_results = harness.verify_files(test_case.expected_files)
|
||||
assertions["files"] = all(file_results.values())
|
||||
else:
|
||||
assertions["files"] = True
|
||||
|
||||
# Response assertion
|
||||
if test_case.expected_response:
|
||||
assertions["response"] = harness.verify_response(response, test_case.expected_response)
|
||||
else:
|
||||
assertions["response"] = True
|
||||
|
||||
# Judge evaluation
|
||||
judge_score = None
|
||||
judge_passed = None
|
||||
judge_reasoning = ""
|
||||
|
||||
if test_case.judge_rubric and not self.no_judge:
|
||||
judge_result = self._run_judge(
|
||||
response=response,
|
||||
rubric=test_case.judge_rubric,
|
||||
threshold=test_case.judge_threshold,
|
||||
model=test_case.judge_model or self.judge_model,
|
||||
)
|
||||
judge_score = judge_result.get("score")
|
||||
judge_passed = judge_result.get("passed")
|
||||
judge_reasoning = judge_result.get("reasoning", "")
|
||||
assertions["judge"] = judge_passed if judge_passed is not None else True
|
||||
harness.artifacts.judge_result = judge_result
|
||||
|
||||
# Determine overall status
|
||||
all_passed = all(assertions.values()) and exec_result.success
|
||||
status = "passed" if all_passed else "failed"
|
||||
|
||||
# Snapshot workspace if keeping artifacts
|
||||
if self.keep_artifacts:
|
||||
harness.snapshot_workspace()
|
||||
|
||||
# Save artifacts
|
||||
artifacts_path = str(harness.save_artifacts(test_case.id))
|
||||
|
||||
return TestResult(
|
||||
test_id=test_case.id,
|
||||
test_name=test_case.name,
|
||||
status=status,
|
||||
duration=duration,
|
||||
tool_calls=harness._executor.get_tools_called() if harness._executor else [],
|
||||
response=response,
|
||||
judge_score=judge_score,
|
||||
judge_passed=judge_passed,
|
||||
judge_reasoning=judge_reasoning,
|
||||
artifacts_path=artifacts_path,
|
||||
assertions=assertions,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Test {test_case.id} error: {e}", exc_info=True)
|
||||
return TestResult(
|
||||
test_id=test_case.id,
|
||||
test_name=test_case.name,
|
||||
status="error",
|
||||
duration=time.time() - start_time,
|
||||
error=str(e),
|
||||
)
|
||||
finally:
|
||||
if not self.keep_artifacts:
|
||||
try:
|
||||
import shutil
|
||||
if test_workspace.exists() and not self.workspace:
|
||||
shutil.rmtree(test_workspace)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _run_judge(
|
||||
self,
|
||||
response: str,
|
||||
rubric: str,
|
||||
threshold: float,
|
||||
model: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run LLM judge on response using Agent class.
|
||||
|
||||
Args:
|
||||
response: Response to evaluate
|
||||
rubric: Evaluation rubric
|
||||
threshold: Pass threshold (1-10)
|
||||
model: Judge model
|
||||
|
||||
Returns:
|
||||
Dict with score, passed, reasoning
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create judge agent
|
||||
judge_agent = Agent(
|
||||
name="Judge",
|
||||
role="Evaluator",
|
||||
goal="Evaluate response quality based on rubric",
|
||||
instructions=f"""You are an expert evaluator. Your task is to evaluate a response based on the given rubric.
|
||||
|
||||
RUBRIC:
|
||||
{rubric}
|
||||
|
||||
RESPONSE TO EVALUATE:
|
||||
{response}
|
||||
|
||||
Provide your evaluation in the following format:
|
||||
SCORE: [1-10]
|
||||
REASONING: [Your detailed reasoning]
|
||||
|
||||
Be strict but fair. A score of 7+ means the response adequately meets the rubric criteria.""",
|
||||
llm=model,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Get evaluation
|
||||
eval_response = judge_agent.chat("Please evaluate the response based on the rubric.")
|
||||
|
||||
# Parse score from response
|
||||
score = self._parse_judge_score(eval_response)
|
||||
passed = score >= threshold if score is not None else None
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"passed": passed,
|
||||
"reasoning": eval_response,
|
||||
"threshold": threshold,
|
||||
"model": model,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Judge evaluation failed: {e}")
|
||||
return {
|
||||
"score": None,
|
||||
"passed": None,
|
||||
"reasoning": f"Judge error: {e}",
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def _parse_judge_score(self, response: str) -> Optional[float]:
|
||||
"""Parse score from judge response."""
|
||||
import re
|
||||
|
||||
# Try to find "SCORE: X" pattern
|
||||
match = re.search(r"SCORE:\s*(\d+(?:\.\d+)?)", response, re.IGNORECASE)
|
||||
if match:
|
||||
try:
|
||||
return float(match.group(1))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try to find standalone number at start
|
||||
match = re.search(r"^(\d+(?:\.\d+)?)", response.strip())
|
||||
if match:
|
||||
try:
|
||||
score = float(match.group(1))
|
||||
if 1 <= score <= 10:
|
||||
return score
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _create_summary(self, total_duration: float) -> TestSummary:
|
||||
"""Create test summary."""
|
||||
summary = TestSummary(
|
||||
total=len(self.results),
|
||||
passed=sum(1 for r in self.results if r.status == "passed"),
|
||||
failed=sum(1 for r in self.results if r.status == "failed"),
|
||||
skipped=sum(1 for r in self.results if r.status == "skipped"),
|
||||
errors=sum(1 for r in self.results if r.status == "error"),
|
||||
duration=total_duration,
|
||||
results=self.results,
|
||||
)
|
||||
|
||||
# Calculate average judge score
|
||||
judge_scores = [r.judge_score for r in self.results if r.judge_score is not None]
|
||||
if judge_scores:
|
||||
summary.judge_avg = sum(judge_scores) / len(judge_scores)
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def generate_csv_template(output_path: Path = None) -> str:
|
||||
"""
|
||||
Generate a CSV template with all columns.
|
||||
|
||||
Args:
|
||||
output_path: Path to write template (optional)
|
||||
|
||||
Returns:
|
||||
CSV template string
|
||||
"""
|
||||
headers = list(CSV_SCHEMA.keys())
|
||||
|
||||
# Example rows
|
||||
examples = [
|
||||
{
|
||||
"id": "smoke_01",
|
||||
"name": "Basic Chat",
|
||||
"prompts": "Hello, what is 2+2?",
|
||||
"description": "Test basic chat response",
|
||||
"mode": "headless",
|
||||
"workspace_fixture": "empty",
|
||||
"expected_tools": "",
|
||||
"forbidden_tools": "",
|
||||
"expected_files": "{}",
|
||||
"expected_response": "4",
|
||||
"judge_rubric": "Response contains the number 4",
|
||||
"judge_threshold": "7.0",
|
||||
"judge_model": "gpt-4o-mini",
|
||||
"timeout": "60",
|
||||
"retries": "0",
|
||||
"skip_if": "",
|
||||
"agents": "[]",
|
||||
"workflow": "{}",
|
||||
},
|
||||
{
|
||||
"id": "tools_01",
|
||||
"name": "Create File",
|
||||
"prompts": "Create a file called hello.py with print('hello')",
|
||||
"description": "Test file creation",
|
||||
"mode": "headless",
|
||||
"workspace_fixture": "empty",
|
||||
"expected_tools": "acp_create_file",
|
||||
"forbidden_tools": "",
|
||||
"expected_files": '{"hello.py": "print.*hello"}',
|
||||
"expected_response": "",
|
||||
"judge_rubric": "File was created successfully",
|
||||
"judge_threshold": "7.0",
|
||||
"judge_model": "gpt-4o-mini",
|
||||
"timeout": "60",
|
||||
"retries": "0",
|
||||
"skip_if": "no_openai_key",
|
||||
"agents": "[]",
|
||||
"workflow": "{}",
|
||||
},
|
||||
{
|
||||
"id": "multi_01",
|
||||
"name": "Multi-step Edit",
|
||||
"prompts": '["Create test.py with x=1", "Edit test.py to change x=1 to x=2"]',
|
||||
"description": "Test multi-step editing",
|
||||
"mode": "headless",
|
||||
"workspace_fixture": "empty",
|
||||
"expected_tools": "acp_create_file,acp_edit_file",
|
||||
"forbidden_tools": "",
|
||||
"expected_files": '{"test.py": "x.*=.*2"}',
|
||||
"expected_response": "",
|
||||
"judge_rubric": "",
|
||||
"judge_threshold": "7.0",
|
||||
"judge_model": "gpt-4o-mini",
|
||||
"timeout": "120",
|
||||
"retries": "0",
|
||||
"skip_if": "no_openai_key",
|
||||
"agents": "[]",
|
||||
"workflow": "{}",
|
||||
},
|
||||
]
|
||||
|
||||
# Build CSV string
|
||||
import io
|
||||
output = io.StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
for example in examples:
|
||||
writer.writerow(example)
|
||||
|
||||
csv_content = output.getvalue()
|
||||
|
||||
if output_path:
|
||||
Path(output_path).write_text(csv_content)
|
||||
logger.info(f"Generated CSV template at {output_path}")
|
||||
|
||||
return csv_content
|
||||
@@ -0,0 +1,450 @@
|
||||
"""
|
||||
Headless Interactive Core Executor for PraisonAI.
|
||||
|
||||
This module provides a headless execution mode that uses the SAME interactive core
|
||||
pipeline as the interactive TUI:
|
||||
- InteractiveRuntime config
|
||||
- get_interactive_tools() tool resolution
|
||||
- ActionOrchestrator behavior (plan/approve/apply/verify)
|
||||
- Tool call tracing
|
||||
|
||||
This is for tests and CI - no terminal/prompt_toolkit dependency.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCallTrace:
|
||||
"""Record of a single tool call."""
|
||||
tool_name: str
|
||||
args: Tuple
|
||||
kwargs: Dict[str, Any]
|
||||
result: Any
|
||||
success: bool
|
||||
error: Optional[str]
|
||||
duration: float
|
||||
timestamp: str
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"tool_name": self.tool_name,
|
||||
"args": _redact_sensitive(self.args),
|
||||
"kwargs": _redact_sensitive(self.kwargs),
|
||||
"result": _truncate(str(self.result), 500) if self.result else None,
|
||||
"success": self.success,
|
||||
"error": self.error,
|
||||
"duration": self.duration,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeadlessExecutionResult:
|
||||
"""Result of a headless execution."""
|
||||
success: bool
|
||||
responses: List[str]
|
||||
tool_trace: List[ToolCallTrace]
|
||||
duration: float
|
||||
error: Optional[str] = None
|
||||
transcript: List[Dict[str, str]] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"success": self.success,
|
||||
"responses": self.responses,
|
||||
"tool_trace": [t.to_dict() for t in self.tool_trace],
|
||||
"duration": self.duration,
|
||||
"error": self.error,
|
||||
"transcript": self.transcript,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentConfig:
|
||||
"""Configuration for a single agent in headless mode."""
|
||||
name: str = "HeadlessAgent"
|
||||
instructions: str = "You are a helpful assistant with file and code tools."
|
||||
role: str = "assistant"
|
||||
llm: str = "gpt-4o-mini"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"instructions": self.instructions,
|
||||
"role": self.role,
|
||||
"llm": self.llm,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeadlessConfig:
|
||||
"""Configuration for headless interactive core execution."""
|
||||
workspace: str = field(default_factory=os.getcwd)
|
||||
model: str = "gpt-4o-mini"
|
||||
approval_mode: str = "auto" # auto, manual, scoped
|
||||
enable_acp: bool = True
|
||||
enable_lsp: bool = True
|
||||
enable_basic: bool = True
|
||||
timeout: int = 60
|
||||
verbose: bool = False
|
||||
# Multi-agent support
|
||||
agents: List[AgentConfig] = field(default_factory=list)
|
||||
workflow: Optional[Dict[str, Any]] = None # Turn routing/handoffs
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.agents:
|
||||
# Default single agent
|
||||
self.agents = [AgentConfig(llm=self.model)]
|
||||
|
||||
|
||||
def _redact_sensitive(obj: Any, sensitive_keys: set = None) -> Any:
|
||||
"""Redact sensitive information from args/kwargs."""
|
||||
if sensitive_keys is None:
|
||||
sensitive_keys = {"api_key", "password", "secret", "token", "key"}
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
k: "[REDACTED]" if any(s in k.lower() for s in sensitive_keys) else _redact_sensitive(v, sensitive_keys)
|
||||
for k, v in obj.items()
|
||||
}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
return type(obj)(_redact_sensitive(item, sensitive_keys) for item in obj)
|
||||
elif isinstance(obj, str) and len(obj) > 100:
|
||||
return obj[:100] + "...[truncated]"
|
||||
return obj
|
||||
|
||||
|
||||
def _truncate(s: str, max_len: int) -> str:
|
||||
"""Truncate string to max length."""
|
||||
if len(s) <= max_len:
|
||||
return s
|
||||
return s[:max_len] + "...[truncated]"
|
||||
|
||||
|
||||
class HeadlessInteractiveCore:
|
||||
"""
|
||||
Headless executor that uses the same interactive core pipeline as TUI.
|
||||
|
||||
This class reuses:
|
||||
- InteractiveRuntime for ACP/LSP subsystems
|
||||
- get_interactive_tools() for tool resolution
|
||||
- ActionOrchestrator for plan/approve/apply/verify flow
|
||||
|
||||
Usage:
|
||||
executor = HeadlessInteractiveCore(config)
|
||||
result = executor.run(["Create a file hello.py with print('hello')"])
|
||||
executor.cleanup() # IMPORTANT: Always call cleanup to stop LSP/ACP
|
||||
|
||||
Or use as context manager:
|
||||
with HeadlessInteractiveCore(config) as executor:
|
||||
result = executor.run(["Create a file"])
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[HeadlessConfig] = None):
|
||||
self.config = config or HeadlessConfig()
|
||||
self._runtime = None
|
||||
self._tools: List[Callable] = []
|
||||
self._tool_trace: List[ToolCallTrace] = []
|
||||
self._agents: List[Any] = [] # Will hold Agent instances
|
||||
self._initialized = False
|
||||
self._cleanup_done = False
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit - ensures cleanup."""
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
def __del__(self):
|
||||
"""Destructor - attempt cleanup if not done."""
|
||||
if not self._cleanup_done:
|
||||
try:
|
||||
self.cleanup()
|
||||
except Exception:
|
||||
pass # Best effort cleanup in destructor
|
||||
|
||||
def _wrap_tool_for_trace(self, tool: Callable) -> Callable:
|
||||
"""Wrap a tool to capture calls in trace."""
|
||||
@functools.wraps(tool)
|
||||
def wrapper(*args, **kwargs):
|
||||
start = time.time()
|
||||
timestamp = datetime.utcnow().isoformat()
|
||||
try:
|
||||
result = tool(*args, **kwargs)
|
||||
trace = ToolCallTrace(
|
||||
tool_name=tool.__name__,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
result=result,
|
||||
success=True,
|
||||
error=None,
|
||||
duration=time.time() - start,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
self._tool_trace.append(trace)
|
||||
logger.debug(f"Tool {tool.__name__} succeeded in {trace.duration:.3f}s")
|
||||
return result
|
||||
except Exception as e:
|
||||
trace = ToolCallTrace(
|
||||
tool_name=tool.__name__,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
result=None,
|
||||
success=False,
|
||||
error=str(e),
|
||||
duration=time.time() - start,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
self._tool_trace.append(trace)
|
||||
logger.debug(f"Tool {tool.__name__} failed: {e}")
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
def _initialize(self) -> None:
|
||||
"""Initialize runtime and tools using interactive core path."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
logger.debug(f"Initializing headless core with workspace={self.config.workspace}")
|
||||
|
||||
# Use the SAME tool loading path as interactive TUI
|
||||
# This is from: praisonai/cli/features/interactive_tools.py
|
||||
from praisonai.cli.features.interactive_tools import (
|
||||
get_interactive_tools,
|
||||
ToolConfig,
|
||||
)
|
||||
|
||||
# Create tool config matching interactive core
|
||||
tool_config = ToolConfig(
|
||||
workspace=self.config.workspace,
|
||||
enable_acp=self.config.enable_acp,
|
||||
enable_lsp=self.config.enable_lsp,
|
||||
enable_basic=self.config.enable_basic,
|
||||
approval_mode=self.config.approval_mode,
|
||||
lsp_enabled=self.config.enable_lsp,
|
||||
acp_enabled=self.config.enable_acp,
|
||||
)
|
||||
|
||||
# Get tools using the SAME function as TUI
|
||||
raw_tools = get_interactive_tools(config=tool_config)
|
||||
|
||||
# Wrap tools for tracing
|
||||
self._tools = [self._wrap_tool_for_trace(t) for t in raw_tools]
|
||||
|
||||
logger.debug(f"Loaded {len(self._tools)} tools: {[t.__name__ for t in self._tools]}")
|
||||
|
||||
# Create agents using the SAME Agent class as TUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
for agent_config in self.config.agents:
|
||||
agent = Agent(
|
||||
name=agent_config.name,
|
||||
role=agent_config.role,
|
||||
goal="Help the user with their requests",
|
||||
instructions=agent_config.instructions,
|
||||
llm=agent_config.llm,
|
||||
tools=self._tools if self._tools else None,
|
||||
)
|
||||
self._agents.append(agent)
|
||||
logger.debug(f"Created agent: {agent_config.name}")
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def run(self, prompts: Union[str, List[str]]) -> HeadlessExecutionResult:
|
||||
"""
|
||||
Run prompts through headless interactive core.
|
||||
|
||||
Args:
|
||||
prompts: Single prompt or list of prompts for multi-step execution
|
||||
|
||||
Returns:
|
||||
HeadlessExecutionResult with responses, tool trace, and transcript
|
||||
"""
|
||||
if isinstance(prompts, str):
|
||||
prompts = [prompts]
|
||||
|
||||
start_time = time.time()
|
||||
self._tool_trace = [] # Reset trace
|
||||
responses = []
|
||||
transcript = []
|
||||
|
||||
try:
|
||||
self._initialize()
|
||||
|
||||
# Execute prompts
|
||||
for i, prompt in enumerate(prompts):
|
||||
logger.debug(f"Executing prompt {i+1}/{len(prompts)}: {prompt[:50]}...")
|
||||
transcript.append({"role": "user", "content": prompt})
|
||||
|
||||
# Determine which agent to use
|
||||
agent_idx = self._get_agent_for_prompt(i, prompt)
|
||||
agent = self._agents[agent_idx]
|
||||
|
||||
# Execute using the SAME chat method as TUI
|
||||
response = agent.chat(prompt)
|
||||
responses.append(response)
|
||||
transcript.append({"role": "assistant", "content": response})
|
||||
|
||||
logger.debug(f"Got response: {response[:100] if response else 'None'}...")
|
||||
|
||||
return HeadlessExecutionResult(
|
||||
success=True,
|
||||
responses=responses,
|
||||
tool_trace=self._tool_trace,
|
||||
duration=time.time() - start_time,
|
||||
transcript=transcript,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Headless execution failed: {e}", exc_info=True)
|
||||
return HeadlessExecutionResult(
|
||||
success=False,
|
||||
responses=responses,
|
||||
tool_trace=self._tool_trace,
|
||||
duration=time.time() - start_time,
|
||||
error=str(e),
|
||||
transcript=transcript,
|
||||
)
|
||||
|
||||
def _get_agent_for_prompt(self, prompt_idx: int, prompt: str) -> int:
|
||||
"""Determine which agent should handle a prompt (for multi-agent)."""
|
||||
if len(self._agents) == 1:
|
||||
return 0
|
||||
|
||||
# If workflow is defined, use it for routing
|
||||
if self.config.workflow:
|
||||
routing = self.config.workflow.get("routing", {})
|
||||
# Simple round-robin or keyword-based routing
|
||||
if "round_robin" in routing:
|
||||
return prompt_idx % len(self._agents)
|
||||
elif "keywords" in routing:
|
||||
for i, agent_config in enumerate(self.config.agents):
|
||||
keywords = routing["keywords"].get(agent_config.name, [])
|
||||
if any(kw.lower() in prompt.lower() for kw in keywords):
|
||||
return i
|
||||
|
||||
# Default: round-robin
|
||||
return prompt_idx % len(self._agents)
|
||||
|
||||
def get_tool_trace(self) -> List[Dict[str, Any]]:
|
||||
"""Get the tool call trace as list of dicts."""
|
||||
return [t.to_dict() for t in self._tool_trace]
|
||||
|
||||
def get_tools_called(self) -> List[str]:
|
||||
"""Get list of tool names that were called."""
|
||||
return [t.tool_name for t in self._tool_trace]
|
||||
|
||||
def clear_trace(self) -> None:
|
||||
"""Clear the tool trace."""
|
||||
self._tool_trace = []
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up resources - MUST be called when done.
|
||||
|
||||
Stops LSP and ACP subsystems that were started during initialization.
|
||||
This is critical to prevent resource leaks and orphaned processes.
|
||||
"""
|
||||
if self._cleanup_done:
|
||||
return
|
||||
|
||||
logger.debug("Cleaning up HeadlessInteractiveCore resources...")
|
||||
|
||||
# Stop the runtime if it was created
|
||||
if self._runtime is not None:
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
# Get or create event loop
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
if loop and loop.is_running():
|
||||
# Schedule the stop coroutine
|
||||
asyncio.ensure_future(self._runtime.stop())
|
||||
else:
|
||||
# Create new loop and run
|
||||
asyncio.run(self._runtime.stop())
|
||||
|
||||
logger.debug("Runtime stopped successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error stopping runtime: {e}")
|
||||
finally:
|
||||
self._runtime = None
|
||||
|
||||
# Clear agents
|
||||
self._agents = []
|
||||
self._tools = []
|
||||
self._initialized = False
|
||||
self._cleanup_done = True
|
||||
|
||||
logger.debug("HeadlessInteractiveCore cleanup complete")
|
||||
|
||||
|
||||
def run_headless(
|
||||
prompts: Union[str, List[str]],
|
||||
workspace: Optional[str] = None,
|
||||
model: str = "gpt-4o-mini",
|
||||
approval_mode: str = "auto",
|
||||
timeout: int = 60,
|
||||
agents: Optional[List[Dict[str, Any]]] = None,
|
||||
workflow: Optional[Dict[str, Any]] = None,
|
||||
) -> HeadlessExecutionResult:
|
||||
"""
|
||||
Convenience function to run prompts through headless interactive core.
|
||||
|
||||
Args:
|
||||
prompts: Single prompt or list of prompts
|
||||
workspace: Working directory (default: cwd)
|
||||
model: LLM model to use
|
||||
approval_mode: Approval mode (auto, manual, scoped)
|
||||
timeout: Timeout in seconds
|
||||
agents: List of agent configs for multi-agent
|
||||
workflow: Workflow config for turn routing
|
||||
|
||||
Returns:
|
||||
HeadlessExecutionResult
|
||||
|
||||
Example:
|
||||
result = run_headless("Create a file hello.py with print('hello')")
|
||||
print(result.responses[0])
|
||||
print(result.get_tools_called())
|
||||
"""
|
||||
agent_configs = []
|
||||
if agents:
|
||||
for a in agents:
|
||||
agent_configs.append(AgentConfig(
|
||||
name=a.get("name", "Agent"),
|
||||
instructions=a.get("instructions", "You are a helpful assistant."),
|
||||
role=a.get("role", "assistant"),
|
||||
llm=a.get("llm", model),
|
||||
))
|
||||
|
||||
config = HeadlessConfig(
|
||||
workspace=workspace or os.getcwd(),
|
||||
model=model,
|
||||
approval_mode=approval_mode,
|
||||
timeout=timeout,
|
||||
agents=agent_configs if agent_configs else None,
|
||||
workflow=workflow,
|
||||
)
|
||||
|
||||
# Use context manager to ensure cleanup
|
||||
with HeadlessInteractiveCore(config) as executor:
|
||||
return executor.run(prompts)
|
||||
@@ -40,7 +40,7 @@ class RuntimeConfig:
|
||||
workspace: str = "."
|
||||
lsp_enabled: bool = True
|
||||
acp_enabled: bool = True
|
||||
approval_mode: str = "manual" # manual, auto, scoped
|
||||
approval_mode: str = "auto" # auto (full privileges), manual, scoped
|
||||
trace_enabled: bool = False
|
||||
trace_file: Optional[str] = None
|
||||
json_output: bool = False
|
||||
@@ -146,15 +146,21 @@ class InteractiveRuntime:
|
||||
def read_only(self) -> bool:
|
||||
"""Check if runtime is in read-only mode.
|
||||
|
||||
Note: When PRAISON_APPROVAL_MODE=auto, we allow writes even without ACP
|
||||
to support non-interactive automation and testing scenarios.
|
||||
Note: When approval_mode=auto (via config or env), we allow writes even
|
||||
without ACP to support non-interactive automation and testing scenarios.
|
||||
"""
|
||||
# Auto-approval mode bypasses read-only for automation
|
||||
import os as _os
|
||||
approval_mode = _os.environ.get("PRAISON_APPROVAL_MODE", "").lower()
|
||||
if approval_mode == "auto":
|
||||
# Check config approval_mode first (takes precedence)
|
||||
if self.config.approval_mode == "auto":
|
||||
return False
|
||||
return self._read_only or not self.acp_ready
|
||||
|
||||
# Then check environment variable as fallback
|
||||
import os as _os
|
||||
env_approval_mode = _os.environ.get("PRAISON_APPROVAL_MODE", "").lower()
|
||||
if env_approval_mode == "auto":
|
||||
return False
|
||||
|
||||
# Only enforce read-only if explicitly set or ACP not ready
|
||||
return self._read_only
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get runtime status."""
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
Interactive Test Harness for PraisonAI.
|
||||
|
||||
Provides workspace isolation, fixtures, tool trace wrappers, and artifact writing
|
||||
for testing interactive mode through the headless core executor.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Blocked commands for safe execution
|
||||
BLOCKED_COMMANDS = {
|
||||
"rm -rf /",
|
||||
"rm -rf /*",
|
||||
"sudo rm",
|
||||
"mkfs",
|
||||
"dd if=",
|
||||
":(){:|:&};:", # Fork bomb
|
||||
"chmod -R 777 /",
|
||||
"curl | sh",
|
||||
"wget | sh",
|
||||
}
|
||||
|
||||
# Allowed commands (whitelist for strict mode)
|
||||
SAFE_COMMANDS = {
|
||||
"echo",
|
||||
"cat",
|
||||
"ls",
|
||||
"pwd",
|
||||
"head",
|
||||
"tail",
|
||||
"wc",
|
||||
"grep",
|
||||
"find",
|
||||
"python",
|
||||
"python3",
|
||||
"pip",
|
||||
"pip3",
|
||||
"node",
|
||||
"npm",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkspaceFixture:
|
||||
"""Definition of a workspace fixture."""
|
||||
name: str
|
||||
files: Dict[str, str] = field(default_factory=dict) # path -> content
|
||||
directories: List[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> "WorkspaceFixture":
|
||||
"""Create an empty workspace fixture."""
|
||||
return cls(name="empty")
|
||||
|
||||
@classmethod
|
||||
def seeded(cls) -> "WorkspaceFixture":
|
||||
"""Create a seeded workspace with common files."""
|
||||
return cls(
|
||||
name="seeded",
|
||||
files={
|
||||
"README.md": "# Test Project\n\nThis is a test project.",
|
||||
"main.py": "# Main entry point\nprint('Hello, World!')\n",
|
||||
"config.json": '{"name": "test", "version": "1.0.0"}',
|
||||
},
|
||||
directories=["src", "tests"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def git(cls) -> "WorkspaceFixture":
|
||||
"""Create a git-initialized workspace."""
|
||||
fixture = cls.seeded()
|
||||
fixture.name = "git"
|
||||
fixture.files[".gitignore"] = "__pycache__/\n*.pyc\n.env\n"
|
||||
return fixture
|
||||
|
||||
@classmethod
|
||||
def python_project(cls) -> "WorkspaceFixture":
|
||||
"""Create a Python project workspace."""
|
||||
return cls(
|
||||
name="python_project",
|
||||
files={
|
||||
"README.md": "# Python Project\n",
|
||||
"main.py": '''"""Main module."""
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
print("Hello from main!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
''',
|
||||
"utils.py": '''"""Utility functions."""
|
||||
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
def multiply(a: int, b: int) -> int:
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
class Calculator:
|
||||
"""Simple calculator class."""
|
||||
|
||||
def __init__(self):
|
||||
self.result = 0
|
||||
|
||||
def add(self, x: int) -> int:
|
||||
self.result += x
|
||||
return self.result
|
||||
''',
|
||||
"requirements.txt": "# No dependencies\n",
|
||||
},
|
||||
directories=["tests"],
|
||||
)
|
||||
|
||||
|
||||
BUILTIN_FIXTURES = {
|
||||
"empty": WorkspaceFixture.empty,
|
||||
"seeded": WorkspaceFixture.seeded,
|
||||
"git": WorkspaceFixture.git,
|
||||
"python_project": WorkspaceFixture.python_project,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestArtifacts:
|
||||
"""Container for test artifacts."""
|
||||
transcript: List[Dict[str, str]] = field(default_factory=list)
|
||||
tool_trace: List[Dict[str, Any]] = field(default_factory=list)
|
||||
result: Dict[str, Any] = field(default_factory=dict)
|
||||
workspace_snapshot: Optional[Dict[str, str]] = None
|
||||
judge_result: Optional[Dict[str, Any]] = None
|
||||
|
||||
def save(self, artifacts_dir: Path) -> None:
|
||||
"""Save artifacts to directory."""
|
||||
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save transcript
|
||||
transcript_path = artifacts_dir / "transcript.txt"
|
||||
with open(transcript_path, "w") as f:
|
||||
for entry in self.transcript:
|
||||
role = entry.get("role", "unknown")
|
||||
content = entry.get("content", "")
|
||||
f.write(f"[{role.upper()}]\n{content}\n\n")
|
||||
|
||||
# Save tool trace as JSONL
|
||||
trace_path = artifacts_dir / "tool_trace.jsonl"
|
||||
with open(trace_path, "w") as f:
|
||||
for trace in self.tool_trace:
|
||||
f.write(json.dumps(trace) + "\n")
|
||||
|
||||
# Save result
|
||||
result_path = artifacts_dir / "result.json"
|
||||
with open(result_path, "w") as f:
|
||||
json.dump(self.result, f, indent=2, default=str)
|
||||
|
||||
# Save workspace snapshot if present
|
||||
if self.workspace_snapshot:
|
||||
snapshot_dir = artifacts_dir / "workspace"
|
||||
snapshot_dir.mkdir(exist_ok=True)
|
||||
for rel_path, content in self.workspace_snapshot.items():
|
||||
file_path = snapshot_dir / rel_path
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
|
||||
# Save judge result if present
|
||||
if self.judge_result:
|
||||
judge_path = artifacts_dir / "judge_result.json"
|
||||
with open(judge_path, "w") as f:
|
||||
json.dump(self.judge_result, f, indent=2, default=str)
|
||||
|
||||
logger.debug(f"Saved artifacts to {artifacts_dir}")
|
||||
|
||||
|
||||
class InteractiveTestHarness:
|
||||
"""
|
||||
Test harness for interactive mode testing.
|
||||
|
||||
Provides:
|
||||
- Workspace isolation (temp directory)
|
||||
- Fixture setup
|
||||
- Tool trace capture
|
||||
- Artifact generation
|
||||
- File assertions
|
||||
- Tool call assertions
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: Optional[Path] = None,
|
||||
artifacts_dir: Optional[Path] = None,
|
||||
keep_workspace: bool = False,
|
||||
safe_commands: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize test harness.
|
||||
|
||||
Args:
|
||||
workspace: Workspace directory (created if None)
|
||||
artifacts_dir: Directory for artifacts (created if None)
|
||||
keep_workspace: Keep workspace after cleanup
|
||||
safe_commands: Enforce safe command execution
|
||||
"""
|
||||
self._workspace_created = workspace is None
|
||||
self.workspace = workspace or Path(tempfile.mkdtemp(prefix="praison_test_"))
|
||||
self.artifacts_dir = artifacts_dir or self.workspace / "artifacts"
|
||||
self.keep_workspace = keep_workspace
|
||||
self.safe_commands = safe_commands
|
||||
self.artifacts = TestArtifacts()
|
||||
self._executor = None
|
||||
|
||||
def setup_workspace(self, fixture: str = "empty") -> None:
|
||||
"""
|
||||
Set up workspace with fixture.
|
||||
|
||||
Args:
|
||||
fixture: Fixture name (empty, seeded, git, python_project)
|
||||
"""
|
||||
# Get fixture
|
||||
if fixture in BUILTIN_FIXTURES:
|
||||
fixture_obj = BUILTIN_FIXTURES[fixture]()
|
||||
else:
|
||||
logger.warning(f"Unknown fixture '{fixture}', using empty")
|
||||
fixture_obj = WorkspaceFixture.empty()
|
||||
|
||||
# Create directories
|
||||
self.workspace.mkdir(parents=True, exist_ok=True)
|
||||
for dir_path in fixture_obj.directories:
|
||||
(self.workspace / dir_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create files
|
||||
for rel_path, content in fixture_obj.files.items():
|
||||
file_path = self.workspace / rel_path
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
|
||||
# Initialize git if needed
|
||||
if fixture == "git":
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "init"],
|
||||
cwd=self.workspace,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Git init failed: {e}")
|
||||
|
||||
logger.debug(f"Set up workspace with fixture '{fixture}' at {self.workspace}")
|
||||
|
||||
def create_executor(
|
||||
self,
|
||||
model: str = "gpt-4o-mini",
|
||||
approval_mode: str = "auto",
|
||||
agents: Optional[List[Dict[str, Any]]] = None,
|
||||
workflow: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Create headless executor using interactive core.
|
||||
|
||||
Args:
|
||||
model: LLM model
|
||||
approval_mode: Approval mode (auto, manual, scoped)
|
||||
agents: Multi-agent configs
|
||||
workflow: Workflow routing config
|
||||
|
||||
Returns:
|
||||
HeadlessInteractiveCore executor
|
||||
"""
|
||||
from .interactive_core_headless import (
|
||||
HeadlessConfig,
|
||||
HeadlessInteractiveCore,
|
||||
AgentConfig,
|
||||
)
|
||||
|
||||
agent_configs = []
|
||||
if agents:
|
||||
for a in agents:
|
||||
agent_configs.append(AgentConfig(
|
||||
name=a.get("name", "Agent"),
|
||||
instructions=a.get("instructions", "You are a helpful assistant."),
|
||||
role=a.get("role", "assistant"),
|
||||
llm=a.get("llm", model),
|
||||
))
|
||||
|
||||
config = HeadlessConfig(
|
||||
workspace=str(self.workspace),
|
||||
model=model,
|
||||
approval_mode=approval_mode,
|
||||
agents=agent_configs if agent_configs else None,
|
||||
workflow=workflow,
|
||||
)
|
||||
|
||||
self._executor = HeadlessInteractiveCore(config)
|
||||
return self._executor
|
||||
|
||||
def run(
|
||||
self,
|
||||
prompts: List[str],
|
||||
model: str = "gpt-4o-mini",
|
||||
approval_mode: str = "auto",
|
||||
agents: Optional[List[Dict[str, Any]]] = None,
|
||||
workflow: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Run prompts through headless interactive core.
|
||||
|
||||
Args:
|
||||
prompts: List of prompts to execute
|
||||
model: LLM model
|
||||
approval_mode: Approval mode
|
||||
agents: Multi-agent configs
|
||||
workflow: Workflow routing
|
||||
|
||||
Returns:
|
||||
HeadlessExecutionResult
|
||||
"""
|
||||
if not self._executor:
|
||||
self.create_executor(model, approval_mode, agents, workflow)
|
||||
|
||||
result = self._executor.run(prompts)
|
||||
|
||||
# Store in artifacts
|
||||
self.artifacts.transcript = result.transcript
|
||||
self.artifacts.tool_trace = self._executor.get_tool_trace()
|
||||
self.artifacts.result = result.to_dict()
|
||||
|
||||
return result
|
||||
|
||||
def verify_files(self, expected_files: Dict[str, str]) -> Dict[str, bool]:
|
||||
"""
|
||||
Verify expected files exist and match content patterns.
|
||||
|
||||
Args:
|
||||
expected_files: Dict of {relative_path: content_regex}
|
||||
|
||||
Returns:
|
||||
Dict of {path: passed}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for rel_path, content_pattern in expected_files.items():
|
||||
file_path = self.workspace / rel_path
|
||||
|
||||
if not file_path.exists():
|
||||
logger.debug(f"File not found: {rel_path}")
|
||||
results[rel_path] = False
|
||||
continue
|
||||
|
||||
content = file_path.read_text()
|
||||
|
||||
if content_pattern:
|
||||
try:
|
||||
if re.search(content_pattern, content, re.MULTILINE | re.DOTALL):
|
||||
results[rel_path] = True
|
||||
else:
|
||||
logger.debug(f"Content mismatch for {rel_path}: pattern '{content_pattern}' not found")
|
||||
results[rel_path] = False
|
||||
except re.error as e:
|
||||
logger.warning(f"Invalid regex pattern for {rel_path}: {e}")
|
||||
results[rel_path] = False
|
||||
else:
|
||||
# Just check existence
|
||||
results[rel_path] = True
|
||||
|
||||
return results
|
||||
|
||||
def verify_tool_calls(
|
||||
self,
|
||||
expected_tools: Optional[List[str]] = None,
|
||||
forbidden_tools: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify tool calls match expectations.
|
||||
|
||||
Args:
|
||||
expected_tools: Tools that MUST have been called
|
||||
forbidden_tools: Tools that MUST NOT have been called
|
||||
|
||||
Returns:
|
||||
Dict with verification results
|
||||
"""
|
||||
called_tools = set()
|
||||
if self._executor:
|
||||
called_tools = set(self._executor.get_tools_called())
|
||||
|
||||
results = {
|
||||
"called_tools": list(called_tools),
|
||||
"expected_passed": True,
|
||||
"forbidden_passed": True,
|
||||
"missing_tools": [],
|
||||
"forbidden_called": [],
|
||||
}
|
||||
|
||||
# Check expected tools
|
||||
if expected_tools:
|
||||
expected_set = set(expected_tools)
|
||||
missing = expected_set - called_tools
|
||||
if missing:
|
||||
results["expected_passed"] = False
|
||||
results["missing_tools"] = list(missing)
|
||||
|
||||
# Check forbidden tools
|
||||
if forbidden_tools:
|
||||
forbidden_set = set(forbidden_tools)
|
||||
forbidden_called = forbidden_set & called_tools
|
||||
if forbidden_called:
|
||||
results["forbidden_passed"] = False
|
||||
results["forbidden_called"] = list(forbidden_called)
|
||||
|
||||
results["passed"] = results["expected_passed"] and results["forbidden_passed"]
|
||||
return results
|
||||
|
||||
def verify_response(self, response: str, pattern: str) -> bool:
|
||||
"""
|
||||
Verify response matches pattern.
|
||||
|
||||
Args:
|
||||
response: Response text
|
||||
pattern: Regex pattern
|
||||
|
||||
Returns:
|
||||
True if matches
|
||||
"""
|
||||
if not pattern:
|
||||
return True
|
||||
|
||||
try:
|
||||
return bool(re.search(pattern, response, re.MULTILINE | re.DOTALL | re.IGNORECASE))
|
||||
except re.error as e:
|
||||
logger.warning(f"Invalid response pattern: {e}")
|
||||
return False
|
||||
|
||||
def snapshot_workspace(self) -> Dict[str, str]:
|
||||
"""
|
||||
Create snapshot of workspace files.
|
||||
|
||||
Returns:
|
||||
Dict of {relative_path: content}
|
||||
"""
|
||||
snapshot = {}
|
||||
|
||||
for file_path in self.workspace.rglob("*"):
|
||||
if file_path.is_file():
|
||||
rel_path = str(file_path.relative_to(self.workspace))
|
||||
# Skip binary files and large files
|
||||
if file_path.suffix in {".pyc", ".so", ".dll", ".exe", ".bin"}:
|
||||
continue
|
||||
if file_path.stat().st_size > 100_000: # 100KB limit
|
||||
continue
|
||||
try:
|
||||
snapshot[rel_path] = file_path.read_text()
|
||||
except Exception:
|
||||
pass # Skip unreadable files
|
||||
|
||||
self.artifacts.workspace_snapshot = snapshot
|
||||
return snapshot
|
||||
|
||||
def save_artifacts(self, test_id: str = "test") -> Path:
|
||||
"""
|
||||
Save all artifacts to directory.
|
||||
|
||||
Args:
|
||||
test_id: Test identifier for subdirectory
|
||||
|
||||
Returns:
|
||||
Path to artifacts directory
|
||||
"""
|
||||
test_artifacts_dir = self.artifacts_dir / test_id
|
||||
self.artifacts.save(test_artifacts_dir)
|
||||
return test_artifacts_dir
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up workspace and executor resources."""
|
||||
# Clean up executor (stops LSP/ACP subsystems)
|
||||
if self._executor is not None:
|
||||
try:
|
||||
self._executor.cleanup()
|
||||
logger.debug("Cleaned up executor resources")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clean up executor: {e}")
|
||||
finally:
|
||||
self._executor = None
|
||||
|
||||
# Clean up workspace
|
||||
if self._workspace_created and not self.keep_workspace:
|
||||
try:
|
||||
shutil.rmtree(self.workspace)
|
||||
logger.debug(f"Cleaned up workspace: {self.workspace}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clean up workspace: {e}")
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
|
||||
def create_test_harness(
|
||||
fixture: str = "empty",
|
||||
workspace: Optional[Path] = None,
|
||||
artifacts_dir: Optional[Path] = None,
|
||||
keep_workspace: bool = False,
|
||||
) -> InteractiveTestHarness:
|
||||
"""
|
||||
Create and set up a test harness.
|
||||
|
||||
Args:
|
||||
fixture: Workspace fixture name
|
||||
workspace: Workspace path (temp if None)
|
||||
artifacts_dir: Artifacts directory
|
||||
keep_workspace: Keep workspace after cleanup
|
||||
|
||||
Returns:
|
||||
Configured InteractiveTestHarness
|
||||
"""
|
||||
harness = InteractiveTestHarness(
|
||||
workspace=workspace,
|
||||
artifacts_dir=artifacts_dir,
|
||||
keep_workspace=keep_workspace,
|
||||
)
|
||||
harness.setup_workspace(fixture)
|
||||
return harness
|
||||
@@ -72,7 +72,7 @@ class ToolConfig:
|
||||
enable_acp: bool = True
|
||||
enable_lsp: bool = True
|
||||
enable_basic: bool = True
|
||||
approval_mode: str = "auto" # auto, manual, scoped
|
||||
approval_mode: str = "auto" # auto (full privileges), manual, scoped
|
||||
lsp_enabled: bool = True
|
||||
acp_enabled: bool = True
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user