diff --git a/docs.json b/docs.json index 89a03e6b..d1312489 100644 --- a/docs.json +++ b/docs.json @@ -288,6 +288,7 @@ "docs/features/bot-commands", "docs/features/bot-gateway", "docs/features/bot-routing", + "docs/features/bot-pairing", "docs/features/botos", "docs/features/push-notifications" ] @@ -649,6 +650,8 @@ "pages": [ "docs/features/cli", "docs/features/async-agent-scheduler", + "docs/features/clarify-tool", + "docs/features/tool-availability", "docs/features/hooks", "docs/features/hook-events", "docs/features/dynamic-variables", diff --git a/docs/best-practices/bot-security.mdx b/docs/best-practices/bot-security.mdx index 55bbad8f..a33d46fe 100644 --- a/docs/best-practices/bot-security.mdx +++ b/docs/best-practices/bot-security.mdx @@ -34,12 +34,17 @@ graph LR ```python from praisonaiagents import Agent +from praisonaiagents.bots import BotConfig # Secure bot with allowlist +config = BotConfig( + allowed_users=["@your_username", "123456789"], + unknown_user_policy="deny" # Default secure behavior +) + agent = Agent( instructions="You are a helpful assistant", - # Note: Security features shown are conceptual - # Actual implementation may vary + # Bot configuration handled by adapter ) ``` @@ -49,12 +54,19 @@ agent = Agent( ```python from praisonaiagents import Agent +from praisonaiagents.bots import BotConfig + +# Production security setup with pairing +config = BotConfig( + allowed_users=["@admin_user"], + unknown_user_policy="pair", # Secure pairing flow + auto_approve_tools=True, # For bot environments + group_policy="mention_only" # Only respond when mentioned +) -# Production security setup agent = Agent( instructions="Secure production assistant", - # Security configuration would go here - # when implemented in the SDK + # Configure with your bot adapter ) ``` @@ -214,11 +226,7 @@ WhatsApp has the **strongest security defaults** and serves as the reference imp ## Gateway Pairing - -**Note:** The pairing system described below represents planned functionality. Current SDK implementation may differ. Verify against actual SDK documentation. - - -For production deployments, use **gateway pairing** to authorize channels dynamically: +For production deployments, use **gateway pairing** to authorize channels dynamically with the shipped pairing system: ### 1. Set Gateway Secret @@ -226,54 +234,55 @@ For production deployments, use **gateway pairing** to authorize channels dynami export PRAISONAI_GATEWAY_SECRET="your-secure-secret-key" ``` - -Without `PRAISONAI_GATEWAY_SECRET`, pairing codes will **not persist across restarts**. Set this in production. - + +The gateway secret is optional - if unset, a per-install secret is auto-generated at `/.gateway_secret` with `0600` permissions and reused across restarts. + -### 2. Generate Pairing Code +### 2. Enable Pairing Policy ```python -# Note: This API is conceptual - verify implementation -from praisonaiagents.gateway.pairing import PairingStore +from praisonaiagents.bots import BotConfig + +config = BotConfig( + allowed_users=["@owner"], + unknown_user_policy="pair" # Enable pairing for unknown users +) -store = PairingStore() -code = store.generate_code(channel_type="telegram") -print(f"Pairing code: {code}") # 8-character hex code +# Unknown users will automatically receive pairing codes when they DM the bot ``` -### 3. Verify in Channel +### 3. Approve Pairing Requests -Send the code to your bot in the target channel: +When unknown users DM your bot, they receive pairing codes. Approve them via CLI: +```bash +# User receives: "Your pairing code: ABCD1234" +# Owner approves: +praisonai pairing approve telegram ABCD1234 --label "alice" ``` -/pair abc12345 -``` -The bot will verify the HMAC signature and authorize the channel. +### 4. Manage Pairings + +```bash +# List all paired channels +praisonai pairing list -### 4. Check Status +# Revoke access for specific channel +praisonai pairing revoke telegram 987654321 -```python -# Check if channel is paired -# Note: Verify this API exists in current SDK -paired = store.is_paired("@username", "telegram") -print(f"Channel paired: {paired}") - -# List all paired channels -for channel in store.list_paired(): - print(f"{channel.channel_type}: {channel.channel_id}") +# Clear all pairings +praisonai pairing clear --confirm ``` -## Doctor Security Check + +For detailed pairing documentation, see the [Bot Pairing](/docs/features/bot-pairing) guide. + - -**Note:** The doctor command shown may not be available in current SDK version. Verify implementation status. - +## Doctor Security Check Use the built-in doctor to audit your bot security configuration: ```bash -# Note: Verify this command exists praisonai doctor --category bots ``` diff --git a/docs/features/bot-pairing.mdx b/docs/features/bot-pairing.mdx new file mode 100644 index 00000000..c31c1006 --- /dev/null +++ b/docs/features/bot-pairing.mdx @@ -0,0 +1,415 @@ +--- +title: "Bot Pairing" +sidebarTitle: "Bot Pairing" +description: "Secure unknown user onboarding with CLI-approved pairing codes" +icon: "handshake" +--- + +Bot pairing lets unknown users self-request access to your bot with secure 8-character codes that you approve from the CLI. + +```mermaid +graph LR + subgraph "Pairing Flow" + A[👤 Unknown User] --> B[🤖 Bot DM] + B --> C[🔐 Generate Code] + C --> D[📱 Send Code] + D --> E[💻 CLI Approve] + E --> F[✅ User Paired] + end + + classDef user fill:#8B0000,stroke:#7C90A0,color:#fff + classDef bot fill:#189AB4,stroke:#7C90A0,color:#fff + classDef code fill:#F59E0B,stroke:#7C90A0,color:#fff + classDef cli fill:#6366F1,stroke:#7C90A0,color:#fff + classDef success fill:#10B981,stroke:#7C90A0,color:#fff + + class A user + class B bot + class C,D code + class E cli + class F success +``` + +## Quick Start + + + + +```python +from praisonaiagents import Agent +from praisonaiagents.bots import BotConfig + +config = BotConfig( + allowed_users=["@owner"], + unknown_user_policy="pair", # Enable pairing for unknown users +) + +# Bot setup with Telegram adapter (other platforms pending) +# from praisonai.bots.telegram import TelegramAdapter +# bot = TelegramAdapter(config) +``` + + + + + +```bash +# List pending pairing requests +praisonai pairing list + +# Approve a user's pairing code +praisonai pairing approve telegram ABCD1234 --label "alice" + +# View all paired channels +praisonai pairing list +``` + + + + +--- + +## How It Works + +```mermaid +sequenceDiagram + participant User + participant Bot + participant Handler + participant PairingStore + participant Owner + + User->>Bot: DM message + Bot->>Handler: Check if user allowed + Handler->>PairingStore: is_paired(channel_id)? + PairingStore-->>Handler: No + Handler->>PairingStore: generate_code(telegram, channel_id) + PairingStore-->>Handler: ABCD1234 + Handler->>Bot: Send pairing instructions + Bot->>User: "Code: ABCD1234" + + Note over Owner: Receives notification + Owner->>PairingStore: praisonai pairing approve telegram ABCD1234 + PairingStore->>PairingStore: Mark channel as paired + + User->>Bot: Next DM + Bot->>Handler: Check if user allowed + Handler->>PairingStore: is_paired(channel_id)? + PairingStore-->>Handler: Yes + Handler-->>Bot: Allow message + Bot->>User: Normal response +``` + +--- + +## Policy Configuration + +### Unknown User Policies + +```python +from praisonaiagents.bots import BotConfig + +# Policy options +config = BotConfig( + allowed_users=["@owner", "123456789"], + unknown_user_policy="pair" # Choose one below +) +``` + +| Policy | Behavior | Use Case | +|--------|----------|----------| +| `"deny"` | Silently drop messages (default) | Private bots, testing | +| `"allow"` | Allow all users (overrides allowed_users) | Public bots | +| `"pair"` | Use pairing flow for approval | Controlled access | + +### Pairing Rate Limiting + +The system includes built-in protection against code generation spam: + +- **Rate Limit**: 10 minutes between code generations per channel +- **Code TTL**: Codes expire after a configurable time (default: check `PaisingStore` implementation) +- **Automatic Cleanup**: Stale rate limit entries are automatically evicted + +--- + +## CLI Commands + +### List Commands + +```bash +# List all paired channels +praisonai pairing list + +# Example output: +# Found 3 paired channels: +# +# Platform: telegram +# Channel: @alice_username +# Paired: 2026-04-22 10:30:15 +# Label: alice +# +# Platform: telegram +# Channel: 987654321 +# Paired: 2026-04-22 11:45:22 +# Label: bob +``` + +### Approve Command + +```bash +# Approve with auto-resolved channel ID (when code is bound) +praisonai pairing approve telegram ABCD1234 + +# Approve with explicit channel ID +praisonai pairing approve telegram ABCD1234 987654321 + +# Approve with human-readable label +praisonai pairing approve telegram ABCD1234 --label "alice" + +# Use custom store directory +praisonai pairing approve telegram ABCD1234 --store-dir /path/to/store +``` + +### Revoke Access + +```bash +# Revoke specific channel +praisonai pairing revoke telegram 987654321 + +# Clear all pairings (with confirmation) +praisonai pairing clear +# Are you sure you want to clear ALL paired channels? [y/N]: y +# ✅ Cleared 3 paired channels + +# Clear without confirmation prompt +praisonai pairing clear --confirm +``` + +--- + +## Platform Support + +### Current Implementation + +| Platform | Status | Handler Wiring | CLI Support | +|----------|--------|----------------|-------------| +| **Telegram** | ✅ Shipped | ✅ Complete | ✅ Full | +| **Discord** | 🔧 Pending | ❌ Not wired | ✅ CLI ready | +| **Slack** | 🔧 Pending | ❌ Not wired | ✅ CLI ready | +| **WhatsApp** | 🔧 Pending | ❌ Not wired | ✅ CLI ready | + + +**Platform Implementation Status**: PR #1504 ships the pairing system and CLI with full Telegram support. Other platform adapters need handler wiring to complete the integration. + + +### Telegram Integration + +```python +# Telegram adapter includes UnknownUserHandler wiring +from praisonai.bots.telegram import TelegramAdapter +from praisonaiagents.bots import BotConfig + +config = BotConfig( + token=os.getenv("TELEGRAM_BOT_TOKEN"), + unknown_user_policy="pair" +) + +# Handler automatically wired in Telegram adapter +bot = TelegramAdapter(config) +``` + +--- + +## Security Model + +### Code Generation + +- **8-character codes**: Hex format (e.g., `ABCD1234`) +- **HMAC signatures**: Codes are cryptographically signed +- **Per-install secret**: Auto-generated if `PRAISONAI_GATEWAY_SECRET` unset +- **Channel binding**: Codes can be bound to specific channel IDs + +### Secret Management + +```bash +# Option 1: Set explicit gateway secret (recommended for production) +export PRAISONAI_GATEWAY_SECRET="your-secure-secret-key" + +# Option 2: Auto-generated per-install secret +# Stored at /.gateway_secret with 0600 permissions +# Persists across restarts, unique per installation +``` + + +**Secret Persistence**: Without `PRAISONAI_GATEWAY_SECRET`, a per-install secret is auto-generated and stored at `/.gateway_secret` with mode `0600`. This file is critical for code verification across restarts. + + +### Security Features + +```mermaid +graph TD + A[Code Request] --> B{Rate Limited?} + B -->|Yes| C[Drop Request ❌] + B -->|No| D[Generate HMAC Code] + D --> E[Store with TTL] + E --> F[Send to User] + F --> G[Owner Approval] + G --> H{Valid Code?} + H -->|Yes| I[Mark Paired ✅] + H -->|No| J[Reject ❌] + + classDef security fill:#8B0000,stroke:#7C90A0,color:#fff + classDef process fill:#189AB4,stroke:#7C90A0,color:#fff + classDef success fill:#10B981,stroke:#7C90A0,color:#fff + + class A,B,D,G,H security + class E,F process + class I success + class C,J security +``` + +1. **Rate Limiting**: 600s (10 min) window per channel prevents spam +2. **HMAC Verification**: Codes are cryptographically signed and verified +3. **TTL Expiration**: Codes automatically expire after configured time +4. **Atomic Operations**: Pairing state persisted atomically to disk + +--- + +## User Interaction Flow + +### Step-by-Step Process + +1. **Unknown User DMs Bot** + ``` + Unknown User: Hello! + ``` + +2. **Bot Generates Pairing Code** + ``` + Bot: Your pairing code: `ABCD1234` + Owner: `praisonai pairing approve telegram ABCD1234` + ``` + +3. **Owner Approves via CLI** + ```bash + $ praisonai pairing approve telegram ABCD1234 --label "alice" + ✅ Successfully paired telegram channel 987654321 + Label: alice + ``` + +4. **User Can Now Interact Normally** + ``` + Unknown User: Hello! + Bot: Hi there! How can I help you today? + ``` + +### Rate Limit Handling + +If a user tries to generate codes too frequently: + +``` +User: Hello! +Bot: [no response - rate limited] + +# In logs: +# DEBUG: Rate limited channel 987654321 (last code: 120.5s ago) +``` + +The user must wait for the rate limit window (10 minutes) to expire before requesting a new code. + +--- + +## Configuration Options + +### Store Directory + +```bash +# Default: ~/.praisonai/pairing/ +praisonai pairing list + +# Custom directory +praisonai pairing list --store-dir /custom/path + +# All commands support --store-dir +praisonai pairing approve telegram ABCD1234 --store-dir /custom/path +``` + +### Environment Variables + +```bash +# Explicit gateway secret (recommended for production) +export PRAISONAI_GATEWAY_SECRET="your-256-bit-secret" + +# Custom store directory (optional) +export PRAISONAI_STORE_DIR="/custom/store/path" +``` + +--- + +## Best Practices + + + +Set `PRAISONAI_GATEWAY_SECRET` explicitly in production environments to ensure consistent code verification across deployments. + +```bash +# Generate secure secret +openssl rand -hex 32 > gateway_secret.txt + +# Set in production +export PRAISONAI_GATEWAY_SECRET=$(cat gateway_secret.txt) +``` + + + +Watch for rate limiting warnings in logs - they indicate potential pairing spam or legitimate users hitting limits. + +```bash +# Look for these log patterns: +# DEBUG: Rate limited channel 123456 (last code: 45.2s ago) +# INFO: Generated pairing code for user123 on telegram: ABCD1234 +``` + + + +Add labels when approving pairings to identify channels later. + +```bash +# Good - easy to identify +praisonai pairing approve telegram ABCD1234 --label "alice-work" +praisonai pairing approve telegram EFGH5678 --label "bob-personal" + +# Less helpful +praisonai pairing approve telegram ABCD1234 +``` + + + +Periodically review paired channels and revoke access for inactive users. + +```bash +# Review all pairings +praisonai pairing list + +# Revoke specific channels +praisonai pairing revoke telegram 987654321 + +# Clear all if starting fresh +praisonai pairing clear --confirm +``` + + + +--- + +## Related + + + +Comprehensive bot security and DM policies + + + +Bot platform setup and configuration + + \ No newline at end of file diff --git a/docs/features/clarify-tool.mdx b/docs/features/clarify-tool.mdx new file mode 100644 index 00000000..efecb3eb --- /dev/null +++ b/docs/features/clarify-tool.mdx @@ -0,0 +1,327 @@ +--- +title: "Clarify Tool" +sidebarTitle: "Clarify Tool" +description: "Ask focused clarifying questions when agents need user input to proceed" +icon: "messages-question" +--- + +Clarify enables agents to pause mid-task and ask focused questions instead of guessing, improving decision accuracy and user control. + +```mermaid +graph LR + subgraph "Clarify Flow" + A[🤖 Agent Task] --> B[❓ Clarify Question] + B --> C[📱 User Reply] + C --> D[✅ Continue Task] + end + + classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff + classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff + classDef input fill:#189AB4,stroke:#7C90A0,color:#fff + classDef continue fill:#10B981,stroke:#7C90A0,color:#fff + + class A agent + class B question + class C input + class D continue +``` + +## Quick Start + + + + +```python +from praisonaiagents import Agent +from praisonaiagents.tools.clarify import clarify + +agent = Agent( + name="Writer", + instructions="Write code. If requirements are ambiguous, ask clarifying questions.", + tools=[clarify], +) + +agent.start("Build me a web scraper") +# Agent may call: clarify(question="Which language?", choices=["python", "rust"]) +``` + + + + + +```python +from praisonaiagents import Agent +from praisonaiagents.tools.clarify import clarify, create_cli_clarify_handler + +# Setup CLI handler for interactive questions +handler = create_cli_clarify_handler() + +agent = Agent( + name="Researcher", + instructions="Research topics. Ask for clarification when needed.", + tools=[clarify], + ctx={"clarify_handler": handler} +) +``` + + + + +--- + +## How It Works + +```mermaid +sequenceDiagram + participant Agent + participant Clarify + participant Channel + participant User + + Agent->>Clarify: clarify(question, choices) + Clarify->>Channel: Show question + options + Channel->>User: Display prompt + User->>Channel: Provide answer + Channel->>Clarify: Return response + Clarify->>Agent: Continue with answer +``` + +| Component | Purpose | Behavior | +|-----------|---------|----------| +| **ClarifyTool** | Core tool implementation | Pauses execution for user input | +| **ClarifyHandler** | Channel-specific behavior | Routes questions to CLI/bot/UI | +| **Context Integration** | Runtime handler resolution | Uses ctx['clarify_handler'] if available | + +--- + +## Channel Integration + +### CLI Usage + +```python +from praisonaiagents.tools.clarify import create_cli_clarify_handler + +handler = create_cli_clarify_handler() +# Shows: 🤔 Which language? +# Choices: +# 1. python +# 2. rust +# Your choice (number or text): 1 +# Returns: "python" +``` + +### Bot Usage + +```python +from praisonaiagents.tools.clarify import create_bot_clarify_handler + +async def send_message(channel_id, text): + # Send to Telegram/Discord/Slack + pass + +async def wait_for_reply(): + # Wait for user response + return "python" + +handler = create_bot_clarify_handler(send_message, wait_for_reply) +``` + +### Custom Context Handler + +```python +from praisonaiagents import Agent +from praisonaiagents.tools.clarify import clarify + +async def my_clarify_handler(question, choices): + # Custom UI/web interface + return await show_dialog(question, choices) + +agent = Agent( + name="Assistant", + tools=[clarify], + ctx={"clarify_handler": my_clarify_handler} +) +``` + +--- + +## Handler Resolution + +The tool uses this priority order for handling questions: + +```mermaid +graph TD + A[Clarify Called] --> B{ctx['clarify_handler']?} + B -->|Yes| C[Use Context Handler] + B -->|No| D{tool.handler exists?} + D -->|Yes| E[Use Tool Handler] + D -->|No| F[Fallback Message] + + C --> G[Execute Handler] + E --> G + F --> H["No interactive channel available"] + G --> I[Return Response] + + classDef process fill:#189AB4,stroke:#7C90A0,color:#fff + classDef fallback fill:#F59E0B,stroke:#7C90A0,color:#fff + classDef result fill:#10B981,stroke:#7C90A0,color:#fff + + class A,B,D,G process + class F,H fallback + class C,E,I result +``` + +1. **Context Handler**: `kwargs["ctx"]["clarify_handler"]` (highest priority) +2. **Tool Handler**: `self.handler` (default `ClarifyHandler()`) +3. **Fallback**: Returns guidance message when no interactive channel available + +--- + +## Configuration Options + +### Tool Schema + +```python +# LLM sees this tool signature: +{ + "name": "clarify", + "description": "Ask the user a focused clarifying question when genuinely ambiguous. Use sparingly - only when you cannot proceed without their input.", + "parameters": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The clarifying question to ask" + }, + "choices": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of predefined answer choices" + } + }, + "required": ["question"] + } +} +``` + +### Bot Auto-Approval + +```python +from praisonaiagents.bots import BotConfig + +config = BotConfig( + default_tools=["clarify"], # Included by default + auto_approve_tools=True # No approval prompt for clarify +) +``` + +The `clarify` tool is included in the bot's default auto-approve list, so it won't require manual approval in bot environments. + +--- + +## Common Patterns + +### Progressive Clarification + +```python +from praisonaiagents import Agent +from praisonaiagents.tools.clarify import clarify + +agent = Agent( + name="CodeWriter", + instructions=""" + Write code based on user requests. Use clarify for: + 1. Language/framework choice when unspecified + 2. Feature priorities when scope is broad + 3. Architecture decisions when requirements are complex + """, + tools=[clarify] +) + +# Example interaction: +# User: "Build a REST API" +# Agent: clarify("Which language?", ["python", "node.js", "go"]) +# User: "python" +# Agent: clarify("Which framework?", ["fastapi", "flask", "django"]) +``` + +### Context-Aware Questions + +```python +async def smart_clarify_handler(question, choices): + """Handler that considers conversation context""" + # Check previous messages for hints + context = get_conversation_context() + + if "python" in context and "web" in question.lower(): + # Auto-suggest based on context + return "fastapi" + + return await show_user_dialog(question, choices) +``` + +### Fallback Behavior + +```python +# When no interactive channel available: +result = await clarify("Which database?", ["postgres", "mysql"]) +# Returns: "No interactive channel available. Please proceed with your best judgment for: Which database?" + +# Agent can handle this gracefully: +if "no interactive channel" in result.lower(): + # Use reasonable defaults + database = "postgres" # Pick sensible default +``` + +--- + +## Best Practices + + + +Only call clarify when you genuinely cannot proceed without user input. Don't ask for preferences that have reasonable defaults. + +**Good**: "Which API endpoint format?" when building an API +**Bad**: "Should I use descriptive variable names?" (obvious default) + + + +When possible, offer specific choices rather than open-ended questions. + +**Good**: `choices=["fastapi", "flask", "django"]` +**Bad**: `"What Python web framework should I use?"` (no choices) + + + +Always check if the response indicates no interactive channel and proceed with sensible defaults. + +```python +response = await clarify("Pick a color", ["blue", "red"]) +if "no interactive channel" in response.lower(): + color = "blue" # Use default +else: + color = response +``` + + + +Frame questions with enough context for users to make informed decisions. + +**Good**: "Which authentication method for your user API?" +**Bad**: "Which auth?" (unclear context) + + + +--- + +## Related + + + +Core tool system and custom tools + + + +Agent setup and tool integration + + \ No newline at end of file diff --git a/docs/features/tool-availability.mdx b/docs/features/tool-availability.mdx new file mode 100644 index 00000000..6da1823f --- /dev/null +++ b/docs/features/tool-availability.mdx @@ -0,0 +1,432 @@ +--- +title: "Tool Availability Gating" +sidebarTitle: "Tool Availability" +description: "Hide tools from the LLM when environment dependencies are missing" +icon: "toggle-on" +--- + +Tool availability gating filters unavailable tools at schema-build time, preventing the LLM from hallucinating calls to tools that can't run. + +```mermaid +graph LR + subgraph "Availability Gating" + A[🔧 Tool Registry] --> B[✅ Check Available] + B --> C[📋 LLM Schema] + B --> D[❌ Hide Unavailable] + C --> E[🤖 Agent Uses Tool] + end + + classDef registry fill:#189AB4,stroke:#7C90A0,color:#fff + classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff + classDef available fill:#10B981,stroke:#7C90A0,color:#fff + classDef hidden fill:#8B0000,stroke:#7C90A0,color:#fff + classDef agent fill:#6366F1,stroke:#7C90A0,color:#fff + + class A registry + class B check + class C,E available + class D hidden +``` + +## Quick Start + + + + +```python +import os +from praisonaiagents.tools import tool + +@tool(availability=lambda: (bool(os.getenv("SERP_API_KEY")), "SERP_API_KEY not set")) +def search_web(query: str) -> str: + """Search the web for information.""" + api_key = os.getenv("SERP_API_KEY") + # ... search implementation + return f"Search results for: {query}" + +from praisonaiagents import Agent + +agent = Agent( + name="Researcher", + instructions="Research topics the user asks about.", + tools=[search_web] +) +# If SERP_API_KEY missing → tool hidden from LLM +# If SERP_API_KEY set → tool appears and works normally +agent.start("Research quantum computing") +``` + + + + + +```python +from praisonaiagents.tools.base import BaseTool + +class DatabaseTool(BaseTool): + name = "query_database" + description = "Query the application database" + + def __init__(self, connection_string: str = None): + super().__init__() + self.connection_string = connection_string or os.getenv("DATABASE_URL") + + def check_availability(self) -> tuple[bool, str]: + if not self.connection_string: + return False, "DATABASE_URL not configured" + + try: + # Quick connection test (must be fast, no I/O heavy operations) + import psycopg2 + return True, "" + except ImportError: + return False, "psycopg2 package not installed" + + def run(self, query: str) -> str: + # Implementation here + return f"Query result: {query}" +``` + + + + +--- + +## How It Works + +```mermaid +sequenceDiagram + participant Agent + participant Registry + participant Tool + participant LLM + + Agent->>Registry: Get available tools + Registry->>Tool: check_availability() + Tool-->>Registry: (is_available, reason) + alt Available + Registry->>LLM: Include in schema + LLM->>Tool: Call tool + else Unavailable + Registry-->>Agent: Skip tool (hidden) + Note over LLM: Tool not visible to LLM + end +``` + +| Phase | Behavior | Performance | +|-------|----------|-------------| +| **Schema Build** | Availability checks run once | Zero runtime cost | +| **Tool Execution** | Only available tools included | No availability overhead | +| **LLM Interaction** | Only sees usable tools | Prevents hallucination | + +--- + +## Implementation Methods + +### Function Decorator + +```python +from praisonaiagents.tools import tool +import os + +# Simple environment check +@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing")) +def api_tool(query: str) -> str: + return f"API result: {query}" + +# Complex dependency check +def check_docker_available(): + try: + import docker + client = docker.from_env() + client.ping() # Quick ping, not heavy I/O + return True, "" + except Exception as e: + return False, f"Docker unavailable: {e}" + +@tool(availability=check_docker_available) +def docker_command(cmd: str) -> str: + """Run Docker commands.""" + return f"Docker: {cmd}" +``` + +### BaseTool Protocol + +```python +from praisonaiagents.tools.base import BaseTool +from praisonaiagents.tools.protocols import ToolAvailabilityProtocol + +class CloudTool(BaseTool, ToolAvailabilityProtocol): + name = "cloud_deploy" + description = "Deploy to cloud services" + + def check_availability(self) -> tuple[bool, str]: + # Check multiple dependencies + if not os.getenv("AWS_ACCESS_KEY"): + return False, "AWS credentials not configured" + + try: + import boto3 + # Quick credential test (fast operation only) + boto3.Session().get_credentials() + return True, "" + except Exception as e: + return False, f"AWS SDK error: {e}" + + def run(self, service: str) -> str: + return f"Deployed {service} to cloud" +``` + +### Registry Functions + +```python +from praisonaiagents.tools import list_available_tools, get_registry + +# Get only available tools +available_tools = list_available_tools() +print(f"Available: {len(available_tools)} tools") + +# Get all tools (including unavailable) +all_tools = get_registry().list_tools() +print(f"Total registered: {len(all_tools)} tools") + +# Check specific tool +web_tool = get_registry().get("search_web") +if hasattr(web_tool, 'check_availability'): + is_available, reason = web_tool.check_availability() + print(f"Web tool available: {is_available}") + if not is_available: + print(f"Reason: {reason}") +``` + +--- + +## Availability Rules + +### Behavior Patterns + +```mermaid +graph TD + A[Tool Registration] --> B{Has check_availability?} + B -->|No| C[Always Available] + B -->|Yes| D[Run Check] + D --> E{Check Result?} + E -->|True| F[Include in Schema] + E -->|False| G[Hide from LLM] + E -->|Exception| H[Log Warning + Hide] + + classDef default fill:#189AB4,stroke:#7C90A0,color:#fff + classDef available fill:#10B981,stroke:#7C90A0,color:#fff + classDef unavailable fill:#8B0000,stroke:#7C90A0,color:#fff + + class A,B,D default + class C,F available + class G,H unavailable +``` + +1. **No Check Method**: Tool is always considered available +2. **Check Returns True**: Tool included in LLM schema +3. **Check Returns False**: Tool hidden from LLM +4. **Check Throws Exception**: Tool hidden + warning logged + +### Exception Handling + +```python +# If check_availability() raises an exception: +def broken_availability_check(): + raise ValueError("Network unreachable") + +@tool(availability=broken_availability_check) +def network_tool(host: str) -> str: + return f"Ping {host}" + +# Result: Tool is hidden, log shows: +# WARNING: Tool 'network_tool' unavailable: Availability check failed: Network unreachable +``` + +### Plain Function Registry + +```python +from praisonaiagents.tools import register_tool + +def simple_function(text: str) -> str: + return f"Processed: {text}" + +# Plain functions are always available (no protocol support yet) +register_tool(simple_function) +``` + +--- + +## Configuration Patterns + +### Environment-Based Availability + +```python +import os +from praisonaiagents.tools import tool + +def check_environment(required_vars: list[str]): + """Factory for environment-based availability checks.""" + def check(): + missing = [var for var in required_vars if not os.getenv(var)] + if missing: + return False, f"Missing environment variables: {', '.join(missing)}" + return True, "" + return check + +@tool(availability=check_environment(["OPENAI_API_KEY", "PINECONE_API_KEY"])) +def ai_research(topic: str) -> str: + """Research topics using AI and vector search.""" + return f"AI research on: {topic}" + +@tool(availability=check_environment(["SLACK_TOKEN"])) +def notify_team(message: str) -> str: + """Send notifications to team Slack.""" + return f"Notified team: {message}" +``` + +### Service Discovery + +```python +from praisonaiagents.tools import tool +import socket + +def check_service_available(host: str, port: int): + """Check if a network service is reachable.""" + def check(): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(2) # Keep fast + s.connect((host, port)) + return True, "" + except Exception as e: + return False, f"Service {host}:{port} unreachable: {e}" + return check + +@tool(availability=check_service_available("localhost", 5432)) +def query_local_db(sql: str) -> str: + """Query local PostgreSQL database.""" + return f"SQL result: {sql}" + +@tool(availability=check_service_available("redis-server", 6379)) +def cache_data(key: str, value: str) -> str: + """Cache data in Redis.""" + return f"Cached {key}: {value}" +``` + +### Conditional Tool Loading + +```python +from praisonaiagents import Agent +from praisonaiagents.tools import list_available_tools + +# Build agent with only available tools +available_tools = list_available_tools() +print(f"Loading agent with {len(available_tools)} available tools") + +agent = Agent( + name="AdaptiveAgent", + instructions="Use whatever tools are available in the current environment.", + tools=available_tools +) + +# Agent automatically adapts to environment capabilities +``` + +--- + +## Performance Guidelines + + + +Availability checks run at schema-build time and must be fast (< 100ms recommended). + +**Good**: Environment variable checks, import tests, quick pings +**Bad**: Full API calls, heavy file operations, long network requests + +```python +# Fast check +@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing")) + +# Slow check (avoid) +def slow_check(): + import requests + requests.get("https://api.example.com/health", timeout=30) # Too slow! + return True, "" +``` + + + +For checks that might be expensive, consider caching results. + +```python +import time +from functools import lru_cache + +@lru_cache(maxsize=1) +def cached_docker_check(): + """Cache Docker availability for 5 minutes.""" + try: + import docker + docker.from_env().ping() + return True, "" + except Exception as e: + return False, str(e) + +# Refresh cache periodically +cached_docker_check.cache_clear() +``` + + + +Check critical dependencies first, avoid unnecessary work. + +```python +def check_ml_stack(): + # Check imports first (fast) + try: + import torch + import transformers + except ImportError as e: + return False, f"Missing ML dependencies: {e}" + + # Then check GPU availability (slower) + if not torch.cuda.is_available(): + return False, "CUDA not available" + + return True, "" +``` + + + +Design tools to degrade gracefully when dependencies are partially available. + +```python +@tool(availability=lambda: (True, "")) # Always available +def search_content(query: str) -> str: + """Search using best available method.""" + + # Try premium search first + if os.getenv("PREMIUM_SEARCH_KEY"): + return premium_search(query) + + # Fall back to basic search + return basic_search(query) +``` + + + +--- + +## Related + + + +Core tool system and registration + + + +Agent setup and tool integration + + \ No newline at end of file