mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 01:41:26 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a14ca673da | ||
|
|
02d9d1e04f | ||
|
|
d3ee549ccc | ||
|
|
63fbd04af3 | ||
|
|
51f95ad904 | ||
|
|
44b800df0e | ||
|
|
6e3a99b8b6 | ||
|
|
b7d53b8f11 | ||
|
|
643fac5762 | ||
|
|
92b71cbc8f | ||
|
|
d7555ba1b2 | ||
|
|
22dd13ec66 | ||
|
|
ba40e818e4 | ||
|
|
12f829321c | ||
|
|
149ed5bd94 | ||
|
|
15f2937dd5 | ||
|
|
d28199a18d | ||
|
|
0d3d6bb852 | ||
|
|
5f93f83c9f | ||
|
|
c9786fbd1e | ||
|
|
95e8e38998 |
No files matched your search
@@ -23,7 +23,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Agent, Agents, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
|
||||
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -80,6 +80,7 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str = None, **kwargs)</code>
|
||||
@@ -89,45 +90,6 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
|
||||
@@ -240,7 +202,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Loop, Parallel, Pipeline, Repeat, Route, Workflow, WorkflowStep, loop, parallel, repeat, route
|
||||
from praisonaiagents import Loop, Parallel, Pipeline, Repeat, Route, Workflow, loop, parallel, repeat, route
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -262,22 +224,6 @@ Methods:
|
||||
* <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>
|
||||
@@ -734,11 +680,50 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import EmbeddingResult, Task, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
from praisonaiagents import AgentManager, EmbeddingResult, Task, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
|
||||
@@ -753,7 +738,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
|
||||
from praisonai import AgentApp, CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
|
||||
```
|
||||
|
||||
# CLI
|
||||
@@ -765,6 +750,7 @@ Methods:
|
||||
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
|
||||
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
@@ -778,6 +764,19 @@ Methods:
|
||||
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot bot-callback <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot discord <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot slack <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot telegram <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser browser-callback <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser click <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser navigate <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser open <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser profiles <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser screenshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser snapshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser status <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser type <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
|
||||
@@ -881,6 +880,12 @@ Methods:
|
||||
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins doctor <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins enable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins info <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins list <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins plugins-callback <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
@@ -921,6 +926,11 @@ Methods:
|
||||
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox explain <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox list <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
@@ -954,7 +964,9 @@ Methods:
|
||||
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills check <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills eligible <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
|
||||
@@ -13,6 +13,12 @@ GOOGLE_API_KEY=your_google_api_key_here
|
||||
GROQ_API_KEY=your_groq_api_key_here
|
||||
COHERE_API_KEY=your_cohere_api_key_here
|
||||
|
||||
# Bot Tokens (for messaging bot services)
|
||||
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
|
||||
SLACK_APP_TOKEN=xapp-your-slack-app-token
|
||||
DISCORD_BOT_TOKEN=your-discord-bot-token
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
|
||||
# Optional: Service Configuration
|
||||
FLASK_ENV=development
|
||||
CHAINLIT_HOST=0.0.0.0
|
||||
|
||||
@@ -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.11.2" \
|
||||
"praisonai>=3.12.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.11.2" \
|
||||
"praisonai>=3.12.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.11.2" \
|
||||
"praisonai>=3.12.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bot Environment Variables
|
||||
# Copy to .env and fill in your values
|
||||
|
||||
# Required: LLM API Key
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Slack Bot (Socket Mode)
|
||||
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
|
||||
SLACK_APP_TOKEN=xapp-your-slack-app-token
|
||||
|
||||
# Discord Bot
|
||||
DISCORD_BOT_TOKEN=your-discord-bot-token
|
||||
|
||||
# Telegram Bot
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
@@ -0,0 +1,42 @@
|
||||
# Dockerfile for PraisonAI Slack Bot
|
||||
# Secrets are passed at runtime via environment variables
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
python3-dev \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create praison config directory
|
||||
RUN mkdir -p /root/.praison
|
||||
|
||||
# Install praisonai with bot dependencies
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai \
|
||||
slack-bolt \
|
||||
slack-sdk \
|
||||
"discord.py>=2.0.0" \
|
||||
"python-telegram-bot>=20.0"
|
||||
|
||||
# Set environment variables for directory management
|
||||
ENV PRAISON_CONFIG_DIR=/root/.praison
|
||||
ENV DOCKER_CONTAINER=true
|
||||
|
||||
# Bot tokens should be passed at runtime (not baked into image)
|
||||
# SLACK_BOT_TOKEN=xoxb-...
|
||||
# SLACK_APP_TOKEN=xapp-...
|
||||
|
||||
# Optional: Copy custom agent configuration
|
||||
# COPY agent.yaml /app/agent.yaml
|
||||
|
||||
# Expose no ports - Socket Mode doesn't need them
|
||||
# For webhook mode, expose port 3000
|
||||
|
||||
# Default command - tokens from environment
|
||||
CMD ["praisonai", "bot", "slack"]
|
||||
@@ -0,0 +1,66 @@
|
||||
# PraisonAI Bot Docker Deployment
|
||||
|
||||
Deploy Slack, Discord, or Telegram bots using Docker.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Copy environment template
|
||||
cp .env.template .env
|
||||
|
||||
# 2. Edit .env with your tokens
|
||||
nano .env
|
||||
|
||||
# 3. Run specific bot
|
||||
docker compose up slack-bot -d
|
||||
|
||||
# Or run all bots
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `OPENAI_API_KEY` | Yes | LLM API key |
|
||||
| `SLACK_BOT_TOKEN` | For Slack | Bot token (xoxb-...) |
|
||||
| `SLACK_APP_TOKEN` | For Slack | App token (xapp-...) |
|
||||
| `DISCORD_BOT_TOKEN` | For Discord | Bot token |
|
||||
| `TELEGRAM_BOT_TOKEN` | For Telegram | Bot token |
|
||||
|
||||
## Slack Setup
|
||||
|
||||
1. Create app at https://api.slack.com/apps
|
||||
2. Enable **Socket Mode** → Generate App-Level Token with `connections:write`
|
||||
3. Add **OAuth Scopes**: `chat:write`, `app_mentions:read`, `im:history`
|
||||
4. Enable **Event Subscriptions** → Subscribe to `app_mention`, `message.im`
|
||||
5. **Install to Workspace**
|
||||
6. Copy Bot Token and App Token to `.env`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Start Slack bot
|
||||
docker compose up slack-bot -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f slack-bot
|
||||
|
||||
# Stop bot
|
||||
docker compose down slack-bot
|
||||
|
||||
# Rebuild after updates
|
||||
docker compose build --no-cache slack-bot
|
||||
```
|
||||
|
||||
## Custom Agent Configuration
|
||||
|
||||
Mount your agent config:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
slack-bot:
|
||||
volumes:
|
||||
- ./agent.yaml:/app/agent.yaml
|
||||
command: ["praisonai", "bot", "slack", "--agent", "/app/agent.yaml"]
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
version: '3.8'
|
||||
|
||||
# PraisonAI Bot Services
|
||||
# Run specific bots: docker compose up slack-bot
|
||||
# Run all bots: docker compose up
|
||||
|
||||
services:
|
||||
# Slack Bot
|
||||
slack-bot:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/bots/Dockerfile
|
||||
volumes:
|
||||
- praison_data:/root/.praison
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
|
||||
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai", "bot", "slack" ]
|
||||
restart: unless-stopped
|
||||
|
||||
# Discord Bot
|
||||
discord-bot:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/bots/Dockerfile
|
||||
volumes:
|
||||
- praison_data:/root/.praison
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai", "bot", "discord" ]
|
||||
restart: unless-stopped
|
||||
|
||||
# Telegram Bot
|
||||
telegram-bot:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/bots/Dockerfile
|
||||
volumes:
|
||||
- praison_data:/root/.praison
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai", "bot", "telegram" ]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
praison_data:
|
||||
driver: local
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
CHAINLIT_DB_DIR: /root/.praison
|
||||
command: praisonai ui --host 0.0.0.0
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8082"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8082" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
CHAINLIT_DB_DIR: /root/.praison
|
||||
command: praisonai chat --host 0.0.0.0 --port 8083
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8083"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8083" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -89,6 +89,7 @@ services:
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
networks:
|
||||
- praison_network
|
||||
# For bot services (Slack, Discord, Telegram), see docker/bots/
|
||||
|
||||
volumes:
|
||||
praison_data:
|
||||
@@ -96,4 +97,4 @@ volumes:
|
||||
|
||||
networks:
|
||||
praison_network:
|
||||
driver: bridge
|
||||
driver: bridge
|
||||
@@ -22,6 +22,8 @@ Simpler than competitors • More extensible • Faster • Agent-centric
|
||||
| **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
|
||||
Easy and developer frienldy for non developers
|
||||
Easy and user friendly
|
||||
---
|
||||
|
||||
## 2. Repository Structure
|
||||
@@ -372,6 +374,9 @@ class MyTool(BaseTool):
|
||||
- `praisonaiagents/tools/decorator.py` - @tool decorator
|
||||
- `praisonaiagents/tools/registry.py` - Tool registry
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Agents only recognize parameters.** When creating tools, always expose all options as function parameters—agents cannot discover env vars or config files.
|
||||
|
||||
### 6.2 Hooks & Middleware
|
||||
|
||||
```python
|
||||
|
||||
@@ -222,8 +222,9 @@ _LAZY_IMPORTS = {
|
||||
'CodeAgent': ('praisonaiagents.agent.code_agent', 'CodeAgent'),
|
||||
'CodeConfig': ('praisonaiagents.agent.code_agent', 'CodeConfig'),
|
||||
|
||||
# Agents
|
||||
'Agents': ('praisonaiagents.agents.agents', 'Agents'),
|
||||
# Agents / AgentManager
|
||||
'AgentManager': ('praisonaiagents.agents.agents', 'AgentManager'),
|
||||
# Note: 'Agents' is handled by _custom_handler for deprecation warning
|
||||
'Task': ('praisonaiagents.task.task', 'Task'),
|
||||
'AutoAgents': ('praisonaiagents.agents.autoagents', 'AutoAgents'),
|
||||
'AutoRagAgent': ('praisonaiagents.agents.auto_rag_agent', 'AutoRagAgent'),
|
||||
@@ -233,6 +234,10 @@ _LAZY_IMPORTS = {
|
||||
# Session
|
||||
'Session': ('praisonaiagents.session', 'Session'),
|
||||
|
||||
# App (AgentApp protocol and config)
|
||||
'AgentAppProtocol': ('praisonaiagents.app.protocols', 'AgentAppProtocol'),
|
||||
'AgentAppConfig': ('praisonaiagents.app.config', 'AgentAppConfig'),
|
||||
|
||||
# MCP (optional)
|
||||
'MCP': ('praisonaiagents.mcp.mcp', 'MCP'),
|
||||
|
||||
@@ -420,19 +425,46 @@ def _custom_handler(name, cache):
|
||||
"""Handle special cases that need custom logic."""
|
||||
import warnings
|
||||
|
||||
# PraisonAIAgents deprecation warning
|
||||
if name == "PraisonAIAgents":
|
||||
# Agents deprecation warning (use AgentManager instead)
|
||||
if name == "Agents":
|
||||
warnings.warn(
|
||||
"PraisonAIAgents is deprecated, use Agents instead. "
|
||||
"Example: from praisonaiagents import Agents",
|
||||
"Agents is deprecated, use AgentManager instead. "
|
||||
"Example: from praisonaiagents import AgentManager",
|
||||
DeprecationWarning,
|
||||
stacklevel=4
|
||||
)
|
||||
value = lazy_import('praisonaiagents.agents.agents', 'Agents', cache)
|
||||
value = lazy_import('praisonaiagents.agents.agents', 'AgentManager', cache)
|
||||
cache['AgentManager'] = value
|
||||
cache['Agents'] = value
|
||||
return value
|
||||
|
||||
# PraisonAIAgents deprecation warning (use AgentManager instead)
|
||||
if name == "PraisonAIAgents":
|
||||
warnings.warn(
|
||||
"PraisonAIAgents is deprecated, use AgentManager instead. "
|
||||
"Example: from praisonaiagents import AgentManager",
|
||||
DeprecationWarning,
|
||||
stacklevel=4
|
||||
)
|
||||
value = lazy_import('praisonaiagents.agents.agents', 'AgentManager', cache)
|
||||
cache['AgentManager'] = value
|
||||
cache['PraisonAIAgents'] = value
|
||||
return value
|
||||
|
||||
# WorkflowStep deprecation warning (use Task instead) - Phase 4 Consolidation
|
||||
if name == "WorkflowStep":
|
||||
warnings.warn(
|
||||
"WorkflowStep is deprecated, use Task instead. "
|
||||
"Task now supports all WorkflowStep features including action, handler, loop_over, etc. "
|
||||
"Example: from praisonaiagents import Task",
|
||||
DeprecationWarning,
|
||||
stacklevel=4
|
||||
)
|
||||
value = lazy_import('praisonaiagents.task.task', 'Task', cache)
|
||||
cache['Task'] = value
|
||||
cache['WorkflowStep'] = value
|
||||
return value
|
||||
|
||||
# Module imports (return the module itself)
|
||||
if name == 'memory':
|
||||
import importlib
|
||||
@@ -567,7 +599,8 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
|
||||
__all__ = [
|
||||
# Core classes - the essentials
|
||||
'Agent',
|
||||
'Agents',
|
||||
'AgentManager', # Primary class for multi-agent coordination (v0.14.16+)
|
||||
'Agents', # Deprecated alias for AgentManager
|
||||
'Task',
|
||||
|
||||
# Tool essentials
|
||||
|
||||
@@ -3841,33 +3841,38 @@ Your Goal: {self.goal}"""
|
||||
"""Internal tool execution implementation."""
|
||||
|
||||
# Check if approval is required for this tool
|
||||
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, get_approval_callback
|
||||
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, get_approval_callback, is_env_auto_approve, is_yaml_approved
|
||||
if is_approval_required(function_name):
|
||||
risk_level = get_risk_level(function_name)
|
||||
logging.debug(f"Tool {function_name} requires approval (risk level: {risk_level})")
|
||||
|
||||
# Use global approval callback or default console callback
|
||||
callback = get_approval_callback() or console_approval_callback
|
||||
|
||||
try:
|
||||
decision = callback(function_name, arguments, risk_level)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
# Mark as approved in context to prevent double approval in decorator
|
||||
# Skip approval if auto-approve env var is set or tool is YAML-approved
|
||||
if is_env_auto_approve() or is_yaml_approved(function_name):
|
||||
logging.debug(f"Tool {function_name} auto-approved (env={is_env_auto_approve()}, yaml={is_yaml_approved(function_name)})")
|
||||
mark_approved(function_name)
|
||||
else:
|
||||
risk_level = get_risk_level(function_name)
|
||||
logging.debug(f"Tool {function_name} requires approval (risk level: {risk_level})")
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
# Use global approval callback or default console callback
|
||||
callback = get_approval_callback() or console_approval_callback
|
||||
|
||||
try:
|
||||
decision = callback(function_name, arguments, risk_level)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error during approval process: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg, "approval_error": True}
|
||||
# Mark as approved in context to prevent double approval in decorator
|
||||
mark_approved(function_name)
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error during approval process: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg, "approval_error": True}
|
||||
|
||||
# Special handling for MCP tools
|
||||
# Check if tools is an MCP instance with the requested function name
|
||||
@@ -6560,18 +6565,23 @@ Write the complete compiled report:"""
|
||||
logging.info(f"Executing async tool: {function_name} with arguments: {arguments}")
|
||||
|
||||
# Check if approval is required for this tool
|
||||
from ..approval import is_approval_required, request_approval
|
||||
from ..approval import is_approval_required, request_approval, is_env_auto_approve, is_yaml_approved, mark_approved
|
||||
if is_approval_required(function_name):
|
||||
decision = await request_approval(function_name, arguments)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
# Skip approval if auto-approve env var is set or tool is YAML-approved
|
||||
if is_env_auto_approve() or is_yaml_approved(function_name):
|
||||
logging.debug(f"Tool {function_name} auto-approved (env={is_env_auto_approve()}, yaml={is_yaml_approved(function_name)})")
|
||||
mark_approved(function_name)
|
||||
else:
|
||||
decision = await request_approval(function_name, arguments)
|
||||
if not decision.approved:
|
||||
error_msg = f"Tool execution denied: {decision.reason}"
|
||||
logging.warning(error_msg)
|
||||
return {"error": error_msg, "approval_denied": True}
|
||||
|
||||
# Use modified arguments if provided
|
||||
if decision.modified_args:
|
||||
arguments = decision.modified_args
|
||||
logging.info(f"Using modified arguments: {arguments}")
|
||||
|
||||
# Try to find the function in the agent's tools list first
|
||||
func = None
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"""Agents module for managing multiple AI agents"""
|
||||
from .agents import Agents, PraisonAIAgents
|
||||
"""Agents module for managing multiple AI agents.
|
||||
|
||||
AgentManager is the primary class for multi-agent coordination.
|
||||
Agents is a deprecated alias for backward compatibility.
|
||||
"""
|
||||
from .agents import AgentManager, Agents, PraisonAIAgents
|
||||
from .autoagents import AutoAgents
|
||||
from .auto_rag_agent import AutoRagAgent, AutoRagConfig, RetrievalPolicy
|
||||
from .protocols import MergeStrategyProtocol, FirstWinsMerge, ConcatMerge, DictMerge
|
||||
|
||||
__all__ = [
|
||||
'Agents', 'PraisonAIAgents', 'AutoAgents',
|
||||
'AgentManager', # Primary class (v0.14.16+)
|
||||
'Agents', # Deprecated alias for AgentManager
|
||||
'PraisonAIAgents', # Deprecated alias for AgentManager
|
||||
'AutoAgents',
|
||||
'AutoRagAgent', 'AutoRagConfig', 'RetrievalPolicy',
|
||||
'MergeStrategyProtocol', 'FirstWinsMerge', 'ConcatMerge', 'DictMerge',
|
||||
]
|
||||
@@ -169,7 +169,34 @@ def process_task_context(context_item, verbose=0, user_id=None):
|
||||
else:
|
||||
return str(context_item) # Fallback for unknown types
|
||||
|
||||
class Agents:
|
||||
class AgentManager:
|
||||
"""
|
||||
Multi-agent coordinator that manages and delegates work to multiple agents.
|
||||
|
||||
AgentManager orchestrates the execution of tasks across multiple Agent instances,
|
||||
supporting sequential, parallel, and hierarchical execution patterns.
|
||||
|
||||
Example:
|
||||
from praisonaiagents import Agent, AgentManager, Task
|
||||
|
||||
researcher = Agent(role="Researcher", instructions="Research topics")
|
||||
writer = Agent(role="Writer", instructions="Write content")
|
||||
|
||||
task1 = Task(description="Research AI trends", agent=researcher)
|
||||
task2 = Task(description="Write article", agent=writer)
|
||||
|
||||
manager = AgentManager(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
process="sequential"
|
||||
)
|
||||
result = manager.start()
|
||||
|
||||
Note:
|
||||
The class was renamed from `Agents` to `AgentManager` in v0.14.16.
|
||||
`Agents` remains as a deprecated alias for backward compatibility.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agents,
|
||||
@@ -194,7 +221,7 @@ class Agents:
|
||||
caching: Optional[Any] = None, # Union[bool, CachingConfig] - caching
|
||||
):
|
||||
"""
|
||||
Initialize PraisonAIAgents with consolidated feature parameters.
|
||||
Initialize AgentManager with consolidated feature parameters.
|
||||
|
||||
Args:
|
||||
agents: List of Agent instances
|
||||
@@ -2373,6 +2400,7 @@ Context:
|
||||
# Keep plan tasks for results but note original tasks are preserved in _plan_tasks
|
||||
|
||||
|
||||
# Backward compatibility alias - Agents points to Agents
|
||||
Agents = Agents
|
||||
PraisonAIAgents = Agents
|
||||
# Backward compatibility aliases
|
||||
# Agents is deprecated in favor of AgentManager (v0.14.16+)
|
||||
Agents = AgentManager
|
||||
PraisonAIAgents = AgentManager
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
App module for production deployment of AI agents.
|
||||
|
||||
This module provides the protocol and configuration for AgentApp,
|
||||
a production platform for deploying agents as web services.
|
||||
|
||||
The protocol is defined here in the core SDK (lightweight).
|
||||
The implementation lives in the praisonai wrapper (heavy deps like FastAPI).
|
||||
|
||||
Example:
|
||||
# Protocol is importable from core SDK
|
||||
from praisonaiagents import AgentAppProtocol, AgentAppConfig
|
||||
|
||||
# Implementation is in wrapper
|
||||
from praisonai import AgentApp
|
||||
|
||||
app = AgentApp(
|
||||
name="My App",
|
||||
agents=[researcher, writer],
|
||||
)
|
||||
app.serve(port=8000)
|
||||
"""
|
||||
|
||||
from .protocols import AgentAppProtocol
|
||||
from .config import AgentAppConfig
|
||||
|
||||
__all__ = [
|
||||
'AgentAppProtocol',
|
||||
'AgentAppConfig',
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
AgentApp configuration.
|
||||
|
||||
This module defines the configuration dataclass for AgentApp.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentAppConfig:
|
||||
"""
|
||||
Configuration for AgentApp.
|
||||
|
||||
This dataclass holds all configuration options for an AgentApp instance.
|
||||
It follows the PraisonAI principle of sensible defaults with explicit overrides.
|
||||
|
||||
Attributes:
|
||||
name: Name of the application (default: "PraisonAI App")
|
||||
host: Host address to bind to (default: "0.0.0.0")
|
||||
port: Port number to listen on (default: 8000)
|
||||
reload: Enable auto-reload for development (default: False)
|
||||
cors_origins: List of allowed CORS origins (default: ["*"])
|
||||
api_prefix: API route prefix (default: "/api")
|
||||
docs_url: URL for API documentation (default: "/docs")
|
||||
openapi_url: URL for OpenAPI schema (default: "/openapi.json")
|
||||
debug: Enable debug mode (default: False)
|
||||
log_level: Logging level (default: "info")
|
||||
workers: Number of worker processes (default: 1)
|
||||
timeout: Request timeout in seconds (default: 60)
|
||||
metadata: Additional metadata for the app
|
||||
|
||||
Example:
|
||||
config = AgentAppConfig(
|
||||
name="My AI App",
|
||||
port=9000,
|
||||
reload=True,
|
||||
debug=True
|
||||
)
|
||||
"""
|
||||
|
||||
name: str = "PraisonAI App"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
reload: bool = False
|
||||
cors_origins: List[str] = field(default_factory=lambda: ["*"])
|
||||
api_prefix: str = "/api"
|
||||
docs_url: str = "/docs"
|
||||
openapi_url: str = "/openapi.json"
|
||||
debug: bool = False
|
||||
log_level: str = "info"
|
||||
workers: int = 1
|
||||
timeout: int = 60
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
AgentApp protocol definitions.
|
||||
|
||||
This module defines the protocol (interface) for AgentApp implementations.
|
||||
The protocol is lightweight and lives in the core SDK.
|
||||
Implementations live in the praisonai wrapper.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agent.agent import Agent
|
||||
from ..agents.agents import AgentManager
|
||||
from ..workflows.workflows import Workflow
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AgentAppProtocol(Protocol):
|
||||
"""
|
||||
Protocol for AgentApp implementations.
|
||||
|
||||
AgentApp is a production platform for deploying agents as web services.
|
||||
It wraps agents, managers, and workflows into a unified API server.
|
||||
|
||||
Implementations should:
|
||||
- Provide FastAPI-based HTTP endpoints
|
||||
- Support WebSocket connections for real-time communication
|
||||
- Handle agent lifecycle management
|
||||
- Provide health checks and monitoring endpoints
|
||||
|
||||
Example implementation (in praisonai wrapper):
|
||||
class AgentApp:
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "PraisonAI App",
|
||||
agents: List[Agent] = None,
|
||||
managers: List[AgentManager] = None,
|
||||
workflows: List[Workflow] = None,
|
||||
):
|
||||
...
|
||||
|
||||
def serve(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
'''Start the FastAPI server.'''
|
||||
...
|
||||
|
||||
def get_app(self):
|
||||
'''Get the FastAPI app instance for custom mounting.'''
|
||||
...
|
||||
"""
|
||||
|
||||
def serve(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
reload: bool = False,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Start the AgentApp server.
|
||||
|
||||
Args:
|
||||
host: Host address to bind to (default: "0.0.0.0")
|
||||
port: Port number to listen on (default: 8000)
|
||||
reload: Enable auto-reload for development (default: False)
|
||||
**kwargs: Additional server configuration
|
||||
"""
|
||||
...
|
||||
|
||||
def get_app(self) -> Any:
|
||||
"""
|
||||
Get the underlying web application instance.
|
||||
|
||||
Returns:
|
||||
The FastAPI application instance for custom mounting or configuration.
|
||||
"""
|
||||
...
|
||||
@@ -40,6 +40,15 @@ class BotConfig:
|
||||
max_message_length: int = 4096
|
||||
retry_attempts: int = 3
|
||||
timeout: int = 30
|
||||
reply_in_thread: bool = False # Default to inline, set True for thread replies
|
||||
thread_threshold: int = 500 # Auto-thread responses longer than this (0 = disabled)
|
||||
|
||||
# Default tools enabled for all bots
|
||||
default_tools: List[str] = field(default_factory=lambda: ["execute_command", "search_web"])
|
||||
|
||||
# Auto-approve tool calls (useful for trusted environments)
|
||||
auto_approve_tools: bool = False # If True, skip confirmation for tool execution
|
||||
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -57,6 +66,10 @@ class BotConfig:
|
||||
"max_message_length": self.max_message_length,
|
||||
"retry_attempts": self.retry_attempts,
|
||||
"timeout": self.timeout,
|
||||
"reply_in_thread": self.reply_in_thread,
|
||||
"thread_threshold": self.thread_threshold,
|
||||
"default_tools": self.default_tools,
|
||||
"auto_approve_tools": self.auto_approve_tools,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,47 @@ from typing import Optional, Dict, Any, List, Callable, Union, Literal
|
||||
|
||||
class HookEvent(str, Enum):
|
||||
"""Event names for the hook system."""
|
||||
# Tool lifecycle
|
||||
BEFORE_TOOL = "before_tool"
|
||||
AFTER_TOOL = "after_tool"
|
||||
|
||||
# Agent lifecycle
|
||||
BEFORE_AGENT = "before_agent"
|
||||
AFTER_AGENT = "after_agent"
|
||||
|
||||
# Session lifecycle
|
||||
SESSION_START = "session_start"
|
||||
SESSION_END = "session_end"
|
||||
|
||||
# LLM lifecycle
|
||||
BEFORE_LLM = "before_llm"
|
||||
AFTER_LLM = "after_llm"
|
||||
|
||||
# Error handling
|
||||
ON_ERROR = "on_error"
|
||||
ON_RETRY = "on_retry"
|
||||
|
||||
# Message lifecycle (for bot/channel integrations)
|
||||
MESSAGE_RECEIVED = "message_received"
|
||||
MESSAGE_SENDING = "message_sending"
|
||||
MESSAGE_SENT = "message_sent"
|
||||
|
||||
# Gateway lifecycle
|
||||
GATEWAY_START = "gateway_start"
|
||||
GATEWAY_STOP = "gateway_stop"
|
||||
|
||||
# Compaction (memory management)
|
||||
BEFORE_COMPACTION = "before_compaction"
|
||||
AFTER_COMPACTION = "after_compaction"
|
||||
|
||||
# Tool result persistence (for modifying tool results before storage)
|
||||
TOOL_RESULT_PERSIST = "tool_result_persist"
|
||||
|
||||
# Claude Code parity events
|
||||
USER_PROMPT_SUBMIT = "user_prompt_submit" # When user submits a prompt
|
||||
NOTIFICATION = "notification" # When notification is sent
|
||||
SUBAGENT_STOP = "subagent_stop" # When subagent completes
|
||||
SETUP = "setup" # On initialization/maintenance
|
||||
|
||||
|
||||
# Decision types for hook outputs
|
||||
|
||||
@@ -799,15 +799,26 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
# For Ollama, always generate summary when we have tool results
|
||||
# This prevents infinite loops caused by empty/minimal responses
|
||||
|
||||
# Filter out error results first
|
||||
# Filter out error results and collect error messages
|
||||
valid_results = []
|
||||
error_messages = []
|
||||
for result in tool_results:
|
||||
# Skip error responses
|
||||
# Check for error responses
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
error_messages.append(result.get('error', 'Unknown error'))
|
||||
continue
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
first_item = result[0]
|
||||
if isinstance(first_item, dict) and 'error' in first_item:
|
||||
error_messages.append(first_item.get('error', 'Unknown error'))
|
||||
continue
|
||||
valid_results.append(result)
|
||||
|
||||
# If no valid results, return None to continue
|
||||
# If no valid results but we have errors, return a friendly error message
|
||||
if not valid_results and error_messages:
|
||||
return "I'm sorry, but I wasn't able to complete the search. The service encountered some issues. Please try again later or try a different search query."
|
||||
|
||||
# If no valid results at all, return None to let the loop continue
|
||||
if not valid_results:
|
||||
return None
|
||||
|
||||
@@ -818,6 +829,12 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
# For simple numeric results, create a more natural response
|
||||
if isinstance(result, (int, float)):
|
||||
return f"The result is {result}."
|
||||
# For search results (list of dicts with title/url/snippet), format nicely
|
||||
elif isinstance(result, list) and len(result) > 0 and isinstance(result[0], dict):
|
||||
first_item = result[0]
|
||||
# Check if this looks like search results
|
||||
if 'title' in first_item or 'url' in first_item or 'snippet' in first_item:
|
||||
return self._format_search_results_summary(result)
|
||||
return str(result)
|
||||
else:
|
||||
# Multiple tool results - create coherent summary
|
||||
@@ -840,21 +857,71 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
|
||||
# Join the parts naturally
|
||||
return " ".join(summary_parts)
|
||||
|
||||
def _format_search_results_summary(self, results: List[Dict]) -> str:
|
||||
"""Format search results into a natural language summary."""
|
||||
if not results:
|
||||
return "No search results found."
|
||||
|
||||
lines = ["Here's what I found:\n"]
|
||||
for i, item in enumerate(results[:5], 1): # Limit to 5 results
|
||||
title = item.get('title', 'Untitled')
|
||||
url = item.get('url', '')
|
||||
snippet = item.get('snippet', item.get('content', ''))
|
||||
|
||||
lines.append(f"**{i}. {title}**")
|
||||
if snippet:
|
||||
# Truncate long snippets
|
||||
if len(snippet) > 200:
|
||||
snippet = snippet[:200] + "..."
|
||||
lines.append(f" {snippet}")
|
||||
if url:
|
||||
lines.append(f" Link: {url}")
|
||||
lines.append("") # Empty line between results
|
||||
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
def _format_ollama_tool_result_message(self, function_name: str, tool_result: Any) -> Dict[str, str]:
|
||||
"""
|
||||
Format tool result message for Ollama provider.
|
||||
Enhanced to instruct model to use the result for final answer.
|
||||
Handles error results with specific instructions to provide user-friendly responses.
|
||||
"""
|
||||
tool_result_str = str(tool_result)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"""Tool execution complete.
|
||||
# Check if the result is an error
|
||||
is_error = False
|
||||
error_message = None
|
||||
|
||||
if isinstance(tool_result, dict) and 'error' in tool_result:
|
||||
is_error = True
|
||||
error_message = tool_result.get('error', 'Unknown error')
|
||||
elif isinstance(tool_result, list) and len(tool_result) > 0:
|
||||
first_item = tool_result[0]
|
||||
if isinstance(first_item, dict) and 'error' in first_item:
|
||||
is_error = True
|
||||
error_message = first_item.get('error', 'Unknown error')
|
||||
|
||||
if is_error:
|
||||
# For errors, provide clear instructions to give a helpful response
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"""The tool "{function_name}" encountered an error:
|
||||
{error_message}
|
||||
|
||||
Please provide a helpful response to the user explaining that the operation could not be completed.
|
||||
Be apologetic and suggest alternatives if possible. Do NOT repeat the raw error message.
|
||||
Give a natural, conversational response."""
|
||||
}
|
||||
else:
|
||||
# For successful results, format normally
|
||||
tool_result_str = str(tool_result)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"""Tool execution complete.
|
||||
Function: {function_name}
|
||||
Result: {tool_result_str}
|
||||
|
||||
Now provide your final answer using this result."""
|
||||
}
|
||||
Now provide your final answer using this result. Summarize the information naturally for the user."""
|
||||
}
|
||||
|
||||
def _get_tool_names_for_prompt(self, formatted_tools: Optional[List]) -> str:
|
||||
"""Extract tool names from formatted tools for prompts."""
|
||||
@@ -2254,10 +2321,19 @@ Now provide your final answer using this result."""
|
||||
messages.append(self._format_ollama_tool_result_message(function_name, tool_result))
|
||||
else:
|
||||
# For other providers, use tool role with tool_call_id
|
||||
# Format error results more clearly
|
||||
if tool_result is None:
|
||||
content = "Function returned an empty output"
|
||||
elif isinstance(tool_result, dict) and 'error' in tool_result:
|
||||
content = f"Error: {tool_result.get('error', 'Unknown error')}. Please inform the user that the operation could not be completed."
|
||||
elif isinstance(tool_result, list) and len(tool_result) > 0 and isinstance(tool_result[0], dict) and 'error' in tool_result[0]:
|
||||
content = f"Error: {tool_result[0].get('error', 'Unknown error')}. Please inform the user that the operation could not be completed."
|
||||
else:
|
||||
content = json.dumps(tool_result)
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": json.dumps(tool_result) if tool_result is not None else "Function returned an empty output"
|
||||
"content": content
|
||||
})
|
||||
|
||||
# Check if we should continue (for tools like sequential thinking)
|
||||
@@ -3281,10 +3357,19 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
messages.append(self._format_ollama_tool_result_message(function_name, tool_result))
|
||||
else:
|
||||
# For other providers, use tool role with tool_call_id
|
||||
# Format error results more clearly
|
||||
if tool_result is None:
|
||||
content = "Function returned an empty output"
|
||||
elif isinstance(tool_result, dict) and 'error' in tool_result:
|
||||
content = f"Error: {tool_result.get('error', 'Unknown error')}. Please inform the user that the operation could not be completed."
|
||||
elif isinstance(tool_result, list) and len(tool_result) > 0 and isinstance(tool_result[0], dict) and 'error' in tool_result[0]:
|
||||
content = f"Error: {tool_result[0].get('error', 'Unknown error')}. Please inform the user that the operation could not be completed."
|
||||
else:
|
||||
content = json.dumps(tool_result)
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": json.dumps(tool_result) if tool_result is not None else "Function returned an empty output"
|
||||
"content": content
|
||||
})
|
||||
|
||||
# For Ollama, add explicit prompt if we need a final answer
|
||||
|
||||
@@ -32,6 +32,7 @@ __all__ = [
|
||||
"PermissionManager",
|
||||
"PermissionRule",
|
||||
"PermissionAction",
|
||||
"PermissionMode",
|
||||
"PermissionResult",
|
||||
"DoomLoopDetector",
|
||||
]
|
||||
@@ -51,6 +52,10 @@ def __getattr__(name: str):
|
||||
from .rules import PermissionAction
|
||||
return PermissionAction
|
||||
|
||||
if name == "PermissionMode":
|
||||
from .rules import PermissionMode
|
||||
return PermissionMode
|
||||
|
||||
if name == "PermissionResult":
|
||||
from .rules import PermissionResult
|
||||
return PermissionResult
|
||||
|
||||
@@ -21,6 +21,25 @@ class PermissionAction(str, Enum):
|
||||
ASK = "ask"
|
||||
|
||||
|
||||
class PermissionMode(str, Enum):
|
||||
"""
|
||||
Permission modes for agent execution (Claude Code parity).
|
||||
|
||||
These modes control how permissions are handled during agent execution:
|
||||
- DEFAULT: Standard permission checking with allow/deny/ask rules
|
||||
- ACCEPT_EDITS: Auto-accept file edit operations
|
||||
- DONT_ASK: Auto-deny any prompts that would require user input
|
||||
- BYPASS: Skip all permission checks (dangerous, use with caution)
|
||||
- PLAN: Read-only exploration mode, no write operations allowed
|
||||
"""
|
||||
|
||||
DEFAULT = "default"
|
||||
ACCEPT_EDITS = "accept_edits"
|
||||
DONT_ASK = "dont_ask"
|
||||
BYPASS = "bypass_permissions"
|
||||
PLAN = "plan"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PermissionRule:
|
||||
"""
|
||||
|
||||
@@ -28,14 +28,32 @@ class PluginHook(str, Enum):
|
||||
BEFORE_TOOL = "before_tool"
|
||||
AFTER_TOOL = "after_tool"
|
||||
|
||||
# Message hooks
|
||||
# Message hooks (for bot/channel integrations)
|
||||
BEFORE_MESSAGE = "before_message"
|
||||
AFTER_MESSAGE = "after_message"
|
||||
MESSAGE_RECEIVED = "message_received"
|
||||
MESSAGE_SENDING = "message_sending"
|
||||
MESSAGE_SENT = "message_sent"
|
||||
|
||||
# LLM hooks
|
||||
BEFORE_LLM = "before_llm"
|
||||
AFTER_LLM = "after_llm"
|
||||
|
||||
# Session hooks
|
||||
SESSION_START = "session_start"
|
||||
SESSION_END = "session_end"
|
||||
|
||||
# Gateway hooks
|
||||
GATEWAY_START = "gateway_start"
|
||||
GATEWAY_STOP = "gateway_stop"
|
||||
|
||||
# Compaction hooks (memory management)
|
||||
BEFORE_COMPACTION = "before_compaction"
|
||||
AFTER_COMPACTION = "after_compaction"
|
||||
|
||||
# Tool result persistence (for modifying tool results before storage)
|
||||
TOOL_RESULT_PERSIST = "tool_result_persist"
|
||||
|
||||
# Permission hooks
|
||||
ON_PERMISSION_ASK = "on_permission_ask"
|
||||
|
||||
@@ -44,6 +62,16 @@ class PluginHook(str, Enum):
|
||||
|
||||
# Auth hooks
|
||||
ON_AUTH = "on_auth"
|
||||
|
||||
# Error hooks
|
||||
ON_ERROR = "on_error"
|
||||
ON_RETRY = "on_retry"
|
||||
|
||||
# Claude Code parity hooks
|
||||
USER_PROMPT_SUBMIT = "user_prompt_submit" # When user submits a prompt
|
||||
NOTIFICATION = "notification" # When notification is sent
|
||||
SUBAGENT_STOP = "subagent_stop" # When subagent completes
|
||||
SETUP = "setup" # On initialization/maintenance
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -13,9 +13,35 @@ import time
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Task:
|
||||
"""
|
||||
A unit of work that can be executed by an Agent or a custom handler function.
|
||||
|
||||
Task is the unified abstraction for both AgentManager tasks and Workflow steps.
|
||||
It supports all features from the legacy WorkflowStep class.
|
||||
|
||||
Simple Usage:
|
||||
task = Task(description="Research AI trends")
|
||||
|
||||
With action alias (from WorkflowStep):
|
||||
task = Task(action="Write a blog post about {{topic}}")
|
||||
|
||||
With custom handler function:
|
||||
task = Task(
|
||||
name="process_data",
|
||||
action="Process the input",
|
||||
handler=my_custom_function
|
||||
)
|
||||
|
||||
With loop iteration:
|
||||
task = Task(
|
||||
action="Process {{item}}",
|
||||
loop_over="items",
|
||||
loop_var="item"
|
||||
)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
description: str,
|
||||
description: Optional[str] = None,
|
||||
expected_output: Optional[str] = None,
|
||||
agent: Optional[Agent] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -40,14 +66,43 @@ class Task:
|
||||
memory=None,
|
||||
quality_check=True,
|
||||
input_file: Optional[str] = None,
|
||||
rerun: bool = False, # Renamed from can_rerun and logic inverted, default True for backward compatibility
|
||||
retain_full_context: bool = False, # By default, only use previous task output, not all previous tasks
|
||||
rerun: bool = False,
|
||||
retain_full_context: bool = False,
|
||||
guardrail: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None,
|
||||
guardrails: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None, # Alias for guardrail
|
||||
guardrails: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None,
|
||||
max_retries: int = 3,
|
||||
retry_count: int = 0,
|
||||
agent_config: Optional[Dict[str, Any]] = None,
|
||||
variables: Optional[Dict[str, Any]] = None
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
# ============================================================
|
||||
# ROBUSTNESS PARAMS (graceful degradation & retry control)
|
||||
# ============================================================
|
||||
skip_on_failure: bool = False, # Allow workflow to continue if this task fails
|
||||
retry_delay: float = 0.0, # Seconds between retries (supports exponential backoff)
|
||||
# ============================================================
|
||||
# NEW PARAMS FROM WORKFLOWSTEP (Phase 4 Consolidation)
|
||||
# ============================================================
|
||||
# Action alias - alternative to description (user-friendly)
|
||||
action: Optional[str] = None,
|
||||
# Handler function - custom function instead of agent
|
||||
handler: Optional[Callable] = None,
|
||||
# Condition function - check if task should run
|
||||
should_run: Optional[Callable] = None,
|
||||
# Loop support - iterate over a list
|
||||
loop_over: Optional[str] = None,
|
||||
loop_var: str = "item",
|
||||
# Consolidated config objects (from WorkflowStep)
|
||||
execution: Optional[Any] = None,
|
||||
routing: Optional[Any] = None,
|
||||
output_config: Optional[Any] = None,
|
||||
# Feature configs (from WorkflowStep)
|
||||
autonomy: Optional[Any] = None,
|
||||
knowledge: Optional[Any] = None,
|
||||
web: Optional[Any] = None,
|
||||
reflection: Optional[Any] = None,
|
||||
planning: Optional[Any] = None,
|
||||
hooks: Optional[Any] = None,
|
||||
caching: Optional[Any] = None,
|
||||
):
|
||||
# Add check if memory config is provided
|
||||
if memory is not None or (config and config.get('memory_config')):
|
||||
@@ -60,6 +115,17 @@ class Task:
|
||||
MEMORY_AVAILABLE = False
|
||||
# Don't raise - let it continue with limited functionality
|
||||
|
||||
# Handle action as alias for description (from WorkflowStep)
|
||||
# If action provided but description not, use action as description
|
||||
if action is not None and description is None:
|
||||
description = action
|
||||
# Store both - action is the user-friendly alias
|
||||
self.action = action if action is not None else description
|
||||
|
||||
# Validate that we have either description or action
|
||||
if description is None:
|
||||
raise ValueError("Task requires either 'description' or 'action' parameter")
|
||||
|
||||
self.input_file = input_file
|
||||
self.id = str(uuid.uuid4()) if id is None else str(id)
|
||||
self.name = name
|
||||
@@ -95,6 +161,35 @@ class Task:
|
||||
self.agent_config = agent_config # Per-task agent configuration {role, goal, backstory, llm}
|
||||
self.variables = variables if variables else {} # Variables for substitution in description
|
||||
|
||||
# ============================================================
|
||||
# ROBUSTNESS PARAMS (graceful degradation & retry control)
|
||||
# ============================================================
|
||||
self.skip_on_failure = skip_on_failure
|
||||
self.retry_delay = retry_delay
|
||||
|
||||
# ============================================================
|
||||
# NEW PARAMS FROM WORKFLOWSTEP (Phase 4 Consolidation)
|
||||
# ============================================================
|
||||
# Handler function - custom function instead of agent
|
||||
self.handler = handler
|
||||
# Condition function - check if task should run
|
||||
self.should_run = should_run
|
||||
# Loop support - iterate over a list
|
||||
self.loop_over = loop_over
|
||||
self.loop_var = loop_var
|
||||
# Consolidated config objects (from WorkflowStep)
|
||||
self.execution = execution
|
||||
self.routing = routing
|
||||
self.output_config = output_config
|
||||
# Feature configs (from WorkflowStep)
|
||||
self.autonomy = autonomy
|
||||
self.knowledge = knowledge
|
||||
self.web = web
|
||||
self.reflection = reflection
|
||||
self.planning = planning
|
||||
self.hooks = hooks
|
||||
self.caching = caching
|
||||
|
||||
# Set logger level based on config verbose level
|
||||
verbose = self.config.get("verbose", 0)
|
||||
if verbose >= 5:
|
||||
|
||||
@@ -64,6 +64,15 @@ class ShellTools:
|
||||
else:
|
||||
command = shlex.split(command)
|
||||
|
||||
# Expand tilde and environment variables in command arguments
|
||||
# (shell=False means the shell won't do this for us)
|
||||
command = [os.path.expanduser(os.path.expandvars(arg)) for arg in command]
|
||||
|
||||
# Expand tilde in cwd (subprocess doesn't do this)
|
||||
if cwd:
|
||||
cwd = os.path.expanduser(cwd)
|
||||
cwd = os.path.expandvars(cwd) # Also expand $HOME, $USER, etc.
|
||||
|
||||
# Set up process environment
|
||||
process_env = os.environ.copy()
|
||||
if env:
|
||||
|
||||
@@ -15,6 +15,8 @@ def create_subagent_tool(
|
||||
agent_factory: Optional[Callable[..., Any]] = None,
|
||||
allowed_agents: Optional[List[str]] = None,
|
||||
max_depth: int = 3,
|
||||
default_llm: Optional[str] = None,
|
||||
default_permission_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a subagent tool for task delegation.
|
||||
@@ -26,6 +28,8 @@ def create_subagent_tool(
|
||||
agent_factory: Optional factory function to create agents
|
||||
allowed_agents: Optional list of allowed agent names
|
||||
max_depth: Maximum nesting depth for subagents
|
||||
default_llm: Default LLM model for subagents (Claude Code parity)
|
||||
default_permission_mode: Default permission mode for subagents (Claude Code parity)
|
||||
|
||||
Returns:
|
||||
Tool definition dictionary
|
||||
@@ -38,6 +42,8 @@ def create_subagent_tool(
|
||||
agent_name: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
tools: Optional[List[str]] = None,
|
||||
llm: Optional[str] = None,
|
||||
permission_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Spawn a subagent to handle a specific task.
|
||||
@@ -47,6 +53,8 @@ def create_subagent_tool(
|
||||
agent_name: Optional specific agent to use
|
||||
context: Optional additional context
|
||||
tools: Optional list of tools to give the subagent
|
||||
llm: Optional LLM model override (Claude Code parity)
|
||||
permission_mode: Optional permission mode (Claude Code parity)
|
||||
|
||||
Returns:
|
||||
Result from the subagent execution
|
||||
@@ -72,11 +80,17 @@ def create_subagent_tool(
|
||||
try:
|
||||
_current_depth += 1
|
||||
|
||||
# Resolve LLM (parameter > default > None)
|
||||
effective_llm = llm or default_llm
|
||||
# Resolve permission mode (parameter > default > None)
|
||||
effective_permission_mode = permission_mode or default_permission_mode
|
||||
|
||||
# If we have an agent factory, use it
|
||||
if agent_factory:
|
||||
subagent = agent_factory(
|
||||
name=agent_name or "subagent",
|
||||
tools=tools,
|
||||
llm=effective_llm,
|
||||
)
|
||||
|
||||
# Build prompt with context
|
||||
@@ -92,6 +106,8 @@ def create_subagent_tool(
|
||||
"output": result,
|
||||
"agent_name": agent_name or "subagent",
|
||||
"task": task,
|
||||
"llm": effective_llm,
|
||||
"permission_mode": effective_permission_mode,
|
||||
}
|
||||
else:
|
||||
# Without factory, return a placeholder
|
||||
@@ -100,6 +116,8 @@ def create_subagent_tool(
|
||||
"output": f"[Subagent would execute: {task}]",
|
||||
"agent_name": agent_name or "subagent",
|
||||
"task": task,
|
||||
"llm": effective_llm,
|
||||
"permission_mode": effective_permission_mode,
|
||||
"note": "No agent factory provided - simulation mode",
|
||||
}
|
||||
|
||||
@@ -142,6 +160,15 @@ def create_subagent_tool(
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional list of tool names to give the subagent",
|
||||
},
|
||||
"llm": {
|
||||
"type": "string",
|
||||
"description": "Optional LLM model to use for the subagent (e.g., 'gpt-4o-mini', 'claude-3-sonnet')",
|
||||
},
|
||||
"permission_mode": {
|
||||
"type": "string",
|
||||
"enum": ["default", "accept_edits", "dont_ask", "bypass_permissions", "plan"],
|
||||
"description": "Optional permission mode for the subagent (default, accept_edits, dont_ask, bypass_permissions, plan)",
|
||||
},
|
||||
},
|
||||
"required": ["task"],
|
||||
},
|
||||
|
||||
@@ -5,10 +5,11 @@ multiple search providers in order, falling back to the next if one fails.
|
||||
|
||||
Search Provider Priority:
|
||||
1. Tavily (requires TAVILY_API_KEY + tavily-python)
|
||||
2. Exa (requires EXA_API_KEY + exa_py)
|
||||
3. You.com (requires YDC_API_KEY + youdotcom)
|
||||
4. DuckDuckGo (requires duckduckgo_search package, no API key)
|
||||
5. SearxNG (requires requests + running SearxNG instance)
|
||||
2. Brave (requires BRAVE_API_KEY + requests)
|
||||
3. Exa (requires EXA_API_KEY + exa_py)
|
||||
4. You.com (requires YDC_API_KEY + youdotcom)
|
||||
5. DuckDuckGo (requires duckduckgo_search package, no API key)
|
||||
6. SearxNG (requires requests + running SearxNG instance)
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import search_web
|
||||
@@ -62,6 +63,15 @@ def _check_duckduckgo() -> tuple[bool, Optional[str]]:
|
||||
return True, None
|
||||
|
||||
|
||||
def _check_brave() -> tuple[bool, Optional[str]]:
|
||||
"""Check if Brave Search is available."""
|
||||
if not os.environ.get("BRAVE_API_KEY"):
|
||||
return False, "BRAVE_API_KEY not set"
|
||||
if util.find_spec("requests") is None:
|
||||
return False, "requests package not installed"
|
||||
return True, None
|
||||
|
||||
|
||||
def _check_searxng() -> tuple[bool, Optional[str]]:
|
||||
"""Check if SearxNG is available."""
|
||||
if util.find_spec("requests") is None:
|
||||
@@ -69,37 +79,51 @@ def _check_searxng() -> tuple[bool, Optional[str]]:
|
||||
return True, None
|
||||
|
||||
|
||||
def _search_tavily(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
||||
def _search_tavily(query: str, max_results: int = 5, search_depth: str = "basic", raw_content: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Search using Tavily."""
|
||||
from tavily import TavilyClient
|
||||
|
||||
client = TavilyClient(api_key=os.environ.get("TAVILY_API_KEY"))
|
||||
response = client.search(query=query, max_results=max_results)
|
||||
depth = os.environ.get("TAVILY_SEARCH_DEPTH", search_depth).lower()
|
||||
if depth not in ("basic", "advanced"):
|
||||
depth = "basic"
|
||||
response = client.search(
|
||||
query=query,
|
||||
max_results=max_results,
|
||||
search_depth=depth,
|
||||
include_raw_content=raw_content
|
||||
)
|
||||
|
||||
results = []
|
||||
for r in response.get("results", []):
|
||||
snippet = r.get("raw_content", "") if raw_content else r.get("content", "")
|
||||
results.append({
|
||||
"title": r.get("title", ""),
|
||||
"url": r.get("url", ""),
|
||||
"snippet": r.get("content", ""),
|
||||
"snippet": snippet,
|
||||
"provider": "tavily"
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def _search_exa(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Search using Exa."""
|
||||
def _search_exa(query: str, max_results: int = 5, full_text: bool = True) -> List[Dict[str, Any]]:
|
||||
"""Search using Exa.
|
||||
|
||||
Args:
|
||||
full_text: True for full page text (more tokens), False for highlights only.
|
||||
"""
|
||||
from exa_py import Exa
|
||||
|
||||
client = Exa(os.environ.get("EXA_API_KEY"))
|
||||
response = client.search(query=query, num_results=max_results)
|
||||
response = client.search(query=query, num_results=max_results, text=full_text)
|
||||
|
||||
results = []
|
||||
for r in response.results:
|
||||
snippet = getattr(r, "text", "") if full_text else getattr(r, "highlights", [""])[0] if getattr(r, "highlights", None) else ""
|
||||
results.append({
|
||||
"title": getattr(r, "title", "") or "",
|
||||
"url": r.url,
|
||||
"snippet": getattr(r, "text", "") or "",
|
||||
"snippet": snippet or "",
|
||||
"provider": "exa"
|
||||
})
|
||||
return results
|
||||
@@ -117,24 +141,79 @@ def _search_youdotcom(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
||||
if hasattr(response, 'results'):
|
||||
web_results = getattr(response.results, 'web', []) or []
|
||||
for r in web_results[:max_results]:
|
||||
description = getattr(r, "description", "") or ""
|
||||
snippets = getattr(r, "snippets", []) or []
|
||||
full_snippet = description
|
||||
if snippets:
|
||||
full_snippet += "\n" + "\n".join(snippets)
|
||||
|
||||
results.append({
|
||||
"title": getattr(r, "title", "") or "",
|
||||
"url": getattr(r, "url", "") or "",
|
||||
"snippet": getattr(r, "description", "") or "",
|
||||
"snippet": full_snippet.strip(),
|
||||
"provider": "youdotcom"
|
||||
})
|
||||
elif isinstance(response, dict):
|
||||
web_results = response.get("results", {}).get("web", [])
|
||||
for r in web_results[:max_results]:
|
||||
description = r.get("description", "")
|
||||
snippets = r.get("snippets", [])
|
||||
full_snippet = description
|
||||
if snippets:
|
||||
full_snippet += "\n" + "\n".join(snippets)
|
||||
|
||||
results.append({
|
||||
"title": r.get("title", ""),
|
||||
"url": r.get("url", ""),
|
||||
"snippet": r.get("description", ""),
|
||||
"snippet": full_snippet.strip(),
|
||||
"provider": "youdotcom"
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def _search_brave(query: str, max_results: int = 5, extra_snippets: bool = True) -> List[Dict[str, Any]]:
|
||||
"""Search using Brave Search.
|
||||
|
||||
Args:
|
||||
extra_snippets: Include extra snippets for richer content (True by default).
|
||||
"""
|
||||
import requests
|
||||
|
||||
url = "https://api.search.brave.com/res/v1/web/search"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"X-Subscription-Token": os.environ.get("BRAVE_API_KEY")
|
||||
}
|
||||
params = {
|
||||
"q": query,
|
||||
"count": max_results,
|
||||
"result_filter": "web"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
results = []
|
||||
for r in data.get("web", {}).get("results", []):
|
||||
description = r.get("description", "")
|
||||
full_snippet = description
|
||||
|
||||
if extra_snippets:
|
||||
extra = r.get("extra_snippets", [])
|
||||
if extra:
|
||||
full_snippet += "\n" + "\n".join(extra)
|
||||
|
||||
results.append({
|
||||
"title": r.get("title", ""),
|
||||
"url": r.get("url", ""),
|
||||
"snippet": full_snippet.strip(),
|
||||
"provider": "brave"
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def _search_duckduckgo(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Search using DuckDuckGo with retry logic."""
|
||||
import time
|
||||
@@ -207,6 +286,7 @@ def _search_searxng(query: str, max_results: int = 5, searxng_url: Optional[str]
|
||||
# Provider configuration: (name, check_func, search_func)
|
||||
SEARCH_PROVIDERS = [
|
||||
("tavily", _check_tavily, _search_tavily),
|
||||
("brave", _check_brave, _search_brave),
|
||||
("exa", _check_exa, _search_exa),
|
||||
("youdotcom", _check_youdotcom, _search_youdotcom),
|
||||
("duckduckgo", _check_duckduckgo, _search_duckduckgo),
|
||||
@@ -219,52 +299,69 @@ def search_web(
|
||||
max_results: int = 5,
|
||||
providers: Optional[List[str]] = None,
|
||||
searxng_url: Optional[str] = None,
|
||||
search_depth: str = "basic",
|
||||
tavily_raw_content: bool = False,
|
||||
exa_full_text: bool = True,
|
||||
brave_extra_snippets: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Search the web using multiple providers with automatic fallback.
|
||||
"""Search the web using one or more providers with automatic fallback.
|
||||
|
||||
Tries each search provider in order until one succeeds. For each provider,
|
||||
it first checks if the API key is set (if required) and if the package is
|
||||
installed, then attempts the search. If the search fails, it moves to the
|
||||
next provider.
|
||||
VALID PROVIDER NAMES: tavily, brave, exa, youdotcom, duckduckgo, searxng
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
max_results: Maximum number of results to return (default: 5)
|
||||
providers: Optional list of provider names to try, in order.
|
||||
If not specified, uses default order:
|
||||
["tavily", "exa", "youdotcom", "duckduckgo", "searxng"]
|
||||
searxng_url: Optional custom SearxNG instance URL
|
||||
query: Search query string
|
||||
max_results: Maximum results to return (default: 5)
|
||||
providers: Which provider(s) to use. Accepts:
|
||||
- Single string: "tavily" (uses just that provider)
|
||||
- Comma-separated: "tavily, brave" (tries in order, falls back on failure)
|
||||
- List: ["tavily", "brave"] (same as comma-separated)
|
||||
If not specified, tries all providers in order until one succeeds.
|
||||
|
||||
Provider-specific options (only used by their respective provider):
|
||||
- searxng_url: [SearxNG] Custom instance URL
|
||||
- search_depth: [Tavily] "basic" (1 credit) or "advanced" (2 credits)
|
||||
- tavily_raw_content: [Tavily] Include full page content (default: False for snippets)
|
||||
- exa_full_text: [Exa] Full page text (True) vs highlights (False)
|
||||
- brave_extra_snippets: [Brave] Include extra snippets (True) for richer content
|
||||
|
||||
Returns:
|
||||
List of search results, each containing:
|
||||
- title: Result title
|
||||
- url: Result URL
|
||||
- snippet: Result description/snippet
|
||||
- provider: Name of the provider that returned the result
|
||||
|
||||
If all providers fail, returns a list with a single error dict.
|
||||
|
||||
Example:
|
||||
# Use default provider order
|
||||
results = search_web("AI news 2024")
|
||||
|
||||
# Specify providers to try
|
||||
results = search_web("Python tutorials", providers=["duckduckgo", "tavily"])
|
||||
|
||||
# With custom SearxNG URL
|
||||
results = search_web("tech news", searxng_url="http://my-searxng:8080/search")
|
||||
List of dicts with: title, url, snippet, provider. Returns error dict if all fail.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Valid provider names for reference
|
||||
valid_provider_names = [p[0] for p in SEARCH_PROVIDERS]
|
||||
|
||||
# Check for WEB_SEARCH_PROVIDER env var (set by CLI --web-provider flag)
|
||||
if providers is None:
|
||||
env_provider = os.environ.get("WEB_SEARCH_PROVIDER")
|
||||
if env_provider:
|
||||
providers = env_provider
|
||||
logger.debug(f"Using provider from WEB_SEARCH_PROVIDER env var: {env_provider}")
|
||||
|
||||
# Determine which providers to try
|
||||
if providers:
|
||||
# Handle string input (LLM might pass "provider1, provider2" instead of list)
|
||||
if isinstance(providers, str):
|
||||
providers = [p.strip() for p in providers.split(",")]
|
||||
|
||||
# Filter to only valid provider names
|
||||
provider_order = []
|
||||
for name in providers:
|
||||
name_lower = name.lower().strip()
|
||||
matched = False
|
||||
for pname, check_fn, search_fn in SEARCH_PROVIDERS:
|
||||
if pname == name.lower():
|
||||
if pname == name_lower:
|
||||
provider_order.append((pname, check_fn, search_fn))
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
logger.warning(f"Invalid provider '{name}'. Valid providers: {valid_provider_names}")
|
||||
|
||||
# If no valid providers specified, fall back to default order
|
||||
if not provider_order:
|
||||
logger.warning(f"No valid providers in {providers}. Using default order: {valid_provider_names}")
|
||||
provider_order = SEARCH_PROVIDERS
|
||||
else:
|
||||
provider_order = SEARCH_PROVIDERS
|
||||
|
||||
@@ -282,6 +379,12 @@ def search_web(
|
||||
|
||||
if provider_name == "searxng":
|
||||
results = search_func(query, max_results, searxng_url)
|
||||
elif provider_name == "tavily":
|
||||
results = _search_tavily(query, max_results, search_depth, tavily_raw_content)
|
||||
elif provider_name == "exa":
|
||||
results = _search_exa(query, max_results, exa_full_text)
|
||||
elif provider_name == "brave":
|
||||
results = _search_brave(query, max_results, brave_extra_snippets)
|
||||
else:
|
||||
results = search_func(query, max_results)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from .workflows import (
|
||||
# Core classes
|
||||
Workflow,
|
||||
Pipeline, # Alias for Workflow
|
||||
WorkflowStep,
|
||||
WorkflowStep as _OriginalWorkflowStep, # Keep original for internal use
|
||||
WorkflowContext,
|
||||
StepResult,
|
||||
WorkflowManager,
|
||||
@@ -27,6 +27,7 @@ from .workflows import (
|
||||
loop,
|
||||
repeat,
|
||||
include,
|
||||
when,
|
||||
if_,
|
||||
|
||||
# Constants
|
||||
@@ -86,6 +87,7 @@ __all__ = [
|
||||
"loop",
|
||||
"repeat",
|
||||
"include",
|
||||
"when",
|
||||
"if_",
|
||||
|
||||
# Constants
|
||||
@@ -130,7 +132,21 @@ _LAZY_IMPORTS = {
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import mechanism for heavy modules."""
|
||||
"""Lazy import mechanism for heavy modules and deprecation handling."""
|
||||
import warnings
|
||||
|
||||
# WorkflowStep deprecation - return Task with warning (Phase 4 Consolidation)
|
||||
if name == "WorkflowStep":
|
||||
warnings.warn(
|
||||
"WorkflowStep is deprecated, use Task instead. "
|
||||
"Task now supports all WorkflowStep features including action, handler, loop_over, etc. "
|
||||
"Example: from praisonaiagents import Task",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
from ..task.task import Task
|
||||
return Task
|
||||
|
||||
if name in _LAZY_IMPORTS:
|
||||
module_name = _LAZY_IMPORTS[name]
|
||||
import importlib
|
||||
|
||||
@@ -357,14 +357,15 @@ def repeat(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None,
|
||||
@dataclass
|
||||
class Include:
|
||||
"""
|
||||
Include another recipe as a workflow step.
|
||||
Include another recipe or workflow as a workflow step.
|
||||
|
||||
Enables modular recipe composition - recipes can include other recipes.
|
||||
Enables modular composition - recipes can include other recipes or workflows.
|
||||
|
||||
Usage:
|
||||
workflow = Workflow(steps=[
|
||||
content_writer,
|
||||
include("wordpress-publisher"), # Include another recipe
|
||||
include(workflow=my_other_workflow), # Include another workflow
|
||||
])
|
||||
|
||||
YAML syntax:
|
||||
@@ -377,17 +378,30 @@ class Include:
|
||||
recipe: wordpress-publisher
|
||||
input: "{{previous_output}}"
|
||||
"""
|
||||
recipe: str = "" # Recipe name or path
|
||||
recipe: Optional[str] = None # Recipe name or path
|
||||
workflow: Optional[Any] = None # Workflow instance for composition
|
||||
input: Optional[str] = None # Input override (default: previous_output)
|
||||
|
||||
def __init__(self, recipe: str, input: Optional[str] = None):
|
||||
def __init__(self, recipe: Optional[str] = None, workflow: Optional[Any] = None, input: Optional[str] = None):
|
||||
if recipe is None and workflow is None:
|
||||
raise ValueError("Either 'recipe' or 'workflow' must be provided")
|
||||
self.recipe = recipe
|
||||
self.workflow = workflow
|
||||
self.input = input
|
||||
|
||||
|
||||
def include(recipe: str, input: Optional[str] = None) -> Include:
|
||||
"""Include another recipe as a workflow step."""
|
||||
return Include(recipe=recipe, input=input)
|
||||
def include(recipe: Optional[str] = None, workflow: Optional[Any] = None, input: Optional[str] = None) -> Include:
|
||||
"""Include another recipe or workflow as a workflow step.
|
||||
|
||||
Args:
|
||||
recipe: Recipe name or path to include
|
||||
workflow: Workflow instance to include
|
||||
input: Input override (default: previous_output)
|
||||
|
||||
Returns:
|
||||
Include object for workflow composition
|
||||
"""
|
||||
return Include(recipe=recipe, workflow=workflow, input=input)
|
||||
|
||||
|
||||
# Maximum nesting depth for patterns (prevents stack overflow)
|
||||
@@ -440,7 +454,7 @@ class If:
|
||||
self.else_steps = else_steps or []
|
||||
|
||||
|
||||
def if_(
|
||||
def when(
|
||||
condition: str,
|
||||
then_steps: List[Any],
|
||||
else_steps: Optional[List[Any]] = None
|
||||
@@ -448,6 +462,8 @@ def if_(
|
||||
"""
|
||||
Create a conditional branching step.
|
||||
|
||||
This is the preferred function for conditional branching (cleaner name than if_).
|
||||
|
||||
Args:
|
||||
condition: Condition expression with {{var}} placeholders
|
||||
then_steps: Steps to execute if condition is true
|
||||
@@ -457,7 +473,7 @@ def if_(
|
||||
If object for conditional execution
|
||||
|
||||
Example:
|
||||
if_(
|
||||
when(
|
||||
condition="{{score}} > 80",
|
||||
then_steps=[approve_agent],
|
||||
else_steps=[reject_agent]
|
||||
@@ -466,6 +482,34 @@ def if_(
|
||||
return If(condition=condition, then_steps=then_steps, else_steps=else_steps)
|
||||
|
||||
|
||||
def if_(
|
||||
condition: str,
|
||||
then_steps: List[Any],
|
||||
else_steps: Optional[List[Any]] = None
|
||||
) -> If:
|
||||
"""
|
||||
Create a conditional branching step.
|
||||
|
||||
.. deprecated::
|
||||
Use :func:`when` instead. `if_` will be removed in a future version.
|
||||
|
||||
Args:
|
||||
condition: Condition expression with {{var}} placeholders
|
||||
then_steps: Steps to execute if condition is true
|
||||
else_steps: Steps to execute if condition is false (optional)
|
||||
|
||||
Returns:
|
||||
If object for conditional execution
|
||||
"""
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"if_() is deprecated, use when() instead for cleaner syntax",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return If(condition=condition, then_steps=then_steps, else_steps=else_steps)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowStep:
|
||||
"""
|
||||
@@ -858,6 +902,11 @@ class Workflow:
|
||||
execution: Optional[Any] = None # Union[str, ExecutionConfig] - execution control
|
||||
caching: Optional[Any] = None # Union[bool, CachingConfig] - caching
|
||||
|
||||
# ============================================================
|
||||
# ROBUSTNESS PARAMS (debugging & audit trail)
|
||||
# ============================================================
|
||||
history: bool = False # Enable execution history tracking for debugging
|
||||
|
||||
# Status tracking
|
||||
status: str = "not_started" # not_started, running, completed, failed
|
||||
step_statuses: Dict[str, str] = field(default_factory=dict) # {step_name: status}
|
||||
@@ -880,6 +929,8 @@ class Workflow:
|
||||
_guardrails_config: Optional[Any] = field(default=None, repr=False)
|
||||
_web_config: Optional[Any] = field(default=None, repr=False)
|
||||
_reflection_config: Optional[Any] = field(default=None, repr=False)
|
||||
# Execution history for debugging (only populated when history=True)
|
||||
_execution_history: List[Dict[str, Any]] = field(default_factory=list, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve consolidated params to internal values."""
|
||||
@@ -1005,6 +1056,20 @@ class Workflow:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def get_history(self) -> List[Dict[str, Any]]:
|
||||
"""Return execution trace for debugging.
|
||||
|
||||
Returns a list of step execution records, each containing:
|
||||
- step: Step name
|
||||
- timestamp: ISO format timestamp
|
||||
- success: Whether the step succeeded
|
||||
- output: Truncated output (first 500 chars)
|
||||
- error: Error message if failed
|
||||
|
||||
Only populated when history=True is set on the workflow.
|
||||
"""
|
||||
return self._execution_history
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.14.2"
|
||||
version = "0.15.2"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Tests for AgentManager rename (Agents → AgentManager).
|
||||
|
||||
TDD: These tests define the expected behavior for the API consolidation.
|
||||
"""
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestAgentManagerRename:
|
||||
"""Test that AgentManager is the primary class and Agents is deprecated alias."""
|
||||
|
||||
def test_agent_manager_importable(self):
|
||||
"""AgentManager should be importable from praisonaiagents."""
|
||||
from praisonaiagents import AgentManager
|
||||
assert AgentManager is not None
|
||||
|
||||
def test_agents_still_importable(self):
|
||||
"""Agents should still be importable for backward compatibility."""
|
||||
from praisonaiagents import Agents
|
||||
assert Agents is not None
|
||||
|
||||
def test_agent_manager_is_agents(self):
|
||||
"""AgentManager and Agents should be the same class."""
|
||||
from praisonaiagents import AgentManager, Agents
|
||||
assert AgentManager is Agents
|
||||
|
||||
def test_agents_emits_deprecation_warning(self):
|
||||
"""Importing Agents should emit a DeprecationWarning."""
|
||||
# Clear any cached imports
|
||||
import sys
|
||||
# Remove from cache to trigger fresh import
|
||||
modules_to_remove = [k for k in sys.modules.keys() if 'praisonaiagents' in k]
|
||||
for mod in modules_to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
# Import fresh
|
||||
import praisonaiagents
|
||||
# Access Agents to trigger lazy loading
|
||||
_ = praisonaiagents.Agents
|
||||
|
||||
# Check for deprecation warning
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
agents_warnings = [x for x in deprecation_warnings if 'Agents' in str(x.message)]
|
||||
assert len(agents_warnings) >= 1, f"Expected deprecation warning for Agents, got: {[str(x.message) for x in w]}"
|
||||
|
||||
def test_agent_manager_no_deprecation_warning(self):
|
||||
"""Importing AgentManager should NOT emit a deprecation warning."""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
from praisonaiagents import AgentManager
|
||||
_ = AgentManager # Use it
|
||||
|
||||
# Check no deprecation warning for AgentManager
|
||||
agent_manager_warnings = [x for x in w if 'AgentManager' in str(x.message) and issubclass(x.category, DeprecationWarning)]
|
||||
assert len(agent_manager_warnings) == 0, f"Unexpected deprecation warning for AgentManager: {agent_manager_warnings}"
|
||||
|
||||
def test_agent_manager_in_all(self):
|
||||
"""AgentManager should be in __all__ for IDE autocomplete."""
|
||||
import praisonaiagents
|
||||
assert 'AgentManager' in praisonaiagents.__all__
|
||||
|
||||
def test_agent_manager_class_name(self):
|
||||
"""The class should be named AgentManager."""
|
||||
from praisonaiagents import AgentManager
|
||||
assert AgentManager.__name__ == 'AgentManager'
|
||||
|
||||
|
||||
class TestAgentManagerFunctionality:
|
||||
"""Test that AgentManager works correctly with agents and tasks."""
|
||||
|
||||
def test_agent_manager_instantiation(self):
|
||||
"""AgentManager should be instantiable with agents list."""
|
||||
from praisonaiagents import AgentManager, Agent
|
||||
|
||||
agent = Agent(name="test", instructions="Test agent")
|
||||
manager = AgentManager(agents=[agent])
|
||||
|
||||
assert manager is not None
|
||||
assert len(manager.agents) == 1
|
||||
|
||||
def test_agents_alias_instantiation(self):
|
||||
"""Agents alias should work identically to AgentManager."""
|
||||
from praisonaiagents import Agents, Agent
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # Ignore deprecation for this test
|
||||
agent = Agent(name="test", instructions="Test agent")
|
||||
manager = Agents(agents=[agent])
|
||||
|
||||
assert manager is not None
|
||||
assert len(manager.agents) == 1
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test backward compatibility with existing code patterns."""
|
||||
|
||||
def test_from_agents_import_pattern(self):
|
||||
"""Common import pattern should still work."""
|
||||
from praisonaiagents.agents import Agents
|
||||
assert Agents is not None
|
||||
|
||||
def test_from_agents_import_agent_manager(self):
|
||||
"""New import pattern should work."""
|
||||
from praisonaiagents.agents import AgentManager
|
||||
assert AgentManager is not None
|
||||
|
||||
def test_praison_ai_agents_deprecated(self):
|
||||
"""PraisonAIAgents should still work as deprecated alias."""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
from praisonaiagents import PraisonAIAgents
|
||||
|
||||
# Should emit deprecation warning
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) >= 1
|
||||
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Tests for AgentApp protocol and configuration.
|
||||
|
||||
TDD: These tests define the expected behavior for the AgentApp feature.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
|
||||
class TestAgentAppProtocol:
|
||||
"""Test that AgentAppProtocol is importable from core SDK."""
|
||||
|
||||
def test_agent_app_protocol_importable(self):
|
||||
"""AgentAppProtocol should be importable from praisonaiagents."""
|
||||
from praisonaiagents import AgentAppProtocol
|
||||
assert AgentAppProtocol is not None
|
||||
|
||||
def test_agent_app_config_importable(self):
|
||||
"""AgentAppConfig should be importable from praisonaiagents."""
|
||||
from praisonaiagents import AgentAppConfig
|
||||
assert AgentAppConfig is not None
|
||||
|
||||
|
||||
class TestAgentAppProtocolInterface:
|
||||
"""Test the AgentAppProtocol interface definition."""
|
||||
|
||||
def test_protocol_has_serve_method(self):
|
||||
"""AgentAppProtocol should define a serve method."""
|
||||
from praisonaiagents import AgentAppProtocol
|
||||
import inspect
|
||||
|
||||
# Check that serve is defined in the protocol
|
||||
assert hasattr(AgentAppProtocol, 'serve')
|
||||
|
||||
def test_protocol_has_get_app_method(self):
|
||||
"""AgentAppProtocol should define a get_app method."""
|
||||
from praisonaiagents import AgentAppProtocol
|
||||
|
||||
assert hasattr(AgentAppProtocol, 'get_app')
|
||||
|
||||
|
||||
class TestAgentAppConfig:
|
||||
"""Test AgentAppConfig dataclass."""
|
||||
|
||||
def test_config_default_values(self):
|
||||
"""AgentAppConfig should have sensible defaults."""
|
||||
from praisonaiagents import AgentAppConfig
|
||||
|
||||
config = AgentAppConfig()
|
||||
assert config.host == "0.0.0.0"
|
||||
assert config.port == 8000
|
||||
assert config.reload is False
|
||||
|
||||
def test_config_custom_values(self):
|
||||
"""AgentAppConfig should accept custom values."""
|
||||
from praisonaiagents import AgentAppConfig
|
||||
|
||||
config = AgentAppConfig(
|
||||
name="My App",
|
||||
host="127.0.0.1",
|
||||
port=9000,
|
||||
reload=True
|
||||
)
|
||||
assert config.name == "My App"
|
||||
assert config.host == "127.0.0.1"
|
||||
assert config.port == 9000
|
||||
assert config.reload is True
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Tests for bot CLI commands.
|
||||
|
||||
Tests the enhanced bot command with full capability options.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestBotCapabilities:
|
||||
"""Tests for BotCapabilities dataclass."""
|
||||
|
||||
def test_bot_capabilities_defaults(self):
|
||||
"""Test default values for BotCapabilities."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities()
|
||||
|
||||
assert caps.browser is False
|
||||
assert caps.browser_profile == "default"
|
||||
assert caps.browser_headless is False
|
||||
assert caps.tools == []
|
||||
assert caps.skills == []
|
||||
assert caps.memory is False
|
||||
assert caps.knowledge is False
|
||||
assert caps.web_search is False
|
||||
assert caps.sandbox is False
|
||||
assert caps.exec_enabled is False
|
||||
assert caps.model is None
|
||||
|
||||
def test_bot_capabilities_custom_values(self):
|
||||
"""Test custom values for BotCapabilities."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities(
|
||||
browser=True,
|
||||
browser_profile="chrome",
|
||||
tools=["DuckDuckGoTool", "WikipediaTool"],
|
||||
memory=True,
|
||||
web_search=True,
|
||||
web_search_provider="tavily",
|
||||
model="gpt-4o",
|
||||
)
|
||||
|
||||
assert caps.browser is True
|
||||
assert caps.browser_profile == "chrome"
|
||||
assert caps.tools == ["DuckDuckGoTool", "WikipediaTool"]
|
||||
assert caps.memory is True
|
||||
assert caps.web_search is True
|
||||
assert caps.web_search_provider == "tavily"
|
||||
assert caps.model == "gpt-4o"
|
||||
|
||||
def test_bot_capabilities_to_dict(self):
|
||||
"""Test to_dict method."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities(browser=True, model="gpt-4o")
|
||||
result = caps.to_dict()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["browser"] is True
|
||||
assert result["model"] == "gpt-4o"
|
||||
assert "tools" in result
|
||||
assert "memory" in result
|
||||
|
||||
|
||||
class TestBotHandler:
|
||||
"""Tests for BotHandler class."""
|
||||
|
||||
def test_bot_handler_init(self):
|
||||
"""Test BotHandler initialization."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler
|
||||
|
||||
handler = BotHandler()
|
||||
assert handler is not None
|
||||
|
||||
@patch('praisonai.cli.features.bots_cli.BotHandler._load_agent')
|
||||
def test_build_tools_browser(self, mock_load_agent):
|
||||
"""Test _build_tools with browser enabled."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
caps = BotCapabilities(browser=True)
|
||||
|
||||
# Mock the browser tool import
|
||||
with patch.dict('sys.modules', {'praisonai_tools': MagicMock()}):
|
||||
tools = handler._build_tools(caps)
|
||||
# Should attempt to add browser tool
|
||||
assert isinstance(tools, list)
|
||||
|
||||
@patch('praisonai.cli.features.bots_cli.BotHandler._load_agent')
|
||||
def test_build_tools_web_search(self, mock_load_agent):
|
||||
"""Test _build_tools with web search enabled."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
caps = BotCapabilities(web_search=True, web_search_provider="duckduckgo")
|
||||
|
||||
with patch.dict('sys.modules', {'praisonai_tools': MagicMock()}):
|
||||
tools = handler._build_tools(caps)
|
||||
assert isinstance(tools, list)
|
||||
|
||||
def test_resolve_tool_by_name_not_found(self):
|
||||
"""Test _resolve_tool_by_name with non-existent tool."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler
|
||||
|
||||
handler = BotHandler()
|
||||
result = handler._resolve_tool_by_name("NonExistentTool12345")
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_print_startup_info_no_capabilities(self, capsys):
|
||||
"""Test _print_startup_info without capabilities."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler
|
||||
|
||||
handler = BotHandler()
|
||||
handler._print_startup_info("Telegram", None)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Starting Telegram bot" in captured.out
|
||||
assert "Press Ctrl+C to stop" in captured.out
|
||||
|
||||
def test_print_startup_info_with_capabilities(self, capsys):
|
||||
"""Test _print_startup_info with capabilities."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
caps = BotCapabilities(
|
||||
browser=True,
|
||||
memory=True,
|
||||
web_search=True,
|
||||
)
|
||||
handler._print_startup_info("Slack", caps)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Starting Slack bot" in captured.out
|
||||
assert "Capabilities:" in captured.out
|
||||
assert "browser" in captured.out
|
||||
assert "memory" in captured.out
|
||||
|
||||
|
||||
class TestHandleBotCommand:
|
||||
"""Tests for handle_bot_command function."""
|
||||
|
||||
def test_handle_bot_command_no_platform(self, capsys):
|
||||
"""Test handle_bot_command with no platform."""
|
||||
from praisonai.cli.features.bots_cli import handle_bot_command
|
||||
|
||||
result = handle_bot_command([])
|
||||
|
||||
assert result == 1
|
||||
captured = capsys.readouterr()
|
||||
assert "Available platforms" in captured.out
|
||||
|
||||
def test_handle_bot_command_help_output(self, capsys):
|
||||
"""Test handle_bot_command shows help."""
|
||||
from praisonai.cli.features.bots_cli import handle_bot_command
|
||||
|
||||
handle_bot_command([])
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "telegram" in captured.out.lower()
|
||||
assert "discord" in captured.out.lower()
|
||||
assert "slack" in captured.out.lower()
|
||||
|
||||
|
||||
class TestBuildCapabilitiesFromArgs:
|
||||
"""Tests for _build_capabilities_from_args function."""
|
||||
|
||||
def test_build_capabilities_from_args_defaults(self):
|
||||
"""Test building capabilities from args with defaults."""
|
||||
from praisonai.cli.features.bots_cli import _build_capabilities_from_args
|
||||
|
||||
class MockArgs:
|
||||
browser = False
|
||||
browser_profile = "default"
|
||||
browser_headless = False
|
||||
tools = []
|
||||
skills = []
|
||||
skills_dir = None
|
||||
memory = False
|
||||
memory_provider = "default"
|
||||
knowledge = False
|
||||
knowledge_sources = []
|
||||
web_search = False
|
||||
web_provider = "duckduckgo"
|
||||
sandbox = False
|
||||
exec_enabled = False
|
||||
model = None
|
||||
thinking = None
|
||||
session_id = None
|
||||
user_id = None
|
||||
|
||||
caps = _build_capabilities_from_args(MockArgs())
|
||||
|
||||
assert caps.browser is False
|
||||
assert caps.memory is False
|
||||
assert caps.web_search is False
|
||||
|
||||
def test_build_capabilities_from_args_custom(self):
|
||||
"""Test building capabilities from args with custom values."""
|
||||
from praisonai.cli.features.bots_cli import _build_capabilities_from_args
|
||||
|
||||
class MockArgs:
|
||||
browser = True
|
||||
browser_profile = "chrome"
|
||||
browser_headless = True
|
||||
tools = ["DuckDuckGoTool"]
|
||||
skills = []
|
||||
skills_dir = None
|
||||
memory = True
|
||||
memory_provider = "redis"
|
||||
knowledge = False
|
||||
knowledge_sources = []
|
||||
web_search = True
|
||||
web_provider = "tavily"
|
||||
sandbox = False
|
||||
exec_enabled = False
|
||||
model = "gpt-4o"
|
||||
thinking = "medium"
|
||||
session_id = "test-session"
|
||||
user_id = "user-123"
|
||||
|
||||
caps = _build_capabilities_from_args(MockArgs())
|
||||
|
||||
assert caps.browser is True
|
||||
assert caps.browser_profile == "chrome"
|
||||
assert caps.tools == ["DuckDuckGoTool"]
|
||||
assert caps.memory is True
|
||||
assert caps.web_search is True
|
||||
assert caps.model == "gpt-4o"
|
||||
|
||||
|
||||
class TestAddCapabilityArgs:
|
||||
"""Tests for _add_capability_args function."""
|
||||
|
||||
def test_add_capability_args(self):
|
||||
"""Test that capability args are added to parser."""
|
||||
import argparse
|
||||
from praisonai.cli.features.bots_cli import _add_capability_args
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
_add_capability_args(parser)
|
||||
|
||||
# Parse with defaults
|
||||
args = parser.parse_args([])
|
||||
|
||||
assert hasattr(args, 'browser')
|
||||
assert hasattr(args, 'memory')
|
||||
assert hasattr(args, 'web_search')
|
||||
assert hasattr(args, 'tools')
|
||||
assert hasattr(args, 'model')
|
||||
|
||||
def test_add_capability_args_with_values(self):
|
||||
"""Test parsing capability args with values."""
|
||||
import argparse
|
||||
from praisonai.cli.features.bots_cli import _add_capability_args
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
_add_capability_args(parser)
|
||||
|
||||
args = parser.parse_args([
|
||||
'--browser',
|
||||
'--memory',
|
||||
'--web',
|
||||
'--model', 'gpt-4o',
|
||||
'--tools', 'DuckDuckGoTool', 'WikipediaTool',
|
||||
])
|
||||
|
||||
assert args.browser is True
|
||||
assert args.memory is True
|
||||
assert args.web_search is True
|
||||
assert args.model == "gpt-4o"
|
||||
assert args.tools == ["DuckDuckGoTool", "WikipediaTool"]
|
||||
@@ -33,6 +33,34 @@ from praisonaiagents.hooks.runner import HookRunner
|
||||
# HookResult Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestHookEvent:
|
||||
"""Tests for HookEvent enum - Claude Code parity."""
|
||||
|
||||
def test_claude_code_parity_events(self):
|
||||
"""Test Claude Code parity hook events."""
|
||||
# USER_PROMPT_SUBMIT - when user submits a prompt
|
||||
assert HookEvent.USER_PROMPT_SUBMIT.value == "user_prompt_submit"
|
||||
# NOTIFICATION - when notification is sent
|
||||
assert HookEvent.NOTIFICATION.value == "notification"
|
||||
# SUBAGENT_STOP - when subagent completes
|
||||
assert HookEvent.SUBAGENT_STOP.value == "subagent_stop"
|
||||
# SETUP - on initialization/maintenance
|
||||
assert HookEvent.SETUP.value == "setup"
|
||||
|
||||
def test_existing_events_unchanged(self):
|
||||
"""Verify existing events are unchanged (backward compatibility)."""
|
||||
assert HookEvent.BEFORE_TOOL.value == "before_tool"
|
||||
assert HookEvent.AFTER_TOOL.value == "after_tool"
|
||||
assert HookEvent.BEFORE_AGENT.value == "before_agent"
|
||||
assert HookEvent.AFTER_AGENT.value == "after_agent"
|
||||
assert HookEvent.SESSION_START.value == "session_start"
|
||||
assert HookEvent.SESSION_END.value == "session_end"
|
||||
assert HookEvent.BEFORE_LLM.value == "before_llm"
|
||||
assert HookEvent.AFTER_LLM.value == "after_llm"
|
||||
assert HookEvent.ON_ERROR.value == "on_error"
|
||||
assert HookEvent.ON_RETRY.value == "on_retry"
|
||||
|
||||
|
||||
class TestHookResult:
|
||||
"""Tests for HookResult class."""
|
||||
|
||||
|
||||
@@ -29,6 +29,25 @@ class TestPermissionAction:
|
||||
assert PermissionAction.ASK.value == "ask"
|
||||
|
||||
|
||||
class TestPermissionMode:
|
||||
"""Tests for PermissionMode enum - Claude Code parity."""
|
||||
|
||||
def test_mode_values(self):
|
||||
"""Test permission mode enum values."""
|
||||
from praisonaiagents.permissions.rules import PermissionMode
|
||||
|
||||
# DEFAULT - Standard permission checking
|
||||
assert PermissionMode.DEFAULT.value == "default"
|
||||
# ACCEPT_EDITS - Auto-accept file edits
|
||||
assert PermissionMode.ACCEPT_EDITS.value == "accept_edits"
|
||||
# DONT_ASK - Auto-deny prompts
|
||||
assert PermissionMode.DONT_ASK.value == "dont_ask"
|
||||
# BYPASS - Skip all checks (dangerous)
|
||||
assert PermissionMode.BYPASS.value == "bypass_permissions"
|
||||
# PLAN - Read-only exploration
|
||||
assert PermissionMode.PLAN.value == "plan"
|
||||
|
||||
|
||||
class TestPermissionRule:
|
||||
"""Tests for PermissionRule."""
|
||||
|
||||
|
||||
@@ -27,6 +27,47 @@ class TestPluginHook:
|
||||
assert PluginHook.ON_INIT.value == "on_init"
|
||||
assert PluginHook.BEFORE_TOOL.value == "before_tool"
|
||||
assert PluginHook.AFTER_AGENT.value == "after_agent"
|
||||
|
||||
def test_message_hooks(self):
|
||||
"""Test message lifecycle hooks (moltbot parity)."""
|
||||
assert PluginHook.MESSAGE_RECEIVED.value == "message_received"
|
||||
assert PluginHook.MESSAGE_SENDING.value == "message_sending"
|
||||
assert PluginHook.MESSAGE_SENT.value == "message_sent"
|
||||
|
||||
def test_gateway_hooks(self):
|
||||
"""Test gateway lifecycle hooks (moltbot parity)."""
|
||||
assert PluginHook.GATEWAY_START.value == "gateway_start"
|
||||
assert PluginHook.GATEWAY_STOP.value == "gateway_stop"
|
||||
|
||||
def test_session_hooks(self):
|
||||
"""Test session lifecycle hooks."""
|
||||
assert PluginHook.SESSION_START.value == "session_start"
|
||||
assert PluginHook.SESSION_END.value == "session_end"
|
||||
|
||||
def test_compaction_hooks(self):
|
||||
"""Test compaction hooks (memory management)."""
|
||||
assert PluginHook.BEFORE_COMPACTION.value == "before_compaction"
|
||||
assert PluginHook.AFTER_COMPACTION.value == "after_compaction"
|
||||
|
||||
def test_error_hooks(self):
|
||||
"""Test error handling hooks."""
|
||||
assert PluginHook.ON_ERROR.value == "on_error"
|
||||
assert PluginHook.ON_RETRY.value == "on_retry"
|
||||
|
||||
def test_tool_result_persist_hook(self):
|
||||
"""Test tool_result_persist hook (moltbot parity)."""
|
||||
assert PluginHook.TOOL_RESULT_PERSIST.value == "tool_result_persist"
|
||||
|
||||
def test_claude_code_parity_hooks(self):
|
||||
"""Test Claude Code parity hooks (new additions)."""
|
||||
# USER_PROMPT_SUBMIT - when user submits a prompt
|
||||
assert PluginHook.USER_PROMPT_SUBMIT.value == "user_prompt_submit"
|
||||
# NOTIFICATION - when notification is sent
|
||||
assert PluginHook.NOTIFICATION.value == "notification"
|
||||
# SUBAGENT_STOP - when subagent completes
|
||||
assert PluginHook.SUBAGENT_STOP.value == "subagent_stop"
|
||||
# SETUP - on initialization/maintenance
|
||||
assert PluginHook.SETUP.value == "setup"
|
||||
|
||||
|
||||
class TestPluginInfo:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for Task module."""
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Tests for PraisonAI Robustness Improvements.
|
||||
|
||||
Tests the following features:
|
||||
1. Task.skip_on_failure - Allow workflows to continue when one step fails
|
||||
2. Task.retry_delay - Exponential backoff for retries
|
||||
3. Workflow.history - Execution trace for debugging
|
||||
4. Include(workflow=) - Workflow composition
|
||||
5. when() - Alias for if_() with cleaner name
|
||||
|
||||
TDD: These tests are written FIRST before implementation.
|
||||
"""
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTaskSkipOnFailure:
|
||||
"""Tests for Task.skip_on_failure parameter."""
|
||||
|
||||
def test_task_skip_on_failure_default_false(self):
|
||||
"""skip_on_failure should default to False."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Test task")
|
||||
assert task.skip_on_failure is False
|
||||
|
||||
def test_task_skip_on_failure_true(self):
|
||||
"""Task should accept skip_on_failure=True."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Optional step", skip_on_failure=True)
|
||||
assert task.skip_on_failure is True
|
||||
|
||||
def test_task_skip_on_failure_false_explicit(self):
|
||||
"""Task should accept skip_on_failure=False explicitly."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Critical step", skip_on_failure=False)
|
||||
assert task.skip_on_failure is False
|
||||
|
||||
|
||||
class TestTaskRetryDelay:
|
||||
"""Tests for Task.retry_delay parameter."""
|
||||
|
||||
def test_task_retry_delay_default_zero(self):
|
||||
"""retry_delay should default to 0.0."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Test task")
|
||||
assert task.retry_delay == 0.0
|
||||
|
||||
def test_task_retry_delay_custom_value(self):
|
||||
"""Task should accept custom retry_delay."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Test task", retry_delay=1.0)
|
||||
assert task.retry_delay == 1.0
|
||||
|
||||
def test_task_retry_delay_float(self):
|
||||
"""retry_delay should accept float values."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Test task", retry_delay=0.5)
|
||||
assert task.retry_delay == 0.5
|
||||
|
||||
def test_task_retry_delay_with_max_retries(self):
|
||||
"""retry_delay should work alongside max_retries."""
|
||||
from praisonaiagents import Task
|
||||
task = Task(description="Test task", retry_delay=2.0, max_retries=5)
|
||||
assert task.retry_delay == 2.0
|
||||
assert task.max_retries == 5
|
||||
|
||||
|
||||
class TestWorkflowHistory:
|
||||
"""Tests for Workflow.history parameter."""
|
||||
|
||||
def test_workflow_history_default_false(self):
|
||||
"""history should default to False."""
|
||||
from praisonaiagents.workflows import Workflow
|
||||
workflow = Workflow(steps=[])
|
||||
assert workflow.history is False
|
||||
|
||||
def test_workflow_history_true(self):
|
||||
"""Workflow should accept history=True."""
|
||||
from praisonaiagents.workflows import Workflow
|
||||
workflow = Workflow(steps=[], history=True)
|
||||
assert workflow.history is True
|
||||
|
||||
def test_workflow_execution_history_initialized(self):
|
||||
"""_execution_history should be initialized as empty list."""
|
||||
from praisonaiagents.workflows import Workflow
|
||||
workflow = Workflow(steps=[], history=True)
|
||||
assert hasattr(workflow, '_execution_history')
|
||||
assert workflow._execution_history == []
|
||||
|
||||
def test_workflow_get_history_method_exists(self):
|
||||
"""Workflow should have get_history() method."""
|
||||
from praisonaiagents.workflows import Workflow
|
||||
workflow = Workflow(steps=[], history=True)
|
||||
assert hasattr(workflow, 'get_history')
|
||||
assert callable(workflow.get_history)
|
||||
|
||||
def test_workflow_get_history_returns_list(self):
|
||||
"""get_history() should return a list."""
|
||||
from praisonaiagents.workflows import Workflow
|
||||
workflow = Workflow(steps=[], history=True)
|
||||
history = workflow.get_history()
|
||||
assert isinstance(history, list)
|
||||
|
||||
|
||||
class TestIncludeWorkflow:
|
||||
"""Tests for Include with workflow parameter."""
|
||||
|
||||
def test_include_with_recipe(self):
|
||||
"""Include should work with recipe parameter (existing behavior)."""
|
||||
from praisonaiagents.workflows import include
|
||||
inc = include(recipe="wordpress-publisher")
|
||||
assert inc.recipe == "wordpress-publisher"
|
||||
|
||||
def test_include_with_workflow(self):
|
||||
"""Include should accept workflow parameter."""
|
||||
from praisonaiagents.workflows import include, Workflow
|
||||
|
||||
sub_workflow = Workflow(name="sub", steps=[])
|
||||
inc = include(workflow=sub_workflow)
|
||||
assert inc.workflow is sub_workflow
|
||||
|
||||
def test_include_requires_recipe_or_workflow(self):
|
||||
"""Include should raise error if neither recipe nor workflow provided."""
|
||||
from praisonaiagents.workflows import Include
|
||||
|
||||
with pytest.raises(ValueError, match="Either 'recipe' or 'workflow' must be provided"):
|
||||
Include()
|
||||
|
||||
def test_include_with_workflow_and_input(self):
|
||||
"""Include should accept workflow with input override."""
|
||||
from praisonaiagents.workflows import include, Workflow
|
||||
|
||||
sub_workflow = Workflow(name="sub", steps=[])
|
||||
inc = include(workflow=sub_workflow, input="custom input")
|
||||
assert inc.workflow is sub_workflow
|
||||
assert inc.input == "custom input"
|
||||
|
||||
|
||||
class TestWhenAlias:
|
||||
"""Tests for when() alias for if_()."""
|
||||
|
||||
def test_when_function_exists(self):
|
||||
"""when() function should exist."""
|
||||
from praisonaiagents.workflows import when
|
||||
assert callable(when)
|
||||
|
||||
def test_when_returns_if_object(self):
|
||||
"""when() should return an If object."""
|
||||
from praisonaiagents.workflows import when, If
|
||||
result = when(condition="True", then_steps=[])
|
||||
assert isinstance(result, If)
|
||||
|
||||
def test_when_with_condition_and_then_steps(self):
|
||||
"""when() should accept condition and then_steps."""
|
||||
from praisonaiagents.workflows import when
|
||||
result = when(
|
||||
condition="{{score}} > 80",
|
||||
then_steps=["step1", "step2"]
|
||||
)
|
||||
assert result.condition == "{{score}} > 80"
|
||||
assert result.then_steps == ["step1", "step2"]
|
||||
|
||||
def test_when_with_else_steps(self):
|
||||
"""when() should accept optional else_steps."""
|
||||
from praisonaiagents.workflows import when
|
||||
result = when(
|
||||
condition="{{approved}}",
|
||||
then_steps=["approve"],
|
||||
else_steps=["reject"]
|
||||
)
|
||||
assert result.then_steps == ["approve"]
|
||||
assert result.else_steps == ["reject"]
|
||||
|
||||
def test_if_deprecated_warning(self):
|
||||
"""if_() should show deprecation warning."""
|
||||
from praisonaiagents.workflows import if_
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
_ = if_(condition="True", then_steps=[])
|
||||
|
||||
# Check deprecation warning was issued
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) >= 1
|
||||
assert "when()" in str(deprecation_warnings[0].message)
|
||||
|
||||
|
||||
class TestExportsAndImports:
|
||||
"""Tests for proper exports in __init__.py files."""
|
||||
|
||||
def test_when_exported_from_workflows(self):
|
||||
"""when should be exported from praisonaiagents.workflows."""
|
||||
from praisonaiagents.workflows import when
|
||||
assert callable(when)
|
||||
|
||||
def test_when_in_workflows_all(self):
|
||||
"""when should be in workflows __all__."""
|
||||
import praisonaiagents.workflows as workflows_module
|
||||
assert 'when' in workflows_module.__all__
|
||||
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Tests for Task + WorkflowStep Consolidation (Phase 4).
|
||||
|
||||
Tests the new parameters added to Task from WorkflowStep:
|
||||
- action (alias for description)
|
||||
- handler (custom function instead of agent)
|
||||
- should_run (condition function)
|
||||
- loop_over / loop_var (iteration support)
|
||||
- execution, routing, output (consolidated configs)
|
||||
- autonomy, knowledge, web, reflection, planning, hooks, caching (feature configs)
|
||||
|
||||
TDD: These tests are written FIRST before implementation.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
|
||||
class TestTaskActionAlias:
|
||||
"""Test that 'action' works as an alias for 'description'."""
|
||||
|
||||
def test_action_as_description_alias(self):
|
||||
"""When action is provided without description, action becomes description."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(action="Write a blog post about AI")
|
||||
|
||||
assert task.description == "Write a blog post about AI"
|
||||
assert task.action == "Write a blog post about AI"
|
||||
|
||||
def test_description_takes_precedence(self):
|
||||
"""When both action and description provided, description takes precedence."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Primary description",
|
||||
action="Secondary action"
|
||||
)
|
||||
|
||||
assert task.description == "Primary description"
|
||||
assert task.action == "Secondary action"
|
||||
|
||||
def test_action_attribute_always_set(self):
|
||||
"""Task.action should always be set (from action or description)."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Research AI trends")
|
||||
|
||||
assert task.action == "Research AI trends"
|
||||
|
||||
|
||||
class TestTaskHandler:
|
||||
"""Test that Task supports handler functions (from WorkflowStep)."""
|
||||
|
||||
def test_handler_param_exists(self):
|
||||
"""Task should accept a handler parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def my_handler(context):
|
||||
return {"output": "processed"}
|
||||
|
||||
task = Task(
|
||||
description="Process data",
|
||||
handler=my_handler
|
||||
)
|
||||
|
||||
assert task.handler is my_handler
|
||||
|
||||
def test_handler_default_none(self):
|
||||
"""Handler should default to None."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Simple task")
|
||||
|
||||
assert task.handler is None
|
||||
|
||||
def test_handler_callable_check(self):
|
||||
"""Handler must be callable if provided."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def valid_handler(ctx):
|
||||
return "result"
|
||||
|
||||
task = Task(description="Test", handler=valid_handler)
|
||||
assert callable(task.handler)
|
||||
|
||||
|
||||
class TestTaskShouldRun:
|
||||
"""Test that Task supports should_run condition (from WorkflowStep)."""
|
||||
|
||||
def test_should_run_param_exists(self):
|
||||
"""Task should accept a should_run parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def check_condition(context):
|
||||
return context.get("enabled", False)
|
||||
|
||||
task = Task(
|
||||
description="Conditional task",
|
||||
should_run=check_condition
|
||||
)
|
||||
|
||||
assert task.should_run is check_condition
|
||||
|
||||
def test_should_run_default_none(self):
|
||||
"""should_run should default to None (always run)."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Always runs")
|
||||
|
||||
assert task.should_run is None
|
||||
|
||||
|
||||
class TestTaskLoopSupport:
|
||||
"""Test that Task supports loop_over and loop_var (from WorkflowStep)."""
|
||||
|
||||
def test_loop_over_param_exists(self):
|
||||
"""Task should accept loop_over parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Process {{item}}",
|
||||
loop_over="items"
|
||||
)
|
||||
|
||||
assert task.loop_over == "items"
|
||||
|
||||
def test_loop_var_param_exists(self):
|
||||
"""Task should accept loop_var parameter with default 'item'."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Process {{item}}",
|
||||
loop_over="items"
|
||||
)
|
||||
|
||||
assert task.loop_var == "item"
|
||||
|
||||
def test_loop_var_custom_value(self):
|
||||
"""Task should accept custom loop_var value."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Process {{doc}}",
|
||||
loop_over="documents",
|
||||
loop_var="doc"
|
||||
)
|
||||
|
||||
assert task.loop_var == "doc"
|
||||
|
||||
def test_loop_defaults_none(self):
|
||||
"""loop_over should default to None."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="No loop")
|
||||
|
||||
assert task.loop_over is None
|
||||
|
||||
|
||||
class TestTaskConsolidatedConfigs:
|
||||
"""Test that Task supports consolidated config objects (from WorkflowStep)."""
|
||||
|
||||
def test_execution_config_dict(self):
|
||||
"""Task should accept execution config as dict."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Execute task",
|
||||
execution={"max_retries": 5, "async_exec": True}
|
||||
)
|
||||
|
||||
assert task.execution is not None
|
||||
|
||||
def test_routing_config_dict(self):
|
||||
"""Task should accept routing config as dict."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Route task",
|
||||
routing={"next": ["task_b"], "on_error": "stop"}
|
||||
)
|
||||
|
||||
assert task.routing is not None
|
||||
|
||||
def test_output_config_dict(self):
|
||||
"""Task should accept output config as dict (alternative to output_file/json)."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Output task",
|
||||
output_config={"file": "result.txt", "variable": "result"}
|
||||
)
|
||||
|
||||
assert task.output_config is not None
|
||||
|
||||
|
||||
class TestTaskFeatureConfigs:
|
||||
"""Test that Task supports feature configs (from WorkflowStep)."""
|
||||
|
||||
def test_autonomy_param(self):
|
||||
"""Task should accept autonomy parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Autonomous task", autonomy=True)
|
||||
|
||||
assert task.autonomy is True
|
||||
|
||||
def test_knowledge_param(self):
|
||||
"""Task should accept knowledge parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Knowledge task", knowledge=["docs/"])
|
||||
|
||||
assert task.knowledge == ["docs/"]
|
||||
|
||||
def test_web_param(self):
|
||||
"""Task should accept web parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Web task", web=True)
|
||||
|
||||
assert task.web is True
|
||||
|
||||
def test_reflection_param(self):
|
||||
"""Task should accept reflection parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Reflective task", reflection=True)
|
||||
|
||||
assert task.reflection is True
|
||||
|
||||
def test_planning_param(self):
|
||||
"""Task should accept planning parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Planning task", planning=True)
|
||||
|
||||
assert task.planning is True
|
||||
|
||||
def test_hooks_param(self):
|
||||
"""Task should accept hooks parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def my_hook(ctx):
|
||||
pass
|
||||
|
||||
task = Task(description="Hooked task", hooks=[my_hook])
|
||||
|
||||
assert task.hooks == [my_hook]
|
||||
|
||||
def test_caching_param(self):
|
||||
"""Task should accept caching parameter."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Cached task", caching=True)
|
||||
|
||||
assert task.caching is True
|
||||
|
||||
|
||||
class TestTaskBackwardCompatibility:
|
||||
"""Ensure existing Task API still works unchanged."""
|
||||
|
||||
def test_basic_task_creation(self):
|
||||
"""Basic Task creation should work as before."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(description="Simple task")
|
||||
|
||||
assert task.description == "Simple task"
|
||||
assert task.status == "not started"
|
||||
|
||||
def test_task_with_expected_output(self):
|
||||
"""Task with expected_output should work."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="Research task",
|
||||
expected_output="A detailed report"
|
||||
)
|
||||
|
||||
assert task.expected_output == "A detailed report"
|
||||
|
||||
def test_task_with_tools(self):
|
||||
"""Task with tools should work."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def my_tool():
|
||||
pass
|
||||
|
||||
task = Task(description="Tool task", tools=[my_tool])
|
||||
|
||||
assert my_tool in task.tools
|
||||
|
||||
def test_task_with_output_file(self):
|
||||
"""Task with output_file should still work."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
task = Task(
|
||||
description="File output task",
|
||||
output_file="output.txt"
|
||||
)
|
||||
|
||||
assert task.output_file == "output.txt"
|
||||
|
||||
def test_task_with_guardrails(self):
|
||||
"""Task with guardrails should still work."""
|
||||
from praisonaiagents import Task
|
||||
|
||||
def my_guardrail(output):
|
||||
return (True, output)
|
||||
|
||||
task = Task(
|
||||
description="Guarded task",
|
||||
guardrails=my_guardrail
|
||||
)
|
||||
|
||||
assert task.guardrail is my_guardrail
|
||||
|
||||
|
||||
class TestWorkflowStepDeprecation:
|
||||
"""Test that WorkflowStep shows deprecation warning and creates Task."""
|
||||
|
||||
def test_workflowstep_import_shows_warning(self):
|
||||
"""Importing WorkflowStep should show deprecation warning."""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
from praisonaiagents import WorkflowStep
|
||||
|
||||
# Check that a deprecation warning was issued
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) >= 1
|
||||
assert "WorkflowStep" in str(deprecation_warnings[0].message)
|
||||
assert "Task" in str(deprecation_warnings[0].message)
|
||||
|
||||
def test_workflowstep_creates_task_instance(self):
|
||||
"""WorkflowStep(...) should create a Task instance."""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
from praisonaiagents import WorkflowStep, Task
|
||||
|
||||
step = WorkflowStep(
|
||||
name="writer",
|
||||
action="Write content"
|
||||
)
|
||||
|
||||
# Should be a Task instance
|
||||
assert isinstance(step, Task)
|
||||
|
||||
def test_workflowstep_params_pass_through(self):
|
||||
"""WorkflowStep params should pass through to Task."""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
from praisonaiagents import WorkflowStep
|
||||
|
||||
def my_handler(ctx):
|
||||
return "result"
|
||||
|
||||
step = WorkflowStep(
|
||||
name="processor",
|
||||
action="Process data",
|
||||
handler=my_handler,
|
||||
loop_over="items"
|
||||
)
|
||||
|
||||
assert step.name == "processor"
|
||||
assert step.action == "Process data"
|
||||
assert step.handler is my_handler
|
||||
assert step.loop_over == "items"
|
||||
|
||||
|
||||
class TestWorkflowWithTask:
|
||||
"""Test that Workflow works with Task instances."""
|
||||
|
||||
def test_workflow_accepts_task_in_steps(self):
|
||||
"""Workflow should accept Task instances in steps parameter."""
|
||||
from praisonaiagents import Task
|
||||
from praisonaiagents.workflows import Workflow
|
||||
|
||||
task1 = Task(name="step1", action="First step")
|
||||
task2 = Task(name="step2", action="Second step")
|
||||
|
||||
workflow = Workflow(steps=[task1, task2])
|
||||
|
||||
assert len(workflow.steps) == 2
|
||||
@@ -35,9 +35,10 @@ class TestSubagentTool:
|
||||
def test_subagent_with_factory(self):
|
||||
"""Test subagent execution with factory."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, tools=None):
|
||||
def __init__(self, name, tools=None, llm=None):
|
||||
self.name = name
|
||||
self.tools = tools
|
||||
self.llm = llm
|
||||
|
||||
def chat(self, prompt):
|
||||
return f"Executed: {prompt}"
|
||||
@@ -87,6 +88,48 @@ class TestSubagentTool:
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_subagent_with_llm_selection(self):
|
||||
"""Test subagent with custom LLM model selection (Claude Code parity)."""
|
||||
class MockAgent:
|
||||
def __init__(self, name, tools=None, llm=None):
|
||||
self.name = name
|
||||
self.tools = tools
|
||||
self.llm = llm
|
||||
|
||||
def chat(self, prompt):
|
||||
return f"Model: {self.llm}, Task: {prompt}"
|
||||
|
||||
tool = create_subagent_tool(
|
||||
agent_factory=lambda **kwargs: MockAgent(**kwargs),
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
func = tool["function"]
|
||||
|
||||
# Test with default LLM
|
||||
result = func(task="Test task")
|
||||
assert result["success"] is True
|
||||
assert "gpt-4o-mini" in result["output"]
|
||||
|
||||
# Test with custom LLM override
|
||||
result2 = func(task="Test task", llm="claude-3-sonnet")
|
||||
assert result2["success"] is True
|
||||
assert "claude-3-sonnet" in result2["output"]
|
||||
|
||||
def test_subagent_with_permission_mode(self):
|
||||
"""Test subagent with permission mode (Claude Code parity)."""
|
||||
tool = create_subagent_tool()
|
||||
func = tool["function"]
|
||||
|
||||
# Test with permission_mode parameter
|
||||
result = func(
|
||||
task="Analyze code",
|
||||
permission_mode="plan" # Read-only mode
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
# Permission mode should be passed through
|
||||
assert result.get("permission_mode") == "plan"
|
||||
|
||||
|
||||
class TestBatchTool:
|
||||
|
||||
Generated
+1
-1
@@ -2549,7 +2549,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.14.2"
|
||||
version = "0.15.2"
|
||||
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.11.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.11.2.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.12.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.12.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -18,6 +18,7 @@ __all__ = [
|
||||
'DeployConfig',
|
||||
'DeployType',
|
||||
'CloudProvider',
|
||||
'AgentApp', # Production deployment platform (v0.14.16+)
|
||||
'recipe',
|
||||
'embed',
|
||||
'embedding',
|
||||
@@ -62,6 +63,9 @@ def __getattr__(name):
|
||||
elif name == 'EmbeddingResult':
|
||||
from praisonaiagents.embedding import EmbeddingResult
|
||||
return EmbeddingResult
|
||||
elif name == 'AgentApp':
|
||||
from .app import AgentApp
|
||||
return AgentApp
|
||||
|
||||
# Try praisonaiagents exports
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
AgentApp module for production deployment of AI agents.
|
||||
|
||||
This module provides the AgentApp class which implements the AgentAppProtocol
|
||||
from the core SDK. It wraps agents, managers, and workflows into a unified
|
||||
FastAPI-based web service.
|
||||
|
||||
Example:
|
||||
from praisonai import AgentApp
|
||||
from praisonaiagents import Agent
|
||||
|
||||
assistant = Agent(name="assistant", instructions="Be helpful")
|
||||
|
||||
app = AgentApp(
|
||||
name="My AI App",
|
||||
agents=[assistant],
|
||||
)
|
||||
app.serve(port=8000)
|
||||
"""
|
||||
|
||||
from .app import AgentApp
|
||||
|
||||
__all__ = ['AgentApp']
|
||||
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
AgentApp implementation for production deployment.
|
||||
|
||||
This module provides the AgentApp class which implements the AgentAppProtocol.
|
||||
It creates a FastAPI-based web service for deploying agents.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol
|
||||
|
||||
|
||||
class AgentApp:
|
||||
"""
|
||||
Production platform for deploying AI agents as web services.
|
||||
|
||||
AgentApp wraps agents, managers, and workflows into a unified FastAPI
|
||||
application with REST and WebSocket endpoints.
|
||||
|
||||
Example:
|
||||
from praisonai import AgentApp
|
||||
from praisonaiagents import Agent
|
||||
|
||||
assistant = Agent(name="assistant", instructions="Be helpful")
|
||||
|
||||
# Simple usage
|
||||
app = AgentApp(agents=[assistant])
|
||||
app.serve(port=8000)
|
||||
|
||||
# With configuration
|
||||
app = AgentApp(
|
||||
name="My AI App",
|
||||
agents=[assistant],
|
||||
config=AgentAppConfig(port=9000, reload=True)
|
||||
)
|
||||
app.serve()
|
||||
|
||||
Attributes:
|
||||
name: Name of the application
|
||||
agents: List of Agent instances
|
||||
managers: List of AgentManager instances
|
||||
workflows: List of Workflow instances
|
||||
config: AgentAppConfig instance
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "PraisonAI App",
|
||||
agents: Optional[List[Any]] = None,
|
||||
managers: Optional[List[Any]] = None,
|
||||
workflows: Optional[List[Any]] = None,
|
||||
config: Optional[AgentAppConfig] = None,
|
||||
**kwargs: Any
|
||||
):
|
||||
"""
|
||||
Initialize AgentApp.
|
||||
|
||||
Args:
|
||||
name: Name of the application
|
||||
agents: List of Agent instances to serve
|
||||
managers: List of AgentManager instances to serve
|
||||
workflows: List of Workflow instances to serve
|
||||
config: AgentAppConfig for server configuration
|
||||
**kwargs: Additional configuration passed to AgentAppConfig
|
||||
"""
|
||||
self.name = name
|
||||
self.agents = agents or []
|
||||
self.managers = managers or []
|
||||
self.workflows = workflows or []
|
||||
|
||||
# Merge kwargs into config
|
||||
if config is None:
|
||||
config = AgentAppConfig(name=name, **kwargs)
|
||||
self.config = config
|
||||
|
||||
# FastAPI app instance (lazy initialized)
|
||||
self._app = None
|
||||
|
||||
def _create_app(self) -> Any:
|
||||
"""Create the FastAPI application."""
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"FastAPI is required for AgentApp. "
|
||||
"Install with: pip install praisonai[api]"
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title=self.name,
|
||||
description="PraisonAI Agent Application",
|
||||
version="1.0.0",
|
||||
docs_url=self.config.docs_url,
|
||||
openapi_url=self.config.openapi_url,
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=self.config.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Register routes
|
||||
self._register_routes(app)
|
||||
|
||||
return app
|
||||
|
||||
def _register_routes(self, app: Any) -> None:
|
||||
"""Register API routes."""
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
agent_name: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
response: str
|
||||
agent_name: str
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"name": self.name,
|
||||
"status": "running",
|
||||
"agents": [getattr(a, 'name', str(a)) for a in self.agents],
|
||||
"managers": len(self.managers),
|
||||
"workflows": len(self.workflows),
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get(f"{self.config.api_prefix}/agents")
|
||||
async def list_agents():
|
||||
return {
|
||||
"agents": [
|
||||
{
|
||||
"name": getattr(a, 'name', f'agent_{i}'),
|
||||
"role": getattr(a, 'role', None),
|
||||
"instructions": getattr(a, 'instructions', None)[:100] + "..."
|
||||
if getattr(a, 'instructions', None) and len(getattr(a, 'instructions', '')) > 100
|
||||
else getattr(a, 'instructions', None),
|
||||
}
|
||||
for i, a in enumerate(self.agents)
|
||||
]
|
||||
}
|
||||
|
||||
@app.post(f"{self.config.api_prefix}/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
# Find the agent
|
||||
agent = None
|
||||
if request.agent_name:
|
||||
for a in self.agents:
|
||||
if getattr(a, 'name', None) == request.agent_name:
|
||||
agent = a
|
||||
break
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=404, detail=f"Agent '{request.agent_name}' not found")
|
||||
elif self.agents:
|
||||
agent = self.agents[0]
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="No agents available")
|
||||
|
||||
# Call the agent
|
||||
try:
|
||||
response = agent.chat(request.message)
|
||||
return ChatResponse(
|
||||
response=str(response),
|
||||
agent_name=getattr(agent, 'name', 'unknown'),
|
||||
session_id=request.session_id,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def get_app(self) -> Any:
|
||||
"""
|
||||
Get the FastAPI application instance.
|
||||
|
||||
Returns:
|
||||
The FastAPI application instance for custom mounting or configuration.
|
||||
"""
|
||||
if self._app is None:
|
||||
self._app = self._create_app()
|
||||
return self._app
|
||||
|
||||
def serve(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
reload: bool = False,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Start the AgentApp server.
|
||||
|
||||
Args:
|
||||
host: Host address to bind to (default from config)
|
||||
port: Port number to listen on (default from config)
|
||||
reload: Enable auto-reload for development
|
||||
**kwargs: Additional uvicorn configuration
|
||||
"""
|
||||
try:
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Uvicorn is required for AgentApp. "
|
||||
"Install with: pip install praisonai[api]"
|
||||
)
|
||||
|
||||
app = self.get_app()
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host or self.config.host,
|
||||
port=port or self.config.port,
|
||||
reload=reload or self.config.reload,
|
||||
log_level=self.config.log_level,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# Verify protocol compliance
|
||||
def _verify_protocol():
|
||||
"""Verify that AgentApp implements AgentAppProtocol."""
|
||||
assert isinstance(AgentApp(agents=[]), AgentAppProtocol), \
|
||||
"AgentApp must implement AgentAppProtocol"
|
||||
|
||||
# Run verification at import time (only in debug mode)
|
||||
# _verify_protocol()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Media parsing utilities for bot responses.
|
||||
|
||||
Parses MEDIA: protocol from agent responses to extract audio/image paths.
|
||||
Similar to moltbot's splitMediaFromOutput() pattern.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# Regex to match MEDIA:/path lines
|
||||
MEDIA_REGEX = re.compile(r"^MEDIA:(.+)$", re.MULTILINE)
|
||||
|
||||
# Voice bubble control tag (Telegram)
|
||||
VOICE_TAG = "[[audio_as_voice]]"
|
||||
|
||||
|
||||
def split_media_from_output(text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract MEDIA: paths from agent response text.
|
||||
|
||||
Parses lines like:
|
||||
MEDIA:/tmp/tts_abc123.mp3
|
||||
[[audio_as_voice]]
|
||||
|
||||
Args:
|
||||
text: Raw agent response text
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- text: Cleaned text without MEDIA: lines
|
||||
- media_urls: List of extracted file paths
|
||||
- audio_as_voice: True if [[audio_as_voice]] tag present
|
||||
|
||||
Example:
|
||||
>>> split_media_from_output("Hello\\nMEDIA:/tmp/audio.mp3")
|
||||
{"text": "Hello", "media_urls": ["/tmp/audio.mp3"], "audio_as_voice": False}
|
||||
"""
|
||||
if not text:
|
||||
return {"text": "", "media_urls": [], "audio_as_voice": False}
|
||||
|
||||
# Check for voice tag
|
||||
audio_as_voice = VOICE_TAG in text
|
||||
clean = text.replace(VOICE_TAG, "")
|
||||
|
||||
# Extract media paths
|
||||
media_urls: List[str] = []
|
||||
for match in MEDIA_REGEX.finditer(clean):
|
||||
path = match.group(1).strip()
|
||||
# Remove quotes if present
|
||||
if path.startswith('"') and path.endswith('"'):
|
||||
path = path[1:-1]
|
||||
elif path.startswith("'") and path.endswith("'"):
|
||||
path = path[1:-1]
|
||||
|
||||
# Validate path exists or is URL
|
||||
if path and (os.path.exists(path) or path.startswith("http")):
|
||||
media_urls.append(path)
|
||||
|
||||
# Remove MEDIA: lines from text
|
||||
clean = MEDIA_REGEX.sub("", clean)
|
||||
|
||||
# Clean up whitespace
|
||||
clean = "\n".join(line for line in clean.split("\n") if line.strip())
|
||||
clean = clean.strip()
|
||||
|
||||
return {
|
||||
"text": clean,
|
||||
"media_urls": media_urls,
|
||||
"audio_as_voice": audio_as_voice,
|
||||
}
|
||||
|
||||
|
||||
def is_audio_file(path: str) -> bool:
|
||||
"""Check if file path is an audio file by extension."""
|
||||
audio_extensions = {".mp3", ".wav", ".ogg", ".opus", ".m4a", ".aac", ".flac"}
|
||||
return os.path.splitext(path.lower())[1] in audio_extensions
|
||||
@@ -9,6 +9,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,6 +24,8 @@ from praisonaiagents.bots import (
|
||||
MessageType,
|
||||
)
|
||||
|
||||
from .media import split_media_from_output, is_audio_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -81,6 +85,13 @@ class SlackBot:
|
||||
|
||||
self._message_handlers: List[Callable] = []
|
||||
self._command_handlers: Dict[str, Callable] = {}
|
||||
|
||||
# Audio capabilities
|
||||
self._stt_enabled: bool = False
|
||||
|
||||
def enable_stt(self, enabled: bool = True) -> None:
|
||||
"""Enable STT for audio file transcription."""
|
||||
self._stt_enabled = enabled
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -159,11 +170,28 @@ class SlackBot:
|
||||
|
||||
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"))
|
||||
logger.info(f"Message received: {text[:100]}...")
|
||||
# Run sync agent.chat() in executor to avoid asyncio.run() conflicts
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self._agent.chat, text)
|
||||
logger.info(f"Response sent: {response[:100]}...")
|
||||
|
||||
# Determine if we should reply in thread
|
||||
thread_ts = None
|
||||
if self.config.reply_in_thread:
|
||||
thread_ts = event.get("thread_ts") or event.get("ts")
|
||||
elif self.config.thread_threshold > 0 and len(response) > self.config.thread_threshold:
|
||||
# Auto-thread long responses
|
||||
thread_ts = event.get("thread_ts") or event.get("ts")
|
||||
|
||||
await self._send_response_with_media(
|
||||
event.get("channel"), say, response, thread_ts=thread_ts
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Agent error: {e}")
|
||||
await say(f"Error: {str(e)}")
|
||||
error_msg = str(e)
|
||||
if error_msg:
|
||||
await say(text=f"Error: {error_msg}", thread_ts=event.get("ts"))
|
||||
|
||||
@self._app.event("app_mention")
|
||||
async def handle_mention(event, say):
|
||||
@@ -176,11 +204,28 @@ class SlackBot:
|
||||
|
||||
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"))
|
||||
logger.info(f"@mention received: {text[:100]}...")
|
||||
# Run sync agent.chat() in executor to avoid asyncio.run() conflicts
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self._agent.chat, text)
|
||||
logger.info(f"Response sent: {response[:100]}...")
|
||||
|
||||
# Determine if we should reply in thread
|
||||
thread_ts = None
|
||||
if self.config.reply_in_thread:
|
||||
thread_ts = event.get("thread_ts") or event.get("ts")
|
||||
elif self.config.thread_threshold > 0 and len(response) > self.config.thread_threshold:
|
||||
# Auto-thread long responses
|
||||
thread_ts = event.get("thread_ts") or event.get("ts")
|
||||
|
||||
await self._send_response_with_media(
|
||||
event.get("channel"), say, response, thread_ts=thread_ts
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Agent error: {e}")
|
||||
await say(f"Error: {str(e)}")
|
||||
error_msg = str(e)
|
||||
if error_msg:
|
||||
await say(text=f"Error: {error_msg}", thread_ts=event.get("ts"))
|
||||
|
||||
for command, handler in self._command_handlers.items():
|
||||
@self._app.command(f"/{command}")
|
||||
@@ -263,12 +308,52 @@ class SlackBot:
|
||||
"""Send a long message, splitting if necessary."""
|
||||
max_len = min(self.config.max_message_length, 4000)
|
||||
|
||||
if not text or not text.strip():
|
||||
logger.warning("Attempted to send empty message")
|
||||
return
|
||||
|
||||
if len(text) <= max_len:
|
||||
await say(text=text, thread_ts=thread_ts)
|
||||
await say(text=text.strip(), 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)
|
||||
if chunk and chunk.strip():
|
||||
await say(text=chunk.strip(), thread_ts=thread_ts)
|
||||
|
||||
async def _send_response_with_media(
|
||||
self,
|
||||
channel_id: str,
|
||||
say,
|
||||
response: str,
|
||||
thread_ts: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Send response, extracting and uploading any MEDIA: files."""
|
||||
# Parse response for media
|
||||
parsed = split_media_from_output(response)
|
||||
text = parsed["text"]
|
||||
media_urls = parsed.get("media_urls", [])
|
||||
|
||||
# Send text first if present
|
||||
if text:
|
||||
await self._send_long_message(say, text, thread_ts=thread_ts)
|
||||
|
||||
# Upload audio files to Slack
|
||||
for media_path in media_urls:
|
||||
if not os.path.exists(media_path):
|
||||
logger.warning(f"Media file not found: {media_path}")
|
||||
continue
|
||||
|
||||
if is_audio_file(media_path) and self._client:
|
||||
try:
|
||||
# Upload file to Slack
|
||||
await self._client.files_upload_v2(
|
||||
channel=channel_id,
|
||||
file=media_path,
|
||||
title=os.path.basename(media_path),
|
||||
thread_ts=thread_ts,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload audio to Slack: {e}")
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
|
||||
@@ -9,6 +9,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,6 +24,8 @@ from praisonaiagents.bots import (
|
||||
MessageType,
|
||||
)
|
||||
|
||||
from .media import split_media_from_output, is_audio_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -70,6 +74,18 @@ class TelegramBot:
|
||||
|
||||
self._message_handlers: List[Callable] = []
|
||||
self._command_handlers: Dict[str, Callable] = {}
|
||||
|
||||
# Audio capabilities (set by BotCapabilities)
|
||||
self._stt_enabled: bool = False
|
||||
self._auto_tts: bool = False
|
||||
|
||||
def enable_stt(self, enabled: bool = True) -> None:
|
||||
"""Enable STT for voice message transcription."""
|
||||
self._stt_enabled = enabled
|
||||
|
||||
def enable_auto_tts(self, enabled: bool = True) -> None:
|
||||
"""Enable auto-TTS for responses."""
|
||||
self._auto_tts = enabled
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -115,10 +131,20 @@ class TelegramBot:
|
||||
)
|
||||
|
||||
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not update.message or not update.message.text:
|
||||
# Handle text OR audio messages
|
||||
message_text = None
|
||||
|
||||
if update.message:
|
||||
# Check for voice/audio first
|
||||
if update.message.voice or update.message.audio:
|
||||
message_text = await self._transcribe_audio(update)
|
||||
elif update.message.text:
|
||||
message_text = update.message.text
|
||||
|
||||
if not update.message or not message_text:
|
||||
return
|
||||
|
||||
message = self._convert_update_to_message(update)
|
||||
message = self._convert_update_to_message(update, override_text=message_text)
|
||||
|
||||
if not self.config.is_user_allowed(message.sender.user_id if message.sender else ""):
|
||||
return
|
||||
@@ -139,8 +165,8 @@ class TelegramBot:
|
||||
await update.message.chat.send_action("typing")
|
||||
|
||||
try:
|
||||
response = self._agent.chat(message.text)
|
||||
await self._send_long_message(
|
||||
response = self._agent.chat(message_text)
|
||||
await self._send_response_with_media(
|
||||
update.message.chat_id,
|
||||
response,
|
||||
reply_to=update.message.message_id,
|
||||
@@ -149,6 +175,10 @@ class TelegramBot:
|
||||
logger.error(f"Agent error: {e}")
|
||||
await update.message.reply_text(f"Error: {str(e)}")
|
||||
|
||||
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle voice messages."""
|
||||
await handle_message(update, context)
|
||||
|
||||
async def handle_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not update.message or not update.message.text:
|
||||
return
|
||||
@@ -173,6 +203,11 @@ class TelegramBot:
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
|
||||
)
|
||||
|
||||
# Add voice/audio handlers
|
||||
self._application.add_handler(
|
||||
MessageHandler(filters.VOICE | filters.AUDIO, handle_voice)
|
||||
)
|
||||
|
||||
self._is_running = True
|
||||
logger.info(f"Telegram bot started: @{self._bot_user.username}")
|
||||
|
||||
@@ -259,6 +294,82 @@ class TelegramBot:
|
||||
kwargs["reply_to_message_id"] = reply_to
|
||||
await self._application.bot.send_message(**kwargs)
|
||||
|
||||
async def _transcribe_audio(self, update) -> Optional[str]:
|
||||
"""Download and transcribe voice/audio message."""
|
||||
if not self._stt_enabled:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Get file info
|
||||
if update.message.voice:
|
||||
file = await update.message.voice.get_file()
|
||||
elif update.message.audio:
|
||||
file = await update.message.audio.get_file()
|
||||
else:
|
||||
return None
|
||||
|
||||
# Download to temp file
|
||||
temp_path = os.path.join(tempfile.gettempdir(), f"voice_{file.file_id}.ogg")
|
||||
await file.download_to_drive(temp_path)
|
||||
|
||||
# Transcribe using existing stt_tool
|
||||
try:
|
||||
from praisonai.tools.audio import stt_tool
|
||||
result = stt_tool(temp_path)
|
||||
if result.get("success"):
|
||||
text = result.get("text", "")
|
||||
logger.info(f"Transcribed voice message: {text[:50]}...")
|
||||
return f"[Voice message]: {text}"
|
||||
else:
|
||||
logger.warning(f"STT failed: {result.get('error')}")
|
||||
return None
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Audio transcription error: {e}")
|
||||
return None
|
||||
|
||||
async def _send_response_with_media(
|
||||
self,
|
||||
chat_id: int,
|
||||
response: str,
|
||||
reply_to: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Send response, extracting and sending any MEDIA: files."""
|
||||
# Parse response for media
|
||||
parsed = split_media_from_output(response)
|
||||
text = parsed["text"]
|
||||
media_urls = parsed.get("media_urls", [])
|
||||
audio_as_voice = parsed.get("audio_as_voice", False)
|
||||
|
||||
# Send text first if present
|
||||
if text:
|
||||
await self._send_long_message(chat_id, text, reply_to=reply_to)
|
||||
|
||||
# Send audio files
|
||||
for media_path in media_urls:
|
||||
if not os.path.exists(media_path):
|
||||
logger.warning(f"Media file not found: {media_path}")
|
||||
continue
|
||||
|
||||
if is_audio_file(media_path):
|
||||
try:
|
||||
with open(media_path, "rb") as f:
|
||||
if audio_as_voice:
|
||||
await self._application.bot.send_voice(
|
||||
chat_id=chat_id, voice=f
|
||||
)
|
||||
else:
|
||||
await self._application.bot.send_audio(
|
||||
chat_id=chat_id, audio=f
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send audio: {e}")
|
||||
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
channel_id: str,
|
||||
@@ -355,7 +466,7 @@ class TelegramBot:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _convert_update_to_message(self, update) -> BotMessage:
|
||||
def _convert_update_to_message(self, update, override_text: Optional[str] = None) -> BotMessage:
|
||||
"""Convert Telegram Update to BotMessage."""
|
||||
msg = update.message
|
||||
|
||||
@@ -373,14 +484,17 @@ class TelegramBot:
|
||||
channel_type=channel_type,
|
||||
)
|
||||
|
||||
msg_type = MessageType.COMMAND if msg.text and msg.text.startswith("/") else MessageType.TEXT
|
||||
# Use override text for transcribed audio, otherwise message text
|
||||
content = override_text if override_text else (msg.text or "")
|
||||
msg_type = MessageType.COMMAND if content and content.startswith("/") else MessageType.TEXT
|
||||
|
||||
return BotMessage(
|
||||
message_id=str(msg.message_id),
|
||||
content=msg.text or "",
|
||||
content=content,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -219,6 +219,12 @@ def register_commands():
|
||||
from .commands.replay import app as replay_app
|
||||
from .commands.loop import app as loop_app
|
||||
|
||||
# Import new moltbot-inspired commands
|
||||
from .commands.bot import app as bot_app
|
||||
from .commands.browser import app as browser_app
|
||||
from .commands.plugins import app as plugins_app
|
||||
from .commands.sandbox import app as sandbox_app
|
||||
|
||||
# Import TUI and queue commands
|
||||
from .features.tui.debug import create_debug_app as create_tui_debug_app
|
||||
from .features.tui.cli import create_queue_app
|
||||
@@ -279,6 +285,118 @@ def register_commands():
|
||||
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")
|
||||
|
||||
# Helper function for loading agents from config
|
||||
def _load_agents_from_config_file(config_path: str, console) -> list:
|
||||
"""Load agents from a YAML config file."""
|
||||
import yaml
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error loading config: {e}[/red]")
|
||||
return []
|
||||
|
||||
if not config_data:
|
||||
return []
|
||||
|
||||
agents = []
|
||||
|
||||
# Try to load agents from config
|
||||
agents_config = config_data.get('agents', [])
|
||||
if not agents_config and 'agent' in config_data:
|
||||
agents_config = [config_data['agent']]
|
||||
|
||||
if agents_config:
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
for agent_data in agents_config:
|
||||
if isinstance(agent_data, dict):
|
||||
agent = Agent(
|
||||
name=agent_data.get('name', 'Agent'),
|
||||
role=agent_data.get('role'),
|
||||
instructions=agent_data.get('instructions', agent_data.get('goal', '')),
|
||||
llm=agent_data.get('llm'),
|
||||
)
|
||||
agents.append(agent)
|
||||
console.print(f"[green]✓ Loaded agent: {agent.name}[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not load agents from config: {e}[/yellow]")
|
||||
|
||||
return agents
|
||||
|
||||
# Register app command directly using Typer
|
||||
@app.command(name="app")
|
||||
def app_cmd(
|
||||
port: int = typer.Option(8000, "--port", "-p", help="Port to listen on"),
|
||||
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
||||
config: str = typer.Option(None, "--config", "-c", help="Path to config file (YAML)"),
|
||||
reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload for development"),
|
||||
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode"),
|
||||
name: str = typer.Option("PraisonAI App", "--name", "-n", help="Application name"),
|
||||
):
|
||||
"""
|
||||
Start an AgentApp server for production deployment.
|
||||
|
||||
AgentApp provides a FastAPI-based web service for deploying AI agents
|
||||
with REST and WebSocket endpoints.
|
||||
"""
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
||||
try:
|
||||
from praisonai import AgentApp
|
||||
from praisonaiagents import AgentAppConfig
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Error importing AgentApp: {e}[/red]")
|
||||
console.print("[yellow]Install with: pip install praisonai[api][/yellow]")
|
||||
raise typer.Abort()
|
||||
|
||||
# Load agents from config file if provided
|
||||
agents = []
|
||||
if config:
|
||||
agents = _load_agents_from_config_file(config, console)
|
||||
|
||||
# Create config
|
||||
app_config = AgentAppConfig(
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
# Create and start app
|
||||
console.print(f"\n[bold green]🚀 Starting {name}[/bold green]")
|
||||
console.print(f"[dim]Host: {host}:{port}[/dim]")
|
||||
if agents:
|
||||
console.print(f"[dim]Agents: {len(agents)}[/dim]")
|
||||
if reload:
|
||||
console.print("[yellow]Auto-reload enabled (development mode)[/yellow]")
|
||||
console.print()
|
||||
|
||||
try:
|
||||
agent_app = AgentApp(
|
||||
name=name,
|
||||
agents=agents,
|
||||
config=app_config,
|
||||
)
|
||||
agent_app.serve()
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Missing dependency: {e}[/red]")
|
||||
console.print("[yellow]Install with: pip install praisonai[api][/yellow]")
|
||||
raise typer.Abort()
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error starting server: {e}[/red]")
|
||||
raise typer.Abort()
|
||||
|
||||
# Register moltbot-inspired commands
|
||||
app.add_typer(bot_app, name="bot", help="Messaging bots with full agent capabilities")
|
||||
app.add_typer(browser_app, name="browser", help="Browser control for agent automation")
|
||||
app.add_typer(plugins_app, name="plugins", help="Plugin management and inspection")
|
||||
app.add_typer(sandbox_app, name="sandbox", help="Sandbox container management")
|
||||
|
||||
# Register standardise command
|
||||
try:
|
||||
standardise_app = typer.Typer(name="standardise", help="Documentation and examples standardisation (FDEP)")
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
CLI command for AgentApp - production deployment of AI agents.
|
||||
|
||||
Usage:
|
||||
praisonai app # Start server with default config
|
||||
praisonai app --port 9000 # Start on custom port
|
||||
praisonai app --config app.yaml # Load from config file
|
||||
praisonai app --reload # Enable auto-reload for development
|
||||
"""
|
||||
|
||||
import click
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@click.command("app")
|
||||
@click.option("--port", "-p", type=int, default=8000, help="Port to listen on")
|
||||
@click.option("--host", "-h", type=str, default="0.0.0.0", help="Host to bind to")
|
||||
@click.option("--config", "-c", type=click.Path(exists=True), help="Path to config file (YAML)")
|
||||
@click.option("--reload", "-r", is_flag=True, help="Enable auto-reload for development")
|
||||
@click.option("--debug", "-d", is_flag=True, help="Enable debug mode")
|
||||
@click.option("--name", "-n", type=str, default="PraisonAI App", help="Application name")
|
||||
def app_command(
|
||||
port: int,
|
||||
host: str,
|
||||
config: Optional[str],
|
||||
reload: bool,
|
||||
debug: bool,
|
||||
name: str,
|
||||
):
|
||||
"""
|
||||
Start an AgentApp server for production deployment.
|
||||
|
||||
AgentApp provides a FastAPI-based web service for deploying AI agents
|
||||
with REST and WebSocket endpoints.
|
||||
|
||||
Examples:
|
||||
|
||||
# Start with defaults (port 8000)
|
||||
praisonai app
|
||||
|
||||
# Start on custom port
|
||||
praisonai app --port 9000
|
||||
|
||||
# Enable auto-reload for development
|
||||
praisonai app --reload
|
||||
|
||||
# Load agents from config file
|
||||
praisonai app --config agents.yaml
|
||||
"""
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
||||
try:
|
||||
from praisonai import AgentApp
|
||||
from praisonaiagents import AgentAppConfig
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Error importing AgentApp: {e}[/red]")
|
||||
console.print("[yellow]Install with: pip install praisonai[api][/yellow]")
|
||||
raise click.Abort()
|
||||
|
||||
# Load agents from config file if provided
|
||||
agents = []
|
||||
if config:
|
||||
agents = _load_agents_from_config(config, console)
|
||||
|
||||
# Create config
|
||||
app_config = AgentAppConfig(
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
# Create and start app
|
||||
console.print(f"\n[bold green]🚀 Starting {name}[/bold green]")
|
||||
console.print(f"[dim]Host: {host}:{port}[/dim]")
|
||||
if agents:
|
||||
console.print(f"[dim]Agents: {len(agents)}[/dim]")
|
||||
if reload:
|
||||
console.print("[yellow]Auto-reload enabled (development mode)[/yellow]")
|
||||
console.print()
|
||||
|
||||
try:
|
||||
agent_app = AgentApp(
|
||||
name=name,
|
||||
agents=agents,
|
||||
config=app_config,
|
||||
)
|
||||
agent_app.serve()
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Missing dependency: {e}[/red]")
|
||||
console.print("[yellow]Install with: pip install praisonai[api][/yellow]")
|
||||
raise click.Abort()
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error starting server: {e}[/red]")
|
||||
raise click.Abort()
|
||||
|
||||
|
||||
def _load_agents_from_config(config_path: str, console) -> list:
|
||||
"""Load agents from a YAML config file."""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error loading config: {e}[/red]")
|
||||
return []
|
||||
|
||||
if not config_data:
|
||||
return []
|
||||
|
||||
agents = []
|
||||
|
||||
# Try to load agents from config
|
||||
agents_config = config_data.get('agents', [])
|
||||
if not agents_config and 'agent' in config_data:
|
||||
agents_config = [config_data['agent']]
|
||||
|
||||
if agents_config:
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
for agent_data in agents_config:
|
||||
if isinstance(agent_data, dict):
|
||||
agent = Agent(
|
||||
name=agent_data.get('name', 'Agent'),
|
||||
role=agent_data.get('role'),
|
||||
instructions=agent_data.get('instructions', agent_data.get('goal', '')),
|
||||
llm=agent_data.get('llm'),
|
||||
)
|
||||
agents.append(agent)
|
||||
console.print(f"[green]✓ Loaded agent: {agent.name}[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not load agents from config: {e}[/yellow]")
|
||||
|
||||
return agents
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Bot command group for PraisonAI CLI.
|
||||
|
||||
Provides commands for starting messaging bots with full agent capabilities.
|
||||
Inspired by moltbot's comprehensive CLI options.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(
|
||||
help="Start messaging bots with full agent capabilities",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
def _common_options():
|
||||
"""Common options for all bot commands."""
|
||||
pass
|
||||
|
||||
|
||||
@app.command("telegram")
|
||||
def bot_telegram(
|
||||
token: Optional[str] = typer.Option(None, "--token", "-t", help="Telegram bot token", envvar="TELEGRAM_BOT_TOKEN"),
|
||||
agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Agent YAML configuration file"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
browser: bool = typer.Option(False, "--browser", help="Enable browser control"),
|
||||
browser_profile: str = typer.Option("default", "--browser-profile", help="Browser profile name"),
|
||||
browser_headless: bool = typer.Option(False, "--browser-headless", help="Run browser headless"),
|
||||
tools: Optional[List[str]] = typer.Option(None, "--tools", help="Tools to enable"),
|
||||
skills: Optional[List[str]] = typer.Option(None, "--skills", help="Skills to enable"),
|
||||
skills_dir: Optional[str] = typer.Option(None, "--skills-dir", help="Custom skills directory"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
memory_provider: str = typer.Option("default", "--memory-provider", help="Memory provider"),
|
||||
knowledge: bool = typer.Option(False, "--knowledge", help="Enable knowledge/RAG"),
|
||||
knowledge_sources: Optional[List[str]] = typer.Option(None, "--knowledge-sources", help="Knowledge sources"),
|
||||
web_search: bool = typer.Option(False, "--web", "--web-search", help="Enable web search"),
|
||||
web_provider: str = typer.Option("duckduckgo", "--web-provider", help="Web search provider"),
|
||||
sandbox: bool = typer.Option(False, "--sandbox", help="Enable sandbox mode"),
|
||||
exec_enabled: bool = typer.Option(False, "--exec", help="Enable exec tool"),
|
||||
auto_approve: bool = typer.Option(False, "--auto-approve", help="Auto-approve all tool executions"),
|
||||
session_id: Optional[str] = typer.Option(None, "--session-id", help="Session ID"),
|
||||
user_id: Optional[str] = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
thinking: Optional[str] = typer.Option(None, "--thinking", help="Thinking mode (off, minimal, low, medium, high)"),
|
||||
tts: bool = typer.Option(False, "--tts", help="Enable TTS tool for text-to-speech"),
|
||||
tts_voice: str = typer.Option("alloy", "--tts-voice", help="TTS voice (alloy, echo, fable, onyx, nova, shimmer)"),
|
||||
tts_model: Optional[str] = typer.Option(None, "--tts-model", help="TTS model (default: openai/tts-1)"),
|
||||
auto_tts: bool = typer.Option(False, "--auto-tts", help="Auto-convert all responses to speech"),
|
||||
stt: bool = typer.Option(False, "--stt", help="Enable STT tool for speech-to-text"),
|
||||
stt_model: Optional[str] = typer.Option(None, "--stt-model", help="STT model (default: openai/whisper-1)"),
|
||||
):
|
||||
"""Start a Telegram bot with full agent capabilities.
|
||||
|
||||
Examples:
|
||||
praisonai bot telegram --token $TELEGRAM_BOT_TOKEN
|
||||
praisonai bot telegram --agent agents.yaml --browser --web
|
||||
praisonai bot telegram --tools DuckDuckGoTool WikipediaTool --memory
|
||||
"""
|
||||
from ..features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
capabilities = BotCapabilities(
|
||||
browser=browser,
|
||||
browser_profile=browser_profile,
|
||||
browser_headless=browser_headless,
|
||||
tools=tools or [],
|
||||
skills=skills or [],
|
||||
skills_dir=skills_dir,
|
||||
memory=memory,
|
||||
memory_provider=memory_provider,
|
||||
knowledge=knowledge,
|
||||
knowledge_sources=knowledge_sources or [],
|
||||
web_search=web_search,
|
||||
web_search_provider=web_provider,
|
||||
sandbox=sandbox,
|
||||
exec_enabled=exec_enabled,
|
||||
auto_approve=auto_approve,
|
||||
model=model,
|
||||
thinking=thinking,
|
||||
tts=tts,
|
||||
tts_voice=tts_voice,
|
||||
tts_model=tts_model,
|
||||
auto_tts=auto_tts,
|
||||
stt=stt,
|
||||
stt_model=stt_model,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
handler = BotHandler()
|
||||
handler.start_telegram(token=token, agent_file=agent, capabilities=capabilities)
|
||||
|
||||
|
||||
@app.command("discord")
|
||||
def bot_discord(
|
||||
token: Optional[str] = typer.Option(None, "--token", "-t", help="Discord bot token", envvar="DISCORD_BOT_TOKEN"),
|
||||
agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Agent YAML configuration file"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
browser: bool = typer.Option(False, "--browser", help="Enable browser control"),
|
||||
browser_profile: str = typer.Option("default", "--browser-profile", help="Browser profile name"),
|
||||
browser_headless: bool = typer.Option(False, "--browser-headless", help="Run browser headless"),
|
||||
tools: Optional[List[str]] = typer.Option(None, "--tools", help="Tools to enable"),
|
||||
skills: Optional[List[str]] = typer.Option(None, "--skills", help="Skills to enable"),
|
||||
skills_dir: Optional[str] = typer.Option(None, "--skills-dir", help="Custom skills directory"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
memory_provider: str = typer.Option("default", "--memory-provider", help="Memory provider"),
|
||||
knowledge: bool = typer.Option(False, "--knowledge", help="Enable knowledge/RAG"),
|
||||
knowledge_sources: Optional[List[str]] = typer.Option(None, "--knowledge-sources", help="Knowledge sources"),
|
||||
web_search: bool = typer.Option(False, "--web", "--web-search", help="Enable web search"),
|
||||
web_provider: str = typer.Option("duckduckgo", "--web-provider", help="Web search provider"),
|
||||
sandbox: bool = typer.Option(False, "--sandbox", help="Enable sandbox mode"),
|
||||
exec_enabled: bool = typer.Option(False, "--exec", help="Enable exec tool"),
|
||||
auto_approve: bool = typer.Option(False, "--auto-approve", help="Auto-approve all tool executions"),
|
||||
session_id: Optional[str] = typer.Option(None, "--session-id", help="Session ID"),
|
||||
user_id: Optional[str] = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
thinking: Optional[str] = typer.Option(None, "--thinking", help="Thinking mode (off, minimal, low, medium, high)"),
|
||||
tts: bool = typer.Option(False, "--tts", help="Enable TTS tool for text-to-speech"),
|
||||
tts_voice: str = typer.Option("alloy", "--tts-voice", help="TTS voice (alloy, echo, fable, onyx, nova, shimmer)"),
|
||||
tts_model: Optional[str] = typer.Option(None, "--tts-model", help="TTS model (default: openai/tts-1)"),
|
||||
auto_tts: bool = typer.Option(False, "--auto-tts", help="Auto-convert all responses to speech"),
|
||||
stt: bool = typer.Option(False, "--stt", help="Enable STT tool for speech-to-text"),
|
||||
stt_model: Optional[str] = typer.Option(None, "--stt-model", help="STT model (default: openai/whisper-1)"),
|
||||
):
|
||||
"""Start a Discord bot with full agent capabilities.
|
||||
|
||||
Examples:
|
||||
praisonai bot discord --token $DISCORD_BOT_TOKEN
|
||||
praisonai bot discord --agent agents.yaml --browser --web
|
||||
praisonai bot discord --tools DuckDuckGoTool --memory
|
||||
"""
|
||||
from ..features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
capabilities = BotCapabilities(
|
||||
browser=browser,
|
||||
browser_profile=browser_profile,
|
||||
browser_headless=browser_headless,
|
||||
tools=tools or [],
|
||||
skills=skills or [],
|
||||
skills_dir=skills_dir,
|
||||
memory=memory,
|
||||
memory_provider=memory_provider,
|
||||
knowledge=knowledge,
|
||||
knowledge_sources=knowledge_sources or [],
|
||||
web_search=web_search,
|
||||
web_search_provider=web_provider,
|
||||
sandbox=sandbox,
|
||||
exec_enabled=exec_enabled,
|
||||
auto_approve=auto_approve,
|
||||
model=model,
|
||||
thinking=thinking,
|
||||
tts=tts,
|
||||
tts_voice=tts_voice,
|
||||
tts_model=tts_model,
|
||||
auto_tts=auto_tts,
|
||||
stt=stt,
|
||||
stt_model=stt_model,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
handler = BotHandler()
|
||||
handler.start_discord(token=token, agent_file=agent, capabilities=capabilities)
|
||||
|
||||
|
||||
@app.command("slack")
|
||||
def bot_slack(
|
||||
token: Optional[str] = typer.Option(None, "--token", "-t", help="Slack bot token", envvar="SLACK_BOT_TOKEN"),
|
||||
app_token: Optional[str] = typer.Option(None, "--app-token", help="Slack app token for Socket Mode", envvar="SLACK_APP_TOKEN"),
|
||||
agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Agent YAML configuration file"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
browser: bool = typer.Option(False, "--browser", help="Enable browser control"),
|
||||
browser_profile: str = typer.Option("default", "--browser-profile", help="Browser profile name"),
|
||||
browser_headless: bool = typer.Option(False, "--browser-headless", help="Run browser headless"),
|
||||
tools: Optional[List[str]] = typer.Option(None, "--tools", help="Tools to enable"),
|
||||
skills: Optional[List[str]] = typer.Option(None, "--skills", help="Skills to enable"),
|
||||
skills_dir: Optional[str] = typer.Option(None, "--skills-dir", help="Custom skills directory"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
memory_provider: str = typer.Option("default", "--memory-provider", help="Memory provider"),
|
||||
knowledge: bool = typer.Option(False, "--knowledge", help="Enable knowledge/RAG"),
|
||||
knowledge_sources: Optional[List[str]] = typer.Option(None, "--knowledge-sources", help="Knowledge sources"),
|
||||
web_search: bool = typer.Option(False, "--web", "--web-search", help="Enable web search"),
|
||||
web_provider: str = typer.Option("duckduckgo", "--web-provider", help="Web search provider"),
|
||||
sandbox: bool = typer.Option(False, "--sandbox", help="Enable sandbox mode"),
|
||||
exec_enabled: bool = typer.Option(False, "--exec", help="Enable exec tool"),
|
||||
auto_approve: bool = typer.Option(False, "--auto-approve", help="Auto-approve all tool executions"),
|
||||
session_id: Optional[str] = typer.Option(None, "--session-id", help="Session ID"),
|
||||
user_id: Optional[str] = typer.Option(None, "--user-id", help="User ID for memory isolation"),
|
||||
thinking: Optional[str] = typer.Option(None, "--thinking", help="Thinking mode (off, minimal, low, medium, high)"),
|
||||
tts: bool = typer.Option(False, "--tts", help="Enable TTS tool for text-to-speech"),
|
||||
tts_voice: str = typer.Option("alloy", "--tts-voice", help="TTS voice (alloy, echo, fable, onyx, nova, shimmer)"),
|
||||
tts_model: Optional[str] = typer.Option(None, "--tts-model", help="TTS model (default: openai/tts-1)"),
|
||||
auto_tts: bool = typer.Option(False, "--auto-tts", help="Auto-convert all responses to speech"),
|
||||
stt: bool = typer.Option(False, "--stt", help="Enable STT tool for speech-to-text"),
|
||||
stt_model: Optional[str] = typer.Option(None, "--stt-model", help="STT model (default: openai/whisper-1)"),
|
||||
):
|
||||
"""Start a Slack bot with full agent capabilities.
|
||||
|
||||
Examples:
|
||||
praisonai bot slack --token $SLACK_BOT_TOKEN
|
||||
praisonai bot slack --agent agents.yaml --browser --web
|
||||
praisonai bot slack --tools DuckDuckGoTool --memory --app-token $SLACK_APP_TOKEN
|
||||
"""
|
||||
from ..features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
capabilities = BotCapabilities(
|
||||
browser=browser,
|
||||
browser_profile=browser_profile,
|
||||
browser_headless=browser_headless,
|
||||
tools=tools or [],
|
||||
skills=skills or [],
|
||||
skills_dir=skills_dir,
|
||||
memory=memory,
|
||||
memory_provider=memory_provider,
|
||||
knowledge=knowledge,
|
||||
knowledge_sources=knowledge_sources or [],
|
||||
web_search=web_search,
|
||||
web_search_provider=web_provider,
|
||||
sandbox=sandbox,
|
||||
exec_enabled=exec_enabled,
|
||||
auto_approve=auto_approve,
|
||||
model=model,
|
||||
thinking=thinking,
|
||||
tts=tts,
|
||||
tts_voice=tts_voice,
|
||||
tts_model=tts_model,
|
||||
auto_tts=auto_tts,
|
||||
stt=stt,
|
||||
stt_model=stt_model,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
handler = BotHandler()
|
||||
handler.start_slack(token=token, app_token=app_token, agent_file=agent, capabilities=capabilities)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def bot_callback(ctx: typer.Context):
|
||||
"""Show bot help if no subcommand provided."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
help_text = """
|
||||
[bold cyan]PraisonAI Bot - Messaging Bots with Full Agent Capabilities[/bold cyan]
|
||||
|
||||
Start a bot on any platform with: praisonai bot <platform>
|
||||
|
||||
[bold]Platforms:[/bold]
|
||||
[green]telegram[/green] Telegram Bot API
|
||||
[green]discord[/green] Discord Bot API
|
||||
[green]slack[/green] Slack Socket Mode
|
||||
|
||||
[bold]Capability Options (all platforms):[/bold]
|
||||
[yellow]--agent FILE[/yellow] Agent YAML configuration
|
||||
[yellow]--model MODEL[/yellow] LLM model to use
|
||||
[yellow]--browser[/yellow] Enable browser control
|
||||
[yellow]--web[/yellow] Enable web search
|
||||
[yellow]--memory[/yellow] Enable memory
|
||||
[yellow]--knowledge[/yellow] Enable knowledge/RAG
|
||||
[yellow]--tools TOOL...[/yellow] Enable specific tools
|
||||
[yellow]--sandbox[/yellow] Enable sandbox mode
|
||||
[yellow]--tts[/yellow] Enable TTS tool
|
||||
[yellow]--stt[/yellow] Enable STT tool
|
||||
[yellow]--auto-tts[/yellow] Auto-convert responses to speech
|
||||
|
||||
[bold]Examples:[/bold]
|
||||
praisonai bot telegram --token $TELEGRAM_BOT_TOKEN
|
||||
praisonai bot slack --agent agents.yaml --browser --web
|
||||
praisonai bot discord --tools DuckDuckGoTool --memory --model gpt-4o
|
||||
"""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(help_text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', help_text)
|
||||
print(plain)
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Browser command group for PraisonAI CLI.
|
||||
|
||||
Provides browser control commands for agent automation.
|
||||
Inspired by moltbot's browser CLI.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(
|
||||
help="Browser control for agent automation",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def browser_status(
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check browser status.
|
||||
|
||||
Examples:
|
||||
praisonai browser status
|
||||
praisonai browser --profile chrome status
|
||||
"""
|
||||
try:
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
||||
# Check if browser tools are available
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
console.print(f"[green]✓[/green] Browser tools available")
|
||||
console.print(f" Profile: {profile}")
|
||||
console.print(f" Status: Ready")
|
||||
except ImportError:
|
||||
console.print("[yellow]![/yellow] Browser tools not installed")
|
||||
console.print(" Install with: pip install praisonai-tools")
|
||||
|
||||
except ImportError:
|
||||
print(f"Browser status: Profile={profile}")
|
||||
print("Install rich for better output: pip install rich")
|
||||
|
||||
|
||||
@app.command("open")
|
||||
def browser_open(
|
||||
url: str = typer.Argument(..., help="URL to open"),
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
headless: bool = typer.Option(False, "--headless", help="Run in headless mode"),
|
||||
):
|
||||
"""Open a URL in the browser.
|
||||
|
||||
Examples:
|
||||
praisonai browser open https://example.com
|
||||
praisonai browser open https://example.com --headless
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Opening {url} in browser (profile: {profile})...")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="navigate", url=url)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
print("Install with: pip install praisonai-tools")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("snapshot")
|
||||
def browser_snapshot(
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
||||
format: str = typer.Option("aria", "--format", "-f", help="Snapshot format (aria, ai)"),
|
||||
):
|
||||
"""Take a snapshot of the current page.
|
||||
|
||||
Examples:
|
||||
praisonai browser snapshot
|
||||
praisonai browser snapshot --output snapshot.txt
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Taking snapshot (profile: {profile})...")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="snapshot")
|
||||
|
||||
if output:
|
||||
with open(output, "w") as f:
|
||||
f.write(str(result))
|
||||
print(f"Snapshot saved to: {output}")
|
||||
else:
|
||||
print(result)
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
print("Install with: pip install praisonai-tools")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("screenshot")
|
||||
def browser_screenshot(
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
||||
full_page: bool = typer.Option(False, "--full-page", help="Capture full page"),
|
||||
format: str = typer.Option("png", "--format", "-f", help="Image format (png, jpeg)"),
|
||||
):
|
||||
"""Take a screenshot of the current page.
|
||||
|
||||
Examples:
|
||||
praisonai browser screenshot
|
||||
praisonai browser screenshot --output page.png --full-page
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Taking screenshot (profile: {profile})...")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="screenshot", full_page=full_page)
|
||||
|
||||
if output:
|
||||
# Save screenshot to file
|
||||
print(f"Screenshot saved to: {output}")
|
||||
else:
|
||||
print(f"Screenshot captured: {result}")
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
print("Install with: pip install praisonai-tools")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("click")
|
||||
def browser_click(
|
||||
selector: str = typer.Argument(..., help="Element selector or ref"),
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
double: bool = typer.Option(False, "--double", help="Double click"),
|
||||
):
|
||||
"""Click an element on the page.
|
||||
|
||||
Examples:
|
||||
praisonai browser click "#submit-button"
|
||||
praisonai browser click "ref:123" --double
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Clicking element: {selector}")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="click", selector=selector)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("type")
|
||||
def browser_type(
|
||||
selector: str = typer.Argument(..., help="Element selector or ref"),
|
||||
text: str = typer.Argument(..., help="Text to type"),
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
submit: bool = typer.Option(False, "--submit", help="Submit after typing"),
|
||||
):
|
||||
"""Type text into an element.
|
||||
|
||||
Examples:
|
||||
praisonai browser type "#search-input" "hello world"
|
||||
praisonai browser type "#search-input" "hello world" --submit
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Typing into element: {selector}")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="type", selector=selector, text=text)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("navigate")
|
||||
def browser_navigate(
|
||||
url: str = typer.Argument(..., help="URL to navigate to"),
|
||||
profile: str = typer.Option("default", "--profile", "-p", help="Browser profile name"),
|
||||
):
|
||||
"""Navigate to a URL.
|
||||
|
||||
Examples:
|
||||
praisonai browser navigate https://example.com
|
||||
"""
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
|
||||
print(f"Navigating to: {url}")
|
||||
browser = BrowserBaseTool()
|
||||
result = browser.run(action="navigate", url=url)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except ImportError:
|
||||
print("Error: Browser tools not installed")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("profiles")
|
||||
def browser_profiles(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List available browser profiles.
|
||||
|
||||
Examples:
|
||||
praisonai browser profiles
|
||||
praisonai browser profiles --json
|
||||
"""
|
||||
profiles = [
|
||||
{"name": "default", "description": "Default browser profile"},
|
||||
{"name": "chrome", "description": "Chrome browser via extension relay"},
|
||||
{"name": "headless", "description": "Headless browser for automation"},
|
||||
]
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(profiles, indent=2))
|
||||
else:
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Browser Profiles")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Description")
|
||||
|
||||
for p in profiles:
|
||||
table.add_row(p["name"], p["description"])
|
||||
|
||||
console.print(table)
|
||||
except ImportError:
|
||||
for p in profiles:
|
||||
print(f" {p['name']}: {p['description']}")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def browser_callback(ctx: typer.Context):
|
||||
"""Show browser help if no subcommand provided."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
help_text = """
|
||||
[bold cyan]PraisonAI Browser - Browser Control for Agent Automation[/bold cyan]
|
||||
|
||||
Control browsers for agent automation with: praisonai browser <command>
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
[green]status[/green] Check browser status
|
||||
[green]open[/green] Open a URL
|
||||
[green]navigate[/green] Navigate to a URL
|
||||
[green]snapshot[/green] Take a page snapshot
|
||||
[green]screenshot[/green] Take a screenshot
|
||||
[green]click[/green] Click an element
|
||||
[green]type[/green] Type text into an element
|
||||
[green]profiles[/green] List browser profiles
|
||||
|
||||
[bold]Examples:[/bold]
|
||||
praisonai browser status
|
||||
praisonai browser open https://example.com
|
||||
praisonai browser snapshot --output page.txt
|
||||
praisonai browser click "#submit-button"
|
||||
"""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(help_text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', help_text)
|
||||
print(plain)
|
||||
@@ -0,0 +1,343 @@
|
||||
"""
|
||||
Plugins command group for PraisonAI CLI.
|
||||
|
||||
Provides plugin management commands inspired by moltbot's plugins CLI.
|
||||
Supports listing, enabling, disabling, and inspecting plugins.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(
|
||||
help="Plugin management and inspection",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def plugins_list(
|
||||
enabled_only: bool = typer.Option(False, "--enabled", help="Show only enabled plugins"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List available plugins.
|
||||
|
||||
Examples:
|
||||
praisonai plugins list
|
||||
praisonai plugins list --enabled
|
||||
praisonai plugins list --json
|
||||
"""
|
||||
try:
|
||||
# Try to get plugins from the plugin system
|
||||
plugins = _get_available_plugins()
|
||||
|
||||
if enabled_only:
|
||||
plugins = [p for p in plugins if p.get("enabled", False)]
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(plugins, indent=2))
|
||||
else:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title=f"Plugins ({len(plugins)} available)")
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Name")
|
||||
table.add_column("Status")
|
||||
table.add_column("Description")
|
||||
|
||||
for plugin in plugins:
|
||||
status = "[green]enabled[/green]" if plugin.get("enabled") else "[dim]disabled[/dim]"
|
||||
table.add_row(
|
||||
plugin.get("id", "-"),
|
||||
plugin.get("name", "-"),
|
||||
status,
|
||||
plugin.get("description", "-")[:40] + "..." if len(plugin.get("description", "")) > 40 else plugin.get("description", "-"),
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error listing plugins: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("info")
|
||||
def plugins_info(
|
||||
plugin_id: str = typer.Argument(..., help="Plugin ID"),
|
||||
):
|
||||
"""Show detailed information about a plugin.
|
||||
|
||||
Examples:
|
||||
praisonai plugins info memory-core
|
||||
praisonai plugins info browser-tool
|
||||
"""
|
||||
try:
|
||||
plugins = _get_available_plugins()
|
||||
plugin = next((p for p in plugins if p.get("id") == plugin_id), None)
|
||||
|
||||
if not plugin:
|
||||
typer.echo(f"Plugin not found: {plugin_id}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
|
||||
console.print(f"\n[bold cyan]{plugin.get('name', plugin_id)}[/bold cyan]")
|
||||
console.print(f"ID: {plugin.get('id', '-')}")
|
||||
console.print(f"Status: {'[green]enabled[/green]' if plugin.get('enabled') else '[dim]disabled[/dim]'}")
|
||||
console.print(f"Description: {plugin.get('description', '-')}")
|
||||
|
||||
if plugin.get("version"):
|
||||
console.print(f"Version: {plugin.get('version')}")
|
||||
if plugin.get("author"):
|
||||
console.print(f"Author: {plugin.get('author')}")
|
||||
if plugin.get("hooks"):
|
||||
console.print(f"Hooks: {', '.join(plugin.get('hooks', []))}")
|
||||
if plugin.get("config_schema"):
|
||||
console.print("\n[bold]Config Schema:[/bold]")
|
||||
import json
|
||||
console.print(Panel(json.dumps(plugin.get("config_schema"), indent=2)))
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("enable")
|
||||
def plugins_enable(
|
||||
plugin_id: str = typer.Argument(..., help="Plugin ID to enable"),
|
||||
):
|
||||
"""Enable a plugin.
|
||||
|
||||
Examples:
|
||||
praisonai plugins enable memory-core
|
||||
praisonai plugins enable browser-tool
|
||||
"""
|
||||
try:
|
||||
# Update config to enable plugin
|
||||
config_path = _get_config_path()
|
||||
config = _load_config(config_path)
|
||||
|
||||
if "plugins" not in config:
|
||||
config["plugins"] = {}
|
||||
if "enabled" not in config["plugins"]:
|
||||
config["plugins"]["enabled"] = []
|
||||
|
||||
if plugin_id not in config["plugins"]["enabled"]:
|
||||
config["plugins"]["enabled"].append(plugin_id)
|
||||
_save_config(config_path, config)
|
||||
typer.echo(f"[green]✓[/green] Plugin enabled: {plugin_id}")
|
||||
else:
|
||||
typer.echo(f"Plugin already enabled: {plugin_id}")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error enabling plugin: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("disable")
|
||||
def plugins_disable(
|
||||
plugin_id: str = typer.Argument(..., help="Plugin ID to disable"),
|
||||
):
|
||||
"""Disable a plugin.
|
||||
|
||||
Examples:
|
||||
praisonai plugins disable memory-core
|
||||
praisonai plugins disable browser-tool
|
||||
"""
|
||||
try:
|
||||
config_path = _get_config_path()
|
||||
config = _load_config(config_path)
|
||||
|
||||
if "plugins" in config and "enabled" in config["plugins"]:
|
||||
if plugin_id in config["plugins"]["enabled"]:
|
||||
config["plugins"]["enabled"].remove(plugin_id)
|
||||
_save_config(config_path, config)
|
||||
typer.echo(f"[yellow]![/yellow] Plugin disabled: {plugin_id}")
|
||||
else:
|
||||
typer.echo(f"Plugin not enabled: {plugin_id}")
|
||||
else:
|
||||
typer.echo(f"Plugin not enabled: {plugin_id}")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error disabling plugin: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("doctor")
|
||||
def plugins_doctor():
|
||||
"""Check plugin health and configuration.
|
||||
|
||||
Verifies that all enabled plugins are properly configured and
|
||||
their dependencies are met.
|
||||
|
||||
Examples:
|
||||
praisonai plugins doctor
|
||||
"""
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
plugins = _get_available_plugins()
|
||||
enabled_plugins = [p for p in plugins if p.get("enabled")]
|
||||
|
||||
console.print("[bold]Plugin Health Check[/bold]\n")
|
||||
|
||||
if not enabled_plugins:
|
||||
console.print("[yellow]No plugins enabled[/yellow]")
|
||||
return
|
||||
|
||||
table = Table()
|
||||
table.add_column("Plugin", style="cyan")
|
||||
table.add_column("Status")
|
||||
table.add_column("Issues")
|
||||
|
||||
issues_found = 0
|
||||
|
||||
for plugin in enabled_plugins:
|
||||
issues = []
|
||||
|
||||
# Check if plugin module exists
|
||||
if plugin.get("module"):
|
||||
try:
|
||||
__import__(plugin["module"])
|
||||
except ImportError:
|
||||
issues.append("Module not found")
|
||||
|
||||
# Check required config
|
||||
if plugin.get("required_config"):
|
||||
for key in plugin["required_config"]:
|
||||
# Check if config key exists
|
||||
pass # Would check actual config
|
||||
|
||||
if issues:
|
||||
status = "[red]✗ Issues[/red]"
|
||||
issues_found += len(issues)
|
||||
else:
|
||||
status = "[green]✓ OK[/green]"
|
||||
|
||||
table.add_row(
|
||||
plugin.get("name", plugin.get("id")),
|
||||
status,
|
||||
", ".join(issues) if issues else "-",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
if issues_found:
|
||||
console.print(f"\n[red]Found {issues_found} issue(s)[/red]")
|
||||
else:
|
||||
console.print(f"\n[green]All {len(enabled_plugins)} plugins healthy[/green]")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _get_available_plugins():
|
||||
"""Get list of available plugins."""
|
||||
# Built-in plugins
|
||||
plugins = [
|
||||
{
|
||||
"id": "memory-core",
|
||||
"name": "Memory Core",
|
||||
"description": "Semantic memory indexing and search",
|
||||
"enabled": True,
|
||||
"hooks": ["on_message", "on_session_start"],
|
||||
},
|
||||
{
|
||||
"id": "browser-tool",
|
||||
"name": "Browser Tool",
|
||||
"description": "Browser automation and control",
|
||||
"enabled": False,
|
||||
"hooks": ["on_tool_call"],
|
||||
},
|
||||
{
|
||||
"id": "knowledge-rag",
|
||||
"name": "Knowledge RAG",
|
||||
"description": "Retrieval-augmented generation",
|
||||
"enabled": False,
|
||||
"hooks": ["on_message", "on_context_build"],
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
"name": "Telemetry",
|
||||
"description": "Usage tracking and analytics",
|
||||
"enabled": False,
|
||||
"hooks": ["on_request", "on_response"],
|
||||
},
|
||||
]
|
||||
|
||||
# Try to load actual plugin registry
|
||||
try:
|
||||
from praisonaiagents.plugins import get_plugin_registry
|
||||
registry = get_plugin_registry()
|
||||
if registry:
|
||||
# Merge with actual plugins
|
||||
pass
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def _get_config_path():
|
||||
"""Get config file path."""
|
||||
from pathlib import Path
|
||||
|
||||
config_dir = Path.home() / ".praisonai"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return config_dir / "config.json"
|
||||
|
||||
|
||||
def _load_config(path):
|
||||
"""Load config from file."""
|
||||
import json
|
||||
|
||||
if path.exists():
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _save_config(path, config):
|
||||
"""Save config to file."""
|
||||
import json
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def plugins_callback(ctx: typer.Context):
|
||||
"""Show plugins help if no subcommand provided."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
help_text = """
|
||||
[bold cyan]PraisonAI Plugins - Plugin Management[/bold cyan]
|
||||
|
||||
Manage plugins with: praisonai plugins <command>
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
[green]list[/green] List available plugins
|
||||
[green]info[/green] Show plugin details
|
||||
[green]enable[/green] Enable a plugin
|
||||
[green]disable[/green] Disable a plugin
|
||||
[green]doctor[/green] Check plugin health
|
||||
|
||||
[bold]Examples:[/bold]
|
||||
praisonai plugins list
|
||||
praisonai plugins info memory-core
|
||||
praisonai plugins enable browser-tool
|
||||
praisonai plugins doctor
|
||||
"""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(help_text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', help_text)
|
||||
print(plain)
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Sandbox command group for PraisonAI CLI.
|
||||
|
||||
Provides sandbox management commands inspired by moltbot's sandbox CLI.
|
||||
Supports listing, explaining, and recreating sandbox containers.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(
|
||||
help="Sandbox container management",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def sandbox_status(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show sandbox status and configuration.
|
||||
|
||||
Examples:
|
||||
praisonai sandbox status
|
||||
praisonai sandbox status --json
|
||||
"""
|
||||
try:
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
# Check sandbox availability
|
||||
sandbox_info = _get_sandbox_info()
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(sandbox_info, indent=2))
|
||||
else:
|
||||
console.print("[bold cyan]Sandbox Status[/bold cyan]\n")
|
||||
|
||||
status = "[green]Available[/green]" if sandbox_info["available"] else "[red]Not Available[/red]"
|
||||
console.print(f"Status: {status}")
|
||||
console.print(f"Mode: {sandbox_info.get('mode', 'off')}")
|
||||
console.print(f"Scope: {sandbox_info.get('scope', 'session')}")
|
||||
|
||||
if sandbox_info.get("docker_available"):
|
||||
console.print("Docker: [green]Available[/green]")
|
||||
console.print(f"Image: {sandbox_info.get('image', 'default')}")
|
||||
else:
|
||||
console.print("Docker: [yellow]Not Available[/yellow]")
|
||||
|
||||
if sandbox_info.get("containers"):
|
||||
console.print(f"\n[bold]Active Containers:[/bold] {len(sandbox_info['containers'])}")
|
||||
for container in sandbox_info["containers"]:
|
||||
console.print(f" - {container['name']} ({container['status']})")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("explain")
|
||||
def sandbox_explain(
|
||||
session: Optional[str] = typer.Option(None, "--session", "-s", help="Session key"),
|
||||
agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Agent ID"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Explain effective sandbox policy and tool access.
|
||||
|
||||
Shows the effective sandbox mode, scope, workspace access,
|
||||
and tool policy for a session or agent.
|
||||
|
||||
Examples:
|
||||
praisonai sandbox explain
|
||||
praisonai sandbox explain --session main
|
||||
praisonai sandbox explain --agent work
|
||||
"""
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
policy = _get_sandbox_policy(session, agent)
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(policy, indent=2))
|
||||
else:
|
||||
console.print("[bold cyan]Sandbox Policy[/bold cyan]\n")
|
||||
|
||||
if session:
|
||||
console.print(f"Session: {session}")
|
||||
if agent:
|
||||
console.print(f"Agent: {agent}")
|
||||
|
||||
console.print("\n[bold]Effective Settings:[/bold]")
|
||||
console.print(f" Mode: {policy.get('mode', 'off')}")
|
||||
console.print(f" Scope: {policy.get('scope', 'session')}")
|
||||
console.print(f" Workspace Access: {policy.get('workspace_access', 'full')}")
|
||||
|
||||
console.print("\n[bold]Tool Policy:[/bold]")
|
||||
table = Table()
|
||||
table.add_column("Tool", style="cyan")
|
||||
table.add_column("Access")
|
||||
table.add_column("Notes")
|
||||
|
||||
for tool, access in policy.get("tools", {}).items():
|
||||
status = "[green]allowed[/green]" if access.get("allowed") else "[red]denied[/red]"
|
||||
table.add_row(tool, status, access.get("notes", "-"))
|
||||
|
||||
console.print(table)
|
||||
|
||||
if policy.get("elevated_gates"):
|
||||
console.print("\n[bold]Elevated Gates:[/bold]")
|
||||
for gate in policy["elevated_gates"]:
|
||||
console.print(f" - {gate}")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def sandbox_list(
|
||||
browser: bool = typer.Option(False, "--browser", help="List only browser containers"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List sandbox containers.
|
||||
|
||||
Examples:
|
||||
praisonai sandbox list
|
||||
praisonai sandbox list --browser
|
||||
praisonai sandbox list --json
|
||||
"""
|
||||
try:
|
||||
containers = _get_sandbox_containers(browser_only=browser)
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(containers, indent=2))
|
||||
else:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
title = "Browser Containers" if browser else "Sandbox Containers"
|
||||
table = Table(title=f"{title} ({len(containers)} found)")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Status")
|
||||
table.add_column("Image")
|
||||
table.add_column("Age")
|
||||
table.add_column("Session/Agent")
|
||||
|
||||
for container in containers:
|
||||
status = "[green]running[/green]" if container.get("running") else "[dim]stopped[/dim]"
|
||||
table.add_row(
|
||||
container.get("name", "-"),
|
||||
status,
|
||||
container.get("image", "-"),
|
||||
container.get("age", "-"),
|
||||
container.get("session", "-"),
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
if not containers:
|
||||
console.print("[dim]No containers found[/dim]")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("recreate")
|
||||
def sandbox_recreate(
|
||||
all_containers: bool = typer.Option(False, "--all", help="Recreate all containers"),
|
||||
session: Optional[str] = typer.Option(None, "--session", "-s", help="Recreate for specific session"),
|
||||
agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Recreate for specific agent"),
|
||||
browser: bool = typer.Option(False, "--browser", help="Only recreate browser containers"),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
||||
):
|
||||
"""Recreate sandbox containers.
|
||||
|
||||
Removes sandbox containers to force recreation with updated
|
||||
images or configuration. Containers are automatically recreated
|
||||
when the agent is next used.
|
||||
|
||||
Examples:
|
||||
praisonai sandbox recreate --all
|
||||
praisonai sandbox recreate --session main
|
||||
praisonai sandbox recreate --agent mybot
|
||||
praisonai sandbox recreate --browser --force
|
||||
"""
|
||||
if not any([all_containers, session, agent]):
|
||||
typer.echo("Error: Specify --all, --session, or --agent", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
containers = _get_sandbox_containers(browser_only=browser)
|
||||
|
||||
# Filter containers
|
||||
if session:
|
||||
containers = [c for c in containers if c.get("session") == session]
|
||||
if agent:
|
||||
containers = [c for c in containers if c.get("agent") == agent]
|
||||
|
||||
if not containers:
|
||||
typer.echo("No matching containers found")
|
||||
return
|
||||
|
||||
# Confirm
|
||||
if not force:
|
||||
typer.echo(f"Will recreate {len(containers)} container(s):")
|
||||
for c in containers:
|
||||
typer.echo(f" - {c.get('name')}")
|
||||
|
||||
if not typer.confirm("Continue?"):
|
||||
typer.echo("Cancelled")
|
||||
return
|
||||
|
||||
# Remove containers
|
||||
removed = 0
|
||||
for container in containers:
|
||||
try:
|
||||
_remove_container(container.get("name"))
|
||||
removed += 1
|
||||
typer.echo(f"[green]✓[/green] Removed: {container.get('name')}")
|
||||
except Exception as e:
|
||||
typer.echo(f"[red]✗[/red] Failed to remove {container.get('name')}: {e}")
|
||||
|
||||
typer.echo(f"\nRemoved {removed}/{len(containers)} containers")
|
||||
typer.echo("Containers will be recreated on next use")
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _get_sandbox_info():
|
||||
"""Get sandbox status information."""
|
||||
import shutil
|
||||
|
||||
info = {
|
||||
"available": False,
|
||||
"mode": "off",
|
||||
"scope": "session",
|
||||
"docker_available": shutil.which("docker") is not None,
|
||||
"image": "praisonai-sandbox:latest",
|
||||
"containers": [],
|
||||
}
|
||||
|
||||
# Check if Docker is available
|
||||
if info["docker_available"]:
|
||||
info["available"] = True
|
||||
|
||||
# Try to list containers
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "--filter", "name=praisonai-sbx-", "--format", "{{.Names}}\t{{.Status}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
info["containers"].append({
|
||||
"name": parts[0],
|
||||
"status": parts[1],
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def _get_sandbox_policy(session: Optional[str], agent: Optional[str]):
|
||||
"""Get effective sandbox policy."""
|
||||
return {
|
||||
"mode": "off",
|
||||
"scope": "session",
|
||||
"workspace_access": "full",
|
||||
"tools": {
|
||||
"exec": {"allowed": True, "notes": "Host execution"},
|
||||
"browser": {"allowed": True, "notes": "Browser automation"},
|
||||
"file_read": {"allowed": True, "notes": "Read workspace files"},
|
||||
"file_write": {"allowed": True, "notes": "Write workspace files"},
|
||||
},
|
||||
"elevated_gates": [],
|
||||
}
|
||||
|
||||
|
||||
def _get_sandbox_containers(browser_only: bool = False):
|
||||
"""Get list of sandbox containers."""
|
||||
containers = []
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
filter_name = "praisonai-browser-" if browser_only else "praisonai-sbx-"
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "-a", "--filter", f"name={filter_name}", "--format", "{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.CreatedAt}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 4:
|
||||
containers.append({
|
||||
"name": parts[0],
|
||||
"running": "Up" in parts[1],
|
||||
"status": parts[1],
|
||||
"image": parts[2],
|
||||
"age": parts[3],
|
||||
"session": parts[0].split("-")[-1] if "-" in parts[0] else "-",
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return containers
|
||||
|
||||
|
||||
def _remove_container(name: str):
|
||||
"""Remove a Docker container."""
|
||||
import subprocess
|
||||
|
||||
# Stop if running
|
||||
subprocess.run(["docker", "stop", name], capture_output=True, timeout=10)
|
||||
# Remove
|
||||
result = subprocess.run(["docker", "rm", name], capture_output=True, timeout=10)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise Exception(result.stderr.decode() if result.stderr else "Unknown error")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def sandbox_callback(ctx: typer.Context):
|
||||
"""Show sandbox help if no subcommand provided."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
help_text = """
|
||||
[bold cyan]PraisonAI Sandbox - Container Management[/bold cyan]
|
||||
|
||||
Manage sandbox containers with: praisonai sandbox <command>
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
[green]status[/green] Show sandbox status
|
||||
[green]explain[/green] Explain effective sandbox policy
|
||||
[green]list[/green] List sandbox containers
|
||||
[green]recreate[/green] Recreate containers
|
||||
|
||||
[bold]Examples:[/bold]
|
||||
praisonai sandbox status
|
||||
praisonai sandbox explain --agent work
|
||||
praisonai sandbox list --browser
|
||||
praisonai sandbox recreate --all --force
|
||||
"""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(help_text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', help_text)
|
||||
print(plain)
|
||||
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
Skills command group for PraisonAI CLI.
|
||||
|
||||
Provides skill management commands.
|
||||
Provides skill management commands inspired by moltbot's skills CLI.
|
||||
Supports listing, checking eligibility, and managing skills.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Skill management")
|
||||
app = typer.Typer(help="Skill management and inspection")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
@@ -215,3 +216,155 @@ def skills_info(
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("check")
|
||||
def skills_check(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed check results"),
|
||||
):
|
||||
"""Check skill requirements and eligibility.
|
||||
|
||||
Verifies that all required binaries, environment variables, and
|
||||
configurations are present for skills to be eligible.
|
||||
|
||||
Examples:
|
||||
praisonai skills check
|
||||
praisonai skills check --verbose
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents.skills import discover_skills
|
||||
import os
|
||||
import shutil
|
||||
|
||||
skills = discover_skills(include_defaults=True)
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Skill Requirements Check")
|
||||
table.add_column("Skill", style="cyan")
|
||||
table.add_column("Status")
|
||||
table.add_column("Missing", style="yellow")
|
||||
|
||||
total = 0
|
||||
eligible = 0
|
||||
|
||||
for skill in skills:
|
||||
total += 1
|
||||
missing = []
|
||||
|
||||
# Check metadata requirements if available
|
||||
metadata = getattr(skill, 'metadata', {}) or {}
|
||||
requires = metadata.get('requires', {}) if isinstance(metadata, dict) else {}
|
||||
|
||||
# Check required binaries
|
||||
bins = requires.get('bins', []) if isinstance(requires, dict) else []
|
||||
for bin_name in bins:
|
||||
if not shutil.which(bin_name):
|
||||
missing.append(f"bin:{bin_name}")
|
||||
|
||||
# Check required env vars
|
||||
env_vars = requires.get('env', []) if isinstance(requires, dict) else []
|
||||
for env_var in env_vars:
|
||||
if not os.environ.get(env_var):
|
||||
missing.append(f"env:{env_var}")
|
||||
|
||||
if missing:
|
||||
status = "[red]✗ Missing requirements[/red]"
|
||||
else:
|
||||
status = "[green]✓ Eligible[/green]"
|
||||
eligible += 1
|
||||
|
||||
if verbose or missing:
|
||||
table.add_row(
|
||||
skill.name,
|
||||
status,
|
||||
", ".join(missing) if missing else "-",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[bold]Summary:[/bold] {eligible}/{total} skills eligible")
|
||||
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("eligible")
|
||||
def skills_eligible(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List only eligible skills (requirements met).
|
||||
|
||||
Shows skills that have all their requirements satisfied and are
|
||||
ready to use.
|
||||
|
||||
Examples:
|
||||
praisonai skills eligible
|
||||
praisonai skills eligible --json
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents.skills import discover_skills
|
||||
import os
|
||||
import shutil
|
||||
|
||||
skills = discover_skills(include_defaults=True)
|
||||
eligible_skills = []
|
||||
|
||||
for skill in skills:
|
||||
# Check metadata requirements if available
|
||||
metadata = getattr(skill, 'metadata', {}) or {}
|
||||
requires = metadata.get('requires', {}) if isinstance(metadata, dict) else {}
|
||||
|
||||
is_eligible = True
|
||||
|
||||
# Check required binaries
|
||||
bins = requires.get('bins', []) if isinstance(requires, dict) else []
|
||||
for bin_name in bins:
|
||||
if not shutil.which(bin_name):
|
||||
is_eligible = False
|
||||
break
|
||||
|
||||
# Check required env vars
|
||||
if is_eligible:
|
||||
env_vars = requires.get('env', []) if isinstance(requires, dict) else []
|
||||
for env_var in env_vars:
|
||||
if not os.environ.get(env_var):
|
||||
is_eligible = False
|
||||
break
|
||||
|
||||
if is_eligible:
|
||||
eligible_skills.append(skill)
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
output = [
|
||||
{
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"path": str(s.path) if s.path else None,
|
||||
}
|
||||
for s in eligible_skills
|
||||
]
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title=f"Eligible Skills ({len(eligible_skills)} ready)")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Description")
|
||||
|
||||
for skill in eligible_skills:
|
||||
table.add_row(
|
||||
skill.name,
|
||||
skill.description[:60] + "..." if len(skill.description) > 60 else skill.description,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except ImportError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -160,17 +160,25 @@ def _launch_chainlit_ui(ui_type: str, port: int, host: str, public: bool):
|
||||
ui_script = ui_scripts.get(ui_type, "chainlit_ui.py")
|
||||
ui_path = os.path.join(os.path.dirname(praisonai.__file__), ui_script)
|
||||
|
||||
# Get chainlit run function
|
||||
from chainlit.cli import run as chainlit_run
|
||||
|
||||
# Build args
|
||||
args = [ui_path]
|
||||
if host:
|
||||
args.extend(["--host", host])
|
||||
args.extend(["--port", str(port)])
|
||||
|
||||
print(f"Starting {ui_type} UI at http://{host}:{port}")
|
||||
chainlit_run(args)
|
||||
|
||||
# Use subprocess to run chainlit with proper CLI args
|
||||
import subprocess
|
||||
|
||||
cmd = ["chainlit", "run", ui_path, "--host", host, "--port", str(port)]
|
||||
if public:
|
||||
cmd.append("--public")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except FileNotFoundError:
|
||||
# Fallback: try running with python -m chainlit
|
||||
cmd = [sys.executable, "-m", "chainlit", "run", ui_path, "--host", host, "--port", str(port)]
|
||||
if public:
|
||||
cmd.append("--public")
|
||||
subprocess.run(cmd, check=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\nUI stopped.")
|
||||
|
||||
|
||||
def _launch_gradio_ui(port: int, host: str, public: bool):
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
"""
|
||||
Bot CLI commands for PraisonAI.
|
||||
|
||||
Provides CLI commands for managing messaging bots.
|
||||
Provides CLI commands for managing messaging bots with full tool support.
|
||||
Inspired by moltbot's comprehensive CLI options.
|
||||
|
||||
Features:
|
||||
- Browser control integration
|
||||
- Skills/tools enabling
|
||||
- YAML agent configuration
|
||||
- Memory support
|
||||
- Knowledge/RAG integration
|
||||
- Web search capability
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -9,24 +18,112 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BotCapabilities:
|
||||
"""Capabilities configuration for bot instances.
|
||||
|
||||
Enables various agent capabilities when running bots.
|
||||
All capabilities are opt-in for zero performance impact.
|
||||
"""
|
||||
|
||||
# Browser control
|
||||
browser: bool = False
|
||||
browser_profile: str = "default"
|
||||
browser_headless: bool = False
|
||||
|
||||
# Tools and skills
|
||||
tools: List[str] = field(default_factory=list)
|
||||
skills: List[str] = field(default_factory=list)
|
||||
skills_dir: Optional[str] = None
|
||||
|
||||
# Memory
|
||||
memory: bool = False
|
||||
memory_provider: str = "default"
|
||||
|
||||
# Knowledge/RAG
|
||||
knowledge: bool = False
|
||||
knowledge_sources: List[str] = field(default_factory=list)
|
||||
|
||||
# Web search
|
||||
web_search: bool = False
|
||||
web_search_provider: str = "duckduckgo"
|
||||
|
||||
# Execution
|
||||
sandbox: bool = False
|
||||
exec_enabled: bool = False
|
||||
auto_approve: bool = False # Auto-approve CLI tool execution
|
||||
|
||||
# Model settings
|
||||
model: Optional[str] = None
|
||||
thinking: Optional[str] = None # off, minimal, low, medium, high
|
||||
|
||||
# Audio (TTS/STT)
|
||||
tts: bool = False # Enable TTS tool
|
||||
tts_voice: str = "alloy" # TTS voice
|
||||
tts_model: Optional[str] = None # TTS model (default: openai/tts-1)
|
||||
auto_tts: bool = False # Auto-convert responses to speech
|
||||
stt: bool = False # Enable STT tool
|
||||
stt_model: Optional[str] = None # STT model (default: openai/whisper-1)
|
||||
|
||||
# Session
|
||||
session_id: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"browser": self.browser,
|
||||
"browser_profile": self.browser_profile,
|
||||
"browser_headless": self.browser_headless,
|
||||
"tools": self.tools,
|
||||
"skills": self.skills,
|
||||
"skills_dir": self.skills_dir,
|
||||
"memory": self.memory,
|
||||
"memory_provider": self.memory_provider,
|
||||
"knowledge": self.knowledge,
|
||||
"knowledge_sources": self.knowledge_sources,
|
||||
"web_search": self.web_search,
|
||||
"web_search_provider": self.web_search_provider,
|
||||
"sandbox": self.sandbox,
|
||||
"exec_enabled": self.exec_enabled,
|
||||
"auto_approve": self.auto_approve,
|
||||
"model": self.model,
|
||||
"thinking": self.thinking,
|
||||
"tts": self.tts,
|
||||
"tts_voice": self.tts_voice,
|
||||
"tts_model": self.tts_model,
|
||||
"auto_tts": self.auto_tts,
|
||||
"stt": self.stt,
|
||||
"stt_model": self.stt_model,
|
||||
"session_id": self.session_id,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
|
||||
|
||||
class BotHandler:
|
||||
"""Handler for bot CLI commands."""
|
||||
"""Handler for bot CLI commands.
|
||||
|
||||
Supports comprehensive tool/capability configuration inspired by moltbot.
|
||||
"""
|
||||
|
||||
def start_telegram(
|
||||
self,
|
||||
token: Optional[str] = None,
|
||||
agent_file: Optional[str] = None,
|
||||
capabilities: Optional[BotCapabilities] = 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
|
||||
capabilities: Optional capabilities configuration
|
||||
"""
|
||||
token = token or os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||
if not token:
|
||||
@@ -34,6 +131,11 @@ class BotHandler:
|
||||
print("Provide via --token or TELEGRAM_BOT_TOKEN environment variable")
|
||||
return
|
||||
|
||||
# Set auto-approve if enabled - use environment variable (persists across async contexts)
|
||||
if capabilities and capabilities.auto_approve:
|
||||
os.environ["PRAISONAI_AUTO_APPROVE"] = "true"
|
||||
logger.info("Auto-approve enabled for all tool executions")
|
||||
|
||||
try:
|
||||
from praisonai.bots import TelegramBot
|
||||
except ImportError as e:
|
||||
@@ -41,11 +143,10 @@ class BotHandler:
|
||||
print("Install with: pip install python-telegram-bot")
|
||||
return
|
||||
|
||||
agent = self._load_agent(agent_file)
|
||||
agent = self._load_agent(agent_file, capabilities)
|
||||
bot = TelegramBot(token=token, agent=agent)
|
||||
|
||||
print("Starting Telegram bot...")
|
||||
print("Press Ctrl+C to stop")
|
||||
self._print_startup_info("Telegram", capabilities)
|
||||
|
||||
try:
|
||||
asyncio.run(bot.start())
|
||||
@@ -57,12 +158,14 @@ class BotHandler:
|
||||
self,
|
||||
token: Optional[str] = None,
|
||||
agent_file: Optional[str] = None,
|
||||
capabilities: Optional[BotCapabilities] = 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
|
||||
capabilities: Optional capabilities configuration
|
||||
"""
|
||||
token = token or os.environ.get("DISCORD_BOT_TOKEN")
|
||||
if not token:
|
||||
@@ -70,6 +173,11 @@ class BotHandler:
|
||||
print("Provide via --token or DISCORD_BOT_TOKEN environment variable")
|
||||
return
|
||||
|
||||
# Set auto-approve if enabled - use environment variable (persists across async contexts)
|
||||
if capabilities and capabilities.auto_approve:
|
||||
os.environ["PRAISONAI_AUTO_APPROVE"] = "true"
|
||||
logger.info("Auto-approve enabled for all tool executions")
|
||||
|
||||
try:
|
||||
from praisonai.bots import DiscordBot
|
||||
except ImportError as e:
|
||||
@@ -77,11 +185,10 @@ class BotHandler:
|
||||
print("Install with: pip install discord.py")
|
||||
return
|
||||
|
||||
agent = self._load_agent(agent_file)
|
||||
agent = self._load_agent(agent_file, capabilities)
|
||||
bot = DiscordBot(token=token, agent=agent)
|
||||
|
||||
print("Starting Discord bot...")
|
||||
print("Press Ctrl+C to stop")
|
||||
self._print_startup_info("Discord", capabilities)
|
||||
|
||||
try:
|
||||
asyncio.run(bot.start())
|
||||
@@ -94,6 +201,7 @@ class BotHandler:
|
||||
token: Optional[str] = None,
|
||||
app_token: Optional[str] = None,
|
||||
agent_file: Optional[str] = None,
|
||||
capabilities: Optional[BotCapabilities] = None,
|
||||
) -> None:
|
||||
"""Start a Slack bot.
|
||||
|
||||
@@ -101,6 +209,7 @@ class BotHandler:
|
||||
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
|
||||
capabilities: Optional capabilities configuration
|
||||
"""
|
||||
token = token or os.environ.get("SLACK_BOT_TOKEN")
|
||||
app_token = app_token or os.environ.get("SLACK_APP_TOKEN")
|
||||
@@ -110,6 +219,11 @@ class BotHandler:
|
||||
print("Provide via --token or SLACK_BOT_TOKEN environment variable")
|
||||
return
|
||||
|
||||
# Set auto-approve if enabled - use environment variable (persists across async contexts)
|
||||
if capabilities and capabilities.auto_approve:
|
||||
os.environ["PRAISONAI_AUTO_APPROVE"] = "true"
|
||||
logger.info("Auto-approve enabled for all tool executions")
|
||||
|
||||
try:
|
||||
from praisonai.bots import SlackBot
|
||||
except ImportError as e:
|
||||
@@ -117,11 +231,10 @@ class BotHandler:
|
||||
print("Install with: pip install slack-bolt")
|
||||
return
|
||||
|
||||
agent = self._load_agent(agent_file)
|
||||
agent = self._load_agent(agent_file, capabilities)
|
||||
bot = SlackBot(token=token, app_token=app_token, agent=agent)
|
||||
|
||||
print("Starting Slack bot...")
|
||||
print("Press Ctrl+C to stop")
|
||||
self._print_startup_info("Slack", capabilities)
|
||||
|
||||
try:
|
||||
asyncio.run(bot.start())
|
||||
@@ -129,43 +242,418 @@ class BotHandler:
|
||||
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.")
|
||||
def _get_agent_kwargs(self, capabilities: Optional[BotCapabilities]) -> Dict[str, Any]:
|
||||
"""Extract Agent constructor kwargs from BotCapabilities (DRY).
|
||||
|
||||
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.")
|
||||
Follows progressive disclosure pattern from agents.md:
|
||||
- bool True enables defaults
|
||||
- List/Config provides custom configuration
|
||||
"""
|
||||
if not capabilities:
|
||||
return {}
|
||||
|
||||
kwargs: Dict[str, Any] = {}
|
||||
|
||||
# LLM model
|
||||
if capabilities.model:
|
||||
kwargs["llm"] = capabilities.model
|
||||
|
||||
# Memory: True enables defaults (agents.md pattern)
|
||||
if capabilities.memory:
|
||||
kwargs["memory"] = True
|
||||
|
||||
# Knowledge: List of sources or True
|
||||
if capabilities.knowledge and capabilities.knowledge_sources:
|
||||
kwargs["knowledge"] = capabilities.knowledge_sources
|
||||
elif capabilities.knowledge:
|
||||
kwargs["knowledge"] = True
|
||||
|
||||
# Skills: List of skill names
|
||||
if capabilities.skills:
|
||||
kwargs["skills"] = capabilities.skills
|
||||
|
||||
# Thinking mode → reflection (maps off/minimal/low/medium/high)
|
||||
if capabilities.thinking:
|
||||
# Map thinking mode to reflection config
|
||||
# medium/high enables extended thinking, off/minimal/low disables
|
||||
if capabilities.thinking in ("medium", "high"):
|
||||
kwargs["reflection"] = True
|
||||
|
||||
return kwargs
|
||||
|
||||
def _print_startup_info(self, platform: str, capabilities: Optional[BotCapabilities]) -> None:
|
||||
"""Print startup information with enabled capabilities."""
|
||||
print(f"Starting {platform} bot...")
|
||||
|
||||
if capabilities:
|
||||
enabled = []
|
||||
if capabilities.browser:
|
||||
enabled.append(f"browser ({capabilities.browser_profile})")
|
||||
if capabilities.tools:
|
||||
enabled.append(f"tools ({len(capabilities.tools)})")
|
||||
if capabilities.skills:
|
||||
enabled.append(f"skills ({len(capabilities.skills)})")
|
||||
if capabilities.memory:
|
||||
enabled.append("memory")
|
||||
if capabilities.knowledge:
|
||||
enabled.append("knowledge")
|
||||
if capabilities.web_search:
|
||||
enabled.append(f"web ({capabilities.web_search_provider})")
|
||||
if capabilities.sandbox:
|
||||
enabled.append("sandbox")
|
||||
if capabilities.exec_enabled:
|
||||
enabled.append("exec")
|
||||
|
||||
if enabled:
|
||||
print(f"Capabilities: {', '.join(enabled)}")
|
||||
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
def _load_agent(
|
||||
self,
|
||||
file_path: Optional[str],
|
||||
capabilities: Optional[BotCapabilities] = None,
|
||||
):
|
||||
"""Load agent from configuration file with capabilities.
|
||||
|
||||
Args:
|
||||
file_path: Path to agent YAML configuration
|
||||
capabilities: Bot capabilities to apply
|
||||
|
||||
Returns:
|
||||
Configured Agent instance
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Build tools list from capabilities
|
||||
tools = self._build_tools(capabilities) if capabilities else []
|
||||
|
||||
# Get agent kwargs from capabilities (DRY)
|
||||
agent_kwargs = self._get_agent_kwargs(capabilities)
|
||||
|
||||
# Default agent if no file
|
||||
if not file_path:
|
||||
return Agent(
|
||||
name="assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
tools=tools if tools else None,
|
||||
**agent_kwargs,
|
||||
)
|
||||
|
||||
# Resolve file path - try current dir first, then check if it's absolute
|
||||
resolved_path = file_path
|
||||
if not os.path.isabs(file_path):
|
||||
# Try relative to current working directory
|
||||
cwd_path = os.path.join(os.getcwd(), file_path)
|
||||
if os.path.exists(cwd_path):
|
||||
resolved_path = cwd_path
|
||||
|
||||
if not os.path.exists(resolved_path):
|
||||
print(f"Warning: Agent file not found: {file_path} (resolved: {resolved_path})")
|
||||
return Agent(
|
||||
name="assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
tools=tools if tools else None,
|
||||
**agent_kwargs,
|
||||
)
|
||||
|
||||
try:
|
||||
import yaml
|
||||
with open(file_path, "r") as f:
|
||||
with open(resolved_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Support both list-style and dict-style YAML agents
|
||||
agent_config = None
|
||||
if "agents" in config and config["agents"]:
|
||||
agent_config = config["agents"][0]
|
||||
else:
|
||||
agents = config["agents"]
|
||||
if isinstance(agents, list):
|
||||
# List style: agents: - name: foo
|
||||
agent_config = agents[0]
|
||||
elif isinstance(agents, dict):
|
||||
# Dict style: agents: searcher: name: foo
|
||||
first_key = next(iter(agents))
|
||||
agent_config = agents[first_key]
|
||||
|
||||
if not agent_config:
|
||||
agent_config = config
|
||||
|
||||
# Merge tools from YAML and capabilities
|
||||
yaml_tools = agent_config.get("tools", [])
|
||||
all_tools = self._resolve_tools(yaml_tools) + tools
|
||||
|
||||
# Override llm from YAML if not provided in capabilities
|
||||
if "llm" not in agent_kwargs and agent_config.get("llm"):
|
||||
agent_kwargs["llm"] = agent_config.get("llm")
|
||||
|
||||
return Agent(
|
||||
name=agent_config.get("name", "assistant"),
|
||||
instructions=agent_config.get("instructions", "You are a helpful assistant."),
|
||||
llm=agent_config.get("llm"),
|
||||
tools=all_tools if all_tools else None,
|
||||
**agent_kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"Error loading agent: {e}")
|
||||
from praisonaiagents import Agent
|
||||
return Agent(name="assistant", instructions="You are a helpful assistant.")
|
||||
traceback.print_exc()
|
||||
return Agent(
|
||||
name="assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
tools=tools if tools else None,
|
||||
**agent_kwargs,
|
||||
)
|
||||
|
||||
def _build_tools(self, capabilities: BotCapabilities) -> List:
|
||||
"""Build tools list from capabilities."""
|
||||
import os
|
||||
tools = []
|
||||
|
||||
# Set WEB_SEARCH_PROVIDER env var to respect --web-provider CLI flag
|
||||
# This must be done BEFORE importing search_web
|
||||
if capabilities.web_search_provider:
|
||||
os.environ["WEB_SEARCH_PROVIDER"] = capabilities.web_search_provider
|
||||
logger.info(f"Web search provider set to: {capabilities.web_search_provider}")
|
||||
|
||||
# Default tools: execute_command and search_web (always enabled)
|
||||
try:
|
||||
from praisonaiagents.tools import execute_command, search_web
|
||||
tools.append(execute_command)
|
||||
tools.append(search_web)
|
||||
logger.info("Default tools enabled: execute_command, search_web")
|
||||
except ImportError:
|
||||
logger.warning("Default tools not available from praisonaiagents.tools")
|
||||
|
||||
# Browser tool
|
||||
if capabilities.browser:
|
||||
try:
|
||||
from praisonai_tools import BrowserBaseTool
|
||||
tools.append(BrowserBaseTool())
|
||||
logger.info("Browser tool enabled")
|
||||
except ImportError:
|
||||
logger.warning("Browser tool not available. Install praisonai-tools.")
|
||||
|
||||
# Web search (additional provider-specific tool)
|
||||
if capabilities.web_search:
|
||||
try:
|
||||
if capabilities.web_search_provider == "duckduckgo":
|
||||
from praisonai_tools import DuckDuckGoTool
|
||||
tools.append(DuckDuckGoTool())
|
||||
elif capabilities.web_search_provider == "tavily":
|
||||
from praisonai_tools import TavilyTool
|
||||
tools.append(TavilyTool())
|
||||
elif capabilities.web_search_provider == "serper":
|
||||
from praisonai_tools import SerperTool
|
||||
tools.append(SerperTool())
|
||||
else:
|
||||
from praisonai_tools import DuckDuckGoTool
|
||||
tools.append(DuckDuckGoTool())
|
||||
logger.info(f"Web search provider enabled: {capabilities.web_search_provider}")
|
||||
except ImportError:
|
||||
logger.warning("Web search tool not available. Install praisonai-tools.")
|
||||
|
||||
# Exec tool (code execution) - already included in default via execute_command
|
||||
if capabilities.exec_enabled:
|
||||
logger.info("Exec mode enabled (execute_command already included)")
|
||||
|
||||
# TTS tool
|
||||
if capabilities.tts:
|
||||
try:
|
||||
from praisonai.tools.audio import create_tts_tool
|
||||
tts_tool = create_tts_tool()
|
||||
tools.append(tts_tool)
|
||||
logger.info("TTS tool enabled")
|
||||
except ImportError as e:
|
||||
logger.warning(f"TTS tool not available: {e}")
|
||||
|
||||
# STT tool
|
||||
if capabilities.stt:
|
||||
try:
|
||||
from praisonai.tools.audio import create_stt_tool
|
||||
stt_tool = create_stt_tool()
|
||||
tools.append(stt_tool)
|
||||
logger.info("STT tool enabled")
|
||||
except ImportError as e:
|
||||
logger.warning(f"STT tool not available: {e}")
|
||||
|
||||
# Named tools
|
||||
for tool_name in capabilities.tools:
|
||||
tool = self._resolve_tool_by_name(tool_name)
|
||||
if tool:
|
||||
tools.append(tool)
|
||||
|
||||
return tools
|
||||
|
||||
def _resolve_tool_by_name(self, name: str):
|
||||
"""Resolve a tool by name."""
|
||||
try:
|
||||
import praisonai_tools
|
||||
tool_class = getattr(praisonai_tools, name, None)
|
||||
if tool_class:
|
||||
return tool_class()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from praisonaiagents import tools as agent_tools
|
||||
tool_func = getattr(agent_tools, name, None)
|
||||
if tool_func:
|
||||
return tool_func
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger.warning(f"Tool not found: {name}")
|
||||
return None
|
||||
|
||||
def _resolve_tools(self, tool_names: List[str]) -> List:
|
||||
"""Resolve a list of tool names to tool instances."""
|
||||
tools = []
|
||||
for name in tool_names:
|
||||
tool = self._resolve_tool_by_name(name)
|
||||
if tool:
|
||||
tools.append(tool)
|
||||
return tools
|
||||
|
||||
|
||||
def handle_bot_command(args) -> None:
|
||||
"""Handle bot CLI command."""
|
||||
def _add_capability_args(parser) -> None:
|
||||
"""Add capability arguments to a parser."""
|
||||
# Agent configuration
|
||||
parser.add_argument("--agent", help="Path to agent YAML configuration file")
|
||||
parser.add_argument("--model", "-m", help="LLM model to use")
|
||||
|
||||
# Browser
|
||||
parser.add_argument("--browser", action="store_true", help="Enable browser control")
|
||||
parser.add_argument("--browser-profile", default="default", help="Browser profile name")
|
||||
parser.add_argument("--browser-headless", action="store_true", help="Run browser in headless mode")
|
||||
|
||||
# Tools
|
||||
parser.add_argument("--tools", "-t", nargs="*", default=[], help="Tools to enable (e.g., DuckDuckGoTool)")
|
||||
parser.add_argument("--skills", nargs="*", default=[], help="Skills to enable")
|
||||
parser.add_argument("--skills-dir", help="Custom skills directory")
|
||||
|
||||
# Memory
|
||||
parser.add_argument("--memory", action="store_true", help="Enable memory")
|
||||
parser.add_argument("--memory-provider", default="default", help="Memory provider")
|
||||
|
||||
# Knowledge/RAG
|
||||
parser.add_argument("--knowledge", action="store_true", help="Enable knowledge/RAG")
|
||||
parser.add_argument("--knowledge-sources", nargs="*", default=[], help="Knowledge sources")
|
||||
|
||||
# Web search
|
||||
parser.add_argument("--web", "--web-search", dest="web_search", action="store_true", help="Enable web search")
|
||||
parser.add_argument("--web-provider", default="duckduckgo", help="Web search provider (duckduckgo, tavily, serper)")
|
||||
|
||||
# Execution
|
||||
parser.add_argument("--sandbox", action="store_true", help="Enable sandbox mode")
|
||||
parser.add_argument("--exec", dest="exec_enabled", action="store_true", help="Enable exec tool")
|
||||
parser.add_argument("--auto-approve", dest="auto_approve", action="store_true", help="Auto-approve all tool executions")
|
||||
|
||||
# Session
|
||||
parser.add_argument("--session-id", help="Session ID")
|
||||
parser.add_argument("--user-id", help="User ID for memory isolation")
|
||||
|
||||
# Thinking mode
|
||||
parser.add_argument("--thinking", choices=["off", "minimal", "low", "medium", "high"], help="Thinking mode")
|
||||
|
||||
# Audio (TTS/STT)
|
||||
parser.add_argument("--tts", action="store_true", help="Enable TTS tool for text-to-speech")
|
||||
parser.add_argument("--tts-voice", default="alloy", help="TTS voice (alloy, echo, fable, onyx, nova, shimmer)")
|
||||
parser.add_argument("--tts-model", help="TTS model (default: openai/tts-1)")
|
||||
parser.add_argument("--auto-tts", action="store_true", help="Auto-convert all responses to speech")
|
||||
parser.add_argument("--stt", action="store_true", help="Enable STT tool for speech-to-text")
|
||||
parser.add_argument("--stt-model", help="STT model (default: openai/whisper-1)")
|
||||
|
||||
|
||||
def _build_capabilities_from_args(args) -> BotCapabilities:
|
||||
"""Build BotCapabilities from parsed arguments."""
|
||||
return BotCapabilities(
|
||||
browser=getattr(args, "browser", False),
|
||||
browser_profile=getattr(args, "browser_profile", "default"),
|
||||
browser_headless=getattr(args, "browser_headless", False),
|
||||
tools=getattr(args, "tools", []) or [],
|
||||
skills=getattr(args, "skills", []) or [],
|
||||
skills_dir=getattr(args, "skills_dir", None),
|
||||
memory=getattr(args, "memory", False),
|
||||
memory_provider=getattr(args, "memory_provider", "default"),
|
||||
knowledge=getattr(args, "knowledge", False),
|
||||
knowledge_sources=getattr(args, "knowledge_sources", []) or [],
|
||||
web_search=getattr(args, "web_search", False),
|
||||
web_search_provider=getattr(args, "web_provider", "duckduckgo"),
|
||||
sandbox=getattr(args, "sandbox", False),
|
||||
exec_enabled=getattr(args, "exec_enabled", False),
|
||||
auto_approve=getattr(args, "auto_approve", False),
|
||||
model=getattr(args, "model", None),
|
||||
thinking=getattr(args, "thinking", None),
|
||||
tts=getattr(args, "tts", False),
|
||||
tts_voice=getattr(args, "tts_voice", "alloy"),
|
||||
tts_model=getattr(args, "tts_model", None),
|
||||
auto_tts=getattr(args, "auto_tts", False),
|
||||
stt=getattr(args, "stt", False),
|
||||
stt_model=getattr(args, "stt_model", None),
|
||||
session_id=getattr(args, "session_id", None),
|
||||
user_id=getattr(args, "user_id", None),
|
||||
)
|
||||
|
||||
|
||||
def handle_bot_command(args) -> int:
|
||||
"""Handle bot CLI command.
|
||||
|
||||
Args:
|
||||
args: Either a list of command-line arguments (from main.py unknown_args)
|
||||
or an argparse.Namespace object (from legacy add_bot_parser).
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for error)
|
||||
"""
|
||||
import argparse
|
||||
|
||||
# If args is a list, parse it using argparse
|
||||
if isinstance(args, list):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="praisonai bot",
|
||||
description="Start a messaging bot with full agent capabilities",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Basic bot
|
||||
praisonai bot telegram --token $TELEGRAM_BOT_TOKEN
|
||||
|
||||
# Bot with agent YAML
|
||||
praisonai bot slack --token $SLACK_BOT_TOKEN --agent agents.yaml
|
||||
|
||||
# Bot with browser and web search
|
||||
praisonai bot discord --token $DISCORD_BOT_TOKEN --browser --web
|
||||
|
||||
# Bot with tools
|
||||
praisonai bot telegram --token $TELEGRAM_BOT_TOKEN --tools DuckDuckGoTool WikipediaTool
|
||||
|
||||
# Full-featured bot
|
||||
praisonai bot slack --token $SLACK_BOT_TOKEN --agent agents.yaml --browser --web --memory --model gpt-4o
|
||||
""",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="platform", help="Bot platform")
|
||||
|
||||
# Telegram
|
||||
telegram_parser = subparsers.add_parser("telegram", help="Start a Telegram bot")
|
||||
telegram_parser.add_argument("--token", help="Telegram bot token")
|
||||
_add_capability_args(telegram_parser)
|
||||
|
||||
# Discord
|
||||
discord_parser = subparsers.add_parser("discord", help="Start a Discord bot")
|
||||
discord_parser.add_argument("--token", help="Discord bot token")
|
||||
_add_capability_args(discord_parser)
|
||||
|
||||
# Slack
|
||||
slack_parser = subparsers.add_parser("slack", help="Start a Slack bot")
|
||||
slack_parser.add_argument("--token", help="Slack bot token")
|
||||
slack_parser.add_argument("--app-token", dest="app_token", help="Slack app token for Socket Mode")
|
||||
_add_capability_args(slack_parser)
|
||||
|
||||
try:
|
||||
args = parser.parse_args(args)
|
||||
except SystemExit:
|
||||
return 1
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = _build_capabilities_from_args(args)
|
||||
|
||||
platform = getattr(args, "platform", None)
|
||||
|
||||
@@ -173,28 +661,51 @@ def handle_bot_command(args) -> None:
|
||||
handler.start_telegram(
|
||||
token=getattr(args, "token", None),
|
||||
agent_file=getattr(args, "agent", None),
|
||||
capabilities=capabilities,
|
||||
)
|
||||
return 0
|
||||
elif platform == "discord":
|
||||
handler.start_discord(
|
||||
token=getattr(args, "token", None),
|
||||
agent_file=getattr(args, "agent", None),
|
||||
capabilities=capabilities,
|
||||
)
|
||||
return 0
|
||||
elif platform == "slack":
|
||||
handler.start_slack(
|
||||
token=getattr(args, "token", None),
|
||||
app_token=getattr(args, "app_token", None),
|
||||
agent_file=getattr(args, "agent", None),
|
||||
capabilities=capabilities,
|
||||
)
|
||||
return 0
|
||||
else:
|
||||
print("Available platforms: telegram, discord, slack")
|
||||
print("")
|
||||
print("Usage: praisonai bot <platform> [options]")
|
||||
print("")
|
||||
print("Capability Options:")
|
||||
print(" --agent FILE Agent YAML configuration file")
|
||||
print(" --model MODEL LLM model to use")
|
||||
print(" --browser Enable browser control")
|
||||
print(" --web Enable web search")
|
||||
print(" --memory Enable memory")
|
||||
print(" --knowledge Enable knowledge/RAG")
|
||||
print(" --tools TOOL... Enable specific tools")
|
||||
print(" --sandbox Enable sandbox mode")
|
||||
print("")
|
||||
print("Examples:")
|
||||
print(" praisonai bot telegram --token $TELEGRAM_BOT_TOKEN")
|
||||
print(" praisonai bot slack --token $SLACK_BOT_TOKEN --agent agents.yaml --browser --web")
|
||||
print(" praisonai bot discord --token $DISCORD_BOT_TOKEN --tools DuckDuckGoTool --memory")
|
||||
return 1
|
||||
|
||||
|
||||
def add_bot_parser(subparsers) -> None:
|
||||
"""Add bot subparser to CLI."""
|
||||
"""Add bot subparser to CLI with full capability options."""
|
||||
bot_parser = subparsers.add_parser(
|
||||
"bot",
|
||||
help="Start a messaging bot",
|
||||
help="Start a messaging bot with full agent capabilities",
|
||||
)
|
||||
|
||||
bot_subparsers = bot_parser.add_subparsers(
|
||||
@@ -202,6 +713,7 @@ def add_bot_parser(subparsers) -> None:
|
||||
help="Bot platform",
|
||||
)
|
||||
|
||||
# Telegram
|
||||
telegram_parser = bot_subparsers.add_parser(
|
||||
"telegram",
|
||||
help="Start a Telegram bot",
|
||||
@@ -210,11 +722,9 @@ def add_bot_parser(subparsers) -> None:
|
||||
"--token",
|
||||
help="Telegram bot token (or use TELEGRAM_BOT_TOKEN env var)",
|
||||
)
|
||||
telegram_parser.add_argument(
|
||||
"--agent",
|
||||
help="Path to agent configuration file",
|
||||
)
|
||||
_add_capability_args(telegram_parser)
|
||||
|
||||
# Discord
|
||||
discord_parser = bot_subparsers.add_parser(
|
||||
"discord",
|
||||
help="Start a Discord bot",
|
||||
@@ -223,11 +733,9 @@ def add_bot_parser(subparsers) -> None:
|
||||
"--token",
|
||||
help="Discord bot token (or use DISCORD_BOT_TOKEN env var)",
|
||||
)
|
||||
discord_parser.add_argument(
|
||||
"--agent",
|
||||
help="Path to agent configuration file",
|
||||
)
|
||||
_add_capability_args(discord_parser)
|
||||
|
||||
# Slack
|
||||
slack_parser = bot_subparsers.add_parser(
|
||||
"slack",
|
||||
help="Start a Slack bot",
|
||||
@@ -241,9 +749,6 @@ def add_bot_parser(subparsers) -> None:
|
||||
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",
|
||||
)
|
||||
_add_capability_args(slack_parser)
|
||||
|
||||
bot_parser.set_defaults(func=handle_bot_command)
|
||||
@@ -21,6 +21,8 @@ LEGACY_COMMANDS = {
|
||||
'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api',
|
||||
'doctor', 'registry', 'package', 'install', 'uninstall', 'acp', 'debug',
|
||||
'lsp', 'diag', 'persistence', 'browser',
|
||||
# Bot/Gateway/Sandbox commands (added for resident agent features)
|
||||
'bot', 'gateway', 'sandbox', 'wizard', 'migrate', 'security',
|
||||
}
|
||||
|
||||
# Typer commands that have been implemented
|
||||
@@ -38,9 +40,14 @@ TYPER_COMMANDS = {
|
||||
'replay',
|
||||
# Standardisation commands
|
||||
'standardise', 'standardize',
|
||||
# Moltbot-inspired commands (bots, browser, plugins, sandbox)
|
||||
'bot', 'browser', 'plugins', 'sandbox', 'loop',
|
||||
# RAG commands
|
||||
'rag', 'index', 'query', 'search',
|
||||
}
|
||||
|
||||
|
||||
|
||||
def is_legacy_invocation(argv: List[str]) -> bool:
|
||||
"""
|
||||
Check if the command line represents a legacy invocation.
|
||||
|
||||
@@ -794,7 +794,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates', 'recipe', 'endpoints', 'audio', 'embed', 'embedding', 'images', 'moderate', 'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants', 'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos', 'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api', 'doctor', 'registry', 'package', 'install', 'uninstall', 'acp', 'debug', 'lsp', 'diag', 'browser', 'replay']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates', 'recipe', 'endpoints', 'audio', 'embed', 'embedding', 'images', 'moderate', 'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants', 'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos', 'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api', 'doctor', 'registry', 'package', 'install', 'uninstall', 'acp', 'debug', 'lsp', 'diag', 'browser', 'replay', 'bot', 'gateway', 'sandbox', 'wizard', 'migrate', 'security', 'persistence']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -832,7 +832,7 @@ class PraisonAI:
|
||||
parser.add_argument("--no-lsp", action="store_true", help="Disable LSP tools (code intelligence: symbols, definitions, references)")
|
||||
parser.add_argument("--save", "-s", action="store_true", help="Save research output to file (output/research/)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output for research")
|
||||
parser.add_argument("--web-search", action="store_true", help="Enable native web search (OpenAI, Gemini, Anthropic, xAI, Perplexity)")
|
||||
parser.add_argument("--web", "--web-search", action="store_true", help="Enable native web search (OpenAI, Gemini, Anthropic, xAI, Perplexity)")
|
||||
parser.add_argument("--web-fetch", action="store_true", help="Enable web fetch to retrieve URL content (Anthropic only)")
|
||||
parser.add_argument("--prompt-caching", action="store_true", help="Enable prompt caching to reduce costs (OpenAI, Anthropic, Bedrock, Deepseek)")
|
||||
|
||||
@@ -1648,8 +1648,12 @@ class PraisonAI:
|
||||
|
||||
elif args.command == 'bot':
|
||||
# Bot command - messaging bot runtimes (Telegram, Discord, Slack)
|
||||
# Re-inject flags consumed by main parser into unknown_args for bot handler
|
||||
bot_args = list(unknown_args)
|
||||
if getattr(args, 'model', None) and '--model' not in bot_args and '-m' not in bot_args:
|
||||
bot_args.extend(['--model', args.model])
|
||||
from .features.bots_cli import handle_bot_command
|
||||
exit_code = handle_bot_command(unknown_args)
|
||||
exit_code = handle_bot_command(bot_args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
elif args.command == 'sandbox':
|
||||
|
||||
@@ -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.11.2 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==3.12.2 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ __all__ = [
|
||||
"multiedit",
|
||||
"glob_files",
|
||||
"grep_search",
|
||||
"tts_tool",
|
||||
"stt_tool",
|
||||
"create_tts_tool",
|
||||
"create_stt_tool",
|
||||
]
|
||||
|
||||
_lazy_cache = {}
|
||||
@@ -34,4 +38,24 @@ def __getattr__(name: str):
|
||||
_lazy_cache[name] = grep_search
|
||||
return grep_search
|
||||
|
||||
if name == "tts_tool":
|
||||
from .audio import tts_tool
|
||||
_lazy_cache[name] = tts_tool
|
||||
return tts_tool
|
||||
|
||||
if name == "stt_tool":
|
||||
from .audio import stt_tool
|
||||
_lazy_cache[name] = stt_tool
|
||||
return stt_tool
|
||||
|
||||
if name == "create_tts_tool":
|
||||
from .audio import create_tts_tool
|
||||
_lazy_cache[name] = create_tts_tool
|
||||
return create_tts_tool
|
||||
|
||||
if name == "create_stt_tool":
|
||||
from .audio import create_stt_tool
|
||||
_lazy_cache[name] = create_stt_tool
|
||||
return create_stt_tool
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Audio Tools for PraisonAI Bots.
|
||||
|
||||
Provides TTS and STT tools that wrap the core AudioAgent for use by agents.
|
||||
These tools enable agents to convert text to speech and transcribe audio.
|
||||
|
||||
Following moltbot patterns:
|
||||
- tts_tool: Convert text to speech, return audio file path
|
||||
- stt_tool: Transcribe audio file to text
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Lazy-loaded AudioAgent instance (shared for efficiency)
|
||||
_audio_agent = None
|
||||
|
||||
|
||||
def _get_audio_agent():
|
||||
"""Lazy load AudioAgent from core SDK."""
|
||||
global _audio_agent
|
||||
if _audio_agent is None:
|
||||
try:
|
||||
from praisonaiagents import AudioAgent
|
||||
_audio_agent = AudioAgent(verbose=False)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"AudioAgent requires litellm. "
|
||||
"Install with: pip install praisonaiagents[llm]"
|
||||
)
|
||||
return _audio_agent
|
||||
|
||||
|
||||
def tts_tool(
|
||||
text: str,
|
||||
voice: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
output_dir: Optional[str] = None,
|
||||
output_format: str = "mp3",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert text to speech and return the audio file path.
|
||||
|
||||
This tool wraps the core AudioAgent.speech() method for use by agents.
|
||||
Similar to moltbot's tts-tool, it returns a MEDIA: path that can be
|
||||
sent to messaging platforms.
|
||||
|
||||
Args:
|
||||
text: Text to convert to speech
|
||||
voice: Voice to use (e.g., "alloy", "echo", "fable", "onyx", "nova", "shimmer")
|
||||
model: TTS model (default: "openai/tts-1")
|
||||
output_dir: Directory to save audio (default: temp directory)
|
||||
output_format: Audio format (mp3, opus, aac, flac, wav)
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- success: bool
|
||||
- audio_path: str (path to audio file)
|
||||
- media_line: str (MEDIA:path for bot responses)
|
||||
- error: str (if failed)
|
||||
|
||||
Example:
|
||||
result = tts_tool("Hello world!")
|
||||
if result["success"]:
|
||||
print(result["media_line"]) # MEDIA:/tmp/tts_abc123.mp3
|
||||
"""
|
||||
try:
|
||||
agent = _get_audio_agent()
|
||||
|
||||
# Determine output path
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_path = os.path.join(output_dir, f"tts_{os.urandom(8).hex()}.{output_format}")
|
||||
else:
|
||||
# Use temp directory
|
||||
output_path = os.path.join(
|
||||
tempfile.gettempdir(),
|
||||
f"tts_{os.urandom(8).hex()}.{output_format}"
|
||||
)
|
||||
|
||||
# Build kwargs
|
||||
kwargs = {"response_format": output_format}
|
||||
if voice:
|
||||
kwargs["voice"] = voice
|
||||
if model:
|
||||
kwargs["model"] = model
|
||||
|
||||
# Generate speech
|
||||
agent.speech(text, output=output_path, **kwargs)
|
||||
|
||||
# Check if file was created
|
||||
if os.path.exists(output_path):
|
||||
# Determine if voice-compatible (opus for Telegram)
|
||||
voice_compatible = output_format.lower() in ("opus", "ogg")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"audio_path": output_path,
|
||||
"media_line": f"MEDIA:{output_path}",
|
||||
"voice_compatible": voice_compatible,
|
||||
"format": output_format,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Audio file was not created",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS error: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
def stt_tool(
|
||||
audio_path: str,
|
||||
language: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe audio file to text.
|
||||
|
||||
This tool wraps the core AudioAgent.transcribe() method for use by agents.
|
||||
|
||||
Args:
|
||||
audio_path: Path to audio file to transcribe
|
||||
language: Language code (e.g., "en", "es", "fr")
|
||||
model: STT model (default: "openai/whisper-1")
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- success: bool
|
||||
- text: str (transcribed text)
|
||||
- error: str (if failed)
|
||||
|
||||
Example:
|
||||
result = stt_tool("audio.mp3")
|
||||
if result["success"]:
|
||||
print(result["text"])
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(audio_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Audio file not found: {audio_path}",
|
||||
}
|
||||
|
||||
agent = _get_audio_agent()
|
||||
|
||||
# Build kwargs
|
||||
kwargs = {}
|
||||
if language:
|
||||
kwargs["language"] = language
|
||||
if model:
|
||||
kwargs["model"] = model
|
||||
|
||||
# Transcribe
|
||||
text = agent.transcribe(audio_path, **kwargs)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"text": text,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"STT error: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
# Tool functions decorated for agent use
|
||||
def create_tts_tool():
|
||||
"""
|
||||
Create a TTS tool function for use with agents.
|
||||
|
||||
Returns a function that can be added to an agent's tools list.
|
||||
|
||||
Example:
|
||||
from praisonai.tools.audio import create_tts_tool
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="assistant",
|
||||
tools=[create_tts_tool()]
|
||||
)
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import tool
|
||||
except ImportError:
|
||||
raise ImportError("praisonaiagents is required")
|
||||
|
||||
@tool
|
||||
def tts(text: str, voice: str = "alloy") -> str:
|
||||
"""
|
||||
Convert text to speech and return the audio file path.
|
||||
|
||||
Args:
|
||||
text: Text to convert to speech
|
||||
voice: Voice to use (alloy, echo, fable, onyx, nova, shimmer)
|
||||
|
||||
Returns:
|
||||
MEDIA: path to the generated audio file, or error message
|
||||
"""
|
||||
result = tts_tool(text, voice=voice)
|
||||
if result["success"]:
|
||||
return result["media_line"]
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
return tts
|
||||
|
||||
|
||||
def create_stt_tool():
|
||||
"""
|
||||
Create an STT tool function for use with agents.
|
||||
|
||||
Returns a function that can be added to an agent's tools list.
|
||||
|
||||
Example:
|
||||
from praisonai.tools.audio import create_stt_tool
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="assistant",
|
||||
tools=[create_stt_tool()]
|
||||
)
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import tool
|
||||
except ImportError:
|
||||
raise ImportError("praisonaiagents is required")
|
||||
|
||||
@tool
|
||||
def stt(audio_path: str, language: str = None) -> str:
|
||||
"""
|
||||
Transcribe audio file to text.
|
||||
|
||||
Args:
|
||||
audio_path: Path to audio file to transcribe
|
||||
language: Optional language code (en, es, fr, etc.)
|
||||
|
||||
Returns:
|
||||
Transcribed text, or error message
|
||||
"""
|
||||
result = stt_tool(audio_path, language=language)
|
||||
if result["success"]:
|
||||
return result["text"]
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
return stt
|
||||
|
||||
|
||||
# Convenience exports
|
||||
__all__ = [
|
||||
"tts_tool",
|
||||
"stt_tool",
|
||||
"create_tts_tool",
|
||||
"create_stt_tool",
|
||||
]
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.11.2"
|
||||
__version__ = "3.12.2"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.14.2",
|
||||
"praisonaiagents>=0.15.2",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY run.sh /usr/local/bin/praisonai-install-smoke
|
||||
RUN chmod +x /usr/local/bin/praisonai-install-smoke
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/praisonai-install-smoke"]
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_URL="${PRAISONAI_INSTALL_URL:-https://praison.ai/install.sh}"
|
||||
EXPECTED_VERSION="${PRAISONAI_EXPECTED_VERSION:-}"
|
||||
EXTRAS="${PRAISONAI_EXTRAS:-}"
|
||||
|
||||
echo "==> PraisonAI Install Smoke Test"
|
||||
echo "install_url=$INSTALL_URL"
|
||||
echo "expected_version=$EXPECTED_VERSION"
|
||||
echo "extras=$EXTRAS"
|
||||
|
||||
# Get latest version from PyPI if not specified
|
||||
if [[ -z "$EXPECTED_VERSION" ]]; then
|
||||
echo "==> Fetching latest version from PyPI..."
|
||||
EXPECTED_VERSION=$(pip index versions praisonaiagents 2>/dev/null | head -1 | grep -oP '\(\K[^)]+' || echo "")
|
||||
if [[ -z "$EXPECTED_VERSION" ]]; then
|
||||
# Fallback: try pip show after install
|
||||
echo "==> Could not determine version from PyPI, will verify after install"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> Running official installer one-liner"
|
||||
if [[ -n "$EXTRAS" ]]; then
|
||||
PRAISONAI_EXTRAS="$EXTRAS" curl -fsSL "$INSTALL_URL" | bash
|
||||
else
|
||||
curl -fsSL "$INSTALL_URL" | bash
|
||||
fi
|
||||
|
||||
echo "==> Verify installation"
|
||||
|
||||
# Source the venv if it exists
|
||||
VENV_DIR="$HOME/.praisonai/venv"
|
||||
if [[ -d "$VENV_DIR" ]]; then
|
||||
source "$VENV_DIR/bin/activate"
|
||||
fi
|
||||
|
||||
# Check import works
|
||||
python3 -c "import praisonaiagents; print(f'Installed version: {praisonaiagents.__version__}')"
|
||||
|
||||
INSTALLED_VERSION=$(python3 -c "import praisonaiagents; print(praisonaiagents.__version__)" 2>/dev/null)
|
||||
echo "installed=$INSTALLED_VERSION expected=$EXPECTED_VERSION"
|
||||
|
||||
if [[ -n "$EXPECTED_VERSION" && "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then
|
||||
echo "WARNING: Version mismatch (expected $EXPECTED_VERSION, got $INSTALLED_VERSION)"
|
||||
# Don't fail on version mismatch for now, as PyPI version detection may be flaky
|
||||
fi
|
||||
|
||||
echo "==> Sanity check: Basic agent creation"
|
||||
python3 -c "
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(name='test', instructions='You are a test agent')
|
||||
print(f'Agent created: {agent.name}')
|
||||
print('Basic sanity check passed!')
|
||||
"
|
||||
|
||||
echo "==> Smoke test PASSED"
|
||||
@@ -0,0 +1,459 @@
|
||||
# PraisonAI Installer for Windows
|
||||
# Usage: iwr -useb https://praison.ai/install.ps1 | iex
|
||||
#
|
||||
# Parameters:
|
||||
# -Version Specific version to install (default: latest)
|
||||
# -Extras Comma-separated extras (e.g., "ui,chat,code")
|
||||
# -NoVenv Skip virtual environment creation
|
||||
# -Python Path to Python executable
|
||||
# -DryRun Print what would happen without making changes
|
||||
# -Help Show help message
|
||||
|
||||
param(
|
||||
[string]$Version = "latest",
|
||||
[string]$Extras = "",
|
||||
[switch]$NoVenv,
|
||||
[string]$Python = "",
|
||||
[switch]$DryRun,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# Environment variable overrides
|
||||
if ($env:PRAISONAI_VERSION) { $Version = $env:PRAISONAI_VERSION }
|
||||
if ($env:PRAISONAI_EXTRAS) { $Extras = $env:PRAISONAI_EXTRAS }
|
||||
if ($env:PRAISONAI_SKIP_VENV -eq "1") { $NoVenv = $true }
|
||||
if ($env:PRAISONAI_PYTHON) { $Python = $env:PRAISONAI_PYTHON }
|
||||
if ($env:PRAISONAI_DRY_RUN -eq "1") { $DryRun = $true }
|
||||
|
||||
$MinPythonVersion = [version]"3.10"
|
||||
$VenvDir = "$env:USERPROFILE\.praisonai\venv"
|
||||
|
||||
function Write-Banner {
|
||||
Write-Host ""
|
||||
Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ ║" -ForegroundColor Cyan
|
||||
Write-Host "║ PraisonAI - AI Agents Made Simple ║" -ForegroundColor Cyan
|
||||
Write-Host "║ ║" -ForegroundColor Cyan
|
||||
Write-Host "║ Works everywhere. Installs everything. You're welcome. ║" -ForegroundColor Cyan
|
||||
Write-Host "║ ║" -ForegroundColor Cyan
|
||||
Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Info {
|
||||
param([string]$Message)
|
||||
Write-Host "==> " -ForegroundColor Blue -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host "✓ " -ForegroundColor Green -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host "⚠ " -ForegroundColor Yellow -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "✗ " -ForegroundColor Red -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "→ " -ForegroundColor Cyan -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Show-Help {
|
||||
Write-Host "PraisonAI Installer for Windows"
|
||||
Write-Host ""
|
||||
Write-Host "Usage: iwr -useb https://praison.ai/install.ps1 | iex"
|
||||
Write-Host " & ([scriptblock]::Create((iwr -useb https://praison.ai/install.ps1))) [OPTIONS]"
|
||||
Write-Host ""
|
||||
Write-Host "Options:"
|
||||
Write-Host " -Version VERSION Install specific version (default: latest)"
|
||||
Write-Host " -Extras EXTRAS Install with extras (e.g., ui,chat,code)"
|
||||
Write-Host " -NoVenv Skip virtual environment creation"
|
||||
Write-Host " -Python PATH Use specific Python executable"
|
||||
Write-Host " -DryRun Print what would happen without making changes"
|
||||
Write-Host " -Help Show this help message"
|
||||
Write-Host ""
|
||||
Write-Host "Environment variables:"
|
||||
Write-Host " PRAISONAI_VERSION Specific version to install"
|
||||
Write-Host " PRAISONAI_EXTRAS Comma-separated extras"
|
||||
Write-Host " PRAISONAI_SKIP_VENV Skip virtual environment (1 to enable)"
|
||||
Write-Host " PRAISONAI_PYTHON Path to Python executable"
|
||||
Write-Host " PRAISONAI_DRY_RUN Print what would happen (1 to enable)"
|
||||
Write-Host ""
|
||||
Write-Host "Examples:"
|
||||
Write-Host " # Basic install"
|
||||
Write-Host " iwr -useb https://praison.ai/install.ps1 | iex"
|
||||
Write-Host ""
|
||||
Write-Host " # Install with extras"
|
||||
Write-Host ' & ([scriptblock]::Create((iwr -useb https://praison.ai/install.ps1))) -Extras "ui,chat"'
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Test-PythonVersion {
|
||||
param([string]$PythonPath)
|
||||
|
||||
try {
|
||||
$versionOutput = & $PythonPath -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null
|
||||
if ($versionOutput) {
|
||||
$version = [version]$versionOutput
|
||||
return $version -ge $MinPythonVersion
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Find-Python {
|
||||
# Check user-specified Python first
|
||||
if ($Python -and (Test-Path $Python)) {
|
||||
if (Test-PythonVersion $Python) {
|
||||
return $Python
|
||||
}
|
||||
}
|
||||
|
||||
# Try common Python commands
|
||||
$pythonCommands = @("python", "python3", "py -3.12", "py -3.11", "py -3.10", "py")
|
||||
|
||||
foreach ($cmd in $pythonCommands) {
|
||||
try {
|
||||
$cmdParts = $cmd -split " "
|
||||
$exe = $cmdParts[0]
|
||||
$args = if ($cmdParts.Length -gt 1) { $cmdParts[1..($cmdParts.Length-1)] } else { @() }
|
||||
|
||||
if (Get-Command $exe -ErrorAction SilentlyContinue) {
|
||||
$fullCmd = if ($args) { "$exe $($args -join ' ')" } else { $exe }
|
||||
if (Test-PythonVersion $fullCmd) {
|
||||
return $fullCmd
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Install-Python {
|
||||
Write-Info "Python $MinPythonVersion+ not found. Installing..."
|
||||
|
||||
# Try winget first
|
||||
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
||||
Write-Step "Installing Python via winget..."
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would run: winget install Python.Python.3.12"
|
||||
} else {
|
||||
winget install Python.Python.3.12 --accept-source-agreements --accept-package-agreements
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Try Chocolatey
|
||||
if (Get-Command choco -ErrorAction SilentlyContinue) {
|
||||
Write-Step "Installing Python via Chocolatey..."
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would run: choco install python -y"
|
||||
} else {
|
||||
choco install python -y
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Try Scoop
|
||||
if (Get-Command scoop -ErrorAction SilentlyContinue) {
|
||||
Write-Step "Installing Python via Scoop..."
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would run: scoop install python"
|
||||
} else {
|
||||
scoop install python
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Could not find a package manager (winget, choco, or scoop)."
|
||||
Write-Host "Please install Python $MinPythonVersion+ manually from: https://www.python.org/downloads/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Ensure-Python {
|
||||
Write-Info "Checking Python installation..."
|
||||
|
||||
$pythonCmd = Find-Python
|
||||
|
||||
if ($pythonCmd) {
|
||||
$version = & $pythonCmd -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')" 2>$null
|
||||
Write-Success "Python $version found: $pythonCmd"
|
||||
return $pythonCmd
|
||||
}
|
||||
|
||||
Install-Python
|
||||
|
||||
# Refresh PATH
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
|
||||
$pythonCmd = Find-Python
|
||||
if ($pythonCmd) {
|
||||
Write-Success "Python installed successfully"
|
||||
return $pythonCmd
|
||||
}
|
||||
|
||||
Write-Error "Failed to install Python. Please install Python $MinPythonVersion+ manually."
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Ensure-Pip {
|
||||
param([string]$PythonCmd)
|
||||
|
||||
Write-Info "Checking pip..."
|
||||
|
||||
try {
|
||||
& $PythonCmd -m pip --version 2>$null | Out-Null
|
||||
Write-Success "pip is available"
|
||||
return
|
||||
} catch {}
|
||||
|
||||
Write-Step "Installing pip..."
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would install pip"
|
||||
return
|
||||
}
|
||||
|
||||
# Try ensurepip
|
||||
try {
|
||||
& $PythonCmd -m ensurepip --upgrade 2>$null
|
||||
Write-Success "pip installed via ensurepip"
|
||||
return
|
||||
} catch {}
|
||||
|
||||
# Fall back to get-pip.py
|
||||
$getPipUrl = "https://bootstrap.pypa.io/get-pip.py"
|
||||
$getPipPath = "$env:TEMP\get-pip.py"
|
||||
Invoke-WebRequest -Uri $getPipUrl -OutFile $getPipPath
|
||||
& $PythonCmd $getPipPath
|
||||
Remove-Item $getPipPath -ErrorAction SilentlyContinue
|
||||
Write-Success "pip installed via get-pip.py"
|
||||
}
|
||||
|
||||
function Create-Venv {
|
||||
param([string]$PythonCmd)
|
||||
|
||||
if ($NoVenv) {
|
||||
Write-Info "Skipping virtual environment (NoVenv specified)"
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Info "Creating virtual environment..."
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would create venv at: $VenvDir"
|
||||
return $VenvDir
|
||||
}
|
||||
|
||||
$parentDir = Split-Path $VenvDir -Parent
|
||||
if (-not (Test-Path $parentDir)) {
|
||||
New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
|
||||
}
|
||||
|
||||
if (Test-Path $VenvDir) {
|
||||
Write-Step "Virtual environment already exists at $VenvDir"
|
||||
} else {
|
||||
& $PythonCmd -m venv $VenvDir
|
||||
Write-Success "Virtual environment created at $VenvDir"
|
||||
}
|
||||
|
||||
return $VenvDir
|
||||
}
|
||||
|
||||
function Install-PraisonAI {
|
||||
param(
|
||||
[string]$PythonCmd,
|
||||
[string]$VenvPath
|
||||
)
|
||||
|
||||
Write-Info "Installing PraisonAI..."
|
||||
|
||||
$pipCmd = "$PythonCmd -m pip"
|
||||
|
||||
# Use venv pip if available
|
||||
if ($VenvPath -and (Test-Path "$VenvPath\Scripts\pip.exe")) {
|
||||
$pipCmd = "$VenvPath\Scripts\pip.exe"
|
||||
}
|
||||
|
||||
# Build install package name
|
||||
$installPkg = "praisonaiagents"
|
||||
if ($Version -ne "latest") {
|
||||
$installPkg = "praisonaiagents==$Version"
|
||||
}
|
||||
|
||||
# Add extras if specified
|
||||
if ($Extras) {
|
||||
$installPkg = "praisonaiagents[$Extras]"
|
||||
if ($Version -ne "latest") {
|
||||
$installPkg = "praisonaiagents[$Extras]==$Version"
|
||||
}
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would run: $pipCmd install --upgrade $installPkg"
|
||||
return
|
||||
}
|
||||
|
||||
# Upgrade pip first
|
||||
Invoke-Expression "$pipCmd install --upgrade pip"
|
||||
|
||||
# Install PraisonAI
|
||||
Invoke-Expression "$pipCmd install --upgrade $installPkg"
|
||||
|
||||
# Also install wrapper if extras include ui/chat/code
|
||||
if ($Extras -match "ui|chat|code") {
|
||||
$wrapperPkg = "praisonai[$Extras]"
|
||||
if ($Version -ne "latest") {
|
||||
$wrapperPkg = "praisonai[$Extras]==$Version"
|
||||
}
|
||||
Invoke-Expression "$pipCmd install --upgrade $wrapperPkg"
|
||||
}
|
||||
|
||||
Write-Success "PraisonAI installed successfully!"
|
||||
}
|
||||
|
||||
function Setup-Path {
|
||||
param([string]$VenvPath)
|
||||
|
||||
if ($NoVenv -or -not $VenvPath) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Info "Setting up PATH..."
|
||||
|
||||
$binDir = "$VenvPath\Scripts"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would add to PATH: $binDir"
|
||||
return
|
||||
}
|
||||
|
||||
# Check if already in PATH
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if ($currentPath -notlike "*$binDir*") {
|
||||
$newPath = "$binDir;$currentPath"
|
||||
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
||||
Write-Success "Added PraisonAI to user PATH"
|
||||
} else {
|
||||
Write-Step "PATH already configured"
|
||||
}
|
||||
|
||||
# Update current session
|
||||
$env:Path = "$binDir;$env:Path"
|
||||
}
|
||||
|
||||
function Test-Installation {
|
||||
param([string]$VenvPath)
|
||||
|
||||
Write-Info "Verifying installation..."
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "[DRY RUN] Would verify installation"
|
||||
return
|
||||
}
|
||||
|
||||
$pythonCmd = "python"
|
||||
if ($VenvPath -and (Test-Path "$VenvPath\Scripts\python.exe")) {
|
||||
$pythonCmd = "$VenvPath\Scripts\python.exe"
|
||||
}
|
||||
|
||||
try {
|
||||
$version = & $pythonCmd -c "import praisonaiagents; print(f'Version: {praisonaiagents.__version__}')" 2>$null
|
||||
if ($version) {
|
||||
Write-Success "PraisonAI agents package verified: $version"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to import praisonaiagents"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check CLI
|
||||
if (Get-Command praisonai -ErrorAction SilentlyContinue) {
|
||||
$cliVersion = & praisonai --version 2>$null | Select-Object -First 1
|
||||
Write-Success "CLI available: praisonai $cliVersion"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-NextSteps {
|
||||
param([string]$VenvPath)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Installation complete!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
if ($VenvPath -and -not $NoVenv) {
|
||||
Write-Host " 1. Activate the virtual environment:"
|
||||
Write-Host " $VenvPath\Scripts\Activate.ps1" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " 2. Or restart your terminal to use PraisonAI globally"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host " Quick start:"
|
||||
Write-Host ' python -c "from praisonaiagents import Agent; Agent(name=''test'').start(''Hello!'')"' -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " Documentation:"
|
||||
Write-Host " https://docs.praison.ai" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " Set your API key:"
|
||||
Write-Host ' $env:OPENAI_API_KEY = "your_key"' -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Main
|
||||
function Main {
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
return
|
||||
}
|
||||
|
||||
Write-Banner
|
||||
|
||||
Write-Info "Detected OS: Windows"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Warning "Running in DRY RUN mode - no changes will be made"
|
||||
}
|
||||
|
||||
# Ensure Python
|
||||
$pythonCmd = Ensure-Python
|
||||
|
||||
# Ensure pip
|
||||
Ensure-Pip $pythonCmd
|
||||
|
||||
# Create virtual environment
|
||||
$venvPath = Create-Venv $pythonCmd
|
||||
|
||||
# Install PraisonAI
|
||||
Install-PraisonAI $pythonCmd $venvPath
|
||||
|
||||
# Setup PATH
|
||||
Setup-Path $venvPath
|
||||
|
||||
# Verify installation
|
||||
Test-Installation $venvPath
|
||||
|
||||
# Show next steps
|
||||
Show-NextSteps $venvPath
|
||||
}
|
||||
|
||||
Main
|
||||
Executable
+636
@@ -0,0 +1,636 @@
|
||||
#!/usr/bin/env bash
|
||||
# PraisonAI Installer
|
||||
# Works on macOS, Linux, and Windows (WSL)
|
||||
# Usage: curl -fsSL https://praison.ai/install.sh | bash
|
||||
#
|
||||
# Environment variables:
|
||||
# PRAISONAI_VERSION - Specific version to install (default: latest)
|
||||
# PRAISONAI_EXTRAS - Comma-separated extras (e.g., "ui,chat,code")
|
||||
# PRAISONAI_NO_PROMPT - Skip interactive prompts (1 to enable)
|
||||
# PRAISONAI_DRY_RUN - Print what would happen without making changes
|
||||
# PRAISONAI_PYTHON - Path to Python executable
|
||||
# PRAISONAI_SKIP_VENV - Skip virtual environment creation (1 to enable)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
|
||||
# Configuration
|
||||
VERSION="${PRAISONAI_VERSION:-latest}"
|
||||
EXTRAS="${PRAISONAI_EXTRAS:-}"
|
||||
NO_PROMPT="${PRAISONAI_NO_PROMPT:-0}"
|
||||
DRY_RUN="${PRAISONAI_DRY_RUN:-0}"
|
||||
PYTHON_CMD="${PRAISONAI_PYTHON:-}"
|
||||
SKIP_VENV="${PRAISONAI_SKIP_VENV:-0}"
|
||||
MIN_PYTHON_VERSION="3.10"
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}==>${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}✗${NC} $1" >&2
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}→${NC} $1"
|
||||
}
|
||||
|
||||
# Print banner
|
||||
print_banner() {
|
||||
echo -e "${BOLD}"
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ ${CYAN}PraisonAI${NC}${BOLD} - AI Agents Made Simple ║"
|
||||
echo "║ ║"
|
||||
echo "║ Works everywhere. Installs everything. You're welcome. ║"
|
||||
echo "║ ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
}
|
||||
|
||||
# Detect OS
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
OS="macos"
|
||||
;;
|
||||
Linux*)
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
OS="wsl"
|
||||
else
|
||||
OS="linux"
|
||||
fi
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
OS="windows"
|
||||
;;
|
||||
*)
|
||||
OS="unknown"
|
||||
;;
|
||||
esac
|
||||
echo "$OS"
|
||||
}
|
||||
|
||||
# Detect package manager
|
||||
detect_package_manager() {
|
||||
if command -v brew &>/dev/null; then
|
||||
echo "brew"
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
echo "apt"
|
||||
elif command -v dnf &>/dev/null; then
|
||||
echo "dnf"
|
||||
elif command -v yum &>/dev/null; then
|
||||
echo "yum"
|
||||
elif command -v pacman &>/dev/null; then
|
||||
echo "pacman"
|
||||
elif command -v apk &>/dev/null; then
|
||||
echo "apk"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Compare Python versions
|
||||
version_gte() {
|
||||
# Returns 0 if $1 >= $2
|
||||
printf '%s\n%s\n' "$2" "$1" | sort -V -C
|
||||
}
|
||||
|
||||
# Find suitable Python
|
||||
find_python() {
|
||||
local python_cmd=""
|
||||
|
||||
# Check user-specified Python first
|
||||
if [[ -n "$PYTHON_CMD" ]]; then
|
||||
if command_exists "$PYTHON_CMD"; then
|
||||
python_cmd="$PYTHON_CMD"
|
||||
else
|
||||
log_error "Specified Python not found: $PYTHON_CMD"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Try common Python commands
|
||||
for cmd in python3.12 python3.11 python3.10 python3 python; do
|
||||
if command_exists "$cmd"; then
|
||||
python_cmd="$cmd"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$python_cmd" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify version
|
||||
local version
|
||||
version=$("$python_cmd" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null)
|
||||
|
||||
if version_gte "$version" "$MIN_PYTHON_VERSION"; then
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install Python on macOS
|
||||
install_python_macos() {
|
||||
log_step "Installing Python via Homebrew..."
|
||||
|
||||
if ! command_exists brew; then
|
||||
log_step "Installing Homebrew first..."
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would install Homebrew"
|
||||
else
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# Add Homebrew to PATH for this session
|
||||
if [[ -f "/opt/homebrew/bin/brew" ]]; then
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
elif [[ -f "/usr/local/bin/brew" ]]; then
|
||||
eval "$(/usr/local/bin/brew shellenv)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: brew install [email protected]"
|
||||
else
|
||||
brew install [email protected] || brew upgrade [email protected] || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Install Python on Linux
|
||||
install_python_linux() {
|
||||
local pkg_manager
|
||||
pkg_manager=$(detect_package_manager)
|
||||
|
||||
log_step "Installing Python via $pkg_manager..."
|
||||
|
||||
case "$pkg_manager" in
|
||||
apt)
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: apt-get update && apt-get install -y python3 python3-pip python3-venv"
|
||||
else
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip python3-venv
|
||||
fi
|
||||
;;
|
||||
dnf)
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: dnf install -y python3 python3-pip"
|
||||
else
|
||||
sudo dnf install -y python3 python3-pip
|
||||
fi
|
||||
;;
|
||||
yum)
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: yum install -y python3 python3-pip"
|
||||
else
|
||||
sudo yum install -y python3 python3-pip
|
||||
fi
|
||||
;;
|
||||
pacman)
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: pacman -Sy --noconfirm python python-pip"
|
||||
else
|
||||
sudo pacman -Sy --noconfirm python python-pip
|
||||
fi
|
||||
;;
|
||||
apk)
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: apk add python3 py3-pip"
|
||||
else
|
||||
sudo apk add python3 py3-pip
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported package manager. Please install Python $MIN_PYTHON_VERSION+ manually."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Ensure Python is installed
|
||||
ensure_python() {
|
||||
log_info "Checking Python installation..."
|
||||
|
||||
local python_cmd
|
||||
if python_cmd=$(find_python); then
|
||||
local version
|
||||
version=$("$python_cmd" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')")
|
||||
log_success "Python $version found: $python_cmd"
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Python $MIN_PYTHON_VERSION+ not found. Installing..."
|
||||
|
||||
local os
|
||||
os=$(detect_os)
|
||||
|
||||
case "$os" in
|
||||
macos)
|
||||
install_python_macos
|
||||
;;
|
||||
linux|wsl)
|
||||
install_python_linux
|
||||
;;
|
||||
*)
|
||||
log_error "Cannot auto-install Python on this OS. Please install Python $MIN_PYTHON_VERSION+ manually."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Try to find Python again
|
||||
if python_cmd=$(find_python); then
|
||||
log_success "Python installed successfully"
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Failed to install Python. Please install Python $MIN_PYTHON_VERSION+ manually."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Ensure pip is available
|
||||
ensure_pip() {
|
||||
local python_cmd="$1"
|
||||
|
||||
log_info "Checking pip..."
|
||||
|
||||
if "$python_cmd" -m pip --version &>/dev/null; then
|
||||
log_success "pip is available"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_step "Installing pip..."
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would install pip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try ensurepip first
|
||||
if "$python_cmd" -m ensurepip --upgrade &>/dev/null; then
|
||||
log_success "pip installed via ensurepip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to get-pip.py
|
||||
curl -fsSL https://bootstrap.pypa.io/get-pip.py | "$python_cmd"
|
||||
log_success "pip installed via get-pip.py"
|
||||
}
|
||||
|
||||
# Create virtual environment
|
||||
create_venv() {
|
||||
local python_cmd="$1"
|
||||
local venv_dir="${2:-$HOME/.praisonai/venv}"
|
||||
|
||||
if [[ "$SKIP_VENV" == "1" ]]; then
|
||||
log_info "Skipping virtual environment (PRAISONAI_SKIP_VENV=1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating virtual environment..."
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would create venv at: $venv_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$venv_dir")"
|
||||
|
||||
if [[ -d "$venv_dir" ]]; then
|
||||
log_step "Virtual environment already exists at $venv_dir"
|
||||
else
|
||||
"$python_cmd" -m venv "$venv_dir"
|
||||
log_success "Virtual environment created at $venv_dir"
|
||||
fi
|
||||
|
||||
echo "$venv_dir"
|
||||
}
|
||||
|
||||
# Install PraisonAI
|
||||
install_praisonai() {
|
||||
local python_cmd="$1"
|
||||
local venv_dir="$2"
|
||||
|
||||
log_info "Installing PraisonAI..."
|
||||
|
||||
local pip_cmd="$python_cmd -m pip"
|
||||
|
||||
# Use venv pip if available
|
||||
if [[ -n "$venv_dir" && -f "$venv_dir/bin/pip" ]]; then
|
||||
pip_cmd="$venv_dir/bin/pip"
|
||||
elif [[ -n "$venv_dir" && -f "$venv_dir/Scripts/pip.exe" ]]; then
|
||||
pip_cmd="$venv_dir/Scripts/pip.exe"
|
||||
fi
|
||||
|
||||
# Build install command
|
||||
local install_pkg="praisonaiagents"
|
||||
if [[ "$VERSION" != "latest" ]]; then
|
||||
install_pkg="praisonaiagents==$VERSION"
|
||||
fi
|
||||
|
||||
# Add extras if specified
|
||||
if [[ -n "$EXTRAS" ]]; then
|
||||
install_pkg="praisonaiagents[$EXTRAS]"
|
||||
if [[ "$VERSION" != "latest" ]]; then
|
||||
install_pkg="praisonaiagents[$EXTRAS]==$VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would run: $pip_cmd install --upgrade $install_pkg"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Upgrade pip first
|
||||
$pip_cmd install --upgrade pip
|
||||
|
||||
# Install PraisonAI
|
||||
$pip_cmd install --upgrade "$install_pkg"
|
||||
|
||||
# Also install the wrapper if extras include ui/chat/code
|
||||
if [[ "$EXTRAS" == *"ui"* ]] || [[ "$EXTRAS" == *"chat"* ]] || [[ "$EXTRAS" == *"code"* ]]; then
|
||||
local wrapper_pkg="praisonai[$EXTRAS]"
|
||||
if [[ "$VERSION" != "latest" ]]; then
|
||||
wrapper_pkg="praisonai[$EXTRAS]==$VERSION"
|
||||
fi
|
||||
$pip_cmd install --upgrade "$wrapper_pkg"
|
||||
fi
|
||||
|
||||
log_success "PraisonAI installed successfully!"
|
||||
}
|
||||
|
||||
# Setup shell PATH
|
||||
setup_shell_path() {
|
||||
local venv_dir="$1"
|
||||
|
||||
if [[ "$SKIP_VENV" == "1" ]] || [[ -z "$venv_dir" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up shell PATH..."
|
||||
|
||||
local bin_dir="$venv_dir/bin"
|
||||
if [[ ! -d "$bin_dir" ]]; then
|
||||
bin_dir="$venv_dir/Scripts"
|
||||
fi
|
||||
|
||||
local shell_rc=""
|
||||
case "$SHELL" in
|
||||
*/zsh)
|
||||
shell_rc="$HOME/.zshrc"
|
||||
;;
|
||||
*/bash)
|
||||
if [[ -f "$HOME/.bash_profile" ]]; then
|
||||
shell_rc="$HOME/.bash_profile"
|
||||
else
|
||||
shell_rc="$HOME/.bashrc"
|
||||
fi
|
||||
;;
|
||||
*/fish)
|
||||
shell_rc="$HOME/.config/fish/config.fish"
|
||||
;;
|
||||
*)
|
||||
shell_rc="$HOME/.profile"
|
||||
;;
|
||||
esac
|
||||
|
||||
local path_line="export PATH=\"$bin_dir:\$PATH\""
|
||||
if [[ "$SHELL" == */fish ]]; then
|
||||
path_line="set -gx PATH $bin_dir \$PATH"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would add to $shell_rc: $path_line"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if already in rc file
|
||||
if grep -q "$bin_dir" "$shell_rc" 2>/dev/null; then
|
||||
log_step "PATH already configured in $shell_rc"
|
||||
else
|
||||
echo "" >> "$shell_rc"
|
||||
echo "# PraisonAI" >> "$shell_rc"
|
||||
echo "$path_line" >> "$shell_rc"
|
||||
log_success "Added PraisonAI to PATH in $shell_rc"
|
||||
fi
|
||||
|
||||
# Export for current session
|
||||
export PATH="$bin_dir:$PATH"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_installation() {
|
||||
local venv_dir="$1"
|
||||
|
||||
log_info "Verifying installation..."
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_info "[DRY RUN] Would verify installation"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local python_cmd="python3"
|
||||
if [[ -n "$venv_dir" && -f "$venv_dir/bin/python" ]]; then
|
||||
python_cmd="$venv_dir/bin/python"
|
||||
elif [[ -n "$venv_dir" && -f "$venv_dir/Scripts/python.exe" ]]; then
|
||||
python_cmd="$venv_dir/Scripts/python.exe"
|
||||
fi
|
||||
|
||||
# Check import
|
||||
if $python_cmd -c "import praisonaiagents; print(f'Version: {praisonaiagents.__version__}')" 2>/dev/null; then
|
||||
log_success "PraisonAI agents package verified"
|
||||
else
|
||||
log_error "Failed to import praisonaiagents"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check CLI if wrapper installed
|
||||
if command_exists praisonai; then
|
||||
local cli_version
|
||||
cli_version=$(praisonai --version 2>/dev/null | head -n1 || echo "unknown")
|
||||
log_success "CLI available: praisonai $cli_version"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print next steps
|
||||
print_next_steps() {
|
||||
local venv_dir="$1"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}Installation complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${BOLD}Next steps:${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ -n "$venv_dir" ]] && [[ "$SKIP_VENV" != "1" ]]; then
|
||||
echo " 1. Activate the virtual environment:"
|
||||
if [[ "$SHELL" == */fish ]]; then
|
||||
echo -e " ${CYAN}source $venv_dir/bin/activate.fish${NC}"
|
||||
else
|
||||
echo -e " ${CYAN}source $venv_dir/bin/activate${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo " 2. Or restart your terminal to use PraisonAI globally"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo " Quick start:"
|
||||
echo -e " ${CYAN}python -c \"from praisonaiagents import Agent; Agent(name='test').start('Hello!')\"${NC}"
|
||||
echo ""
|
||||
echo " Documentation:"
|
||||
echo -e " ${CYAN}https://docs.praison.ai${NC}"
|
||||
echo ""
|
||||
echo " Set your API key:"
|
||||
echo -e " ${CYAN}export OPENAI_API_KEY=your_key${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Print help
|
||||
print_help() {
|
||||
echo "PraisonAI Installer"
|
||||
echo ""
|
||||
echo "Usage: curl -fsSL https://praison.ai/install.sh | bash"
|
||||
echo " curl -fsSL https://praison.ai/install.sh | bash -s -- [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --version VERSION Install specific version (default: latest)"
|
||||
echo " --extras EXTRAS Install with extras (e.g., ui,chat,code)"
|
||||
echo " --no-venv Skip virtual environment creation"
|
||||
echo " --python PATH Use specific Python executable"
|
||||
echo " --dry-run Print what would happen without making changes"
|
||||
echo " --no-prompt Skip interactive prompts"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " PRAISONAI_VERSION Specific version to install"
|
||||
echo " PRAISONAI_EXTRAS Comma-separated extras"
|
||||
echo " PRAISONAI_NO_PROMPT Skip interactive prompts (1 to enable)"
|
||||
echo " PRAISONAI_DRY_RUN Print what would happen (1 to enable)"
|
||||
echo " PRAISONAI_PYTHON Path to Python executable"
|
||||
echo " PRAISONAI_SKIP_VENV Skip virtual environment (1 to enable)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Basic install"
|
||||
echo " curl -fsSL https://praison.ai/install.sh | bash"
|
||||
echo ""
|
||||
echo " # Install with UI extras"
|
||||
echo " curl -fsSL https://praison.ai/install.sh | bash -s -- --extras ui,chat"
|
||||
echo ""
|
||||
echo " # Install specific version"
|
||||
echo " curl -fsSL https://praison.ai/install.sh | bash -s -- --version 0.14.0"
|
||||
echo ""
|
||||
echo " # Dry run"
|
||||
echo " curl -fsSL https://praison.ai/install.sh | bash -s -- --dry-run"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--extras)
|
||||
EXTRAS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-venv)
|
||||
SKIP_VENV="1"
|
||||
shift
|
||||
;;
|
||||
--python)
|
||||
PYTHON_CMD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN="1"
|
||||
shift
|
||||
;;
|
||||
--no-prompt)
|
||||
NO_PROMPT="1"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
print_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
print_banner
|
||||
|
||||
local os
|
||||
os=$(detect_os)
|
||||
log_info "Detected OS: $os"
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
log_warn "Running in DRY RUN mode - no changes will be made"
|
||||
fi
|
||||
|
||||
# Ensure Python
|
||||
local python_cmd
|
||||
python_cmd=$(ensure_python)
|
||||
|
||||
# Ensure pip
|
||||
ensure_pip "$python_cmd"
|
||||
|
||||
# Create virtual environment
|
||||
local venv_dir=""
|
||||
if [[ "$SKIP_VENV" != "1" ]]; then
|
||||
venv_dir=$(create_venv "$python_cmd")
|
||||
fi
|
||||
|
||||
# Install PraisonAI
|
||||
install_praisonai "$python_cmd" "$venv_dir"
|
||||
|
||||
# Setup PATH
|
||||
setup_shell_path "$venv_dir"
|
||||
|
||||
# Verify installation
|
||||
verify_installation "$venv_dir"
|
||||
|
||||
# Print next steps
|
||||
print_next_steps "$venv_dir"
|
||||
}
|
||||
|
||||
# Run main
|
||||
main "$@"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test the PraisonAI installer in Docker
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SMOKE_IMAGE="${PRAISONAI_SMOKE_IMAGE:-praisonai-install-smoke:local}"
|
||||
INSTALL_URL="${PRAISONAI_INSTALL_URL:-file:///install.sh}"
|
||||
|
||||
echo "==> Build smoke test image: $SMOKE_IMAGE"
|
||||
docker build \
|
||||
-t "$SMOKE_IMAGE" \
|
||||
-f "$ROOT_DIR/scripts/docker/install-smoke/Dockerfile" \
|
||||
"$ROOT_DIR/scripts/docker/install-smoke"
|
||||
|
||||
echo "==> Run installer smoke test"
|
||||
docker run --rm -t \
|
||||
-v "$ROOT_DIR/scripts/install.sh:/install.sh:ro" \
|
||||
-e PRAISONAI_INSTALL_URL="$INSTALL_URL" \
|
||||
-e PRAISONAI_EXPECTED_VERSION="${PRAISONAI_EXPECTED_VERSION:-}" \
|
||||
-e PRAISONAI_EXTRAS="${PRAISONAI_EXTRAS:-}" \
|
||||
"$SMOKE_IMAGE"
|
||||
|
||||
echo "==> Smoke test completed successfully!"
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Unit tests for audio tools (TTS/STT).
|
||||
|
||||
Tests the wrapper-level audio tools that use the core AudioAgent.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
class TestTTSTool:
|
||||
"""Test TTS tool functionality."""
|
||||
|
||||
def test_tts_tool_success(self):
|
||||
"""Test successful TTS conversion."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import tts_tool
|
||||
|
||||
# Create a temp file to simulate output
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
# Mock speech to create the file
|
||||
def mock_speech(text, output, **kwargs):
|
||||
with open(output, 'w') as f:
|
||||
f.write('fake audio')
|
||||
|
||||
mock_agent.speech.side_effect = mock_speech
|
||||
|
||||
result = tts_tool("Hello world", output_dir=os.path.dirname(temp_path))
|
||||
|
||||
assert result["success"] is True
|
||||
assert "audio_path" in result
|
||||
assert result["media_line"].startswith("MEDIA:")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_tts_tool_with_voice(self):
|
||||
"""Test TTS with custom voice."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import tts_tool
|
||||
|
||||
# Mock speech to create a file
|
||||
def mock_speech(text, output, **kwargs):
|
||||
with open(output, 'w') as f:
|
||||
f.write('fake audio')
|
||||
|
||||
mock_agent.speech.side_effect = mock_speech
|
||||
|
||||
result = tts_tool("Hello", voice="echo")
|
||||
|
||||
# Verify voice was passed
|
||||
mock_agent.speech.assert_called_once()
|
||||
call_kwargs = mock_agent.speech.call_args[1]
|
||||
assert call_kwargs.get("voice") == "echo"
|
||||
|
||||
def test_tts_tool_error_handling(self):
|
||||
"""Test TTS error handling."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.speech.side_effect = Exception("API error")
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import tts_tool
|
||||
|
||||
result = tts_tool("Hello world")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "error" in result
|
||||
assert "API error" in result["error"]
|
||||
|
||||
def test_tts_tool_opus_voice_compatible(self):
|
||||
"""Test that opus format is marked as voice compatible."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import tts_tool
|
||||
|
||||
def mock_speech(text, output, **kwargs):
|
||||
with open(output, 'w') as f:
|
||||
f.write('fake audio')
|
||||
|
||||
mock_agent.speech.side_effect = mock_speech
|
||||
|
||||
result = tts_tool("Hello", output_format="opus")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["voice_compatible"] is True
|
||||
|
||||
|
||||
class TestSTTTool:
|
||||
"""Test STT tool functionality."""
|
||||
|
||||
def test_stt_tool_success(self):
|
||||
"""Test successful STT transcription."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.transcribe.return_value = "Hello world"
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import stt_tool
|
||||
|
||||
# Create a temp audio file
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
|
||||
f.write(b'fake audio data')
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = stt_tool(temp_path)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["text"] == "Hello world"
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_stt_tool_file_not_found(self):
|
||||
"""Test STT with non-existent file."""
|
||||
from praisonai.tools.audio import stt_tool
|
||||
|
||||
result = stt_tool("/nonexistent/path/audio.mp3")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["error"]
|
||||
|
||||
def test_stt_tool_with_language(self):
|
||||
"""Test STT with language parameter."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.transcribe.return_value = "Hola mundo"
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import stt_tool
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
|
||||
f.write(b'fake audio')
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = stt_tool(temp_path, language="es")
|
||||
|
||||
# Verify language was passed
|
||||
mock_agent.transcribe.assert_called_once()
|
||||
call_kwargs = mock_agent.transcribe.call_args[1]
|
||||
assert call_kwargs.get("language") == "es"
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_stt_tool_error_handling(self):
|
||||
"""Test STT error handling."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent') as mock_get_agent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.transcribe.side_effect = Exception("Transcription failed")
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
from praisonai.tools.audio import stt_tool
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
|
||||
f.write(b'fake audio')
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = stt_tool(temp_path)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Transcription failed" in result["error"]
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
|
||||
class TestCreateToolFunctions:
|
||||
"""Test the create_*_tool factory functions."""
|
||||
|
||||
def test_create_tts_tool(self):
|
||||
"""Test create_tts_tool returns a callable."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent'):
|
||||
from praisonai.tools.audio import create_tts_tool
|
||||
|
||||
tool = create_tts_tool()
|
||||
|
||||
assert callable(tool)
|
||||
assert hasattr(tool, '__name__') or hasattr(tool, 'name')
|
||||
|
||||
def test_create_stt_tool(self):
|
||||
"""Test create_stt_tool returns a callable."""
|
||||
with patch('praisonai.tools.audio._get_audio_agent'):
|
||||
from praisonai.tools.audio import create_stt_tool
|
||||
|
||||
tool = create_stt_tool()
|
||||
|
||||
assert callable(tool)
|
||||
assert hasattr(tool, '__name__') or hasattr(tool, 'name')
|
||||
|
||||
|
||||
class TestBotCapabilitiesTTS:
|
||||
"""Test TTS/STT in BotCapabilities."""
|
||||
|
||||
def test_bot_capabilities_tts_fields(self):
|
||||
"""Test BotCapabilities has TTS/STT fields."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities(
|
||||
tts=True,
|
||||
tts_voice="echo",
|
||||
tts_model="openai/tts-1-hd",
|
||||
auto_tts=True,
|
||||
stt=True,
|
||||
stt_model="openai/whisper-1",
|
||||
)
|
||||
|
||||
assert caps.tts is True
|
||||
assert caps.tts_voice == "echo"
|
||||
assert caps.tts_model == "openai/tts-1-hd"
|
||||
assert caps.auto_tts is True
|
||||
assert caps.stt is True
|
||||
assert caps.stt_model == "openai/whisper-1"
|
||||
|
||||
def test_bot_capabilities_to_dict_includes_tts(self):
|
||||
"""Test to_dict includes TTS/STT fields."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities(tts=True, stt=True)
|
||||
d = caps.to_dict()
|
||||
|
||||
assert "tts" in d
|
||||
assert "stt" in d
|
||||
assert "auto_tts" in d
|
||||
assert d["tts"] is True
|
||||
assert d["stt"] is True
|
||||
|
||||
def test_build_capabilities_from_args_tts(self):
|
||||
"""Test _build_capabilities_from_args includes TTS/STT."""
|
||||
from praisonai.cli.features.bots_cli import _build_capabilities_from_args
|
||||
from argparse import Namespace
|
||||
|
||||
args = Namespace(
|
||||
browser=False,
|
||||
browser_profile="default",
|
||||
browser_headless=False,
|
||||
tools=[],
|
||||
skills=[],
|
||||
skills_dir=None,
|
||||
memory=False,
|
||||
memory_provider="default",
|
||||
knowledge=False,
|
||||
knowledge_sources=[],
|
||||
web_search=False,
|
||||
web_provider="duckduckgo",
|
||||
sandbox=False,
|
||||
exec_enabled=False,
|
||||
auto_approve=False,
|
||||
model=None,
|
||||
thinking=None,
|
||||
tts=True,
|
||||
tts_voice="nova",
|
||||
tts_model="openai/tts-1-hd",
|
||||
auto_tts=True,
|
||||
stt=True,
|
||||
stt_model="openai/whisper-1",
|
||||
session_id=None,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
caps = _build_capabilities_from_args(args)
|
||||
|
||||
assert caps.tts is True
|
||||
assert caps.tts_voice == "nova"
|
||||
assert caps.tts_model == "openai/tts-1-hd"
|
||||
assert caps.auto_tts is True
|
||||
assert caps.stt is True
|
||||
assert caps.stt_model == "openai/whisper-1"
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Unit tests for bot CLI capabilities wiring.
|
||||
|
||||
Tests that BotCapabilities are correctly passed to Agent constructor.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
|
||||
class TestBotCapabilitiesWiring:
|
||||
"""Test that bot capabilities are wired to Agent constructor."""
|
||||
|
||||
def test_get_agent_kwargs_memory(self):
|
||||
"""Test that --memory flag is converted to Agent kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(memory=True)
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("memory") is True
|
||||
|
||||
def test_get_agent_kwargs_knowledge(self):
|
||||
"""Test that --knowledge flag with sources is converted to Agent kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(
|
||||
knowledge=True,
|
||||
knowledge_sources=["docs.pdf", "data.txt"]
|
||||
)
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("knowledge") == ["docs.pdf", "data.txt"]
|
||||
|
||||
def test_get_agent_kwargs_knowledge_no_sources(self):
|
||||
"""Test that --knowledge flag without sources sets knowledge=True."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(knowledge=True)
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("knowledge") is True
|
||||
|
||||
def test_get_agent_kwargs_skills(self):
|
||||
"""Test that --skills flag is converted to Agent kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(skills=["web_search", "code_exec"])
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("skills") == ["web_search", "code_exec"]
|
||||
|
||||
def test_get_agent_kwargs_thinking_medium(self):
|
||||
"""Test that --thinking medium enables reflection."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(thinking="medium")
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("reflection") is True
|
||||
|
||||
def test_get_agent_kwargs_thinking_high(self):
|
||||
"""Test that --thinking high enables reflection."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(thinking="high")
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("reflection") is True
|
||||
|
||||
def test_get_agent_kwargs_thinking_low(self):
|
||||
"""Test that --thinking low does NOT enable reflection."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(thinking="low")
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert "reflection" not in kwargs
|
||||
|
||||
def test_get_agent_kwargs_model(self):
|
||||
"""Test that --model is converted to Agent kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(model="gpt-4o")
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs.get("llm") == "gpt-4o"
|
||||
|
||||
def test_get_agent_kwargs_empty_capabilities(self):
|
||||
"""Test that empty capabilities returns empty kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities()
|
||||
|
||||
kwargs = handler._get_agent_kwargs(capabilities)
|
||||
|
||||
assert kwargs == {}
|
||||
|
||||
def test_get_agent_kwargs_none_capabilities(self):
|
||||
"""Test that None capabilities returns empty kwargs."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler
|
||||
|
||||
handler = BotHandler()
|
||||
|
||||
kwargs = handler._get_agent_kwargs(None)
|
||||
|
||||
assert kwargs == {}
|
||||
|
||||
@patch('praisonaiagents.Agent')
|
||||
def test_load_agent_passes_capabilities(self, mock_agent_class):
|
||||
"""Test that _load_agent passes capabilities to Agent constructor."""
|
||||
from praisonai.cli.features.bots_cli import BotHandler, BotCapabilities
|
||||
|
||||
mock_agent = Mock()
|
||||
mock_agent_class.return_value = mock_agent
|
||||
|
||||
handler = BotHandler()
|
||||
capabilities = BotCapabilities(
|
||||
memory=True,
|
||||
knowledge=True,
|
||||
knowledge_sources=["docs.pdf"],
|
||||
skills=["web_search"],
|
||||
thinking="medium",
|
||||
model="gpt-4o",
|
||||
)
|
||||
|
||||
# Call _load_agent without a file (uses default agent)
|
||||
with patch.object(handler, '_build_tools', return_value=[]):
|
||||
_ = handler._load_agent(None, capabilities)
|
||||
|
||||
# Verify Agent was called with the right kwargs
|
||||
call_kwargs = mock_agent_class.call_args[1]
|
||||
|
||||
assert call_kwargs.get("memory") is True
|
||||
assert call_kwargs.get("knowledge") == ["docs.pdf"]
|
||||
assert call_kwargs.get("skills") == ["web_search"]
|
||||
assert call_kwargs.get("reflection") is True
|
||||
assert call_kwargs.get("llm") == "gpt-4o"
|
||||
|
||||
|
||||
class TestBotCapabilitiesDataclass:
|
||||
"""Test BotCapabilities dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default values are correct."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities()
|
||||
|
||||
assert caps.memory is False
|
||||
assert caps.knowledge is False
|
||||
assert caps.knowledge_sources == []
|
||||
assert caps.skills == []
|
||||
assert caps.thinking is None
|
||||
assert caps.model is None
|
||||
assert caps.sandbox is False
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test to_dict method."""
|
||||
from praisonai.cli.features.bots_cli import BotCapabilities
|
||||
|
||||
caps = BotCapabilities(memory=True, model="gpt-4o")
|
||||
d = caps.to_dict()
|
||||
|
||||
assert d["memory"] is True
|
||||
assert d["model"] == "gpt-4o"
|
||||
|
||||
|
||||
class TestBuildCapabilitiesFromArgs:
|
||||
"""Test _build_capabilities_from_args function."""
|
||||
|
||||
def test_builds_from_args(self):
|
||||
"""Test building capabilities from argparse namespace."""
|
||||
from praisonai.cli.features.bots_cli import _build_capabilities_from_args
|
||||
from argparse import Namespace
|
||||
|
||||
args = Namespace(
|
||||
memory=True,
|
||||
knowledge=True,
|
||||
knowledge_sources=["docs.pdf"],
|
||||
skills=["web_search"],
|
||||
thinking="medium",
|
||||
model="gpt-4o",
|
||||
browser=False,
|
||||
browser_profile="default",
|
||||
browser_headless=False,
|
||||
tools=[],
|
||||
skills_dir=None,
|
||||
memory_provider="default",
|
||||
web_search=False,
|
||||
web_provider="duckduckgo",
|
||||
sandbox=False,
|
||||
exec_enabled=False,
|
||||
auto_approve=False,
|
||||
session_id=None,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
caps = _build_capabilities_from_args(args)
|
||||
|
||||
assert caps.memory is True
|
||||
assert caps.knowledge is True
|
||||
assert caps.knowledge_sources == ["docs.pdf"]
|
||||
assert caps.skills == ["web_search"]
|
||||
assert caps.thinking == "medium"
|
||||
assert caps.model == "gpt-4o"
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Unit tests for bot media parsing utilities.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
|
||||
from praisonai.bots.media import split_media_from_output, is_audio_file
|
||||
|
||||
|
||||
class TestSplitMediaFromOutput:
|
||||
"""Tests for split_media_from_output function."""
|
||||
|
||||
def test_empty_text(self):
|
||||
"""Empty text returns empty result."""
|
||||
result = split_media_from_output("")
|
||||
assert result["text"] == ""
|
||||
assert result["media_urls"] == []
|
||||
assert result["audio_as_voice"] is False
|
||||
|
||||
def test_text_only(self):
|
||||
"""Text without MEDIA: is preserved."""
|
||||
result = split_media_from_output("Hello world!")
|
||||
assert result["text"] == "Hello world!"
|
||||
assert result["media_urls"] == []
|
||||
assert result["audio_as_voice"] is False
|
||||
|
||||
def test_media_with_existing_file(self):
|
||||
"""MEDIA: with existing file is extracted."""
|
||||
# Create temp file
|
||||
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = split_media_from_output(f"Hello\nMEDIA:{temp_path}\nWorld")
|
||||
assert "Hello" in result["text"]
|
||||
assert "World" in result["text"]
|
||||
assert temp_path in result["media_urls"]
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_voice_tag_detection(self):
|
||||
"""[[audio_as_voice]] tag is detected and removed."""
|
||||
result = split_media_from_output("[[audio_as_voice]] Hello!")
|
||||
assert result["audio_as_voice"] is True
|
||||
assert "[[audio_as_voice]]" not in result["text"]
|
||||
assert "Hello" in result["text"]
|
||||
|
||||
def test_multiple_media_urls(self):
|
||||
"""Multiple MEDIA: lines are all extracted."""
|
||||
# Create temp files
|
||||
temps = []
|
||||
for i in range(2):
|
||||
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
||||
temps.append(f.name)
|
||||
|
||||
try:
|
||||
text = f"Start\nMEDIA:{temps[0]}\nMiddle\nMEDIA:{temps[1]}\nEnd"
|
||||
result = split_media_from_output(text)
|
||||
assert len(result["media_urls"]) == 2
|
||||
assert temps[0] in result["media_urls"]
|
||||
assert temps[1] in result["media_urls"]
|
||||
finally:
|
||||
for t in temps:
|
||||
os.unlink(t)
|
||||
|
||||
def test_http_url(self):
|
||||
"""HTTP URLs are accepted as media."""
|
||||
result = split_media_from_output("MEDIA:https://example.com/audio.mp3")
|
||||
assert "https://example.com/audio.mp3" in result["media_urls"]
|
||||
|
||||
|
||||
class TestIsAudioFile:
|
||||
"""Tests for is_audio_file function."""
|
||||
|
||||
def test_audio_extensions(self):
|
||||
"""Audio extensions are recognized."""
|
||||
assert is_audio_file("/path/to/file.mp3") is True
|
||||
assert is_audio_file("/path/to/file.wav") is True
|
||||
assert is_audio_file("/path/to/file.ogg") is True
|
||||
assert is_audio_file("/path/to/file.opus") is True
|
||||
assert is_audio_file("/path/to/file.m4a") is True
|
||||
|
||||
def test_non_audio_extensions(self):
|
||||
"""Non-audio extensions are rejected."""
|
||||
assert is_audio_file("/path/to/file.txt") is False
|
||||
assert is_audio_file("/path/to/file.pdf") is False
|
||||
assert is_audio_file("/path/to/file.png") is False
|
||||
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.14.2" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.15.2" },
|
||||
{ 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.14.2"
|
||||
version = "0.15.2"
|
||||
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/d3/15/206a9f99edad0e89f0be2d438d5c545e829971cc119d137eec74d87aae10/praisonaiagents-0.14.2.tar.gz", hash = "sha256:416efe7a9bfa13edd502bedf6046ceb50f790c0954dd6a56fff99716831fcddc", size = 1152873, upload-time = "2026-01-29T13:38:54.142Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/d8/6d815f3dda3295c7f59e1ddf5e9057fd2ee608fce3c816a49e176b66f73d/praisonaiagents-0.15.2.tar.gz", hash = "sha256:fc3b36108a03d047bf913fbca8a6987125506948668e91cfd3f34cc87a541f92", size = 1160924, upload-time = "2026-01-30T10:21:04.375Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/20/8efd51b9a4700d644df858bca3d80aabc7f4511d9000acb110cd08304bf3/praisonaiagents-0.14.2-py3-none-any.whl", hash = "sha256:12f4a339fab6648afddcc4acb1cfcd1cc0e99bbeead3d0ddccfb886b9cfe6a46", size = 1140931, upload-time = "2026-01-29T13:38:51.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/82/9d842def26267ed0be7c9d17adc43a5624909610a6f0add79c06e852ac84/praisonaiagents-0.15.2-py3-none-any.whl", hash = "sha256:19b1b80c79182264b4ed0490ee72d418fa6003375c16b1ac81c52f6ad8bf2b69", size = 1149931, upload-time = "2026-01-30T10:21:02.508Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user