Compare commits

...
3 Commits
Author SHA1 Message Date
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
71 changed files with 8972 additions and 81 deletions

No files matched your search

+862
View File
@@ -0,0 +1,862 @@
# 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
```
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>
# 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 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.2" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.6.2" \
"praisonai>=3.7.2" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.6.2" \
"praisonai>=3.7.2" \
"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)
@@ -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,
},
}
@@ -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):
@@ -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.1"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+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,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,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"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.10.10"
version = "0.11.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+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.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.7.2.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()
+58 -4
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: str = typer.Option("auto", "--ui", 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,
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:
@@ -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,393 @@
"""
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
"""
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)",
)
# 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)
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
# 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
+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.2 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -0,0 +1,73 @@
"""
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",
]
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
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,248 @@
"""
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
if self.docs_root is None:
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
if self.examples_root is None:
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,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,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.2"
+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.1",
"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,217 @@
"""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()
config = StandardiseConfig(sdk_root=sdk_root, docs_root=docs_root, cli_root=cli_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,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,149 @@
"""Tests for standardise validator."""
from praisonai.standardise.config import StandardiseConfig
from praisonai.standardise.discovery import FeatureDiscovery
from praisonai.standardise.validator import ArtifactValidator
from praisonai.standardise.models import ArtifactType, FeatureSlug
class TestArtifactValidator:
"""Tests for ArtifactValidator class."""
def test_validate_feature_all_present(self, tmp_path):
"""Test validation when all artifacts are present."""
# Create complete feature structure
docs_root = tmp_path / "docs"
docs_root.mkdir()
(docs_root / "concepts").mkdir()
(docs_root / "concepts" / "guardrails.mdx").touch()
(docs_root / "features").mkdir()
(docs_root / "features" / "guardrails.mdx").touch()
(docs_root / "cli").mkdir()
(docs_root / "cli" / "guardrails.mdx").touch()
sdk_docs = docs_root / "sdk" / "praisonaiagents" / "guardrails"
sdk_docs.mkdir(parents=True)
(sdk_docs / "guardrails.mdx").touch()
examples_root = tmp_path / "examples"
examples_root.mkdir()
guardrails_examples = examples_root / "guardrails"
guardrails_examples.mkdir()
(guardrails_examples / "guardrails-basic.py").touch()
(guardrails_examples / "guardrails-advanced.py").touch()
config = StandardiseConfig(docs_root=docs_root, examples_root=examples_root)
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
slug = FeatureSlug.from_string("guardrails")
result = validator.validate_feature(slug)
assert result.is_valid
assert len(result.missing_artifacts) == 0
def test_validate_feature_missing_artifacts(self, tmp_path):
"""Test validation when some artifacts are missing."""
# Create partial feature structure
docs_root = tmp_path / "docs"
docs_root.mkdir()
(docs_root / "concepts").mkdir()
(docs_root / "concepts" / "guardrails.mdx").touch()
# Missing: features, cli, sdk docs, examples
config = StandardiseConfig(docs_root=docs_root)
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
slug = FeatureSlug.from_string("guardrails")
result = validator.validate_feature(slug)
assert not result.is_valid
assert len(result.missing_artifacts) > 0
assert ArtifactType.DOCS_FEATURE in result.missing_artifacts
def test_validate_all(self, tmp_path):
"""Test validating all discovered features."""
# Create structure with multiple 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()
(docs_root / "concepts").mkdir()
(docs_root / "concepts" / "guardrails.mdx").touch()
# memory has no docs
config = StandardiseConfig(sdk_root=sdk_root, docs_root=docs_root)
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
results = validator.validate_all()
assert "guardrails" in results
assert "memory" in results
def test_get_missing_artifacts(self, tmp_path):
"""Test getting all missing artifacts."""
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
(sdk_root / "memory").mkdir()
config = StandardiseConfig(sdk_root=sdk_root)
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
missing = validator.get_missing_artifacts()
# Both features should have missing artifacts
assert len(missing) >= 2
def test_finds_variant_paths(self, tmp_path):
"""Test that validator finds files with variant naming."""
docs_root = tmp_path / "docs"
docs_root.mkdir()
(docs_root / "cli").mkdir()
# Using singular form
(docs_root / "cli" / "guardrail.mdx").touch()
config = StandardiseConfig(docs_root=docs_root)
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
slug = FeatureSlug.from_string("guardrails")
artifact_path = validator._find_actual_path(
docs_root / "cli" / "guardrails.mdx",
slug,
"cli"
)
# Should find the singular variant
assert artifact_path is not None
assert artifact_path.exists()
def test_feature_filter(self, tmp_path):
"""Test that feature filter limits validation."""
sdk_root = tmp_path / "sdk"
sdk_root.mkdir()
(sdk_root / "guardrails").mkdir()
(sdk_root / "memory").mkdir()
(sdk_root / "knowledge").mkdir()
config = StandardiseConfig(sdk_root=sdk_root, feature_filter="guardrails")
discovery = FeatureDiscovery(config)
validator = ArtifactValidator(config, discovery)
results = validator.validate_all()
# Only guardrails should be validated
assert len(results) == 1
assert "guardrails" in results
+4 -4
View File
@@ -4276,7 +4276,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
{ name = "praisonaiagents", specifier = ">=0.10.10" },
{ name = "praisonaiagents", specifier = ">=0.11.1" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4324,7 +4324,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.10.10"
version = "0.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4333,9 +4333,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/4f/d01c588827a74ddc58a8bf30cb7eb97671d210b7f5ef46f9eb2a7249f919/praisonaiagents-0.10.10.tar.gz", hash = "sha256:b6cfea93fef40b898575d0c1574134fdc9ec49754d2287e9ade90ba4381ca3d7", size = 962453, upload-time = "2026-01-10T08:57:35.82Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/42/8fe814f73b19a7decd12bca9e3c720a97906658bf993fb4c3063505531fb/praisonaiagents-0.11.1.tar.gz", hash = "sha256:b0f3720d7d5334fb9aaeb8b8e62cc1a20b9ce9c1cad489031414326479a3c23f", size = 963761, upload-time = "2026-01-10T14:23:57.185Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/fe/edd179c792d7f6143d3278c01daf37015fe4a2d98c5dc326fb309b263336/praisonaiagents-0.10.10-py3-none-any.whl", hash = "sha256:fa9bc6db1fb12ae25ea491db8e30eb98ac16c1e9798952cf617594c09999b826", size = 941797, upload-time = "2026-01-10T08:57:34.07Z" },
{ url = "https://files.pythonhosted.org/packages/0f/97/d7d465eec9443b80fd79daa57bbac7292b680a8702b5d6ef3fb82899bf7d/praisonaiagents-0.11.1-py3-none-any.whl", hash = "sha256:3792747bd2f449995bb4f77eb9086c1b9a88bc2f89414dd447e3d687449c389b", size = 943019, upload-time = "2026-01-10T14:23:54.691Z" },
]
[[package]]