Compare commits

...
10 Commits
Author SHA1 Message Date
Mervin Praison f33bf57662 Release v3.7.4 2026-01-10 21:06:16 +00:00
github-actions[bot] df21c419ea docs: auto-update api.md [skip ci] 2026-01-10 19:45:30 +00:00
Mervin Praison fe997d525c ci: add workflow to auto-update api.md on main branch push 2026-01-10 19:45:04 +00:00
Mervin Praison ca26988774 Release v3.7.3 2026-01-10 19:43:43 +00:00
Mervin Praison 3600594316 Before AI example generation 2026-01-10 16:54:16 +00:00
Mervin Praison 097cd42890 Before testing 2026-01-10 16:04:21 +00:00
Mervin Praison 595add2991 Before testing AI generation 2026-01-10 15:55:26 +00:00
Mervin Praison 03a3aa420b Release v3.7.2 2026-01-10 15:31:05 +00:00
Mervin Praison 9533abd8a4 Release v3.7.1 2026-01-10 14:29:43 +00:00
Mervin Praison 39c9e50875 Release v3.7.0 2026-01-10 13:09:09 +00:00
102 changed files with 14511 additions and 100 deletions

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
name: Update API Reference
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai/praisonai/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- '.github/workflows/update-api-md.yml'
permissions:
contents: write
jobs:
update-api-md:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
persist-credentials: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Generate api.md
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_api_md.py
- name: Check for changes
id: check_changes
run: |
if git diff --quiet api.md; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to api.md"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "api.md has been updated"
git diff api.md | head -50
fi
- name: Commit and push changes
if: steps.check_changes.outputs.changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update api.md [skip ci]"
file_pattern: 'api.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
+867
View File
@@ -0,0 +1,867 @@
# 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">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">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 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 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 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 test <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 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 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 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 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.
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.6.2" \
"praisonai>=3.7.4" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.6.2" \
"praisonai>=3.7.4" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.6.2" \
"praisonai>=3.7.4" \
"praisonai[ui]" \
"praisonai[crewai]"
+96
View File
@@ -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)
+18
View File
@@ -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)
+63
View File
@@ -0,0 +1,63 @@
"""
Streaming Example with Profiling
Key Points:
1. output="stream" preset enables streaming by default
2. stream=True in start() is redundant when using output="stream" (but doesn't hurt)
3. Profiler.enable() MUST be called before profiling works
4. Use Profiler.streaming() context manager for TTFT metrics
"""
from praisonai.profiler import Profiler, profile, profile_imports
# IMPORTANT: Enable profiler BEFORE any profiling operations
Profiler.enable()
# Profile imports
with profile_imports():
from praisonaiagents import Agent
@profile
def main():
agent = Agent(
instructions="You are a helpful assistant",
output="stream" # This already enables streaming
)
print("\nStreaming response:")
print("-" * 40)
# Use Profiler.streaming() to track TTFT (Time To First Token)
with Profiler.streaming("llm_stream") as tracker:
first_chunk = True
for chunk in agent.start("Write a short paragraph about the history of computing"):
if first_chunk:
tracker.first_token() # Mark TTFT
first_chunk = False
tracker.chunk() # Count chunks
print(chunk, end="", flush=True)
print("\n" + "-" * 40)
# Run with profiling blocks
with Profiler.block("full_workflow"):
with Profiler.block("setup"):
pass
with Profiler.block("execution"):
result = main()
with Profiler.block("cleanup"):
pass
# Generate report
Profiler.report()
# Show streaming-specific stats
streaming_records = Profiler.get_streaming_records()
if streaming_records:
print("\nStreaming Metrics:")
for record in streaming_records:
print(f" {record.name}: TTFT={record.ttft_ms:.2f}ms, Total={record.total_ms:.2f}ms, Chunks={record.chunk_count}")
stats = Profiler.get_statistics()
print(f"\nMedian execution time: {stats['p50']:.2f}ms")
Whitespace-only changes.
@@ -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,6 @@ __all__ = [
# Parse Utilities
'is_policy_string',
'parse_policy_string',
# Performance utilities
'warmup',
]
@@ -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,
@@ -783,13 +784,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:
@@ -957,9 +975,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
@@ -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
@@ -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
@@ -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:
@@ -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
@@ -76,6 +76,14 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
"metrics": False,
"reasoning_steps": False,
},
# Streaming preset - enables streaming by default
"stream": {
"verbose": True,
"markdown": True,
"stream": True,
"metrics": False,
"reasoning_steps": False,
},
}
@@ -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)
@@ -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,74 @@ 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
# 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()
# 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 +711,54 @@ 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 _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:
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 +777,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 +793,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 +812,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 +832,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 +920,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
@@ -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
@@ -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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.10.10"
version = "0.11.2"
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}")
-11
View File
@@ -1,11 +0,0 @@
from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant",
llm="gpt-4o-mini"
)
# Use chat method with stream=True for streaming responses
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
print(chunk, end="", flush=True)
print() # Final newline
+12 -8
View File
@@ -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,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 @@
"""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"
Whitespace-only changes.
Whitespace-only changes.
View File
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.10.10"
version = "0.11.2"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -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)
+2 -2
View File
@@ -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.7.4.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.7.4.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+17
View File
@@ -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}")
+981
View File
@@ -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()
+109
View File
@@ -266,6 +266,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()
+58 -5
View File
@@ -93,8 +93,45 @@ 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",
help="Output preset. Use --output=verbose, --output=minimal, --output=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)"),
# 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.
@@ -119,6 +156,18 @@ def chat_main(
if workspace:
os.environ["PRAISONAI_WORKSPACE"] = workspace
# Configure UI backend
from praisonai.cli.ui import select_backend, UIConfig
ui_config = UIConfig(
ui_backend=ui_backend,
json_output=json_output,
no_color=no_color,
theme=theme,
compact=compact,
verbose=verbose,
)
backend = select_backend(ui_config)
# Handle profiling for single prompt mode
if prompt and (profile or profile_deep):
_run_profiled_chat(
@@ -156,17 +205,22 @@ def chat_main(
core = InteractiveCore(config=config)
if prompt:
# Single prompt mode
# Single prompt mode - use backend for output
from praisonai.cli.ui.events import UIEventType
async def run_prompt():
if continue_session:
core.continue_session()
backend.emit(UIEventType.MESSAGE_START, {})
response = await core.prompt(prompt)
print(response)
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': response})
backend.emit(UIEventType.MESSAGE_END, {})
asyncio.run(run_prompt())
else:
# Interactive REPL mode
frontend = RichFrontend(core=core, config=config)
# Interactive REPL mode - pass ui_config to frontend
frontend = RichFrontend(core=core, config=config, ui_config=ui_config)
asyncio.run(frontend.run())
except ImportError:
@@ -221,7 +275,6 @@ def _run_profiled_chat(
"name": "ChatAgent",
"role": "Assistant",
"goal": "Help the user",
"verbose": verbose,
}
if model:
agent_config["llm"] = model
@@ -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)
@@ -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())
@@ -31,14 +31,21 @@ class RichFrontend:
- `praisonai chat`
"""
def __init__(self, core: Optional[InteractiveCore] = None, config: Optional[InteractiveConfig] = None):
def __init__(
self,
core: Optional[InteractiveCore] = None,
config: Optional[InteractiveConfig] = None,
ui_config: Optional['UIConfig'] = None,
):
"""Initialize the Rich frontend.
Args:
core: InteractiveCore instance. If None, creates one.
config: Configuration. Used if core is None.
ui_config: UI backend configuration for rendering options.
"""
self.core = core or InteractiveCore(config=config)
self._ui_config = ui_config
self._running = False
self._current_response = ""
self._pending_approval: Optional[ApprovalRequest] = None
+2
View File
@@ -34,6 +34,8 @@ TYPER_COMMANDS = {
'memory', 'workflow', 'tools', 'knowledge', 'deploy', 'agents', 'skills',
'eval', 'templates', 'recipe', 'todo', 'docs', 'commit', 'hooks', 'rules',
'registry', 'package', 'endpoints',
# Standardisation commands
'standardise', 'standardize',
}
+162
View File
@@ -0,0 +1,162 @@
"""
UI Backend system for PraisonAI CLI.
Provides multiple rendering backends for interactive mode:
- PlainBackend: No dependencies, works everywhere
- RichBackend: Rich-based rendering (current default)
- MiddleGroundBackend: Enhanced Rich + prompt_toolkit (new default when deps available)
All backends implement the UIBackend protocol and are selected automatically
based on environment, or explicitly via --ui flag.
"""
import os
import sys
from typing import Optional
__all__ = [
'UIConfig',
'UIEventType',
'select_backend',
'PlainBackend',
'RichBackend',
'MiddleGroundBackend',
]
_lazy_cache = {}
def __getattr__(name: str):
"""Lazy load UI components."""
if name in _lazy_cache:
return _lazy_cache[name]
if name == 'UIConfig':
from .config import UIConfig
_lazy_cache[name] = UIConfig
return UIConfig
if name == 'UIEventType':
from .events import UIEventType
_lazy_cache[name] = UIEventType
return UIEventType
if name == 'PlainBackend':
from .plain import PlainBackend
_lazy_cache[name] = PlainBackend
return PlainBackend
if name == 'RichBackend':
from .rich_backend import RichBackend
_lazy_cache[name] = RichBackend
return RichBackend
if name == 'MiddleGroundBackend':
from .mg_backend import MiddleGroundBackend
_lazy_cache[name] = MiddleGroundBackend
return MiddleGroundBackend
if name == 'select_backend':
from .selector import select_backend
_lazy_cache[name] = select_backend
return select_backend
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def select_backend(config: Optional['UIConfig'] = None):
"""
Select the appropriate UI backend based on config and environment.
Selection priority:
1. Explicit config.ui_backend override
2. PRAISONAI_UI_SAFE=1 → PlainBackend
3. config.json_output=True → PlainBackend
4. Non-TTY stdout → PlainBackend
5. Auto-detect based on available deps:
- MiddleGroundBackend if rich + prompt_toolkit available
- RichBackend if rich available
- PlainBackend otherwise
Args:
config: UIConfig instance with settings
Returns:
UIBackend instance
"""
from .config import UIConfig
from .plain import PlainBackend
if config is None:
config = UIConfig()
# Explicit override
if config.ui_backend and config.ui_backend != 'auto':
return _get_backend_by_name(config.ui_backend, config)
# Environment override for safe mode
if os.environ.get('PRAISONAI_UI_SAFE') == '1':
return PlainBackend(config)
# JSON output forces plain
if config.json_output:
return PlainBackend(config)
# Non-TTY forces plain
if not sys.stdout.isatty():
return PlainBackend(config)
# Auto-detect based on available deps
return _auto_select_backend(config)
def _get_backend_by_name(name: str, config: 'UIConfig'):
"""Get backend by name."""
from .plain import PlainBackend
if name == 'plain':
return PlainBackend(config)
if name == 'rich':
try:
from .rich_backend import RichBackend
return RichBackend(config)
except ImportError:
return PlainBackend(config)
if name == 'mg':
try:
from .mg_backend import MiddleGroundBackend
return MiddleGroundBackend(config)
except ImportError:
# Fallback to rich, then plain
try:
from .rich_backend import RichBackend
return RichBackend(config)
except ImportError:
return PlainBackend(config)
# Unknown backend, use plain
return PlainBackend(config)
def _auto_select_backend(config: 'UIConfig'):
"""Auto-select best available backend."""
from .plain import PlainBackend
# Try MiddleGroundBackend first (requires rich + prompt_toolkit)
try:
from .mg_backend import MiddleGroundBackend
return MiddleGroundBackend(config)
except ImportError:
pass
# Try RichBackend (requires rich)
try:
from .rich_backend import RichBackend
return RichBackend(config)
except ImportError:
pass
# Fallback to PlainBackend
return PlainBackend(config)
+46
View File
@@ -0,0 +1,46 @@
"""
UI Configuration for PraisonAI CLI.
Provides unified configuration for all UI backends.
"""
from dataclasses import dataclass, field
from typing import Dict, Any
@dataclass
class UIConfig:
"""Configuration for UI backends."""
# Backend selection
ui_backend: str = 'auto' # auto, plain, rich, mg
# Output modes
json_output: bool = False
compact: bool = False
verbose: bool = False
# Styling
no_color: bool = False
no_unicode: bool = False
theme: str = 'default'
# Behavior
no_fullscreen: bool = False
# Additional settings
extra: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_cli_args(cls, **kwargs) -> 'UIConfig':
"""Create UIConfig from CLI arguments."""
return cls(
ui_backend=kwargs.get('ui', 'auto'),
json_output=kwargs.get('json', False),
compact=kwargs.get('compact', False),
verbose=kwargs.get('verbose', False),
no_color=kwargs.get('no_color', False),
no_unicode=kwargs.get('no_unicode', False),
theme=kwargs.get('theme', 'default'),
no_fullscreen=kwargs.get('no_fullscreen', False),
)
+61
View File
@@ -0,0 +1,61 @@
"""
UI Event types for backend communication.
Events flow from InteractiveCore to UI backends for rendering.
"""
from enum import Enum
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
class UIEventType(Enum):
"""Event types emitted by InteractiveCore to UI backends."""
# Message lifecycle
MESSAGE_START = "message.start"
MESSAGE_CHUNK = "message.chunk"
MESSAGE_END = "message.end"
# Tool lifecycle
TOOL_START = "tool.start"
TOOL_PROGRESS = "tool.progress"
TOOL_END = "tool.end"
# Approval flow
APPROVAL_REQUEST = "approval.request"
APPROVAL_RESPONSE = "approval.response"
# Status updates
STATUS_UPDATE = "status.update"
STATUS_CLEAR = "status.clear"
# Errors
ERROR = "error"
WARNING = "warning"
# Session events
SESSION_START = "session.start"
SESSION_END = "session.end"
# Agent events (multi-agent)
AGENT_SWITCH = "agent.switch"
# Input events
INPUT_REQUEST = "input.request"
INPUT_RECEIVED = "input.received"
@dataclass
class UIEvent:
"""A UI event with type and data payload."""
event_type: UIEventType
data: Dict[str, Any] = field(default_factory=dict)
agent_name: Optional[str] = None
timestamp: Optional[float] = None
def __post_init__(self):
if self.timestamp is None:
import time
self.timestamp = time.time()
@@ -0,0 +1,342 @@
"""
MiddleGroundBackend - Enhanced Rich + prompt_toolkit UI backend.
The "middle ground" between plain text and full TUI:
- Rich for formatted output (panels, markdown, syntax highlighting)
- prompt_toolkit for advanced input (history, completion, key bindings)
- Streaming markdown with no-flicker approach (stable lines + live window)
- Terminal cleanup on exit/crash
Key design: Uses Aider-style streaming where stable lines go to console
above a small Live window, preserving terminal scrollback.
"""
import sys
import time
import atexit
import signal
from typing import Dict, Any, Optional, List, TYPE_CHECKING
if TYPE_CHECKING:
from .config import UIConfig
from .events import UIEventType
try:
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.live import Live
from rich.text import Text
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory
PROMPT_TOOLKIT_AVAILABLE = True
except ImportError:
PROMPT_TOOLKIT_AVAILABLE = False
class MarkdownStreamer:
"""Streaming markdown renderer with no-flicker approach.
Splits output into stable lines (printed above) and unstable lines
(in Live window). This preserves terminal scrollback while allowing
smooth streaming updates.
"""
def __init__(self, console: 'Console', live_window: int = 6):
"""Initialize streamer.
Args:
console: Rich Console instance
live_window: Number of lines to keep in live area
"""
self._console = console
self._live_window = live_window
self._live: Optional[Live] = None
self._printed_lines: List[str] = []
self._current_text = ""
self._min_delay = 1.0 / 20 # 20 FPS max
self._last_update = 0.0
def start(self) -> None:
"""Start the streaming display."""
self._live = Live(Text(""), console=self._console, refresh_per_second=20)
self._live.start()
self._printed_lines = []
self._current_text = ""
def update(self, text: str, final: bool = False) -> None:
"""Update the displayed content.
Args:
text: Full markdown text so far
final: If True, this is the final update
"""
if not self._live:
return
now = time.time()
if not final and now - self._last_update < self._min_delay:
return
self._last_update = now
self._current_text = text
lines = text.splitlines(keepends=True)
num_lines = len(lines)
# How many lines are stable (not in live window)?
stable_count = num_lines - self._live_window if not final else num_lines
if stable_count > len(self._printed_lines):
# Print new stable lines above live area
new_lines = lines[len(self._printed_lines):stable_count]
for line in new_lines:
self._console.print(line.rstrip('\n'))
self._printed_lines = lines[:stable_count]
if final:
self._live.update(Text(""))
self._live.stop()
self._live = None
else:
# Update live window with remaining lines
rest = lines[stable_count:]
rest_text = "".join(rest)
self._live.update(Text(rest_text))
def stop(self) -> None:
"""Stop the streaming display."""
if self._live:
self._live.stop()
self._live = None
class MiddleGroundBackend:
"""Enhanced Rich + prompt_toolkit UI backend."""
def __init__(self, config: Optional['UIConfig'] = None):
"""Initialize MiddleGroundBackend.
Args:
config: Optional UIConfig instance
Raises:
ImportError: If required deps not installed
"""
if not RICH_AVAILABLE:
raise ImportError("Rich is required. Install with: pip install rich")
if not PROMPT_TOOLKIT_AVAILABLE:
raise ImportError("prompt_toolkit is required. Install with: pip install prompt_toolkit")
self._config = config
self._current_message = ""
self._in_stream = False
# Configure console
no_color = config.no_color if config else False
self._console = Console(
force_terminal=True,
no_color=no_color,
highlight=not no_color,
)
# Markdown streamer for no-flicker streaming
self._streamer: Optional[MarkdownStreamer] = None
# prompt_toolkit session
self._prompt_session: Optional[PromptSession] = None
self._history = InMemoryHistory()
# Register cleanup handlers
self._register_cleanup()
def _register_cleanup(self) -> None:
"""Register terminal cleanup handlers."""
def cleanup():
self._cleanup_terminal()
atexit.register(cleanup)
# Handle signals
def signal_handler(signum, frame):
self._cleanup_terminal()
sys.exit(128 + signum)
try:
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
except (ValueError, OSError):
# Can't set signal handlers in some contexts
pass
def _cleanup_terminal(self) -> None:
"""Restore terminal state."""
try:
# Stop any active streaming
if self._streamer:
self._streamer.stop()
# Restore cursor
sys.stdout.write("\033[?25h") # Show cursor
sys.stdout.write("\033[0m") # Reset colors
sys.stdout.flush()
except Exception:
pass
def emit(self, event_type: UIEventType, data: Dict[str, Any]) -> None:
"""Emit an event to the UI.
Args:
event_type: Type of event
data: Event data payload
"""
if event_type == UIEventType.MESSAGE_START:
self._in_stream = True
self._current_message = ""
agent_name = data.get('agent_name', '')
if agent_name:
self._console.print(f"[bold cyan][{agent_name}][/bold cyan]")
# Start streaming display
self._streamer = MarkdownStreamer(self._console)
self._streamer.start()
elif event_type == UIEventType.MESSAGE_CHUNK:
content = data.get('content', '')
self._current_message += content
if self._streamer:
self._streamer.update(self._current_message)
elif event_type == UIEventType.MESSAGE_END:
self._in_stream = False
if self._streamer:
self._streamer.update(self._current_message, final=True)
self._streamer = None
self._current_message = ""
elif event_type == UIEventType.TOOL_START:
tool_name = data.get('tool_name', 'tool')
args = data.get('args', '')
self._console.print(f"\n[dim]▶ {tool_name}[/dim]", end="")
if args and self._config and self._config.verbose:
self._console.print(f" [dim]{args}[/dim]")
else:
self._console.print()
elif event_type == UIEventType.TOOL_END:
result = data.get('result', '')
success = data.get('success', True)
if result:
style = "green" if success else "red"
# Truncate long results
if len(result) > 200 and not (self._config and self._config.verbose):
result = result[:200] + "..."
self._console.print(f"[{style}]{result}[/{style}]")
elif event_type == UIEventType.ERROR:
message = data.get('message', 'Unknown error')
self._console.print(Panel(
f"[bold red]{message}[/bold red]",
title="Error",
border_style="red",
))
elif event_type == UIEventType.WARNING:
message = data.get('message', '')
self._console.print(f"[yellow]⚠ {message}[/yellow]")
elif event_type == UIEventType.STATUS_UPDATE:
status = data.get('status', '')
if status:
# Use transient status (overwrite with \r)
sys.stdout.write(f"\r[{status}]")
sys.stdout.flush()
elif event_type == UIEventType.STATUS_CLEAR:
sys.stdout.write("\r" + " " * 40 + "\r")
sys.stdout.flush()
elif event_type == UIEventType.APPROVAL_REQUEST:
prompt = data.get('prompt', 'Approve?')
action = data.get('action', '')
self._console.print()
self._console.print(Panel(
f"{prompt}\n\n[dim]{action}[/dim]" if action else prompt,
title="[bold yellow]Approval Required[/bold yellow]",
border_style="yellow",
))
self._console.print("[bold][y][/bold]es / [bold][n][/bold]o / [bold][a][/bold]lways: ", end="")
def prompt(self, message: str = "") -> str:
"""Get user input with prompt_toolkit.
Args:
message: Optional prompt message
Returns:
User input string
"""
if not self._prompt_session:
self._prompt_session = PromptSession(
history=self._history,
enable_history_search=True,
)
try:
return self._prompt_session.prompt(message)
except EOFError:
return ""
except KeyboardInterrupt:
return ""
async def prompt_async(self, message: str = "") -> str:
"""Get user input asynchronously.
Args:
message: Optional prompt message
Returns:
User input string
"""
if not self._prompt_session:
self._prompt_session = PromptSession(
history=self._history,
enable_history_search=True,
)
try:
return await self._prompt_session.prompt_async(message)
except EOFError:
return ""
except KeyboardInterrupt:
return ""
def is_tty(self) -> bool:
"""Check if running in a TTY.
Returns:
True if stdout is a TTY
"""
return sys.stdout.isatty()
def render_markdown(self, text: str) -> None:
"""Render markdown text.
Args:
text: Markdown text to render
"""
md = Markdown(text)
self._console.print(md)
def cleanup(self) -> None:
"""Cleanup resources."""
self._cleanup_terminal()
+133
View File
@@ -0,0 +1,133 @@
"""
PlainBackend - Zero-dependency UI backend.
Works in any environment: non-TTY, piped output, dumb terminals.
No colors, no formatting, just plain text output.
"""
import sys
from typing import Dict, Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .config import UIConfig
from .events import UIEventType
class PlainBackend:
"""Plain text UI backend with no dependencies."""
def __init__(self, config: Optional['UIConfig'] = None):
"""Initialize PlainBackend.
Args:
config: Optional UIConfig instance
"""
self._config = config
self._current_message = ""
self._in_stream = False
def emit(self, event_type: UIEventType, data: Dict[str, Any]) -> None:
"""Emit an event to the UI.
Args:
event_type: Type of event
data: Event data payload
"""
if event_type == UIEventType.MESSAGE_START:
self._in_stream = True
self._current_message = ""
agent_name = data.get('agent_name', '')
if agent_name:
sys.stdout.write(f"[{agent_name}] ")
sys.stdout.flush()
elif event_type == UIEventType.MESSAGE_CHUNK:
content = data.get('content', '')
sys.stdout.write(content)
sys.stdout.flush()
self._current_message += content
elif event_type == UIEventType.MESSAGE_END:
self._in_stream = False
if not self._current_message.endswith('\n'):
sys.stdout.write('\n')
sys.stdout.flush()
self._current_message = ""
elif event_type == UIEventType.TOOL_START:
tool_name = data.get('tool_name', 'tool')
sys.stdout.write(f"\n[Tool: {tool_name}]\n")
sys.stdout.flush()
elif event_type == UIEventType.TOOL_END:
result = data.get('result', '')
if result:
sys.stdout.write(f"{result}\n")
sys.stdout.flush()
elif event_type == UIEventType.ERROR:
message = data.get('message', 'Unknown error')
sys.stderr.write(f"Error: {message}\n")
sys.stderr.flush()
elif event_type == UIEventType.WARNING:
message = data.get('message', '')
sys.stderr.write(f"Warning: {message}\n")
sys.stderr.flush()
elif event_type == UIEventType.STATUS_UPDATE:
status = data.get('status', '')
if status and self._config and self._config.verbose:
sys.stderr.write(f"[{status}]\n")
sys.stderr.flush()
elif event_type == UIEventType.APPROVAL_REQUEST:
prompt = data.get('prompt', 'Approve?')
sys.stdout.write(f"\n{prompt} [y/n]: ")
sys.stdout.flush()
def prompt(self, message: str = "") -> str:
"""Get user input.
Args:
message: Optional prompt message
Returns:
User input string
"""
if message:
sys.stdout.write(message)
sys.stdout.flush()
try:
return input()
except EOFError:
return ""
except KeyboardInterrupt:
return ""
async def prompt_async(self, message: str = "") -> str:
"""Get user input asynchronously.
For PlainBackend, this is just a sync call wrapped.
Args:
message: Optional prompt message
Returns:
User input string
"""
return self.prompt(message)
def is_tty(self) -> bool:
"""Check if running in a TTY.
Returns:
True if stdout is a TTY
"""
return sys.stdout.isatty()
def cleanup(self) -> None:
"""Cleanup resources. No-op for PlainBackend."""
pass
@@ -0,0 +1,162 @@
"""
RichBackend - Rich-based UI backend.
Uses Rich library for formatted terminal output with colors,
panels, markdown rendering, and syntax highlighting.
"""
import sys
from typing import Dict, Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .config import UIConfig
from .events import UIEventType
try:
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
class RichBackend:
"""Rich-based UI backend with formatted output."""
def __init__(self, config: Optional['UIConfig'] = None):
"""Initialize RichBackend.
Args:
config: Optional UIConfig instance
Raises:
ImportError: If rich is not installed
"""
if not RICH_AVAILABLE:
raise ImportError("Rich is required for RichBackend. Install with: pip install rich")
self._config = config
self._current_message = ""
self._in_stream = False
# Configure console
no_color = config.no_color if config else False
self._console = Console(
force_terminal=True,
no_color=no_color,
highlight=not no_color,
)
def emit(self, event_type: UIEventType, data: Dict[str, Any]) -> None:
"""Emit an event to the UI.
Args:
event_type: Type of event
data: Event data payload
"""
if event_type == UIEventType.MESSAGE_START:
self._in_stream = True
self._current_message = ""
agent_name = data.get('agent_name', '')
if agent_name:
self._console.print(f"[bold cyan][{agent_name}][/bold cyan] ", end="")
elif event_type == UIEventType.MESSAGE_CHUNK:
content = data.get('content', '')
# For streaming, use raw print to avoid Rich overhead
sys.stdout.write(content)
sys.stdout.flush()
self._current_message += content
elif event_type == UIEventType.MESSAGE_END:
self._in_stream = False
if not self._current_message.endswith('\n'):
sys.stdout.write('\n')
sys.stdout.flush()
self._current_message = ""
elif event_type == UIEventType.TOOL_START:
tool_name = data.get('tool_name', 'tool')
self._console.print(f"\n[dim]▶ {tool_name}[/dim]")
elif event_type == UIEventType.TOOL_END:
result = data.get('result', '')
success = data.get('success', True)
if result:
style = "green" if success else "red"
self._console.print(f"[{style}]{result}[/{style}]")
elif event_type == UIEventType.ERROR:
message = data.get('message', 'Unknown error')
self._console.print(f"[bold red]Error:[/bold red] {message}")
elif event_type == UIEventType.WARNING:
message = data.get('message', '')
self._console.print(f"[yellow]Warning:[/yellow] {message}")
elif event_type == UIEventType.STATUS_UPDATE:
status = data.get('status', '')
if status and self._config and self._config.verbose:
self._console.print(f"[dim][{status}][/dim]")
elif event_type == UIEventType.APPROVAL_REQUEST:
prompt = data.get('prompt', 'Approve?')
self._console.print(Panel(
prompt,
title="[bold yellow]Approval Required[/bold yellow]",
border_style="yellow",
))
self._console.print("[y]es / [n]o / [a]lways: ", end="")
def prompt(self, message: str = "") -> str:
"""Get user input.
Args:
message: Optional prompt message
Returns:
User input string
"""
if message:
self._console.print(message, end="")
try:
return input()
except EOFError:
return ""
except KeyboardInterrupt:
return ""
async def prompt_async(self, message: str = "") -> str:
"""Get user input asynchronously.
Args:
message: Optional prompt message
Returns:
User input string
"""
return self.prompt(message)
def is_tty(self) -> bool:
"""Check if running in a TTY.
Returns:
True if stdout is a TTY
"""
return sys.stdout.isatty()
def render_markdown(self, text: str) -> None:
"""Render markdown text.
Args:
text: Markdown text to render
"""
md = Markdown(text)
self._console.print(md)
def cleanup(self) -> None:
"""Cleanup resources."""
pass
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==3.6.2 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==3.7.4 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -0,0 +1,84 @@
"""
PraisonAI Standardise Module
Feature Docs/Examples Protocol (FDEP) implementation for standardising
documentation and examples across the PraisonAI ecosystem.
All imports are lazy-loaded for zero performance impact.
"""
__all__ = [
# Models
"FeatureSlug",
"FeatureManifest",
"ArtifactStatus",
"ValidationResult",
"DuplicateCluster",
"DriftReport",
# Engine
"StandardiseEngine",
"FeatureDiscovery",
"ArtifactValidator",
"DedupeDetector",
"DriftDetector",
"TemplateGenerator",
"ReportGenerator",
# Config
"StandardiseConfig",
# AI Generation
"AIGenerator",
"ExampleVerifier",
]
def __getattr__(name: str):
"""Lazy loading of standardise components."""
if name in ("FeatureSlug", "FeatureManifest", "ArtifactStatus",
"ValidationResult", "DuplicateCluster", "DriftReport"):
from .models import (
FeatureSlug, FeatureManifest, ArtifactStatus,
ValidationResult, DuplicateCluster, DriftReport
)
return locals()[name]
if name == "StandardiseEngine":
from .engine import StandardiseEngine
return StandardiseEngine
if name == "FeatureDiscovery":
from .discovery import FeatureDiscovery
return FeatureDiscovery
if name == "ArtifactValidator":
from .validator import ArtifactValidator
return ArtifactValidator
if name == "DedupeDetector":
from .dedupe import DedupeDetector
return DedupeDetector
if name == "DriftDetector":
from .drift import DriftDetector
return DriftDetector
if name == "TemplateGenerator":
from .templates import TemplateGenerator
return TemplateGenerator
if name == "ReportGenerator":
from .reports import ReportGenerator
return ReportGenerator
if name == "StandardiseConfig":
from .config import StandardiseConfig
return StandardiseConfig
if name == "AIGenerator":
from .ai_generator import AIGenerator
return AIGenerator
if name == "ExampleVerifier":
from .example_verifier import ExampleVerifier
return ExampleVerifier
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,260 @@
"""
Configuration for the FDEP standardisation system.
Supports configuration via:
1. CLI flags (highest priority)
2. Config file (.praison/standardise.yaml)
3. Environment variables
4. Defaults (lowest priority)
"""
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
@dataclass
class StandardiseConfig:
"""Configuration for the standardiser engine."""
# Root paths (auto-detected if not specified)
project_root: Optional[Path] = None
docs_root: Optional[Path] = None
examples_root: Optional[Path] = None
sdk_root: Optional[Path] = None
cli_root: Optional[Path] = None
# Mintlify config
mintlify_config: Optional[Path] = None
# Manifest storage
manifest_dir: Optional[Path] = None
# Safety settings
dry_run: bool = True
backup: bool = True
force: bool = False
# Scope settings
scope: str = "all" # all, docs, examples, sdk, cli
feature_filter: Optional[str] = None # Specific feature slug to check
# Dedupe settings
dedupe_mode: str = "prompt" # prompt, auto, skip
similarity_threshold: float = 0.8
# Report settings
report_format: str = "text" # text, json, markdown
# CI settings
ci_mode: bool = False
# Excluded paths (relative to project root)
excluded_paths: List[str] = field(default_factory=lambda: [
"node_modules",
"__pycache__",
".git",
".venv",
"venv",
"dist",
"build",
])
# Required artifact types per feature
required_artifacts: List[str] = field(default_factory=lambda: [
"docs_concept",
"docs_feature",
"docs_cli",
"docs_sdk",
"example_basic",
"example_advanced",
])
def __post_init__(self):
"""Auto-detect paths if not specified."""
# Convert string paths to Path objects
if isinstance(self.project_root, str):
self.project_root = Path(self.project_root)
if isinstance(self.docs_root, str):
self.docs_root = Path(self.docs_root)
if isinstance(self.examples_root, str):
self.examples_root = Path(self.examples_root)
if isinstance(self.sdk_root, str):
self.sdk_root = Path(self.sdk_root)
if isinstance(self.cli_root, str):
self.cli_root = Path(self.cli_root)
if isinstance(self.mintlify_config, str):
self.mintlify_config = Path(self.mintlify_config)
if isinstance(self.manifest_dir, str):
self.manifest_dir = Path(self.manifest_dir)
if self.project_root is None:
self.project_root = self._detect_project_root()
if self.project_root:
self._auto_detect_paths()
def _detect_project_root(self) -> Optional[Path]:
"""Detect project root by looking for common markers."""
cwd = Path.cwd()
# Look for .praison directory or pyproject.toml
for parent in [cwd] + list(cwd.parents):
if (parent / ".praison").exists():
return parent
if (parent / "pyproject.toml").exists():
return parent
if (parent / "mint.json").exists():
return parent
return cwd
def _auto_detect_paths(self):
"""Auto-detect paths based on project root."""
if not self.project_root:
return
root = self.project_root
# Docs root detection - default: ~/PraisonAIDocs/docs
if self.docs_root is None:
# First check home directory for PraisonAIDocs
home_docs = Path.home() / "PraisonAIDocs" / "docs"
if home_docs.exists():
self.docs_root = home_docs
else:
# Fallback to project-relative paths
for candidate in ["docs", "PraisonAIDocs/docs"]:
path = root / candidate
if path.exists():
self.docs_root = path
break
# Check parent for PraisonAIDocs
parent_docs = root.parent / "PraisonAIDocs"
if parent_docs.exists() and self.docs_root is None:
self.docs_root = parent_docs / "docs"
# Examples root detection - default: ~/praisonai-package/examples
if self.examples_root is None:
# First check home directory for praisonai-package/examples
home_examples = Path.home() / "praisonai-package" / "examples"
if home_examples.exists():
self.examples_root = home_examples
else:
# Fallback to project-relative paths
for candidate in ["examples/python", "examples"]:
path = root / candidate
if path.exists():
self.examples_root = path
break
# SDK root detection
if self.sdk_root is None:
for candidate in [
"src/praisonai-agents/praisonaiagents",
"praisonaiagents",
]:
path = root / candidate
if path.exists():
self.sdk_root = path
break
# CLI root detection
if self.cli_root is None:
for candidate in [
"src/praisonai/praisonai/cli",
"praisonai/cli",
]:
path = root / candidate
if path.exists():
self.cli_root = path
break
# Mintlify config detection
if self.mintlify_config is None:
# Check docs root parent for mint.json
if self.docs_root:
mint_path = self.docs_root.parent / "mint.json"
if mint_path.exists():
self.mintlify_config = mint_path
# Check project root
if self.mintlify_config is None:
for name in ["mint.json", "mintlify.json", "docs.json"]:
path = root / name
if path.exists():
self.mintlify_config = path
break
# Manifest directory
if self.manifest_dir is None:
self.manifest_dir = root / ".praison" / "features"
@classmethod
def from_file(cls, path: Path) -> "StandardiseConfig":
"""Load configuration from a YAML file."""
try:
import yaml
except ImportError:
# Fallback to basic parsing if PyYAML not available
return cls()
if not path.exists():
return cls()
with open(path, "r") as f:
data = yaml.safe_load(f) or {}
return cls(
project_root=Path(data["project_root"]) if data.get("project_root") else None,
docs_root=Path(data["docs_root"]) if data.get("docs_root") else None,
examples_root=Path(data["examples_root"]) if data.get("examples_root") else None,
sdk_root=Path(data["sdk_root"]) if data.get("sdk_root") else None,
cli_root=Path(data["cli_root"]) if data.get("cli_root") else None,
mintlify_config=Path(data["mintlify_config"]) if data.get("mintlify_config") else None,
manifest_dir=Path(data["manifest_dir"]) if data.get("manifest_dir") else None,
dry_run=data.get("dry_run", True),
backup=data.get("backup", True),
force=data.get("force", False),
scope=data.get("scope", "all"),
feature_filter=data.get("feature_filter"),
dedupe_mode=data.get("dedupe_mode", "prompt"),
similarity_threshold=data.get("similarity_threshold", 0.8),
report_format=data.get("report_format", "text"),
ci_mode=data.get("ci_mode", False),
excluded_paths=data.get("excluded_paths", cls.__dataclass_fields__["excluded_paths"].default_factory()),
required_artifacts=data.get("required_artifacts", cls.__dataclass_fields__["required_artifacts"].default_factory()),
)
@classmethod
def from_env(cls) -> "StandardiseConfig":
"""Load configuration from environment variables."""
return cls(
project_root=Path(os.environ["PRAISON_PROJECT_ROOT"]) if os.environ.get("PRAISON_PROJECT_ROOT") else None,
docs_root=Path(os.environ["PRAISON_DOCS_ROOT"]) if os.environ.get("PRAISON_DOCS_ROOT") else None,
dry_run=os.environ.get("PRAISON_DRY_RUN", "true").lower() == "true",
ci_mode=os.environ.get("CI", "false").lower() == "true",
)
def to_dict(self) -> Dict:
"""Convert to dictionary for serialisation."""
return {
"project_root": str(self.project_root) if self.project_root else None,
"docs_root": str(self.docs_root) if self.docs_root else None,
"examples_root": str(self.examples_root) if self.examples_root else None,
"sdk_root": str(self.sdk_root) if self.sdk_root else None,
"cli_root": str(self.cli_root) if self.cli_root else None,
"mintlify_config": str(self.mintlify_config) if self.mintlify_config else None,
"manifest_dir": str(self.manifest_dir) if self.manifest_dir else None,
"dry_run": self.dry_run,
"backup": self.backup,
"force": self.force,
"scope": self.scope,
"feature_filter": self.feature_filter,
"dedupe_mode": self.dedupe_mode,
"similarity_threshold": self.similarity_threshold,
"report_format": self.report_format,
"ci_mode": self.ci_mode,
"excluded_paths": self.excluded_paths,
"required_artifacts": self.required_artifacts,
}
@@ -0,0 +1,221 @@
"""
Duplicate detection for the FDEP standardisation system.
Detects duplicates via:
- Same slug in multiple locations
- High title similarity (>80%)
- High code-block overlap (>40%)
"""
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from .config import StandardiseConfig
from .discovery import FeatureDiscovery
from .models import DuplicateCluster, FeatureSlug
class DedupeDetector:
"""Detects duplicate and near-duplicate content."""
def __init__(self, config: StandardiseConfig, discovery: FeatureDiscovery):
self.config = config
self.discovery = discovery
self._frontmatter_cache: Dict[Path, Dict] = {}
def detect_all(self) -> List[DuplicateCluster]:
"""Detect all duplicate clusters."""
clusters = []
# Detect same-slug duplicates
clusters.extend(self._detect_same_slug_duplicates())
# Detect title similarity duplicates
clusters.extend(self._detect_title_duplicates())
return clusters
def _detect_same_slug_duplicates(self) -> List[DuplicateCluster]:
"""Detect pages with the same slug in different locations."""
clusters = []
all_features = self.discovery.get_all_features()
for slug in all_features:
pages = self.discovery.find_docs_pages(slug)
if len(pages) > 1:
# Determine primary page (prefer features > concepts > cli > sdk)
primary = self._select_primary_page(pages)
clusters.append(DuplicateCluster(
slug=slug,
pages=pages,
similarity_score=1.0, # Exact slug match
issue_type="same_slug",
recommendation="merge" if len(pages) <= 4 else "review",
primary_page=primary,
))
return clusters
def _detect_title_duplicates(self) -> List[DuplicateCluster]:
"""Detect pages with similar titles."""
clusters = []
if not self.config.docs_root or not self.config.docs_root.exists():
return clusters
# Collect all page titles
title_to_pages: Dict[str, List[Tuple[Path, str]]] = {}
for mdx_file in self.config.docs_root.rglob("*.mdx"):
# Skip node_modules and other excluded paths
if any(excluded in str(mdx_file) for excluded in self.config.excluded_paths):
continue
title = self._extract_title(mdx_file)
if title:
normalised_title = self._normalise_title(title)
if normalised_title not in title_to_pages:
title_to_pages[normalised_title] = []
title_to_pages[normalised_title].append((mdx_file, title))
# Find duplicates
for normalised_title, pages in title_to_pages.items():
if len(pages) > 1:
# Check if these are already captured by same-slug detection
slugs = set()
for page, _ in pages:
slug = FeatureSlug.from_path(page, "docs")
slugs.add(slug.normalised)
if len(slugs) > 1:
# Different slugs but same title - this is a title duplicate
slug = FeatureSlug.from_string(normalised_title)
clusters.append(DuplicateCluster(
slug=slug,
pages=[p for p, _ in pages],
similarity_score=0.9,
issue_type="title_similarity",
recommendation="review",
primary_page=pages[0][0],
))
return clusters
def _extract_title(self, mdx_file: Path) -> Optional[str]:
"""Extract title from MDX frontmatter."""
if mdx_file in self._frontmatter_cache:
return self._frontmatter_cache[mdx_file].get("title")
try:
content = mdx_file.read_text(encoding="utf-8")
except Exception:
return None
frontmatter = self._parse_frontmatter(content)
self._frontmatter_cache[mdx_file] = frontmatter
return frontmatter.get("title")
def _parse_frontmatter(self, content: str) -> Dict:
"""Parse YAML frontmatter from MDX content."""
frontmatter = {}
# Check for frontmatter delimiters
if not content.startswith("---"):
return frontmatter
# Find end of frontmatter
end_match = re.search(r"\n---\s*\n", content[3:])
if not end_match:
return frontmatter
frontmatter_text = content[3:end_match.start() + 3]
# Simple YAML parsing (avoid dependency on PyYAML)
for line in frontmatter_text.split("\n"):
line = line.strip()
if ":" in line:
key, value = line.split(":", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
frontmatter[key] = value
return frontmatter
def _normalise_title(self, title: str) -> str:
"""Normalise a title for comparison."""
# Lowercase, remove special chars, collapse whitespace
normalised = title.lower()
normalised = re.sub(r"[^a-z0-9\s]", "", normalised)
normalised = re.sub(r"\s+", " ", normalised).strip()
return normalised
def _select_primary_page(self, pages: List[Path]) -> Optional[Path]:
"""Select the primary page from a list of duplicates."""
# Priority: features > concepts > cli > sdk
priority_order = ["features", "concepts", "cli", "sdk"]
for priority_dir in priority_order:
for page in pages:
if f"/{priority_dir}/" in str(page) or f"\\{priority_dir}\\" in str(page):
return page
# Default to first page
return pages[0] if pages else None
def calculate_similarity(self, text1: str, text2: str) -> float:
"""Calculate similarity between two texts using Levenshtein ratio."""
if not text1 or not text2:
return 0.0
# Simple character-level similarity
len1, len2 = len(text1), len(text2)
if len1 == 0 and len2 == 0:
return 1.0
if len1 == 0 or len2 == 0:
return 0.0
# Use set-based similarity for efficiency
set1 = set(text1.lower().split())
set2 = set(text2.lower().split())
intersection = len(set1 & set2)
union = len(set1 | set2)
if union == 0:
return 0.0
return intersection / union
def get_naming_inconsistencies(self) -> List[Tuple[Path, Path, str]]:
"""Find naming inconsistencies (e.g., handoff.mdx vs handoffs.mdx)."""
inconsistencies = []
if not self.config.docs_root or not self.config.docs_root.exists():
return inconsistencies
# Check each docs directory
for doc_dir in ["concepts", "features", "cli"]:
dir_path = self.config.docs_root / doc_dir
if not dir_path.exists():
continue
# Get all MDX files
mdx_files = list(dir_path.glob("*.mdx"))
file_stems = {f.stem.lower(): f for f in mdx_files}
# Check for singular/plural pairs
from .models import SINGULAR_PLURAL_MAP
for singular, plural in SINGULAR_PLURAL_MAP.items():
if singular in file_stems and plural in file_stems:
inconsistencies.append((
file_stems[singular],
file_stems[plural],
f"Both {singular}.mdx and {plural}.mdx exist in {doc_dir}/",
))
return inconsistencies
@@ -0,0 +1,319 @@
"""
Feature discovery for the FDEP standardisation system.
Discovers features from:
- SDK modules (praisonaiagents/*)
- CLI features (praisonai/cli/features/*)
- Docs pages (docs/**)
- Examples (examples/python/**)
"""
from pathlib import Path
from typing import Dict, List, Optional, Set
from .config import StandardiseConfig
from .models import FeatureSlug
class FeatureDiscovery:
"""Discovers features from various sources in the codebase."""
# Directories to skip during discovery
SKIP_DIRS = {
"__pycache__", ".git", ".venv", "venv", "node_modules",
"dist", "build", ".pytest_cache", ".mypy_cache",
}
# Files to skip
SKIP_FILES = {"__init__.py", "base.py", "utils.py", "helpers.py"}
def __init__(self, config: StandardiseConfig):
self.config = config
self._sdk_features: Optional[Set[FeatureSlug]] = None
self._cli_features: Optional[Set[FeatureSlug]] = None
self._docs_features: Optional[Set[FeatureSlug]] = None
self._example_features: Optional[Set[FeatureSlug]] = None
def discover_all(self) -> Dict[str, Set[FeatureSlug]]:
"""Discover features from all sources."""
return {
"sdk": self.discover_sdk_features(),
"cli": self.discover_cli_features(),
"docs": self.discover_docs_features(),
"examples": self.discover_example_features(),
}
def get_all_features(self) -> Set[FeatureSlug]:
"""Get union of all discovered features."""
all_sources = self.discover_all()
result: Set[FeatureSlug] = set()
for features in all_sources.values():
result.update(features)
return result
def discover_sdk_features(self) -> Set[FeatureSlug]:
"""Discover features from SDK modules."""
if self._sdk_features is not None:
return self._sdk_features
self._sdk_features = set()
if not self.config.sdk_root or not self.config.sdk_root.exists():
return self._sdk_features
# Each top-level directory in SDK is a feature module
for item in self.config.sdk_root.iterdir():
if not item.is_dir():
continue
if item.name in self.SKIP_DIRS:
continue
if item.name.startswith("."):
continue
slug = FeatureSlug.from_path(item, "sdk")
if slug.is_valid:
self._sdk_features.add(slug)
return self._sdk_features
def discover_cli_features(self) -> Set[FeatureSlug]:
"""Discover features from CLI feature files."""
if self._cli_features is not None:
return self._cli_features
self._cli_features = set()
if not self.config.cli_root or not self.config.cli_root.exists():
return self._cli_features
# Check features directory
features_dir = self.config.cli_root / "features"
if features_dir.exists():
for item in features_dir.iterdir():
if not item.is_file():
continue
if item.suffix != ".py":
continue
if item.name in self.SKIP_FILES:
continue
slug = FeatureSlug.from_path(item, "cli")
if slug.is_valid:
self._cli_features.add(slug)
# Check commands directory
commands_dir = self.config.cli_root / "commands"
if commands_dir.exists():
for item in commands_dir.iterdir():
if not item.is_file():
continue
if item.suffix != ".py":
continue
if item.name in self.SKIP_FILES:
continue
slug = FeatureSlug.from_path(item, "cli")
if slug.is_valid:
self._cli_features.add(slug)
return self._cli_features
def discover_docs_features(self) -> Set[FeatureSlug]:
"""Discover features from docs pages."""
if self._docs_features is not None:
return self._docs_features
self._docs_features = set()
if not self.config.docs_root or not self.config.docs_root.exists():
return self._docs_features
# Scan key docs directories
docs_dirs = ["concepts", "features", "cli", "sdk"]
for dir_name in docs_dirs:
dir_path = self.config.docs_root / dir_name
if not dir_path.exists():
continue
self._scan_docs_directory(dir_path)
return self._docs_features
def _scan_docs_directory(self, directory: Path, depth: int = 0):
"""Recursively scan a docs directory for feature slugs."""
if depth > 3: # Limit recursion depth
return
for item in directory.iterdir():
if item.name in self.SKIP_DIRS:
continue
if item.name.startswith("."):
continue
if item.is_file() and item.suffix == ".mdx":
slug = FeatureSlug.from_path(item, "docs")
if slug.is_valid:
self._docs_features.add(slug)
elif item.is_dir():
# Directory name itself might be a feature
slug = FeatureSlug.from_path(item, "docs")
if slug.is_valid:
self._docs_features.add(slug)
# Recurse into subdirectory
self._scan_docs_directory(item, depth + 1)
def discover_example_features(self) -> Set[FeatureSlug]:
"""Discover features from examples."""
if self._example_features is not None:
return self._example_features
self._example_features = set()
if not self.config.examples_root or not self.config.examples_root.exists():
return self._example_features
# Each top-level directory in examples is a feature
for item in self.config.examples_root.iterdir():
if not item.is_dir():
continue
if item.name in self.SKIP_DIRS:
continue
if item.name.startswith("."):
continue
slug = FeatureSlug.from_path(item, "examples")
if slug.is_valid:
self._example_features.add(slug)
return self._example_features
def get_feature_sources(self, slug: FeatureSlug) -> Dict[str, bool]:
"""Get which sources contain a given feature."""
return {
"sdk": slug in self.discover_sdk_features(),
"cli": slug in self.discover_cli_features(),
"docs": slug in self.discover_docs_features(),
"examples": slug in self.discover_example_features(),
}
def find_docs_pages(self, slug: FeatureSlug) -> List[Path]:
"""Find all docs pages for a given feature slug."""
pages = []
if not self.config.docs_root or not self.config.docs_root.exists():
return pages
# Search in key directories
search_dirs = [
self.config.docs_root / "concepts",
self.config.docs_root / "features",
self.config.docs_root / "cli",
]
# Also search SDK docs
sdk_docs = self.config.docs_root / "sdk" / "praisonaiagents"
if sdk_docs.exists():
search_dirs.append(sdk_docs)
slug_str = slug.normalised
slug_variants = {slug_str, slug_str.replace("-", "_")}
# Add singular/plural variants
from .models import SINGULAR_PLURAL_MAP
for singular, plural in SINGULAR_PLURAL_MAP.items():
if slug_str == plural:
slug_variants.add(singular)
elif slug_str == singular:
slug_variants.add(plural)
for search_dir in search_dirs:
if not search_dir.exists():
continue
self._find_pages_recursive(search_dir, slug_variants, pages)
return pages
def _find_pages_recursive(self, directory: Path, slug_variants: Set[str],
pages: List[Path], depth: int = 0):
"""Recursively find pages matching slug variants."""
if depth > 4:
return
for item in directory.iterdir():
if item.name in self.SKIP_DIRS:
continue
if item.is_file() and item.suffix == ".mdx":
stem = item.stem.lower().replace("_", "-")
if stem in slug_variants:
pages.append(item)
elif item.is_dir():
dir_name = item.name.lower().replace("_", "-")
if dir_name in slug_variants:
# Check for index or main file in this directory
for mdx_file in item.glob("*.mdx"):
pages.append(mdx_file)
else:
self._find_pages_recursive(item, slug_variants, pages, depth + 1)
def find_examples(self, slug: FeatureSlug) -> List[Path]:
"""Find all example files for a given feature slug."""
examples = []
if not self.config.examples_root or not self.config.examples_root.exists():
return examples
slug_str = slug.normalised
slug_variants = {slug_str, slug_str.replace("-", "_")}
# Check for feature directory
for variant in slug_variants:
feature_dir = self.config.examples_root / variant
if feature_dir.exists() and feature_dir.is_dir():
for py_file in feature_dir.glob("*.py"):
examples.append(py_file)
return examples
def get_sdk_module_path(self, slug: FeatureSlug) -> Optional[Path]:
"""Get the SDK module path for a feature."""
if not self.config.sdk_root or not self.config.sdk_root.exists():
return None
slug_str = slug.normalised
variants = [slug_str, slug_str.replace("-", "_")]
for variant in variants:
module_path = self.config.sdk_root / variant
if module_path.exists() and module_path.is_dir():
return module_path
return None
def get_cli_feature_path(self, slug: FeatureSlug) -> Optional[Path]:
"""Get the CLI feature file path for a feature."""
if not self.config.cli_root or not self.config.cli_root.exists():
return None
slug_str = slug.normalised
variants = [slug_str, slug_str.replace("-", "_")]
# Check features directory
features_dir = self.config.cli_root / "features"
if features_dir.exists():
for variant in variants:
feature_path = features_dir / f"{variant}.py"
if feature_path.exists():
return feature_path
# Check commands directory
commands_dir = self.config.cli_root / "commands"
if commands_dir.exists():
for variant in variants:
command_path = commands_dir / f"{variant}.py"
if command_path.exists():
return command_path
return None
@@ -0,0 +1,263 @@
"""
Drift detection for the FDEP standardisation system.
Detects drift between:
- SDK exports vs SDK docs
- CLI help/flags vs CLI docs
"""
import ast
import re
from pathlib import Path
from typing import List, Optional, Set
from .config import StandardiseConfig
from .discovery import FeatureDiscovery
from .models import DriftItem, DriftReport, FeatureSlug
class DriftDetector:
"""Detects drift between code and documentation."""
def __init__(self, config: StandardiseConfig, discovery: FeatureDiscovery):
self.config = config
self.discovery = discovery
def detect_all(self) -> List[DriftReport]:
"""Detect drift for all features."""
reports = []
all_features = self.discovery.get_all_features()
for slug in all_features:
report = self.detect_feature_drift(slug)
if report.has_drift:
reports.append(report)
return reports
def detect_feature_drift(self, slug: FeatureSlug) -> DriftReport:
"""Detect drift for a single feature."""
sdk_drift = self._detect_sdk_drift(slug)
cli_drift = self._detect_cli_drift(slug)
return DriftReport(
slug=slug,
sdk_drift=sdk_drift,
cli_drift=cli_drift,
)
def _detect_sdk_drift(self, slug: FeatureSlug) -> List[DriftItem]:
"""Detect drift between SDK module and SDK docs."""
drift_items = []
# Get SDK module path
sdk_module = self.discovery.get_sdk_module_path(slug)
if not sdk_module or not sdk_module.exists():
return drift_items
# Extract public exports from SDK module
sdk_exports = self._extract_sdk_exports(sdk_module)
# Extract documented items from SDK docs
sdk_docs = self._get_sdk_docs_path(slug)
if sdk_docs:
documented_items = self._extract_documented_items(sdk_docs)
else:
documented_items = set()
# Find items in SDK but not in docs
for export in sdk_exports:
if export not in documented_items:
drift_items.append(DriftItem(
name=export,
source="sdk",
missing_in="docs",
item_type="class" if export[0].isupper() else "function",
))
# Find items in docs but not in SDK
for item in documented_items:
if item not in sdk_exports:
drift_items.append(DriftItem(
name=item,
source="docs",
missing_in="sdk",
item_type="class" if item[0].isupper() else "function",
))
return drift_items
def _detect_cli_drift(self, slug: FeatureSlug) -> List[DriftItem]:
"""Detect drift between CLI feature and CLI docs."""
drift_items = []
# Get CLI feature path
cli_feature = self.discovery.get_cli_feature_path(slug)
if not cli_feature or not cli_feature.exists():
return drift_items
# Extract CLI flags/commands from code
cli_flags = self._extract_cli_flags(cli_feature)
# Extract documented flags from CLI docs
cli_docs = self._get_cli_docs_path(slug)
if cli_docs:
documented_flags = self._extract_documented_flags(cli_docs)
else:
documented_flags = set()
# Find flags in code but not in docs
for flag in cli_flags:
if flag not in documented_flags:
drift_items.append(DriftItem(
name=flag,
source="cli",
missing_in="docs",
item_type="flag",
))
return drift_items
def _extract_sdk_exports(self, module_path: Path) -> Set[str]:
"""Extract public exports from an SDK module."""
exports = set()
# Check __init__.py for __all__
init_file = module_path / "__init__.py"
if init_file.exists():
try:
content = init_file.read_text(encoding="utf-8")
# Look for __all__ = [...]
all_match = re.search(r"__all__\s*=\s*\[(.*?)\]", content, re.DOTALL)
if all_match:
items = re.findall(r'["\'](\w+)["\']', all_match.group(1))
exports.update(items)
except Exception:
pass
# Also scan for public classes and functions
for py_file in module_path.glob("*.py"):
if py_file.name.startswith("_"):
continue
try:
content = py_file.read_text(encoding="utf-8")
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if not node.name.startswith("_"):
exports.add(node.name)
elif isinstance(node, ast.FunctionDef):
if not node.name.startswith("_"):
exports.add(node.name)
except Exception:
pass
return exports
def _extract_cli_flags(self, cli_file: Path) -> Set[str]:
"""Extract CLI flags from a CLI feature file."""
flags = set()
try:
content = cli_file.read_text(encoding="utf-8")
# Look for click.option or argparse add_argument patterns
# click: @click.option("--flag-name", ...)
click_flags = re.findall(r'@click\.option\(["\'](-{1,2}[\w-]+)["\']', content)
flags.update(click_flags)
# argparse: parser.add_argument("--flag-name", ...)
argparse_flags = re.findall(r'add_argument\(["\'](-{1,2}[\w-]+)["\']', content)
flags.update(argparse_flags)
# Also look for flag definitions in strings
string_flags = re.findall(r'["\'](-{2}[\w-]+)["\']', content)
flags.update(string_flags)
except Exception:
pass
return flags
def _extract_documented_items(self, docs_path: Path) -> Set[str]:
"""Extract documented class/function names from docs."""
items = set()
try:
content = docs_path.read_text(encoding="utf-8")
# Look for class/function references in code blocks
# Pattern: `ClassName` or `function_name`
backtick_items = re.findall(r'`(\w+)`', content)
items.update(backtick_items)
# Look for headings that might be class/function names
heading_items = re.findall(r'^#{1,3}\s+`?(\w+)`?', content, re.MULTILINE)
items.update(heading_items)
except Exception:
pass
return items
def _extract_documented_flags(self, docs_path: Path) -> Set[str]:
"""Extract documented CLI flags from docs."""
flags = set()
try:
content = docs_path.read_text(encoding="utf-8")
# Look for flags in code blocks and inline code
flag_patterns = re.findall(r'`(-{1,2}[\w-]+)`', content)
flags.update(flag_patterns)
# Also look for flags in plain text
text_flags = re.findall(r'\s(-{2}[\w-]+)\s', content)
flags.update(text_flags)
except Exception:
pass
return flags
def _get_sdk_docs_path(self, slug: FeatureSlug) -> Optional[Path]:
"""Get the SDK docs path for a feature."""
if not self.config.docs_root:
return None
sdk_docs = self.config.docs_root / "sdk" / "praisonaiagents"
if not sdk_docs.exists():
return None
slug_str = slug.normalised
variants = [slug_str, slug_str.replace("-", "_")]
for variant in variants:
module_dir = sdk_docs / variant
if module_dir.exists():
for mdx_file in module_dir.glob("*.mdx"):
return mdx_file
return None
def _get_cli_docs_path(self, slug: FeatureSlug) -> Optional[Path]:
"""Get the CLI docs path for a feature."""
if not self.config.docs_root:
return None
cli_docs = self.config.docs_root / "cli"
if not cli_docs.exists():
return None
slug_str = slug.normalised
variants = [slug_str, slug_str.replace("-", "_")]
for variant in variants:
path = cli_docs / f"{variant}.mdx"
if path.exists():
return path
return None
@@ -0,0 +1,270 @@
"""
Main standardisation engine for the FDEP system.
Orchestrates all standardisation operations:
- Discovery
- Validation
- Dedupe detection
- Drift detection
- Report generation
- Fix application
"""
import shutil
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from .config import StandardiseConfig
from .dedupe import DedupeDetector
from .discovery import FeatureDiscovery
from .drift import DriftDetector
from .models import (
ArtifactType,
FeatureSlug,
IssueType,
StandardiseIssue,
StandardiseReport,
)
from .reports import ReportGenerator
from .templates import TemplateGenerator
from .validator import ArtifactValidator
class StandardiseEngine:
"""Main engine for standardisation operations."""
def __init__(self, config: Optional[StandardiseConfig] = None):
self.config = config or StandardiseConfig()
self.discovery = FeatureDiscovery(self.config)
self.validator = ArtifactValidator(self.config, self.discovery)
self.dedupe = DedupeDetector(self.config, self.discovery)
self.drift = DriftDetector(self.config, self.discovery)
self.templates = TemplateGenerator()
self.reports = ReportGenerator()
def check(self) -> StandardiseReport:
"""Run a full standardisation check and return a report."""
timestamp = datetime.now().isoformat()
# Discover features
all_features = self.discovery.get_all_features()
# Count docs and examples
docs_count = self._count_docs()
examples_count = self._count_examples()
# Validate artifacts
validation_results = self.validator.validate_all()
missing_artifacts = self.validator.get_missing_artifacts()
# Detect duplicates
duplicates = self.dedupe.detect_all()
# Detect naming inconsistencies
naming_issues = self.dedupe.get_naming_inconsistencies()
# Detect drift
drift_reports = self.drift.detect_all()
# Compile issues
issues = []
# Add naming inconsistency issues
for path1, path2, message in naming_issues:
issues.append(StandardiseIssue(
issue_type=IssueType.NAMING_INCONSISTENCY,
slug=None,
path=path1,
message=message,
severity="warning",
auto_fixable=True,
fix_action=f"rename {path1} or {path2}",
))
# Add drift issues
for drift_report in drift_reports:
for item in drift_report.sdk_drift:
issues.append(StandardiseIssue(
issue_type=IssueType.DRIFT_SDK_DOCS,
slug=drift_report.slug,
path=None,
message=f"{item.name} in {item.source} but missing in {item.missing_in}",
severity="info",
auto_fixable=False,
))
for item in drift_report.cli_drift:
issues.append(StandardiseIssue(
issue_type=IssueType.DRIFT_CLI_DOCS,
slug=drift_report.slug,
path=None,
message=f"{item.name} in {item.source} but missing in {item.missing_in}",
severity="info",
auto_fixable=False,
))
return StandardiseReport(
timestamp=timestamp,
features_scanned=len(all_features),
docs_pages=docs_count,
examples_count=examples_count,
issues=issues,
duplicates=duplicates,
missing_artifacts=missing_artifacts,
drift_reports=drift_reports,
validation_results=list(validation_results.values()),
)
def report(self, format: str = "text") -> str:
"""Generate a report in the specified format."""
report = self.check()
return self.reports.generate(report, format)
def fix(self, feature: Optional[str] = None,
apply: bool = False) -> Dict[str, List[str]]:
"""
Fix standardisation issues.
Args:
feature: Specific feature slug to fix (None = all)
apply: Actually apply changes (False = dry-run)
Returns:
Dict with 'planned' and 'applied' actions
"""
actions = {"planned": [], "applied": []}
# Get current state
report = self.check()
# Plan fixes for missing artifacts
for slug_str, missing in report.missing_artifacts.items():
if feature and slug_str != feature:
continue
slug = FeatureSlug.from_string(slug_str)
for artifact_type in missing:
path = self.templates.get_expected_path(
slug, artifact_type,
docs_root=self.config.docs_root,
examples_root=self.config.examples_root,
)
if path:
action = f"CREATE {path}"
actions["planned"].append(action)
if apply and not self.config.dry_run:
self._create_artifact(slug, artifact_type, path)
actions["applied"].append(action)
# Plan fixes for naming inconsistencies
naming_issues = self.dedupe.get_naming_inconsistencies()
for path1, path2, message in naming_issues:
if feature:
slug1 = FeatureSlug.from_path(path1, "docs")
slug2 = FeatureSlug.from_path(path2, "docs")
if slug1.normalised != feature and slug2.normalised != feature:
continue
# Prefer plural form
if "singular" in str(path1).lower() or len(path1.stem) < len(path2.stem):
action = f"RENAME {path1} -> deprecated, keep {path2}"
else:
action = f"RENAME {path2} -> deprecated, keep {path1}"
actions["planned"].append(action)
return actions
def init(self, feature: str) -> Dict[str, str]:
"""
Initialise a new feature with all required artifacts.
Args:
feature: Feature slug to initialise
Returns:
Dict mapping artifact type to created path
"""
created = {}
slug = FeatureSlug.from_string(feature)
if not slug.is_valid:
raise ValueError(f"Invalid feature slug: {slug.validation_error}")
for artifact_type in ArtifactType:
if artifact_type == ArtifactType.MANIFEST:
continue
path = self.templates.get_expected_path(
slug, artifact_type,
docs_root=self.config.docs_root,
examples_root=self.config.examples_root,
)
if path and not path.exists():
if not self.config.dry_run:
self._create_artifact(slug, artifact_type, path)
created[artifact_type.value] = str(path)
return created
def _create_artifact(self, slug: FeatureSlug, artifact_type: ArtifactType,
path: Path) -> None:
"""Create an artifact file."""
# Backup if exists
if path.exists() and self.config.backup:
self._backup_file(path)
# Create parent directories
path.parent.mkdir(parents=True, exist_ok=True)
# Generate content
content = self.templates.generate(slug, artifact_type)
# Write file
path.write_text(content, encoding="utf-8")
def _backup_file(self, path: Path) -> Path:
"""Create a backup of a file."""
if not self.config.project_root:
return path
backup_dir = self.config.project_root / ".praison" / "backups"
backup_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{path.stem}_{timestamp}{path.suffix}"
backup_path = backup_dir / backup_name
shutil.copy2(path, backup_path)
return backup_path
def _count_docs(self) -> int:
"""Count total docs pages."""
if not self.config.docs_root or not self.config.docs_root.exists():
return 0
count = 0
for mdx_file in self.config.docs_root.rglob("*.mdx"):
if not any(excluded in str(mdx_file) for excluded in self.config.excluded_paths):
count += 1
return count
def _count_examples(self) -> int:
"""Count total example files."""
if not self.config.examples_root or not self.config.examples_root.exists():
return 0
count = 0
for py_file in self.config.examples_root.rglob("*.py"):
if not any(excluded in str(py_file) for excluded in self.config.excluded_paths):
count += 1
return count
def get_exit_code(self, report: StandardiseReport) -> int:
"""Get CI exit code based on report."""
if report.has_issues:
return 1 # Issues found
return 0 # No issues
@@ -0,0 +1,674 @@
"""
Enhanced templates for the FDEP standardisation system.
Based on analysis of existing PraisonAI documentation best practices:
- Progressive disclosure (simple first, advanced later)
- Mermaid diagrams for concepts
- CodeGroup/Tabs for multiple examples
- Consistent structure across page types
"""
from pathlib import Path
from typing import Optional
from .models import ArtifactType, FeatureSlug
# Template constants based on existing doc analysis
CONCEPT_TEMPLATE = '''---
title: "{name}"
sidebarTitle: "{name}"
description: "{description}"
icon: "{icon}"
---
## Overview
{name} provides [brief description of what this feature does and why it exists].
```mermaid
flowchart LR
subgraph Input
A[User Request]
end
subgraph {name}
B[Process] --> C[Validate]
C --> D[Execute]
end
subgraph Output
E[Result]
end
A --> B
D --> E
style B fill:#189AB4,color:#fff
style C fill:#2E8B57,color:#fff
style D fill:#8B0000,color:#fff
```
## Types of {name}
<CardGroup cols={{2}}>
<Card title="Type 1" icon="code">
Description of first type
</Card>
<Card title="Type 2" icon="brain">
Description of second type
</Card>
</CardGroup>
## Quick Start
```python
from praisonaiagents import Agent, Task
# Basic {slug} usage
agent = Agent(
instructions="You are a helpful assistant",
# Configure {slug} here
)
result = agent.start("Your prompt here")
print(result)
```
## When to Use
| Use Case | Recommendation |
|----------|----------------|
| Simple tasks | Use basic configuration |
| Complex workflows | Use advanced patterns |
| Production | Enable all safety features |
## Related Features
- [Related Feature 1](/docs/concepts/related1)
- [Related Feature 2](/docs/concepts/related2)
'''
FEATURE_TEMPLATE = '''---
title: "{name}"
description: "How to use {slug} in PraisonAI"
icon: "code"
---
## Quick Start
<CodeGroup>
```python Basic Usage
from praisonaiagents import Agent, Task
# Simple {slug} example
agent = Agent(
instructions="You are a helpful assistant",
# {slug} configuration
)
result = agent.start("Hello!")
print(result)
```
```python Advanced Usage
from praisonaiagents import Agent, Task, Agents
# Advanced {slug} with multiple agents
agent1 = Agent(name="Agent1", instructions="First agent")
agent2 = Agent(name="Agent2", instructions="Second agent")
task = Task(
description="Complex task using {slug}",
agent=agent1,
)
agents = Agents(agents=[agent1, agent2], tasks=[task])
result = agents.start()
```
</CodeGroup>
## Configuration Options
<Tabs>
<Tab title="Basic">
```python
# Minimal configuration
agent = Agent(
instructions="Your instructions",
# {slug}=True, # Enable feature
)
```
</Tab>
<Tab title="Advanced">
```python
# Full configuration
agent = Agent(
instructions="Your instructions",
# {slug}={{
# "option1": "value1",
# "option2": "value2",
# }},
)
```
</Tab>
</Tabs>
## API Reference
### Parameters
<ParamField body="param1" type="string" required>
Description of the first parameter.
</ParamField>
<ParamField body="param2" type="boolean" default="false">
Description of the second parameter.
</ParamField>
<ParamField body="param3" type="object">
Advanced configuration object.
</ParamField>
## Examples
### Example 1: Basic Usage
```python
from praisonaiagents import Agent
agent = Agent(instructions="You are helpful")
result = agent.start("Hello")
```
### Example 2: With Callbacks
```python
from praisonaiagents import Agent
def on_complete(result):
print(f"Completed: {{result}}")
agent = Agent(
instructions="You are helpful",
# callbacks=[on_complete],
)
```
## Troubleshooting
<Accordion title="Common Issue 1">
**Problem**: Description of the issue
**Solution**: How to fix it
</Accordion>
<Accordion title="Common Issue 2">
**Problem**: Another common issue
**Solution**: The fix for this issue
</Accordion>
## See Also
- [Concept Guide](/docs/concepts/{slug})
- [CLI Reference](/docs/cli/{slug})
- [API Reference](/docs/sdk/praisonaiagents/{slug}/{slug})
'''
CLI_TEMPLATE = '''---
title: "{name}"
sidebarTitle: "{name}"
description: "CLI commands for {slug}"
icon: "terminal"
---
## Quick Start
```bash
praisonai {slug} [OPTIONS]
```
## Commands
### Basic Command
```bash
praisonai {slug}
```
**Expected Output:**
```
[Output example here]
```
### With Options
```bash
praisonai {slug} --option value
```
## Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--help` | flag | - | Show help message |
| `--verbose` | flag | false | Enable verbose output |
| `--config` | string | - | Path to config file |
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PRAISONAI_{slug_upper}` | Configure {slug} | None |
## Examples
### Example 1: Basic
```bash
praisonai {slug}
```
### Example 2: With Config
```bash
praisonai {slug} --config config.yaml
```
### Example 3: Verbose Mode
```bash
praisonai {slug} --verbose
```
## Integration with Python
You can also use {slug} programmatically:
```python
from praisonaiagents import Agent
# Equivalent to CLI command
agent = Agent(instructions="Your instructions")
result = agent.start("Your prompt")
```
## Troubleshooting
<Accordion title="Command not found">
Ensure PraisonAI is installed: `pip install praisonai`
</Accordion>
<Accordion title="Permission denied">
Check file permissions and try with `sudo` if needed.
</Accordion>
'''
SDK_TEMPLATE = '''---
title: "{name} Module"
description: "API reference for {slug}"
icon: "code"
---
# {name}
The {name} module provides [brief description].
## Installation
```bash
pip install praisonaiagents
```
## Quick Start
```python
from praisonaiagents import {class_name}
# Basic usage
instance = {class_name}()
result = instance.execute()
```
## Classes
### `{class_name}`
Main class for {slug} functionality.
#### Constructor
```python
{class_name}(
param1: str,
param2: bool = False,
**kwargs
)
```
#### Parameters
<ParamField body="param1" type="string" required>
Primary configuration parameter.
</ParamField>
<ParamField body="param2" type="boolean" default="false">
Enable advanced features.
</ParamField>
#### Methods
##### `execute()`
Execute the main functionality.
```python
result = instance.execute(input_data)
```
**Parameters:**
- `input_data` (str): The input to process
**Returns:** `str` - The processed result
##### `configure()`
Update configuration.
```python
instance.configure(option="value")
```
## Type Definitions
```python
from typing import TypedDict, Optional
class {class_name}Config(TypedDict):
param1: str
param2: Optional[bool]
```
## Examples
### Basic Example
```python
from praisonaiagents import {class_name}
instance = {class_name}(param1="value")
result = instance.execute()
print(result)
```
### Advanced Example
```python
from praisonaiagents import {class_name}
# With full configuration
instance = {class_name}(
param1="value",
param2=True,
)
# Custom processing
result = instance.execute()
```
## See Also
- [Concept Guide](/docs/concepts/{slug})
- [Feature Guide](/docs/features/{slug})
'''
EXAMPLE_BASIC_TEMPLATE = '''"""
{name} - Basic Example
Demonstrates minimal usage of {slug} with a single agent.
Features:
- Simple configuration
- Single agent setup
- Basic execution
Expected Output:
Agent response demonstrating {slug} functionality.
Usage:
python {slug}-basic.py
"""
from praisonaiagents import Agent
# ============================================================
# Basic {name} Configuration
# ============================================================
# Create an agent with {slug} enabled
agent = Agent(
name="{name}Agent",
role="{name} Specialist",
goal="Demonstrate basic {slug} functionality",
backstory="You are an expert in {slug} operations.",
instructions="Provide clear, helpful responses demonstrating {slug}.",
# {slug}=True, # Enable {slug} feature
)
# ============================================================
# Execute
# ============================================================
if __name__ == "__main__":
# Run the agent
result = agent.start("Demonstrate {slug} functionality")
print(result)
'''
EXAMPLE_ADVANCED_TEMPLATE = '''"""
{name} - Advanced Example
Demonstrates advanced {slug} patterns including:
- Multi-agent configuration
- Custom callbacks/handlers
- Error handling
- All available options
Features:
- Multiple agents working together
- Custom configuration
- Production-ready patterns
Expected Output:
Comprehensive demonstration of {slug} with multiple agents.
Usage:
python {slug}-advanced.py
"""
from praisonaiagents import Agent, Task, Agents
from typing import Any
# ============================================================
# Section 1: Custom Configuration
# ============================================================
# Define custom {slug} configuration
# config = {{
# "option1": "value1",
# "option2": True,
# }}
# ============================================================
# Section 2: Custom Callbacks
# ============================================================
def on_task_complete(task_output: Any) -> None:
"""Callback when a task completes."""
print(f"Task completed: {{task_output}}")
def on_error(error: Exception) -> None:
"""Callback when an error occurs."""
print(f"Error occurred: {{error}}")
# ============================================================
# Section 3: Multi-Agent Setup
# ============================================================
# Primary agent
primary_agent = Agent(
name="Primary{name}Agent",
role="Primary {name} Handler",
goal="Handle primary {slug} operations",
backstory="You are the primary handler for {slug} tasks.",
instructions="Process {slug} requests thoroughly and accurately.",
# {slug}=config,
)
# Secondary agent
secondary_agent = Agent(
name="Secondary{name}Agent",
role="Secondary {name} Handler",
goal="Support and verify {slug} operations",
backstory="You verify and enhance {slug} results.",
instructions="Review and improve {slug} outputs.",
# {slug}=config,
)
# ============================================================
# Section 4: Task Definition
# ============================================================
primary_task = Task(
description="Demonstrate advanced {slug} with primary processing",
agent=primary_agent,
expected_output="Detailed {slug} demonstration",
)
verification_task = Task(
description="Verify and enhance the {slug} results",
agent=secondary_agent,
expected_output="Verified and enhanced {slug} output",
)
# ============================================================
# Section 5: Execution
# ============================================================
if __name__ == "__main__":
# Create agents team
agents = Agents(
agents=[primary_agent, secondary_agent],
tasks=[primary_task, verification_task],
verbose=True,
)
# Execute
result = agents.start()
print("\\n" + "=" * 60)
print("FINAL RESULT:")
print("=" * 60)
print(result)
# ============================================================
# Section 6: All Options Reference
# ============================================================
"""
{name} Options:
──────────────────────────────────────────────────────────────
| Option | Type | Default | Description |
|-----------------|----------|---------|----------------------|
| option1 | str | None | Primary option |
| option2 | bool | False | Enable advanced mode |
| option3 | int | 10 | Iteration count |
| option4 | list | [] | Additional handlers |
──────────────────────────────────────────────────────────────
"""
'''
class EnhancedTemplateGenerator:
"""Enhanced template generator with best-practice templates."""
# Icon mapping for features
ICON_MAP = {
"guardrails": "shield-halved",
"memory": "brain",
"knowledge": "book",
"tools": "wrench",
"agents": "robot",
"tasks": "list-check",
"workflows": "diagram-project",
"handoffs": "arrow-right-arrow-left",
"sessions": "clock",
"hooks": "plug",
"callbacks": "phone",
"telemetry": "chart-line",
"default": "code",
}
def get_icon(self, slug: str) -> str:
"""Get appropriate icon for a feature."""
return self.ICON_MAP.get(slug, self.ICON_MAP["default"])
def generate(self, slug: FeatureSlug, artifact_type: ArtifactType) -> str:
"""Generate template content for an artifact type."""
slug_str = slug.normalised
name = slug_str.replace("-", " ").title()
class_name = "".join(word.title() for word in slug_str.split("-"))
icon = self.get_icon(slug_str)
slug_upper = slug_str.upper().replace("-", "_")
# Select template
if artifact_type == ArtifactType.DOCS_CONCEPT:
template = CONCEPT_TEMPLATE
elif artifact_type == ArtifactType.DOCS_FEATURE:
template = FEATURE_TEMPLATE
elif artifact_type == ArtifactType.DOCS_CLI:
template = CLI_TEMPLATE
elif artifact_type == ArtifactType.DOCS_SDK:
template = SDK_TEMPLATE
elif artifact_type == ArtifactType.EXAMPLE_BASIC:
template = EXAMPLE_BASIC_TEMPLATE
elif artifact_type == ArtifactType.EXAMPLE_ADVANCED:
template = EXAMPLE_ADVANCED_TEMPLATE
else:
return ""
# Fill template
return template.format(
name=name,
slug=slug_str,
class_name=class_name,
icon=icon,
slug_upper=slug_upper,
description=f"{name} functionality for PraisonAI agents",
)
def get_expected_path(self, slug: FeatureSlug, artifact_type: ArtifactType,
docs_root: Optional[Path] = None,
examples_root: Optional[Path] = None) -> Optional[Path]:
"""Get the expected path for an artifact."""
slug_str = slug.normalised
if artifact_type == ArtifactType.EXAMPLE_BASIC:
if examples_root:
return examples_root / slug_str / f"{slug_str}-basic.py"
elif artifact_type == ArtifactType.EXAMPLE_ADVANCED:
if examples_root:
return examples_root / slug_str / f"{slug_str}-advanced.py"
elif artifact_type == ArtifactType.DOCS_CONCEPT:
if docs_root:
return docs_root / "concepts" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_FEATURE:
if docs_root:
return docs_root / "features" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_CLI:
if docs_root:
return docs_root / "cli" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_SDK:
if docs_root:
return docs_root / "sdk" / "praisonaiagents" / slug_str / f"{slug_str}.mdx"
return None
@@ -0,0 +1,229 @@
"""
Example verification module for the FDEP standardisation system.
Verifies that generated Python examples are:
1. Syntactically correct (can be parsed)
2. Runnable (can be executed without errors)
3. Handle external library dependencies gracefully
Features:
- Syntax validation via ast.parse
- Execution in subprocess with timeout
- Detection of missing external libraries
- Detailed error reporting
"""
import ast
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple
@dataclass
class VerificationResult:
"""Result of example verification."""
success: bool
syntax_valid: bool
execution_passed: bool
output: str
error: str
missing_libraries: List[str]
requires_external: bool
@property
def can_write(self) -> bool:
"""Whether the example is safe to write."""
return self.syntax_valid and (self.execution_passed or self.requires_external)
class ExampleVerifier:
"""Verifies that generated examples are runnable."""
# Known external libraries that may not be installed
EXTERNAL_LIBRARIES = {
"requests", "httpx", "aiohttp", # HTTP
"pandas", "numpy", "scipy", # Data science
"torch", "tensorflow", "transformers", # ML
"redis", "pymongo", "psycopg2", # Databases
"boto3", "google-cloud", # Cloud
"playwright", "selenium", # Browser automation
"beautifulsoup4", "scrapy", # Web scraping
}
# Imports that indicate external dependencies
EXTERNAL_IMPORT_PATTERNS = [
"import requests",
"import httpx",
"import pandas",
"import numpy",
"import torch",
"import tensorflow",
"from transformers",
"import redis",
"import pymongo",
"import boto3",
]
# Patterns that indicate LLM API calls (will timeout during verification)
LLM_CALL_PATTERNS = [
"agent.start(",
"agents.start(",
".chat(",
".run(",
]
def __init__(self, timeout: int = 30):
self.timeout = timeout
def verify(self, code: str) -> VerificationResult:
"""
Verify that the code is valid and runnable.
Args:
code: Python source code to verify
Returns:
VerificationResult with details
"""
# Step 1: Syntax validation
syntax_valid, syntax_error = self._check_syntax(code)
if not syntax_valid:
return VerificationResult(
success=False,
syntax_valid=False,
execution_passed=False,
output="",
error=f"Syntax error: {syntax_error}",
missing_libraries=[],
requires_external=False,
)
# Step 2: Check for external dependencies
requires_external, external_libs = self._detect_external_deps(code)
# Step 3: Skip execution for examples with LLM calls (they will timeout)
if "LLM_API_CALL" in external_libs:
return VerificationResult(
success=True,
syntax_valid=True,
execution_passed=False,
output="",
error="Skipped: Example makes LLM API calls",
missing_libraries=["praisonaiagents (LLM API)"],
requires_external=True,
)
# Step 4: Execute the code
execution_passed, output, error, missing_libs = self._execute(code)
# Combine missing libraries
all_missing = list(set(missing_libs + external_libs))
return VerificationResult(
success=execution_passed or requires_external,
syntax_valid=True,
execution_passed=execution_passed,
output=output,
error=error,
missing_libraries=all_missing,
requires_external=requires_external and not execution_passed,
)
def _check_syntax(self, code: str) -> Tuple[bool, str]:
"""Check if code is syntactically valid."""
try:
ast.parse(code)
return True, ""
except SyntaxError as e:
return False, f"Line {e.lineno}: {e.msg}"
def _detect_external_deps(self, code: str) -> Tuple[bool, List[str]]:
"""Detect if code requires external libraries or makes LLM calls."""
external_libs = []
for pattern in self.EXTERNAL_IMPORT_PATTERNS:
if pattern in code:
lib_name = pattern.split()[-1]
external_libs.append(lib_name)
# Check for LLM API calls that will timeout during verification
for pattern in self.LLM_CALL_PATTERNS:
if pattern in code:
external_libs.append("LLM_API_CALL")
break
return len(external_libs) > 0, external_libs
def _execute(self, code: str) -> Tuple[bool, str, str, List[str]]:
"""Execute code in a subprocess."""
missing_libs = []
# Write code to temp file
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write(code)
temp_path = f.name
try:
# Run with timeout
result = subprocess.run(
[sys.executable, temp_path],
capture_output=True,
text=True,
timeout=self.timeout,
env={**subprocess.os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
)
output = result.stdout
error = result.stderr
# Check for import errors
if result.returncode != 0:
if "ModuleNotFoundError" in error or "ImportError" in error:
# Extract module name
for line in error.split("\n"):
if "No module named" in line:
# Extract module name from error
parts = line.split("'")
if len(parts) >= 2:
missing_libs.append(parts[1].split(".")[0])
return False, output, error, missing_libs
return True, output, error, missing_libs
except subprocess.TimeoutExpired:
return False, "", f"Execution timed out after {self.timeout}s", missing_libs
except Exception as e:
return False, "", str(e), missing_libs
finally:
# Clean up temp file
Path(temp_path).unlink(missing_ok=True)
def format_result(self, result: VerificationResult) -> str:
"""Format verification result for display."""
lines = []
if result.success:
lines.append("✅ Verification PASSED")
else:
lines.append("❌ Verification FAILED")
lines.append(f" Syntax valid: {'✓' if result.syntax_valid else '✗'}")
lines.append(f" Execution passed: {'✓' if result.execution_passed else '✗'}")
if result.requires_external:
lines.append(f" ⚠️ Requires external libraries: {', '.join(result.missing_libraries)}")
lines.append(" (Will be written with warning)")
if result.error and not result.requires_external:
lines.append(f" Error: {result.error[:200]}")
if result.output:
lines.append(f" Output preview: {result.output[:100]}...")
return "\n".join(lines)
@@ -0,0 +1,201 @@
"""
Manifest schema and storage for the FDEP standardisation system.
Manifests are stored in .praison/features/{slug}.yaml
"""
from pathlib import Path
from typing import List, Optional
from .models import FeatureManifest, FeatureSlug
class ManifestStorage:
"""Manages feature manifest files."""
def __init__(self, manifest_dir: Optional[Path] = None):
self.manifest_dir = manifest_dir or Path(".praison/features")
def get_manifest_path(self, slug: FeatureSlug) -> Path:
"""Get the path to a manifest file."""
return self.manifest_dir / f"{slug.normalised}.yaml"
def load(self, slug: FeatureSlug) -> Optional[FeatureManifest]:
"""Load a manifest from disk."""
path = self.get_manifest_path(slug)
if not path.exists():
return None
try:
import yaml
except ImportError:
return self._load_simple(path, slug)
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return FeatureManifest.from_dict(data)
except Exception:
return None
def _load_simple(self, path: Path, slug: FeatureSlug) -> Optional[FeatureManifest]:
"""Simple YAML loading without PyYAML dependency."""
try:
content = path.read_text(encoding="utf-8")
data = {}
for line in content.split("\n"):
line = line.strip()
if ":" in line and not line.startswith("#"):
key, value = line.split(":", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
data[key] = value
return FeatureManifest.from_dict(data)
except Exception:
return None
def save(self, manifest: FeatureManifest) -> Path:
"""Save a manifest to disk."""
path = self.get_manifest_path(manifest.slug)
path.parent.mkdir(parents=True, exist_ok=True)
try:
import yaml
content = yaml.dump(manifest.to_dict(), default_flow_style=False, sort_keys=False)
except ImportError:
content = self._to_simple_yaml(manifest)
path.write_text(content, encoding="utf-8")
return path
def _to_simple_yaml(self, manifest: FeatureManifest) -> str:
"""Convert manifest to simple YAML without PyYAML."""
lines = []
data = manifest.to_dict()
for key, value in data.items():
if value is None:
continue
if isinstance(value, list):
if value:
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
elif isinstance(value, dict):
if value:
lines.append(f"{key}:")
for k, v in value.items():
lines.append(f" {k}: {v}")
else:
lines.append(f"{key}: {value}")
return "\n".join(lines)
def list_all(self) -> List[FeatureManifest]:
"""List all manifests."""
manifests = []
if not self.manifest_dir.exists():
return manifests
for yaml_file in self.manifest_dir.glob("*.yaml"):
slug = FeatureSlug.from_path(yaml_file, "manifest")
manifest = self.load(slug)
if manifest:
manifests.append(manifest)
return manifests
def delete(self, slug: FeatureSlug) -> bool:
"""Delete a manifest."""
path = self.get_manifest_path(slug)
if path.exists():
path.unlink()
return True
return False
def exists(self, slug: FeatureSlug) -> bool:
"""Check if a manifest exists."""
return self.get_manifest_path(slug).exists()
def create_default(self, slug: FeatureSlug,
sdk_module: Optional[str] = None,
cli_commands: Optional[List[str]] = None) -> FeatureManifest:
"""Create a default manifest for a feature."""
name = slug.normalised.replace("-", " ").title()
return FeatureManifest(
slug=slug,
name=name,
description=f"{name} feature for PraisonAI",
status="stable",
sdk_module=sdk_module or slug.normalised,
cli_commands=cli_commands or [],
)
# Schema for manifest validation
MANIFEST_SCHEMA = {
"type": "object",
"required": ["slug", "name", "description"],
"properties": {
"slug": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{0,62}[a-z0-9]$|^[a-z]$",
"description": "Feature slug (lowercase alphanumeric + hyphens)",
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Human-readable feature name",
},
"description": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "Brief description of the feature",
},
"status": {
"type": "string",
"enum": ["stable", "beta", "experimental", "deprecated"],
"default": "stable",
},
"min_version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$",
"description": "Minimum PraisonAI version required",
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tags for categorisation",
},
"related_features": {
"type": "array",
"items": {"type": "string"},
"description": "Related feature slugs",
},
"sdk_module": {
"type": "string",
"description": "SDK module name (e.g., 'guardrails')",
},
"cli_commands": {
"type": "array",
"items": {"type": "string"},
"description": "CLI commands for this feature",
},
"api_classes": {
"type": "array",
"items": {"type": "string"},
"description": "Public API classes",
},
"artifacts": {
"type": "object",
"description": "Paths to artifacts",
"additionalProperties": {"type": "string"},
},
},
}
@@ -0,0 +1,355 @@
"""
Data models for the FDEP standardisation system.
All models use dataclasses for lightweight, stdlib-only implementation.
"""
import re
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional
class ArtifactType(Enum):
"""Types of required artifacts per feature."""
DOCS_CONCEPT = "docs_concept"
DOCS_FEATURE = "docs_feature"
DOCS_CLI = "docs_cli"
DOCS_SDK = "docs_sdk"
EXAMPLE_BASIC = "example_basic"
EXAMPLE_ADVANCED = "example_advanced"
MANIFEST = "manifest"
class ArtifactStatus(Enum):
"""Status of an artifact."""
PRESENT = "present"
MISSING = "missing"
DUPLICATE = "duplicate"
OUTDATED = "outdated"
class IssueType(Enum):
"""Types of standardisation issues."""
MISSING_ARTIFACT = "missing_artifact"
DUPLICATE_CONTENT = "duplicate_content"
NAMING_INCONSISTENCY = "naming_inconsistency"
DRIFT_SDK_DOCS = "drift_sdk_docs"
DRIFT_CLI_DOCS = "drift_cli_docs"
INVALID_FRONTMATTER = "invalid_frontmatter"
# Slug validation regex: lowercase alphanumeric + hyphens, 2-64 chars
SLUG_PATTERN = re.compile(r"^[a-z][a-z0-9-]{0,62}[a-z0-9]$|^[a-z]$")
# Singular to plural mappings for normalisation
SINGULAR_PLURAL_MAP = {
"agent": "agents",
"task": "tasks",
"tool": "tools",
"guardrail": "guardrails",
"handoff": "handoffs",
"workflow": "workflows",
"checkpoint": "checkpoints",
"hook": "hooks",
"skill": "skills",
"plugin": "plugins",
"session": "sessions",
"process": "processes",
}
# Legacy alias mappings
LEGACY_ALIASES = {
"rag": "knowledge",
"retrieval": "knowledge",
"llm": "models",
"model": "models",
"db": "databases",
"database": "databases",
}
@dataclass
class FeatureSlug:
"""
Represents a normalised feature slug.
Slug rules:
- Lowercase alphanumeric + hyphens only
- 1-64 characters
- Stable normalisation (singular/plural, legacy aliases)
"""
raw: str
normalised: str
is_valid: bool
validation_error: Optional[str] = None
@classmethod
def from_string(cls, value: str) -> "FeatureSlug":
"""Create a FeatureSlug from a raw string."""
raw = value.strip().lower()
# Replace underscores with hyphens
normalised = raw.replace("_", "-")
# Remove .py, .mdx extensions if present
for ext in (".py", ".mdx", ".md"):
if normalised.endswith(ext):
normalised = normalised[:-len(ext)]
# Apply legacy alias mapping
if normalised in LEGACY_ALIASES:
normalised = LEGACY_ALIASES[normalised]
# Apply singular to plural normalisation (prefer plural for collections)
if normalised in SINGULAR_PLURAL_MAP:
normalised = SINGULAR_PLURAL_MAP[normalised]
# Validate
is_valid = bool(SLUG_PATTERN.match(normalised))
validation_error = None
if not is_valid:
if not normalised:
validation_error = "Slug cannot be empty"
elif not normalised[0].isalpha():
validation_error = "Slug must start with a letter"
elif len(normalised) > 64:
validation_error = "Slug must be 64 characters or less"
else:
validation_error = "Slug must contain only lowercase letters, numbers, and hyphens"
return cls(raw=raw, normalised=normalised, is_valid=is_valid,
validation_error=validation_error)
@classmethod
def from_path(cls, path: Path, base_type: str = "unknown") -> "FeatureSlug":
"""
Derive a slug from a file or directory path.
Args:
path: Path to derive slug from
base_type: Type hint (sdk, cli, docs, examples)
"""
# Use the stem (filename without extension) or directory name
name = path.stem if path.is_file() else path.name
return cls.from_string(name)
def __str__(self) -> str:
return self.normalised
def __hash__(self) -> int:
return hash(self.normalised)
def __eq__(self, other) -> bool:
if isinstance(other, FeatureSlug):
return self.normalised == other.normalised
if isinstance(other, str):
return self.normalised == other.lower()
return False
@dataclass
class ArtifactPath:
"""Represents a path to a required artifact."""
artifact_type: ArtifactType
expected_path: Path
actual_path: Optional[Path] = None
status: ArtifactStatus = ArtifactStatus.MISSING
@property
def exists(self) -> bool:
return self.actual_path is not None and self.actual_path.exists()
@dataclass
class FeatureManifest:
"""
Manifest entry for a single feature.
This is the source of truth for what artifacts a feature should have.
"""
slug: FeatureSlug
name: str
description: str
status: str = "stable" # stable, beta, experimental, deprecated
min_version: Optional[str] = None
tags: List[str] = field(default_factory=list)
related_features: List[str] = field(default_factory=list)
sdk_module: Optional[str] = None
cli_commands: List[str] = field(default_factory=list)
api_classes: List[str] = field(default_factory=list)
artifacts: Dict[str, str] = field(default_factory=dict)
def to_dict(self) -> dict:
"""Convert to dictionary for YAML serialisation."""
return {
"slug": self.slug.normalised,
"name": self.name,
"description": self.description,
"status": self.status,
"min_version": self.min_version,
"tags": self.tags,
"related_features": self.related_features,
"sdk_module": self.sdk_module,
"cli_commands": self.cli_commands,
"api_classes": self.api_classes,
"artifacts": self.artifacts,
}
@classmethod
def from_dict(cls, data: dict) -> "FeatureManifest":
"""Create from dictionary."""
slug = FeatureSlug.from_string(data.get("slug", ""))
return cls(
slug=slug,
name=data.get("name", slug.normalised.replace("-", " ").title()),
description=data.get("description", ""),
status=data.get("status", "stable"),
min_version=data.get("min_version"),
tags=data.get("tags", []),
related_features=data.get("related_features", []),
sdk_module=data.get("sdk_module"),
cli_commands=data.get("cli_commands", []),
api_classes=data.get("api_classes", []),
artifacts=data.get("artifacts", {}),
)
@dataclass
class ValidationResult:
"""Result of validating a feature's artifacts."""
slug: FeatureSlug
is_valid: bool
missing_artifacts: List[ArtifactType] = field(default_factory=list)
present_artifacts: List[ArtifactPath] = field(default_factory=list)
issues: List[str] = field(default_factory=list)
@property
def missing_count(self) -> int:
return len(self.missing_artifacts)
@property
def present_count(self) -> int:
return len(self.present_artifacts)
@dataclass
class DuplicateCluster:
"""A cluster of duplicate or near-duplicate content."""
slug: FeatureSlug
pages: List[Path]
similarity_score: float # 0.0 to 1.0
issue_type: str # "same_slug", "title_similarity", "content_overlap"
recommendation: str # "merge", "keep_both", "review"
primary_page: Optional[Path] = None # Recommended canonical page
def __str__(self) -> str:
return f"DuplicateCluster({self.slug}, {len(self.pages)} pages, {self.issue_type})"
@dataclass
class DriftItem:
"""A single drift item (SDK export or CLI flag not in docs, or vice versa)."""
name: str
source: str # "sdk", "cli", "docs"
missing_in: str # Where it's missing
item_type: str # "class", "function", "flag", "command"
@dataclass
class DriftReport:
"""Report of drift between code and documentation."""
slug: FeatureSlug
sdk_drift: List[DriftItem] = field(default_factory=list)
cli_drift: List[DriftItem] = field(default_factory=list)
@property
def has_drift(self) -> bool:
return bool(self.sdk_drift or self.cli_drift)
@property
def total_drift_items(self) -> int:
return len(self.sdk_drift) + len(self.cli_drift)
@dataclass
class StandardiseIssue:
"""A single standardisation issue."""
issue_type: IssueType
slug: Optional[FeatureSlug]
path: Optional[Path]
message: str
severity: str = "warning" # error, warning, info
auto_fixable: bool = False
fix_action: Optional[str] = None
@dataclass
class StandardiseReport:
"""Complete standardisation report."""
timestamp: str
features_scanned: int
docs_pages: int
examples_count: int
issues: List[StandardiseIssue] = field(default_factory=list)
duplicates: List[DuplicateCluster] = field(default_factory=list)
missing_artifacts: Dict[str, List[ArtifactType]] = field(default_factory=dict)
drift_reports: List[DriftReport] = field(default_factory=list)
validation_results: List[ValidationResult] = field(default_factory=list)
@property
def total_issues(self) -> int:
return len(self.issues)
@property
def total_missing(self) -> int:
return sum(len(v) for v in self.missing_artifacts.values())
@property
def total_duplicates(self) -> int:
return len(self.duplicates)
@property
def has_issues(self) -> bool:
return self.total_issues > 0 or self.total_missing > 0 or self.total_duplicates > 0
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialisation."""
return {
"timestamp": self.timestamp,
"summary": {
"features_scanned": self.features_scanned,
"docs_pages": self.docs_pages,
"examples_count": self.examples_count,
"total_issues": self.total_issues,
"total_missing": self.total_missing,
"total_duplicates": self.total_duplicates,
},
"issues": [
{
"type": i.issue_type.value,
"slug": str(i.slug) if i.slug else None,
"path": str(i.path) if i.path else None,
"message": i.message,
"severity": i.severity,
"auto_fixable": i.auto_fixable,
}
for i in self.issues
],
"duplicates": [
{
"slug": str(d.slug),
"pages": [str(p) for p in d.pages],
"similarity": d.similarity_score,
"type": d.issue_type,
"recommendation": d.recommendation,
}
for d in self.duplicates
],
"missing_artifacts": {
slug: [a.value for a in artifacts]
for slug, artifacts in self.missing_artifacts.items()
},
}
@@ -0,0 +1,176 @@
"""
Report generators for the FDEP standardisation system.
Generates reports in:
- Text (terminal-friendly)
- JSON (machine-readable)
- Markdown (documentation-friendly)
"""
import json
from pathlib import Path
from .models import StandardiseReport
class ReportGenerator:
"""Generates standardisation reports in various formats."""
def generate(self, report: StandardiseReport, format: str = "text") -> str:
"""Generate a report in the specified format."""
if format == "text":
return self._generate_text(report)
elif format == "json":
return self._generate_json(report)
elif format == "markdown":
return self._generate_markdown(report)
else:
raise ValueError(f"Unknown format: {format}")
def _generate_text(self, report: StandardiseReport) -> str:
"""Generate a text report for terminal output."""
lines = []
# Header
lines.append("")
lines.append("🔍 PraisonAI Standardisation Check")
lines.append("══════════════════════════════════")
lines.append("")
# Summary
lines.append("📊 Summary")
lines.append("──────────")
lines.append(f"Features scanned: {report.features_scanned}")
lines.append(f"Docs pages: {report.docs_pages}")
lines.append(f"Examples: {report.examples_count}")
lines.append("")
if not report.has_issues:
lines.append("✅ No issues found!")
return "\n".join(lines)
lines.append(f"⚠️ Issues Found: {report.total_issues + report.total_missing + report.total_duplicates}")
lines.append("──────────────────────")
lines.append("")
# Duplicates
if report.duplicates:
lines.append(f"🔴 DUPLICATES ({len(report.duplicates)} clusters)")
for cluster in report.duplicates:
pages_str = ", ".join(self._short_path(p) for p in cluster.pages[:3])
if len(cluster.pages) > 3:
pages_str += f" (+{len(cluster.pages) - 3} more)"
lines.append(f" • {cluster.slug}: {len(cluster.pages)} pages ({cluster.issue_type})")
lines.append("")
# Missing artifacts
if report.missing_artifacts:
lines.append(f"🟡 MISSING ARTIFACTS ({len(report.missing_artifacts)} features)")
for slug, artifacts in list(report.missing_artifacts.items())[:10]:
artifact_names = [a.value.replace("_", " ") for a in artifacts]
lines.append(f" • {slug}: missing {', '.join(artifact_names[:3])}")
if len(artifact_names) > 3:
lines.append(f" (+{len(artifact_names) - 3} more)")
if len(report.missing_artifacts) > 10:
lines.append(f" ... and {len(report.missing_artifacts) - 10} more features")
lines.append("")
# Other issues
if report.issues:
naming_issues = [i for i in report.issues if "naming" in i.message.lower()]
if naming_issues:
lines.append(f"🟢 NAMING ISSUES ({len(naming_issues)})")
for issue in naming_issues[:5]:
lines.append(f" • {issue.message}")
lines.append("")
# Next actions
lines.append("📋 Next Actions")
lines.append("───────────────")
lines.append("1. Run `praisonai standardise report --format markdown` for details")
if report.duplicates:
lines.append("2. Run `praisonai standardise fix --feature <slug>` to consolidate")
if report.missing_artifacts:
lines.append("3. Run `praisonai standardise init --feature <slug>` to create missing")
lines.append("")
return "\n".join(lines)
def _generate_json(self, report: StandardiseReport) -> str:
"""Generate a JSON report."""
return json.dumps(report.to_dict(), indent=2)
def _generate_markdown(self, report: StandardiseReport) -> str:
"""Generate a Markdown report."""
lines = []
# Header
lines.append("# PraisonAI Standardisation Report")
lines.append("")
lines.append(f"Generated: {report.timestamp}")
lines.append("")
# Summary table
lines.append("## Summary")
lines.append("")
lines.append("| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| Features scanned | {report.features_scanned} |")
lines.append(f"| Docs pages | {report.docs_pages} |")
lines.append(f"| Examples | {report.examples_count} |")
lines.append(f"| Total issues | {report.total_issues} |")
lines.append(f"| Missing artifacts | {report.total_missing} |")
lines.append(f"| Duplicate clusters | {report.total_duplicates} |")
lines.append("")
# Duplicates section
if report.duplicates:
lines.append("## Duplicate Clusters")
lines.append("")
for cluster in report.duplicates:
lines.append(f"### {cluster.slug}")
lines.append("")
lines.append(f"- **Type**: {cluster.issue_type}")
lines.append(f"- **Similarity**: {cluster.similarity_score:.0%}")
lines.append(f"- **Recommendation**: {cluster.recommendation}")
lines.append("")
lines.append("**Pages:**")
for page in cluster.pages:
lines.append(f"- `{page}`")
lines.append("")
# Missing artifacts section
if report.missing_artifacts:
lines.append("## Missing Artifacts")
lines.append("")
lines.append("| Feature | Missing |")
lines.append("|---------|---------|")
for slug, artifacts in report.missing_artifacts.items():
artifact_names = ", ".join(a.value for a in artifacts)
lines.append(f"| {slug} | {artifact_names} |")
lines.append("")
# Issues section
if report.issues:
lines.append("## Other Issues")
lines.append("")
for issue in report.issues:
severity_icon = {"error": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
lines.append(f"- {severity_icon} **{issue.issue_type.value}**: {issue.message}")
lines.append("")
return "\n".join(lines)
def _short_path(self, path: Path) -> str:
"""Get a shortened path for display."""
parts = path.parts
if len(parts) > 3:
return "/".join(parts[-3:])
return str(path)
def save(self, report: StandardiseReport, output_path: Path,
format: str = "text") -> None:
"""Save a report to a file."""
content = self.generate(report, format)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content, encoding="utf-8")
@@ -0,0 +1,399 @@
"""
Template generators for the FDEP standardisation system.
Generates:
- Example files (basic and advanced)
- Docs pages (concept, feature, cli, sdk)
"""
from pathlib import Path
from typing import Optional
from .models import ArtifactType, FeatureSlug
class TemplateGenerator:
"""Generates template files for features."""
def generate(self, slug: FeatureSlug, artifact_type: ArtifactType,
output_path: Optional[Path] = None) -> str:
"""Generate template content for an artifact type."""
if artifact_type == ArtifactType.EXAMPLE_BASIC:
return self._generate_basic_example(slug)
elif artifact_type == ArtifactType.EXAMPLE_ADVANCED:
return self._generate_advanced_example(slug)
elif artifact_type == ArtifactType.DOCS_CONCEPT:
return self._generate_concept_doc(slug)
elif artifact_type == ArtifactType.DOCS_FEATURE:
return self._generate_feature_doc(slug)
elif artifact_type == ArtifactType.DOCS_CLI:
return self._generate_cli_doc(slug)
elif artifact_type == ArtifactType.DOCS_SDK:
return self._generate_sdk_doc(slug)
else:
raise ValueError(f"Unknown artifact type: {artifact_type}")
def _generate_basic_example(self, slug: FeatureSlug) -> str:
"""Generate a basic example template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
return f'''"""
{name} - Basic Example
Demonstrates minimal usage of {slug_str} with a single agent.
Expected Output:
Agent response demonstrating {slug_str} functionality.
"""
from praisonaiagents import Agent
# Basic {slug_str} usage
agent = Agent(
instructions="You are a helpful assistant",
# {slug_str}=... # Configure {slug_str} here
)
# Run the agent
result = agent.start("Hello! Demonstrate {slug_str} functionality.")
print(result)
'''
def _generate_advanced_example(self, slug: FeatureSlug) -> str:
"""Generate an advanced example template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
return f'''"""
{name} - Advanced Example
Demonstrates advanced {slug_str} patterns including:
- Multi-agent configuration
- Custom callbacks/handlers
- Error handling
- All available options
Expected Output:
Comprehensive demonstration of {slug_str} with multiple agents.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
# ============================================================
# Section 1: Custom Configuration
# ============================================================
# Define custom {slug_str} configuration
# config = ...
# ============================================================
# Section 2: Multi-Agent Setup
# ============================================================
agent1 = Agent(
name="Agent1",
instructions="You are the first agent",
# {slug_str}=config,
)
agent2 = Agent(
name="Agent2",
instructions="You are the second agent",
# {slug_str}=config,
)
# ============================================================
# Section 3: Task Definition
# ============================================================
task = Task(
description="Demonstrate {slug_str} 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
# ============================================================
"""
{name} Options:
──────────────────
| Option | Type | Default | Description |
|-----------------|----------|---------|----------------------|
| option1 | str | None | Description here |
| option2 | bool | False | Description here |
"""
'''
def _generate_concept_doc(self, slug: FeatureSlug) -> str:
"""Generate a concept documentation template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
return f'''---
title: "{name}"
description: "Understanding {slug_str} in PraisonAI"
icon: "lightbulb"
---
## Overview
{name} provides [brief description of what this feature does and why it exists].
```mermaid
graph LR
A[Input] --> B[{name}]
B --> C[Output]
```
## When to Use
<CardGroup cols={{2}}>
<Card title="Use When" icon="check">
- Use case 1
- Use case 2
</Card>
<Card title="Don't Use When" icon="xmark">
- Anti-pattern 1
- Anti-pattern 2
</Card>
</CardGroup>
## How It Works
[Brief explanation of the mechanism, 3-5 sentences max.]
## Related Features
- [Related Feature 1](/docs/concepts/related1)
- [Related Feature 2](/docs/concepts/related2)
'''
def _generate_feature_doc(self, slug: FeatureSlug) -> str:
"""Generate a feature documentation template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
return f'''---
title: "{name}"
description: "How to use {slug_str} in PraisonAI"
icon: "code"
---
## Quick Start
```python
from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant",
# {slug_str}=...
)
result = agent.start("Hello!")
```
## Basic Usage
<Steps>
<Step title="Step 1: Configure">
Configure {slug_str} for your agent.
```python
# Configuration code
```
</Step>
<Step title="Step 2: Run">
Run the agent with {slug_str} enabled.
</Step>
</Steps>
## Advanced Patterns
<Tabs>
<Tab title="Pattern 1">
```python
# Pattern 1 code
```
</Tab>
<Tab title="Pattern 2">
```python
# Pattern 2 code
```
</Tab>
</Tabs>
## All Options
<ParamField body="option1" type="string" required>
Description of option1.
</ParamField>
<ParamField body="option2" type="boolean" default="false">
Description of option2.
</ParamField>
## See Also
- [CLI Reference](/docs/cli/{slug_str})
- [API Reference](/docs/sdk/praisonaiagents/{slug_str}/{slug_str})
'''
def _generate_cli_doc(self, slug: FeatureSlug) -> str:
"""Generate a CLI documentation template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
return f'''---
title: "{name}"
description: "CLI commands for {slug_str}"
icon: "terminal"
---
## Quick Start
```bash
praisonai "Your prompt" --{slug_str}
```
## Commands
### `--{slug_str}`
Enable {slug_str} functionality.
```bash
praisonai "Your prompt" --{slug_str}
```
**Expected Output:**
```
[Example output here]
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PRAISONAI_{slug_str.upper().replace("-", "_")}` | Configure {slug_str} | None |
## Examples
### Basic
```bash
praisonai "Hello" --{slug_str}
```
### With Other Flags
```bash
praisonai "Hello" --{slug_str} --verbose
```
## Troubleshooting
<Accordion title="Common Issue 1">
Solution for common issue 1.
</Accordion>
'''
def _generate_sdk_doc(self, slug: FeatureSlug) -> str:
"""Generate an SDK documentation template."""
name = slug.normalised.replace("-", " ").title()
slug_str = slug.normalised
class_name = "".join(word.title() for word in slug_str.split("-"))
return f'''---
title: "{name} Module"
description: "API reference for {slug_str}"
icon: "code"
---
# {name}
{name} module provides [brief description].
## Quick Start
```python
from praisonaiagents import {class_name}
# Basic usage
instance = {class_name}()
```
## Classes
### `{class_name}`
Main class for {slug_str} functionality.
#### Constructor
```python
{class_name}(
param1: str,
param2: bool = False
)
```
#### Parameters
<ParamField body="param1" type="string" required>
Description of param1.
</ParamField>
<ParamField body="param2" type="boolean" default="false">
Description of param2.
</ParamField>
#### Methods
##### `method_name()`
Description of the method.
```python
result = instance.method_name(args)
```
**Returns:** `ReturnType` - Description of return value.
## Type Definitions
```python
# Type aliases used in this module
```
'''
def get_expected_path(self, slug: FeatureSlug, artifact_type: ArtifactType,
docs_root: Optional[Path] = None,
examples_root: Optional[Path] = None) -> Optional[Path]:
"""Get the expected path for an artifact."""
slug_str = slug.normalised
if artifact_type == ArtifactType.EXAMPLE_BASIC:
if examples_root:
return examples_root / slug_str / f"{slug_str}-basic.py"
elif artifact_type == ArtifactType.EXAMPLE_ADVANCED:
if examples_root:
return examples_root / slug_str / f"{slug_str}-advanced.py"
elif artifact_type == ArtifactType.DOCS_CONCEPT:
if docs_root:
return docs_root / "concepts" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_FEATURE:
if docs_root:
return docs_root / "features" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_CLI:
if docs_root:
return docs_root / "cli" / f"{slug_str}.mdx"
elif artifact_type == ArtifactType.DOCS_SDK:
if docs_root:
return docs_root / "sdk" / "praisonaiagents" / slug_str / f"{slug_str}.mdx"
return None
@@ -0,0 +1,232 @@
"""
Git-based undo/redo system for the FDEP standardisation system.
Provides:
- Checkpoint creation before changes
- Undo to previous checkpoint
- Redo to next checkpoint
- List checkpoints
"""
import subprocess
from datetime import datetime
from pathlib import Path
from typing import List, Optional, Tuple
class UndoRedoManager:
"""Git-based undo/redo for standardisation changes."""
CHECKPOINT_PREFIX = "standardise-checkpoint-"
def __init__(self, repo_path: Optional[Path] = None):
self.repo_path = repo_path or Path.cwd()
self._verify_git_repo()
def _verify_git_repo(self) -> bool:
"""Verify we're in a git repository."""
try:
result = subprocess.run(
["git", "rev-parse", "--git-dir"],
cwd=self.repo_path,
capture_output=True,
text=True,
)
return result.returncode == 0
except Exception:
return False
def _run_git(self, *args) -> Tuple[bool, str]:
"""Run a git command."""
try:
result = subprocess.run(
["git"] + list(args),
cwd=self.repo_path,
capture_output=True,
text=True,
)
return result.returncode == 0, result.stdout + result.stderr
except Exception as e:
return False, str(e)
def create_checkpoint(self, message: Optional[str] = None) -> Tuple[bool, str]:
"""
Create a checkpoint before making changes.
Returns:
Tuple of (success, checkpoint_id or error message)
"""
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
checkpoint_id = f"{self.CHECKPOINT_PREFIX}{timestamp}"
# Stage all changes
success, output = self._run_git("add", "-A")
if not success:
return False, f"Failed to stage changes: {output}"
# Check if there are changes to commit
success, output = self._run_git("diff", "--cached", "--quiet")
if success:
# No changes to commit, just create a tag at current HEAD
pass
else:
# Commit changes
commit_msg = message or f"Standardise checkpoint: {timestamp}"
success, output = self._run_git("commit", "-m", commit_msg)
if not success and "nothing to commit" not in output:
return False, f"Failed to commit: {output}"
# Create tag
tag_msg = message or f"Standardise checkpoint at {timestamp}"
success, output = self._run_git("tag", "-a", checkpoint_id, "-m", tag_msg)
if not success:
return False, f"Failed to create tag: {output}"
return True, checkpoint_id
def undo(self, checkpoint_id: Optional[str] = None) -> Tuple[bool, str]:
"""
Undo to a previous checkpoint.
Args:
checkpoint_id: Specific checkpoint to restore (default: previous)
Returns:
Tuple of (success, message)
"""
if checkpoint_id is None:
# Find the most recent checkpoint
checkpoints = self.list_checkpoints()
if len(checkpoints) < 2:
return False, "No previous checkpoint to undo to"
checkpoint_id = checkpoints[1][0] # Second most recent
# Verify checkpoint exists
success, output = self._run_git("rev-parse", checkpoint_id)
if not success:
return False, f"Checkpoint not found: {checkpoint_id}"
# Create a safety checkpoint before undoing
self.create_checkpoint("Auto-checkpoint before undo")
# Reset to checkpoint
success, output = self._run_git("reset", "--hard", checkpoint_id)
if not success:
return False, f"Failed to reset: {output}"
return True, f"Restored to checkpoint: {checkpoint_id}"
def redo(self) -> Tuple[bool, str]:
"""
Redo to the next checkpoint (after an undo).
Returns:
Tuple of (success, message)
"""
# Get reflog to find the previous HEAD
success, output = self._run_git("reflog", "-n", "10", "--format=%H %s")
if not success:
return False, "Failed to read reflog"
# Find the commit before the last reset
lines = output.strip().split("\n")
for i, line in enumerate(lines):
if "reset: moving to" in line and i > 0:
# The previous line has the commit we want to redo to
target_hash = lines[i - 1].split()[0]
success, output = self._run_git("reset", "--hard", target_hash)
if success:
return True, "Redo successful"
return False, f"Failed to redo: {output}"
return False, "No redo available"
def list_checkpoints(self) -> List[Tuple[str, str, str]]:
"""
List all standardise checkpoints.
Returns:
List of (checkpoint_id, date, message) tuples
"""
success, output = self._run_git(
"tag", "-l", f"{self.CHECKPOINT_PREFIX}*",
"--format=%(refname:short)|%(creatordate:short)|%(subject)"
)
if not success:
return []
checkpoints = []
for line in output.strip().split("\n"):
if line and "|" in line:
parts = line.split("|", 2)
if len(parts) >= 3:
checkpoints.append((parts[0], parts[1], parts[2]))
elif len(parts) == 2:
checkpoints.append((parts[0], parts[1], ""))
# Sort by date descending
checkpoints.sort(key=lambda x: x[1], reverse=True)
return checkpoints
def delete_checkpoint(self, checkpoint_id: str) -> Tuple[bool, str]:
"""Delete a checkpoint."""
success, output = self._run_git("tag", "-d", checkpoint_id)
if success:
return True, f"Deleted checkpoint: {checkpoint_id}"
return False, f"Failed to delete: {output}"
def get_changes_since(self, checkpoint_id: str) -> List[str]:
"""Get list of files changed since a checkpoint."""
success, output = self._run_git(
"diff", "--name-only", checkpoint_id, "HEAD"
)
if not success:
return []
return [f for f in output.strip().split("\n") if f]
def preview_undo(self, checkpoint_id: str) -> List[str]:
"""Preview what files would change if we undo to a checkpoint."""
return self.get_changes_since(checkpoint_id)
class FileBackupManager:
"""Simple file-based backup for non-git scenarios."""
def __init__(self, backup_dir: Optional[Path] = None):
self.backup_dir = backup_dir or Path(".praison/backups")
self.backup_dir.mkdir(parents=True, exist_ok=True)
def backup_file(self, file_path: Path) -> Optional[Path]:
"""Create a backup of a file."""
if not file_path.exists():
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{file_path.stem}_{timestamp}{file_path.suffix}"
backup_path = self.backup_dir / backup_name
import shutil
shutil.copy2(file_path, backup_path)
return backup_path
def restore_file(self, backup_path: Path, target_path: Path) -> bool:
"""Restore a file from backup."""
if not backup_path.exists():
return False
import shutil
shutil.copy2(backup_path, target_path)
return True
def list_backups(self, file_stem: Optional[str] = None) -> List[Path]:
"""List all backups, optionally filtered by file stem."""
backups = list(self.backup_dir.glob("*"))
if file_stem:
backups = [b for b in backups if b.stem.startswith(file_stem)]
backups.sort(key=lambda x: x.stat().st_mtime, reverse=True)
return backups
@@ -0,0 +1,283 @@
"""
Artifact validation for the FDEP standardisation system.
Validates that features have all required artifacts:
- docs/concepts/{slug}.mdx
- docs/features/{slug}.mdx
- docs/cli/{slug}.mdx
- docs/sdk/praisonaiagents/{module}/{slug}.mdx
- examples/python/{slug}/{slug}-basic.py
- examples/python/{slug}/{slug}-advanced.py
"""
from pathlib import Path
from typing import Dict, List, Optional
from .config import StandardiseConfig
from .discovery import FeatureDiscovery
from .models import (
ArtifactPath,
ArtifactStatus,
ArtifactType,
FeatureSlug,
ValidationResult,
)
class ArtifactValidator:
"""Validates that features have all required artifacts."""
def __init__(self, config: StandardiseConfig, discovery: FeatureDiscovery):
self.config = config
self.discovery = discovery
def validate_feature(self, slug: FeatureSlug) -> ValidationResult:
"""Validate all artifacts for a single feature."""
missing = []
present = []
issues = []
# Check each required artifact type
for artifact_type in ArtifactType:
if artifact_type == ArtifactType.MANIFEST:
continue # Manifest is optional
artifact_path = self._get_expected_path(slug, artifact_type)
if artifact_path is None:
continue
if artifact_path.exists:
present.append(ArtifactPath(
artifact_type=artifact_type,
expected_path=artifact_path.expected_path,
actual_path=artifact_path.actual_path,
status=ArtifactStatus.PRESENT,
))
else:
missing.append(artifact_type)
present.append(ArtifactPath(
artifact_type=artifact_type,
expected_path=artifact_path.expected_path,
actual_path=None,
status=ArtifactStatus.MISSING,
))
is_valid = len(missing) == 0
return ValidationResult(
slug=slug,
is_valid=is_valid,
missing_artifacts=missing,
present_artifacts=present,
issues=issues,
)
def validate_all(self) -> Dict[str, ValidationResult]:
"""Validate all discovered features."""
results = {}
all_features = self.discovery.get_all_features()
for slug in all_features:
if self.config.feature_filter:
if slug.normalised != self.config.feature_filter:
continue
results[slug.normalised] = self.validate_feature(slug)
return results
def get_missing_artifacts(self) -> Dict[str, List[ArtifactType]]:
"""Get all missing artifacts across all features."""
missing = {}
results = self.validate_all()
for slug_str, result in results.items():
if result.missing_artifacts:
missing[slug_str] = result.missing_artifacts
return missing
def _get_expected_path(self, slug: FeatureSlug,
artifact_type: ArtifactType) -> Optional[ArtifactPath]:
"""Get the expected path for an artifact."""
slug_str = slug.normalised
if artifact_type == ArtifactType.DOCS_CONCEPT:
if not self.config.docs_root:
return None
expected = self.config.docs_root / "concepts" / f"{slug_str}.mdx"
actual = self._find_actual_path(expected, slug, "concepts")
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
elif artifact_type == ArtifactType.DOCS_FEATURE:
if not self.config.docs_root:
return None
expected = self.config.docs_root / "features" / f"{slug_str}.mdx"
actual = self._find_actual_path(expected, slug, "features")
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
elif artifact_type == ArtifactType.DOCS_CLI:
if not self.config.docs_root:
return None
expected = self.config.docs_root / "cli" / f"{slug_str}.mdx"
actual = self._find_actual_path(expected, slug, "cli")
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
elif artifact_type == ArtifactType.DOCS_SDK:
if not self.config.docs_root:
return None
# SDK docs are in docs/sdk/praisonaiagents/{module}/
sdk_docs = self.config.docs_root / "sdk" / "praisonaiagents"
expected = sdk_docs / slug_str / f"{slug_str}.mdx"
actual = self._find_sdk_docs_path(slug, sdk_docs)
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
elif artifact_type == ArtifactType.EXAMPLE_BASIC:
if not self.config.examples_root:
return None
expected = self.config.examples_root / slug_str / f"{slug_str}-basic.py"
actual = self._find_example_path(slug, "basic")
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
elif artifact_type == ArtifactType.EXAMPLE_ADVANCED:
if not self.config.examples_root:
return None
expected = self.config.examples_root / slug_str / f"{slug_str}-advanced.py"
actual = self._find_example_path(slug, "advanced")
return ArtifactPath(
artifact_type=artifact_type,
expected_path=expected,
actual_path=actual,
status=ArtifactStatus.PRESENT if actual else ArtifactStatus.MISSING,
)
return None
def _find_actual_path(self, expected: Path, slug: FeatureSlug,
doc_type: str) -> Optional[Path]:
"""Find the actual path, checking variants."""
if expected.exists():
return expected
if not self.config.docs_root:
return None
# Check variants
slug_str = slug.normalised
variants = [
slug_str,
slug_str.replace("-", "_"),
slug_str.replace("-", ""),
]
# Add singular/plural variants
from .models import SINGULAR_PLURAL_MAP
for singular, plural in SINGULAR_PLURAL_MAP.items():
if slug_str == plural:
variants.append(singular)
elif slug_str == singular:
variants.append(plural)
doc_dir = self.config.docs_root / doc_type
if not doc_dir.exists():
return None
for variant in variants:
path = doc_dir / f"{variant}.mdx"
if path.exists():
return path
return None
def _find_sdk_docs_path(self, slug: FeatureSlug,
sdk_docs: Path) -> Optional[Path]:
"""Find SDK docs path, checking module directories."""
if not sdk_docs.exists():
return None
slug_str = slug.normalised
variants = [
slug_str,
slug_str.replace("-", "_"),
]
# Check for module directory
for variant in variants:
module_dir = sdk_docs / variant
if module_dir.exists() and module_dir.is_dir():
# Look for any .mdx file in the module directory
for mdx_file in module_dir.glob("*.mdx"):
return mdx_file
return None
def _find_example_path(self, slug: FeatureSlug,
example_type: str) -> Optional[Path]:
"""Find example path, checking variants."""
if not self.config.examples_root:
return None
slug_str = slug.normalised
variants = [
slug_str,
slug_str.replace("-", "_"),
]
for variant in variants:
example_dir = self.config.examples_root / variant
if not example_dir.exists():
continue
# Check for basic/advanced files with various naming patterns
patterns = [
f"{variant}-{example_type}.py",
f"{variant}_{example_type}.py",
f"{example_type}.py",
f"{example_type}_{variant}.py",
]
for pattern in patterns:
path = example_dir / pattern
if path.exists():
return path
# If looking for basic, any simple example file counts
if example_type == "basic":
for py_file in example_dir.glob("*.py"):
if "advanced" not in py_file.name.lower():
return py_file
# If looking for advanced, any advanced/complex example counts
if example_type == "advanced":
for py_file in example_dir.glob("*.py"):
name_lower = py_file.name.lower()
if any(x in name_lower for x in ["advanced", "complex", "multi"]):
return py_file
return None
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.6.2"
__version__ = "3.7.4"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.10.10",
"praisonaiagents>=0.11.2",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""
Generate api.md for PraisonAI.
This script generates a comprehensive API reference document
covering all public exports from praisonaiagents, praisonai, CLI, and TypeScript.
Usage:
./scripts/generate_api_md.py # Generate api.md
./scripts/generate_api_md.py --check # Check if api.md is up to date
./scripts/generate_api_md.py --stdout # Print to stdout
"""
import sys
from pathlib import Path
# Add the praisonai package to path
# Script is in src/praisonai/scripts/, so go up 3 levels to repo root
repo_root = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(repo_root / "src" / "praisonai"))
from praisonai._dev.api_md import generate_api_md
if __name__ == '__main__':
# Parse simple args
check = '--check' in sys.argv
stdout = '--stdout' in sys.argv
exit_code = generate_api_md(
repo_root=repo_root,
check=check,
stdout=stdout
)
sys.exit(exit_code)
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# CI script for standardisation checks
# Usage: ./scripts/standardise_check.sh [--strict]
#
# Exit codes:
# 0 - No issues found
# 1 - Issues found
# 2 - Error running check
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Check if strict mode
STRICT_MODE=false
if [[ "$1" == "--strict" ]]; then
STRICT_MODE=true
fi
echo "🔍 Running PraisonAI Standardisation Check..."
echo ""
# Run the check
cd "$PROJECT_ROOT"
python -m praisonai.cli.commands.standardise check --ci --path "$PROJECT_ROOT"
EXIT_CODE=$?
if [[ $EXIT_CODE -eq 0 ]]; then
echo ""
echo "✅ Standardisation check passed!"
exit 0
elif [[ $EXIT_CODE -eq 1 ]]; then
echo ""
echo "⚠️ Standardisation issues found."
if [[ "$STRICT_MODE" == "true" ]]; then
echo "Failing in strict mode."
exit 1
else
echo "Run 'praisonai standardise report' for details."
exit 0 # Non-strict mode: warn but don't fail
fi
else
echo ""
echo "❌ Error running standardisation check."
exit 2
fi
@@ -0,0 +1 @@
"""Tests for CLI UI backends."""
@@ -0,0 +1,143 @@
"""
Tests for UI backend selection logic.
Verifies:
1. Auto-selection picks correct backend based on environment
2. Non-TTY forces PlainBackend
3. --json forces PlainBackend
4. PRAISONAI_UI_SAFE=1 forces PlainBackend
5. Explicit --ui flag overrides auto-selection
6. Fallback chain works when deps missing
"""
import os
import sys
from unittest.mock import patch
class TestBackendSelection:
"""Test suite for UI backend auto-selection."""
def test_select_backend_returns_plain_for_non_tty(self):
"""Non-TTY stdout should select PlainBackend."""
from praisonai.cli.ui import select_backend, UIConfig
with patch.object(sys.stdout, 'isatty', return_value=False):
config = UIConfig()
backend = select_backend(config)
assert backend.__class__.__name__ == 'PlainBackend'
def test_select_backend_returns_plain_for_json_mode(self):
"""--json flag should force PlainBackend."""
from praisonai.cli.ui import select_backend, UIConfig
config = UIConfig(json_output=True)
backend = select_backend(config)
assert backend.__class__.__name__ == 'PlainBackend'
def test_select_backend_returns_plain_for_safe_mode(self):
"""PRAISONAI_UI_SAFE=1 should force PlainBackend."""
from praisonai.cli.ui import select_backend, UIConfig
with patch.dict(os.environ, {'PRAISONAI_UI_SAFE': '1'}):
config = UIConfig()
backend = select_backend(config)
assert backend.__class__.__name__ == 'PlainBackend'
def test_select_backend_explicit_override(self):
"""Explicit --ui flag should override auto-selection."""
from praisonai.cli.ui import select_backend, UIConfig
config = UIConfig(ui_backend='plain')
backend = select_backend(config)
assert backend.__class__.__name__ == 'PlainBackend'
def test_select_backend_auto_with_deps(self):
"""Auto-selection with deps available should select MiddleGroundBackend."""
from praisonai.cli.ui import select_backend, UIConfig
with patch.object(sys.stdout, 'isatty', return_value=True):
with patch.dict(os.environ, {}, clear=False):
# Remove PRAISONAI_UI_SAFE if present
os.environ.pop('PRAISONAI_UI_SAFE', None)
config = UIConfig()
backend = select_backend(config)
# Should be MiddleGroundBackend or RichBackend depending on deps
assert backend.__class__.__name__ in ('MiddleGroundBackend', 'RichBackend', 'PlainBackend')
def test_select_backend_fallback_chain(self):
"""When deps missing, should fallback gracefully."""
from praisonai.cli.ui import select_backend, UIConfig
with patch.object(sys.stdout, 'isatty', return_value=True):
config = UIConfig(ui_backend='auto')
backend = select_backend(config)
# Should return some valid backend
assert hasattr(backend, 'emit')
assert hasattr(backend, 'prompt')
class TestUIConfig:
"""Test UIConfig dataclass."""
def test_ui_config_defaults(self):
"""UIConfig should have sensible defaults."""
from praisonai.cli.ui import UIConfig
config = UIConfig()
assert config.ui_backend == 'auto'
assert config.json_output is False
assert config.no_color is False
assert config.theme == 'default'
def test_ui_config_from_cli_args(self):
"""UIConfig should be constructible from CLI args."""
from praisonai.cli.ui import UIConfig
config = UIConfig(
ui_backend='mg',
json_output=True,
no_color=True,
theme='dark',
compact=True,
)
assert config.ui_backend == 'mg'
assert config.json_output is True
assert config.no_color is True
assert config.theme == 'dark'
assert config.compact is True
class TestBackendProtocol:
"""Test that backends implement the UIBackend protocol."""
def test_plain_backend_has_required_methods(self):
"""PlainBackend should implement UIBackend protocol."""
from praisonai.cli.ui.plain import PlainBackend
backend = PlainBackend()
assert hasattr(backend, 'emit')
assert hasattr(backend, 'prompt')
assert hasattr(backend, 'is_tty')
assert callable(backend.emit)
assert callable(backend.prompt)
assert callable(backend.is_tty)
def test_plain_backend_is_tty_returns_false_for_non_tty(self):
"""PlainBackend.is_tty() should return False for non-TTY."""
from praisonai.cli.ui.plain import PlainBackend
with patch.object(sys.stdout, 'isatty', return_value=False):
backend = PlainBackend()
assert backend.is_tty() is False
def test_plain_backend_emit_writes_to_stdout(self, capsys):
"""PlainBackend.emit() should write to stdout."""
from praisonai.cli.ui.plain import PlainBackend
from praisonai.cli.ui.events import UIEventType
backend = PlainBackend()
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'Hello'})
captured = capsys.readouterr()
assert 'Hello' in captured.out
@@ -0,0 +1,178 @@
"""
Tests for UI event routing.
Verifies:
1. Events are properly routed from InteractiveCore to backends
2. All event types are handled correctly
3. Streaming events work properly
4. Multi-agent events include agent name
"""
from praisonai.cli.ui.events import UIEventType, UIEvent
from praisonai.cli.ui.plain import PlainBackend
class TestUIEventType:
"""Test UIEventType enum."""
def test_message_events_exist(self):
"""Message lifecycle events should exist."""
assert UIEventType.MESSAGE_START
assert UIEventType.MESSAGE_CHUNK
assert UIEventType.MESSAGE_END
def test_tool_events_exist(self):
"""Tool lifecycle events should exist."""
assert UIEventType.TOOL_START
assert UIEventType.TOOL_PROGRESS
assert UIEventType.TOOL_END
def test_approval_events_exist(self):
"""Approval flow events should exist."""
assert UIEventType.APPROVAL_REQUEST
assert UIEventType.APPROVAL_RESPONSE
def test_status_events_exist(self):
"""Status events should exist."""
assert UIEventType.STATUS_UPDATE
assert UIEventType.STATUS_CLEAR
def test_error_events_exist(self):
"""Error events should exist."""
assert UIEventType.ERROR
assert UIEventType.WARNING
class TestUIEvent:
"""Test UIEvent dataclass."""
def test_event_has_timestamp(self):
"""UIEvent should auto-generate timestamp."""
event = UIEvent(event_type=UIEventType.MESSAGE_START)
assert event.timestamp is not None
assert event.timestamp > 0
def test_event_with_data(self):
"""UIEvent should accept data payload."""
event = UIEvent(
event_type=UIEventType.MESSAGE_CHUNK,
data={'content': 'Hello'}
)
assert event.data['content'] == 'Hello'
def test_event_with_agent_name(self):
"""UIEvent should accept agent name for multi-agent."""
event = UIEvent(
event_type=UIEventType.MESSAGE_START,
agent_name='Researcher'
)
assert event.agent_name == 'Researcher'
class TestPlainBackendEventRouting:
"""Test PlainBackend event handling."""
def test_message_start_with_agent_name(self, capsys):
"""MESSAGE_START with agent name should print agent prefix."""
backend = PlainBackend()
backend.emit(UIEventType.MESSAGE_START, {'agent_name': 'Coder'})
captured = capsys.readouterr()
assert '[Coder]' in captured.out
def test_message_chunk_appends_content(self, capsys):
"""MESSAGE_CHUNK should append content to output."""
backend = PlainBackend()
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'Hello '})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'World'})
captured = capsys.readouterr()
assert 'Hello World' in captured.out
def test_message_end_adds_newline(self, capsys):
"""MESSAGE_END should add newline if needed."""
backend = PlainBackend()
backend.emit(UIEventType.MESSAGE_START, {})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'Test'})
backend.emit(UIEventType.MESSAGE_END, {})
captured = capsys.readouterr()
assert captured.out.endswith('\n')
def test_tool_start_shows_tool_name(self, capsys):
"""TOOL_START should show tool name."""
backend = PlainBackend()
backend.emit(UIEventType.TOOL_START, {'tool_name': 'read_file'})
captured = capsys.readouterr()
assert 'read_file' in captured.out
def test_tool_end_shows_result(self, capsys):
"""TOOL_END should show result."""
backend = PlainBackend()
backend.emit(UIEventType.TOOL_END, {'result': 'File contents here'})
captured = capsys.readouterr()
assert 'File contents here' in captured.out
def test_error_writes_to_stderr(self, capsys):
"""ERROR should write to stderr."""
backend = PlainBackend()
backend.emit(UIEventType.ERROR, {'message': 'Something went wrong'})
captured = capsys.readouterr()
assert 'Something went wrong' in captured.err
assert 'Error' in captured.err
def test_warning_writes_to_stderr(self, capsys):
"""WARNING should write to stderr."""
backend = PlainBackend()
backend.emit(UIEventType.WARNING, {'message': 'Be careful'})
captured = capsys.readouterr()
assert 'Be careful' in captured.err
assert 'Warning' in captured.err
def test_approval_request_shows_prompt(self, capsys):
"""APPROVAL_REQUEST should show prompt."""
backend = PlainBackend()
backend.emit(UIEventType.APPROVAL_REQUEST, {'prompt': 'Allow file write?'})
captured = capsys.readouterr()
assert 'Allow file write?' in captured.out
assert '[y/n]' in captured.out
class TestStreamingBehavior:
"""Test streaming message behavior."""
def test_streaming_preserves_partial_content(self, capsys):
"""Streaming should preserve partial content between chunks."""
backend = PlainBackend()
backend.emit(UIEventType.MESSAGE_START, {})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'Hello '})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'World '})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': '!'})
backend.emit(UIEventType.MESSAGE_END, {})
captured = capsys.readouterr()
assert 'Hello World !' in captured.out
def test_multiple_messages_separated(self, capsys):
"""Multiple messages should be properly separated."""
backend = PlainBackend()
# First message
backend.emit(UIEventType.MESSAGE_START, {})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'First'})
backend.emit(UIEventType.MESSAGE_END, {})
# Second message
backend.emit(UIEventType.MESSAGE_START, {})
backend.emit(UIEventType.MESSAGE_CHUNK, {'content': 'Second'})
backend.emit(UIEventType.MESSAGE_END, {})
captured = capsys.readouterr()
assert 'First' in captured.out
assert 'Second' in captured.out
+1
View File
@@ -0,0 +1 @@
# Tests for development utilities
+312
View File
@@ -0,0 +1,312 @@
"""
Tests for the API markdown generator.
Tests cover:
- Symbol discovery (respects __all__)
- Lazy-loaded symbol discovery from __getattr__
- Link generation (relative paths correct)
- Stable ordering (deterministic)
- TypeScript export parsing
- CLI command discovery
"""
import sys
from pathlib import Path
import pytest
# Add the praisonai package to path
repo_root = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(repo_root / "src" / "praisonai"))
from praisonai._dev.api_md import ApiMdGenerator, generate_api_md
class TestApiMdGenerator:
"""Tests for ApiMdGenerator class."""
@pytest.fixture
def generator(self):
"""Create a generator instance."""
return ApiMdGenerator(repo_root=repo_root)
def test_init_finds_repo_root(self, generator):
"""Test that generator finds the repository root."""
assert generator.repo_root.exists()
assert (generator.repo_root / ".git").exists()
def test_init_finds_package_paths(self, generator):
"""Test that generator finds package paths."""
assert generator.agents_pkg.exists()
assert generator.wrapper_pkg.exists()
assert generator.ts_pkg.exists()
def test_discover_all_finds_symbols(self, generator):
"""Test that discover_all finds symbols from all packages."""
generator.discover_all()
# Should find symbols from praisonaiagents
assert len(generator.agents_symbols) > 0
# Should find CLI commands
assert len(generator.cli_commands) > 0
# Should find TypeScript exports
assert len(generator.ts_exports) > 0
def test_discovers_agent_class(self, generator):
"""Test that Agent class is discovered."""
generator.discover_all()
assert "Agent" in generator.agents_symbols
agent_info = generator.agents_symbols["Agent"]
assert agent_info.kind == "class"
assert "agent.py" in agent_info.file_path
def test_discovers_agents_class(self, generator):
"""Test that Agents class is discovered."""
generator.discover_all()
assert "Agents" in generator.agents_symbols
agents_info = generator.agents_symbols["Agents"]
assert agents_info.kind == "class"
def test_discovers_workflow_exports(self, generator):
"""Test that Workflow exports are discovered."""
generator.discover_all()
# Check for workflow-related exports
workflow_exports = ["Workflow", "WorkflowStep", "Pipeline"]
found = [e for e in workflow_exports if e in generator.agents_symbols]
assert len(found) > 0, f"Expected some workflow exports, found: {list(generator.agents_symbols.keys())[:20]}"
def test_discovers_tool_exports(self, generator):
"""Test that tool exports are discovered."""
generator.discover_all()
# Check for tool-related exports
assert "Tools" in generator.agents_symbols or "BaseTool" in generator.agents_symbols
def test_discovers_lazy_loaded_symbols(self, generator):
"""Test that lazy-loaded symbols from __getattr__ are discovered."""
generator.discover_all()
# These are all lazy-loaded in __getattr__
lazy_symbols = ["Agent", "Agents", "Memory", "Knowledge", "Session"]
found = [s for s in lazy_symbols if s in generator.agents_symbols]
assert len(found) >= 3, f"Expected at least 3 lazy symbols, found: {found}"
def test_symbol_has_file_path(self, generator):
"""Test that discovered symbols have valid file paths."""
generator.discover_all()
for name, info in list(generator.agents_symbols.items())[:10]:
assert info.file_path, f"Symbol {name} missing file_path"
assert info.file_path.startswith("./"), f"Path should be relative: {info.file_path}"
def test_class_has_methods(self, generator):
"""Test that class symbols have methods extracted."""
generator.discover_all()
if "Agent" in generator.agents_symbols:
agent_info = generator.agents_symbols["Agent"]
assert len(agent_info.methods) > 0, "Agent class should have methods"
# Check for common methods
method_names = [m.name for m in agent_info.methods]
assert "start" in method_names or "run" in method_names or "chat" in method_names
class TestTypescriptDiscovery:
"""Tests for TypeScript export discovery."""
@pytest.fixture
def generator(self):
return ApiMdGenerator(repo_root=repo_root)
def test_discovers_ts_exports(self, generator):
"""Test that TypeScript exports are discovered."""
generator.discover_all()
assert len(generator.ts_exports) > 0
def test_ts_exports_have_source_file(self, generator):
"""Test that TS exports have source file info."""
generator.discover_all()
for export in generator.ts_exports[:10]:
assert export.source_file, f"Export {export.name} missing source_file"
def test_discovers_agent_ts_export(self, generator):
"""Test that Agent is exported from TypeScript."""
generator.discover_all()
export_names = [e.name for e in generator.ts_exports]
assert "Agent" in export_names
class TestCLIDiscovery:
"""Tests for CLI command discovery."""
@pytest.fixture
def generator(self):
return ApiMdGenerator(repo_root=repo_root)
def test_discovers_cli_commands(self, generator):
"""Test that CLI commands are discovered."""
generator.discover_all()
assert len(generator.cli_commands) > 0
def test_cli_commands_have_file_path(self, generator):
"""Test that CLI commands have file paths."""
generator.discover_all()
for cmd in generator.cli_commands[:10]:
assert cmd.file_path, f"Command {cmd.command_path} missing file_path"
class TestMarkdownGeneration:
"""Tests for markdown output generation."""
@pytest.fixture
def generator(self):
return ApiMdGenerator(repo_root=repo_root)
def test_generate_returns_string(self, generator):
"""Test that generate returns a string."""
content = generator.generate()
assert isinstance(content, str)
assert len(content) > 0
def test_generate_has_header(self, generator):
"""Test that generated content has header."""
content = generator.generate()
assert "# PraisonAI API Reference" in content
def test_generate_has_regenerate_instruction(self, generator):
"""Test that generated content has regeneration instruction."""
content = generator.generate()
assert "praisonai docs api-md" in content
def test_generate_has_types_block(self, generator):
"""Test that generated content has Types blocks."""
content = generator.generate()
assert "Types:" in content
assert "```python" in content
def test_generate_has_methods_block(self, generator):
"""Test that generated content has Methods blocks."""
content = generator.generate()
assert "Methods:" in content
def test_generate_has_typescript_section(self, generator):
"""Test that generated content has TypeScript section."""
content = generator.generate()
assert "# TypeScript" in content
assert "```ts" in content
def test_generate_has_cli_section(self, generator):
"""Test that generated content has CLI section."""
content = generator.generate()
assert "# CLI" in content
def test_generate_has_plugins_section(self, generator):
"""Test that generated content has Optional Plugins section."""
content = generator.generate()
assert "# Optional Plugins" in content
assert "praisonai-tools" in content
class TestDeterministicOutput:
"""Tests for deterministic/stable output."""
@pytest.fixture
def generator(self):
return ApiMdGenerator(repo_root=repo_root)
def test_generate_is_deterministic(self, generator):
"""Test that two runs produce identical output."""
content1 = generator.generate()
# Create a new generator and generate again
generator2 = ApiMdGenerator(repo_root=repo_root)
content2 = generator2.generate()
assert content1 == content2, "Output should be deterministic"
def test_symbols_are_sorted(self, generator):
"""Test that symbols are sorted alphabetically."""
generator.discover_all()
symbol_names = list(generator.agents_symbols.keys())
assert symbol_names == sorted(symbol_names), "Symbols should be sorted"
class TestGenerateApiMdFunction:
"""Tests for the generate_api_md function."""
def test_generate_to_stdout(self, capsys):
"""Test generating to stdout."""
exit_code = generate_api_md(repo_root=repo_root, stdout=True)
assert exit_code == 0
captured = capsys.readouterr()
assert "# PraisonAI API Reference" in captured.out
def test_check_mode_passes_when_current(self, tmp_path):
"""Test that check mode passes when file is current."""
# Generate to a temp file
output_path = tmp_path / "api.md"
exit_code = generate_api_md(repo_root=repo_root, output_path=output_path)
assert exit_code == 0
# Check should pass
exit_code = generate_api_md(repo_root=repo_root, output_path=output_path, check=True)
assert exit_code == 0
def test_check_mode_fails_when_outdated(self, tmp_path):
"""Test that check mode fails when file is outdated."""
# Create an outdated file
output_path = tmp_path / "api.md"
output_path.write_text("# Old content")
# Check should fail
exit_code = generate_api_md(repo_root=repo_root, output_path=output_path, check=True)
assert exit_code == 1
def test_check_mode_fails_when_missing(self, tmp_path):
"""Test that check mode fails when file is missing."""
output_path = tmp_path / "api.md"
# Check should fail
exit_code = generate_api_md(repo_root=repo_root, output_path=output_path, check=True)
assert exit_code == 1
class TestLinkGeneration:
"""Tests for link generation in output."""
@pytest.fixture
def generator(self):
return ApiMdGenerator(repo_root=repo_root)
def test_links_are_relative(self, generator):
"""Test that links are relative to repo root."""
content = generator.generate()
# All href links should start with ./
import re
hrefs = re.findall(r'href="([^"]+)"', content)
for href in hrefs:
assert href.startswith("./"), f"Link should be relative: {href}"
def test_links_point_to_existing_files(self, generator):
"""Test that links point to existing files."""
generator.discover_all()
for name, info in list(generator.agents_symbols.items())[:10]:
if info.file_path:
# Remove ./ prefix
rel_path = info.file_path[2:] if info.file_path.startswith("./") else info.file_path
full_path = generator.repo_root / rel_path
assert full_path.exists(), f"File not found: {full_path}"
@@ -0,0 +1 @@
"""Tests for the standardise module."""
@@ -0,0 +1,221 @@
"""Tests for standardise discovery."""
from praisonai.standardise.config import StandardiseConfig
from praisonai.standardise.discovery import FeatureDiscovery
from praisonai.standardise.models import FeatureSlug
class TestFeatureDiscovery:
"""Tests for FeatureDiscovery class."""
def test_discover_sdk_features(self, tmp_path):
"""Test discovering features from SDK modules."""
# Create mock SDK structure
sdk_root = tmp_path / "praisonaiagents"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
(sdk_root / "memory").mkdir()
(sdk_root / "knowledge").mkdir()
(sdk_root / "__pycache__").mkdir() # Should be skipped
config = StandardiseConfig(sdk_root=sdk_root)
discovery = FeatureDiscovery(config)
features = discovery.discover_sdk_features()
assert len(features) == 3
slugs = {str(f) for f in features}
assert "guardrails" in slugs
assert "memory" in slugs
assert "knowledge" in slugs
def test_discover_cli_features(self, tmp_path):
"""Test discovering features from CLI files."""
# Create mock CLI structure
cli_root = tmp_path / "cli"
cli_root.mkdir()
features_dir = cli_root / "features"
features_dir.mkdir()
(features_dir / "guardrail.py").touch()
(features_dir / "memory.py").touch()
(features_dir / "__init__.py").touch() # Should be skipped
config = StandardiseConfig(cli_root=cli_root)
discovery = FeatureDiscovery(config)
features = discovery.discover_cli_features()
assert len(features) == 2
slugs = {str(f) for f in features}
assert "guardrails" in slugs # Normalised from guardrail
assert "memory" in slugs
def test_discover_docs_features(self, tmp_path):
"""Test discovering features from docs pages."""
# Create mock docs structure
docs_root = tmp_path / "docs"
docs_root.mkdir()
concepts = docs_root / "concepts"
concepts.mkdir()
(concepts / "guardrails.mdx").touch()
(concepts / "memory.mdx").touch()
features = docs_root / "features"
features.mkdir()
(features / "knowledge.mdx").touch()
config = StandardiseConfig(docs_root=docs_root)
discovery = FeatureDiscovery(config)
features_found = discovery.discover_docs_features()
slugs = {str(f) for f in features_found}
assert "guardrails" in slugs
assert "memory" in slugs
assert "knowledge" in slugs
def test_discover_example_features(self, tmp_path):
"""Test discovering features from examples."""
# Create mock examples structure
examples_root = tmp_path / "examples"
examples_root.mkdir()
(examples_root / "guardrails").mkdir()
(examples_root / "memory").mkdir()
(examples_root / "__pycache__").mkdir() # Should be skipped
config = StandardiseConfig(examples_root=examples_root)
discovery = FeatureDiscovery(config)
features = discovery.discover_example_features()
assert len(features) == 2
slugs = {str(f) for f in features}
assert "guardrails" in slugs
assert "memory" in slugs
def test_get_all_features(self, tmp_path):
"""Test getting union of all features."""
# Create mock structure with overlapping features
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
(sdk_root / "memory").mkdir()
docs_root = tmp_path / "docs"
docs_root.mkdir()
concepts = docs_root / "concepts"
concepts.mkdir()
(concepts / "guardrails.mdx").touch()
(concepts / "knowledge.mdx").touch()
config = StandardiseConfig(sdk_root=sdk_root, docs_root=docs_root)
discovery = FeatureDiscovery(config)
all_features = discovery.get_all_features()
slugs = {str(f) for f in all_features}
assert "guardrails" in slugs
assert "memory" in slugs
assert "knowledge" in slugs
def test_get_feature_sources(self, tmp_path):
"""Test getting sources for a specific feature."""
# Create mock structure with explicit cli_root that doesn't have guardrails
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
docs_root = tmp_path / "docs"
docs_root.mkdir()
concepts = docs_root / "concepts"
concepts.mkdir()
(concepts / "guardrails.mdx").touch()
# Create empty cli root to ensure no CLI features found
cli_root = tmp_path / "cli"
cli_root.mkdir()
(cli_root / "features").mkdir()
# Create empty examples root to ensure no examples found
examples_root = tmp_path / "examples"
examples_root.mkdir()
config = StandardiseConfig(sdk_root=sdk_root, docs_root=docs_root, cli_root=cli_root, examples_root=examples_root)
discovery = FeatureDiscovery(config)
slug = FeatureSlug.from_string("guardrails")
sources = discovery.get_feature_sources(slug)
assert sources["sdk"] is True
assert sources["docs"] is True
assert sources["cli"] is False
assert sources["examples"] is False
def test_find_docs_pages(self, tmp_path):
"""Test finding all docs pages for a feature."""
# Create mock docs with multiple pages for same feature
docs_root = tmp_path / "docs"
docs_root.mkdir()
concepts = docs_root / "concepts"
concepts.mkdir()
(concepts / "guardrails.mdx").touch()
features = docs_root / "features"
features.mkdir()
(features / "guardrails.mdx").touch()
cli = docs_root / "cli"
cli.mkdir()
(cli / "guardrail.mdx").touch() # Singular variant
config = StandardiseConfig(docs_root=docs_root)
discovery = FeatureDiscovery(config)
slug = FeatureSlug.from_string("guardrails")
pages = discovery.find_docs_pages(slug)
assert len(pages) >= 2 # At least concepts and features
def test_find_examples(self, tmp_path):
"""Test finding example files for a feature."""
# Create mock examples
examples_root = tmp_path / "examples"
examples_root.mkdir()
guardrails_dir = examples_root / "guardrails"
guardrails_dir.mkdir()
(guardrails_dir / "guardrails-basic.py").touch()
(guardrails_dir / "guardrails-advanced.py").touch()
config = StandardiseConfig(examples_root=examples_root)
discovery = FeatureDiscovery(config)
slug = FeatureSlug.from_string("guardrails")
examples = discovery.find_examples(slug)
assert len(examples) == 2
def test_caching(self, tmp_path):
"""Test that discovery results are cached."""
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
config = StandardiseConfig(sdk_root=sdk_root)
discovery = FeatureDiscovery(config)
# First call
features1 = discovery.discover_sdk_features()
# Add another directory
(sdk_root / "memory").mkdir()
# Second call should return cached result
features2 = discovery.discover_sdk_features()
assert features1 is features2 # Same object (cached)
@@ -0,0 +1,210 @@
"""Tests for standardise engine."""
from praisonai.standardise.config import StandardiseConfig
from praisonai.standardise.engine import StandardiseEngine
class TestStandardiseEngine:
"""Tests for StandardiseEngine class."""
def test_check_returns_report(self, tmp_path):
"""Test that check returns a valid report."""
# Create minimal structure
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
config = StandardiseConfig(
project_root=tmp_path,
sdk_root=sdk_root,
)
engine = StandardiseEngine(config)
report = engine.check()
assert report.timestamp
assert report.features_scanned >= 1
def test_report_text_format(self, tmp_path):
"""Test text report generation."""
config = StandardiseConfig(project_root=tmp_path)
engine = StandardiseEngine(config)
output = engine.report(format="text")
assert "Standardisation" in output
assert "Summary" in output
def test_report_json_format(self, tmp_path):
"""Test JSON report generation."""
config = StandardiseConfig(project_root=tmp_path)
engine = StandardiseEngine(config)
output = engine.report(format="json")
import json
data = json.loads(output)
assert "timestamp" in data
assert "summary" in data
def test_fix_dry_run(self, tmp_path):
"""Test fix in dry-run mode."""
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
docs_root = tmp_path / "docs"
docs_root.mkdir()
config = StandardiseConfig(
project_root=tmp_path,
sdk_root=sdk_root,
docs_root=docs_root,
dry_run=True,
)
engine = StandardiseEngine(config)
actions = engine.fix(apply=False)
# Should have planned actions but no applied
assert len(actions["applied"]) == 0
def test_fix_apply(self, tmp_path):
"""Test fix with apply."""
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
docs_root = tmp_path / "docs"
docs_root.mkdir()
(docs_root / "concepts").mkdir()
(docs_root / "features").mkdir()
(docs_root / "cli").mkdir()
examples_root = tmp_path / "examples"
examples_root.mkdir()
config = StandardiseConfig(
project_root=tmp_path,
sdk_root=sdk_root,
docs_root=docs_root,
examples_root=examples_root,
dry_run=False,
backup=False,
)
engine = StandardiseEngine(config)
actions = engine.fix(feature="guardrails", apply=True)
# Should have applied some actions
assert len(actions["applied"]) > 0
def test_init_feature(self, tmp_path):
"""Test initialising a new feature."""
docs_root = tmp_path / "docs"
docs_root.mkdir()
(docs_root / "concepts").mkdir()
(docs_root / "features").mkdir()
(docs_root / "cli").mkdir()
examples_root = tmp_path / "examples"
examples_root.mkdir()
config = StandardiseConfig(
project_root=tmp_path,
docs_root=docs_root,
examples_root=examples_root,
dry_run=False,
)
engine = StandardiseEngine(config)
created = engine.init("my-new-feature")
# Should create multiple artifacts
assert len(created) > 0
assert "docs_concept" in created or "docs_feature" in created
def test_init_invalid_slug(self, tmp_path):
"""Test init with invalid slug raises error."""
config = StandardiseConfig(project_root=tmp_path)
engine = StandardiseEngine(config)
try:
engine.init("123invalid")
assert False, "Should have raised ValueError"
except ValueError as e:
assert "letter" in str(e).lower()
def test_get_exit_code_no_issues(self, tmp_path):
"""Test exit code when no issues."""
config = StandardiseConfig(project_root=tmp_path)
engine = StandardiseEngine(config)
report = engine.check()
# With empty project, might have no issues
exit_code = engine.get_exit_code(report)
assert exit_code in [0, 1] # Either no issues or issues found
def test_backup_creation(self, tmp_path):
"""Test that backups are created."""
docs_root = tmp_path / "docs"
docs_root.mkdir()
concepts = docs_root / "concepts"
concepts.mkdir()
# Create existing file
existing = concepts / "guardrails.mdx"
existing.write_text("existing content")
config = StandardiseConfig(
project_root=tmp_path,
docs_root=docs_root,
dry_run=False,
backup=True,
)
engine = StandardiseEngine(config)
# Trigger backup
backup_path = engine._backup_file(existing)
assert backup_path.exists()
assert "guardrails" in backup_path.name
class TestStandardiseConfig:
"""Tests for StandardiseConfig class."""
def test_default_values(self):
"""Test default configuration values."""
config = StandardiseConfig()
assert config.dry_run is True
assert config.backup is True
assert config.force is False
assert config.scope == "all"
assert config.dedupe_mode == "prompt"
def test_auto_detect_paths(self, tmp_path):
"""Test auto-detection of paths."""
# Create structure
(tmp_path / ".praison").mkdir()
docs = tmp_path / "docs"
docs.mkdir()
examples = tmp_path / "examples" / "python"
examples.mkdir(parents=True)
config = StandardiseConfig(project_root=tmp_path)
assert config.docs_root == docs
assert config.examples_root == examples
def test_from_env(self, monkeypatch):
"""Test loading config from environment."""
monkeypatch.setenv("PRAISON_DRY_RUN", "false")
monkeypatch.setenv("CI", "true")
config = StandardiseConfig.from_env()
assert config.dry_run is False
assert config.ci_mode is True
@@ -0,0 +1,121 @@
"""
Unit tests for the ExampleVerifier.
"""
from praisonai.standardise.example_verifier import ExampleVerifier, VerificationResult
class TestExampleVerifier:
"""Tests for ExampleVerifier."""
def test_valid_simple_code(self):
"""Test verification of valid simple code."""
verifier = ExampleVerifier(timeout=10)
code = '''
print("Hello, World!")
'''
result = verifier.verify(code)
assert result.syntax_valid
assert result.execution_passed
assert result.success
assert result.can_write
def test_syntax_error(self):
"""Test detection of syntax errors."""
verifier = ExampleVerifier(timeout=10)
code = '''
def broken(
print("missing closing paren"
'''
result = verifier.verify(code)
assert not result.syntax_valid
assert not result.success
assert not result.can_write
assert "Syntax error" in result.error
def test_runtime_error(self):
"""Test detection of runtime errors."""
verifier = ExampleVerifier(timeout=10)
code = '''
x = 1 / 0 # Division by zero
'''
result = verifier.verify(code)
assert result.syntax_valid
assert not result.execution_passed
assert not result.success
assert "ZeroDivisionError" in result.error
def test_missing_import(self):
"""Test detection of missing imports."""
verifier = ExampleVerifier(timeout=10)
code = '''
import nonexistent_module_xyz
'''
result = verifier.verify(code)
assert result.syntax_valid
assert not result.execution_passed
assert "nonexistent_module_xyz" in result.missing_libraries
def test_external_library_detection(self):
"""Test detection of external library requirements."""
verifier = ExampleVerifier(timeout=10)
code = '''
import pandas
df = pandas.DataFrame()
'''
result = verifier.verify(code)
assert result.syntax_valid
# May or may not pass depending on if pandas is installed
# But should detect pandas as external
assert "pandas" in result.missing_libraries or result.execution_passed
def test_can_write_with_external_libs(self):
"""Test that code requiring external libs can still be written."""
# Create a result that simulates external lib requirement
result = VerificationResult(
success=True,
syntax_valid=True,
execution_passed=False,
output="",
error="ModuleNotFoundError: No module named 'requests'",
missing_libraries=["requests"],
requires_external=True,
)
assert result.can_write # Should allow writing despite execution failure
def test_timeout_handling(self):
"""Test that infinite loops are handled."""
verifier = ExampleVerifier(timeout=2)
code = '''
import time
time.sleep(10) # Will timeout
'''
result = verifier.verify(code)
assert result.syntax_valid
assert not result.execution_passed
assert "timed out" in result.error.lower()
def test_format_result(self):
"""Test result formatting."""
verifier = ExampleVerifier()
result = VerificationResult(
success=True,
syntax_valid=True,
execution_passed=True,
output="Hello",
error="",
missing_libraries=[],
requires_external=False,
)
formatted = verifier.format_result(result)
assert "PASSED" in formatted
assert "Syntax valid: ✓" in formatted
@@ -0,0 +1,168 @@
"""Tests for standardise models."""
from pathlib import Path
from praisonai.standardise.models import (
FeatureSlug,
ArtifactType,
ArtifactStatus,
SINGULAR_PLURAL_MAP,
LEGACY_ALIASES,
)
class TestFeatureSlug:
"""Tests for FeatureSlug class."""
def test_from_string_simple(self):
"""Test simple slug creation."""
slug = FeatureSlug.from_string("guardrails")
assert slug.normalised == "guardrails"
assert slug.is_valid
assert slug.validation_error is None
def test_from_string_with_underscores(self):
"""Test slug with underscores converts to hyphens."""
slug = FeatureSlug.from_string("my_feature")
assert slug.normalised == "my-feature"
assert slug.is_valid
def test_from_string_uppercase(self):
"""Test uppercase is converted to lowercase."""
slug = FeatureSlug.from_string("MyFeature")
assert slug.normalised == "myfeature"
assert slug.is_valid
def test_from_string_with_extension(self):
"""Test file extensions are stripped."""
slug = FeatureSlug.from_string("guardrails.py")
assert slug.normalised == "guardrails"
slug = FeatureSlug.from_string("guardrails.mdx")
assert slug.normalised == "guardrails"
def test_singular_to_plural_normalisation(self):
"""Test singular forms are normalised to plural."""
slug = FeatureSlug.from_string("guardrail")
assert slug.normalised == "guardrails"
slug = FeatureSlug.from_string("agent")
assert slug.normalised == "agents"
slug = FeatureSlug.from_string("task")
assert slug.normalised == "tasks"
def test_legacy_alias_normalisation(self):
"""Test legacy aliases are normalised."""
slug = FeatureSlug.from_string("rag")
assert slug.normalised == "knowledge"
slug = FeatureSlug.from_string("retrieval")
assert slug.normalised == "knowledge"
def test_invalid_slug_empty(self):
"""Test empty slug is invalid."""
slug = FeatureSlug.from_string("")
assert not slug.is_valid
assert "empty" in slug.validation_error.lower()
def test_invalid_slug_starts_with_number(self):
"""Test slug starting with number is invalid."""
slug = FeatureSlug.from_string("123feature")
assert not slug.is_valid
assert "letter" in slug.validation_error.lower()
def test_invalid_slug_special_chars(self):
"""Test slug with special characters is invalid."""
slug = FeatureSlug.from_string("my@feature")
assert not slug.is_valid
def test_from_path_file(self):
"""Test slug from file path."""
path = Path("/some/path/guardrails.py")
slug = FeatureSlug.from_path(path, "sdk")
assert slug.normalised == "guardrails"
def test_from_path_directory(self):
"""Test slug from directory path."""
path = Path("/some/path/guardrails")
slug = FeatureSlug.from_path(path, "sdk")
assert slug.normalised == "guardrails"
def test_equality(self):
"""Test slug equality."""
slug1 = FeatureSlug.from_string("guardrails")
slug2 = FeatureSlug.from_string("guardrail") # Normalises to guardrails
assert slug1 == slug2
slug3 = FeatureSlug.from_string("memory")
assert slug1 != slug3
def test_equality_with_string(self):
"""Test slug equality with string."""
slug = FeatureSlug.from_string("guardrails")
assert slug == "guardrails"
assert slug == "GUARDRAILS" # Case insensitive
def test_hash(self):
"""Test slug hashing for use in sets/dicts."""
slug1 = FeatureSlug.from_string("guardrails")
slug2 = FeatureSlug.from_string("guardrail")
# Should have same hash since they normalise to same value
assert hash(slug1) == hash(slug2)
# Can be used in sets
slug_set = {slug1, slug2}
assert len(slug_set) == 1
def test_str(self):
"""Test string representation."""
slug = FeatureSlug.from_string("guardrails")
assert str(slug) == "guardrails"
class TestArtifactType:
"""Tests for ArtifactType enum."""
def test_all_types_exist(self):
"""Test all required artifact types exist."""
assert ArtifactType.DOCS_CONCEPT
assert ArtifactType.DOCS_FEATURE
assert ArtifactType.DOCS_CLI
assert ArtifactType.DOCS_SDK
assert ArtifactType.EXAMPLE_BASIC
assert ArtifactType.EXAMPLE_ADVANCED
assert ArtifactType.MANIFEST
def test_values(self):
"""Test artifact type values."""
assert ArtifactType.DOCS_CONCEPT.value == "docs_concept"
assert ArtifactType.EXAMPLE_BASIC.value == "example_basic"
class TestArtifactStatus:
"""Tests for ArtifactStatus enum."""
def test_all_statuses_exist(self):
"""Test all statuses exist."""
assert ArtifactStatus.PRESENT
assert ArtifactStatus.MISSING
assert ArtifactStatus.DUPLICATE
assert ArtifactStatus.OUTDATED
class TestMappings:
"""Tests for slug mapping constants."""
def test_singular_plural_map_has_common_terms(self):
"""Test common terms are in the mapping."""
assert "agent" in SINGULAR_PLURAL_MAP
assert "task" in SINGULAR_PLURAL_MAP
assert "guardrail" in SINGULAR_PLURAL_MAP
assert "handoff" in SINGULAR_PLURAL_MAP
def test_legacy_aliases_has_common_aliases(self):
"""Test common aliases are in the mapping."""
assert "rag" in LEGACY_ALIASES
assert "retrieval" in LEGACY_ALIASES
@@ -0,0 +1,207 @@
"""
Unit tests for the undo/redo system.
"""
from unittest.mock import patch
from praisonai.standardise.undo_redo import UndoRedoManager, FileBackupManager
class TestUndoRedoManager:
"""Tests for UndoRedoManager."""
def test_init_with_path(self, tmp_path):
"""Test initialization with a path."""
manager = UndoRedoManager(tmp_path)
assert manager.repo_path == tmp_path
def test_checkpoint_prefix(self):
"""Test checkpoint prefix constant."""
assert UndoRedoManager.CHECKPOINT_PREFIX == "standardise-checkpoint-"
@patch.object(UndoRedoManager, '_run_git')
def test_create_checkpoint_success(self, mock_git, tmp_path):
"""Test successful checkpoint creation."""
mock_git.side_effect = [
(True, ""), # git add
(False, "changes"), # git diff (has changes)
(True, ""), # git commit
(True, ""), # git tag
]
manager = UndoRedoManager(tmp_path)
success, result = manager.create_checkpoint("Test checkpoint")
assert success
assert result.startswith("standardise-checkpoint-")
@patch.object(UndoRedoManager, '_run_git')
def test_create_checkpoint_no_changes(self, mock_git, tmp_path):
"""Test checkpoint creation with no changes."""
mock_git.side_effect = [
(True, ""), # git add
(True, ""), # git diff (no changes)
(True, ""), # git tag
]
manager = UndoRedoManager(tmp_path)
success, result = manager.create_checkpoint()
assert success
@patch.object(UndoRedoManager, '_run_git')
def test_list_checkpoints(self, mock_git, tmp_path):
"""Test listing checkpoints."""
mock_git.return_value = (True, "standardise-checkpoint-20240101|2024-01-01|Test message\n")
manager = UndoRedoManager(tmp_path)
checkpoints = manager.list_checkpoints()
assert len(checkpoints) == 1
assert checkpoints[0][0] == "standardise-checkpoint-20240101"
assert checkpoints[0][1] == "2024-01-01"
assert checkpoints[0][2] == "Test message"
@patch.object(UndoRedoManager, '_run_git')
def test_list_checkpoints_empty(self, mock_git, tmp_path):
"""Test listing checkpoints when none exist."""
mock_git.return_value = (True, "")
manager = UndoRedoManager(tmp_path)
checkpoints = manager.list_checkpoints()
assert checkpoints == []
@patch.object(UndoRedoManager, '_run_git')
def test_undo_success(self, mock_git, tmp_path):
"""Test successful undo."""
mock_git.side_effect = [
(True, ""), # rev-parse (checkpoint exists)
(True, ""), # git add (safety checkpoint)
(True, ""), # git diff
(True, ""), # git tag
(True, ""), # git reset
]
manager = UndoRedoManager(tmp_path)
success, result = manager.undo("standardise-checkpoint-test")
assert success
assert "Restored" in result
@patch.object(UndoRedoManager, '_run_git')
def test_undo_checkpoint_not_found(self, mock_git, tmp_path):
"""Test undo with non-existent checkpoint."""
mock_git.return_value = (False, "not found")
manager = UndoRedoManager(tmp_path)
success, result = manager.undo("nonexistent")
assert not success
assert "not found" in result
@patch.object(UndoRedoManager, '_run_git')
def test_redo_no_redo_available(self, mock_git, tmp_path):
"""Test redo when no redo is available."""
mock_git.return_value = (True, "abc123 HEAD@{0}: commit: test\n")
manager = UndoRedoManager(tmp_path)
success, result = manager.redo()
assert not success
assert "No redo available" in result
@patch.object(UndoRedoManager, '_run_git')
def test_delete_checkpoint(self, mock_git, tmp_path):
"""Test deleting a checkpoint."""
mock_git.return_value = (True, "")
manager = UndoRedoManager(tmp_path)
success, result = manager.delete_checkpoint("test-checkpoint")
assert success
assert "Deleted" in result
@patch.object(UndoRedoManager, '_run_git')
def test_get_changes_since(self, mock_git, tmp_path):
"""Test getting changes since a checkpoint."""
mock_git.return_value = (True, "file1.py\nfile2.py\n")
manager = UndoRedoManager(tmp_path)
changes = manager.get_changes_since("test-checkpoint")
assert len(changes) == 2
assert "file1.py" in changes
assert "file2.py" in changes
class TestFileBackupManager:
"""Tests for FileBackupManager."""
def test_init_creates_backup_dir(self, tmp_path):
"""Test that init creates backup directory."""
backup_dir = tmp_path / "backups"
_ = FileBackupManager(backup_dir) # Creates dir on init
assert backup_dir.exists()
def test_backup_file(self, tmp_path):
"""Test backing up a file."""
# Create a test file
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
backup_dir = tmp_path / "backups"
manager = FileBackupManager(backup_dir)
backup_path = manager.backup_file(test_file)
assert backup_path is not None
assert backup_path.exists()
assert backup_path.read_text() == "test content"
def test_backup_nonexistent_file(self, tmp_path):
"""Test backing up a non-existent file."""
manager = FileBackupManager(tmp_path / "backups")
result = manager.backup_file(tmp_path / "nonexistent.txt")
assert result is None
def test_restore_file(self, tmp_path):
"""Test restoring a file from backup."""
# Create backup
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
backup_file = backup_dir / "backup.txt"
backup_file.write_text("backup content")
# Target file
target_file = tmp_path / "restored.txt"
manager = FileBackupManager(backup_dir)
result = manager.restore_file(backup_file, target_file)
assert result
assert target_file.exists()
assert target_file.read_text() == "backup content"
def test_list_backups(self, tmp_path):
"""Test listing backups."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
# Create some backup files
(backup_dir / "file1_20240101.txt").write_text("1")
(backup_dir / "file1_20240102.txt").write_text("2")
(backup_dir / "file2_20240101.txt").write_text("3")
backup_manager = FileBackupManager(backup_dir)
# List all
all_backups = backup_manager.list_backups()
assert len(all_backups) == 3
# List filtered
file1_backups = backup_manager.list_backups("file1")
assert len(file1_backups) == 2
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more