mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 04:35:58 -10:00
Release v3.11.0
This commit is contained in:
1 parent
d389ff348f
commit
a843e13641
59 files changed
+10726
-50
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]" \
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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."""
|
||||
...
|
||||
@@ -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"
|
||||
Generated
+1
-1
@@ -2549,7 +2549,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.13.23"
|
||||
version = "0.14.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.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]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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 @@
|
||||
__version__ = "3.10.27"
|
||||
__version__ = "3.11.0"
|
||||
@@ -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",
|
||||
|
||||
Generated
+4
-4
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user