mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 17:31:00 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03a3aa420b | ||
|
|
9533abd8a4 | ||
|
|
39c9e50875 |
No files matched your search
@@ -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.
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]" \
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Consolidated Params Examples
|
||||
|
||||
Examples demonstrating the **Agent-Centric API** with consolidated parameters.
|
||||
|
||||
## Precedence Ladder
|
||||
|
||||
```
|
||||
Instance > Config > Array > Dict > String > Bool > Default
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Feature | Presets | Example |
|
||||
|---------|---------|---------|
|
||||
| `memory` | file, sqlite, redis, postgres, mem0, mongodb | `memory="sqlite"` |
|
||||
| `output` | minimal, normal, verbose, debug, silent | `output="verbose"` |
|
||||
| `execution` | fast, balanced, thorough, unlimited | `execution="balanced"` |
|
||||
| `planning` | reasoning, read_only, auto | `planning="reasoning"` |
|
||||
| `reflection` | minimal, standard, thorough | `reflection="standard"` |
|
||||
| `guardrails` | strict, permissive, safety | `guardrails="strict"` |
|
||||
| `web` | duckduckgo, tavily, google, bing, serper | `web="duckduckgo"` |
|
||||
| `context` | sliding_window, summarize, truncate | `context="sliding_window"` |
|
||||
| `autonomy` | suggest, auto_edit, full_auto | `autonomy="suggest"` |
|
||||
| `caching` | enabled, disabled, prompt | `caching="prompt"` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Examples (Single Feature)
|
||||
|
||||
| File | Feature | Description |
|
||||
|------|---------|-------------|
|
||||
| `basic_agent.py` | Agent | Minimal agent with memory |
|
||||
| `basic_memory.py` | memory | SQLite memory preset |
|
||||
| `basic_output.py` | output | Verbose output preset |
|
||||
| `basic_execution.py` | execution | Execution control |
|
||||
| `basic_guardrails.py` | guardrails | Callable validator |
|
||||
| `basic_reflection.py` | reflection | Self-reflection preset |
|
||||
| `basic_web.py` | web | Web search preset |
|
||||
| `basic_planning.py` | planning | Planning preset |
|
||||
| `basic_autonomy.py` | autonomy | Autonomy preset |
|
||||
| `basic_caching.py` | caching | Caching preset |
|
||||
| `basic_knowledge.py` | knowledge | RAG with sources |
|
||||
| `basic_context.py` | context | Context management |
|
||||
| `basic_hooks.py` | hooks | Lifecycle callbacks |
|
||||
|
||||
### Multi-Agent Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_agents.py` | Multi-agent with memory and planning |
|
||||
|
||||
### Workflow Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_workflow.py` | Simple workflow with output preset |
|
||||
| `basic_workflow_agentlike.py` | Workflow with agent-like params |
|
||||
| `basic_step_override.py` | Step-level param overrides |
|
||||
| `advanced_workflow_full_features.py` | All consolidated params |
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `advanced_output_execution.py` | Combined output + execution |
|
||||
|
||||
## Usage Forms
|
||||
|
||||
Each consolidated param supports multiple input forms:
|
||||
|
||||
```python
|
||||
# Bool - enable with defaults
|
||||
memory=True
|
||||
|
||||
# String preset
|
||||
memory="sqlite"
|
||||
|
||||
# Dict config
|
||||
memory={"backend": "sqlite", "user_id": "user123"}
|
||||
|
||||
# Config instance
|
||||
memory=MemoryConfig(backend="sqlite", user_id="user123")
|
||||
|
||||
# Array with overrides (for some params)
|
||||
guardrails=["strict", {"max_retries": 10}]
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Run any example
|
||||
python basic_agent.py
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Autonomy Example - Agent-Centric API
|
||||
|
||||
Demonstrates autonomy with consolidated params.
|
||||
Presets: suggest, auto_edit, full_auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable autonomy with preset
|
||||
agent = Agent(
|
||||
instructions="You are an autonomous assistant.",
|
||||
autonomy="suggest", # Presets: suggest, auto_edit, full_auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Help me organize my project files.")
|
||||
print(response)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Basic Caching Example - Agent-Centric API
|
||||
|
||||
Demonstrates caching with consolidated params.
|
||||
Presets: enabled, disabled, prompt
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable caching with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with caching enabled.",
|
||||
caching="prompt", # Presets: enabled, disabled, prompt
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# First call - will cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
|
||||
# Second call - may use cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Context Example - Agent-Centric API
|
||||
|
||||
Demonstrates context management with consolidated params.
|
||||
Presets: sliding_window, summarize, truncate
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable context management with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with context management.",
|
||||
context="sliding_window", # Presets: sliding_window, summarize, truncate
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Tell me a long story about a dragon.")
|
||||
print(response)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Basic Hooks Example - Agent-Centric API
|
||||
|
||||
Demonstrates lifecycle hooks with consolidated params.
|
||||
Valid keys: on_step, on_tool_call, middleware
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define hook callbacks
|
||||
def on_step_callback(step_info):
|
||||
print(f"Step: {step_info}")
|
||||
|
||||
def on_tool_call_callback(tool_info):
|
||||
print(f"Tool called: {tool_info}")
|
||||
|
||||
# Basic: Enable hooks with dict
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
hooks={
|
||||
"on_step": on_step_callback,
|
||||
"on_tool_call": on_tool_call_callback,
|
||||
},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What is 2 + 2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Knowledge (RAG) Example - Agent-Centric API
|
||||
|
||||
Demonstrates knowledge with consolidated params.
|
||||
Supports: bool, list of sources, dict config, KnowledgeConfig
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable knowledge with list of sources
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with document knowledge.",
|
||||
knowledge=["docs/"], # List of file/folder paths
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What does the documentation say about installation?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Planning Example - Agent-Centric API
|
||||
|
||||
Demonstrates planning with consolidated params.
|
||||
Presets: reasoning, read_only, auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable planning with preset
|
||||
agent = Agent(
|
||||
instructions="You are a strategic planner.",
|
||||
planning="reasoning", # Presets: reasoning, read_only, auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Plan a 3-day trip to Tokyo.")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Reflection Example - Agent-Centric API
|
||||
|
||||
Demonstrates reflection (self-reflect) with consolidated params.
|
||||
Presets: minimal, standard, thorough
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable reflection with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant that reflects on your answers.",
|
||||
reflection="standard", # Presets: minimal, standard, thorough
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Explain quantum computing in simple terms.")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Web Search Example - Agent-Centric API
|
||||
|
||||
Demonstrates web search with consolidated params.
|
||||
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable web search with preset
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What are the latest AI developments in 2024?")
|
||||
print(response)
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.10"
|
||||
version = "0.11.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Development utilities for PraisonAI.
|
||||
|
||||
This module contains development-only tools that are NOT imported during normal runtime.
|
||||
"""
|
||||
|
||||
__all__ = ["generate_api_md", "ApiMdGenerator"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load development utilities."""
|
||||
if name in ("generate_api_md", "ApiMdGenerator"):
|
||||
from .api_md import generate_api_md, ApiMdGenerator
|
||||
if name == "generate_api_md":
|
||||
return generate_api_md
|
||||
return ApiMdGenerator
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,981 @@
|
||||
"""
|
||||
API Markdown Generator for PraisonAI.
|
||||
|
||||
Generates a comprehensive api.md file documenting all public API surfaces:
|
||||
- Python Core SDK (praisonaiagents)
|
||||
- Python Wrapper/Integrations (praisonai)
|
||||
- CLI commands
|
||||
- TypeScript package (praisonai-ts)
|
||||
|
||||
Uses AST-based static analysis to avoid importing heavy dependencies.
|
||||
|
||||
Example output format (OpenAI SDK style):
|
||||
|
||||
# Agents
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Agent, AgentConfig
|
||||
from praisonaiagents.types import RunResult
|
||||
```
|
||||
|
||||
Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent.py">start</a>(prompt: str, **kwargs) -> RunResult</code>
|
||||
|
||||
Usage:
|
||||
python -m praisonai._dev.api_md --write
|
||||
python -m praisonai._dev.api_md --check
|
||||
python -m praisonai._dev.api_md --stdout
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class SymbolInfo:
|
||||
"""Information about a discovered symbol."""
|
||||
name: str
|
||||
kind: str # 'class', 'function', 'type', 'constant'
|
||||
module_path: str # e.g., 'praisonaiagents.agent.agent'
|
||||
file_path: str # relative to repo root
|
||||
line_number: int
|
||||
signature: Optional[str] = None
|
||||
return_type: Optional[str] = None
|
||||
methods: List["MethodInfo"] = field(default_factory=list)
|
||||
docstring: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodInfo:
|
||||
"""Information about a class method."""
|
||||
name: str
|
||||
signature: str
|
||||
return_type: Optional[str]
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class CLICommand:
|
||||
"""Information about a CLI command."""
|
||||
command_path: str # e.g., 'praisonai docs api-md'
|
||||
help_text: Optional[str]
|
||||
file_path: str
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TSExport:
|
||||
"""Information about a TypeScript export."""
|
||||
name: str
|
||||
kind: str # 'class', 'function', 'type', 'const'
|
||||
source_file: str
|
||||
is_type: bool = False
|
||||
|
||||
|
||||
class ApiMdGenerator:
|
||||
"""
|
||||
Generates api.md from source code using AST analysis.
|
||||
|
||||
Design principles:
|
||||
- AST-first: Never import modules that may have heavy dependencies
|
||||
- Deterministic: Stable ordering for reproducible output
|
||||
- Comprehensive: Cover all public exports
|
||||
"""
|
||||
|
||||
# Section groupings for organizing exports
|
||||
SECTION_GROUPS = {
|
||||
'Shared Types': ['TaskOutput', 'ReflectionOutput', 'ToolResult', 'ToolValidationError',
|
||||
'GuardrailResult', 'StepResult', 'WorkflowContext', 'HandoffResult',
|
||||
'HandoffInputData', 'HandoffConfig', 'ContextPolicy', 'HandoffError',
|
||||
'HandoffCycleError', 'HandoffDepthError', 'HandoffTimeoutError'],
|
||||
'Agents': ['Agent', 'Agents', 'PraisonAIAgents', 'ImageAgent', 'ContextAgent',
|
||||
'DeepResearchAgent', 'QueryRewriterAgent', 'PromptExpanderAgent',
|
||||
'AutoAgents', 'AutoRagAgent', 'PlanningAgent', 'create_context_agent'],
|
||||
'Tools': ['Tools', 'BaseTool', 'tool', 'FunctionTool', 'ToolRegistry',
|
||||
'get_registry', 'register_tool', 'get_tool', 'validate_tool'],
|
||||
'Workflows': ['Workflow', 'WorkflowStep', 'Pipeline', 'Route', 'Parallel',
|
||||
'Loop', 'Repeat', 'route', 'parallel', 'loop', 'repeat'],
|
||||
'DB': ['db', 'DbAdapter', 'AsyncDbAdapter'],
|
||||
'Memory': ['Memory', 'MemoryConfig', 'MemoryBackend'],
|
||||
'Knowledge': ['Knowledge', 'Chunking', 'ChunkingStrategy', 'KnowledgeConfig'],
|
||||
'RAG': ['RAG', 'RAGConfig', 'RAGResult', 'RAGCitation', 'RetrievalConfig',
|
||||
'RetrievalPolicy', 'CitationsMode', 'ContextPack'],
|
||||
'Handoff': ['Handoff', 'handoff', 'handoff_filters', 'RECOMMENDED_PROMPT_PREFIX',
|
||||
'prompt_with_handoff_instructions'],
|
||||
'Guardrails': ['LLMGuardrail', 'GuardrailConfig', 'GuardrailAction', 'GUARDRAIL_PRESETS'],
|
||||
'Planning': ['Plan', 'PlanStep', 'TodoList', 'TodoItem', 'PlanStorage',
|
||||
'ApprovalCallback', 'READ_ONLY_TOOLS', 'RESTRICTED_TOOLS', 'PlanningConfig'],
|
||||
'Skills': ['SkillManager', 'SkillProperties', 'SkillMetadata', 'SkillLoader', 'SkillsConfig'],
|
||||
'Session': ['Session'],
|
||||
'MCP': ['MCP'],
|
||||
'Telemetry': ['get_telemetry', 'enable_telemetry', 'disable_telemetry',
|
||||
'enable_performance_mode', 'disable_performance_mode',
|
||||
'cleanup_telemetry_resources', 'MinimalTelemetry', 'TelemetryCollector'],
|
||||
'Observability': ['obs', 'FlowDisplay', 'track_workflow'],
|
||||
'Context': ['FastContext', 'FastContextResult', 'FileMatch', 'LineRange',
|
||||
'ContextManager', 'ManagerConfig'],
|
||||
'UI': ['AGUI', 'A2A'],
|
||||
'Config': ['MemoryConfig', 'KnowledgeConfig', 'PlanningConfig', 'ReflectionConfig',
|
||||
'GuardrailConfig', 'WebConfig', 'OutputConfig', 'ExecutionConfig',
|
||||
'TemplateConfig', 'CachingConfig', 'HooksConfig', 'SkillsConfig',
|
||||
'AutonomyConfig', 'OutputPreset', 'ExecutionPreset', 'AutonomyLevel',
|
||||
'WebSearchProvider', 'MultiAgentHooksConfig', 'MultiAgentOutputConfig',
|
||||
'MultiAgentExecutionConfig', 'MultiAgentPlanningConfig', 'MultiAgentMemoryConfig'],
|
||||
'Display': ['display_interaction', 'display_self_reflection', 'display_instruction',
|
||||
'display_tool_call', 'display_error', 'display_generating',
|
||||
'clean_triple_backticks', 'error_logs', 'register_display_callback',
|
||||
'sync_display_callbacks', 'async_display_callbacks'],
|
||||
'Utilities': ['resolve', 'ArrayMode', 'resolve_memory', 'resolve_knowledge',
|
||||
'resolve_output', 'resolve_execution', 'resolve_web', 'resolve_planning',
|
||||
'resolve_reflection', 'resolve_context', 'resolve_autonomy',
|
||||
'resolve_caching', 'resolve_hooks', 'resolve_skills', 'resolve_routing',
|
||||
'resolve_guardrails', 'resolve_guardrail_policies', 'is_policy_string',
|
||||
'parse_policy_string'],
|
||||
}
|
||||
|
||||
def __init__(self, repo_root: Optional[Path] = None):
|
||||
"""Initialize the generator with repository root path."""
|
||||
if repo_root is None:
|
||||
# Auto-detect repo root
|
||||
repo_root = self._find_repo_root()
|
||||
self.repo_root = Path(repo_root).resolve()
|
||||
|
||||
# Package paths
|
||||
self.agents_pkg = self.repo_root / "src" / "praisonai-agents" / "praisonaiagents"
|
||||
self.wrapper_pkg = self.repo_root / "src" / "praisonai" / "praisonai"
|
||||
self.ts_pkg = self.repo_root / "src" / "praisonai-ts"
|
||||
|
||||
# Discovered symbols
|
||||
self.agents_symbols: Dict[str, SymbolInfo] = {}
|
||||
self.wrapper_symbols: Dict[str, SymbolInfo] = {}
|
||||
self.cli_commands: List[CLICommand] = []
|
||||
self.ts_exports: List[TSExport] = []
|
||||
|
||||
def _find_repo_root(self) -> Path:
|
||||
"""Find repository root by looking for .git directory."""
|
||||
current = Path.cwd()
|
||||
while current != current.parent:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback to expected location
|
||||
return Path("/Users/praison/praisonai-package")
|
||||
|
||||
def discover_all(self) -> None:
|
||||
"""Discover all public API symbols."""
|
||||
self._discover_python_exports(self.agents_pkg, "praisonaiagents", self.agents_symbols)
|
||||
self._discover_python_exports(self.wrapper_pkg, "praisonai", self.wrapper_symbols)
|
||||
self._discover_cli_commands()
|
||||
self._discover_ts_exports()
|
||||
|
||||
def _discover_python_exports(
|
||||
self,
|
||||
pkg_path: Path,
|
||||
pkg_name: str,
|
||||
symbols: Dict[str, SymbolInfo]
|
||||
) -> None:
|
||||
"""Discover Python exports from __init__.py using AST."""
|
||||
init_file = pkg_path / "__init__.py"
|
||||
if not init_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(init_file, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
# Extract __all__ if present
|
||||
all_exports: Set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == '__all__':
|
||||
if isinstance(node.value, ast.List):
|
||||
for elt in node.value.elts:
|
||||
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
||||
all_exports.add(elt.value)
|
||||
|
||||
# Track imports to find source files
|
||||
import_sources: Dict[str, Tuple[str, str]] = {} # name -> (module, original_name)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
for alias in node.names:
|
||||
name = alias.asname if alias.asname else alias.name
|
||||
import_sources[name] = (node.module, alias.name)
|
||||
|
||||
# Parse __getattr__ for lazy-loaded symbols
|
||||
lazy_sources = self._parse_getattr_lazy_imports(tree)
|
||||
import_sources.update(lazy_sources)
|
||||
|
||||
# For each export, find its definition
|
||||
for export_name in sorted(all_exports):
|
||||
if export_name in import_sources:
|
||||
module_path, original_name = import_sources[export_name]
|
||||
# Resolve to file
|
||||
symbol_info = self._resolve_symbol(
|
||||
pkg_path, pkg_name, module_path, original_name, export_name
|
||||
)
|
||||
if symbol_info:
|
||||
symbols[export_name] = symbol_info
|
||||
else:
|
||||
# Defined in __init__.py itself
|
||||
symbol_info = self._find_symbol_in_file(
|
||||
init_file, export_name, pkg_name
|
||||
)
|
||||
if symbol_info:
|
||||
symbols[export_name] = symbol_info
|
||||
|
||||
def _parse_getattr_lazy_imports(self, tree: ast.Module) -> Dict[str, Tuple[str, str]]:
|
||||
"""
|
||||
Parse __getattr__ function to extract lazy-loaded imports.
|
||||
|
||||
Looks for patterns like:
|
||||
if name == "Agent":
|
||||
from .agent.agent import Agent
|
||||
return Agent
|
||||
"""
|
||||
lazy_sources: Dict[str, Tuple[str, str]] = {}
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef) and node.name == '__getattr__':
|
||||
# Walk through ALL nodes in the function to find if statements
|
||||
for stmt in ast.walk(node):
|
||||
if isinstance(stmt, ast.If):
|
||||
# Check if this is `if name == "SomeName":`
|
||||
symbol_name = self._extract_name_comparison(stmt.test)
|
||||
if symbol_name:
|
||||
# Look for import statement in the body
|
||||
for inner_stmt in stmt.body:
|
||||
if isinstance(inner_stmt, ast.ImportFrom) and inner_stmt.module:
|
||||
for alias in inner_stmt.names:
|
||||
imported_name = alias.name
|
||||
lazy_sources[symbol_name] = (inner_stmt.module, imported_name)
|
||||
break
|
||||
break
|
||||
|
||||
return lazy_sources
|
||||
|
||||
def _extract_name_comparison(self, test: ast.expr) -> Optional[str]:
|
||||
"""
|
||||
Extract the string value from comparisons like `name == "Agent"`.
|
||||
Also handles `name in ("A", "B", "C")` patterns.
|
||||
"""
|
||||
if isinstance(test, ast.Compare):
|
||||
# Handle: name == "Agent"
|
||||
if (len(test.ops) == 1 and
|
||||
isinstance(test.ops[0], ast.Eq) and
|
||||
isinstance(test.left, ast.Name) and test.left.id == 'name'):
|
||||
if len(test.comparators) == 1:
|
||||
comp = test.comparators[0]
|
||||
if isinstance(comp, ast.Constant) and isinstance(comp.value, str):
|
||||
return comp.value
|
||||
# Handle: name in ("A", "B", "C")
|
||||
if (len(test.ops) == 1 and
|
||||
isinstance(test.ops[0], ast.In) and
|
||||
isinstance(test.left, ast.Name) and test.left.id == 'name'):
|
||||
# Return first item for now (we'll get others via elif)
|
||||
if len(test.comparators) == 1:
|
||||
comp = test.comparators[0]
|
||||
if isinstance(comp, ast.Tuple):
|
||||
for elt in comp.elts:
|
||||
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
||||
return elt.value
|
||||
return None
|
||||
|
||||
def _resolve_symbol(
|
||||
self,
|
||||
pkg_path: Path,
|
||||
pkg_name: str,
|
||||
module_path: str,
|
||||
original_name: str,
|
||||
export_name: str
|
||||
) -> Optional[SymbolInfo]:
|
||||
"""Resolve a symbol to its definition file."""
|
||||
file_path = None
|
||||
|
||||
# Handle relative imports (starting with .)
|
||||
if module_path.startswith('.'):
|
||||
# Convert relative to absolute
|
||||
parts = module_path.lstrip('.').split('.')
|
||||
rel_depth = len(module_path) - len(module_path.lstrip('.'))
|
||||
if rel_depth == 1:
|
||||
# Same package
|
||||
file_path = pkg_path
|
||||
else:
|
||||
file_path = pkg_path.parent
|
||||
for _ in range(rel_depth - 1):
|
||||
file_path = file_path.parent
|
||||
for part in parts:
|
||||
if part:
|
||||
file_path = file_path / part
|
||||
# Handle absolute imports starting with package name
|
||||
elif module_path.startswith(pkg_name + '.') or module_path == pkg_name:
|
||||
rel_path = module_path[len(pkg_name):].lstrip('.')
|
||||
parts = rel_path.split('.') if rel_path else []
|
||||
file_path = pkg_path
|
||||
for part in parts:
|
||||
file_path = file_path / part
|
||||
else:
|
||||
# Treat as relative import within package (e.g., 'agent.agent' -> pkg_path/agent/agent.py)
|
||||
# This handles lazy imports in __getattr__ that use implicit relative paths
|
||||
parts = module_path.split('.')
|
||||
file_path = pkg_path
|
||||
for part in parts:
|
||||
file_path = file_path / part
|
||||
|
||||
if file_path is None:
|
||||
return None
|
||||
|
||||
# Try as directory with __init__.py or as .py file
|
||||
if file_path.is_dir():
|
||||
init_file = file_path / "__init__.py"
|
||||
if init_file.exists():
|
||||
return self._find_symbol_in_file(init_file, original_name, pkg_name, export_name)
|
||||
|
||||
py_file = file_path.with_suffix('.py')
|
||||
if py_file.exists():
|
||||
return self._find_symbol_in_file(py_file, original_name, pkg_name, export_name)
|
||||
|
||||
return None
|
||||
|
||||
def _find_symbol_in_file(
|
||||
self,
|
||||
file_path: Path,
|
||||
symbol_name: str,
|
||||
pkg_name: str,
|
||||
export_name: Optional[str] = None
|
||||
) -> Optional[SymbolInfo]:
|
||||
"""Find a symbol definition in a Python file."""
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return None
|
||||
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
module_path = self._file_to_module(file_path, pkg_name)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef) and node.name == symbol_name:
|
||||
methods = self._extract_methods(node)
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='class',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_class_signature(node),
|
||||
methods=methods,
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.FunctionDef) and node.name == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='function',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.AsyncFunctionDef) and node.name == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='function',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
docstring=ast.get_docstring(node)
|
||||
)
|
||||
elif isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == symbol_name:
|
||||
return SymbolInfo(
|
||||
name=export_name or symbol_name,
|
||||
kind='constant',
|
||||
module_path=module_path,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno
|
||||
)
|
||||
|
||||
# Check for re-exports in __init__.py
|
||||
if file_path.name == "__init__.py":
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
for alias in node.names:
|
||||
name = alias.asname if alias.asname else alias.name
|
||||
if name == symbol_name:
|
||||
# Recursively resolve
|
||||
return self._resolve_symbol(
|
||||
file_path.parent, pkg_name,
|
||||
'.' + node.module if not node.module.startswith('.') else node.module,
|
||||
alias.name, export_name or symbol_name
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _extract_methods(self, class_node: ast.ClassDef) -> List[MethodInfo]:
|
||||
"""Extract public methods from a class."""
|
||||
methods = []
|
||||
for node in class_node.body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if not node.name.startswith('_') or node.name in ('__init__', '__call__'):
|
||||
methods.append(MethodInfo(
|
||||
name=node.name,
|
||||
signature=self._extract_function_signature(node),
|
||||
return_type=self._extract_return_type(node),
|
||||
line_number=node.lineno
|
||||
))
|
||||
return sorted(methods, key=lambda m: m.name)
|
||||
|
||||
def _extract_function_signature(self, node: ast.FunctionDef) -> str:
|
||||
"""Extract function signature as string."""
|
||||
args = []
|
||||
|
||||
# Regular args
|
||||
defaults_offset = len(node.args.args) - len(node.args.defaults)
|
||||
for i, arg in enumerate(node.args.args):
|
||||
if arg.arg == 'self' or arg.arg == 'cls':
|
||||
continue
|
||||
arg_str = arg.arg
|
||||
if arg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(arg.annotation)}"
|
||||
if i >= defaults_offset:
|
||||
default = node.args.defaults[i - defaults_offset]
|
||||
arg_str += f" = {self._default_to_str(default)}"
|
||||
args.append(arg_str)
|
||||
|
||||
# *args
|
||||
if node.args.vararg:
|
||||
arg_str = f"*{node.args.vararg.arg}"
|
||||
if node.args.vararg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(node.args.vararg.annotation)}"
|
||||
args.append(arg_str)
|
||||
|
||||
# **kwargs
|
||||
if node.args.kwarg:
|
||||
arg_str = f"**{node.args.kwarg.arg}"
|
||||
if node.args.kwarg.annotation:
|
||||
arg_str += f": {self._annotation_to_str(node.args.kwarg.annotation)}"
|
||||
args.append(arg_str)
|
||||
|
||||
return f"({', '.join(args)})"
|
||||
|
||||
def _extract_class_signature(self, node: ast.ClassDef) -> str:
|
||||
"""Extract class __init__ signature."""
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef) and item.name == '__init__':
|
||||
return self._extract_function_signature(item)
|
||||
return "()"
|
||||
|
||||
def _extract_return_type(self, node: ast.FunctionDef) -> Optional[str]:
|
||||
"""Extract return type annotation."""
|
||||
if node.returns:
|
||||
return self._annotation_to_str(node.returns)
|
||||
return None
|
||||
|
||||
def _annotation_to_str(self, node: ast.expr) -> str:
|
||||
"""Convert AST annotation to string."""
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.Constant):
|
||||
return repr(node.value)
|
||||
elif isinstance(node, ast.Subscript):
|
||||
value = self._annotation_to_str(node.value)
|
||||
slice_val = self._annotation_to_str(node.slice)
|
||||
return f"{value}[{slice_val}]"
|
||||
elif isinstance(node, ast.Attribute):
|
||||
return f"{self._annotation_to_str(node.value)}.{node.attr}"
|
||||
elif isinstance(node, ast.Tuple):
|
||||
elts = ', '.join(self._annotation_to_str(e) for e in node.elts)
|
||||
return elts
|
||||
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
||||
left = self._annotation_to_str(node.left)
|
||||
right = self._annotation_to_str(node.right)
|
||||
return f"{left} | {right}"
|
||||
elif isinstance(node, ast.List):
|
||||
elts = ', '.join(self._annotation_to_str(e) for e in node.elts)
|
||||
return f"[{elts}]"
|
||||
return "Any"
|
||||
|
||||
def _default_to_str(self, node: ast.expr) -> str:
|
||||
"""Convert default value to string."""
|
||||
if isinstance(node, ast.Constant):
|
||||
return repr(node.value)
|
||||
elif isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.List):
|
||||
return "[]"
|
||||
elif isinstance(node, ast.Dict):
|
||||
return "{}"
|
||||
elif isinstance(node, ast.Call):
|
||||
return "..."
|
||||
return "..."
|
||||
|
||||
def _get_relative_path(self, file_path: Path) -> str:
|
||||
"""Get path relative to repo root."""
|
||||
try:
|
||||
return "./" + str(file_path.relative_to(self.repo_root))
|
||||
except ValueError:
|
||||
return str(file_path)
|
||||
|
||||
def _file_to_module(self, file_path: Path, pkg_name: str) -> str:
|
||||
"""Convert file path to module path."""
|
||||
try:
|
||||
if pkg_name == "praisonaiagents":
|
||||
rel = file_path.relative_to(self.agents_pkg.parent)
|
||||
else:
|
||||
rel = file_path.relative_to(self.wrapper_pkg.parent)
|
||||
parts = list(rel.parts)
|
||||
if parts[-1] == "__init__.py":
|
||||
parts = parts[:-1]
|
||||
elif parts[-1].endswith('.py'):
|
||||
parts[-1] = parts[-1][:-3]
|
||||
return '.'.join(parts)
|
||||
except ValueError:
|
||||
return pkg_name
|
||||
|
||||
def _discover_cli_commands(self) -> None:
|
||||
"""Discover CLI commands from Typer apps."""
|
||||
commands_dir = self.wrapper_pkg / "cli" / "commands"
|
||||
if not commands_dir.exists():
|
||||
return
|
||||
|
||||
for py_file in sorted(commands_dir.glob("*.py")):
|
||||
if py_file.name.startswith('_'):
|
||||
continue
|
||||
self._parse_typer_commands(py_file)
|
||||
|
||||
# Also check main.py for argparse commands
|
||||
main_py = self.wrapper_pkg / "cli" / "main.py"
|
||||
if main_py.exists():
|
||||
self._parse_main_commands(main_py)
|
||||
|
||||
def _parse_typer_commands(self, file_path: Path) -> None:
|
||||
"""Parse Typer command definitions from a file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
command_name = file_path.stem
|
||||
|
||||
# Find @app.command() decorators
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for decorator in node.decorator_list:
|
||||
if self._is_command_decorator(decorator):
|
||||
cmd_name = self._extract_command_name(decorator, node.name)
|
||||
help_text = ast.get_docstring(node)
|
||||
self.cli_commands.append(CLICommand(
|
||||
command_path=f"praisonai {command_name} {cmd_name}".strip(),
|
||||
help_text=help_text[:100] if help_text else None,
|
||||
file_path=rel_path,
|
||||
line_number=node.lineno
|
||||
))
|
||||
|
||||
def _is_command_decorator(self, decorator: ast.expr) -> bool:
|
||||
"""Check if decorator is @app.command() or similar."""
|
||||
if isinstance(decorator, ast.Call):
|
||||
if isinstance(decorator.func, ast.Attribute):
|
||||
return decorator.func.attr in ('command', 'callback')
|
||||
elif isinstance(decorator, ast.Attribute):
|
||||
return decorator.attr in ('command', 'callback')
|
||||
return False
|
||||
|
||||
def _extract_command_name(self, decorator: ast.expr, func_name: str) -> str:
|
||||
"""Extract command name from decorator or function name."""
|
||||
if isinstance(decorator, ast.Call):
|
||||
for arg in decorator.args:
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
return arg.value
|
||||
for kw in decorator.keywords:
|
||||
if kw.arg == 'name' and isinstance(kw.value, ast.Constant):
|
||||
return kw.value.value
|
||||
# Convert function name: some_command -> some-command
|
||||
return func_name.replace('_', '-')
|
||||
|
||||
def _parse_main_commands(self, file_path: Path) -> None:
|
||||
"""Parse main CLI commands from argparse in main.py."""
|
||||
# Add base commands that are handled in main.py
|
||||
rel_path = self._get_relative_path(file_path)
|
||||
base_commands = [
|
||||
("praisonai", "Main CLI entry point"),
|
||||
("praisonai run", "Run agents from YAML file"),
|
||||
("praisonai chat", "Terminal chat interface"),
|
||||
("praisonai code", "Terminal code assistant"),
|
||||
]
|
||||
for cmd, help_text in base_commands:
|
||||
self.cli_commands.append(CLICommand(
|
||||
command_path=cmd,
|
||||
help_text=help_text,
|
||||
file_path=rel_path,
|
||||
line_number=1
|
||||
))
|
||||
|
||||
def _discover_ts_exports(self) -> None:
|
||||
"""Discover TypeScript exports from index.ts."""
|
||||
index_file = self.ts_pkg / "src" / "index.ts"
|
||||
if not index_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(index_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except (IOError, UnicodeDecodeError):
|
||||
return
|
||||
|
||||
# Parse export statements using regex
|
||||
# export { Name1, Name2 } from './path'
|
||||
export_pattern = r"export\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]"
|
||||
type_export_pattern = r"export\s+type\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]"
|
||||
|
||||
for match in re.finditer(export_pattern, content):
|
||||
names = match.group(1)
|
||||
source = match.group(2)
|
||||
for name in names.split(','):
|
||||
name = name.strip()
|
||||
if ' as ' in name:
|
||||
name = name.split(' as ')[1].strip()
|
||||
if name and not name.startswith('type '):
|
||||
self.ts_exports.append(TSExport(
|
||||
name=name,
|
||||
kind='export',
|
||||
source_file=source,
|
||||
is_type=False
|
||||
))
|
||||
|
||||
for match in re.finditer(type_export_pattern, content):
|
||||
names = match.group(1)
|
||||
source = match.group(2)
|
||||
for name in names.split(','):
|
||||
name = name.strip()
|
||||
if ' as ' in name:
|
||||
name = name.split(' as ')[1].strip()
|
||||
if name:
|
||||
self.ts_exports.append(TSExport(
|
||||
name=name,
|
||||
kind='type',
|
||||
source_file=source,
|
||||
is_type=True
|
||||
))
|
||||
|
||||
def generate(self) -> str:
|
||||
"""Generate the api.md content."""
|
||||
self.discover_all()
|
||||
|
||||
lines = [
|
||||
"# PraisonAI API Reference",
|
||||
"",
|
||||
"This file is auto-generated. Do not edit manually.",
|
||||
"Regenerate with: `praisonai docs api-md --write`",
|
||||
"",
|
||||
]
|
||||
|
||||
# Group symbols by section
|
||||
sections = self._group_symbols_by_section()
|
||||
|
||||
# Generate each section
|
||||
section_order = [
|
||||
'Shared Types', 'Agents', 'Tools', 'Workflows', 'DB', 'Memory',
|
||||
'Knowledge', 'RAG', 'Handoff', 'Guardrails', 'Planning', 'Skills',
|
||||
'Session', 'MCP', 'Telemetry', 'Observability', 'Context', 'UI',
|
||||
'Config', 'Display', 'Utilities'
|
||||
]
|
||||
|
||||
for section_name in section_order:
|
||||
if section_name in sections and sections[section_name]:
|
||||
lines.extend(self._generate_section(section_name, sections[section_name]))
|
||||
|
||||
# Handle remaining symbols not in predefined sections
|
||||
remaining = self._get_remaining_symbols(sections)
|
||||
if remaining:
|
||||
lines.extend(self._generate_section('Other', remaining))
|
||||
|
||||
# Wrapper package exports
|
||||
if self.wrapper_symbols:
|
||||
lines.extend(self._generate_wrapper_section())
|
||||
|
||||
# CLI section
|
||||
if self.cli_commands:
|
||||
lines.extend(self._generate_cli_section())
|
||||
|
||||
# TypeScript section
|
||||
if self.ts_exports:
|
||||
lines.extend(self._generate_ts_section())
|
||||
|
||||
# Optional Plugins section
|
||||
lines.extend(self._generate_plugins_section())
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _group_symbols_by_section(self) -> Dict[str, List[SymbolInfo]]:
|
||||
"""Group symbols by their section."""
|
||||
sections: Dict[str, List[SymbolInfo]] = {}
|
||||
|
||||
for section_name, symbol_names in self.SECTION_GROUPS.items():
|
||||
section_symbols = []
|
||||
for name in symbol_names:
|
||||
if name in self.agents_symbols:
|
||||
section_symbols.append(self.agents_symbols[name])
|
||||
if section_symbols:
|
||||
sections[section_name] = sorted(section_symbols, key=lambda s: s.name)
|
||||
|
||||
return sections
|
||||
|
||||
def _get_remaining_symbols(self, sections: Dict[str, List[SymbolInfo]]) -> List[SymbolInfo]:
|
||||
"""Get symbols not in any predefined section."""
|
||||
used_names = set()
|
||||
for symbol_list in sections.values():
|
||||
for symbol in symbol_list:
|
||||
used_names.add(symbol.name)
|
||||
|
||||
remaining = []
|
||||
for name, symbol in self.agents_symbols.items():
|
||||
if name not in used_names:
|
||||
remaining.append(symbol)
|
||||
|
||||
return sorted(remaining, key=lambda s: s.name)
|
||||
|
||||
def _generate_section(self, section_name: str, symbols: List[SymbolInfo]) -> List[str]:
|
||||
"""Generate a section of the API documentation."""
|
||||
lines = [
|
||||
f"# {section_name}",
|
||||
"",
|
||||
]
|
||||
|
||||
# Types block
|
||||
imports = []
|
||||
for symbol in symbols:
|
||||
if symbol.module_path.startswith('praisonaiagents'):
|
||||
imports.append(symbol.name)
|
||||
|
||||
if imports:
|
||||
lines.append("Types:")
|
||||
lines.append("```python")
|
||||
# Group imports by module
|
||||
lines.append(f"from praisonaiagents import {', '.join(sorted(imports))}")
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
# Methods block
|
||||
methods_lines = []
|
||||
for symbol in symbols:
|
||||
if symbol.kind == 'class' and symbol.methods:
|
||||
for method in symbol.methods:
|
||||
if method.name == '__init__':
|
||||
continue
|
||||
ret_type = f" -> {method.return_type}" if method.return_type else ""
|
||||
methods_lines.append(
|
||||
f'* <code title="class {symbol.name}">{symbol.name}.'
|
||||
f'<a href="{symbol.file_path}">{method.name}</a>'
|
||||
f'{method.signature}{ret_type}</code>'
|
||||
)
|
||||
elif symbol.kind == 'function':
|
||||
ret_type = f" -> {symbol.return_type}" if symbol.return_type else ""
|
||||
module_prefix = symbol.module_path.split('.')[0]
|
||||
methods_lines.append(
|
||||
f'* <code title="function">{module_prefix}.'
|
||||
f'<a href="{symbol.file_path}">{symbol.name}</a>'
|
||||
f'{symbol.signature or "()"}{ret_type}</code>'
|
||||
)
|
||||
|
||||
if methods_lines:
|
||||
lines.append("Methods:")
|
||||
lines.append("")
|
||||
lines.extend(methods_lines)
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_wrapper_section(self) -> List[str]:
|
||||
"""Generate section for praisonai wrapper package."""
|
||||
lines = [
|
||||
"# Wrapper (praisonai)",
|
||||
"",
|
||||
"Types:",
|
||||
"```python",
|
||||
]
|
||||
|
||||
imports = sorted(self.wrapper_symbols.keys())
|
||||
lines.append(f"from praisonai import {', '.join(imports)}")
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_cli_section(self) -> List[str]:
|
||||
"""Generate CLI commands section."""
|
||||
lines = [
|
||||
"# CLI",
|
||||
"",
|
||||
"Methods:",
|
||||
"",
|
||||
]
|
||||
|
||||
# Sort and deduplicate commands
|
||||
seen = set()
|
||||
for cmd in sorted(self.cli_commands, key=lambda c: c.command_path):
|
||||
if cmd.command_path in seen:
|
||||
continue
|
||||
seen.add(cmd.command_path)
|
||||
lines.append(
|
||||
f'* <code title="cli">{cmd.command_path} '
|
||||
f'<a href="{cmd.file_path}">--help</a></code>'
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def _generate_ts_section(self) -> List[str]:
|
||||
"""Generate TypeScript exports section."""
|
||||
lines = [
|
||||
"# TypeScript",
|
||||
"",
|
||||
"Types/Exports:",
|
||||
"```ts",
|
||||
]
|
||||
|
||||
# Group by source file
|
||||
by_source: Dict[str, List[TSExport]] = {}
|
||||
for export in self.ts_exports:
|
||||
if export.source_file not in by_source:
|
||||
by_source[export.source_file] = []
|
||||
by_source[export.source_file].append(export)
|
||||
|
||||
for source in sorted(by_source.keys()):
|
||||
exports = by_source[source]
|
||||
regular = [e.name for e in exports if not e.is_type]
|
||||
types = [e.name for e in exports if e.is_type]
|
||||
|
||||
if regular:
|
||||
lines.append(f'export {{ {", ".join(sorted(regular))} }} from "{source}";')
|
||||
if types:
|
||||
lines.append(f'export type {{ {", ".join(sorted(types))} }} from "{source}";')
|
||||
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def _generate_plugins_section(self) -> List[str]:
|
||||
"""Generate optional plugins section."""
|
||||
return [
|
||||
"# Optional Plugins",
|
||||
"",
|
||||
"External tools are available via `praisonai-tools` package:",
|
||||
"",
|
||||
"```bash",
|
||||
"pip install praisonai-tools",
|
||||
"```",
|
||||
"",
|
||||
"See [PraisonAI-tools](https://github.com/MervinPraison/PraisonAI-tools) for available tools.",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def generate_api_md(
|
||||
repo_root: Optional[Path] = None,
|
||||
output_path: Optional[Path] = None,
|
||||
check: bool = False,
|
||||
stdout: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Generate api.md file.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path (auto-detected if None)
|
||||
output_path: Output file path (defaults to repo_root/api.md)
|
||||
check: If True, check if existing file matches (exit 1 if different)
|
||||
stdout: If True, print to stdout instead of writing file
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for check failure)
|
||||
"""
|
||||
generator = ApiMdGenerator(repo_root)
|
||||
content = generator.generate()
|
||||
|
||||
if stdout:
|
||||
print(content)
|
||||
return 0
|
||||
|
||||
if output_path is None:
|
||||
output_path = generator.repo_root / "api.md"
|
||||
|
||||
if check:
|
||||
if output_path.exists():
|
||||
with open(output_path, 'r', encoding='utf-8') as f:
|
||||
existing = f.read()
|
||||
if existing == content:
|
||||
print(f"✓ {output_path} is up to date")
|
||||
return 0
|
||||
else:
|
||||
print(f"✗ {output_path} is out of date")
|
||||
print("Run `praisonai docs api-md --write` to regenerate")
|
||||
return 1
|
||||
else:
|
||||
print(f"✗ {output_path} does not exist")
|
||||
print("Run `praisonai docs api-md --write` to generate")
|
||||
return 1
|
||||
|
||||
# Write the file
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Generated {output_path}")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point for the generator."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate api.md for PraisonAI"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--write', action='store_true', default=True,
|
||||
help='Write api.md file (default)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--check', action='store_true',
|
||||
help='Check if api.md is up to date (exit 1 if not)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--stdout', action='store_true',
|
||||
help='Print to stdout instead of writing file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o', type=Path,
|
||||
help='Output file path (default: repo_root/api.md)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
exit_code = generate_api_md(
|
||||
output_path=args.output,
|
||||
check=args.check,
|
||||
stdout=args.stdout
|
||||
)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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 @@
|
||||
__version__ = "3.6.2"
|
||||
__version__ = "3.7.2"
|
||||
@@ -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",
|
||||
|
||||
Executable
+34
@@ -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)
|
||||
Executable
+47
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# Tests for development utilities
|
||||
@@ -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
|
||||
Generated
+4
-4
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user