Release v3.11.0

This commit is contained in:
Mervin Praison committed 2026-01-29 10:45:00 +00:00
1 parent d389ff348f
commit a843e13641
59 files changed
+10726 -50

No files matched your search

+607 -2
View File
@@ -3,11 +3,27 @@
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
from praisonaiagents import Agent, Agents, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
```
Methods:
@@ -112,18 +128,607 @@ Methods:
* <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">aanalyze_codebase</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">acreate_implementation_blueprint</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">agenerate_prp</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_codebase</a>(project_path: str) -> Dict[str, Any]</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_implementation_blueprint</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> 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">generate_prp</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> 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">aedit</a>(image: str, prompt: str, mask: Optional[str] = None, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">agenerate</a>(prompt: str, **kwargs) -> 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">avariation</a>(image: str, n: int = 1, size: Optional[str] = None, **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">edit</a>(image: str, prompt: str, mask: Optional[str] = None, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">generate</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 ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">variation</a>(image: str, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">analyze_context</a>(context: str) -> str</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">analyze_context_sync</a>(context: str) -> str</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">create_plan</a>(request: str, agents: List['Agent'], tasks: Optional[List['Task']] = None, context: Optional[str] = None) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">create_plan_sync</a>(request: str, agents: List['Agent'], tasks: Optional[List['Task']] = None, context: Optional[str] = None) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">is_tool_allowed</a>(tool_name: str) -> bool</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">refine_plan</a>(plan: Plan, feedback: str) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">refine_plan_sync</a>(plan: Plan, feedback: str) -> Plan</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 Tools, tool
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="class Tools">Tools.<a href="./src/praisonai-agents/praisonaiagents/tools/tools.py">internet_search</a>(*args, **kwargs)</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 Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: 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 = None, steps: Optional[List[Any]] = None, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item', parallel: bool = False, max_workers: Optional[int] = None, output_variable: Optional[str] = None) -> 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, MemoryBackend, MemoryConfig
```
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">delete_long_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories_matching</a>(query: str, memory_type: Optional[str] = None, limit: int = 10) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memory</a>(memory_id: str, memory_type: Optional[str] = None) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_short_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_short_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_quality</a>(text: str, quality_score: float, task_id: Optional[str] = None, iteration: Optional[int] = None, metrics: Optional[Dict[str, float]] = None, memory_type: Literal['short', 'long'] = 'long') -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_user_memory</a>(user_id: str, text: str, extra: Dict[str, Any] = None)</code>
* <code title="class MemoryConfig">MemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# Knowledge
Types:
```python
from praisonaiagents import Chunking, ChunkingStrategy, Knowledge, KnowledgeConfig
```
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>
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# RAG
Types:
```python
from praisonaiagents import CitationsMode, ContextPack, RAG, RAGCitation, RAGConfig, RAGResult, RetrievalConfig, RetrievalPolicy
```
Methods:
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">format_for_prompt</a>(include_sources: bool = True) -> str</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'ContextPack'</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">has_citations</a>() -> bool</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <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>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">from_dict</a>(data: Dict[str, Any]) -> 'RetrievalConfig'</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">get_strategy</a>(corpus_stats = None)</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">get_token_budget</a>(model_name: Optional[str] = None)</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">should_retrieve</a>(query: str, force: bool = False, skip: bool = False) -> bool</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_knowledge_config</a>() -> Dict[str, Any]</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_rag_config</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 GuardrailAction, GuardrailConfig, LLMGuardrail
```
Methods:
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <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>
# Planning
Types:
```python
from praisonaiagents import ApprovalCallback, Plan, PlanStep, PlanStorage, PlanningConfig, READ_ONLY_TOOLS, RESTRICTED_TOOLS, TodoItem, TodoList
```
Methods:
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">__call__</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">always_approve</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">always_reject</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">approve_if_no_dangerous_tools</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">approve_if_small</a>(plan: 'Plan', max_steps: int = 5) -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">async_call</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">console_approval</a>(plan: 'Plan') -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">add_step</a>(step: PlanStep) -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">approve</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">cancel</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">complete</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">completed_step_ids</a>() -> List[str]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_dict</a>(data: Dict[str, Any]) -> 'Plan'</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_markdown</a>(markdown: str) -> 'Plan'</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">get_next_steps</a>() -> List[PlanStep]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">get_step</a>(step_id: str) -> Optional[PlanStep]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">is_complete</a>() -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">progress</a>() -> float</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">remove_step</a>(step_id: str) -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">start_execution</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_markdown</a>() -> str</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">update_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_dict</a>(data: Dict[str, Any]) -> 'PlanStep'</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">is_ready</a>(completed_steps: List[str]) -> bool</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_complete</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_in_progress</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_skipped</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">cleanup_old_plans</a>(keep_count: int = 10) -> int</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">delete_plan</a>(plan_id: str) -> bool</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">delete_todo</a>(name: str) -> bool</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">get_latest_plan</a>() -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">list_plans</a>() -> List[Dict[str, Any]]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">list_todos</a>() -> List[str]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_plan</a>(plan_id: str) -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_plan_from_file</a>(path: str) -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_todo</a>(name: str = 'current') -> Optional[TodoList]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">save_plan</a>(plan: Plan, filename: Optional[str] = None) -> str</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">save_todo</a>(todo: TodoList, name: str = 'current') -> str</code>
* <code title="class PlanningConfig">PlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">complete</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_dict</a>(data: Dict[str, Any]) -> 'TodoItem'</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">is_ready</a>(completed_ids: List[str]) -> bool</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">reset</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">start</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">add</a>(item: Union[TodoItem, str]) -> TodoItem</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">complete</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">completed</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">completed_ids</a>() -> List[str]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_dict</a>(data: Dict[str, Any]) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_json</a>(json_str: str) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_markdown</a>(markdown: str) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_plan</a>(plan: 'Plan') -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">get</a>(item_id: str) -> Optional[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">get_ready_items</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">in_progress</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">is_complete</a>() -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">pending</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">progress</a>() -> float</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">remove</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">start</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">sync_with_plan</a>(plan: 'Plan') -> None</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_json</a>() -> str</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_markdown</a>() -> str</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">update_from_plan</a>(plan: 'Plan') -> None</code>
# Skills
Types:
```python
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties, SkillsConfig
```
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>
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</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>
# MCP
Types:
```python
from praisonaiagents import MCP
```
Methods:
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_tools</a>() -> List[Callable]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">shutdown</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">to_openai_tool</a>()</code>
# Telemetry
Types:
```python
from praisonaiagents import MinimalTelemetry, TelemetryCollector, cleanup_telemetry_resources, disable_performance_mode, disable_telemetry, enable_performance_mode, enable_telemetry, get_telemetry
```
Methods:
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">flush</a>()</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">get_metrics</a>() -> Dict[str, Any]</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">shutdown</a>()</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_agent_execution</a>(agent_name: str = None, success: bool = True, async_mode: bool = False)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_error</a>(error_type: str = None)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_feature_usage</a>(feature_name: str)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_task_completion</a>(task_name: str = None, success: bool = True)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_tool_usage</a>(tool_name: str, success: bool = True, execution_time: float = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">get_metrics</a>() -> Dict[str, Any]</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">record_cost</a>(cost: float, model: str = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">record_tokens</a>(prompt_tokens: int, completion_tokens: int, model: str = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">start</a>()</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">stop</a>()</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_agent_execution</a>(agent_name: str, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_llm_call</a>(model: str = None, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_task_execution</a>(task_name: str, agent_name: str = None, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_tool_call</a>(tool_name: str, **attributes)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">cleanup_telemetry_resources</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">disable_performance_mode</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">disable_telemetry</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">enable_performance_mode</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">enable_telemetry</a>()</code>
* <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">emergency_truncate</a>(messages: List[Dict[str, Any]], target_tokens: int) -> List[Dict[str, Any]]</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>
# Config
Types:
```python
from praisonaiagents import AutonomyConfig, AutonomyLevel, CachingConfig, ExecutionConfig, ExecutionPreset, GuardrailConfig, HooksConfig, KnowledgeConfig, MemoryConfig, MultiAgentExecutionConfig, MultiAgentHooksConfig, MultiAgentMemoryConfig, MultiAgentOutputConfig, MultiAgentPlanningConfig, OutputConfig, OutputPreset, PlanningConfig, ReflectionConfig, SkillsConfig, TemplateConfig, WebConfig, WebSearchProvider
```
Methods:
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MemoryConfig">MemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentExecutionConfig">MultiAgentExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentHooksConfig">MultiAgentHooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentMemoryConfig">MultiAgentMemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentOutputConfig">MultiAgentOutputConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentPlanningConfig">MultiAgentPlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class OutputConfig">OutputConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class PlanningConfig">PlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ReflectionConfig">ReflectionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TemplateConfig">TemplateConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class WebConfig">WebConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.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, metrics = 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, elapsed_time: float = None, success: bool = True)</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>
# Utilities
Types:
```python
from praisonaiagents import ArrayMode, is_policy_string, parse_policy_string, resolve, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrail_policies, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web
```
Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/parse_utils.py">is_policy_string</a>(value: str) -> bool</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/parse_utils.py">parse_policy_string</a>(value: str) -> tuple</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve</a>(value: Any, param_name: str, config_class: Optional[Type] = None, presets: Optional[Dict[str, Any]] = None, default: Any = None, instance_check: Optional[Callable[[Any], bool]] = None, url_schemes: Optional[Dict[str, str]] = None, array_mode: Optional[str] = None, string_mode: Optional[str] = None) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_autonomy</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_caching</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_context</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_execution</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_guardrail_policies</a>(policies: list, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_guardrails</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_hooks</a>(value: Any, config_class: Optional[Type] = None) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_knowledge</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_memory</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_output</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_planning</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_reflection</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_routing</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_skills</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_web</a>(value: Any, config_class: Type) -> Any</code>
# Other
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.10.27" \
"praisonai>=3.11.0" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.10.27" \
"praisonai>=3.11.0" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.10.27" \
"praisonai>=3.11.0" \
"praisonai[ui]" \
"praisonai[crewai]"
+76
View File
@@ -0,0 +1,76 @@
"""
Messaging Bot Example - Deploy Agents to Telegram, Discord, Slack
This example demonstrates how to configure bots for different
messaging platforms.
"""
import os
from praisonaiagents import Agent, BotConfig
# Telegram Bot Configuration
telegram_config = BotConfig(
token=os.environ.get("TELEGRAM_BOT_TOKEN", ""),
command_prefix="/",
mention_required=True,
typing_indicator=True,
max_message_length=4096,
metadata={"platform": "telegram"}
)
# Discord Bot Configuration
discord_config = BotConfig(
token=os.environ.get("DISCORD_BOT_TOKEN", ""),
command_prefix="!",
mention_required=True,
typing_indicator=True,
metadata={
"platform": "discord",
"guild_ids": [] # Add your guild IDs here
}
)
# Slack Bot Configuration
slack_config = BotConfig(
token=os.environ.get("SLACK_BOT_TOKEN", ""),
command_prefix="/",
mention_required=True,
typing_indicator=True,
metadata={
"platform": "slack",
"app_token": os.environ.get("SLACK_APP_TOKEN", "")
}
)
# Create the agent that will handle messages
assistant = Agent(
name="assistant",
instructions="""You are a helpful assistant that responds to messages
from users on messaging platforms. Be friendly and concise.""",
llm="gpt-4o-mini"
)
if __name__ == "__main__":
print("Bot Configurations:")
print()
print("Telegram Config:")
print(f" Token configured: {bool(telegram_config.token)}")
print(f" Command prefix: {telegram_config.command_prefix}")
print(f" Mention required: {telegram_config.mention_required}")
print()
print("Discord Config:")
print(f" Token configured: {bool(discord_config.token)}")
print(f" Command prefix: {discord_config.command_prefix}")
print()
print("Slack Config:")
print(f" Token configured: {bool(slack_config.token)}")
print(f" Command prefix: {slack_config.command_prefix}")
print()
print("To start a bot, use the CLI:")
print(" praisonai bot telegram --token $TELEGRAM_BOT_TOKEN")
print(" praisonai bot discord --token $DISCORD_BOT_TOKEN")
print(" praisonai bot slack --token $SLACK_BOT_TOKEN")
+170
View File
@@ -0,0 +1,170 @@
"""
Model Failover Example - Automatic Provider Switching
This example demonstrates how to configure model failover
for reliability and cost optimization.
"""
import os
from praisonaiagents import (
AuthProfile,
FailoverConfig,
FailoverManager,
)
# Create auth profiles for different providers
openai_profile = AuthProfile(
name="openai-primary",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", "sk-..."),
priority=1, # Highest priority
rate_limit=100,
)
anthropic_profile = AuthProfile(
name="anthropic-backup",
provider="anthropic",
api_key=os.environ.get("ANTHROPIC_API_KEY", "sk-ant-..."),
priority=2, # Second priority
rate_limit=50,
)
groq_profile = AuthProfile(
name="groq-fallback",
provider="groq",
api_key=os.environ.get("GROQ_API_KEY", "gsk-..."),
priority=3, # Third priority (fast but limited)
rate_limit=30,
)
# Configure failover behavior
failover_config = FailoverConfig(
max_retries=3,
retry_delay=1.0,
exponential_backoff=True,
max_retry_delay=60.0,
failover_on_rate_limit=True,
failover_on_timeout=True,
failover_on_error=True,
)
# Create failover manager
manager = FailoverManager(failover_config)
manager.add_profile(openai_profile)
manager.add_profile(anthropic_profile)
manager.add_profile(groq_profile)
def demonstrate_failover():
"""Demonstrate failover functionality."""
print("Failover Manager Status:")
print()
# Get current status
status = manager.status()
for name, info in status.items():
print(f" {name}:")
print(f" Status: {info.get('status', 'unknown')}")
print(f" Priority: {info.get('priority', 'N/A')}")
print(f" Failures: {info.get('failure_count', 0)}")
print()
# Get next available profile
next_profile = manager.get_next_profile()
if next_profile:
print(f"Next profile to use: {next_profile.name}")
print(f" Provider: {next_profile.provider}")
print(f" Priority: {next_profile.priority}")
print()
# Simulate marking a failure
print("Simulating failure on primary provider...")
manager.mark_failure("openai-primary", "Rate limit exceeded")
# Get next profile after failure
next_profile = manager.get_next_profile()
if next_profile:
print(f"After failure, next profile: {next_profile.name}")
print()
# Get retry delay
delay = manager.get_retry_delay("openai-primary")
print(f"Retry delay for openai-primary: {delay}s")
print()
# Reset all providers
print("Resetting all providers...")
manager.reset_all()
next_profile = manager.get_next_profile()
if next_profile:
print(f"After reset, next profile: {next_profile.name}")
def cost_optimization_example():
"""Example of cost-optimized failover."""
print("\nCost Optimization Example:")
print("-" * 40)
# Create cost-optimized manager
cost_manager = FailoverManager()
# Cheaper model first
cost_manager.add_profile(AuthProfile(
name="gpt-4o-mini",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", ""),
priority=1,
metadata={"model": "gpt-4o-mini", "cost_per_1k": 0.00015}
))
# Premium model as fallback
cost_manager.add_profile(AuthProfile(
name="gpt-4o",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", ""),
priority=2,
metadata={"model": "gpt-4o", "cost_per_1k": 0.005}
))
# Most expensive as last resort
cost_manager.add_profile(AuthProfile(
name="claude-opus",
provider="anthropic",
api_key=os.environ.get("ANTHROPIC_API_KEY", ""),
priority=3,
metadata={"model": "claude-3-opus", "cost_per_1k": 0.015}
))
print("Cost-optimized failover chain:")
for profile in [
cost_manager.get_profile("gpt-4o-mini"),
cost_manager.get_profile("gpt-4o"),
cost_manager.get_profile("claude-opus"),
]:
if profile:
cost = profile.metadata.get("cost_per_1k", "N/A")
print(f" {profile.priority}. {profile.name} (${cost}/1K tokens)")
if __name__ == "__main__":
print("Model Failover Configuration")
print("=" * 40)
print()
print("Failover Config:")
print(f" Max retries: {failover_config.max_retries}")
print(f" Initial delay: {failover_config.retry_delay}s")
print(f" Exponential backoff: {failover_config.exponential_backoff}")
print(f" Max delay: {failover_config.max_retry_delay}s")
print()
print("Auth Profiles:")
for profile in [openai_profile, anthropic_profile, groq_profile]:
print(f" {profile.name}:")
print(f" Provider: {profile.provider}")
print(f" Priority: {profile.priority}")
print(f" Rate limit: {profile.rate_limit} req/min")
print()
demonstrate_failover()
cost_optimization_example()
+47
View File
@@ -0,0 +1,47 @@
"""
Gateway Example - Multi-Agent Coordination
This example demonstrates how to use the Gateway for coordinating
multiple agents and managing real-time communication.
"""
from praisonaiagents import Agent, GatewayConfig, SessionConfig
# Configure the gateway
gateway_config = GatewayConfig(
host="127.0.0.1",
port=8765,
max_connections=100,
heartbeat_interval=30,
session_config=SessionConfig(
timeout=3600, # 1 hour session timeout
max_messages=500,
)
)
# Create specialized agents
researcher = Agent(
name="researcher",
instructions="You research topics thoroughly and provide detailed information.",
llm="gpt-4o-mini"
)
writer = Agent(
name="writer",
instructions="You write clear, engaging content based on research.",
llm="gpt-4o-mini"
)
# Example: Simple agent interaction
if __name__ == "__main__":
print("Gateway Configuration:")
print(f" Host: {gateway_config.host}")
print(f" Port: {gateway_config.port}")
print(f" WebSocket URL: {gateway_config.ws_url}")
print(f" Max Connections: {gateway_config.max_connections}")
print()
# Test agent
response = researcher.start("What are the key benefits of multi-agent systems?")
print("Researcher Response:")
print(response)
+137
View File
@@ -0,0 +1,137 @@
"""
Sandbox Example - Secure Code Execution
This example demonstrates how to configure and use sandboxes
for safe code execution.
"""
from praisonaiagents import (
SandboxConfig,
ResourceLimits,
SandboxResult,
SandboxStatus
)
from praisonaiagents.sandbox import SecurityPolicy
# Minimal sandbox for untrusted code
minimal_sandbox = SandboxConfig(
sandbox_type="subprocess",
resource_limits=ResourceLimits.minimal(),
security_policy=SecurityPolicy.strict(),
)
# Standard sandbox for general use
standard_sandbox = SandboxConfig(
sandbox_type="subprocess",
resource_limits=ResourceLimits.standard(),
security_policy=SecurityPolicy.standard(),
)
# Docker sandbox for full isolation
docker_sandbox = SandboxConfig.docker(image="python:3.11-slim")
# Custom sandbox configuration
custom_sandbox = SandboxConfig(
sandbox_type="subprocess",
working_dir="/tmp/sandbox",
resource_limits=ResourceLimits(
memory_mb=256,
cpu_percent=50,
timeout_seconds=30,
network_enabled=False,
max_processes=5,
),
security_policy=SecurityPolicy(
allow_network=False,
allow_file_write=True,
allow_subprocess=False,
blocked_imports=["subprocess", "os.system", "eval"],
),
auto_cleanup=True,
)
def demonstrate_result_handling():
"""Show how to handle sandbox results."""
# Simulated successful result
success_result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout="Hello, World!\n",
stderr="",
duration_seconds=0.1,
)
# Simulated timeout result
timeout_result = SandboxResult(
status=SandboxStatus.TIMEOUT,
exit_code=None,
stdout="Partial output...",
stderr="",
error="Execution timed out after 30 seconds",
duration_seconds=30.0,
)
# Simulated error result
error_result = SandboxResult(
status=SandboxStatus.FAILED,
exit_code=1,
stdout="",
stderr="NameError: name 'undefined_var' is not defined",
error="Execution failed",
)
print("Result Handling Examples:")
print()
for name, result in [
("Success", success_result),
("Timeout", timeout_result),
("Error", error_result)
]:
print(f"{name} Result:")
print(f" Status: {result.status.name}")
print(f" Success: {result.success}")
if result.stdout:
print(f" Output: {result.stdout.strip()}")
if result.stderr:
print(f" Error: {result.stderr.strip()}")
if result.error:
print(f" Message: {result.error}")
print()
if __name__ == "__main__":
print("Sandbox Configurations:")
print()
print("Minimal Sandbox (for untrusted code):")
print(f" Type: {minimal_sandbox.sandbox_type}")
print(f" Memory: {minimal_sandbox.resource_limits.memory_mb}MB")
print(f" Timeout: {minimal_sandbox.resource_limits.timeout_seconds}s")
print(f" Network: {minimal_sandbox.resource_limits.network_enabled}")
print()
print("Standard Sandbox:")
print(f" Type: {standard_sandbox.sandbox_type}")
print(f" Memory: {standard_sandbox.resource_limits.memory_mb}MB")
print(f" Timeout: {standard_sandbox.resource_limits.timeout_seconds}s")
print()
print("Docker Sandbox:")
print(f" Type: {docker_sandbox.sandbox_type}")
print(f" Image: {docker_sandbox.image}")
print()
print("Custom Sandbox:")
print(f" Type: {custom_sandbox.sandbox_type}")
print(f" Working Dir: {custom_sandbox.working_dir}")
print(f" Memory: {custom_sandbox.resource_limits.memory_mb}MB")
print(f" Max Processes: {custom_sandbox.resource_limits.max_processes}")
print()
demonstrate_result_handling()
print("To run code in sandbox via CLI:")
print(" praisonai sandbox run \"print('Hello, World!')\"")
print(" praisonai sandbox run --file script.py")
print(" praisonai sandbox shell")
+1 -1
View File
@@ -21,7 +21,7 @@ Simpler than competitors • More extensible • Faster • Agent-centric
| **Minimal API** | Fewer parameters, sensible defaults, explicit overrides |
| **Performance-First** | Lazy loading, optional dependencies, no hot-path regressions |
| **Production-Ready** | Safe by default, multi-agent safe, async-safe |
Powerful, lightweight, highly reliable and robust
---
## 2. Repository Structure
@@ -381,6 +381,38 @@ _LAZY_IMPORTS = {
# db and obs modules
'db': ('praisonaiagents.db', 'db'),
'obs': ('praisonaiagents.obs', 'obs'),
# Gateway protocols and config (implementations in praisonai wrapper)
'GatewayProtocol': ('praisonaiagents.gateway.protocols', 'GatewayProtocol'),
'GatewaySessionProtocol': ('praisonaiagents.gateway.protocols', 'GatewaySessionProtocol'),
'GatewayClientProtocol': ('praisonaiagents.gateway.protocols', 'GatewayClientProtocol'),
'GatewayEvent': ('praisonaiagents.gateway.protocols', 'GatewayEvent'),
'GatewayMessage': ('praisonaiagents.gateway.protocols', 'GatewayMessage'),
'EventType': ('praisonaiagents.gateway.protocols', 'EventType'),
'GatewayConfig': ('praisonaiagents.gateway.config', 'GatewayConfig'),
'SessionConfig': ('praisonaiagents.gateway.config', 'SessionConfig'),
# Bot protocols and config (implementations in praisonai wrapper)
'BotProtocol': ('praisonaiagents.bots.protocols', 'BotProtocol'),
'BotMessage': ('praisonaiagents.bots.protocols', 'BotMessage'),
'BotUser': ('praisonaiagents.bots.protocols', 'BotUser'),
'BotChannel': ('praisonaiagents.bots.protocols', 'BotChannel'),
'MessageType': ('praisonaiagents.bots.protocols', 'MessageType'),
'BotConfig': ('praisonaiagents.bots.config', 'BotConfig'),
# Sandbox protocols and config (implementations in praisonai wrapper)
'SandboxProtocol': ('praisonaiagents.sandbox.protocols', 'SandboxProtocol'),
'SandboxResult': ('praisonaiagents.sandbox.protocols', 'SandboxResult'),
'SandboxStatus': ('praisonaiagents.sandbox.protocols', 'SandboxStatus'),
'ResourceLimits': ('praisonaiagents.sandbox.protocols', 'ResourceLimits'),
'SandboxConfig': ('praisonaiagents.sandbox.config', 'SandboxConfig'),
'SecurityPolicy': ('praisonaiagents.sandbox.config', 'SecurityPolicy'),
# Model failover
'AuthProfile': ('praisonaiagents.llm.failover', 'AuthProfile'),
'ProviderStatus': ('praisonaiagents.llm.failover', 'ProviderStatus'),
'FailoverConfig': ('praisonaiagents.llm.failover', 'FailoverConfig'),
'FailoverManager': ('praisonaiagents.llm.failover', 'FailoverManager'),
}
@@ -1844,6 +1844,8 @@ Summary:"""
prompt: str,
max_iterations: Optional[int] = None,
timeout_seconds: Optional[float] = None,
completion_promise: Optional[str] = None,
clear_context: bool = False,
):
"""Run an autonomous task execution loop.
@@ -1852,12 +1854,17 @@ Summary:"""
- Progressive escalation based on task complexity
- Doom loop detection and recovery
- Iteration limits and timeouts
- Completion detection
- Completion detection (keyword-based or promise-based)
- Optional context clearing between iterations
Args:
prompt: The task to execute
max_iterations: Override max iterations (default from config)
timeout_seconds: Timeout in seconds (default: no timeout)
completion_promise: Optional string that signals completion when
wrapped in <promise>TEXT</promise> tags in the response
clear_context: Whether to clear chat history between iterations
(forces agent to rely on external state like files)
Returns:
AutonomyResult with success status, output, and metadata
@@ -1867,7 +1874,11 @@ Summary:"""
Example:
agent = Agent(instructions="...", autonomy=True)
result = agent.run_autonomous("Refactor the auth module")
result = agent.run_autonomous(
"Refactor the auth module",
completion_promise="DONE",
clear_context=True
)
if result.success:
print(result.output)
"""
@@ -1888,6 +1899,16 @@ Summary:"""
config_max_iter = self.autonomy_config.get("max_iterations", 20)
effective_max_iter = max_iterations if max_iterations is not None else config_max_iter
# Get completion_promise from config if not provided as param
effective_promise = completion_promise
if effective_promise is None:
effective_promise = self.autonomy_config.get("completion_promise")
# Get clear_context from config if not explicitly set
effective_clear_context = clear_context
if not clear_context:
effective_clear_context = self.autonomy_config.get("clear_context", False)
# Analyze prompt and get recommended stage
stage = self.get_recommended_stage(prompt)
@@ -1924,8 +1945,9 @@ Summary:"""
)
# Execute one turn using the agent's chat method
# Always use the original prompt (prompt re-injection)
try:
response = self.chat(prompt if iterations == 1 else "Continue with the task")
response = self.chat(prompt)
except Exception as e:
return AutonomyResult(
success=False,
@@ -1944,8 +1966,24 @@ Summary:"""
"response": str(response)[:500],
})
# Check for completion signals in response
response_lower = str(response).lower()
response_str = str(response)
# Check for completion promise FIRST (structured signal)
if effective_promise:
promise_tag = f"<promise>{effective_promise}</promise>"
if promise_tag in response_str:
return AutonomyResult(
success=True,
output=response_str,
completion_reason="promise",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time_module.time() - start_time,
)
# Check for keyword-based completion signals (fallback)
response_lower = response_str.lower()
completion_signals = [
"task completed", "task complete", "done",
"finished", "completed successfully",
@@ -1954,7 +1992,7 @@ Summary:"""
if any(signal in response_lower for signal in completion_signals):
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
@@ -1966,13 +2004,17 @@ Summary:"""
if stage == "direct":
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time_module.time() - start_time,
)
# Clear context between iterations if enabled
if effective_clear_context:
self.clear_history()
# Max iterations reached
return AutonomyResult(
@@ -4027,7 +4069,7 @@ Your Goal: {self.goal}"""
"execute_tool_fn": self.execute_tool,
"stream": stream,
"console": self.console if (self.verbose or stream) else None,
"display_fn": self.display_generating if self.verbose else None,
"display_fn": self._display_generating if self.verbose else None,
"reasoning_steps": reasoning_steps,
"verbose": self.verbose,
"max_iterations": 10
@@ -53,12 +53,16 @@ class AutonomyConfig:
doom_loop_threshold: Number of repeated actions to trigger doom loop
auto_escalate: Whether to automatically escalate complexity
observe: Whether to emit observability events
completion_promise: Optional string that signals completion when wrapped in <promise>TEXT</promise>
clear_context: Whether to clear chat history between iterations
"""
enabled: bool = True
max_iterations: int = 20
doom_loop_threshold: int = 3
auto_escalate: bool = True
observe: bool = False
completion_promise: Optional[str] = None
clear_context: bool = False
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "AutonomyConfig":
@@ -69,6 +73,8 @@ class AutonomyConfig:
doom_loop_threshold=data.get("doom_loop_threshold", 3),
auto_escalate=data.get("auto_escalate", True),
observe=data.get("observe", False),
completion_promise=data.get("completion_promise"),
clear_context=data.get("clear_context", False),
)
@@ -395,6 +401,8 @@ class AutonomyMixin:
prompt: str,
max_iterations: Optional[int] = None,
timeout_seconds: Optional[float] = None,
completion_promise: Optional[str] = None,
clear_context: bool = False,
) -> AutonomyResult:
"""Run an autonomous task execution loop.
@@ -403,12 +411,17 @@ class AutonomyMixin:
- Progressive escalation based on task complexity
- Doom loop detection and recovery
- Iteration limits and timeouts
- Completion detection
- Completion detection (keyword-based or promise-based)
- Optional context clearing between iterations
Args:
prompt: The task to execute
max_iterations: Override max iterations (default from config)
timeout_seconds: Timeout in seconds (default: no timeout)
completion_promise: Optional string that signals completion when
wrapped in <promise>TEXT</promise> tags in the response
clear_context: Whether to clear chat history between iterations
(forces agent to rely on external state like files)
Returns:
AutonomyResult with success status, output, and metadata
@@ -418,7 +431,11 @@ class AutonomyMixin:
Example:
agent = Agent(instructions="...", autonomy=True)
result = agent.run_autonomous("Refactor the auth module")
result = agent.run_autonomous(
"Refactor the auth module",
completion_promise="DONE",
clear_context=True
)
if result.success:
print(result.output)
"""
@@ -436,8 +453,17 @@ class AutonomyMixin:
config_max_iter = self.autonomy_config.get("max_iterations", 20)
effective_max_iter = max_iterations if max_iterations is not None else config_max_iter
# Get completion_promise from config if not provided as param
effective_promise = completion_promise
if effective_promise is None:
effective_promise = self.autonomy_config.get("completion_promise")
# Get clear_context from config if not explicitly set
effective_clear_context = clear_context
if not clear_context:
effective_clear_context = self.autonomy_config.get("clear_context", False)
# Analyze prompt and get recommended stage
signals = self.analyze_prompt(prompt)
stage = self.get_recommended_stage(prompt)
# Reset doom loop tracker for new task
@@ -474,9 +500,9 @@ class AutonomyMixin:
)
# Execute one turn using the agent's chat method
# The chat method is expected to be provided by the Agent class
# Always use the original prompt (prompt re-injection)
try:
response = self.chat(prompt if iterations == 1 else "Continue with the task")
response = self.chat(prompt)
except Exception as e:
logger.error(f"Error during autonomous execution: {e}")
return AutonomyResult(
@@ -496,8 +522,24 @@ class AutonomyMixin:
"response": str(response)[:500], # Truncate for storage
})
# Check for completion signals in response
response_lower = str(response).lower()
response_str = str(response)
# Check for completion promise FIRST (structured signal)
if effective_promise:
promise_tag = f"<promise>{effective_promise}</promise>"
if promise_tag in response_str:
return AutonomyResult(
success=True,
output=response_str,
completion_reason="promise",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time.time() - start_time,
)
# Check for keyword-based completion signals (fallback)
response_lower = response_str.lower()
completion_signals = [
"task completed",
"task complete",
@@ -509,7 +551,7 @@ class AutonomyMixin:
if any(signal in response_lower for signal in completion_signals):
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
@@ -521,13 +563,17 @@ class AutonomyMixin:
if stage == "direct":
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time.time() - start_time,
)
# Clear context between iterations if enabled
if effective_clear_context:
self.clear_history()
# Max iterations reached
return AutonomyResult(
@@ -0,0 +1,33 @@
"""
Bot Protocols for PraisonAI Agents.
Provides protocols and base classes for building messaging bot implementations
(Telegram, Discord, Slack, etc.) that connect agents to messaging platforms.
This module contains only protocols and lightweight utilities.
Heavy implementations live in the praisonai wrapper package.
"""
from .protocols import (
BotProtocol,
BotMessageProtocol,
BotUserProtocol,
BotChannelProtocol,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
from .config import BotConfig
__all__ = [
"BotProtocol",
"BotMessageProtocol",
"BotUserProtocol",
"BotChannelProtocol",
"BotMessage",
"BotUser",
"BotChannel",
"BotConfig",
"MessageType",
]
@@ -0,0 +1,78 @@
"""
Bot Configuration for PraisonAI Agents.
Provides configuration dataclasses for bot settings.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class BotConfig:
"""Configuration for messaging bots.
Attributes:
token: Bot authentication token
webhook_url: URL for webhook mode (optional)
webhook_path: Path for webhook endpoint
polling_interval: Interval for polling mode (seconds)
allowed_users: List of allowed user IDs (empty = all allowed)
allowed_channels: List of allowed channel IDs (empty = all allowed)
command_prefix: Prefix for commands (default: "/")
mention_required: Whether bot mention is required in groups
typing_indicator: Whether to show typing indicator
max_message_length: Maximum message length before splitting
retry_attempts: Number of retry attempts for failed operations
timeout: Request timeout in seconds
metadata: Additional platform-specific configuration
"""
token: str = ""
webhook_url: Optional[str] = None
webhook_path: str = "/webhook"
polling_interval: float = 1.0
allowed_users: List[str] = field(default_factory=list)
allowed_channels: List[str] = field(default_factory=list)
command_prefix: str = "/"
mention_required: bool = True
typing_indicator: bool = True
max_message_length: int = 4096
retry_attempts: int = 3
timeout: int = 30
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary (hides sensitive data)."""
return {
"token": "***" if self.token else None,
"webhook_url": self.webhook_url,
"webhook_path": self.webhook_path,
"polling_interval": self.polling_interval,
"allowed_users": self.allowed_users,
"allowed_channels": self.allowed_channels,
"command_prefix": self.command_prefix,
"mention_required": self.mention_required,
"typing_indicator": self.typing_indicator,
"max_message_length": self.max_message_length,
"retry_attempts": self.retry_attempts,
"timeout": self.timeout,
"metadata": self.metadata,
}
@property
def is_webhook_mode(self) -> bool:
"""Whether bot is configured for webhook mode."""
return bool(self.webhook_url)
def is_user_allowed(self, user_id: str) -> bool:
"""Check if a user is allowed to interact with the bot."""
if not self.allowed_users:
return True
return user_id in self.allowed_users
def is_channel_allowed(self, channel_id: str) -> bool:
"""Check if a channel is allowed for bot interaction."""
if not self.allowed_channels:
return True
return channel_id in self.allowed_channels
@@ -0,0 +1,453 @@
"""
Bot Protocols for PraisonAI Agents.
Defines the interfaces for messaging bot implementations.
These protocols enable agents to communicate through messaging platforms
like Telegram, Discord, Slack, etc.
All implementations should live in the praisonai wrapper package.
"""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Protocol,
Union,
runtime_checkable,
)
if TYPE_CHECKING:
from ..agent import Agent
class MessageType(str, Enum):
"""Types of bot messages."""
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
VIDEO = "video"
FILE = "file"
LOCATION = "location"
STICKER = "sticker"
COMMAND = "command"
CALLBACK = "callback"
REACTION = "reaction"
REPLY = "reply"
EDIT = "edit"
DELETE = "delete"
@dataclass
class BotUser:
"""Represents a user in a messaging platform.
Attributes:
user_id: Platform-specific user identifier
username: User's username (if available)
display_name: User's display name
is_bot: Whether this user is a bot
metadata: Additional platform-specific metadata
"""
user_id: str
username: Optional[str] = None
display_name: Optional[str] = None
is_bot: bool = False
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"user_id": self.user_id,
"username": self.username,
"display_name": self.display_name,
"is_bot": self.is_bot,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotUser":
"""Create from dictionary."""
return cls(
user_id=data.get("user_id", "unknown"),
username=data.get("username"),
display_name=data.get("display_name"),
is_bot=data.get("is_bot", False),
metadata=data.get("metadata", {}),
)
@dataclass
class BotChannel:
"""Represents a channel/chat in a messaging platform.
Attributes:
channel_id: Platform-specific channel identifier
name: Channel name (if available)
channel_type: Type of channel (dm, group, channel, thread)
metadata: Additional platform-specific metadata
"""
channel_id: str
name: Optional[str] = None
channel_type: str = "dm" # dm, group, channel, thread
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"channel_id": self.channel_id,
"name": self.name,
"channel_type": self.channel_type,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotChannel":
"""Create from dictionary."""
return cls(
channel_id=data.get("channel_id", "unknown"),
name=data.get("name"),
channel_type=data.get("channel_type", "dm"),
metadata=data.get("metadata", {}),
)
@dataclass
class BotMessage:
"""Represents a message in a messaging platform.
Attributes:
message_id: Platform-specific message identifier
content: Message content (text or structured data)
message_type: Type of message
sender: User who sent the message
channel: Channel where the message was sent
timestamp: Message timestamp
reply_to: ID of message being replied to
thread_id: Thread identifier (for threaded conversations)
attachments: List of attachment URLs or data
metadata: Additional platform-specific metadata
"""
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
content: Union[str, Dict[str, Any]] = ""
message_type: MessageType = MessageType.TEXT
sender: Optional[BotUser] = None
channel: Optional[BotChannel] = None
timestamp: float = field(default_factory=time.time)
reply_to: Optional[str] = None
thread_id: Optional[str] = None
attachments: List[Dict[str, Any]] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"message_id": self.message_id,
"content": self.content,
"message_type": self.message_type.value if isinstance(self.message_type, MessageType) else self.message_type,
"sender": self.sender.to_dict() if self.sender else None,
"channel": self.channel.to_dict() if self.channel else None,
"timestamp": self.timestamp,
"reply_to": self.reply_to,
"thread_id": self.thread_id,
"attachments": self.attachments,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotMessage":
"""Create from dictionary."""
msg_type = data.get("message_type", "text")
try:
msg_type = MessageType(msg_type)
except ValueError:
msg_type = MessageType.TEXT
sender_data = data.get("sender")
channel_data = data.get("channel")
return cls(
message_id=data.get("message_id", str(uuid.uuid4())),
content=data.get("content", ""),
message_type=msg_type,
sender=BotUser.from_dict(sender_data) if sender_data else None,
channel=BotChannel.from_dict(channel_data) if channel_data else None,
timestamp=data.get("timestamp", time.time()),
reply_to=data.get("reply_to"),
thread_id=data.get("thread_id"),
attachments=data.get("attachments", []),
metadata=data.get("metadata", {}),
)
@property
def text(self) -> str:
"""Get message text content."""
if isinstance(self.content, str):
return self.content
return self.content.get("text", "")
@property
def is_command(self) -> bool:
"""Check if message is a command."""
return self.message_type == MessageType.COMMAND or (
isinstance(self.content, str) and self.content.startswith("/")
)
@property
def command(self) -> Optional[str]:
"""Extract command name if this is a command message."""
if not self.is_command:
return None
text = self.text
if text.startswith("/"):
return text.split()[0][1:] # Remove "/" and get first word
return None
@property
def command_args(self) -> List[str]:
"""Extract command arguments if this is a command message."""
if not self.is_command:
return []
text = self.text
parts = text.split()
return parts[1:] if len(parts) > 1 else []
@runtime_checkable
class BotMessageProtocol(Protocol):
"""Protocol for bot message handling."""
@property
def message_id(self) -> str:
"""Unique message identifier."""
...
@property
def content(self) -> Union[str, Dict[str, Any]]:
"""Message content."""
...
@property
def sender(self) -> Optional[BotUser]:
"""Message sender."""
...
@property
def channel(self) -> Optional[BotChannel]:
"""Message channel."""
...
@runtime_checkable
class BotUserProtocol(Protocol):
"""Protocol for bot user representation."""
@property
def user_id(self) -> str:
"""Unique user identifier."""
...
@property
def username(self) -> Optional[str]:
"""User's username."""
...
@property
def is_bot(self) -> bool:
"""Whether this user is a bot."""
...
@runtime_checkable
class BotChannelProtocol(Protocol):
"""Protocol for bot channel representation."""
@property
def channel_id(self) -> str:
"""Unique channel identifier."""
...
@property
def channel_type(self) -> str:
"""Type of channel (dm, group, channel, thread)."""
...
@runtime_checkable
class BotProtocol(Protocol):
"""Protocol for messaging bot implementations.
Bots connect agents to messaging platforms, handling:
- Message receiving and sending
- Command handling
- Webhook/polling management
- User and channel management
Example usage (implementation in praisonai wrapper):
from praisonai.bots import TelegramBot
bot = TelegramBot(token="...", agent=my_agent)
await bot.start()
"""
@property
def is_running(self) -> bool:
"""Whether the bot is currently running."""
...
@property
def platform(self) -> str:
"""Platform name (telegram, discord, slack, etc.)."""
...
@property
def bot_user(self) -> Optional[BotUser]:
"""The bot's user information."""
...
# Lifecycle methods
async def start(self) -> None:
"""Start the bot (begin receiving messages)."""
...
async def stop(self) -> None:
"""Stop the bot."""
...
# Agent management
def set_agent(self, agent: "Agent") -> None:
"""Set the agent that handles messages.
Args:
agent: The agent to handle incoming messages
"""
...
def get_agent(self) -> Optional["Agent"]:
"""Get the current agent."""
...
# Message handling
async def send_message(
self,
channel_id: str,
content: Union[str, Dict[str, Any]],
reply_to: Optional[str] = None,
thread_id: Optional[str] = None,
) -> BotMessage:
"""Send a message to a channel.
Args:
channel_id: Target channel ID
content: Message content
reply_to: Optional message ID to reply to
thread_id: Optional thread ID for threaded replies
Returns:
The sent message
"""
...
async def edit_message(
self,
channel_id: str,
message_id: str,
content: Union[str, Dict[str, Any]],
) -> BotMessage:
"""Edit an existing message.
Args:
channel_id: Channel containing the message
message_id: ID of message to edit
content: New message content
Returns:
The edited message
"""
...
async def delete_message(
self,
channel_id: str,
message_id: str,
) -> bool:
"""Delete a message.
Args:
channel_id: Channel containing the message
message_id: ID of message to delete
Returns:
True if deleted successfully
"""
...
# Event handlers
def on_message(self, handler: Callable[[BotMessage], Any]) -> Callable:
"""Register a message handler.
Args:
handler: Function to call when a message is received
Returns:
The handler function (for decorator use)
"""
...
def on_command(self, command: str) -> Callable:
"""Decorator to register a command handler.
Args:
command: Command name (without /)
Example:
@bot.on_command("help")
async def handle_help(message: BotMessage):
await bot.send_message(message.channel.channel_id, "Help text...")
"""
...
# Typing indicator
async def send_typing(self, channel_id: str) -> None:
"""Send typing indicator to a channel.
Args:
channel_id: Target channel ID
"""
...
# User/channel info
async def get_user(self, user_id: str) -> Optional[BotUser]:
"""Get user information.
Args:
user_id: User ID to look up
Returns:
User information or None if not found
"""
...
async def get_channel(self, channel_id: str) -> Optional[BotChannel]:
"""Get channel information.
Args:
channel_id: Channel ID to look up
Returns:
Channel information or None if not found
"""
...
@@ -0,0 +1,30 @@
"""
Gateway module for PraisonAI Agents.
Provides protocols and base classes for building gateway/control plane
implementations that coordinate multi-agent deployments.
This module contains only protocols and lightweight utilities.
Heavy implementations live in the praisonai wrapper package.
"""
from .protocols import (
GatewayProtocol,
GatewaySessionProtocol,
GatewayClientProtocol,
GatewayEvent,
GatewayMessage,
EventType,
)
from .config import GatewayConfig, SessionConfig
__all__ = [
"GatewayProtocol",
"GatewaySessionProtocol",
"GatewayClientProtocol",
"GatewayEvent",
"GatewayMessage",
"EventType",
"GatewayConfig",
"SessionConfig",
]
@@ -0,0 +1,100 @@
"""
Gateway Configuration for PraisonAI Agents.
Provides configuration dataclasses for gateway and session settings.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class SessionConfig:
"""Configuration for gateway sessions.
Attributes:
timeout: Session timeout in seconds (0 = no timeout)
max_messages: Maximum messages to keep in history (0 = unlimited)
persist: Whether to persist session state
persist_path: Path for session persistence
metadata: Additional session metadata
"""
timeout: int = 3600 # 1 hour default
max_messages: int = 1000
persist: bool = False
persist_path: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"timeout": self.timeout,
"max_messages": self.max_messages,
"persist": self.persist,
"persist_path": self.persist_path,
"metadata": self.metadata,
}
@dataclass
class GatewayConfig:
"""Configuration for the gateway server.
Attributes:
host: Host to bind to
port: Port to listen on
cors_origins: Allowed CORS origins
auth_token: Optional authentication token
max_connections: Maximum concurrent connections
max_sessions_per_agent: Maximum sessions per agent (0 = unlimited)
session_config: Default session configuration
heartbeat_interval: Heartbeat interval in seconds
reconnect_timeout: Time to wait for reconnection before closing session
ssl_cert: Path to SSL certificate (for HTTPS/WSS)
ssl_key: Path to SSL key
"""
host: str = "127.0.0.1"
port: int = 8765
cors_origins: List[str] = field(default_factory=lambda: ["*"])
auth_token: Optional[str] = None
max_connections: int = 1000
max_sessions_per_agent: int = 0 # 0 = unlimited
session_config: SessionConfig = field(default_factory=SessionConfig)
heartbeat_interval: int = 30
reconnect_timeout: int = 60
ssl_cert: Optional[str] = None
ssl_key: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary (hides sensitive data)."""
return {
"host": self.host,
"port": self.port,
"cors_origins": self.cors_origins,
"auth_token": "***" if self.auth_token else None,
"max_connections": self.max_connections,
"max_sessions_per_agent": self.max_sessions_per_agent,
"session_config": self.session_config.to_dict(),
"heartbeat_interval": self.heartbeat_interval,
"reconnect_timeout": self.reconnect_timeout,
"ssl_enabled": bool(self.ssl_cert and self.ssl_key),
}
@property
def is_secure(self) -> bool:
"""Whether SSL/TLS is enabled."""
return bool(self.ssl_cert and self.ssl_key)
@property
def ws_url(self) -> str:
"""WebSocket URL for this gateway."""
protocol = "wss" if self.is_secure else "ws"
return f"{protocol}://{self.host}:{self.port}"
@property
def http_url(self) -> str:
"""HTTP URL for this gateway."""
protocol = "https" if self.is_secure else "http"
return f"{protocol}://{self.host}:{self.port}"
@@ -0,0 +1,404 @@
"""
Gateway Protocols for PraisonAI Agents.
Defines the interfaces for gateway/control plane implementations.
These protocols enable multi-agent coordination, session management,
and real-time communication.
All implementations should live in the praisonai wrapper package.
"""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Protocol,
Union,
runtime_checkable,
)
if TYPE_CHECKING:
from ..agent import Agent
class EventType(str, Enum):
"""Standard gateway event types."""
# Connection events
CONNECT = "connect"
DISCONNECT = "disconnect"
RECONNECT = "reconnect"
# Session events
SESSION_START = "session_start"
SESSION_END = "session_end"
SESSION_UPDATE = "session_update"
# Agent events
AGENT_REGISTER = "agent_register"
AGENT_UNREGISTER = "agent_unregister"
AGENT_STATUS = "agent_status"
# Message events
MESSAGE = "message"
MESSAGE_ACK = "message_ack"
TYPING = "typing"
# System events
HEALTH = "health"
ERROR = "error"
BROADCAST = "broadcast"
@dataclass
class GatewayEvent:
"""A gateway event with metadata.
Attributes:
type: The event type
data: Event payload
event_id: Unique event identifier
timestamp: Event creation time
source: Source identifier (agent_id, client_id, etc.)
target: Target identifier (optional, for directed events)
"""
type: Union[EventType, str]
data: Dict[str, Any] = field(default_factory=dict)
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: float = field(default_factory=time.time)
source: Optional[str] = None
target: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"type": self.type.value if isinstance(self.type, EventType) else self.type,
"data": self.data,
"event_id": self.event_id,
"timestamp": self.timestamp,
"source": self.source,
"target": self.target,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "GatewayEvent":
"""Create from dictionary."""
event_type = data.get("type", "message")
try:
event_type = EventType(event_type)
except ValueError:
pass # Keep as string for custom event types
return cls(
type=event_type,
data=data.get("data", {}),
event_id=data.get("event_id", str(uuid.uuid4())),
timestamp=data.get("timestamp", time.time()),
source=data.get("source"),
target=data.get("target"),
)
@dataclass
class GatewayMessage:
"""A message sent through the gateway.
Attributes:
content: Message content (text or structured data)
sender_id: Sender identifier
session_id: Session this message belongs to
message_id: Unique message identifier
timestamp: Message creation time
metadata: Additional message metadata
reply_to: ID of message being replied to (optional)
"""
content: Union[str, Dict[str, Any]]
sender_id: str
session_id: str
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: float = field(default_factory=time.time)
metadata: Dict[str, Any] = field(default_factory=dict)
reply_to: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"content": self.content,
"sender_id": self.sender_id,
"session_id": self.session_id,
"message_id": self.message_id,
"timestamp": self.timestamp,
"metadata": self.metadata,
"reply_to": self.reply_to,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "GatewayMessage":
"""Create from dictionary."""
return cls(
content=data.get("content", ""),
sender_id=data.get("sender_id", "unknown"),
session_id=data.get("session_id", "default"),
message_id=data.get("message_id", str(uuid.uuid4())),
timestamp=data.get("timestamp", time.time()),
metadata=data.get("metadata", {}),
reply_to=data.get("reply_to"),
)
@runtime_checkable
class GatewaySessionProtocol(Protocol):
"""Protocol for gateway session management.
Sessions track conversations between clients and agents,
maintaining state and message history.
"""
@property
def session_id(self) -> str:
"""Unique session identifier."""
...
@property
def agent_id(self) -> Optional[str]:
"""ID of the agent handling this session."""
...
@property
def client_id(self) -> Optional[str]:
"""ID of the client in this session."""
...
@property
def is_active(self) -> bool:
"""Whether the session is currently active."""
...
@property
def created_at(self) -> float:
"""Session creation timestamp."""
...
@property
def last_activity(self) -> float:
"""Last activity timestamp."""
...
def get_state(self) -> Dict[str, Any]:
"""Get session state."""
...
def set_state(self, key: str, value: Any) -> None:
"""Set a session state value."""
...
def add_message(self, message: GatewayMessage) -> None:
"""Add a message to the session history."""
...
def get_messages(self, limit: Optional[int] = None) -> List[GatewayMessage]:
"""Get session message history."""
...
def close(self) -> None:
"""Close the session."""
...
@runtime_checkable
class GatewayClientProtocol(Protocol):
"""Protocol for gateway client connections.
Clients are external connections (WebSocket, HTTP, etc.)
that communicate with agents through the gateway.
"""
@property
def client_id(self) -> str:
"""Unique client identifier."""
...
@property
def is_connected(self) -> bool:
"""Whether the client is currently connected."""
...
@property
def connected_at(self) -> float:
"""Connection timestamp."""
...
async def send(self, event: GatewayEvent) -> None:
"""Send an event to the client."""
...
async def receive(self) -> GatewayEvent:
"""Receive an event from the client."""
...
async def close(self) -> None:
"""Close the client connection."""
...
@runtime_checkable
class GatewayProtocol(Protocol):
"""Protocol for gateway/control plane implementations.
The gateway coordinates communication between clients and agents,
manages sessions, and provides health/presence tracking.
Example usage (implementation in praisonai wrapper):
from praisonai.gateway import WebSocketGateway
gateway = WebSocketGateway(port=8765)
gateway.register_agent(my_agent)
await gateway.start()
"""
@property
def is_running(self) -> bool:
"""Whether the gateway is currently running."""
...
@property
def port(self) -> int:
"""Port the gateway is listening on."""
...
@property
def host(self) -> str:
"""Host the gateway is bound to."""
...
# Lifecycle methods
async def start(self) -> None:
"""Start the gateway server."""
...
async def stop(self) -> None:
"""Stop the gateway server."""
...
# Agent management
def register_agent(self, agent: "Agent", agent_id: Optional[str] = None) -> str:
"""Register an agent with the gateway.
Args:
agent: The agent to register
agent_id: Optional custom agent ID
Returns:
The agent ID (generated if not provided)
"""
...
def unregister_agent(self, agent_id: str) -> bool:
"""Unregister an agent from the gateway.
Args:
agent_id: The agent ID to unregister
Returns:
True if agent was unregistered, False if not found
"""
...
def get_agent(self, agent_id: str) -> Optional["Agent"]:
"""Get a registered agent by ID."""
...
def list_agents(self) -> List[str]:
"""List all registered agent IDs."""
...
# Session management
def create_session(
self,
agent_id: str,
client_id: Optional[str] = None,
session_id: Optional[str] = None,
) -> GatewaySessionProtocol:
"""Create a new session.
Args:
agent_id: The agent to handle this session
client_id: Optional client ID
session_id: Optional custom session ID
Returns:
The created session
"""
...
def get_session(self, session_id: str) -> Optional[GatewaySessionProtocol]:
"""Get a session by ID."""
...
def close_session(self, session_id: str) -> bool:
"""Close a session.
Args:
session_id: The session ID to close
Returns:
True if session was closed, False if not found
"""
...
def list_sessions(self, agent_id: Optional[str] = None) -> List[str]:
"""List session IDs, optionally filtered by agent."""
...
# Event handling
def on_event(self, event_type: Union[EventType, str]) -> Callable:
"""Decorator to register an event handler.
Example:
@gateway.on_event(EventType.MESSAGE)
async def handle_message(event: GatewayEvent):
print(f"Message: {event.data}")
"""
...
async def emit(self, event: GatewayEvent) -> None:
"""Emit an event to registered handlers."""
...
async def broadcast(
self,
event: GatewayEvent,
exclude: Optional[List[str]] = None,
) -> None:
"""Broadcast an event to all connected clients.
Args:
event: The event to broadcast
exclude: Optional list of client IDs to exclude
"""
...
# Health and status
def health(self) -> Dict[str, Any]:
"""Get gateway health status.
Returns:
Health information including:
- status: "healthy" or "unhealthy"
- uptime: Seconds since start
- agents: Number of registered agents
- sessions: Number of active sessions
- clients: Number of connected clients
"""
...
@@ -0,0 +1,409 @@
"""
Model Failover for PraisonAI Agents.
Provides auth profile management and automatic failover between
LLM providers when rate limits or errors occur.
This module is lightweight and protocol-driven.
Heavy implementations live in the praisonai wrapper.
"""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Protocol,
runtime_checkable,
)
logger = logging.getLogger(__name__)
class ProviderStatus(str, Enum):
"""Status of an LLM provider."""
AVAILABLE = "available"
RATE_LIMITED = "rate_limited"
ERROR = "error"
DISABLED = "disabled"
@dataclass
class AuthProfile:
"""Authentication profile for an LLM provider.
Attributes:
name: Profile name for identification
provider: Provider name (openai, anthropic, google, etc.)
api_key: API key for authentication
base_url: Optional base URL override
model: Default model for this profile
priority: Priority for failover (lower = higher priority)
rate_limit_rpm: Requests per minute limit
rate_limit_tpm: Tokens per minute limit
status: Current status
last_error: Last error message
last_error_time: Timestamp of last error
cooldown_until: Timestamp until which this profile is in cooldown
metadata: Additional provider-specific configuration
"""
name: str
provider: str
api_key: str
base_url: Optional[str] = None
model: Optional[str] = None
priority: int = 0
rate_limit_rpm: Optional[int] = None
rate_limit_tpm: Optional[int] = None
status: ProviderStatus = ProviderStatus.AVAILABLE
last_error: Optional[str] = None
last_error_time: Optional[float] = None
cooldown_until: Optional[float] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary (hides sensitive data)."""
return {
"name": self.name,
"provider": self.provider,
"api_key": "***" + self.api_key[-4:] if self.api_key else None,
"base_url": self.base_url,
"model": self.model,
"priority": self.priority,
"rate_limit_rpm": self.rate_limit_rpm,
"rate_limit_tpm": self.rate_limit_tpm,
"status": self.status.value,
"last_error": self.last_error,
"last_error_time": self.last_error_time,
"cooldown_until": self.cooldown_until,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "AuthProfile":
"""Create from dictionary."""
status = data.get("status", "available")
try:
status = ProviderStatus(status)
except ValueError:
status = ProviderStatus.AVAILABLE
return cls(
name=data.get("name", "default"),
provider=data.get("provider", "openai"),
api_key=data.get("api_key", ""),
base_url=data.get("base_url"),
model=data.get("model"),
priority=data.get("priority", 0),
rate_limit_rpm=data.get("rate_limit_rpm"),
rate_limit_tpm=data.get("rate_limit_tpm"),
status=status,
last_error=data.get("last_error"),
last_error_time=data.get("last_error_time"),
cooldown_until=data.get("cooldown_until"),
metadata=data.get("metadata", {}),
)
@property
def is_available(self) -> bool:
"""Check if this profile is currently available."""
if self.status == ProviderStatus.DISABLED:
return False
if self.cooldown_until and time.time() < self.cooldown_until:
return False
return True
def mark_rate_limited(self, cooldown_seconds: float = 60.0) -> None:
"""Mark this profile as rate limited."""
self.status = ProviderStatus.RATE_LIMITED
self.cooldown_until = time.time() + cooldown_seconds
self.last_error = "Rate limited"
self.last_error_time = time.time()
def mark_error(self, error: str, cooldown_seconds: float = 30.0) -> None:
"""Mark this profile as having an error."""
self.status = ProviderStatus.ERROR
self.cooldown_until = time.time() + cooldown_seconds
self.last_error = error
self.last_error_time = time.time()
def reset(self) -> None:
"""Reset this profile to available status."""
self.status = ProviderStatus.AVAILABLE
self.cooldown_until = None
self.last_error = None
self.last_error_time = None
@runtime_checkable
class FailoverProtocol(Protocol):
"""Protocol for failover management."""
def get_next_profile(self) -> Optional[AuthProfile]:
"""Get the next available profile."""
...
def mark_failure(self, profile: AuthProfile, error: str) -> None:
"""Mark a profile as failed."""
...
def mark_success(self, profile: AuthProfile) -> None:
"""Mark a profile as successful."""
...
@dataclass
class FailoverConfig:
"""Configuration for failover behavior.
Attributes:
max_retries: Maximum retry attempts per request
retry_delay: Base delay between retries (seconds)
exponential_backoff: Whether to use exponential backoff
max_retry_delay: Maximum retry delay (seconds)
cooldown_on_rate_limit: Cooldown duration for rate limits (seconds)
cooldown_on_error: Cooldown duration for errors (seconds)
rotate_on_success: Whether to rotate profiles on success
"""
max_retries: int = 3
retry_delay: float = 1.0
exponential_backoff: bool = True
max_retry_delay: float = 60.0
cooldown_on_rate_limit: float = 60.0
cooldown_on_error: float = 30.0
rotate_on_success: bool = False
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"max_retries": self.max_retries,
"retry_delay": self.retry_delay,
"exponential_backoff": self.exponential_backoff,
"max_retry_delay": self.max_retry_delay,
"cooldown_on_rate_limit": self.cooldown_on_rate_limit,
"cooldown_on_error": self.cooldown_on_error,
"rotate_on_success": self.rotate_on_success,
}
class FailoverManager:
"""Manages failover between multiple LLM auth profiles.
Provides automatic failover when rate limits or errors occur,
with configurable retry behavior and cooldown periods.
Example:
manager = FailoverManager()
manager.add_profile(AuthProfile(
name="openai-primary",
provider="openai",
api_key="sk-...",
priority=0,
))
manager.add_profile(AuthProfile(
name="openai-backup",
provider="openai",
api_key="sk-...",
priority=1,
))
# Get next available profile
profile = manager.get_next_profile()
# On failure
manager.mark_failure(profile, "Rate limit exceeded")
# Get next profile (will return backup)
profile = manager.get_next_profile()
"""
def __init__(self, config: Optional[FailoverConfig] = None):
"""Initialize the failover manager.
Args:
config: Failover configuration
"""
self.config = config or FailoverConfig()
self._profiles: List[AuthProfile] = []
self._current_index: int = 0
self._on_failover_callbacks: List[Callable[[AuthProfile, AuthProfile], None]] = []
def add_profile(self, profile: AuthProfile) -> None:
"""Add an auth profile.
Args:
profile: The profile to add
"""
self._profiles.append(profile)
self._profiles.sort(key=lambda p: p.priority)
def remove_profile(self, name: str) -> bool:
"""Remove a profile by name.
Args:
name: Profile name to remove
Returns:
True if removed, False if not found
"""
for i, profile in enumerate(self._profiles):
if profile.name == name:
self._profiles.pop(i)
return True
return False
def get_profile(self, name: str) -> Optional[AuthProfile]:
"""Get a profile by name.
Args:
name: Profile name
Returns:
The profile or None if not found
"""
for profile in self._profiles:
if profile.name == name:
return profile
return None
def list_profiles(self) -> List[AuthProfile]:
"""List all profiles.
Returns:
List of all profiles
"""
return list(self._profiles)
def get_next_profile(self) -> Optional[AuthProfile]:
"""Get the next available profile.
Returns profiles in priority order, skipping those that are
rate limited or in cooldown.
Returns:
The next available profile, or None if all are unavailable
"""
if not self._profiles:
return None
# First, check if any cooldowns have expired
current_time = time.time()
for profile in self._profiles:
if profile.cooldown_until and current_time >= profile.cooldown_until:
profile.reset()
# Find first available profile
for profile in self._profiles:
if profile.is_available:
return profile
# If none available, return the one with shortest remaining cooldown
available_profiles = [p for p in self._profiles if p.status != ProviderStatus.DISABLED]
if available_profiles:
return min(
available_profiles,
key=lambda p: p.cooldown_until or 0
)
return None
def mark_failure(
self,
profile: AuthProfile,
error: str,
is_rate_limit: bool = False,
) -> None:
"""Mark a profile as failed.
Args:
profile: The profile that failed
error: Error message
is_rate_limit: Whether this is a rate limit error
"""
if is_rate_limit:
profile.mark_rate_limited(self.config.cooldown_on_rate_limit)
logger.warning(
f"Profile '{profile.name}' rate limited, "
f"cooldown for {self.config.cooldown_on_rate_limit}s"
)
else:
profile.mark_error(error, self.config.cooldown_on_error)
logger.warning(
f"Profile '{profile.name}' error: {error}, "
f"cooldown for {self.config.cooldown_on_error}s"
)
# Notify callbacks
next_profile = self.get_next_profile()
if next_profile and next_profile != profile:
for callback in self._on_failover_callbacks:
try:
callback(profile, next_profile)
except Exception as e:
logger.error(f"Failover callback error: {e}")
def mark_success(self, profile: AuthProfile) -> None:
"""Mark a profile as successful.
Args:
profile: The profile that succeeded
"""
if profile.status != ProviderStatus.AVAILABLE:
profile.reset()
logger.info(f"Profile '{profile.name}' recovered")
def on_failover(
self,
callback: Callable[[AuthProfile, AuthProfile], None],
) -> None:
"""Register a callback for failover events.
Args:
callback: Function called with (failed_profile, new_profile)
"""
self._on_failover_callbacks.append(callback)
def get_retry_delay(self, attempt: int) -> float:
"""Calculate retry delay for an attempt.
Args:
attempt: Attempt number (0-indexed)
Returns:
Delay in seconds
"""
if self.config.exponential_backoff:
delay = self.config.retry_delay * (2 ** attempt)
else:
delay = self.config.retry_delay
return min(delay, self.config.max_retry_delay)
def status(self) -> Dict[str, Any]:
"""Get failover manager status.
Returns:
Status information
"""
available = sum(1 for p in self._profiles if p.is_available)
return {
"total_profiles": len(self._profiles),
"available_profiles": available,
"profiles": [p.to_dict() for p in self._profiles],
"config": self.config.to_dict(),
}
def reset_all(self) -> None:
"""Reset all profiles to available status."""
for profile in self._profiles:
profile.reset()
@@ -0,0 +1,26 @@
"""
Sandbox Protocols for PraisonAI Agents.
Provides protocols and base classes for building sandbox implementations
that enable safe code execution in isolated environments.
This module contains only protocols and lightweight utilities.
Heavy implementations (Docker, etc.) live in the praisonai wrapper package.
"""
from .protocols import (
SandboxProtocol,
SandboxResult,
SandboxStatus,
ResourceLimits,
)
from .config import SandboxConfig, SecurityPolicy
__all__ = [
"SandboxProtocol",
"SandboxResult",
"SandboxStatus",
"ResourceLimits",
"SandboxConfig",
"SecurityPolicy",
]
@@ -0,0 +1,155 @@
"""
Sandbox Configuration for PraisonAI Agents.
Provides configuration dataclasses for sandbox settings.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List
from .protocols import ResourceLimits
@dataclass
class SecurityPolicy:
"""Security policy for sandbox execution.
Attributes:
allow_network: Whether to allow network access
allow_file_write: Whether to allow file writes
allow_subprocess: Whether to allow subprocess creation
allowed_paths: List of paths that can be accessed
blocked_paths: List of paths that are blocked
allowed_commands: List of allowed shell commands (empty = all)
blocked_commands: List of blocked shell commands
allowed_imports: List of allowed Python imports (empty = all)
blocked_imports: List of blocked Python imports
max_output_size: Maximum output size in bytes
"""
allow_network: bool = False
allow_file_write: bool = True
allow_subprocess: bool = False
allowed_paths: List[str] = field(default_factory=list)
blocked_paths: List[str] = field(default_factory=lambda: [
"/etc/passwd", "/etc/shadow", "~/.ssh", "~/.aws", "~/.config"
])
allowed_commands: List[str] = field(default_factory=list)
blocked_commands: List[str] = field(default_factory=lambda: [
"rm -rf", "dd", "mkfs", "fdisk", "shutdown", "reboot"
])
allowed_imports: List[str] = field(default_factory=list)
blocked_imports: List[str] = field(default_factory=lambda: [
"subprocess", "os.system", "eval", "exec", "compile"
])
max_output_size: int = 1024 * 1024 # 1MB
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"allow_network": self.allow_network,
"allow_file_write": self.allow_file_write,
"allow_subprocess": self.allow_subprocess,
"allowed_paths": self.allowed_paths,
"blocked_paths": self.blocked_paths,
"allowed_commands": self.allowed_commands,
"blocked_commands": self.blocked_commands,
"allowed_imports": self.allowed_imports,
"blocked_imports": self.blocked_imports,
"max_output_size": self.max_output_size,
}
@classmethod
def strict(cls) -> "SecurityPolicy":
"""Create a strict security policy for untrusted code."""
return cls(
allow_network=False,
allow_file_write=False,
allow_subprocess=False,
max_output_size=100 * 1024, # 100KB
)
@classmethod
def standard(cls) -> "SecurityPolicy":
"""Create a standard security policy."""
return cls()
@classmethod
def permissive(cls) -> "SecurityPolicy":
"""Create a permissive security policy for trusted code."""
return cls(
allow_network=True,
allow_file_write=True,
allow_subprocess=True,
blocked_paths=[],
blocked_commands=[],
blocked_imports=[],
max_output_size=10 * 1024 * 1024, # 10MB
)
@dataclass
class SandboxConfig:
"""Configuration for sandbox execution.
Attributes:
sandbox_type: Type of sandbox (docker, subprocess, e2b)
image: Docker image to use (for docker sandbox)
working_dir: Working directory within sandbox
env: Environment variables
resource_limits: Resource limits
security_policy: Security policy
auto_cleanup: Whether to auto-cleanup after execution
persist_files: Whether to persist files between executions
mount_paths: Paths to mount into sandbox (host:container)
metadata: Additional configuration
"""
sandbox_type: str = "subprocess"
image: str = "python:3.11-slim"
working_dir: str = "/workspace"
env: Dict[str, str] = field(default_factory=dict)
resource_limits: ResourceLimits = field(default_factory=ResourceLimits)
security_policy: SecurityPolicy = field(default_factory=SecurityPolicy)
auto_cleanup: bool = True
persist_files: bool = False
mount_paths: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"sandbox_type": self.sandbox_type,
"image": self.image,
"working_dir": self.working_dir,
"env": {k: "***" if "key" in k.lower() or "secret" in k.lower() else v
for k, v in self.env.items()},
"resource_limits": self.resource_limits.to_dict(),
"security_policy": self.security_policy.to_dict(),
"auto_cleanup": self.auto_cleanup,
"persist_files": self.persist_files,
"mount_paths": self.mount_paths,
"metadata": self.metadata,
}
@classmethod
def docker(cls, image: str = "python:3.11-slim") -> "SandboxConfig":
"""Create a Docker sandbox configuration."""
return cls(
sandbox_type="docker",
image=image,
)
@classmethod
def subprocess(cls) -> "SandboxConfig":
"""Create a subprocess sandbox configuration."""
return cls(
sandbox_type="subprocess",
)
@classmethod
def e2b(cls) -> "SandboxConfig":
"""Create an E2B sandbox configuration."""
return cls(
sandbox_type="e2b",
)
@@ -0,0 +1,359 @@
"""
Sandbox Protocols for PraisonAI Agents.
Defines the interfaces for sandbox implementations that enable
safe code execution in isolated environments.
All implementations should live in the praisonai wrapper package.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Any,
Dict,
List,
Optional,
Protocol,
Union,
runtime_checkable,
)
class SandboxStatus(str, Enum):
"""Status of a sandbox execution."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
TIMEOUT = "timeout"
KILLED = "killed"
@dataclass
class ResourceLimits:
"""Resource limits for sandbox execution.
Attributes:
memory_mb: Maximum memory in megabytes (0 = unlimited)
cpu_percent: Maximum CPU percentage (0 = unlimited)
timeout_seconds: Maximum execution time in seconds
max_processes: Maximum number of processes
max_open_files: Maximum number of open files
network_enabled: Whether network access is allowed
disk_write_mb: Maximum disk write in megabytes (0 = unlimited)
"""
memory_mb: int = 512
cpu_percent: int = 100
timeout_seconds: int = 60
max_processes: int = 10
max_open_files: int = 100
network_enabled: bool = False
disk_write_mb: int = 100
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"memory_mb": self.memory_mb,
"cpu_percent": self.cpu_percent,
"timeout_seconds": self.timeout_seconds,
"max_processes": self.max_processes,
"max_open_files": self.max_open_files,
"network_enabled": self.network_enabled,
"disk_write_mb": self.disk_write_mb,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ResourceLimits":
"""Create from dictionary."""
return cls(
memory_mb=data.get("memory_mb", 512),
cpu_percent=data.get("cpu_percent", 100),
timeout_seconds=data.get("timeout_seconds", 60),
max_processes=data.get("max_processes", 10),
max_open_files=data.get("max_open_files", 100),
network_enabled=data.get("network_enabled", False),
disk_write_mb=data.get("disk_write_mb", 100),
)
@classmethod
def minimal(cls) -> "ResourceLimits":
"""Create minimal resource limits for untrusted code."""
return cls(
memory_mb=128,
cpu_percent=50,
timeout_seconds=30,
max_processes=5,
max_open_files=50,
network_enabled=False,
disk_write_mb=10,
)
@classmethod
def standard(cls) -> "ResourceLimits":
"""Create standard resource limits."""
return cls()
@classmethod
def generous(cls) -> "ResourceLimits":
"""Create generous resource limits for trusted code."""
return cls(
memory_mb=2048,
cpu_percent=100,
timeout_seconds=300,
max_processes=50,
max_open_files=500,
network_enabled=True,
disk_write_mb=1000,
)
@dataclass
class SandboxResult:
"""Result of a sandbox execution.
Attributes:
execution_id: Unique execution identifier
status: Execution status
exit_code: Process exit code (None if not completed)
stdout: Standard output
stderr: Standard error
duration_seconds: Execution duration
started_at: Start timestamp
completed_at: Completion timestamp
error: Error message if failed
metadata: Additional execution metadata
"""
execution_id: str = field(default_factory=lambda: str(uuid.uuid4()))
status: SandboxStatus = SandboxStatus.PENDING
exit_code: Optional[int] = None
stdout: str = ""
stderr: str = ""
duration_seconds: float = 0.0
started_at: Optional[float] = None
completed_at: Optional[float] = None
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"execution_id": self.execution_id,
"status": self.status.value if isinstance(self.status, SandboxStatus) else self.status,
"exit_code": self.exit_code,
"stdout": self.stdout,
"stderr": self.stderr,
"duration_seconds": self.duration_seconds,
"started_at": self.started_at,
"completed_at": self.completed_at,
"error": self.error,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SandboxResult":
"""Create from dictionary."""
status = data.get("status", "pending")
try:
status = SandboxStatus(status)
except ValueError:
status = SandboxStatus.PENDING
return cls(
execution_id=data.get("execution_id", str(uuid.uuid4())),
status=status,
exit_code=data.get("exit_code"),
stdout=data.get("stdout", ""),
stderr=data.get("stderr", ""),
duration_seconds=data.get("duration_seconds", 0.0),
started_at=data.get("started_at"),
completed_at=data.get("completed_at"),
error=data.get("error"),
metadata=data.get("metadata", {}),
)
@property
def success(self) -> bool:
"""Check if execution was successful."""
return self.status == SandboxStatus.COMPLETED and self.exit_code == 0
@property
def output(self) -> str:
"""Get combined output (stdout + stderr)."""
parts = []
if self.stdout:
parts.append(self.stdout)
if self.stderr:
parts.append(f"[stderr]\n{self.stderr}")
return "\n".join(parts)
@runtime_checkable
class SandboxProtocol(Protocol):
"""Protocol for sandbox implementations.
Sandboxes provide isolated environments for safe code execution.
Implementations can use Docker, subprocess isolation, or other
containerization technologies.
Example usage (implementation in praisonai wrapper):
from praisonai.sandbox import DockerSandbox
sandbox = DockerSandbox(image="python:3.11-slim")
result = await sandbox.execute("print('Hello, World!')")
print(result.stdout)
"""
@property
def is_available(self) -> bool:
"""Whether the sandbox backend is available."""
...
@property
def sandbox_type(self) -> str:
"""Type of sandbox (docker, subprocess, etc.)."""
...
# Lifecycle methods
async def start(self) -> None:
"""Start/initialize the sandbox environment."""
...
async def stop(self) -> None:
"""Stop/cleanup the sandbox environment."""
...
# Execution methods
async def execute(
self,
code: str,
language: str = "python",
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Execute code in the sandbox.
Args:
code: Code to execute
language: Programming language (python, bash, etc.)
limits: Resource limits for execution
env: Environment variables
working_dir: Working directory for execution
Returns:
Execution result
"""
...
async def execute_file(
self,
file_path: str,
args: Optional[List[str]] = None,
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
) -> SandboxResult:
"""Execute a file in the sandbox.
Args:
file_path: Path to file to execute
args: Command line arguments
limits: Resource limits for execution
env: Environment variables
Returns:
Execution result
"""
...
async def run_command(
self,
command: Union[str, List[str]],
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Run a shell command in the sandbox.
Args:
command: Command to run (string or list of args)
limits: Resource limits for execution
env: Environment variables
working_dir: Working directory
Returns:
Execution result
"""
...
# File operations
async def write_file(
self,
path: str,
content: Union[str, bytes],
) -> bool:
"""Write a file to the sandbox.
Args:
path: Path within sandbox
content: File content
Returns:
True if successful
"""
...
async def read_file(
self,
path: str,
) -> Optional[Union[str, bytes]]:
"""Read a file from the sandbox.
Args:
path: Path within sandbox
Returns:
File content or None if not found
"""
...
async def list_files(
self,
path: str = "/",
) -> List[str]:
"""List files in a sandbox directory.
Args:
path: Directory path within sandbox
Returns:
List of file paths
"""
...
# Status and cleanup
def get_status(self) -> Dict[str, Any]:
"""Get sandbox status information.
Returns:
Status information including:
- available: Whether sandbox is available
- type: Sandbox type
- running: Whether sandbox is running
- resource_usage: Current resource usage
"""
...
async def cleanup(self) -> None:
"""Clean up sandbox resources (files, processes, etc.)."""
...
async def reset(self) -> None:
"""Reset sandbox to initial state."""
...
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.13.23"
version = "0.14.0"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -0,0 +1,297 @@
"""
Real API integration tests for autonomous loop features.
These tests make actual API calls to verify the implementation works end-to-end.
Run with: pytest tests/integration/test_autonomy_loop_real.py -v -s
"""
import time
from praisonaiagents import Agent
class TestAutonomousLoopReal:
"""Real API tests for autonomous loop features."""
def test_1_basic_completion_promise(self):
"""Test 1: Basic autonomous loop with completion promise detection."""
print("\n" + "="*60)
print("TEST 1: Basic Completion Promise Detection")
print("="*60)
agent = Agent(
name="promise_tester",
instructions="""You are a simple task agent.
When asked to do something, do it and then output <promise>DONE</promise> to signal completion.
Keep your response brief.""",
autonomy=True,
llm="gpt-4o-mini"
)
result = agent.run_autonomous(
prompt="Say hello and confirm you're ready. Then output <promise>DONE</promise>",
max_iterations=3,
completion_promise="DONE"
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Output preview: {result.output[:200] if result.output else 'None'}...")
assert result.success, f"Expected success, got failure: {result.completion_reason}"
assert result.completion_reason == "promise", f"Expected 'promise', got '{result.completion_reason}'"
assert result.iterations <= 3, f"Expected <= 3 iterations, got {result.iterations}"
assert "<promise>DONE</promise>" in result.output, "Promise tag not found in output"
print("✅ TEST 1 PASSED")
def test_2_max_iterations_limit(self):
"""Test 2: Verify max iterations limit is respected."""
print("\n" + "="*60)
print("TEST 2: Max Iterations Limit")
print("="*60)
agent = Agent(
name="iteration_tester",
instructions="""You are a philosophical agent.
When asked a question, respond with a thoughtful but incomplete answer.
Always end with 'Let me think more about this...'
NEVER use words like: done, finished, completed, task, complete.""",
autonomy=True,
llm="gpt-4o-mini",
)
result = agent.run_autonomous(
prompt="What is the meaning of existence? Keep pondering, never conclude.",
max_iterations=2,
completion_promise="NEVER_MATCH_THIS_UNIQUE_STRING_12345"
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Output preview: {result.output[:100] if result.output else 'None'}...")
# Either max_iterations or goal (if LLM accidentally says completion word)
# The key test is that it respects the iteration limit
assert result.iterations <= 2, f"Expected <= 2 iterations, got {result.iterations}"
print("✅ TEST 2 PASSED")
def test_3_keyword_completion_detection(self):
"""Test 3: Verify keyword-based completion detection still works."""
print("\n" + "="*60)
print("TEST 3: Keyword Completion Detection")
print("="*60)
agent = Agent(
name="keyword_tester",
instructions="""You are a simple assistant.
When asked to complete a task, do it and say 'Task completed' at the end.
Keep your response brief.""",
autonomy=True,
llm="gpt-4o-mini",
)
result = agent.run_autonomous(
prompt="Count from 1 to 3 and then say 'Task completed'",
max_iterations=3
# No completion_promise - should use keyword detection
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Output preview: {result.output[:200] if result.output else 'None'}...")
assert result.success, f"Expected success, got failure: {result.completion_reason}"
assert result.completion_reason == "goal", f"Expected 'goal', got '{result.completion_reason}'"
print("✅ TEST 3 PASSED")
def test_4_context_clearing(self):
"""Test 4: Verify context clearing works between iterations."""
print("\n" + "="*60)
print("TEST 4: Context Clearing Between Iterations")
print("="*60)
agent = Agent(
name="context_tester",
instructions="""You are a counting agent.
Each time you're asked, respond with a number starting from 1.
If you remember previous numbers, continue the sequence.
If this is your first response, start with 1.
After responding with 3 or higher, output <promise>COUNTED</promise>""",
autonomy=True,
llm="gpt-4o-mini",
)
# With clear_context=True, agent should NOT remember previous iterations
result = agent.run_autonomous(
prompt="What number are you on? Respond with just the number and continue counting. After 3, output <promise>COUNTED</promise>",
max_iterations=5,
completion_promise="COUNTED",
clear_context=True
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Output preview: {result.output[:300] if result.output else 'None'}...")
# The test verifies clear_context runs without error
# Actual behavior depends on LLM interpretation
assert result.iterations <= 5, f"Expected <= 5 iterations, got {result.iterations}"
print("✅ TEST 4 PASSED")
def test_5_prompt_reinjection(self):
"""Test 5: Verify original prompt is used every iteration (not 'Continue')."""
print("\n" + "="*60)
print("TEST 5: Prompt Re-injection")
print("="*60)
agent = Agent(
name="reinjection_tester",
instructions="""You are a task tracker.
When you receive a task, acknowledge it by repeating the task name.
On iteration 1: say "Starting: [task]"
On iteration 2: say "Continuing: [task]"
On iteration 3: say "Completing: [task]" and add <promise>FINISHED</promise>
Track which iteration you're on internally.""",
autonomy=True,
llm="gpt-4o-mini",
)
task_name = "BUILD_API_ENDPOINT"
result = agent.run_autonomous(
prompt=f"Your task is: {task_name}. Follow your instructions for each iteration.",
max_iterations=5,
completion_promise="FINISHED",
clear_context=True # Forces fresh context each time
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Output preview: {result.output[:300] if result.output else 'None'}...")
# Verify the task name appears in output (proves prompt was injected)
assert result.output is not None, "Output should not be None"
print("✅ TEST 5 PASSED")
def test_6_autonomy_config_from_dict(self):
"""Test 6: Verify autonomy config can be passed as dict."""
print("\n" + "="*60)
print("TEST 6: Autonomy Config from Dict")
print("="*60)
agent = Agent(
name="config_tester",
instructions="""You are a simple agent.
Respond with 'Hello!' and then <promise>READY</promise>""",
autonomy={
"max_iterations": 5,
"completion_promise": "READY",
"clear_context": False
},
llm="gpt-4o-mini",
)
result = agent.run_autonomous(
prompt="Say hello and signal you're ready",
# Not passing params - should use config values
)
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
assert result.success, f"Expected success, got failure: {result.completion_reason}"
assert result.completion_reason == "promise", f"Expected 'promise', got '{result.completion_reason}'"
print("✅ TEST 6 PASSED")
def test_7_timeout_handling(self):
"""Test 7: Verify timeout is respected."""
print("\n" + "="*60)
print("TEST 7: Timeout Handling")
print("="*60)
agent = Agent(
name="timeout_tester",
instructions="""You are a slow agent.
Take your time responding. Write a very long response about anything.
Never say you're done.""",
autonomy=True,
llm="gpt-4o-mini",
)
start_time = time.time()
result = agent.run_autonomous(
prompt="Write an extremely long essay about philosophy",
max_iterations=100, # High limit
timeout_seconds=5, # Short timeout
completion_promise="NEVER_MATCH"
)
elapsed = time.time() - start_time
print(f"Success: {result.success}")
print(f"Completion reason: {result.completion_reason}")
print(f"Iterations: {result.iterations}")
print(f"Elapsed time: {elapsed:.2f}s")
# Either timeout or max_iterations (if API is fast)
assert result.completion_reason in ["timeout", "max_iterations"], \
f"Expected 'timeout' or 'max_iterations', got '{result.completion_reason}'"
print("✅ TEST 7 PASSED")
def run_all_tests():
"""Run all tests and report results."""
print("\n" + "="*70)
print("RUNNING REAL API INTEGRATION TESTS FOR AUTONOMOUS LOOP")
print("="*70)
test_suite = TestAutonomousLoopReal()
tests = [
("Test 1: Basic Completion Promise", test_suite.test_1_basic_completion_promise),
("Test 2: Max Iterations Limit", test_suite.test_2_max_iterations_limit),
("Test 3: Keyword Completion Detection", test_suite.test_3_keyword_completion_detection),
("Test 4: Context Clearing", test_suite.test_4_context_clearing),
("Test 5: Prompt Re-injection", test_suite.test_5_prompt_reinjection),
("Test 6: Autonomy Config from Dict", test_suite.test_6_autonomy_config_from_dict),
("Test 7: Timeout Handling", test_suite.test_7_timeout_handling),
]
results = []
for name, test_func in tests:
try:
test_func()
results.append((name, "PASSED", None))
except Exception as e:
results.append((name, "FAILED", str(e)))
print(f"❌ {name} FAILED: {e}")
print("\n" + "="*70)
print("TEST RESULTS SUMMARY")
print("="*70)
passed = sum(1 for _, status, _ in results if status == "PASSED")
failed = sum(1 for _, status, _ in results if status == "FAILED")
for name, status, error in results:
emoji = "✅" if status == "PASSED" else "❌"
print(f"{emoji} {name}: {status}")
if error:
print(f" Error: {error}")
print(f"\nTotal: {passed}/{len(results)} passed, {failed} failed")
return passed, failed
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,402 @@
"""
Integration tests for Bots module.
These tests verify the Bot protocols and implementations work correctly
with real scenarios.
"""
import pytest
import os
from unittest.mock import MagicMock, patch
# Import from core SDK
from praisonaiagents.bots import (
BotConfig,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
from praisonaiagents.bots.protocols import (
BotProtocol,
BotMessageProtocol,
BotUserProtocol,
BotChannelProtocol,
)
class TestBotConfigIntegration:
"""Integration tests for BotConfig."""
def test_telegram_config(self):
"""Test Telegram bot configuration."""
config = BotConfig(
token='test-token-123',
webhook_url='https://example.com/webhook',
metadata={'platform': 'telegram'},
)
assert config.token == 'test-token-123'
assert config.metadata['platform'] == 'telegram'
# Token should be hidden in dict
config_dict = config.to_dict()
assert 'test-token-123' not in str(config_dict)
def test_discord_config(self):
"""Test Discord bot configuration."""
config = BotConfig(
token='discord-token-456',
metadata={'platform': 'discord', 'guild_ids': ['123456789', '987654321']},
)
assert config.token == 'discord-token-456'
assert len(config.metadata['guild_ids']) == 2
def test_slack_config(self):
"""Test Slack bot configuration."""
config = BotConfig(
token='xoxb-slack-token',
metadata={'platform': 'slack', 'app_token': 'xapp-slack-app-token'},
)
assert config.token == 'xoxb-slack-token'
def test_config_from_environment(self):
"""Test config can be created from environment variables."""
with patch.dict(os.environ, {
'TELEGRAM_BOT_TOKEN': 'env-token-789',
}):
config = BotConfig(
token=os.environ.get('TELEGRAM_BOT_TOKEN'),
)
assert config.token == 'env-token-789'
class TestBotUserIntegration:
"""Integration tests for BotUser."""
def test_user_creation(self):
"""Test user creation with all fields."""
user = BotUser(
user_id='user-123',
username='johndoe',
display_name='John Doe',
is_bot=False,
metadata={'language': 'en', 'timezone': 'UTC'},
)
assert user.user_id == 'user-123'
assert user.username == 'johndoe'
assert user.display_name == 'John Doe'
assert user.is_bot is False
assert user.metadata['language'] == 'en'
def test_user_serialization_roundtrip(self):
"""Test user can be serialized and deserialized."""
original = BotUser(
user_id='user-456',
username='janedoe',
display_name='Jane Doe',
is_bot=False,
)
user_dict = original.to_dict()
restored = BotUser.from_dict(user_dict)
assert restored.user_id == original.user_id
assert restored.username == original.username
assert restored.display_name == original.display_name
def test_bot_user_flag(self):
"""Test bot user identification."""
bot_user = BotUser(
user_id='bot-123',
username='assistant_bot',
is_bot=True,
)
assert bot_user.is_bot is True
class TestBotChannelIntegration:
"""Integration tests for BotChannel."""
def test_dm_channel(self):
"""Test direct message channel."""
channel = BotChannel(
channel_id='dm-123',
name=None,
channel_type='dm',
)
assert channel.channel_type == 'dm'
def test_group_channel(self):
"""Test group channel."""
channel = BotChannel(
channel_id='group-456',
name='Team Chat',
channel_type='group',
metadata={'member_count': 25},
)
assert channel.channel_type == 'group'
assert channel.name == 'Team Chat'
assert channel.metadata['member_count'] == 25
def test_thread_channel(self):
"""Test thread channel."""
channel = BotChannel(
channel_id='thread-789',
name='Discussion Thread',
channel_type='thread',
metadata={'parent_channel': 'channel-123'},
)
assert channel.channel_type == 'thread'
def test_channel_serialization_roundtrip(self):
"""Test channel can be serialized and deserialized."""
original = BotChannel(
channel_id='channel-999',
name='General',
channel_type='channel',
)
channel_dict = original.to_dict()
restored = BotChannel.from_dict(channel_dict)
assert restored.channel_id == original.channel_id
assert restored.name == original.name
class TestBotMessageIntegration:
"""Integration tests for BotMessage."""
def test_text_message(self):
"""Test text message creation."""
sender = BotUser(user_id='user-1', username='alice')
channel = BotChannel(channel_id='channel-1', channel_type='dm')
message = BotMessage(
content='Hello, World!',
message_type=MessageType.TEXT,
sender=sender,
channel=channel,
)
assert message.text == 'Hello, World!'
assert message.message_type == MessageType.TEXT
assert message.sender.username == 'alice'
def test_command_message(self):
"""Test command message parsing."""
message = BotMessage(
content='/help search query',
message_type=MessageType.COMMAND,
)
assert message.is_command is True
assert message.command == 'help'
assert message.command_args == ['search', 'query']
def test_command_detection_from_text(self):
"""Test command detection from text content."""
message = BotMessage(
content='/status',
message_type=MessageType.TEXT,
)
# Should detect as command even if type is TEXT
assert message.is_command is True
assert message.command == 'status'
assert message.command_args == []
def test_message_with_attachments(self):
"""Test message with attachments."""
message = BotMessage(
content='Check out these files',
attachments=[
{'type': 'image', 'url': 'https://example.com/image.png'},
{'type': 'document', 'url': 'https://example.com/doc.pdf'},
],
)
assert len(message.attachments) == 2
assert message.attachments[0]['type'] == 'image'
def test_reply_message(self):
"""Test reply message."""
original = BotMessage(content='Original message')
reply = BotMessage(
content='This is a reply',
reply_to=original.message_id,
)
assert reply.reply_to == original.message_id
def test_threaded_message(self):
"""Test threaded message."""
message = BotMessage(
content='Thread reply',
thread_id='thread-123',
)
assert message.thread_id == 'thread-123'
def test_message_serialization_roundtrip(self):
"""Test message can be serialized and deserialized."""
sender = BotUser(user_id='user-1', username='bob')
channel = BotChannel(channel_id='channel-1', channel_type='group')
original = BotMessage(
content='Test message',
message_type=MessageType.TEXT,
sender=sender,
channel=channel,
metadata={'priority': 'high'},
)
msg_dict = original.to_dict()
restored = BotMessage.from_dict(msg_dict)
assert restored.content == original.content
assert restored.message_type == original.message_type
assert restored.sender.user_id == original.sender.user_id
assert restored.channel.channel_id == original.channel.channel_id
def test_all_message_types(self):
"""Test all message types can be used."""
for msg_type in MessageType:
message = BotMessage(
content='Test',
message_type=msg_type,
)
assert message.message_type == msg_type
class TestBotProtocolCompliance:
"""Test that implementations comply with protocols."""
def test_bot_protocol_methods(self):
"""Verify BotProtocol has all required methods."""
required_properties = ['is_running', 'platform', 'bot_user']
required_methods = [
'start', 'stop', 'set_agent', 'get_agent',
'send_message', 'edit_message', 'delete_message',
'on_message', 'on_command', 'send_typing',
'get_user', 'get_channel'
]
for prop in required_properties:
assert hasattr(BotProtocol, prop)
for method in required_methods:
assert hasattr(BotProtocol, method)
def test_bot_message_protocol_methods(self):
"""Verify BotMessageProtocol has all required methods."""
required_properties = ['message_id', 'content', 'sender', 'channel']
for prop in required_properties:
assert hasattr(BotMessageProtocol, prop)
def test_bot_user_protocol_methods(self):
"""Verify BotUserProtocol has all required methods."""
required_properties = ['user_id', 'username', 'is_bot']
for prop in required_properties:
assert hasattr(BotUserProtocol, prop)
def test_bot_channel_protocol_methods(self):
"""Verify BotChannelProtocol has all required methods."""
required_properties = ['channel_id', 'channel_type']
for prop in required_properties:
assert hasattr(BotChannelProtocol, prop)
class TestMockBotWorkflow:
"""Test a mock bot workflow."""
def test_message_handling_workflow(self):
"""Test complete message handling workflow."""
# Create mock bot
bot = MagicMock()
bot.platform = 'telegram'
bot.is_running = True
messages_received = []
responses_sent = []
def on_message(handler):
def wrapper(msg):
messages_received.append(msg)
return handler(msg)
return wrapper
async def send_message(channel_id, content, **kwargs):
msg = BotMessage(
content=content,
sender=BotUser(user_id='bot-1', is_bot=True),
channel=BotChannel(channel_id=channel_id, channel_type='dm'),
)
responses_sent.append(msg)
return msg
bot.on_message = on_message
bot.send_message = send_message
# Simulate incoming message
incoming = BotMessage(
content='Hello bot!',
sender=BotUser(user_id='user-1', username='alice'),
channel=BotChannel(channel_id='dm-123', channel_type='dm'),
)
@bot.on_message
def handle_message(msg):
return f"Received: {msg.content}"
result = handle_message(incoming)
assert len(messages_received) == 1
assert result == "Received: Hello bot!"
@pytest.mark.asyncio
class TestAsyncBotOperations:
"""Test async bot operations."""
async def test_async_message_send(self):
"""Test async message sending."""
sent_messages = []
async def send_message(channel_id: str, content: str):
msg = BotMessage(
content=content,
sender=BotUser(user_id='bot-1', is_bot=True),
channel=BotChannel(channel_id=channel_id, channel_type='dm'),
)
sent_messages.append(msg)
return msg
result = await send_message('channel-1', 'Hello!')
assert len(sent_messages) == 1
assert result.content == 'Hello!'
async def test_async_typing_indicator(self):
"""Test async typing indicator."""
typing_channels = []
async def send_typing(channel_id: str):
typing_channels.append(channel_id)
await send_typing('channel-1')
await send_typing('channel-2')
assert len(typing_channels) == 2
if __name__ == '__main__':
pytest.main([__file__, '-v'])
@@ -0,0 +1,342 @@
"""
Integration tests for Gateway module.
These tests verify the Gateway protocols and implementations work correctly
with real scenarios. Some tests require API keys and are skipped if not available.
"""
import pytest
import os
from unittest.mock import MagicMock, patch
# Import from core SDK
from praisonaiagents.gateway import (
GatewayConfig,
SessionConfig,
GatewayEvent,
GatewayMessage,
EventType,
)
from praisonaiagents.gateway.protocols import (
GatewayProtocol,
GatewaySessionProtocol,
GatewayClientProtocol,
)
class TestGatewayConfigIntegration:
"""Integration tests for GatewayConfig."""
def test_config_from_environment(self):
"""Test config can be created from environment variables."""
with patch.dict(os.environ, {
'GATEWAY_HOST': '0.0.0.0',
'GATEWAY_PORT': '9000',
}):
config = GatewayConfig(
host=os.environ.get('GATEWAY_HOST', 'localhost'),
port=int(os.environ.get('GATEWAY_PORT', '8765')),
)
assert config.host == '0.0.0.0'
assert config.port == 9000
def test_config_with_auth(self):
"""Test config with authentication settings."""
config = GatewayConfig(
host='localhost',
port=8765,
auth_token='test-token-123',
)
assert config.auth_token == 'test-token-123'
# Verify token is hidden in dict representation
config_dict = config.to_dict()
assert 'test-token-123' not in str(config_dict)
def test_session_config_defaults(self):
"""Test SessionConfig has sensible defaults."""
config = SessionConfig()
assert config.timeout > 0
assert config.max_messages > 0
class TestGatewayEventIntegration:
"""Integration tests for GatewayEvent."""
def test_event_serialization_roundtrip(self):
"""Test event can be serialized and deserialized."""
original = GatewayEvent(
type=EventType.MESSAGE,
data={'content': 'Hello, World!', 'user_id': '123'},
source='agent-1',
target='client-1',
)
# Serialize
event_dict = original.to_dict()
# Deserialize
restored = GatewayEvent.from_dict(event_dict)
assert restored.type == EventType.MESSAGE
assert restored.data['content'] == 'Hello, World!'
assert restored.source == 'agent-1'
assert restored.target == 'client-1'
def test_event_with_custom_type(self):
"""Test event with custom (non-enum) type."""
event = GatewayEvent(
type='custom_event',
data={'custom': 'data'},
)
event_dict = event.to_dict()
assert event_dict['type'] == 'custom_event'
restored = GatewayEvent.from_dict(event_dict)
assert restored.type == 'custom_event'
def test_all_event_types(self):
"""Test all standard event types can be used."""
for event_type in EventType:
event = GatewayEvent(type=event_type, data={})
assert event.type == event_type
event_dict = event.to_dict()
restored = GatewayEvent.from_dict(event_dict)
assert restored.type == event_type
class TestGatewayMessageIntegration:
"""Integration tests for GatewayMessage."""
def test_message_with_metadata(self):
"""Test message with rich metadata."""
message = GatewayMessage(
content='Test message',
sender_id='user-123',
session_id='session-456',
metadata={
'channel': 'telegram',
'priority': 'high',
'attachments': ['file1.pdf', 'file2.png'],
},
)
msg_dict = message.to_dict()
assert msg_dict['metadata']['channel'] == 'telegram'
assert len(msg_dict['metadata']['attachments']) == 2
def test_message_reply_chain(self):
"""Test message reply chain."""
original = GatewayMessage(
content='Original message',
sender_id='user-1',
session_id='session-1',
)
reply = GatewayMessage(
content='Reply to original',
sender_id='agent-1',
session_id='session-1',
reply_to=original.message_id,
)
assert reply.reply_to == original.message_id
def test_message_structured_content(self):
"""Test message with structured content."""
message = GatewayMessage(
content={
'type': 'card',
'title': 'Weather Report',
'body': 'Sunny, 25°C',
'actions': [
{'label': 'Details', 'action': 'show_details'},
{'label': 'Dismiss', 'action': 'dismiss'},
],
},
sender_id='weather-agent',
session_id='session-1',
)
assert isinstance(message.content, dict)
assert message.content['type'] == 'card'
class TestGatewayProtocolCompliance:
"""Test that implementations comply with protocols."""
def test_gateway_session_protocol_methods(self):
"""Verify GatewaySessionProtocol has all required methods."""
required_properties = [
'session_id', 'agent_id', 'client_id',
'is_active', 'created_at', 'last_activity'
]
required_methods = [
'get_state', 'set_state', 'add_message',
'get_messages', 'close'
]
# Check protocol has all required attributes
for prop in required_properties:
assert hasattr(GatewaySessionProtocol, prop)
for method in required_methods:
assert hasattr(GatewaySessionProtocol, method)
def test_gateway_client_protocol_methods(self):
"""Verify GatewayClientProtocol has all required methods."""
required_properties = ['client_id', 'is_connected', 'connected_at']
required_methods = ['send', 'receive', 'close']
for prop in required_properties:
assert hasattr(GatewayClientProtocol, prop)
for method in required_methods:
assert hasattr(GatewayClientProtocol, method)
def test_gateway_protocol_methods(self):
"""Verify GatewayProtocol has all required methods."""
required_properties = ['is_running', 'port', 'host']
required_methods = [
'start', 'stop', 'register_agent', 'unregister_agent',
'get_agent', 'list_agents', 'create_session', 'get_session',
'close_session', 'list_sessions', 'on_event', 'emit',
'broadcast', 'health'
]
for prop in required_properties:
assert hasattr(GatewayProtocol, prop)
for method in required_methods:
assert hasattr(GatewayProtocol, method)
class TestMockGatewaySession:
"""Test a mock implementation of GatewaySessionProtocol."""
def test_mock_session_workflow(self):
"""Test a complete session workflow with mock."""
# Create mock session
session = MagicMock()
session.session_id = 'test-session-123'
session.agent_id = 'agent-1'
session.client_id = 'client-1'
session.is_active = True
session.created_at = 1704067200.0
session.last_activity = 1704067200.0
state = {}
messages = []
def get_state():
return state.copy()
def set_state(key, value):
state[key] = value
def add_message(msg):
messages.append(msg)
def get_messages(limit=None):
if limit:
return messages[-limit:]
return messages.copy()
session.get_state = get_state
session.set_state = set_state
session.add_message = add_message
session.get_messages = get_messages
# Test workflow
session.set_state('user_name', 'Alice')
assert session.get_state()['user_name'] == 'Alice'
msg1 = GatewayMessage(
content='Hello',
sender_id='client-1',
session_id='test-session-123',
)
session.add_message(msg1)
msg2 = GatewayMessage(
content='Hi there!',
sender_id='agent-1',
session_id='test-session-123',
)
session.add_message(msg2)
history = session.get_messages()
assert len(history) == 2
assert history[0].content == 'Hello'
assert history[1].content == 'Hi there!'
@pytest.mark.asyncio
class TestAsyncGatewayOperations:
"""Test async gateway operations."""
async def test_async_event_handling(self):
"""Test async event emission and handling."""
events_received = []
async def event_handler(event: GatewayEvent):
events_received.append(event)
# Simulate event emission
event = GatewayEvent(
type=EventType.MESSAGE,
data={'content': 'Test'},
)
await event_handler(event)
assert len(events_received) == 1
assert events_received[0].type == EventType.MESSAGE
async def test_async_broadcast(self):
"""Test async broadcast to multiple clients."""
clients_notified = []
async def notify_client(client_id: str, event: GatewayEvent):
clients_notified.append(client_id)
# Simulate broadcast
event = GatewayEvent(
type=EventType.BROADCAST,
data={'announcement': 'Server maintenance in 5 minutes'},
)
client_ids = ['client-1', 'client-2', 'client-3']
for client_id in client_ids:
await notify_client(client_id, event)
assert len(clients_notified) == 3
assert 'client-1' in clients_notified
class TestGatewayHealthCheck:
"""Test gateway health check functionality."""
def test_health_response_structure(self):
"""Test health response has required fields."""
# Mock health response
health = {
'status': 'healthy',
'uptime': 3600,
'agents': 2,
'sessions': 5,
'clients': 3,
}
assert health['status'] in ['healthy', 'unhealthy']
assert isinstance(health['uptime'], (int, float))
assert isinstance(health['agents'], int)
assert isinstance(health['sessions'], int)
assert isinstance(health['clients'], int)
if __name__ == '__main__':
pytest.main([__file__, '-v'])
@@ -0,0 +1,574 @@
"""
Real Integration Tests for Gateway, Bots, Sandbox, and Failover Features.
These tests use real API calls and verify actual functionality.
Requires: OPENAI_API_KEY environment variable.
"""
import asyncio
import os
import sys
import time
import pytest
from typing import Dict, Any
from unittest.mock import patch, MagicMock
# Add parent to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from praisonaiagents import Agent, tool
from praisonaiagents import (
# Gateway
GatewayConfig, SessionConfig, GatewayEvent, GatewayMessage, EventType,
GatewayProtocol, GatewaySessionProtocol, GatewayClientProtocol,
# Bots
BotConfig, BotMessage, BotUser, BotChannel, MessageType, BotProtocol,
# Sandbox
SandboxConfig, SandboxResult, SandboxStatus, ResourceLimits, SecurityPolicy,
SandboxProtocol,
# Failover
AuthProfile, FailoverConfig, FailoverManager, ProviderStatus,
)
# ============================================================================
# GATEWAY REAL TESTS (5+ tests)
# ============================================================================
class TestGatewayReal:
"""Real integration tests for Gateway feature."""
def test_gateway_config_creation_and_serialization(self):
"""Test 1: Gateway config can be created and serialized."""
config = GatewayConfig(
host="127.0.0.1",
port=8765,
auth_token="test-secret-token",
max_connections=500,
heartbeat_interval=30,
)
# Verify attributes
assert config.host == "127.0.0.1"
assert config.port == 8765
assert config.auth_token == "test-secret-token"
assert config.max_connections == 500
# Verify serialization hides token
config_dict = config.to_dict()
assert "test-secret-token" not in str(config_dict)
assert config_dict["auth_token"] == "***"
# Verify computed properties
assert config.ws_url == "ws://127.0.0.1:8765"
assert config.http_url == "http://127.0.0.1:8765"
assert config.is_secure is False
def test_gateway_session_config(self):
"""Test 2: Session config works correctly."""
session_config = SessionConfig(
timeout=7200,
max_messages=500,
persist=True,
persist_path="/tmp/sessions",
)
assert session_config.timeout == 7200
assert session_config.max_messages == 500
assert session_config.persist is True
# Verify serialization
session_dict = session_config.to_dict()
assert session_dict["timeout"] == 7200
assert session_dict["persist_path"] == "/tmp/sessions"
def test_gateway_event_creation_and_roundtrip(self):
"""Test 3: Gateway events can be created and serialized."""
event = GatewayEvent(
type=EventType.MESSAGE,
data={"content": "Hello, World!"},
source="agent-1",
target="client-1",
)
assert event.type == EventType.MESSAGE
assert event.data["content"] == "Hello, World!"
assert event.source == "agent-1"
assert event.target == "client-1"
assert event.event_id is not None
assert event.timestamp > 0
# Test serialization roundtrip
event_dict = event.to_dict()
restored = GatewayEvent.from_dict(event_dict)
assert restored.type == EventType.MESSAGE
assert restored.data["content"] == "Hello, World!"
def test_gateway_message_creation(self):
"""Test 4: Gateway messages work correctly."""
message = GatewayMessage(
content="Test message",
role="user",
agent_id="agent-1",
session_id="session-123",
)
assert message.content == "Test message"
assert message.role == "user"
assert message.agent_id == "agent-1"
assert message.session_id == "session-123"
# Test serialization
msg_dict = message.to_dict()
assert msg_dict["content"] == "Test message"
# Test roundtrip
restored = GatewayMessage.from_dict(msg_dict)
assert restored.content == "Test message"
def test_gateway_event_types_complete(self):
"""Test 5: All event types are defined."""
expected_types = [
"CONNECT", "DISCONNECT", "RECONNECT",
"SESSION_START", "SESSION_END", "SESSION_UPDATE",
"AGENT_REGISTER", "AGENT_UNREGISTER", "AGENT_STATUS",
"MESSAGE", "MESSAGE_ACK", "TYPING",
"HEALTH", "ERROR", "BROADCAST",
]
for event_type in expected_types:
assert hasattr(EventType, event_type), f"Missing EventType.{event_type}"
def test_gateway_config_with_ssl(self):
"""Test 6: Gateway config with SSL settings."""
config = GatewayConfig(
host="0.0.0.0",
port=443,
ssl_cert="/path/to/cert.pem",
ssl_key="/path/to/key.pem",
)
assert config.is_secure is True
assert config.ws_url == "wss://0.0.0.0:443"
assert config.http_url == "https://0.0.0.0:443"
# ============================================================================
# BOTS REAL TESTS (5+ tests)
# ============================================================================
class TestBotsReal:
"""Real integration tests for Bots feature."""
def test_bot_config_telegram(self):
"""Test 1: Telegram bot config works correctly."""
config = BotConfig(
token="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
webhook_url="https://example.com/webhook",
command_prefix="/",
mention_required=True,
typing_indicator=True,
metadata={"platform": "telegram"},
)
assert config.token == "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
assert config.webhook_url == "https://example.com/webhook"
assert config.is_webhook_mode is True
assert config.command_prefix == "/"
# Token should be hidden
config_dict = config.to_dict()
assert "ABC-DEF" not in str(config_dict)
def test_bot_config_discord(self):
"""Test 2: Discord bot config works correctly."""
config = BotConfig(
token="MTIzNDU2Nzg5MDEyMzQ1Njc4OQ.Gg1234.abcdefghijklmnop",
command_prefix="!",
mention_required=True,
metadata={
"platform": "discord",
"guild_ids": ["123456789", "987654321"],
},
)
assert config.command_prefix == "!"
assert config.metadata["platform"] == "discord"
assert len(config.metadata["guild_ids"]) == 2
def test_bot_message_creation(self):
"""Test 3: Bot messages work correctly."""
user = BotUser(
id="user123",
username="testuser",
display_name="Test User",
is_bot=False,
)
channel = BotChannel(
id="channel456",
name="general",
type="text",
)
message = BotMessage(
id="msg789",
content="Hello from bot!",
type=MessageType.TEXT,
user=user,
channel=channel,
)
assert message.content == "Hello from bot!"
assert message.type == MessageType.TEXT
assert message.user.username == "testuser"
assert message.channel.name == "general"
def test_bot_user_allowed_check(self):
"""Test 4: User allowlist works correctly."""
config = BotConfig(
token="test-token",
allowed_users=["user1", "user2", "user3"],
allowed_channels=["channel1"],
)
assert config.is_user_allowed("user1") is True
assert config.is_user_allowed("user4") is False
assert config.is_channel_allowed("channel1") is True
assert config.is_channel_allowed("channel2") is False
# Empty allowlist means all allowed
config_open = BotConfig(token="test")
assert config_open.is_user_allowed("anyone") is True
assert config_open.is_channel_allowed("anywhere") is True
def test_bot_message_types_complete(self):
"""Test 5: All message types are defined."""
expected_types = ["TEXT", "IMAGE", "AUDIO", "VIDEO", "FILE", "STICKER", "COMMAND"]
for msg_type in expected_types:
assert hasattr(MessageType, msg_type), f"Missing MessageType.{msg_type}"
def test_bot_config_polling_mode(self):
"""Test 6: Polling mode config works correctly."""
config = BotConfig(
token="test-token",
polling_interval=2.0,
retry_attempts=5,
timeout=60,
)
assert config.is_webhook_mode is False
assert config.polling_interval == 2.0
assert config.retry_attempts == 5
assert config.timeout == 60
# ============================================================================
# SANDBOX REAL TESTS (5+ tests)
# ============================================================================
class TestSandboxReal:
"""Real integration tests for Sandbox feature."""
def test_sandbox_config_subprocess(self):
"""Test 1: Subprocess sandbox config works correctly."""
config = SandboxConfig.subprocess()
assert config.sandbox_type == "subprocess"
assert config.auto_cleanup is True
def test_sandbox_config_docker(self):
"""Test 2: Docker sandbox config works correctly."""
config = SandboxConfig.docker(image="python:3.11-slim")
assert config.sandbox_type == "docker"
assert config.image == "python:3.11-slim"
def test_sandbox_resource_limits_presets(self):
"""Test 3: Resource limit presets work correctly."""
minimal = ResourceLimits.minimal()
standard = ResourceLimits.standard()
generous = ResourceLimits.generous()
# Minimal should be most restrictive
assert minimal.memory_mb < standard.memory_mb < generous.memory_mb
assert minimal.timeout_seconds < standard.timeout_seconds < generous.timeout_seconds
assert minimal.network_enabled is False
assert generous.network_enabled is True
# Verify serialization
minimal_dict = minimal.to_dict()
assert "memory_mb" in minimal_dict
assert "timeout_seconds" in minimal_dict
def test_sandbox_security_policy_presets(self):
"""Test 4: Security policy presets work correctly."""
strict = SecurityPolicy.strict()
standard = SecurityPolicy.standard()
permissive = SecurityPolicy.permissive()
assert strict.allow_network is False
assert strict.allow_subprocess is False
assert permissive.allow_network is True
assert permissive.allow_subprocess is True
def test_sandbox_result_creation(self):
"""Test 5: Sandbox results work correctly."""
result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout="Hello, World!\n",
stderr="",
duration_seconds=0.5,
)
assert result.success is True
assert result.status == SandboxStatus.COMPLETED
assert result.stdout == "Hello, World!\n"
assert result.duration_seconds == 0.5
# Test combined output
assert "Hello, World!" in result.combined_output
def test_sandbox_result_failure(self):
"""Test 6: Failed sandbox results work correctly."""
result = SandboxResult(
status=SandboxStatus.FAILED,
exit_code=1,
stdout="",
stderr="NameError: name 'undefined' is not defined",
error="Execution failed",
)
assert result.success is False
assert result.status == SandboxStatus.FAILED
assert "NameError" in result.stderr
def test_sandbox_status_values(self):
"""Test 7: All sandbox status values are defined."""
expected_statuses = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "TIMEOUT", "KILLED"]
for status in expected_statuses:
assert hasattr(SandboxStatus, status), f"Missing SandboxStatus.{status}"
# ============================================================================
# FAILOVER REAL TESTS (5+ tests)
# ============================================================================
class TestFailoverReal:
"""Real integration tests for Failover feature."""
def test_auth_profile_creation(self):
"""Test 1: Auth profiles can be created correctly."""
profile = AuthProfile(
name="openai-primary",
provider="openai",
api_key="sk-test-key-12345",
priority=1,
rate_limit_rpm=100,
)
assert profile.name == "openai-primary"
assert profile.provider == "openai"
assert profile.priority == 1
assert profile.status == ProviderStatus.AVAILABLE
# API key should be hidden
profile_dict = profile.to_dict()
assert "sk-test-key-12345" not in str(profile_dict)
assert "***" in profile_dict["api_key"]
def test_failover_config_creation(self):
"""Test 2: Failover config works correctly."""
config = FailoverConfig(
max_retries=3,
retry_delay=1.0,
exponential_backoff=True,
max_retry_delay=60.0,
failover_on_rate_limit=True,
failover_on_timeout=True,
)
assert config.max_retries == 3
assert config.retry_delay == 1.0
assert config.exponential_backoff is True
assert config.max_retry_delay == 60.0
def test_failover_manager_add_profiles(self):
"""Test 3: Failover manager can add and retrieve profiles."""
manager = FailoverManager()
profile1 = AuthProfile(
name="openai",
provider="openai",
api_key="sk-test-1",
priority=1,
)
profile2 = AuthProfile(
name="anthropic",
provider="anthropic",
api_key="sk-ant-test",
priority=2,
)
manager.add_profile(profile1)
manager.add_profile(profile2)
# Retrieve by name
assert manager.get_profile("openai") is not None
assert manager.get_profile("anthropic") is not None
assert manager.get_profile("nonexistent") is None
# Get next profile (should be highest priority)
next_profile = manager.get_next_profile()
assert next_profile.name == "openai"
def test_failover_manager_mark_failure(self):
"""Test 4: Failover manager handles failures correctly."""
manager = FailoverManager(FailoverConfig(max_retries=2))
profile = AuthProfile(
name="test-provider",
provider="openai",
api_key="sk-test",
priority=1,
)
manager.add_profile(profile)
# Mark failure
manager.mark_failure("test-provider", "Rate limit exceeded")
# Profile should still be retrievable but with updated status
updated = manager.get_profile("test-provider")
assert updated.last_error == "Rate limit exceeded"
assert updated.last_error_time is not None
def test_failover_manager_reset(self):
"""Test 5: Failover manager can reset profiles."""
manager = FailoverManager()
profile = AuthProfile(
name="test",
provider="openai",
api_key="sk-test",
priority=1,
)
manager.add_profile(profile)
manager.mark_failure("test", "Error")
# Reset
manager.mark_success("test")
updated = manager.get_profile("test")
assert updated.status == ProviderStatus.AVAILABLE
def test_failover_manager_priority_order(self):
"""Test 6: Profiles are returned in priority order."""
manager = FailoverManager()
# Add in reverse priority order
manager.add_profile(AuthProfile(name="low", provider="x", api_key="k", priority=3))
manager.add_profile(AuthProfile(name="high", provider="x", api_key="k", priority=1))
manager.add_profile(AuthProfile(name="mid", provider="x", api_key="k", priority=2))
# Should get highest priority first
next_profile = manager.get_next_profile()
assert next_profile.name == "high"
def test_provider_status_values(self):
"""Test 7: All provider status values are defined."""
expected_statuses = ["AVAILABLE", "RATE_LIMITED", "ERROR", "DISABLED"]
for status in expected_statuses:
assert hasattr(ProviderStatus, status), f"Missing ProviderStatus.{status}"
# ============================================================================
# REAL AGENT TESTS WITH API (requires OPENAI_API_KEY)
# ============================================================================
@pytest.mark.skipif(
not os.environ.get("OPENAI_API_KEY"),
reason="OPENAI_API_KEY not set"
)
class TestRealAgentWithAPI:
"""Real agent tests that make actual API calls."""
def test_agent_basic_chat(self):
"""Test 1: Basic agent chat works."""
agent = Agent(
name="test-agent",
instructions="You are a helpful assistant. Respond briefly.",
llm="gpt-4o-mini",
)
response = agent.start("Say 'Hello' and nothing else.")
assert response is not None
assert len(response) > 0
assert "hello" in response.lower()
def test_agent_with_tool(self):
"""Test 2: Agent with tool works."""
@tool
def get_current_time() -> str:
"""Get the current time."""
return "12:00 PM"
agent = Agent(
name="time-agent",
instructions="Use the get_current_time tool when asked about time.",
llm="gpt-4o-mini",
tools=[get_current_time],
)
response = agent.start("What time is it?")
assert response is not None
assert "12:00" in response or "time" in response.lower()
def test_agent_streaming(self):
"""Test 3: Agent streaming works."""
agent = Agent(
name="stream-agent",
instructions="You are helpful. Respond briefly.",
llm="gpt-4o-mini",
)
chunks = []
for chunk in agent.start("Say 'test' only.", stream=True):
if chunk:
chunks.append(chunk)
assert len(chunks) > 0
full_response = "".join(chunks)
assert len(full_response) > 0
# ============================================================================
# SANDBOX EXECUTION TESTS (requires subprocess)
# ============================================================================
class TestSandboxExecution:
"""Tests for actual sandbox code execution."""
def test_subprocess_sandbox_simple_code(self):
"""Test subprocess sandbox can execute simple code."""
# This test verifies the sandbox config is correct
# Actual execution would require the wrapper implementation
config = SandboxConfig(
sandbox_type="subprocess",
resource_limits=ResourceLimits(
memory_mb=128,
timeout_seconds=10,
network_enabled=False,
),
)
assert config.sandbox_type == "subprocess"
assert config.resource_limits.timeout_seconds == 10
assert config.resource_limits.network_enabled is False
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,371 @@
"""
Integration tests for Sandbox module.
These tests verify the Sandbox protocols and implementations work correctly
with real scenarios.
"""
import pytest
from unittest.mock import MagicMock
# Import from core SDK
from praisonaiagents.sandbox import (
SandboxConfig,
SandboxResult,
SandboxStatus,
ResourceLimits,
)
from praisonaiagents.sandbox.protocols import SandboxProtocol
class TestSandboxConfigIntegration:
"""Integration tests for SandboxConfig."""
def test_docker_sandbox_config(self):
"""Test Docker sandbox configuration."""
config = SandboxConfig(
sandbox_type='docker',
image='python:3.11-slim',
)
assert config.sandbox_type == 'docker'
assert config.image == 'python:3.11-slim'
# Network is controlled via resource_limits
assert config.resource_limits.network_enabled is False
def test_subprocess_sandbox_config(self):
"""Test subprocess sandbox configuration."""
limits = ResourceLimits(timeout_seconds=30)
config = SandboxConfig(
sandbox_type='subprocess',
resource_limits=limits,
)
assert config.sandbox_type == 'subprocess'
assert config.resource_limits.timeout_seconds == 30
def test_config_with_resource_limits(self):
"""Test config with resource limits."""
limits = ResourceLimits(
memory_mb=256,
cpu_percent=50,
timeout_seconds=60,
network_enabled=False,
)
config = SandboxConfig(
sandbox_type='docker',
resource_limits=limits,
)
assert config.resource_limits.memory_mb == 256
assert config.resource_limits.cpu_percent == 50
class TestResourceLimitsIntegration:
"""Integration tests for ResourceLimits."""
def test_minimal_limits(self):
"""Test minimal resource limits for untrusted code."""
limits = ResourceLimits.minimal()
assert limits.memory_mb == 128
assert limits.cpu_percent == 50
assert limits.timeout_seconds == 30
assert limits.network_enabled is False
def test_standard_limits(self):
"""Test standard resource limits."""
limits = ResourceLimits.standard()
assert limits.memory_mb == 512
assert limits.timeout_seconds == 60
def test_generous_limits(self):
"""Test generous resource limits for trusted code."""
limits = ResourceLimits.generous()
assert limits.memory_mb == 2048
assert limits.timeout_seconds == 300
assert limits.network_enabled is True
def test_limits_serialization_roundtrip(self):
"""Test limits can be serialized and deserialized."""
original = ResourceLimits(
memory_mb=1024,
cpu_percent=75,
timeout_seconds=120,
max_processes=20,
network_enabled=True,
)
limits_dict = original.to_dict()
restored = ResourceLimits.from_dict(limits_dict)
assert restored.memory_mb == original.memory_mb
assert restored.cpu_percent == original.cpu_percent
assert restored.timeout_seconds == original.timeout_seconds
assert restored.max_processes == original.max_processes
assert restored.network_enabled == original.network_enabled
class TestSandboxResultIntegration:
"""Integration tests for SandboxResult."""
def test_successful_result(self):
"""Test successful execution result."""
result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout='Hello, World!\n',
stderr='',
duration_seconds=0.5,
)
assert result.success is True
assert result.status == SandboxStatus.COMPLETED
assert result.exit_code == 0
assert 'Hello' in result.stdout
def test_failed_result(self):
"""Test failed execution result."""
result = SandboxResult(
status=SandboxStatus.FAILED,
exit_code=1,
stdout='',
stderr='Error: Division by zero',
error='Execution failed',
)
assert result.success is False
assert result.status == SandboxStatus.FAILED
assert 'Division by zero' in result.stderr
def test_timeout_result(self):
"""Test timeout execution result."""
result = SandboxResult(
status=SandboxStatus.TIMEOUT,
exit_code=None,
stdout='Partial output...',
stderr='',
error='Execution timed out after 60 seconds',
duration_seconds=60.0,
)
assert result.success is False
assert result.status == SandboxStatus.TIMEOUT
assert 'timed out' in result.error
def test_killed_result(self):
"""Test killed execution result."""
result = SandboxResult(
status=SandboxStatus.KILLED,
exit_code=-9,
stdout='',
stderr='',
error='Process killed due to memory limit',
)
assert result.success is False
assert result.status == SandboxStatus.KILLED
def test_combined_output(self):
"""Test combined output property."""
result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout='Standard output\n',
stderr='Warning: deprecated function\n',
)
output = result.output
assert 'Standard output' in output
assert 'Warning' in output
def test_result_serialization_roundtrip(self):
"""Test result can be serialized and deserialized."""
original = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout='Output',
stderr='',
duration_seconds=1.5,
metadata={'language': 'python'},
)
result_dict = original.to_dict()
restored = SandboxResult.from_dict(result_dict)
assert restored.status == original.status
assert restored.exit_code == original.exit_code
assert restored.stdout == original.stdout
assert restored.duration_seconds == original.duration_seconds
def test_all_status_values(self):
"""Test all sandbox status values."""
for status in SandboxStatus:
result = SandboxResult(status=status)
assert result.status == status
class TestSandboxProtocolCompliance:
"""Test that implementations comply with protocols."""
def test_sandbox_protocol_methods(self):
"""Verify SandboxProtocol has all required methods."""
required_properties = ['is_available', 'sandbox_type']
required_methods = [
'start', 'stop', 'execute', 'execute_file', 'run_command',
'write_file', 'read_file', 'list_files',
'get_status', 'cleanup', 'reset'
]
for prop in required_properties:
assert hasattr(SandboxProtocol, prop)
for method in required_methods:
assert hasattr(SandboxProtocol, method)
class TestMockSandboxWorkflow:
"""Test a mock sandbox workflow."""
def test_code_execution_workflow(self):
"""Test complete code execution workflow."""
# Create mock sandbox
sandbox = MagicMock()
sandbox.sandbox_type = 'docker'
sandbox.is_available = True
executions = []
async def execute(code, language='python', limits=None, **kwargs):
executions.append({
'code': code,
'language': language,
'limits': limits,
})
# Simulate execution
if 'error' in code.lower():
return SandboxResult(
status=SandboxStatus.FAILED,
exit_code=1,
stderr='Error in code',
)
return SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout='Execution successful',
duration_seconds=0.1,
)
sandbox.execute = execute
# Test successful execution
import asyncio
result = asyncio.get_event_loop().run_until_complete(
sandbox.execute('print("Hello")', language='python')
)
assert result.success is True
assert len(executions) == 1
assert executions[0]['language'] == 'python'
def test_file_operations_workflow(self):
"""Test file operations in sandbox."""
sandbox = MagicMock()
files = {}
async def write_file(path, content):
files[path] = content
return True
async def read_file(path):
return files.get(path)
async def list_files(path='/'):
return list(files.keys())
sandbox.write_file = write_file
sandbox.read_file = read_file
sandbox.list_files = list_files
import asyncio
loop = asyncio.get_event_loop()
# Write file
loop.run_until_complete(sandbox.write_file('/app/script.py', 'print("test")'))
# Read file
content = loop.run_until_complete(sandbox.read_file('/app/script.py'))
assert content == 'print("test")'
# List files
file_list = loop.run_until_complete(sandbox.list_files('/'))
assert '/app/script.py' in file_list
@pytest.mark.asyncio
class TestAsyncSandboxOperations:
"""Test async sandbox operations."""
async def test_async_execute(self):
"""Test async code execution."""
async def mock_execute(code: str) -> SandboxResult:
return SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout=f'Executed: {code}',
)
result = await mock_execute('print("Hello")')
assert result.success is True
assert 'Executed' in result.stdout
async def test_async_with_timeout(self):
"""Test async execution with timeout."""
async def mock_execute_with_timeout(code: str, timeout: int) -> SandboxResult:
# Simulate timeout check
if timeout < 1:
return SandboxResult(
status=SandboxStatus.TIMEOUT,
error='Timeout too short',
)
return SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout='Done',
)
result = await mock_execute_with_timeout('long_running()', timeout=60)
assert result.success is True
class TestSandboxSecurityPolicies:
"""Test sandbox security policies."""
def test_network_disabled_by_default(self):
"""Test network is disabled by default for security."""
limits = ResourceLimits()
assert limits.network_enabled is False
def test_minimal_limits_are_restrictive(self):
"""Test minimal limits are appropriately restrictive."""
limits = ResourceLimits.minimal()
# Should have low memory
assert limits.memory_mb <= 256
# Should have short timeout
assert limits.timeout_seconds <= 60
# Should have no network
assert limits.network_enabled is False
# Should have limited processes
assert limits.max_processes <= 10
if __name__ == '__main__':
pytest.main([__file__, '-v'])
@@ -0,0 +1,357 @@
"""
Tests for enhanced autonomy loop features.
Tests for:
- Completion promise detection
- Prompt re-injection
- Context clearing between iterations
TDD: These tests are written FIRST, before implementation.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
class TestCompletionPromise:
"""Test completion promise detection in run_autonomous()."""
def test_autonomy_config_accepts_completion_promise(self):
"""AutonomyConfig should accept completion_promise parameter."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig(completion_promise="DONE")
assert config.completion_promise == "DONE"
def test_autonomy_config_completion_promise_default_none(self):
"""AutonomyConfig.completion_promise should default to None."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig()
assert config.completion_promise is None
def test_run_autonomous_accepts_completion_promise_param(self):
"""run_autonomous() should accept completion_promise parameter."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
# Mock chat to return a response with promise
with patch.object(agent, 'chat', return_value="Task done. <promise>DONE</promise>"):
result = agent.run_autonomous(
"Test task",
max_iterations=5,
completion_promise="DONE"
)
assert result.success == True
assert result.completion_reason == "promise"
def test_completion_promise_detected_in_response(self):
"""Agent should detect <promise>TEXT</promise> in response."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
# Mock chat to return response with promise tag
with patch.object(agent, 'chat', return_value="I have completed the task. <promise>TASK_COMPLETE</promise>"):
result = agent.run_autonomous(
"Build something",
max_iterations=10,
completion_promise="TASK_COMPLETE"
)
assert result.success == True
assert result.completion_reason == "promise"
assert result.iterations == 1 # Should stop on first iteration
def test_completion_promise_not_detected_without_tag(self):
"""Agent should NOT detect promise without proper tag."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
# Mock chat to return response WITHOUT promise tag
responses = ["Working on it...", "Still working...", "Almost done. TASK_COMPLETE"]
call_count = [0]
def mock_chat(prompt):
idx = min(call_count[0], len(responses) - 1)
call_count[0] += 1
return responses[idx]
with patch.object(agent, 'chat', side_effect=mock_chat):
result = agent.run_autonomous(
"Build something",
max_iterations=3,
completion_promise="TASK_COMPLETE"
)
# Should NOT succeed because promise tag is missing
assert result.completion_reason != "promise"
def test_completion_promise_exact_match(self):
"""Promise detection should require exact match."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
# Mock chat with wrong promise text
with patch.object(agent, 'chat', return_value="<promise>WRONG</promise>"):
result = agent.run_autonomous(
"Test task",
max_iterations=2,
completion_promise="DONE"
)
# Should NOT detect because promise text doesn't match
assert result.completion_reason != "promise"
class TestPromptReinjection:
"""Test that original prompt is re-injected each iteration."""
def test_original_prompt_used_every_iteration(self):
"""Original prompt should be used for every iteration, not 'Continue with task'."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
prompts_received = []
call_count = [0]
def mock_chat(prompt):
prompts_received.append(prompt)
call_count[0] += 1
if call_count[0] >= 3:
return "<promise>DONE</promise>"
return "Working..."
with patch.object(agent, 'chat', side_effect=mock_chat):
result = agent.run_autonomous(
"Build a REST API",
max_iterations=5,
completion_promise="DONE"
)
# All prompts should be the original, not "Continue with the task"
for prompt in prompts_received:
assert prompt == "Build a REST API", f"Expected original prompt, got: {prompt}"
def test_prompt_not_continue_with_task(self):
"""Prompt should NOT be 'Continue with the task' after first iteration."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
prompts_received = []
def mock_chat(prompt):
prompts_received.append(prompt)
if len(prompts_received) >= 2:
return "<promise>DONE</promise>"
return "Working..."
with patch.object(agent, 'chat', side_effect=mock_chat):
result = agent.run_autonomous(
"My specific task",
max_iterations=5,
completion_promise="DONE"
)
# No prompt should be "Continue with the task"
for prompt in prompts_received:
assert "Continue with the task" not in prompt
class TestContextClearing:
"""Test context clearing between iterations."""
def test_autonomy_config_accepts_clear_context(self):
"""AutonomyConfig should accept clear_context parameter."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig(clear_context=True)
assert config.clear_context == True
def test_autonomy_config_clear_context_default_false(self):
"""AutonomyConfig.clear_context should default to False."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig()
assert config.clear_context == False
def test_run_autonomous_accepts_clear_context_param(self):
"""run_autonomous() should accept clear_context parameter."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
with patch.object(agent, 'chat', return_value="<promise>DONE</promise>"):
# Should not raise
result = agent.run_autonomous(
"Test task",
max_iterations=5,
completion_promise="DONE",
clear_context=True
)
assert result.success == True
def test_clear_history_called_when_clear_context_true(self):
"""clear_history() should be called between iterations when clear_context=True."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
call_count = [0]
def mock_chat(prompt):
call_count[0] += 1
if call_count[0] >= 3:
return "<promise>DONE</promise>"
return "Working..."
with patch.object(agent, 'chat', side_effect=mock_chat):
with patch.object(agent, 'clear_history') as mock_clear:
result = agent.run_autonomous(
"Test task",
max_iterations=5,
completion_promise="DONE",
clear_context=True
)
# clear_history should be called after each iteration (except the last)
assert mock_clear.call_count >= 2 # At least 2 calls for 3 iterations
def test_clear_history_not_called_when_clear_context_false(self):
"""clear_history() should NOT be called when clear_context=False."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
call_count = [0]
def mock_chat(prompt):
call_count[0] += 1
if call_count[0] >= 2:
return "<promise>DONE</promise>"
return "Working..."
with patch.object(agent, 'chat', side_effect=mock_chat):
with patch.object(agent, 'clear_history') as mock_clear:
result = agent.run_autonomous(
"Test task",
max_iterations=5,
completion_promise="DONE",
clear_context=False # Explicitly false
)
# clear_history should NOT be called
assert mock_clear.call_count == 0
class TestBackwardCompatibility:
"""Test that existing behavior is preserved."""
def test_run_autonomous_works_without_new_params(self):
"""run_autonomous() should work without new parameters."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
# Mock to return completion signal (old style)
with patch.object(agent, 'chat', return_value="Task completed successfully"):
result = agent.run_autonomous(
"Test task",
max_iterations=5
)
# Should still work with keyword detection
assert result.success == True
def test_keyword_detection_still_works(self):
"""Old keyword-based completion detection should still work."""
from praisonaiagents import Agent
agent = Agent(
instructions="Test agent",
autonomy=True,
output="silent"
)
with patch.object(agent, 'chat', return_value="I have finished the task"):
result = agent.run_autonomous(
"Test task",
max_iterations=5
# No completion_promise - use old behavior
)
assert result.success == True
assert result.completion_reason == "goal"
class TestAutonomyConfigFromDict:
"""Test AutonomyConfig.from_dict() with new fields."""
def test_from_dict_with_completion_promise(self):
"""from_dict should parse completion_promise."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig.from_dict({
"completion_promise": "TASK_DONE",
"max_iterations": 10
})
assert config.completion_promise == "TASK_DONE"
assert config.max_iterations == 10
def test_from_dict_with_clear_context(self):
"""from_dict should parse clear_context."""
from praisonaiagents.agent.autonomy import AutonomyConfig
config = AutonomyConfig.from_dict({
"clear_context": True
})
assert config.clear_context == True
@@ -0,0 +1,216 @@
"""
Unit tests for Bot protocols and components.
"""
from praisonaiagents.bots import (
BotConfig,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
class TestBotConfig:
"""Tests for BotConfig."""
def test_default_config(self):
"""Test default configuration values."""
config = BotConfig()
assert config.command_prefix == "/"
assert config.mention_required is True
assert config.typing_indicator is True
assert config.max_message_length == 4096
def test_custom_config(self):
"""Test custom configuration values."""
config = BotConfig(
token="test-token",
command_prefix="!",
mention_required=False,
)
assert config.token == "test-token"
assert config.command_prefix == "!"
assert config.mention_required is False
def test_webhook_mode(self):
"""Test webhook mode detection."""
config = BotConfig()
assert config.is_webhook_mode is False
config = BotConfig(webhook_url="https://example.com/webhook")
assert config.is_webhook_mode is True
def test_user_allowed(self):
"""Test user allowlist."""
config = BotConfig()
assert config.is_user_allowed("any-user") is True
config = BotConfig(allowed_users=["user-1", "user-2"])
assert config.is_user_allowed("user-1") is True
assert config.is_user_allowed("user-3") is False
def test_channel_allowed(self):
"""Test channel allowlist."""
config = BotConfig()
assert config.is_channel_allowed("any-channel") is True
config = BotConfig(allowed_channels=["channel-1"])
assert config.is_channel_allowed("channel-1") is True
assert config.is_channel_allowed("channel-2") is False
def test_to_dict_hides_token(self):
"""Test that to_dict hides sensitive data."""
config = BotConfig(token="secret-token")
data = config.to_dict()
assert data["token"] == "***"
class TestBotUser:
"""Tests for BotUser."""
def test_user_creation(self):
"""Test user creation."""
user = BotUser(
user_id="123",
username="testuser",
display_name="Test User",
)
assert user.user_id == "123"
assert user.username == "testuser"
assert user.display_name == "Test User"
assert user.is_bot is False
def test_bot_user(self):
"""Test bot user flag."""
user = BotUser(user_id="bot-1", is_bot=True)
assert user.is_bot is True
def test_to_dict(self):
"""Test user serialization."""
user = BotUser(user_id="123", username="test")
data = user.to_dict()
assert data["user_id"] == "123"
assert data["username"] == "test"
def test_from_dict(self):
"""Test user deserialization."""
data = {"user_id": "456", "username": "user", "is_bot": True}
user = BotUser.from_dict(data)
assert user.user_id == "456"
assert user.username == "user"
assert user.is_bot is True
class TestBotChannel:
"""Tests for BotChannel."""
def test_channel_creation(self):
"""Test channel creation."""
channel = BotChannel(
channel_id="ch-1",
name="general",
channel_type="channel",
)
assert channel.channel_id == "ch-1"
assert channel.name == "general"
assert channel.channel_type == "channel"
def test_dm_channel(self):
"""Test DM channel type."""
channel = BotChannel(channel_id="dm-1", channel_type="dm")
assert channel.channel_type == "dm"
def test_to_dict(self):
"""Test channel serialization."""
channel = BotChannel(channel_id="123", name="test")
data = channel.to_dict()
assert data["channel_id"] == "123"
assert data["name"] == "test"
def test_from_dict(self):
"""Test channel deserialization."""
data = {"channel_id": "789", "name": "chat", "channel_type": "group"}
channel = BotChannel.from_dict(data)
assert channel.channel_id == "789"
assert channel.name == "chat"
assert channel.channel_type == "group"
class TestBotMessage:
"""Tests for BotMessage."""
def test_message_creation(self):
"""Test message creation."""
msg = BotMessage(
content="Hello!",
sender=BotUser(user_id="user-1"),
channel=BotChannel(channel_id="ch-1"),
)
assert msg.content == "Hello!"
assert msg.message_id is not None
assert msg.message_type == MessageType.TEXT
def test_text_property(self):
"""Test text property."""
msg = BotMessage(content="Hello")
assert msg.text == "Hello"
msg = BotMessage(content={"text": "World"})
assert msg.text == "World"
def test_command_detection(self):
"""Test command detection."""
msg = BotMessage(content="/help")
assert msg.is_command is True
assert msg.command == "help"
msg = BotMessage(content="Hello")
assert msg.is_command is False
assert msg.command is None
def test_command_args(self):
"""Test command arguments extraction."""
msg = BotMessage(content="/search python tutorial")
assert msg.command == "search"
assert msg.command_args == ["python", "tutorial"]
msg = BotMessage(content="/help")
assert msg.command_args == []
def test_to_dict(self):
"""Test message serialization."""
msg = BotMessage(
content="Test",
sender=BotUser(user_id="user-1"),
channel=BotChannel(channel_id="ch-1"),
)
data = msg.to_dict()
assert data["content"] == "Test"
assert data["sender"]["user_id"] == "user-1"
assert data["channel"]["channel_id"] == "ch-1"
def test_from_dict(self):
"""Test message deserialization."""
data = {
"content": "Hello",
"message_type": "text",
"sender": {"user_id": "u1"},
"channel": {"channel_id": "c1"},
}
msg = BotMessage.from_dict(data)
assert msg.content == "Hello"
assert msg.sender.user_id == "u1"
assert msg.channel.channel_id == "c1"
class TestMessageType:
"""Tests for MessageType enum."""
def test_message_types(self):
"""Test message type values."""
assert MessageType.TEXT.value == "text"
assert MessageType.IMAGE.value == "image"
assert MessageType.COMMAND.value == "command"
assert MessageType.REPLY.value == "reply"
assert MessageType.EDIT.value == "edit"
@@ -0,0 +1,284 @@
"""
Unit tests for Failover components.
"""
from praisonaiagents.llm.failover import (
AuthProfile,
ProviderStatus,
FailoverConfig,
FailoverManager,
)
class TestAuthProfile:
"""Tests for AuthProfile."""
def test_profile_creation(self):
"""Test profile creation."""
profile = AuthProfile(
name="openai-primary",
provider="openai",
api_key="sk-test123",
)
assert profile.name == "openai-primary"
assert profile.provider == "openai"
assert profile.status == ProviderStatus.AVAILABLE
def test_profile_is_available(self):
"""Test availability check."""
profile = AuthProfile(name="test", provider="openai", api_key="key")
assert profile.is_available is True
profile.status = ProviderStatus.DISABLED
assert profile.is_available is False
def test_mark_rate_limited(self):
"""Test marking profile as rate limited."""
profile = AuthProfile(name="test", provider="openai", api_key="key")
profile.mark_rate_limited(cooldown_seconds=60)
assert profile.status == ProviderStatus.RATE_LIMITED
assert profile.is_available is False
assert profile.cooldown_until is not None
def test_mark_error(self):
"""Test marking profile with error."""
profile = AuthProfile(name="test", provider="openai", api_key="key")
profile.mark_error("Connection failed", cooldown_seconds=30)
assert profile.status == ProviderStatus.ERROR
assert profile.last_error == "Connection failed"
assert profile.is_available is False
def test_reset(self):
"""Test resetting profile."""
profile = AuthProfile(name="test", provider="openai", api_key="key")
profile.mark_error("Error")
profile.reset()
assert profile.status == ProviderStatus.AVAILABLE
assert profile.cooldown_until is None
assert profile.last_error is None
def test_to_dict_hides_key(self):
"""Test that to_dict hides API key."""
profile = AuthProfile(
name="test",
provider="openai",
api_key="sk-verysecretkey123",
)
data = profile.to_dict()
assert data["api_key"].startswith("***")
assert len(data["api_key"]) == 7
def test_from_dict(self):
"""Test creating profile from dict."""
data = {
"name": "test",
"provider": "anthropic",
"api_key": "key",
"model": "claude-3",
"priority": 1,
}
profile = AuthProfile.from_dict(data)
assert profile.name == "test"
assert profile.provider == "anthropic"
assert profile.model == "claude-3"
assert profile.priority == 1
class TestProviderStatus:
"""Tests for ProviderStatus enum."""
def test_status_values(self):
"""Test status values."""
assert ProviderStatus.AVAILABLE.value == "available"
assert ProviderStatus.RATE_LIMITED.value == "rate_limited"
assert ProviderStatus.ERROR.value == "error"
assert ProviderStatus.DISABLED.value == "disabled"
class TestFailoverConfig:
"""Tests for FailoverConfig."""
def test_default_config(self):
"""Test default configuration."""
config = FailoverConfig()
assert config.max_retries == 3
assert config.retry_delay == 1.0
assert config.exponential_backoff is True
def test_custom_config(self):
"""Test custom configuration."""
config = FailoverConfig(
max_retries=5,
retry_delay=2.0,
cooldown_on_rate_limit=120,
)
assert config.max_retries == 5
assert config.retry_delay == 2.0
assert config.cooldown_on_rate_limit == 120
def test_to_dict(self):
"""Test config serialization."""
config = FailoverConfig(max_retries=5)
data = config.to_dict()
assert data["max_retries"] == 5
class TestFailoverManager:
"""Tests for FailoverManager."""
def test_add_profile(self):
"""Test adding profiles."""
manager = FailoverManager()
profile = AuthProfile(name="test", provider="openai", api_key="key")
manager.add_profile(profile)
assert len(manager.list_profiles()) == 1
def test_remove_profile(self):
"""Test removing profiles."""
manager = FailoverManager()
profile = AuthProfile(name="test", provider="openai", api_key="key")
manager.add_profile(profile)
assert manager.remove_profile("test") is True
assert manager.remove_profile("nonexistent") is False
assert len(manager.list_profiles()) == 0
def test_get_profile(self):
"""Test getting profile by name."""
manager = FailoverManager()
profile = AuthProfile(name="test", provider="openai", api_key="key")
manager.add_profile(profile)
found = manager.get_profile("test")
assert found is not None
assert found.name == "test"
assert manager.get_profile("nonexistent") is None
def test_get_next_profile(self):
"""Test getting next available profile."""
manager = FailoverManager()
assert manager.get_next_profile() is None
profile1 = AuthProfile(name="p1", provider="openai", api_key="k1", priority=0)
profile2 = AuthProfile(name="p2", provider="openai", api_key="k2", priority=1)
manager.add_profile(profile2)
manager.add_profile(profile1)
next_profile = manager.get_next_profile()
assert next_profile.name == "p1"
def test_priority_ordering(self):
"""Test profiles are ordered by priority."""
manager = FailoverManager()
manager.add_profile(AuthProfile(name="low", provider="openai", api_key="k", priority=10))
manager.add_profile(AuthProfile(name="high", provider="openai", api_key="k", priority=0))
manager.add_profile(AuthProfile(name="mid", provider="openai", api_key="k", priority=5))
profiles = manager.list_profiles()
assert profiles[0].name == "high"
assert profiles[1].name == "mid"
assert profiles[2].name == "low"
def test_failover_on_rate_limit(self):
"""Test failover when rate limited."""
manager = FailoverManager()
p1 = AuthProfile(name="p1", provider="openai", api_key="k1", priority=0)
p2 = AuthProfile(name="p2", provider="openai", api_key="k2", priority=1)
manager.add_profile(p1)
manager.add_profile(p2)
manager.mark_failure(p1, "Rate limit", is_rate_limit=True)
next_profile = manager.get_next_profile()
assert next_profile.name == "p2"
def test_mark_success(self):
"""Test marking profile as successful."""
manager = FailoverManager()
profile = AuthProfile(name="test", provider="openai", api_key="key")
manager.add_profile(profile)
profile.mark_error("Error")
assert profile.status == ProviderStatus.ERROR
manager.mark_success(profile)
assert profile.status == ProviderStatus.AVAILABLE
def test_get_retry_delay(self):
"""Test retry delay calculation."""
config = FailoverConfig(retry_delay=1.0, exponential_backoff=True)
manager = FailoverManager(config=config)
assert manager.get_retry_delay(0) == 1.0
assert manager.get_retry_delay(1) == 2.0
assert manager.get_retry_delay(2) == 4.0
def test_get_retry_delay_no_backoff(self):
"""Test retry delay without exponential backoff."""
config = FailoverConfig(retry_delay=2.0, exponential_backoff=False)
manager = FailoverManager(config=config)
assert manager.get_retry_delay(0) == 2.0
assert manager.get_retry_delay(1) == 2.0
assert manager.get_retry_delay(5) == 2.0
def test_max_retry_delay(self):
"""Test max retry delay cap."""
config = FailoverConfig(retry_delay=10.0, max_retry_delay=30.0)
manager = FailoverManager(config=config)
assert manager.get_retry_delay(5) == 30.0
def test_status(self):
"""Test status report."""
manager = FailoverManager()
manager.add_profile(AuthProfile(name="p1", provider="openai", api_key="k1"))
manager.add_profile(AuthProfile(name="p2", provider="openai", api_key="k2"))
status = manager.status()
assert status["total_profiles"] == 2
assert status["available_profiles"] == 2
def test_reset_all(self):
"""Test resetting all profiles."""
manager = FailoverManager()
p1 = AuthProfile(name="p1", provider="openai", api_key="k1")
p2 = AuthProfile(name="p2", provider="openai", api_key="k2")
manager.add_profile(p1)
manager.add_profile(p2)
p1.mark_error("Error")
p2.mark_rate_limited()
manager.reset_all()
assert p1.status == ProviderStatus.AVAILABLE
assert p2.status == ProviderStatus.AVAILABLE
def test_on_failover_callback(self):
"""Test failover callback."""
manager = FailoverManager()
p1 = AuthProfile(name="p1", provider="openai", api_key="k1", priority=0)
p2 = AuthProfile(name="p2", provider="openai", api_key="k2", priority=1)
manager.add_profile(p1)
manager.add_profile(p2)
callback_called = []
def on_failover(failed, new):
callback_called.append((failed.name, new.name))
manager.on_failover(on_failover)
manager.mark_failure(p1, "Error")
assert len(callback_called) == 1
assert callback_called[0] == ("p1", "p2")
@@ -0,0 +1,191 @@
"""
Unit tests for Gateway protocols and components.
"""
from praisonaiagents.gateway import (
GatewayConfig,
SessionConfig,
GatewayEvent,
GatewayMessage,
EventType,
)
class TestGatewayConfig:
"""Tests for GatewayConfig."""
def test_default_config(self):
"""Test default configuration values."""
config = GatewayConfig()
assert config.host == "127.0.0.1"
assert config.port == 8765
assert config.max_connections == 1000
assert config.heartbeat_interval == 30
def test_custom_config(self):
"""Test custom configuration values."""
config = GatewayConfig(
host="0.0.0.0",
port=9000,
auth_token="secret",
)
assert config.host == "0.0.0.0"
assert config.port == 9000
assert config.auth_token == "secret"
def test_ws_url(self):
"""Test WebSocket URL generation."""
config = GatewayConfig(host="localhost", port=8080)
assert config.ws_url == "ws://localhost:8080"
def test_secure_ws_url(self):
"""Test secure WebSocket URL generation."""
config = GatewayConfig(
host="localhost",
port=443,
ssl_cert="/path/to/cert",
ssl_key="/path/to/key",
)
assert config.is_secure
assert config.ws_url == "wss://localhost:443"
def test_to_dict_hides_token(self):
"""Test that to_dict hides sensitive data."""
config = GatewayConfig(auth_token="my-secret-token")
data = config.to_dict()
assert data["auth_token"] == "***"
class TestSessionConfig:
"""Tests for SessionConfig."""
def test_default_session_config(self):
"""Test default session configuration."""
config = SessionConfig()
assert config.timeout == 3600
assert config.max_messages == 1000
assert config.persist is False
def test_custom_session_config(self):
"""Test custom session configuration."""
config = SessionConfig(
timeout=7200,
max_messages=500,
persist=True,
persist_path="/tmp/sessions",
)
assert config.timeout == 7200
assert config.max_messages == 500
assert config.persist is True
class TestGatewayEvent:
"""Tests for GatewayEvent."""
def test_event_creation(self):
"""Test event creation."""
event = GatewayEvent(
type=EventType.MESSAGE,
data={"content": "Hello"},
source="client-1",
)
assert event.type == EventType.MESSAGE
assert event.data["content"] == "Hello"
assert event.source == "client-1"
assert event.event_id is not None
assert event.timestamp > 0
def test_event_to_dict(self):
"""Test event serialization."""
event = GatewayEvent(
type=EventType.CONNECT,
data={"client_id": "123"},
)
data = event.to_dict()
assert data["type"] == "connect"
assert data["data"]["client_id"] == "123"
def test_event_from_dict(self):
"""Test event deserialization."""
data = {
"type": "message",
"data": {"text": "Hello"},
"source": "user-1",
}
event = GatewayEvent.from_dict(data)
assert event.type == EventType.MESSAGE
assert event.data["text"] == "Hello"
assert event.source == "user-1"
def test_custom_event_type(self):
"""Test custom event type handling."""
event = GatewayEvent(type="custom_event", data={})
assert event.type == "custom_event"
data = event.to_dict()
restored = GatewayEvent.from_dict(data)
assert restored.type == "custom_event"
class TestGatewayMessage:
"""Tests for GatewayMessage."""
def test_message_creation(self):
"""Test message creation."""
msg = GatewayMessage(
content="Hello, World!",
sender_id="user-1",
session_id="session-1",
)
assert msg.content == "Hello, World!"
assert msg.sender_id == "user-1"
assert msg.session_id == "session-1"
assert msg.message_id is not None
def test_message_to_dict(self):
"""Test message serialization."""
msg = GatewayMessage(
content="Test",
sender_id="user",
session_id="sess",
)
data = msg.to_dict()
assert data["content"] == "Test"
assert data["sender_id"] == "user"
assert data["session_id"] == "sess"
def test_message_from_dict(self):
"""Test message deserialization."""
data = {
"content": "Hello",
"sender_id": "user-1",
"session_id": "session-1",
"reply_to": "msg-0",
}
msg = GatewayMessage.from_dict(data)
assert msg.content == "Hello"
assert msg.sender_id == "user-1"
assert msg.reply_to == "msg-0"
def test_message_with_metadata(self):
"""Test message with metadata."""
msg = GatewayMessage(
content="Test",
sender_id="user",
session_id="sess",
metadata={"priority": "high"},
)
assert msg.metadata["priority"] == "high"
class TestEventType:
"""Tests for EventType enum."""
def test_event_types(self):
"""Test event type values."""
assert EventType.CONNECT.value == "connect"
assert EventType.DISCONNECT.value == "disconnect"
assert EventType.MESSAGE.value == "message"
assert EventType.SESSION_START.value == "session_start"
assert EventType.AGENT_REGISTER.value == "agent_register"
assert EventType.HEALTH.value == "health"
@@ -0,0 +1,180 @@
"""
Unit tests for Sandbox protocols and components.
"""
from praisonaiagents.sandbox import (
SandboxConfig,
SandboxResult,
SandboxStatus,
ResourceLimits,
SecurityPolicy,
)
class TestResourceLimits:
"""Tests for ResourceLimits."""
def test_default_limits(self):
"""Test default resource limits."""
limits = ResourceLimits()
assert limits.memory_mb == 512
assert limits.cpu_percent == 100
assert limits.timeout_seconds == 60
assert limits.network_enabled is False
def test_minimal_limits(self):
"""Test minimal resource limits."""
limits = ResourceLimits.minimal()
assert limits.memory_mb == 128
assert limits.timeout_seconds == 30
assert limits.network_enabled is False
def test_generous_limits(self):
"""Test generous resource limits."""
limits = ResourceLimits.generous()
assert limits.memory_mb == 2048
assert limits.timeout_seconds == 300
assert limits.network_enabled is True
def test_to_dict(self):
"""Test limits serialization."""
limits = ResourceLimits(memory_mb=256, timeout_seconds=30)
data = limits.to_dict()
assert data["memory_mb"] == 256
assert data["timeout_seconds"] == 30
def test_from_dict(self):
"""Test limits deserialization."""
data = {"memory_mb": 1024, "cpu_percent": 50, "network_enabled": True}
limits = ResourceLimits.from_dict(data)
assert limits.memory_mb == 1024
assert limits.cpu_percent == 50
assert limits.network_enabled is True
class TestSecurityPolicy:
"""Tests for SecurityPolicy."""
def test_default_policy(self):
"""Test default security policy."""
policy = SecurityPolicy()
assert policy.allow_network is False
assert policy.allow_file_write is True
assert policy.allow_subprocess is False
def test_strict_policy(self):
"""Test strict security policy."""
policy = SecurityPolicy.strict()
assert policy.allow_network is False
assert policy.allow_file_write is False
assert policy.allow_subprocess is False
def test_permissive_policy(self):
"""Test permissive security policy."""
policy = SecurityPolicy.permissive()
assert policy.allow_network is True
assert policy.allow_file_write is True
assert policy.allow_subprocess is True
def test_blocked_paths(self):
"""Test blocked paths in policy."""
policy = SecurityPolicy()
assert "/etc/passwd" in policy.blocked_paths
assert "~/.ssh" in policy.blocked_paths
class TestSandboxResult:
"""Tests for SandboxResult."""
def test_result_creation(self):
"""Test result creation."""
result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout="Hello, World!",
)
assert result.status == SandboxStatus.COMPLETED
assert result.exit_code == 0
assert result.stdout == "Hello, World!"
def test_success_property(self):
"""Test success property."""
result = SandboxResult(status=SandboxStatus.COMPLETED, exit_code=0)
assert result.success is True
result = SandboxResult(status=SandboxStatus.COMPLETED, exit_code=1)
assert result.success is False
result = SandboxResult(status=SandboxStatus.TIMEOUT)
assert result.success is False
def test_output_property(self):
"""Test combined output property."""
result = SandboxResult(stdout="out", stderr="err")
output = result.output
assert "out" in output
assert "err" in output
def test_to_dict(self):
"""Test result serialization."""
result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout="test",
)
data = result.to_dict()
assert data["status"] == "completed"
assert data["exit_code"] == 0
assert data["stdout"] == "test"
def test_from_dict(self):
"""Test result deserialization."""
data = {
"status": "timeout",
"error": "Timed out",
"duration_seconds": 60.0,
}
result = SandboxResult.from_dict(data)
assert result.status == SandboxStatus.TIMEOUT
assert result.error == "Timed out"
class TestSandboxStatus:
"""Tests for SandboxStatus enum."""
def test_status_values(self):
"""Test status values."""
assert SandboxStatus.PENDING.value == "pending"
assert SandboxStatus.RUNNING.value == "running"
assert SandboxStatus.COMPLETED.value == "completed"
assert SandboxStatus.FAILED.value == "failed"
assert SandboxStatus.TIMEOUT.value == "timeout"
assert SandboxStatus.KILLED.value == "killed"
class TestSandboxConfig:
"""Tests for SandboxConfig."""
def test_default_config(self):
"""Test default sandbox configuration."""
config = SandboxConfig()
assert config.sandbox_type == "subprocess"
assert config.auto_cleanup is True
def test_docker_config(self):
"""Test Docker sandbox configuration."""
config = SandboxConfig.docker("python:3.11")
assert config.sandbox_type == "docker"
assert config.image == "python:3.11"
def test_subprocess_config(self):
"""Test subprocess sandbox configuration."""
config = SandboxConfig.subprocess()
assert config.sandbox_type == "subprocess"
def test_to_dict_hides_secrets(self):
"""Test that to_dict hides sensitive env vars."""
config = SandboxConfig(env={"API_KEY": "secret", "NAME": "test"})
data = config.to_dict()
assert data["env"]["API_KEY"] == "***"
assert data["env"]["NAME"] == "test"
+1 -1
View File
@@ -2549,7 +2549,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.13.23"
version = "0.14.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.10.27.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.10.27.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.11.0.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.11.0.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+11 -1
View File
@@ -219,8 +219,18 @@ class ApiMdGenerator:
dict_sources = self._parse_lazy_imports_dict(tree)
import_sources.update(dict_sources)
# Also include symbols from SECTION_GROUPS that exist in lazy imports
# This ensures lazy-loaded classes like Workflow appear in api.md
# even if they're not in __all__ (which is kept minimal for IDE experience)
section_symbols = set()
for symbol_list in self.SECTION_GROUPS.values():
section_symbols.update(symbol_list)
# Merge: __all__ exports + section group symbols that exist in lazy imports
exports_to_process = all_exports | (section_symbols & set(import_sources.keys()))
# For each export, find its definition
for export_name in sorted(all_exports):
for export_name in sorted(exports_to_process):
if export_name in import_sources:
module_path, original_name = import_sources[export_name]
# Resolve to file
+27
View File
@@ -0,0 +1,27 @@
"""
Bot implementations for PraisonAI.
Provides messaging bot runtimes for Telegram, Discord, and Slack.
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .telegram import TelegramBot
from .discord import DiscordBot
from .slack import SlackBot
def __getattr__(name: str):
"""Lazy loading of bot components."""
if name == "TelegramBot":
from .telegram import TelegramBot
return TelegramBot
if name == "DiscordBot":
from .discord import DiscordBot
return DiscordBot
if name == "SlackBot":
from .slack import SlackBot
return SlackBot
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["TelegramBot", "DiscordBot", "SlackBot"]
+378
View File
@@ -0,0 +1,378 @@
"""
Discord Bot implementation for PraisonAI.
Provides a full Discord bot runtime with slash commands,
message handling, and agent integration.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING:
from praisonaiagents import Agent
from praisonaiagents.bots import (
BotConfig,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
logger = logging.getLogger(__name__)
class DiscordBot:
"""Discord bot runtime for PraisonAI agents.
Connects an agent to Discord, handling messages, slash commands,
and providing full bot functionality.
Example:
from praisonai.bots import DiscordBot
from praisonaiagents import Agent
agent = Agent(name="assistant")
bot = DiscordBot(token="YOUR_BOT_TOKEN", agent=agent)
@bot.on_command("help")
async def help_command(message):
await bot.send_message(message.channel.channel_id, "Help text...")
await bot.start()
Requires: pip install discord.py
"""
def __init__(
self,
token: str,
agent: Optional["Agent"] = None,
config: Optional[BotConfig] = None,
):
"""Initialize the Discord bot.
Args:
token: Discord bot token
agent: Optional agent to handle messages
config: Optional bot configuration
"""
self._token = token
self._agent = agent
self.config = config or BotConfig(token=token)
self._is_running = False
self._bot_user: Optional[BotUser] = None
self._client = None
self._message_handlers: List[Callable] = []
self._command_handlers: Dict[str, Callable] = {}
@property
def is_running(self) -> bool:
return self._is_running
@property
def platform(self) -> str:
return "discord"
@property
def bot_user(self) -> Optional[BotUser]:
return self._bot_user
async def start(self) -> None:
"""Start the Discord bot."""
if self._is_running:
logger.warning("Bot already running")
return
try:
import discord
from discord.ext import commands
except ImportError:
raise ImportError(
"DiscordBot requires discord.py. "
"Install with: pip install discord.py"
)
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
self._client = commands.Bot(
command_prefix=self.config.command_prefix,
intents=intents,
)
@self._client.event
async def on_ready():
self._bot_user = BotUser(
user_id=str(self._client.user.id),
username=self._client.user.name,
display_name=self._client.user.display_name,
is_bot=True,
)
self._is_running = True
logger.info(f"Discord bot started: {self._client.user.name}")
@self._client.event
async def on_message(message):
if message.author.bot:
return
bot_message = self._convert_message(message)
if not self.config.is_user_allowed(bot_message.sender.user_id if bot_message.sender else ""):
return
if not self.config.is_channel_allowed(bot_message.channel.channel_id if bot_message.channel else ""):
return
if bot_message.is_command:
command = bot_message.command
if command and command in self._command_handlers:
handler = self._command_handlers[command]
try:
if asyncio.iscoroutinefunction(handler):
await handler(bot_message)
else:
handler(bot_message)
except Exception as e:
logger.error(f"Command handler error: {e}")
return
should_respond = False
if message.guild is None:
should_respond = True
elif self._client.user.mentioned_in(message) or not self.config.mention_required:
should_respond = True
if should_respond:
for handler in self._message_handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(bot_message)
else:
handler(bot_message)
except Exception as e:
logger.error(f"Message handler error: {e}")
if self._agent:
if self.config.typing_indicator:
async with message.channel.typing():
try:
response = self._agent.chat(bot_message.text)
await self._send_long_message(message.channel, response, reference=message)
except Exception as e:
logger.error(f"Agent error: {e}")
await message.reply(f"Error: {str(e)}")
else:
try:
response = self._agent.chat(bot_message.text)
await self._send_long_message(message.channel, response, reference=message)
except Exception as e:
logger.error(f"Agent error: {e}")
await message.reply(f"Error: {str(e)}")
await self._client.start(self._token)
async def stop(self) -> None:
"""Stop the Discord bot."""
if not self._is_running:
return
self._is_running = False
if self._client:
await self._client.close()
logger.info("Discord bot stopped")
def set_agent(self, agent: "Agent") -> None:
"""Set the agent that handles messages."""
self._agent = agent
def get_agent(self) -> Optional["Agent"]:
"""Get the current agent."""
return self._agent
async def send_message(
self,
channel_id: str,
content: Union[str, Dict[str, Any]],
reply_to: Optional[str] = None,
thread_id: Optional[str] = None,
) -> BotMessage:
"""Send a message to a channel."""
if not self._client:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
channel = self._client.get_channel(int(channel_id))
if not channel:
raise ValueError(f"Channel not found: {channel_id}")
if thread_id:
thread = channel.get_thread(int(thread_id))
if thread:
channel = thread
sent = await channel.send(text)
return BotMessage(
message_id=str(sent.id),
content=text,
message_type=MessageType.TEXT,
channel=BotChannel(channel_id=channel_id),
)
async def _send_long_message(self, channel, text: str, reference=None) -> None:
"""Send a long message, splitting if necessary."""
max_len = min(self.config.max_message_length, 2000)
if len(text) <= max_len:
if reference:
await channel.send(text, reference=reference)
else:
await channel.send(text)
else:
chunks = [text[i:i+max_len] for i in range(0, len(text), max_len)]
for i, chunk in enumerate(chunks):
if i == 0 and reference:
await channel.send(chunk, reference=reference)
else:
await channel.send(chunk)
async def edit_message(
self,
channel_id: str,
message_id: str,
content: Union[str, Dict[str, Any]],
) -> BotMessage:
"""Edit an existing message."""
if not self._client:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
channel = self._client.get_channel(int(channel_id))
if not channel:
raise ValueError(f"Channel not found: {channel_id}")
message = await channel.fetch_message(int(message_id))
await message.edit(content=text)
return BotMessage(
message_id=message_id,
content=text,
message_type=MessageType.EDIT,
channel=BotChannel(channel_id=channel_id),
)
async def delete_message(
self,
channel_id: str,
message_id: str,
) -> bool:
"""Delete a message."""
if not self._client:
raise RuntimeError("Bot not started")
try:
channel = self._client.get_channel(int(channel_id))
if channel:
message = await channel.fetch_message(int(message_id))
await message.delete()
return True
except Exception as e:
logger.error(f"Delete message error: {e}")
return False
def on_message(self, handler: Callable[[BotMessage], Any]) -> Callable:
"""Register a message handler."""
self._message_handlers.append(handler)
return handler
def on_command(self, command: str) -> Callable:
"""Decorator to register a command handler."""
def decorator(func: Callable) -> Callable:
self._command_handlers[command] = func
return func
return decorator
async def send_typing(self, channel_id: str) -> None:
"""Send typing indicator."""
if self._client:
channel = self._client.get_channel(int(channel_id))
if channel:
await channel.trigger_typing()
async def get_user(self, user_id: str) -> Optional[BotUser]:
"""Get user information."""
if not self._client:
return None
try:
user = await self._client.fetch_user(int(user_id))
return BotUser(
user_id=str(user.id),
username=user.name,
display_name=user.display_name,
is_bot=user.bot,
)
except Exception:
return None
async def get_channel(self, channel_id: str) -> Optional[BotChannel]:
"""Get channel information."""
if not self._client:
return None
try:
channel = self._client.get_channel(int(channel_id))
if channel:
channel_type = "dm" if hasattr(channel, "recipient") else "channel"
return BotChannel(
channel_id=str(channel.id),
name=getattr(channel, "name", None),
channel_type=channel_type,
)
except Exception:
pass
return None
def _convert_message(self, message) -> BotMessage:
"""Convert Discord Message to BotMessage."""
sender = BotUser(
user_id=str(message.author.id),
username=message.author.name,
display_name=message.author.display_name,
is_bot=message.author.bot,
)
channel_type = "dm" if message.guild is None else "channel"
channel = BotChannel(
channel_id=str(message.channel.id),
name=getattr(message.channel, "name", None),
channel_type=channel_type,
)
content = message.content
if self._client and self._client.user:
content = content.replace(f"<@{self._client.user.id}>", "").strip()
content = content.replace(f"<@!{self._client.user.id}>", "").strip()
msg_type = MessageType.COMMAND if content.startswith(self.config.command_prefix) else MessageType.TEXT
return BotMessage(
message_id=str(message.id),
content=content,
message_type=msg_type,
sender=sender,
channel=channel,
timestamp=message.created_at.timestamp() if message.created_at else 0,
thread_id=str(message.thread.id) if hasattr(message, "thread") and message.thread else None,
)
+391
View File
@@ -0,0 +1,391 @@
"""
Slack Bot implementation for PraisonAI.
Provides a full Slack bot runtime with slash commands,
event handling, and agent integration.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING:
from praisonaiagents import Agent
from praisonaiagents.bots import (
BotConfig,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
logger = logging.getLogger(__name__)
class SlackBot:
"""Slack bot runtime for PraisonAI agents.
Connects an agent to Slack, handling messages, slash commands,
and providing full bot functionality.
Example:
from praisonai.bots import SlackBot
from praisonaiagents import Agent
agent = Agent(name="assistant")
bot = SlackBot(
token="xoxb-YOUR-BOT-TOKEN",
app_token="xapp-YOUR-APP-TOKEN",
agent=agent,
)
@bot.on_command("help")
async def help_command(message):
await bot.send_message(message.channel.channel_id, "Help text...")
await bot.start()
Requires: pip install slack-bolt
"""
def __init__(
self,
token: str,
app_token: Optional[str] = None,
signing_secret: Optional[str] = None,
agent: Optional["Agent"] = None,
config: Optional[BotConfig] = None,
):
"""Initialize the Slack bot.
Args:
token: Slack bot token (xoxb-...)
app_token: Slack app token for Socket Mode (xapp-...)
signing_secret: Signing secret for webhook verification
agent: Optional agent to handle messages
config: Optional bot configuration
"""
self._token = token
self._app_token = app_token
self._signing_secret = signing_secret
self._agent = agent
self.config = config or BotConfig(token=token)
self._is_running = False
self._bot_user: Optional[BotUser] = None
self._app = None
self._client = None
self._message_handlers: List[Callable] = []
self._command_handlers: Dict[str, Callable] = {}
@property
def is_running(self) -> bool:
return self._is_running
@property
def platform(self) -> str:
return "slack"
@property
def bot_user(self) -> Optional[BotUser]:
return self._bot_user
async def start(self) -> None:
"""Start the Slack bot."""
if self._is_running:
logger.warning("Bot already running")
return
try:
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
from slack_sdk.web.async_client import AsyncWebClient
except ImportError:
raise ImportError(
"SlackBot requires slack-bolt. "
"Install with: pip install slack-bolt"
)
self._app = AsyncApp(
token=self._token,
signing_secret=self._signing_secret,
)
self._client = AsyncWebClient(token=self._token)
auth_response = await self._client.auth_test()
self._bot_user = BotUser(
user_id=auth_response["user_id"],
username=auth_response["user"],
display_name=auth_response.get("bot_id", auth_response["user"]),
is_bot=True,
)
@self._app.event("message")
async def handle_message(event, say):
if event.get("bot_id"):
return
bot_message = self._convert_event_to_message(event)
if not self.config.is_user_allowed(bot_message.sender.user_id if bot_message.sender else ""):
return
if not self.config.is_channel_allowed(bot_message.channel.channel_id if bot_message.channel else ""):
return
for handler in self._message_handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(bot_message)
else:
handler(bot_message)
except Exception as e:
logger.error(f"Message handler error: {e}")
should_respond = False
text = event.get("text", "")
channel_type = event.get("channel_type", "")
if channel_type == "im":
should_respond = True
elif self._bot_user and f"<@{self._bot_user.user_id}>" in text:
should_respond = True
text = text.replace(f"<@{self._bot_user.user_id}>", "").strip()
elif not self.config.mention_required:
should_respond = True
if should_respond and self._agent:
try:
response = self._agent.chat(text)
await self._send_long_message(say, response, thread_ts=event.get("thread_ts") or event.get("ts"))
except Exception as e:
logger.error(f"Agent error: {e}")
await say(f"Error: {str(e)}")
@self._app.event("app_mention")
async def handle_mention(event, say):
if event.get("bot_id"):
return
text = event.get("text", "")
if self._bot_user:
text = text.replace(f"<@{self._bot_user.user_id}>", "").strip()
if self._agent:
try:
response = self._agent.chat(text)
await self._send_long_message(say, response, thread_ts=event.get("thread_ts") or event.get("ts"))
except Exception as e:
logger.error(f"Agent error: {e}")
await say(f"Error: {str(e)}")
for command, handler in self._command_handlers.items():
@self._app.command(f"/{command}")
async def handle_command(ack, command_data, respond, cmd=command, hdlr=handler):
await ack()
bot_message = BotMessage(
message_id=command_data.get("trigger_id", ""),
content=command_data.get("text", ""),
message_type=MessageType.COMMAND,
sender=BotUser(
user_id=command_data.get("user_id", ""),
username=command_data.get("user_name", ""),
),
channel=BotChannel(
channel_id=command_data.get("channel_id", ""),
name=command_data.get("channel_name", ""),
),
)
try:
if asyncio.iscoroutinefunction(hdlr):
await hdlr(bot_message)
else:
hdlr(bot_message)
except Exception as e:
logger.error(f"Command handler error: {e}")
await respond(f"Error: {str(e)}")
self._is_running = True
logger.info(f"Slack bot started: {self._bot_user.username}")
if self._app_token:
handler = AsyncSocketModeHandler(self._app, self._app_token)
await handler.start_async()
else:
logger.warning("No app_token provided, bot will not receive events via Socket Mode")
async def stop(self) -> None:
"""Stop the Slack bot."""
if not self._is_running:
return
self._is_running = False
logger.info("Slack bot stopped")
def set_agent(self, agent: "Agent") -> None:
"""Set the agent that handles messages."""
self._agent = agent
def get_agent(self) -> Optional["Agent"]:
"""Get the current agent."""
return self._agent
async def send_message(
self,
channel_id: str,
content: Union[str, Dict[str, Any]],
reply_to: Optional[str] = None,
thread_id: Optional[str] = None,
) -> BotMessage:
"""Send a message to a channel."""
if not self._client:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
kwargs = {"channel": channel_id, "text": text}
if thread_id:
kwargs["thread_ts"] = thread_id
response = await self._client.chat_postMessage(**kwargs)
return BotMessage(
message_id=response["ts"],
content=text,
message_type=MessageType.TEXT,
channel=BotChannel(channel_id=channel_id),
)
async def _send_long_message(self, say, text: str, thread_ts: Optional[str] = None) -> None:
"""Send a long message, splitting if necessary."""
max_len = min(self.config.max_message_length, 4000)
if len(text) <= max_len:
await say(text=text, thread_ts=thread_ts)
else:
chunks = [text[i:i+max_len] for i in range(0, len(text), max_len)]
for chunk in chunks:
await say(text=chunk, thread_ts=thread_ts)
async def edit_message(
self,
channel_id: str,
message_id: str,
content: Union[str, Dict[str, Any]],
) -> BotMessage:
"""Edit an existing message."""
if not self._client:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
await self._client.chat_update(
channel=channel_id,
ts=message_id,
text=text,
)
return BotMessage(
message_id=message_id,
content=text,
message_type=MessageType.EDIT,
channel=BotChannel(channel_id=channel_id),
)
async def delete_message(
self,
channel_id: str,
message_id: str,
) -> bool:
"""Delete a message."""
if not self._client:
raise RuntimeError("Bot not started")
try:
await self._client.chat_delete(
channel=channel_id,
ts=message_id,
)
return True
except Exception as e:
logger.error(f"Delete message error: {e}")
return False
def on_message(self, handler: Callable[[BotMessage], Any]) -> Callable:
"""Register a message handler."""
self._message_handlers.append(handler)
return handler
def on_command(self, command: str) -> Callable:
"""Decorator to register a command handler."""
def decorator(func: Callable) -> Callable:
self._command_handlers[command] = func
return func
return decorator
async def send_typing(self, channel_id: str) -> None:
"""Send typing indicator (not supported in Slack API)."""
pass
async def get_user(self, user_id: str) -> Optional[BotUser]:
"""Get user information."""
if not self._client:
return None
try:
response = await self._client.users_info(user=user_id)
user = response["user"]
return BotUser(
user_id=user["id"],
username=user.get("name"),
display_name=user.get("real_name") or user.get("name"),
is_bot=user.get("is_bot", False),
)
except Exception:
return None
async def get_channel(self, channel_id: str) -> Optional[BotChannel]:
"""Get channel information."""
if not self._client:
return None
try:
response = await self._client.conversations_info(channel=channel_id)
channel = response["channel"]
channel_type = "dm" if channel.get("is_im") else "channel"
return BotChannel(
channel_id=channel["id"],
name=channel.get("name"),
channel_type=channel_type,
)
except Exception:
return None
def _convert_event_to_message(self, event: Dict[str, Any]) -> BotMessage:
"""Convert Slack event to BotMessage."""
sender = BotUser(
user_id=event.get("user", ""),
username=event.get("user", ""),
)
channel_type = "dm" if event.get("channel_type") == "im" else "channel"
channel = BotChannel(
channel_id=event.get("channel", ""),
channel_type=channel_type,
)
text = event.get("text", "")
msg_type = MessageType.TEXT
return BotMessage(
message_id=event.get("ts", ""),
content=text,
message_type=msg_type,
sender=sender,
channel=channel,
timestamp=float(event.get("ts", "0").split(".")[0]) if event.get("ts") else 0,
thread_id=event.get("thread_ts"),
)
+386
View File
@@ -0,0 +1,386 @@
"""
Telegram Bot implementation for PraisonAI.
Provides a full Telegram bot runtime with webhook/polling support,
command handling, and agent integration.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING:
from praisonaiagents import Agent
from praisonaiagents.bots import (
BotConfig,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
logger = logging.getLogger(__name__)
class TelegramBot:
"""Telegram bot runtime for PraisonAI agents.
Connects an agent to Telegram, handling messages, commands,
and providing full bot functionality.
Example:
from praisonai.bots import TelegramBot
from praisonaiagents import Agent
agent = Agent(name="assistant")
bot = TelegramBot(token="YOUR_BOT_TOKEN", agent=agent)
@bot.on_command("help")
async def help_command(message):
await bot.send_message(message.channel.channel_id, "Help text...")
await bot.start()
Requires: pip install python-telegram-bot
"""
def __init__(
self,
token: str,
agent: Optional["Agent"] = None,
config: Optional[BotConfig] = None,
):
"""Initialize the Telegram bot.
Args:
token: Telegram bot token from @BotFather
agent: Optional agent to handle messages
config: Optional bot configuration
"""
self._token = token
self._agent = agent
self.config = config or BotConfig(token=token)
self._is_running = False
self._bot_user: Optional[BotUser] = None
self._application = None
self._message_handlers: List[Callable] = []
self._command_handlers: Dict[str, Callable] = {}
@property
def is_running(self) -> bool:
return self._is_running
@property
def platform(self) -> str:
return "telegram"
@property
def bot_user(self) -> Optional[BotUser]:
return self._bot_user
async def start(self) -> None:
"""Start the Telegram bot."""
if self._is_running:
logger.warning("Bot already running")
return
try:
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
except ImportError:
raise ImportError(
"TelegramBot requires python-telegram-bot. "
"Install with: pip install python-telegram-bot"
)
self._application = Application.builder().token(self._token).build()
bot_info = await self._application.bot.get_me()
self._bot_user = BotUser(
user_id=str(bot_info.id),
username=bot_info.username,
display_name=bot_info.first_name,
is_bot=True,
)
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message or not update.message.text:
return
message = self._convert_update_to_message(update)
if not self.config.is_user_allowed(message.sender.user_id if message.sender else ""):
return
if not self.config.is_channel_allowed(message.channel.channel_id if message.channel else ""):
return
for handler in self._message_handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(message)
else:
handler(message)
except Exception as e:
logger.error(f"Message handler error: {e}")
if self._agent and not message.is_command:
if self.config.typing_indicator:
await update.message.chat.send_action("typing")
try:
response = self._agent.chat(message.text)
await self._send_long_message(
update.message.chat_id,
response,
reply_to=update.message.message_id,
)
except Exception as e:
logger.error(f"Agent error: {e}")
await update.message.reply_text(f"Error: {str(e)}")
async def handle_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message or not update.message.text:
return
message = self._convert_update_to_message(update)
command = message.command
if command and command in self._command_handlers:
handler = self._command_handlers[command]
try:
if asyncio.iscoroutinefunction(handler):
await handler(message)
else:
handler(message)
except Exception as e:
logger.error(f"Command handler error: {e}")
for command in self._command_handlers:
self._application.add_handler(CommandHandler(command, handle_command))
self._application.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
)
self._is_running = True
logger.info(f"Telegram bot started: @{self._bot_user.username}")
if self.config.is_webhook_mode:
await self._application.run_webhook(
listen="0.0.0.0",
port=8443,
url_path=self.config.webhook_path,
webhook_url=f"{self.config.webhook_url}{self.config.webhook_path}",
)
else:
await self._application.run_polling(
poll_interval=self.config.polling_interval,
)
async def stop(self) -> None:
"""Stop the Telegram bot."""
if not self._is_running:
return
self._is_running = False
if self._application:
await self._application.stop()
await self._application.shutdown()
logger.info("Telegram bot stopped")
def set_agent(self, agent: "Agent") -> None:
"""Set the agent that handles messages."""
self._agent = agent
def get_agent(self) -> Optional["Agent"]:
"""Get the current agent."""
return self._agent
async def send_message(
self,
channel_id: str,
content: Union[str, Dict[str, Any]],
reply_to: Optional[str] = None,
thread_id: Optional[str] = None,
) -> BotMessage:
"""Send a message to a channel."""
if not self._application:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
kwargs = {"chat_id": int(channel_id), "text": text}
if reply_to:
kwargs["reply_to_message_id"] = int(reply_to)
if thread_id:
kwargs["message_thread_id"] = int(thread_id)
sent = await self._application.bot.send_message(**kwargs)
return BotMessage(
message_id=str(sent.message_id),
content=text,
message_type=MessageType.TEXT,
channel=BotChannel(channel_id=channel_id),
)
async def _send_long_message(
self,
chat_id: int,
text: str,
reply_to: Optional[int] = None,
) -> None:
"""Send a long message, splitting if necessary."""
max_len = self.config.max_message_length
if len(text) <= max_len:
kwargs = {"chat_id": chat_id, "text": text}
if reply_to:
kwargs["reply_to_message_id"] = reply_to
await self._application.bot.send_message(**kwargs)
else:
chunks = [text[i:i+max_len] for i in range(0, len(text), max_len)]
for i, chunk in enumerate(chunks):
kwargs = {"chat_id": chat_id, "text": chunk}
if i == 0 and reply_to:
kwargs["reply_to_message_id"] = reply_to
await self._application.bot.send_message(**kwargs)
async def edit_message(
self,
channel_id: str,
message_id: str,
content: Union[str, Dict[str, Any]],
) -> BotMessage:
"""Edit an existing message."""
if not self._application:
raise RuntimeError("Bot not started")
text = content if isinstance(content, str) else str(content)
await self._application.bot.edit_message_text(
chat_id=int(channel_id),
message_id=int(message_id),
text=text,
)
return BotMessage(
message_id=message_id,
content=text,
message_type=MessageType.EDIT,
channel=BotChannel(channel_id=channel_id),
)
async def delete_message(
self,
channel_id: str,
message_id: str,
) -> bool:
"""Delete a message."""
if not self._application:
raise RuntimeError("Bot not started")
try:
await self._application.bot.delete_message(
chat_id=int(channel_id),
message_id=int(message_id),
)
return True
except Exception as e:
logger.error(f"Delete message error: {e}")
return False
def on_message(self, handler: Callable[[BotMessage], Any]) -> Callable:
"""Register a message handler."""
self._message_handlers.append(handler)
return handler
def on_command(self, command: str) -> Callable:
"""Decorator to register a command handler."""
def decorator(func: Callable) -> Callable:
self._command_handlers[command] = func
return func
return decorator
async def send_typing(self, channel_id: str) -> None:
"""Send typing indicator."""
if self._application:
await self._application.bot.send_chat_action(
chat_id=int(channel_id),
action="typing",
)
async def get_user(self, user_id: str) -> Optional[BotUser]:
"""Get user information."""
if not self._application:
return None
try:
chat = await self._application.bot.get_chat(int(user_id))
return BotUser(
user_id=str(chat.id),
username=chat.username,
display_name=chat.first_name,
is_bot=False,
)
except Exception:
return None
async def get_channel(self, channel_id: str) -> Optional[BotChannel]:
"""Get channel information."""
if not self._application:
return None
try:
chat = await self._application.bot.get_chat(int(channel_id))
channel_type = "dm" if chat.type == "private" else chat.type
return BotChannel(
channel_id=str(chat.id),
name=chat.title or chat.username,
channel_type=channel_type,
)
except Exception:
return None
def _convert_update_to_message(self, update) -> BotMessage:
"""Convert Telegram Update to BotMessage."""
msg = update.message
sender = BotUser(
user_id=str(msg.from_user.id),
username=msg.from_user.username,
display_name=msg.from_user.first_name,
is_bot=msg.from_user.is_bot,
) if msg.from_user else None
channel_type = "dm" if msg.chat.type == "private" else msg.chat.type
channel = BotChannel(
channel_id=str(msg.chat.id),
name=msg.chat.title or msg.chat.username,
channel_type=channel_type,
)
msg_type = MessageType.COMMAND if msg.text and msg.text.startswith("/") else MessageType.TEXT
return BotMessage(
message_id=str(msg.message_id),
content=msg.text or "",
message_type=msg_type,
sender=sender,
channel=channel,
timestamp=msg.date.timestamp() if msg.date else 0,
thread_id=str(msg.message_thread_id) if msg.message_thread_id else None,
)
+2
View File
@@ -217,6 +217,7 @@ def register_commands():
from .commands.examples import app as examples_app
from .commands.batch import app as batch_app
from .commands.replay import app as replay_app
from .commands.loop import app as loop_app
# Import TUI and queue commands
from .features.tui.debug import create_debug_app as create_tui_debug_app
@@ -276,6 +277,7 @@ def register_commands():
app.add_typer(examples_app, name="examples", help="Run and manage example files")
app.add_typer(batch_app, name="batch", help="Run all PraisonAI scripts in current folder")
app.add_typer(replay_app, name="replay", help="Context replay for debugging agent execution")
app.add_typer(loop_app, name="loop", help="Autonomous agent execution loops")
# Register standardise command
try:
@@ -0,0 +1,200 @@
"""
Loop command for autonomous agent execution.
Provides CLI interface for running agents in autonomous loops with:
- Completion promise detection
- Context clearing between iterations
- Iteration limits and timeouts
"""
from typing import Optional
import typer
app = typer.Typer(
name="loop",
help="Run agents in autonomous execution loops",
no_args_is_help=False,
)
@app.callback(invoke_without_command=True)
def loop_main(
ctx: typer.Context,
prompt: Optional[str] = typer.Argument(None, help="Task prompt for the agent"),
max_iterations: int = typer.Option(10, "--max-iterations", "-n", help="Maximum iterations (default: 10)"),
completion_promise: Optional[str] = typer.Option(None, "--completion-promise", "-p", help="Promise text to signal completion"),
clear_context: bool = typer.Option(False, "--clear-context", "-c", help="Clear chat history between iterations"),
timeout: Optional[float] = typer.Option(None, "--timeout", "-t", help="Timeout in seconds"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show verbose output"),
):
"""Run an agent in an autonomous loop.
The agent will execute the given PROMPT repeatedly until:
- It outputs <promise>TEXT</promise> matching --completion-promise
- It reaches --max-iterations
- It times out (if --timeout is set)
- It detects a doom loop (repeated actions)
Examples:
praisonai loop "Build a REST API" -n 5
praisonai loop "Refactor auth module" -p DONE
praisonai loop "Implement feature X" -p COMPLETE -c
"""
if ctx.invoked_subcommand is not None:
return
if not prompt:
typer.echo(ctx.get_help())
return
_run_loop(
prompt=prompt,
max_iterations=max_iterations,
completion_promise=completion_promise,
clear_context=clear_context,
timeout=timeout,
model=model,
verbose=verbose,
)
def _run_loop(
prompt: str,
max_iterations: int,
completion_promise: Optional[str],
clear_context: bool,
timeout: Optional[float],
model: Optional[str],
verbose: bool,
):
"""Execute the autonomous loop."""
try:
from praisonaiagents import Agent
except ImportError:
typer.echo("Error: praisonaiagents not installed. Run: pip install praisonaiagents", err=True)
raise typer.Exit(1)
# Build autonomy config
autonomy_config = {
"max_iterations": max_iterations,
"completion_promise": completion_promise,
"clear_context": clear_context,
}
# Create agent
agent_kwargs = {
"name": "loop_agent",
"instructions": "You are an autonomous agent. Complete the given task. "
"When you have fully completed the task, signal completion.",
"autonomy": autonomy_config,
}
if model:
agent_kwargs["llm"] = model
if not verbose:
agent_kwargs["output"] = "silent"
# Add completion promise instructions if provided
if completion_promise:
agent_kwargs["instructions"] += (
f"\n\nIMPORTANT: When you have completed the task, you MUST include "
f"<promise>{completion_promise}</promise> in your response to signal completion."
)
agent = Agent(**agent_kwargs)
# Show configuration
if verbose:
typer.echo("🔄 Starting autonomous loop")
typer.echo(f" Prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}")
typer.echo(f" Max iterations: {max_iterations}")
if completion_promise:
typer.echo(f" Completion promise: <promise>{completion_promise}</promise>")
if clear_context:
typer.echo(" Context clearing: enabled")
if timeout:
typer.echo(f" Timeout: {timeout}s")
typer.echo("")
# Run autonomous loop
try:
result = agent.run_autonomous(
prompt=prompt,
max_iterations=max_iterations,
timeout_seconds=timeout,
completion_promise=completion_promise,
clear_context=clear_context,
)
# Display result
if result.success:
typer.echo("✅ Task completed successfully")
typer.echo(f" Reason: {result.completion_reason}")
typer.echo(f" Iterations: {result.iterations}")
typer.echo(f" Duration: {result.duration_seconds:.2f}s")
if verbose:
typer.echo(f"\n📝 Final output:\n{result.output}")
else:
typer.echo("❌ Task did not complete")
typer.echo(f" Reason: {result.completion_reason}")
typer.echo(f" Iterations: {result.iterations}")
typer.echo(f" Duration: {result.duration_seconds:.2f}s")
if result.error:
typer.echo(f" Error: {result.error}")
raise typer.Exit(1)
except KeyboardInterrupt:
typer.echo("\n⚠️ Loop cancelled by user")
raise typer.Exit(130)
except typer.Exit:
raise
except Exception as e:
typer.echo(f"❌ Error: {e}", err=True)
raise typer.Exit(1)
@app.command(name="help")
def loop_help():
"""Show detailed help for the loop command."""
typer.echo("""
🔄 Autonomous Loop Command
The loop command runs an agent in an autonomous execution loop,
similar to Ralph's self-referential development loop.
FEATURES:
• Completion Promise: Agent signals "done" with <promise>TEXT</promise>
• Context Clearing: Fresh memory each iteration (file-based state)
• Iteration Limits: Prevent runaway loops
• Doom Loop Detection: Stops on repeated actions
USAGE:
praisonai loop "Your task here" [OPTIONS]
OPTIONS:
-n, --max-iterations INT Maximum iterations (default: 10)
-p, --completion-promise STR Promise text to signal completion
-c, --clear-context Clear chat history between iterations
-t, --timeout FLOAT Timeout in seconds
-m, --model STR LLM model to use
-v, --verbose Show verbose output
EXAMPLES:
# Basic loop
praisonai loop "Build a calculator" -n 5
# With completion promise
praisonai loop "Refactor the auth module" -p DONE -v
# With context clearing (forces file-based state)
praisonai loop "Implement feature X" -p COMPLETE -c -n 20
# With timeout
praisonai loop "Debug the issue" -t 300 -v
""")
@@ -0,0 +1,270 @@
"""
Security Audit CLI for PraisonAI.
Provides security scanning and audit capabilities.
"""
from __future__ import annotations
import os
import logging
from typing import Dict, Any, List
logger = logging.getLogger(__name__)
class SecurityAudit:
"""Security audit scanner for PraisonAI projects."""
def __init__(self):
self.findings: List[Dict[str, Any]] = []
def scan(self, path: str = ".") -> Dict[str, Any]:
"""Scan a project for security issues.
Args:
path: Path to scan
Returns:
Audit results
"""
self.findings = []
self._check_env_files(path)
self._check_api_keys_in_code(path)
self._check_yaml_configs(path)
self._check_dependencies(path)
self._check_permissions(path)
critical = sum(1 for f in self.findings if f["severity"] == "critical")
high = sum(1 for f in self.findings if f["severity"] == "high")
medium = sum(1 for f in self.findings if f["severity"] == "medium")
low = sum(1 for f in self.findings if f["severity"] == "low")
return {
"path": path,
"findings": self.findings,
"summary": {
"total": len(self.findings),
"critical": critical,
"high": high,
"medium": medium,
"low": low,
},
"passed": critical == 0 and high == 0,
}
def _check_env_files(self, path: str) -> None:
"""Check for exposed .env files."""
env_files = []
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if not d.startswith(".git")]
for f in files:
if f == ".env" or f.endswith(".env"):
env_files.append(os.path.join(root, f))
for env_file in env_files:
gitignore_path = os.path.join(os.path.dirname(env_file), ".gitignore")
if os.path.exists(gitignore_path):
with open(gitignore_path, "r") as f:
gitignore = f.read()
if ".env" not in gitignore:
self.findings.append({
"type": "exposed_env",
"severity": "critical",
"file": env_file,
"message": ".env file not in .gitignore - secrets may be exposed",
"fix": "Add '.env' to .gitignore",
})
else:
self.findings.append({
"type": "missing_gitignore",
"severity": "high",
"file": env_file,
"message": "No .gitignore found - .env file may be committed",
"fix": "Create .gitignore with '.env' entry",
})
def _check_api_keys_in_code(self, path: str) -> None:
"""Check for hardcoded API keys in code."""
import re
patterns = [
(r'sk-[a-zA-Z0-9]{48}', "OpenAI API key"),
(r'sk-ant-[a-zA-Z0-9-]{95}', "Anthropic API key"),
(r'AIza[a-zA-Z0-9_-]{35}', "Google API key"),
(r'xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+', "Slack bot token"),
(r'xapp-[0-9]+-[a-zA-Z0-9]+', "Slack app token"),
]
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__"]
for f in files:
if f.endswith((".py", ".yaml", ".yml", ".json", ".js", ".ts")):
file_path = os.path.join(root, f)
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as fp:
content = fp.read()
for pattern, key_type in patterns:
if re.search(pattern, content):
self.findings.append({
"type": "hardcoded_key",
"severity": "critical",
"file": file_path,
"message": f"Possible {key_type} found in code",
"fix": "Move API key to environment variable",
})
except Exception:
pass
def _check_yaml_configs(self, path: str) -> None:
"""Check YAML configurations for security issues."""
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
if f.endswith((".yaml", ".yml")):
file_path = os.path.join(root, f)
try:
import yaml
with open(file_path, "r") as fp:
config = yaml.safe_load(fp)
if config and isinstance(config, dict):
if "api_key" in str(config).lower():
self.findings.append({
"type": "key_in_config",
"severity": "high",
"file": file_path,
"message": "API key reference found in YAML config",
"fix": "Use environment variables instead",
})
except Exception:
pass
def _check_dependencies(self, path: str) -> None:
"""Check for known vulnerable dependencies."""
requirements_files = ["requirements.txt", "pyproject.toml"]
for req_file in requirements_files:
req_path = os.path.join(path, req_file)
if os.path.exists(req_path):
self.findings.append({
"type": "dependency_check",
"severity": "low",
"file": req_path,
"message": "Run 'pip-audit' to check for vulnerable dependencies",
"fix": "pip install pip-audit && pip-audit",
})
break
def _check_permissions(self, path: str) -> None:
"""Check file permissions."""
sensitive_files = [".env", "credentials.json", "service-account.json"]
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if not d.startswith(".git")]
for f in files:
if f in sensitive_files:
file_path = os.path.join(root, f)
try:
mode = os.stat(file_path).st_mode
if mode & 0o077:
self.findings.append({
"type": "file_permissions",
"severity": "medium",
"file": file_path,
"message": "Sensitive file has overly permissive permissions",
"fix": f"chmod 600 {file_path}",
})
except Exception:
pass
def handle_audit_command(args) -> None:
"""Handle audit CLI command."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
except ImportError:
print("Error: Rich library required for audit display")
return
console = Console()
audit = SecurityAudit()
path = getattr(args, "path", ".")
results = audit.scan(path)
console.print(Panel.fit(
f"[bold]Security Audit Results[/bold]\n"
f"Path: {results['path']}",
title="PraisonAI Security Audit",
))
console.print()
summary = results["summary"]
if summary["total"] == 0:
console.print("[green]✓ No security issues found![/green]")
else:
table = Table(title="Summary")
table.add_column("Severity", style="bold")
table.add_column("Count", justify="right")
if summary["critical"] > 0:
table.add_row("[red]Critical[/red]", str(summary["critical"]))
if summary["high"] > 0:
table.add_row("[orange1]High[/orange1]", str(summary["high"]))
if summary["medium"] > 0:
table.add_row("[yellow]Medium[/yellow]", str(summary["medium"]))
if summary["low"] > 0:
table.add_row("[blue]Low[/blue]", str(summary["low"]))
console.print(table)
console.print()
findings_table = Table(title="Findings")
findings_table.add_column("Severity")
findings_table.add_column("Type")
findings_table.add_column("File")
findings_table.add_column("Message")
severity_colors = {
"critical": "red",
"high": "orange1",
"medium": "yellow",
"low": "blue",
}
for finding in results["findings"]:
color = severity_colors.get(finding["severity"], "white")
findings_table.add_row(
f"[{color}]{finding['severity'].upper()}[/{color}]",
finding["type"],
finding["file"],
finding["message"],
)
console.print(findings_table)
console.print()
if not results["passed"]:
console.print("[red]✗ Audit failed - critical/high issues found[/red]")
else:
console.print("[yellow]⚠ Audit passed with warnings[/yellow]")
def add_audit_parser(subparsers) -> None:
"""Add audit subparser to CLI."""
audit_parser = subparsers.add_parser(
"audit",
help="Run security audit on project",
)
audit_parser.add_argument(
"path",
nargs="?",
default=".",
help="Path to audit (default: current directory)",
)
audit_parser.set_defaults(func=handle_audit_command)
@@ -0,0 +1,249 @@
"""
Bot CLI commands for PraisonAI.
Provides CLI commands for managing messaging bots.
"""
from __future__ import annotations
import asyncio
import logging
import os
from typing import Optional
logger = logging.getLogger(__name__)
class BotHandler:
"""Handler for bot CLI commands."""
def start_telegram(
self,
token: Optional[str] = None,
agent_file: Optional[str] = None,
) -> None:
"""Start a Telegram bot.
Args:
token: Telegram bot token (or use TELEGRAM_BOT_TOKEN env var)
agent_file: Optional path to agent configuration file
"""
token = token or os.environ.get("TELEGRAM_BOT_TOKEN")
if not token:
print("Error: Telegram bot token required")
print("Provide via --token or TELEGRAM_BOT_TOKEN environment variable")
return
try:
from praisonai.bots import TelegramBot
except ImportError as e:
print(f"Error: TelegramBot requires python-telegram-bot. {e}")
print("Install with: pip install python-telegram-bot")
return
agent = self._load_agent(agent_file)
bot = TelegramBot(token=token, agent=agent)
print("Starting Telegram bot...")
print("Press Ctrl+C to stop")
try:
asyncio.run(bot.start())
except KeyboardInterrupt:
print("\nStopping bot...")
asyncio.run(bot.stop())
def start_discord(
self,
token: Optional[str] = None,
agent_file: Optional[str] = None,
) -> None:
"""Start a Discord bot.
Args:
token: Discord bot token (or use DISCORD_BOT_TOKEN env var)
agent_file: Optional path to agent configuration file
"""
token = token or os.environ.get("DISCORD_BOT_TOKEN")
if not token:
print("Error: Discord bot token required")
print("Provide via --token or DISCORD_BOT_TOKEN environment variable")
return
try:
from praisonai.bots import DiscordBot
except ImportError as e:
print(f"Error: DiscordBot requires discord.py. {e}")
print("Install with: pip install discord.py")
return
agent = self._load_agent(agent_file)
bot = DiscordBot(token=token, agent=agent)
print("Starting Discord bot...")
print("Press Ctrl+C to stop")
try:
asyncio.run(bot.start())
except KeyboardInterrupt:
print("\nStopping bot...")
asyncio.run(bot.stop())
def start_slack(
self,
token: Optional[str] = None,
app_token: Optional[str] = None,
agent_file: Optional[str] = None,
) -> None:
"""Start a Slack bot.
Args:
token: Slack bot token (or use SLACK_BOT_TOKEN env var)
app_token: Slack app token (or use SLACK_APP_TOKEN env var)
agent_file: Optional path to agent configuration file
"""
token = token or os.environ.get("SLACK_BOT_TOKEN")
app_token = app_token or os.environ.get("SLACK_APP_TOKEN")
if not token:
print("Error: Slack bot token required")
print("Provide via --token or SLACK_BOT_TOKEN environment variable")
return
try:
from praisonai.bots import SlackBot
except ImportError as e:
print(f"Error: SlackBot requires slack-bolt. {e}")
print("Install with: pip install slack-bolt")
return
agent = self._load_agent(agent_file)
bot = SlackBot(token=token, app_token=app_token, agent=agent)
print("Starting Slack bot...")
print("Press Ctrl+C to stop")
try:
asyncio.run(bot.start())
except KeyboardInterrupt:
print("\nStopping bot...")
asyncio.run(bot.stop())
def _load_agent(self, file_path: Optional[str]):
"""Load agent from configuration file."""
if not file_path:
from praisonaiagents import Agent
return Agent(name="assistant", instructions="You are a helpful assistant.")
if not os.path.exists(file_path):
print(f"Warning: Agent file not found: {file_path}")
from praisonaiagents import Agent
return Agent(name="assistant", instructions="You are a helpful assistant.")
try:
import yaml
with open(file_path, "r") as f:
config = yaml.safe_load(f)
from praisonaiagents import Agent
if "agents" in config and config["agents"]:
agent_config = config["agents"][0]
else:
agent_config = config
return Agent(
name=agent_config.get("name", "assistant"),
instructions=agent_config.get("instructions", "You are a helpful assistant."),
llm=agent_config.get("llm"),
)
except Exception as e:
print(f"Error loading agent: {e}")
from praisonaiagents import Agent
return Agent(name="assistant", instructions="You are a helpful assistant.")
def handle_bot_command(args) -> None:
"""Handle bot CLI command."""
handler = BotHandler()
platform = getattr(args, "platform", None)
if platform == "telegram":
handler.start_telegram(
token=getattr(args, "token", None),
agent_file=getattr(args, "agent", None),
)
elif platform == "discord":
handler.start_discord(
token=getattr(args, "token", None),
agent_file=getattr(args, "agent", None),
)
elif platform == "slack":
handler.start_slack(
token=getattr(args, "token", None),
app_token=getattr(args, "app_token", None),
agent_file=getattr(args, "agent", None),
)
else:
print("Available platforms: telegram, discord, slack")
print("Usage: praisonai bot <platform> [options]")
def add_bot_parser(subparsers) -> None:
"""Add bot subparser to CLI."""
bot_parser = subparsers.add_parser(
"bot",
help="Start a messaging bot",
)
bot_subparsers = bot_parser.add_subparsers(
dest="platform",
help="Bot platform",
)
telegram_parser = bot_subparsers.add_parser(
"telegram",
help="Start a Telegram bot",
)
telegram_parser.add_argument(
"--token",
help="Telegram bot token (or use TELEGRAM_BOT_TOKEN env var)",
)
telegram_parser.add_argument(
"--agent",
help="Path to agent configuration file",
)
discord_parser = bot_subparsers.add_parser(
"discord",
help="Start a Discord bot",
)
discord_parser.add_argument(
"--token",
help="Discord bot token (or use DISCORD_BOT_TOKEN env var)",
)
discord_parser.add_argument(
"--agent",
help="Path to agent configuration file",
)
slack_parser = bot_subparsers.add_parser(
"slack",
help="Start a Slack bot",
)
slack_parser.add_argument(
"--token",
help="Slack bot token (or use SLACK_BOT_TOKEN env var)",
)
slack_parser.add_argument(
"--app-token",
dest="app_token",
help="Slack app token for Socket Mode (or use SLACK_APP_TOKEN env var)",
)
slack_parser.add_argument(
"--agent",
help="Path to agent configuration file",
)
bot_parser.set_defaults(func=handle_bot_command)
@@ -0,0 +1,180 @@
"""
Gateway CLI commands for PraisonAI.
Provides CLI commands for managing the WebSocket gateway.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class GatewayHandler:
"""Handler for gateway CLI commands."""
def __init__(self):
self._gateway = None
def start(
self,
host: str = "127.0.0.1",
port: int = 8765,
agent_file: Optional[str] = None,
) -> None:
"""Start the gateway server.
Args:
host: Host to bind to
port: Port to listen on
agent_file: Optional path to agent configuration file
"""
try:
from praisonai.gateway import WebSocketGateway
from praisonaiagents.gateway import GatewayConfig
except ImportError as e:
print(f"Error: Gateway requires additional dependencies. {e}")
print("Install with: pip install praisonai[api]")
return
config = GatewayConfig(host=host, port=port)
self._gateway = WebSocketGateway(config=config)
if agent_file:
self._load_agents_from_file(agent_file)
print(f"Starting gateway on ws://{host}:{port}")
print("Press Ctrl+C to stop")
try:
asyncio.run(self._gateway.start())
except KeyboardInterrupt:
print("\nStopping gateway...")
asyncio.run(self._gateway.stop())
def _load_agents_from_file(self, file_path: str) -> None:
"""Load agents from a configuration file."""
import os
import yaml
if not os.path.exists(file_path):
print(f"Warning: Agent file not found: {file_path}")
return
try:
with open(file_path, "r") as f:
config = yaml.safe_load(f)
if "agents" in config:
from praisonaiagents import Agent
for agent_config in config["agents"]:
agent = Agent(
name=agent_config.get("name", "agent"),
instructions=agent_config.get("instructions", ""),
llm=agent_config.get("llm"),
)
agent_id = self._gateway.register_agent(agent)
print(f"Registered agent: {agent_id}")
except Exception as e:
print(f"Error loading agents: {e}")
def status(self, host: str = "127.0.0.1", port: int = 8765) -> None:
"""Check gateway status.
Args:
host: Gateway host
port: Gateway port
"""
import urllib.request
import json
url = f"http://{host}:{port}/health"
try:
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode())
print(f"Gateway Status: {data.get('status', 'unknown')}")
print(f" Uptime: {data.get('uptime', 0):.1f}s")
print(f" Agents: {data.get('agents', 0)}")
print(f" Sessions: {data.get('sessions', 0)}")
print(f" Clients: {data.get('clients', 0)}")
except Exception as e:
print(f"Gateway not reachable at {url}")
print(f"Error: {e}")
def handle_gateway_command(args) -> None:
"""Handle gateway CLI command."""
handler = GatewayHandler()
subcommand = getattr(args, "gateway_command", None) or "start"
if subcommand == "start":
handler.start(
host=getattr(args, "host", "127.0.0.1"),
port=getattr(args, "port", 8765),
agent_file=getattr(args, "agents", None),
)
elif subcommand == "status":
handler.status(
host=getattr(args, "host", "127.0.0.1"),
port=getattr(args, "port", 8765),
)
else:
print(f"Unknown gateway command: {subcommand}")
print("Available commands: start, status")
def add_gateway_parser(subparsers) -> None:
"""Add gateway subparser to CLI."""
gateway_parser = subparsers.add_parser(
"gateway",
help="Manage the WebSocket gateway",
)
gateway_subparsers = gateway_parser.add_subparsers(
dest="gateway_command",
help="Gateway commands",
)
start_parser = gateway_subparsers.add_parser(
"start",
help="Start the gateway server",
)
start_parser.add_argument(
"--host",
default="127.0.0.1",
help="Host to bind to (default: 127.0.0.1)",
)
start_parser.add_argument(
"--port",
type=int,
default=8765,
help="Port to listen on (default: 8765)",
)
start_parser.add_argument(
"--agents",
help="Path to agent configuration file",
)
status_parser = gateway_subparsers.add_parser(
"status",
help="Check gateway status",
)
status_parser.add_argument(
"--host",
default="127.0.0.1",
help="Gateway host (default: 127.0.0.1)",
)
status_parser.add_argument(
"--port",
type=int,
default=8765,
help="Gateway port (default: 8765)",
)
gateway_parser.set_defaults(func=handle_gateway_command)
@@ -0,0 +1,284 @@
"""
Config Migration CLI for PraisonAI.
Provides migration tools for converting configurations between formats.
"""
from __future__ import annotations
import os
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
class ConfigMigrator:
"""Configuration migrator for PraisonAI."""
def migrate(
self,
source: str,
target: Optional[str] = None,
source_format: Optional[str] = None,
target_format: str = "praisonai",
) -> Dict[str, Any]:
"""Migrate configuration from one format to another.
Args:
source: Source configuration file path
target: Target file path (optional)
source_format: Source format (auto-detected if not provided)
target_format: Target format (default: praisonai)
Returns:
Migration result
"""
if not os.path.exists(source):
return {"success": False, "error": f"Source file not found: {source}"}
if not source_format:
source_format = self._detect_format(source)
try:
config = self._load_config(source, source_format)
except Exception as e:
return {"success": False, "error": f"Failed to load source: {e}"}
try:
converted = self._convert_config(config, source_format, target_format)
except Exception as e:
return {"success": False, "error": f"Failed to convert: {e}"}
if target:
try:
self._save_config(converted, target, target_format)
except Exception as e:
return {"success": False, "error": f"Failed to save: {e}"}
return {
"success": True,
"source_format": source_format,
"target_format": target_format,
"config": converted,
"target_file": target,
}
def _detect_format(self, path: str) -> str:
"""Detect configuration format from file."""
import yaml
with open(path, "r") as f:
content = f.read()
try:
config = yaml.safe_load(content)
except Exception:
return "unknown"
if not isinstance(config, dict):
return "unknown"
if "framework" in config and config.get("framework") == "praisonai":
return "praisonai"
if "agents" in config and "tasks" in config:
return "crewai"
if "config_list" in config or "llm_config" in config:
return "autogen"
if "agents" in config:
return "praisonai"
return "unknown"
def _load_config(self, path: str, format: str) -> Dict[str, Any]:
"""Load configuration from file."""
import yaml
with open(path, "r") as f:
return yaml.safe_load(f)
def _convert_config(
self,
config: Dict[str, Any],
source_format: str,
target_format: str,
) -> Dict[str, Any]:
"""Convert configuration between formats."""
if source_format == target_format:
return config
if source_format == "crewai" and target_format == "praisonai":
return self._convert_crewai_to_praisonai(config)
if source_format == "autogen" and target_format == "praisonai":
return self._convert_autogen_to_praisonai(config)
if source_format == "praisonai" and target_format == "crewai":
return self._convert_praisonai_to_crewai(config)
return config
def _convert_crewai_to_praisonai(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Convert CrewAI config to PraisonAI format."""
result = {
"framework": "praisonai",
"topic": config.get("topic", "Converted from CrewAI"),
"agents": [],
"steps": [],
}
for agent in config.get("agents", []):
result["agents"].append({
"name": agent.get("role", "agent").lower().replace(" ", "_"),
"role": agent.get("role", "Agent"),
"goal": agent.get("goal", ""),
"backstory": agent.get("backstory", ""),
"tools": agent.get("tools", []),
})
for i, task in enumerate(config.get("tasks", [])):
agent_name = task.get("agent", "")
if isinstance(agent_name, dict):
agent_name = agent_name.get("role", "agent").lower().replace(" ", "_")
result["steps"].append({
"name": f"step_{i+1}",
"agent": agent_name,
"action": task.get("description", ""),
"expected_output": task.get("expected_output", ""),
})
return result
def _convert_autogen_to_praisonai(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Convert AutoGen config to PraisonAI format."""
result = {
"framework": "praisonai",
"topic": "Converted from AutoGen",
"agents": [],
"steps": [],
}
llm_config = config.get("llm_config", {})
model = "gpt-4o-mini"
if "config_list" in llm_config:
config_list = llm_config["config_list"]
if config_list:
model = config_list[0].get("model", model)
for agent in config.get("agents", []):
result["agents"].append({
"name": agent.get("name", "agent"),
"role": agent.get("name", "Agent"),
"goal": agent.get("system_message", ""),
"backstory": "",
"llm": model,
})
return result
def _convert_praisonai_to_crewai(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Convert PraisonAI config to CrewAI format."""
result = {
"agents": [],
"tasks": [],
}
for agent in config.get("agents", []):
result["agents"].append({
"role": agent.get("role", agent.get("name", "Agent")),
"goal": agent.get("goal", ""),
"backstory": agent.get("backstory", ""),
"tools": agent.get("tools", []),
})
for step in config.get("steps", []):
result["tasks"].append({
"description": step.get("action", ""),
"expected_output": step.get("expected_output", ""),
"agent": step.get("agent", ""),
})
return result
def _save_config(self, config: Dict[str, Any], path: str, format: str) -> None:
"""Save configuration to file."""
import yaml
with open(path, "w") as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
def handle_migrate_command(args) -> None:
"""Handle migrate CLI command."""
try:
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
except ImportError:
print("Error: Rich library required")
return
console = Console()
migrator = ConfigMigrator()
source = getattr(args, "source", None)
target = getattr(args, "target", None)
source_format = getattr(args, "from_format", None)
target_format = getattr(args, "to_format", "praisonai")
if not source:
console.print("[red]Error: Source file required[/red]")
return
result = migrator.migrate(
source=source,
target=target,
source_format=source_format,
target_format=target_format,
)
if not result["success"]:
console.print(f"[red]Migration failed: {result['error']}[/red]")
return
console.print(Panel.fit(
f"[green]Migration successful![/green]\n\n"
f"Source format: {result['source_format']}\n"
f"Target format: {result['target_format']}",
title="Config Migration",
))
if target:
console.print(f"\nSaved to: {target}")
else:
import yaml
yaml_str = yaml.dump(result["config"], default_flow_style=False, sort_keys=False)
console.print("\nConverted configuration:")
console.print(Syntax(yaml_str, "yaml"))
def add_migrate_parser(subparsers) -> None:
"""Add migrate subparser to CLI."""
migrate_parser = subparsers.add_parser(
"migrate",
help="Migrate configuration between formats",
)
migrate_parser.add_argument(
"source",
help="Source configuration file",
)
migrate_parser.add_argument(
"--target", "-o",
help="Target output file",
)
migrate_parser.add_argument(
"--from", dest="from_format",
choices=["praisonai", "crewai", "autogen"],
help="Source format (auto-detected if not provided)",
)
migrate_parser.add_argument(
"--to", dest="to_format",
choices=["praisonai", "crewai"],
default="praisonai",
help="Target format (default: praisonai)",
)
migrate_parser.set_defaults(func=handle_migrate_command)
@@ -407,22 +407,21 @@ BAD: "Use search_web to find {{{{topic}}}}" (variables don't work in actions!)
- No explanations before or after
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HIERARCHICAL PROCESS (MANDATORY - Use for all multi-agent recipes)
SEQUENTIAL PROCESS (DEFAULT - Use for all multi-agent recipes)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**ALWAYS include these fields at the root level for multi-agent workflows:**
**ALWAYS include this field at the root level:**
```yaml
process: hierarchical
manager_llm: gpt-4o-mini
process: sequential
```
**What hierarchical process does:**
- A manager agent validates each step's output before proceeding
- If a step fails validation, the workflow stops with a clear failure reason
- Ensures quality control and prevents cascading errors
**What sequential process does:**
- Executes steps in order, passing output from one step to the next
- Each step can reference previous step outputs via {{agent_name}}_output
- Simple and reliable for most workflows
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EXAMPLE: SEARCH TASK (hierarchical with manager validation)
EXAMPLE: SEARCH TASK (sequential workflow)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
metadata:
@@ -431,8 +430,7 @@ metadata:
env:
- OPENAI_API_KEY
process: hierarchical
manager_llm: gpt-4o-mini
process: sequential
agents:
researcher:
@@ -449,14 +447,14 @@ agents:
goal: Analyze and synthesize research findings
backstory: Expert at identifying patterns, trends, and key insights from raw data.
tools: []
llm: gpt-4.1
llm: gpt-4o-mini
writer:
role: Technical Writer
goal: Create clear, well-structured reports
backstory: Expert at transforming complex information into readable, actionable content.
tools: []
llm: gpt-4.1
llm: gpt-4o-mini
steps:
- agent: researcher
@@ -831,15 +829,29 @@ steps:
Returns:
Python code string
Notes:
- web_crawl: Uses full path import since lazy loading returns module
- search_web: Works with standard import from praisonaiagents.tools
- tavily_*: Imported from praisonaiagents.tools (not praisonai_tools)
"""
# Map tools to their import sources
tool_imports = []
tool_exports = []
for tool in tools:
if tool in ['tavily_search', 'tavily_extract']:
if tool in ['web_crawl', 'crawl_web']:
# web_crawl needs full path import (lazy loading returns module otherwise)
if 'web_crawl' not in str(tool_imports):
tool_imports.append("from praisonaiagents.tools.web_crawl import web_crawl")
tool_exports.append('web_crawl')
elif tool in ['search_web']:
if 'search_web' not in str(tool_imports):
tool_imports.append("from praisonaiagents.tools import search_web")
tool_exports.append('search_web')
elif tool in ['tavily_search', 'tavily_extract']:
if 'tavily' not in str(tool_imports):
tool_imports.append("from praisonai_tools.tools import tavily_search, tavily_extract")
tool_imports.append("from praisonaiagents.tools import tavily_search, tavily_extract")
tool_exports.append(tool)
elif tool in ['read_file', 'write_file', 'list_files']:
if 'file_tools' not in str(tool_imports):
@@ -0,0 +1,255 @@
"""
Sandbox CLI commands for PraisonAI.
Provides CLI commands for sandbox code execution.
"""
from __future__ import annotations
import asyncio
import logging
import os
import sys
from typing import Optional
logger = logging.getLogger(__name__)
class SandboxHandler:
"""Handler for sandbox CLI commands."""
def run(
self,
code: Optional[str] = None,
file: Optional[str] = None,
sandbox_type: str = "subprocess",
image: str = "python:3.11-slim",
timeout: int = 60,
) -> None:
"""Run code in a sandbox.
Args:
code: Code to execute
file: Path to file to execute
sandbox_type: Type of sandbox (subprocess, docker)
image: Docker image to use
timeout: Execution timeout in seconds
"""
if not code and not file:
print("Error: Provide code via --code or --file")
return
if file:
if not os.path.exists(file):
print(f"Error: File not found: {file}")
return
with open(file, "r") as f:
code = f.read()
try:
from praisonaiagents.sandbox import ResourceLimits
if sandbox_type == "docker":
from praisonai.sandbox import DockerSandbox
sandbox = DockerSandbox(image=image)
else:
from praisonai.sandbox import SubprocessSandbox
sandbox = SubprocessSandbox()
except ImportError as e:
print(f"Error: {e}")
return
limits = ResourceLimits(timeout_seconds=timeout)
print(f"Running in {sandbox_type} sandbox...")
async def execute():
await sandbox.start()
try:
result = await sandbox.execute(code, limits=limits)
if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
if result.status.value != "completed":
print(f"\nStatus: {result.status.value}")
if result.error:
print(f"Error: {result.error}")
return result.exit_code or 0
finally:
await sandbox.stop()
exit_code = asyncio.run(execute())
sys.exit(exit_code)
def shell(
self,
sandbox_type: str = "subprocess",
image: str = "python:3.11-slim",
) -> None:
"""Start an interactive sandbox shell.
Args:
sandbox_type: Type of sandbox (subprocess, docker)
image: Docker image to use
"""
try:
from praisonaiagents.sandbox import ResourceLimits
if sandbox_type == "docker":
from praisonai.sandbox import DockerSandbox
sandbox = DockerSandbox(image=image)
else:
from praisonai.sandbox import SubprocessSandbox
sandbox = SubprocessSandbox()
except ImportError as e:
print(f"Error: {e}")
return
print(f"Starting {sandbox_type} sandbox shell...")
print("Type 'exit' or Ctrl+D to quit")
print()
async def run_shell():
await sandbox.start()
try:
while True:
try:
code = input(">>> ")
if code.strip().lower() == "exit":
break
if not code.strip():
continue
result = await sandbox.execute(
code,
limits=ResourceLimits(timeout_seconds=30),
)
if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
if result.error:
print(f"Error: {result.error}")
except EOFError:
break
except KeyboardInterrupt:
print()
continue
finally:
await sandbox.stop()
asyncio.run(run_shell())
def status(self) -> None:
"""Check sandbox availability."""
print("Sandbox Status:")
print()
print("Subprocess sandbox: Available")
try:
from praisonai.sandbox import DockerSandbox
sandbox = DockerSandbox()
if sandbox.is_available:
print("Docker sandbox: Available")
else:
print("Docker sandbox: Not available (Docker not running)")
except ImportError:
print("Docker sandbox: Not available (dependencies not installed)")
def handle_sandbox_command(args) -> None:
"""Handle sandbox CLI command."""
handler = SandboxHandler()
subcommand = getattr(args, "sandbox_command", None) or "status"
if subcommand == "run":
handler.run(
code=getattr(args, "code", None),
file=getattr(args, "file", None),
sandbox_type=getattr(args, "type", "subprocess"),
image=getattr(args, "image", "python:3.11-slim"),
timeout=getattr(args, "timeout", 60),
)
elif subcommand == "shell":
handler.shell(
sandbox_type=getattr(args, "type", "subprocess"),
image=getattr(args, "image", "python:3.11-slim"),
)
elif subcommand == "status":
handler.status()
else:
print(f"Unknown sandbox command: {subcommand}")
print("Available commands: run, shell, status")
def add_sandbox_parser(subparsers) -> None:
"""Add sandbox subparser to CLI."""
sandbox_parser = subparsers.add_parser(
"sandbox",
help="Run code in a sandbox",
)
sandbox_subparsers = sandbox_parser.add_subparsers(
dest="sandbox_command",
help="Sandbox commands",
)
run_parser = sandbox_subparsers.add_parser(
"run",
help="Run code in sandbox",
)
run_parser.add_argument(
"--code", "-c",
help="Code to execute",
)
run_parser.add_argument(
"--file", "-f",
help="File to execute",
)
run_parser.add_argument(
"--type", "-t",
choices=["subprocess", "docker"],
default="subprocess",
help="Sandbox type (default: subprocess)",
)
run_parser.add_argument(
"--image",
default="python:3.11-slim",
help="Docker image (default: python:3.11-slim)",
)
run_parser.add_argument(
"--timeout",
type=int,
default=60,
help="Timeout in seconds (default: 60)",
)
shell_parser = sandbox_subparsers.add_parser(
"shell",
help="Start interactive sandbox shell",
)
shell_parser.add_argument(
"--type", "-t",
choices=["subprocess", "docker"],
default="subprocess",
help="Sandbox type (default: subprocess)",
)
shell_parser.add_argument(
"--image",
default="python:3.11-slim",
help="Docker image (default: python:3.11-slim)",
)
sandbox_subparsers.add_parser(
"status",
help="Check sandbox availability",
)
sandbox_parser.set_defaults(func=handle_sandbox_command)
@@ -0,0 +1,289 @@
"""
Onboarding Wizard for PraisonAI.
Provides an interactive setup wizard for new users.
"""
from __future__ import annotations
import os
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
class OnboardingWizard:
"""Interactive onboarding wizard for PraisonAI setup."""
def __init__(self):
self.config: Dict[str, Any] = {}
def run(self, output_dir: str = ".") -> bool:
"""Run the onboarding wizard.
Args:
output_dir: Directory to create configuration files
Returns:
True if setup completed successfully
"""
try:
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
except ImportError:
print("Error: Rich library required for wizard")
return False
console = Console()
console.print(Panel.fit(
"[bold blue]Welcome to PraisonAI![/bold blue]\n\n"
"This wizard will help you set up your first agent project.",
title="Onboarding Wizard",
))
console.print()
project_name = Prompt.ask(
"Project name",
default="my-agent-project",
)
self.config["project_name"] = project_name
console.print("\n[bold]Select your LLM provider:[/bold]")
console.print(" 1. OpenAI (GPT-4, GPT-3.5)")
console.print(" 2. Anthropic (Claude)")
console.print(" 3. Google (Gemini)")
console.print(" 4. Ollama (Local)")
console.print(" 5. Other (LiteLLM)")
provider_choice = Prompt.ask(
"Provider",
choices=["1", "2", "3", "4", "5"],
default="1",
)
provider_map = {
"1": "openai",
"2": "anthropic",
"3": "google",
"4": "ollama",
"5": "litellm",
}
self.config["provider"] = provider_map[provider_choice]
if self.config["provider"] != "ollama":
api_key = Prompt.ask(
f"API Key for {self.config['provider']}",
password=True,
)
self.config["api_key"] = api_key
model_defaults = {
"openai": "gpt-4o-mini",
"anthropic": "claude-3-sonnet-20240229",
"google": "gemini-1.5-flash",
"ollama": "llama3.2",
"litellm": "gpt-4o-mini",
}
model = Prompt.ask(
"Model",
default=model_defaults.get(self.config["provider"], "gpt-4o-mini"),
)
self.config["model"] = model
console.print("\n[bold]Agent Configuration:[/bold]")
agent_name = Prompt.ask(
"Agent name",
default="assistant",
)
self.config["agent_name"] = agent_name
agent_role = Prompt.ask(
"Agent role/description",
default="A helpful AI assistant",
)
self.config["agent_role"] = agent_role
use_tools = Confirm.ask(
"Enable tools (web search, file operations)?",
default=True,
)
self.config["use_tools"] = use_tools
use_memory = Confirm.ask(
"Enable memory (conversation history)?",
default=True,
)
self.config["use_memory"] = use_memory
console.print("\n[bold]Creating project files...[/bold]")
project_dir = os.path.join(output_dir, project_name)
os.makedirs(project_dir, exist_ok=True)
self._create_env_file(project_dir)
self._create_agent_yaml(project_dir)
self._create_main_py(project_dir)
self._create_readme(project_dir)
console.print(f"\n[green]✓[/green] Project created at: {project_dir}")
console.print("\n[bold]Next steps:[/bold]")
console.print(f" 1. cd {project_name}")
console.print(" 2. pip install praisonaiagents")
console.print(" 3. python main.py")
return True
def _create_env_file(self, project_dir: str) -> None:
"""Create .env file."""
env_content = []
if self.config.get("api_key"):
provider = self.config["provider"]
key_names = {
"openai": "OPENAI_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"google": "GOOGLE_API_KEY",
"litellm": "OPENAI_API_KEY",
}
key_name = key_names.get(provider, "API_KEY")
env_content.append(f"{key_name}={self.config['api_key']}")
env_path = os.path.join(project_dir, ".env")
with open(env_path, "w") as f:
f.write("\n".join(env_content))
gitignore_path = os.path.join(project_dir, ".gitignore")
with open(gitignore_path, "w") as f:
f.write(".env\n__pycache__/\n*.pyc\n")
def _create_agent_yaml(self, project_dir: str) -> None:
"""Create agents.yaml file."""
tools = []
if self.config.get("use_tools"):
tools = ["search_web"]
yaml_content = f"""# PraisonAI Agent Configuration
# Generated by praisonai init
framework: praisonai
topic: {self.config['agent_role']}
agents:
- name: {self.config['agent_name']}
role: {self.config['agent_role']}
goal: Help users with their tasks
backstory: You are a helpful AI assistant.
llm: {self.config['model']}
tools: {tools}
memory: {str(self.config.get('use_memory', True)).lower()}
steps:
- name: main
agent: {self.config['agent_name']}
action: Respond to user input
expected_output: A helpful response
"""
yaml_path = os.path.join(project_dir, "agents.yaml")
with open(yaml_path, "w") as f:
f.write(yaml_content)
def _create_main_py(self, project_dir: str) -> None:
"""Create main.py file."""
main_content = f'''"""
{self.config['project_name']} - PraisonAI Agent
Generated by praisonai init
"""
from praisonaiagents import Agent
def main():
agent = Agent(
name="{self.config['agent_name']}",
instructions="{self.config['agent_role']}",
llm="{self.config['model']}",
memory={self.config.get('use_memory', True)},
)
print("Agent ready! Type 'quit' to exit.")
print()
while True:
user_input = input("You: ")
if user_input.lower() in ["quit", "exit", "q"]:
break
response = agent.chat(user_input)
print(f"Agent: {{response}}")
print()
if __name__ == "__main__":
main()
'''
main_path = os.path.join(project_dir, "main.py")
with open(main_path, "w") as f:
f.write(main_content)
def _create_readme(self, project_dir: str) -> None:
"""Create README.md file."""
readme_content = f"""# {self.config['project_name']}
A PraisonAI agent project.
## Setup
1. Install dependencies:
```bash
pip install praisonaiagents
```
2. Set up your API key in `.env`
3. Run the agent:
```bash
python main.py
```
## Configuration
Edit `agents.yaml` to customize your agent's behavior.
## Documentation
- [PraisonAI Documentation](https://docs.praison.ai)
- [Agent Configuration](https://docs.praison.ai/agents)
- [Tools Reference](https://docs.praison.ai/tools)
"""
readme_path = os.path.join(project_dir, "README.md")
with open(readme_path, "w") as f:
f.write(readme_content)
def handle_init_command(args) -> None:
"""Handle init CLI command."""
wizard = OnboardingWizard()
output_dir = getattr(args, "output", ".")
wizard.run(output_dir=output_dir)
def add_init_parser(subparsers) -> None:
"""Add init subparser to CLI."""
init_parser = subparsers.add_parser(
"init",
help="Initialize a new PraisonAI project",
)
init_parser.add_argument(
"--output", "-o",
default=".",
help="Output directory (default: current directory)",
)
init_parser.set_defaults(func=handle_init_command)
+35
View File
@@ -1640,6 +1640,41 @@ class PraisonAI:
exit_code = handler.cmd_uninstall(unknown_args)
sys.exit(exit_code)
elif args.command == 'gateway':
# Gateway command - WebSocket gateway for multi-agent coordination
from .features.gateway import handle_gateway_command
exit_code = handle_gateway_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'bot':
# Bot command - messaging bot runtimes (Telegram, Discord, Slack)
from .features.bots_cli import handle_bot_command
exit_code = handle_bot_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'sandbox':
# Sandbox command - secure code execution environment
from .features.sandbox_cli import handle_sandbox_command
exit_code = handle_sandbox_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'wizard':
# Wizard command - interactive project setup
from .features.wizard import handle_wizard_command
exit_code = handle_wizard_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'migrate':
# Migrate command - config migration between formats
from .features.migrate import handle_migrate_command
exit_code = handle_migrate_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'security':
# Security command - security audit and scanning
from .features.audit_cli import handle_audit_command
exit_code = handle_audit_command(unknown_args)
sys.exit(exit_code)
# Only check framework availability for agent-related operations
if not args.command and (args.init or args.auto or args.framework):
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==3.10.27 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==3.11.0 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -0,0 +1,22 @@
"""
Gateway implementations for PraisonAI.
Provides WebSocket gateway server for multi-agent coordination.
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .server import WebSocketGateway, GatewaySession
def __getattr__(name: str):
"""Lazy loading of gateway components."""
if name == "WebSocketGateway":
from .server import WebSocketGateway
return WebSocketGateway
if name == "GatewaySession":
from .server import GatewaySession
return GatewaySession
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["WebSocketGateway", "GatewaySession"]
+461
View File
@@ -0,0 +1,461 @@
"""
WebSocket Gateway Server for PraisonAI.
Provides a WebSocket-based gateway for multi-agent coordination,
session management, and real-time communication.
"""
from __future__ import annotations
import asyncio
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from praisonaiagents import Agent
from praisonaiagents.gateway import (
GatewayConfig,
GatewayEvent,
GatewayMessage,
EventType,
)
logger = logging.getLogger(__name__)
@dataclass
class GatewaySession:
"""A gateway session tracking a conversation between client and agent."""
_session_id: str
_agent_id: str
_client_id: Optional[str] = None
_is_active: bool = True
_created_at: float = field(default_factory=time.time)
_last_activity: float = field(default_factory=time.time)
_state: Dict[str, Any] = field(default_factory=dict)
_messages: List[GatewayMessage] = field(default_factory=list)
_max_messages: int = 1000
@property
def session_id(self) -> str:
return self._session_id
@property
def agent_id(self) -> Optional[str]:
return self._agent_id
@property
def client_id(self) -> Optional[str]:
return self._client_id
@property
def is_active(self) -> bool:
return self._is_active
@property
def created_at(self) -> float:
return self._created_at
@property
def last_activity(self) -> float:
return self._last_activity
def get_state(self) -> Dict[str, Any]:
return dict(self._state)
def set_state(self, key: str, value: Any) -> None:
self._state[key] = value
self._last_activity = time.time()
def add_message(self, message: GatewayMessage) -> None:
self._messages.append(message)
self._last_activity = time.time()
if self._max_messages > 0 and len(self._messages) > self._max_messages:
self._messages = self._messages[-self._max_messages:]
def get_messages(self, limit: Optional[int] = None) -> List[GatewayMessage]:
if limit:
return list(self._messages[-limit:])
return list(self._messages)
def close(self) -> None:
self._is_active = False
class WebSocketGateway:
"""WebSocket gateway server for multi-agent coordination.
Implements the GatewayProtocol for WebSocket-based communication.
Example:
from praisonai.gateway import WebSocketGateway
from praisonaiagents import Agent
# Create gateway
gateway = WebSocketGateway(port=8765)
# Register agents
agent = Agent(name="assistant")
gateway.register_agent(agent)
# Start gateway
await gateway.start()
"""
def __init__(
self,
host: str = "127.0.0.1",
port: int = 8765,
config: Optional[GatewayConfig] = None,
):
"""Initialize the gateway.
Args:
host: Host to bind to
port: Port to listen on
config: Optional gateway configuration
"""
self.config = config or GatewayConfig(host=host, port=port)
self._host = self.config.host
self._port = self.config.port
self._is_running = False
self._started_at: Optional[float] = None
self._server = None
self._agents: Dict[str, "Agent"] = {}
self._sessions: Dict[str, GatewaySession] = {}
self._clients: Dict[str, Any] = {} # WebSocket connections
self._client_sessions: Dict[str, str] = {} # client_id -> session_id
self._event_handlers: Dict[str, List[Callable]] = {}
self._message_queue: asyncio.Queue = asyncio.Queue()
@property
def is_running(self) -> bool:
return self._is_running
@property
def port(self) -> int:
return self._port
@property
def host(self) -> str:
return self._host
async def start(self) -> None:
"""Start the gateway server."""
if self._is_running:
logger.warning("Gateway already running")
return
try:
from starlette.applications import Starlette
from starlette.routing import Route, WebSocketRoute
from starlette.responses import JSONResponse
from starlette.websockets import WebSocket, WebSocketDisconnect
import uvicorn
except ImportError:
raise ImportError(
"Gateway requires starlette and uvicorn. "
"Install with: pip install praisonai[api]"
)
async def health(request):
return JSONResponse(self.health())
async def info(request):
return JSONResponse({
"name": "PraisonAI Gateway",
"version": "1.0.0",
"agents": list(self._agents.keys()),
"sessions": len(self._sessions),
"clients": len(self._clients),
})
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
client_id = str(uuid.uuid4())
self._clients[client_id] = websocket
logger.info(f"Client connected: {client_id}")
await self.emit(GatewayEvent(
type=EventType.CONNECT,
data={"client_id": client_id},
source=client_id,
))
try:
while True:
data = await websocket.receive_json()
await self._handle_client_message(client_id, data)
except WebSocketDisconnect:
logger.info(f"Client disconnected: {client_id}")
except Exception as e:
logger.error(f"WebSocket error: {e}")
finally:
self._clients.pop(client_id, None)
session_id = self._client_sessions.pop(client_id, None)
if session_id:
self.close_session(session_id)
await self.emit(GatewayEvent(
type=EventType.DISCONNECT,
data={"client_id": client_id},
source=client_id,
))
routes = [
Route("/health", health, methods=["GET"]),
Route("/info", info, methods=["GET"]),
WebSocketRoute("/ws", websocket_endpoint),
]
app = Starlette(routes=routes)
config = uvicorn.Config(
app,
host=self._host,
port=self._port,
log_level="warning",
)
self._server = uvicorn.Server(config)
self._is_running = True
self._started_at = time.time()
logger.info(f"Gateway started on ws://{self._host}:{self._port}")
await self._server.serve()
async def stop(self) -> None:
"""Stop the gateway server."""
if not self._is_running:
return
self._is_running = False
for client_id, ws in list(self._clients.items()):
try:
await ws.close()
except Exception:
pass
self._clients.clear()
self._client_sessions.clear()
for session in list(self._sessions.values()):
session.close()
if self._server:
self._server.should_exit = True
logger.info("Gateway stopped")
async def _handle_client_message(self, client_id: str, data: Dict[str, Any]) -> None:
"""Handle a message from a client."""
msg_type = data.get("type", "message")
if msg_type == "join":
agent_id = data.get("agent_id")
if agent_id and agent_id in self._agents:
session = self.create_session(agent_id, client_id)
self._client_sessions[client_id] = session.session_id
await self._send_to_client(client_id, {
"type": "joined",
"session_id": session.session_id,
"agent_id": agent_id,
})
else:
await self._send_to_client(client_id, {
"type": "error",
"message": f"Agent not found: {agent_id}",
})
elif msg_type == "message":
session_id = self._client_sessions.get(client_id)
if session_id:
session = self._sessions.get(session_id)
if session:
content = data.get("content", "")
message = GatewayMessage(
content=content,
sender_id=client_id,
session_id=session_id,
)
session.add_message(message)
response = await self._process_agent_message(session, message)
await self._send_to_client(client_id, {
"type": "response",
"content": response,
"session_id": session_id,
})
else:
await self._send_to_client(client_id, {
"type": "error",
"message": "Not joined to any session",
})
elif msg_type == "leave":
session_id = self._client_sessions.pop(client_id, None)
if session_id:
self.close_session(session_id)
await self._send_to_client(client_id, {
"type": "left",
"session_id": session_id,
})
async def _process_agent_message(
self,
session: GatewaySession,
message: GatewayMessage,
) -> str:
"""Process a message through the agent."""
agent = self._agents.get(session.agent_id)
if not agent:
return "Agent not available"
try:
response = agent.chat(message.content if isinstance(message.content, str) else str(message.content))
response_message = GatewayMessage(
content=response,
sender_id=session.agent_id,
session_id=session.session_id,
reply_to=message.message_id,
)
session.add_message(response_message)
return response
except Exception as e:
logger.error(f"Agent error: {e}")
return f"Error: {str(e)}"
async def _send_to_client(self, client_id: str, data: Dict[str, Any]) -> None:
"""Send data to a specific client."""
ws = self._clients.get(client_id)
if ws:
try:
await ws.send_json(data)
except Exception as e:
logger.error(f"Error sending to client {client_id}: {e}")
def register_agent(self, agent: "Agent", agent_id: Optional[str] = None) -> str:
"""Register an agent with the gateway."""
aid = agent_id or getattr(agent, "agent_id", None) or str(uuid.uuid4())
self._agents[aid] = agent
logger.info(f"Agent registered: {aid}")
return aid
def unregister_agent(self, agent_id: str) -> bool:
"""Unregister an agent from the gateway."""
if agent_id in self._agents:
del self._agents[agent_id]
logger.info(f"Agent unregistered: {agent_id}")
return True
return False
def get_agent(self, agent_id: str) -> Optional["Agent"]:
"""Get a registered agent by ID."""
return self._agents.get(agent_id)
def list_agents(self) -> List[str]:
"""List all registered agent IDs."""
return list(self._agents.keys())
def create_session(
self,
agent_id: str,
client_id: Optional[str] = None,
session_id: Optional[str] = None,
) -> GatewaySession:
"""Create a new session."""
sid = session_id or str(uuid.uuid4())
session = GatewaySession(
_session_id=sid,
_agent_id=agent_id,
_client_id=client_id,
_max_messages=self.config.session_config.max_messages,
)
self._sessions[sid] = session
logger.info(f"Session created: {sid} for agent {agent_id}")
return session
def get_session(self, session_id: str) -> Optional[GatewaySession]:
"""Get a session by ID."""
return self._sessions.get(session_id)
def close_session(self, session_id: str) -> bool:
"""Close a session."""
session = self._sessions.pop(session_id, None)
if session:
session.close()
logger.info(f"Session closed: {session_id}")
return True
return False
def list_sessions(self, agent_id: Optional[str] = None) -> List[str]:
"""List session IDs, optionally filtered by agent."""
if agent_id:
return [
sid for sid, session in self._sessions.items()
if session.agent_id == agent_id
]
return list(self._sessions.keys())
def on_event(self, event_type: str) -> Callable:
"""Decorator to register an event handler."""
def decorator(func: Callable) -> Callable:
if event_type not in self._event_handlers:
self._event_handlers[event_type] = []
self._event_handlers[event_type].append(func)
return func
return decorator
async def emit(self, event: GatewayEvent) -> None:
"""Emit an event to registered handlers."""
event_type = event.type.value if hasattr(event.type, "value") else str(event.type)
handlers = self._event_handlers.get(event_type, [])
for handler in handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(event)
else:
handler(event)
except Exception as e:
logger.error(f"Event handler error: {e}")
async def broadcast(
self,
event: GatewayEvent,
exclude: Optional[List[str]] = None,
) -> None:
"""Broadcast an event to all connected clients."""
exclude_set = set(exclude or [])
data = event.to_dict()
for client_id, ws in list(self._clients.items()):
if client_id not in exclude_set:
try:
await ws.send_json(data)
except Exception as e:
logger.error(f"Broadcast error to {client_id}: {e}")
def health(self) -> Dict[str, Any]:
"""Get gateway health status."""
uptime = time.time() - self._started_at if self._started_at else 0
return {
"status": "healthy" if self._is_running else "stopped",
"uptime": uptime,
"agents": len(self._agents),
"sessions": len(self._sessions),
"clients": len(self._clients),
}
@@ -0,0 +1,23 @@
"""
Sandbox implementations for PraisonAI.
Provides Docker and subprocess sandbox for safe code execution.
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .docker import DockerSandbox
from .subprocess import SubprocessSandbox
def __getattr__(name: str):
"""Lazy loading of sandbox components."""
if name == "DockerSandbox":
from .docker import DockerSandbox
return DockerSandbox
if name == "SubprocessSandbox":
from .subprocess import SubprocessSandbox
return SubprocessSandbox
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["DockerSandbox", "SubprocessSandbox"]
+437
View File
@@ -0,0 +1,437 @@
"""
Docker Sandbox implementation for PraisonAI.
Provides isolated code execution in Docker containers.
"""
from __future__ import annotations
import asyncio
import logging
import os
import tempfile
import time
import uuid
from typing import Any, Dict, List, Optional, Union
from praisonaiagents.sandbox import (
SandboxConfig,
SandboxResult,
SandboxStatus,
ResourceLimits,
)
logger = logging.getLogger(__name__)
class DockerSandbox:
"""Docker-based sandbox for safe code execution.
Executes code in isolated Docker containers with resource limits
and security policies.
Example:
from praisonai.sandbox import DockerSandbox
sandbox = DockerSandbox(image="python:3.11-slim")
result = await sandbox.execute("print('Hello, World!')")
print(result.stdout) # Hello, World!
Requires: Docker installed and running
"""
def __init__(
self,
image: str = "python:3.11-slim",
config: Optional[SandboxConfig] = None,
):
"""Initialize the Docker sandbox.
Args:
image: Docker image to use
config: Optional sandbox configuration
"""
self.config = config or SandboxConfig.docker(image)
self._image = image
self._container_id: Optional[str] = None
self._is_running = False
self._temp_dir: Optional[str] = None
@property
def is_available(self) -> bool:
"""Check if Docker is available."""
try:
import subprocess
result = subprocess.run(
["docker", "version"],
capture_output=True,
timeout=5,
)
return result.returncode == 0
except Exception:
return False
@property
def sandbox_type(self) -> str:
return "docker"
async def start(self) -> None:
"""Start/initialize the sandbox environment."""
if self._is_running:
return
if not self.is_available:
raise RuntimeError("Docker is not available")
self._temp_dir = tempfile.mkdtemp(prefix="praisonai_sandbox_")
try:
proc = await asyncio.create_subprocess_exec(
"docker", "pull", self._image,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.wait()
except Exception as e:
logger.warning(f"Failed to pull image: {e}")
self._is_running = True
logger.info(f"Docker sandbox initialized with image: {self._image}")
async def stop(self) -> None:
"""Stop/cleanup the sandbox environment."""
if not self._is_running:
return
if self._container_id:
try:
proc = await asyncio.create_subprocess_exec(
"docker", "rm", "-f", self._container_id,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.wait()
except Exception as e:
logger.warning(f"Failed to remove container: {e}")
self._container_id = None
if self._temp_dir and os.path.exists(self._temp_dir):
import shutil
shutil.rmtree(self._temp_dir, ignore_errors=True)
self._is_running = False
logger.info("Docker sandbox stopped")
async def execute(
self,
code: str,
language: str = "python",
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Execute code in the sandbox."""
if not self._is_running:
await self.start()
limits = limits or self.config.resource_limits
execution_id = str(uuid.uuid4())
code_file = os.path.join(self._temp_dir, f"code_{execution_id}.py")
with open(code_file, "w") as f:
f.write(code)
docker_cmd = self._build_docker_command(
code_file=code_file,
language=language,
limits=limits,
env=env,
working_dir=working_dir,
)
started_at = time.time()
try:
proc = await asyncio.create_subprocess_exec(
*docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=limits.timeout_seconds,
)
completed_at = time.time()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.COMPLETED,
exit_code=proc.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
duration_seconds=completed_at - started_at,
started_at=started_at,
completed_at=completed_at,
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.TIMEOUT,
error=f"Execution timed out after {limits.timeout_seconds}s",
duration_seconds=limits.timeout_seconds,
started_at=started_at,
completed_at=time.time(),
)
except Exception as e:
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.FAILED,
error=str(e),
duration_seconds=time.time() - started_at,
started_at=started_at,
completed_at=time.time(),
)
finally:
if os.path.exists(code_file):
os.remove(code_file)
async def execute_file(
self,
file_path: str,
args: Optional[List[str]] = None,
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
) -> SandboxResult:
"""Execute a file in the sandbox."""
if not os.path.exists(file_path):
return SandboxResult(
status=SandboxStatus.FAILED,
error=f"File not found: {file_path}",
)
with open(file_path, "r") as f:
code = f.read()
return await self.execute(code, limits=limits, env=env)
async def run_command(
self,
command: Union[str, List[str]],
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Run a shell command in the sandbox."""
if not self._is_running:
await self.start()
limits = limits or self.config.resource_limits
execution_id = str(uuid.uuid4())
if isinstance(command, list):
cmd_str = " ".join(command)
else:
cmd_str = command
docker_cmd = [
"docker", "run", "--rm",
"--memory", f"{limits.memory_mb}m",
"--cpus", str(limits.cpu_percent / 100),
]
if not limits.network_enabled:
docker_cmd.extend(["--network", "none"])
if env:
for key, value in env.items():
docker_cmd.extend(["-e", f"{key}={value}"])
if working_dir:
docker_cmd.extend(["-w", working_dir])
docker_cmd.extend([self._image, "sh", "-c", cmd_str])
started_at = time.time()
try:
proc = await asyncio.create_subprocess_exec(
*docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=limits.timeout_seconds,
)
completed_at = time.time()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.COMPLETED,
exit_code=proc.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
duration_seconds=completed_at - started_at,
started_at=started_at,
completed_at=completed_at,
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.TIMEOUT,
error=f"Command timed out after {limits.timeout_seconds}s",
duration_seconds=limits.timeout_seconds,
started_at=started_at,
completed_at=time.time(),
)
except Exception as e:
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.FAILED,
error=str(e),
duration_seconds=time.time() - started_at,
started_at=started_at,
completed_at=time.time(),
)
async def write_file(
self,
path: str,
content: Union[str, bytes],
) -> bool:
"""Write a file to the sandbox."""
if not self._temp_dir:
return False
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
os.makedirs(os.path.dirname(full_path), exist_ok=True)
try:
mode = "wb" if isinstance(content, bytes) else "w"
with open(full_path, mode) as f:
f.write(content)
return True
except Exception as e:
logger.error(f"Failed to write file: {e}")
return False
async def read_file(
self,
path: str,
) -> Optional[Union[str, bytes]]:
"""Read a file from the sandbox."""
if not self._temp_dir:
return None
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
if not os.path.exists(full_path):
return None
try:
with open(full_path, "r") as f:
return f.read()
except UnicodeDecodeError:
with open(full_path, "rb") as f:
return f.read()
except Exception:
return None
async def list_files(
self,
path: str = "/",
) -> List[str]:
"""List files in a sandbox directory."""
if not self._temp_dir:
return []
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
if not os.path.exists(full_path):
return []
try:
files = []
for root, dirs, filenames in os.walk(full_path):
for filename in filenames:
rel_path = os.path.relpath(
os.path.join(root, filename),
self._temp_dir,
)
files.append("/" + rel_path)
return files
except Exception:
return []
def get_status(self) -> Dict[str, Any]:
"""Get sandbox status information."""
return {
"available": self.is_available,
"type": self.sandbox_type,
"running": self._is_running,
"image": self._image,
"container_id": self._container_id,
"temp_dir": self._temp_dir,
}
async def cleanup(self) -> None:
"""Clean up sandbox resources."""
if self._temp_dir and os.path.exists(self._temp_dir):
import shutil
shutil.rmtree(self._temp_dir, ignore_errors=True)
self._temp_dir = tempfile.mkdtemp(prefix="praisonai_sandbox_")
async def reset(self) -> None:
"""Reset sandbox to initial state."""
await self.stop()
await self.start()
def _build_docker_command(
self,
code_file: str,
language: str,
limits: ResourceLimits,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> List[str]:
"""Build the Docker run command."""
docker_cmd = [
"docker", "run", "--rm",
"--memory", f"{limits.memory_mb}m",
"--cpus", str(limits.cpu_percent / 100),
"--pids-limit", str(limits.max_processes),
]
if not limits.network_enabled:
docker_cmd.extend(["--network", "none"])
docker_cmd.extend(["-v", f"{code_file}:/code/script.py:ro"])
if env:
for key, value in env.items():
docker_cmd.extend(["-e", f"{key}={value}"])
docker_cmd.extend(["-w", working_dir or "/code"])
docker_cmd.append(self._image)
if language == "python":
docker_cmd.extend(["python", "/code/script.py"])
elif language == "bash":
docker_cmd.extend(["bash", "/code/script.py"])
else:
docker_cmd.extend(["python", "/code/script.py"])
return docker_cmd
@@ -0,0 +1,349 @@
"""
Subprocess Sandbox implementation for PraisonAI.
Provides isolated code execution using subprocess with resource limits.
"""
from __future__ import annotations
import asyncio
import logging
import os
import tempfile
import time
import uuid
from typing import Any, Dict, List, Optional, Union
from praisonaiagents.sandbox import (
SandboxConfig,
SandboxResult,
SandboxStatus,
ResourceLimits,
)
logger = logging.getLogger(__name__)
class SubprocessSandbox:
"""Subprocess-based sandbox for code execution.
Executes code in isolated subprocesses with timeout limits.
Less secure than Docker but works without Docker installation.
Example:
from praisonai.sandbox import SubprocessSandbox
sandbox = SubprocessSandbox()
result = await sandbox.execute("print('Hello, World!')")
print(result.stdout) # Hello, World!
"""
def __init__(
self,
config: Optional[SandboxConfig] = None,
):
"""Initialize the subprocess sandbox.
Args:
config: Optional sandbox configuration
"""
self.config = config or SandboxConfig.subprocess()
self._is_running = False
self._temp_dir: Optional[str] = None
@property
def is_available(self) -> bool:
"""Subprocess sandbox is always available."""
return True
@property
def sandbox_type(self) -> str:
return "subprocess"
async def start(self) -> None:
"""Start/initialize the sandbox environment."""
if self._is_running:
return
self._temp_dir = tempfile.mkdtemp(prefix="praisonai_sandbox_")
self._is_running = True
logger.info("Subprocess sandbox initialized")
async def stop(self) -> None:
"""Stop/cleanup the sandbox environment."""
if not self._is_running:
return
if self._temp_dir and os.path.exists(self._temp_dir):
import shutil
shutil.rmtree(self._temp_dir, ignore_errors=True)
self._is_running = False
logger.info("Subprocess sandbox stopped")
async def execute(
self,
code: str,
language: str = "python",
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Execute code in the sandbox."""
if not self._is_running:
await self.start()
limits = limits or self.config.resource_limits
execution_id = str(uuid.uuid4())
code_file = os.path.join(self._temp_dir, f"code_{execution_id}.py")
with open(code_file, "w") as f:
f.write(code)
if language == "python":
cmd = ["python", code_file]
elif language == "bash":
cmd = ["bash", code_file]
else:
cmd = ["python", code_file]
process_env = os.environ.copy()
if env:
process_env.update(env)
started_at = time.time()
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=working_dir or self._temp_dir,
env=process_env,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=limits.timeout_seconds,
)
completed_at = time.time()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.COMPLETED,
exit_code=proc.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
duration_seconds=completed_at - started_at,
started_at=started_at,
completed_at=completed_at,
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.TIMEOUT,
error=f"Execution timed out after {limits.timeout_seconds}s",
duration_seconds=limits.timeout_seconds,
started_at=started_at,
completed_at=time.time(),
)
except Exception as e:
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.FAILED,
error=str(e),
duration_seconds=time.time() - started_at,
started_at=started_at,
completed_at=time.time(),
)
finally:
if os.path.exists(code_file):
os.remove(code_file)
async def execute_file(
self,
file_path: str,
args: Optional[List[str]] = None,
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
) -> SandboxResult:
"""Execute a file in the sandbox."""
if not os.path.exists(file_path):
return SandboxResult(
status=SandboxStatus.FAILED,
error=f"File not found: {file_path}",
)
with open(file_path, "r") as f:
code = f.read()
return await self.execute(code, limits=limits, env=env)
async def run_command(
self,
command: Union[str, List[str]],
limits: Optional[ResourceLimits] = None,
env: Optional[Dict[str, str]] = None,
working_dir: Optional[str] = None,
) -> SandboxResult:
"""Run a shell command in the sandbox."""
if not self._is_running:
await self.start()
limits = limits or self.config.resource_limits
execution_id = str(uuid.uuid4())
if isinstance(command, str):
cmd = ["sh", "-c", command]
else:
cmd = command
process_env = os.environ.copy()
if env:
process_env.update(env)
started_at = time.time()
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=working_dir or self._temp_dir,
env=process_env,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=limits.timeout_seconds,
)
completed_at = time.time()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.COMPLETED,
exit_code=proc.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
duration_seconds=completed_at - started_at,
started_at=started_at,
completed_at=completed_at,
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.TIMEOUT,
error=f"Command timed out after {limits.timeout_seconds}s",
duration_seconds=limits.timeout_seconds,
started_at=started_at,
completed_at=time.time(),
)
except Exception as e:
return SandboxResult(
execution_id=execution_id,
status=SandboxStatus.FAILED,
error=str(e),
duration_seconds=time.time() - started_at,
started_at=started_at,
completed_at=time.time(),
)
async def write_file(
self,
path: str,
content: Union[str, bytes],
) -> bool:
"""Write a file to the sandbox."""
if not self._temp_dir:
return False
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
os.makedirs(os.path.dirname(full_path), exist_ok=True)
try:
mode = "wb" if isinstance(content, bytes) else "w"
with open(full_path, mode) as f:
f.write(content)
return True
except Exception as e:
logger.error(f"Failed to write file: {e}")
return False
async def read_file(
self,
path: str,
) -> Optional[Union[str, bytes]]:
"""Read a file from the sandbox."""
if not self._temp_dir:
return None
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
if not os.path.exists(full_path):
return None
try:
with open(full_path, "r") as f:
return f.read()
except UnicodeDecodeError:
with open(full_path, "rb") as f:
return f.read()
except Exception:
return None
async def list_files(
self,
path: str = "/",
) -> List[str]:
"""List files in a sandbox directory."""
if not self._temp_dir:
return []
full_path = os.path.join(self._temp_dir, path.lstrip("/"))
if not os.path.exists(full_path):
return []
try:
files = []
for root, dirs, filenames in os.walk(full_path):
for filename in filenames:
rel_path = os.path.relpath(
os.path.join(root, filename),
self._temp_dir,
)
files.append("/" + rel_path)
return files
except Exception:
return []
def get_status(self) -> Dict[str, Any]:
"""Get sandbox status information."""
return {
"available": self.is_available,
"type": self.sandbox_type,
"running": self._is_running,
"temp_dir": self._temp_dir,
}
async def cleanup(self) -> None:
"""Clean up sandbox resources."""
if self._temp_dir and os.path.exists(self._temp_dir):
import shutil
shutil.rmtree(self._temp_dir, ignore_errors=True)
self._temp_dir = tempfile.mkdtemp(prefix="praisonai_sandbox_")
async def reset(self) -> None:
"""Reset sandbox to initial state."""
await self.stop()
await self.start()
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.10.27"
__version__ = "3.11.0"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.13.23",
"praisonaiagents>=0.14.0",
"python-dotenv>=0.19.0",
"litellm>=1.81.0",
"PyYAML>=6.0",
+4 -4
View File
@@ -3928,7 +3928,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.13.23" },
{ name = "praisonaiagents", specifier = ">=0.14.0" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pyngrok", marker = "extra == 'call'", specifier = ">=1.4.0" },
{ name = "pyparsing", specifier = ">=3.0.0" },
@@ -3973,7 +3973,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.13.23"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -3982,9 +3982,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/99/c285084ea9680f12e8dd07c647014dcb2dd1fa0dac021ed0656b499b1682/praisonaiagents-0.13.23.tar.gz", hash = "sha256:58e86576a451af24f44059439cc851a317b433ef1a7770d5f3e7c97da64e05aa", size = 1132960, upload-time = "2026-01-28T13:15:37.788Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/12/2093300d257228b7886f53dc598a126aa963e178a1949c462dcd502e3d11/praisonaiagents-0.14.0.tar.gz", hash = "sha256:20190c57c9602bb7a4e98d2a2d4b91d47a4da66344a3f9d1bfbe5c015d52da13", size = 1148506, upload-time = "2026-01-29T10:41:42.19Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/f3/9131343fe7163ca2d7f4d70a2764bd0b30d227c73c28bbc08cb722918b68/praisonaiagents-0.13.23-py3-none-any.whl", hash = "sha256:a5b35c19e4b945ad841e4d06172773953558d2e85eb60e17262912a8ef608352", size = 1114213, upload-time = "2026-01-28T13:15:35.688Z" },
{ url = "https://files.pythonhosted.org/packages/24/9a/28c99919c9a1f95644916aa2c831d173d95da142730d9fcb61563e63c4f0/praisonaiagents-0.14.0-py3-none-any.whl", hash = "sha256:b45c768b6240a5fcd6e4e7c82c9ab9f9ec544ed6fa615df42a95578b35699639", size = 1133604, upload-time = "2026-01-29T10:41:39.718Z" },
]
[[package]]