diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 00000000..6bfd7869 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,32 @@ +# Vale Configuration for SignalWire Documentation +# Enforces sentence case headers instead of title case + +# Global settings +StylesPath = .vale/styles +MinAlertLevel = warning +Vocab = Base +VocabPath = .vale/styles/config/vocabularies + +# File patterns to lint - focusing on Markdown files +[*.md] +# Base linting rules - only using Vale built-in and our custom styles +BasedOnStyles = Vale, SentenceCase + +# Built-in Vale rules (tuned for technical documentation) +Vale.Spelling = NO # Too many false positives with technical terms +Vale.Repetition = YES + +# Enable our custom sentence case rule +SentenceCase.SentenceCase = error + +# Simple MDX support - treat as plain text with limited processing +[*.mdx] +BasedOnStyles = SentenceCase +SentenceCase.SentenceCase = error + +# File patterns to ignore +[*.{yml,yaml,json,js,ts,tsx,jsx}] +BasedOnStyles = "" + +[package*.json] +BasedOnStyles = "" \ No newline at end of file diff --git a/.vale/styles/SentenceCase/SentenceCase.yml b/.vale/styles/SentenceCase/SentenceCase.yml new file mode 100644 index 00000000..e3b02e68 --- /dev/null +++ b/.vale/styles/SentenceCase/SentenceCase.yml @@ -0,0 +1,102 @@ +extends: capitalization +message: "Headers should use sentence case. Change '%s' to '%s'." +level: error +scope: heading + +# Define the capitalization style for headers +# Only the first word and proper nouns should be capitalized +match: $sentence + +# Exceptions for proper nouns, acronyms, and technical terms that should always be capitalized +exceptions: + - API + - SDK + - AI + - SWML + - SignalWire + - REST + - HTTP + - HTTPS + - URL + - JSON + - YAML + - XML + - WebSocket + - JavaScript + - TypeScript + - OAuth + - JWT + - UUID + - SQL + - NoSQL + - CSS + - HTML + - DOM + - CLI + - GUI + - UI + - UX + - FAQ + - README + - CHANGELOG + - LICENSE + - GitHub + - GitLab + - npm + - Node.js + - React + - Vue + - Angular + - Docker + - Kubernetes + - AWS + - GCP + - Azure + - TCP + - UDP + - IP + - DNS + - SSL + - TLS + - CORS + - CSRF + - XSS + - GDPR + - CCPA + - SIP + - RTP + - RTCP + - WebRTC + - VoIP + - SMS + - MMS + - E164 + - SIM + - PSTN + - PBX + - IVR + - TTS + - ASR + - NLP + - ML + - LLM + - GPT + - BERT + - CNN + - RNN + - LSTM + - GRU + - PoC + - MVP + - SaaS + - PaaS + - IaaS + - DevOps + - CI/CD + - QA + - SWAIG + - DataMap + - Relay + - Call + - Fabric + - Compatibility \ No newline at end of file diff --git a/.vale/styles/config/vocabularies/Base/accept.txt b/.vale/styles/config/vocabularies/Base/accept.txt new file mode 100644 index 00000000..ff3f6cb8 --- /dev/null +++ b/.vale/styles/config/vocabularies/Base/accept.txt @@ -0,0 +1,161 @@ +SignalWire +SWML +SWAIG +DataMap +Call Fabric +Compatibility API +Relay +WebRTC +VoIP +E164 +SIP +RTP +RTCP +PSTN +PBX +IVR +ASR +TTS +SMS +MMS +AI +SDK +API +REST +HTTP +HTTPS +JSON +YAML +XML +URL +WebSocket +JavaScript +TypeScript +OAuth +JWT +UUID +SQL +NoSQL +CSS +HTML +DOM +CLI +GUI +UI +UX +FAQ +README +CHANGELOG +LICENSE +GitHub +GitLab +npm +Node.js +React +Vue +Angular +Docker +Kubernetes +AWS +GCP +Azure +TCP +UDP +IP +DNS +SSL +TLS +CORS +CSRF +XSS +GDPR +CCPA +NLP +ML +LLM +GPT +BERT +CNN +RNN +LSTM +GRU +PoC +MVP +SaaS +PaaS +IaaS +DevOps +CI/CD +QA +microservice +webhook +endpoint +middleware +namespace +localhost +frontend +backend +fullstack +config +env +localhost +subfolder +subdirectory +filename +pathname +hostname +username +timestamp +metadata +payload +boolean +async +await +callback +middleware +npm +yarn +webpack +babel +eslint +prettier +typescript +javascript +nodejs +reactjs +vuejs +angularjs +dockerfile +kubernetes +devops +github +gitlab +bitbucket +vscode +webstorm +intellij +postman +insomnia +swagger +openapi +graphql +grpc +mongodb +postgresql +mysql +redis +elasticsearch +kibana +grafana +prometheus +jenkins +travis +circleci +heroku +vercel +netlify +cloudflare +aws +gcp +azure +digitalocean \ No newline at end of file diff --git a/config/navbar.ts b/config/navbar.ts index 4056607b..d178ba04 100644 --- a/config/navbar.ts +++ b/config/navbar.ts @@ -17,13 +17,19 @@ const navbar: NavbarItem[] = [ { to: "/platform", label: "Platform", - position: "left", + position: "right", type: "dropdown", items: [ { label: "Call Fabric", to: "/platform/call-fabric", }, + { + href: "https://signalwire.com/signin", + label: "SignalWire Space", + className: "dashboard-navbar-link", + "aria-label": "Open SignalWire Space", + }, { label: "Dashboard", to: "/platform/dashboard", @@ -42,6 +48,26 @@ const navbar: NavbarItem[] = [ }, ], }, + /// SWML Dropdown -------- /// + { + type: "dropdown", + label: "SWML", + position: "left", + items: [ + { + label: "SWML Guides", + to: "/swml/guides", + }, + { + label: "Method Reference", + to: "/swml/methods", + }, + { + label: "Agents SDK", + to: "/agents-sdk", + }, + ], + }, /// Products Dropdown -------- /// { @@ -202,14 +228,6 @@ const navbar: NavbarItem[] = [ }, ], }, - - { - href: "https://signalwire.com/signin", - label: "Dashboard", - position: "right", - className: "dashboard-navbar-link", - "aria-label": "Dashboard", - }, { href: 'https://github.com/signalwire/docs', position: 'right', diff --git a/config/sidebarsConfig/agents-sdk-sidebar.ts b/config/sidebarsConfig/agents-sdk-sidebar.ts new file mode 100644 index 00000000..855489bc --- /dev/null +++ b/config/sidebarsConfig/agents-sdk-sidebar.ts @@ -0,0 +1,9 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +const agentsSdkSidebar: SidebarsConfig = { + agentsSdkSidebar: [ + { type: "autogenerated", dirName: "swml/agents-sdk" } + ], +}; + +export default agentsSdkSidebar; \ No newline at end of file diff --git a/config/sidebarsConfig/index.ts b/config/sidebarsConfig/index.ts index 6121f53a..87017fb0 100644 --- a/config/sidebarsConfig/index.ts +++ b/config/sidebarsConfig/index.ts @@ -22,6 +22,7 @@ import apiSidebar from './rest-api-sidebar'; import swmlTechRefSidebar from './swml-sidebar'; import compatibilityAPI from './compatibility-api-sidebar'; import cantinaSidebar from './cantina-sidebar'; +import agentsSdkSidebar from './agents-sdk-sidebar'; const sidebars: SidebarsConfig = { ...homeSidebar, @@ -32,6 +33,7 @@ const sidebars: SidebarsConfig = { ...swmlTechRefSidebar, ...compatibilityAPI, ...cantinaSidebar, + ...agentsSdkSidebar, }; export default sidebars; \ No newline at end of file diff --git a/docs/swml/agents-sdk/_category_.json b/docs/swml/agents-sdk/_category_.json new file mode 100644 index 00000000..eaebc9df --- /dev/null +++ b/docs/swml/agents-sdk/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "AI Agents SDK", + "position": 6 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/api/_category_.json b/docs/swml/agents-sdk/api/_category_.json new file mode 100644 index 00000000..defb2586 --- /dev/null +++ b/docs/swml/agents-sdk/api/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "API reference", + "collapsible": false, + "className": "menu-category", + "position": 5 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/api/function-results.md b/docs/swml/agents-sdk/api/function-results.md new file mode 100644 index 00000000..8a9ac231 --- /dev/null +++ b/docs/swml/agents-sdk/api/function-results.md @@ -0,0 +1,758 @@ +--- +id: agents-sdk-reference-function-results +slug: /agents-sdk/api/function-results +title: Function Results Reference +sidebar_label: Function Results +--- + +# SwaigFunctionResult Methods Reference + +This document provides a complete reference for all methods available in the `SwaigFunctionResult` class. These methods provide convenient abstractions for SWAIG actions, eliminating the need to manually construct action JSON objects. + +## Core Methods + +### Basic Construction & Control + +#### `__init__(response=None, post_process=False)` +Creates a new result object with optional response text and post-processing behavior. + +```python +result = SwaigFunctionResult("Hello, I'll help you with that") +result = SwaigFunctionResult("Processing request...", post_process=True) +``` + +#### `set_response(response)` +Sets or updates the response text that the AI will speak. + +```python +result.set_response("I've updated your information") +``` + +#### `set_post_process(post_process)` +Controls whether AI gets one more turn before executing actions. + +```python +result.set_post_process(True) # AI speaks response before executing actions +result.set_post_process(False) # Actions execute immediately +``` + +--- + +## Action Methods + +### Call Control Actions + +#### `execute_swml(swml_content, transfer=False)` +Execute SWML content with flexible input support and optional transfer behavior. + +```python +# Raw SWML string +result.execute_swml('{"version":"1.0.0","sections":{"main":[{"say":"Hello"}]}}') + +# SWML dictionary +swml_dict = {"version": "1.0.0", "sections": {"main": [{"say": "Hello"}]}} +result.execute_swml(swml_dict, transfer=True) + +# SWML SDK object +from signalwire.swml import SWML +swml_doc = SWML() +swml_doc.add_application("main", "say", {"text": "Connecting now"}) +result.execute_swml(swml_doc) +``` + +#### **[IMPLEMENTED]** - Transfer/connect call to another destination using SWML. + +```python +result.connect("+15551234567", final=True) # Permanent transfer +result.connect("support@company.com", final=False, from_addr="+15559876543") # Temporary transfer +``` + +#### `send_sms(to_number, from_number, body=None, media=None, tags=None, region=None)` +**[HELPER METHOD]** - Send SMS message to PSTN phone number using SWML. + +```python +# Simple text message +result.send_sms( + to_number="+15551234567", + from_number="+15559876543", + body="Your order has been confirmed!" +) + +# Media message with images +result.send_sms( + to_number="+15551234567", + from_number="+15559876543", + media=["https://example.com/receipt.jpg", "https://example.com/map.png"] +) + +# Full featured message with tags and region +result.send_sms( + to_number="+15551234567", + from_number="+15559876543", + body="Order update with receipt attached", + media=["https://example.com/receipt.pdf"], + tags=["order", "confirmation", "customer"], + region="us" +) +``` + +**Parameters:** +- `to_number` (required): Phone number in E.164 format to send to +- `from_number` (required): Phone number in E.164 format to send from +- `body` (optional): Message text (required if no media) +- `media` (optional): Array of URLs to send (required if no body) +- `tags` (optional): Array of tags for UI searching +- `region` (optional): Region to originate message from + +**Variables Set:** +- `send_sms_result`: "success" or "failed" + +#### `pay(payment_connector_url, **options)` +**[HELPER METHOD]** - Process payments using SWML pay action with extensive customization. + +```python +# Simple payment setup +result.pay( + payment_connector_url="https://api.example.com/accept-payment", + charge_amount="10.99", + description="Monthly subscription" +) + +# Advanced payment with custom prompts +from signalwire_agents.core.function_result import SwaigFunctionResult + +# Create custom prompts +welcome_actions = [ + SwaigFunctionResult.create_payment_action("Say", "Welcome to our payment system"), + SwaigFunctionResult.create_payment_action("Say", "Please enter your credit card number") +] +card_prompt = SwaigFunctionResult.create_payment_prompt("payment-card-number", welcome_actions) + +error_actions = [ + SwaigFunctionResult.create_payment_action("Say", "Invalid card number, please try again") +] +error_prompt = SwaigFunctionResult.create_payment_prompt( + "payment-card-number", + error_actions, + error_type="invalid-card-number timeout" +) + +# Create payment parameters +params = [ + SwaigFunctionResult.create_payment_parameter("customer_id", "12345"), + SwaigFunctionResult.create_payment_parameter("order_id", "ORD-789") +] + +# Full payment configuration +result.pay( + payment_connector_url="https://api.example.com/accept-payment", + status_url="https://api.example.com/payment-status", + timeout=10, + max_attempts=3, + security_code=True, + postal_code=False, + token_type="one-time", + charge_amount="25.50", + currency="usd", + language="en-US", + voice="polly.Sally", + description="Premium service upgrade", + valid_card_types="visa mastercard amex", + parameters=params, + prompts=[card_prompt, error_prompt] +) +``` + +**Core Parameters:** +- `payment_connector_url` (required): URL to process payment requests +- `input_method`: "dtmf" or "voice" (default: "dtmf") +- `payment_method`: "credit-card" (default: "credit-card") +- `timeout`: Seconds to wait for input (default: 5) +- `max_attempts`: Number of retry attempts (default: 1) + +**Security & Validation:** +- `security_code`: Prompt for CVV (default: True) +- `postal_code`: Prompt for postal code or provide known code (default: True) +- `min_postal_code_length`: Minimum postal code digits (default: 0) +- `valid_card_types`: Space-separated card types (default: "visa mastercard amex") + +**Payment Configuration:** +- `token_type`: "one-time" or "reusable" (default: "reusable") +- `charge_amount`: Amount as decimal string +- `currency`: Currency code (default: "usd") +- `description`: Payment description + +**Customization:** +- `language`: Prompt language (default: "en-US") +- `voice`: TTS voice (default: "woman") +- `status_url`: URL for status notifications +- `parameters`: Additional name/value pairs for connector +- `prompts`: Custom prompt configurations + +**Helper Methods for Payment Setup:** +```python +# Create payment action +action = SwaigFunctionResult.create_payment_action("Say", "Enter card number") + +# Create payment prompt +prompt = SwaigFunctionResult.create_payment_prompt( + "payment-card-number", + [action], + error_type="invalid-card-number" +) + +# Create payment parameter +param = SwaigFunctionResult.create_payment_parameter("customer_id", "12345") +``` + +**Variables Set:** +- `pay_result`: "success", "too-many-failed-attempts", "payment-connector-error", etc. +- `pay_payment_results`: JSON with payment details including tokens and card info + +#### `record_call(control_id=None, stereo=False, format="wav", direction="both", **options)` +**[HELPER METHOD]** - Start background call recording using SWML. + +Unlike foreground recording, the script continues executing while recording happens in the background. + +```python +# Simple background recording +result.record_call() + +# Recording with custom settings +result.record_call( + control_id="support_call_001", + stereo=True, + format="mp3", + direction="both", + max_length=300 # 5 minutes max +) + +# Recording with terminator and status webhook +result.record_call( + control_id="customer_voicemail", + format="wav", + direction="speak", # Only record customer voice + terminators="#", # Stop on '#' press + beep=True, # Play beep before recording + initial_timeout=4.0, # Wait 4 seconds for speech + end_silence_timeout=3.0, # Stop after 3 seconds of silence + status_url="https://api.example.com/recording-status" +) +``` + +**Core Parameters:** +- `control_id` (optional): Identifier for this recording (for use with stop_record_call) +- `stereo`: Record in stereo (default: False) +- `format`: "wav" or "mp3" (default: "wav") +- `direction`: "speak", "listen", or "both" (default: "both") + +**Control Options:** +- `terminators`: Digits that stop recording when pressed +- `beep`: Play beep before recording (default: False) +- `max_length`: Maximum recording length in seconds + +**Timing Options:** +- `input_sensitivity`: Input sensitivity (default: 44.0) +- `initial_timeout`: Time to wait for speech start (default: 0.0) +- `end_silence_timeout`: Time to wait in silence before ending (default: 0.0) + +**Webhook Options:** +- `status_url`: URL to send recording status events to + +**Variables Set:** +- `record_call_result`: "success" or "failed" +- `record_call_url`: URL of recorded file (when recording completes) + +#### `stop_record_call(control_id=None)` +**[HELPER METHOD]** - Stop an active background call recording using SWML. + +```python +# Stop the most recent recording +result.stop_record_call() + +# Stop specific recording by ID +result.stop_record_call("support_call_001") + +# Chain to stop recording and provide feedback +result.stop_record_call("customer_voicemail") \ + .say("Thank you, your message has been recorded") +``` + +**Parameters:** +- `control_id` (optional): Identifier for recording to stop. If not provided, stops the most recent recording. + +**Variables Set:** +- `stop_record_call_result`: "success" or "failed" + +#### `join_room(name)` +**[HELPER METHOD]** - Join a RELAY room using SWML. + +RELAY rooms enable multi-party communication and collaboration features. + +```python +# Join a conference room +result.join_room("support_team_room") + +# Join customer meeting room +result.join_room("customer_meeting_001") \ + .say("Welcome to the customer meeting room") + +# Join room and set metadata +result.join_room("sales_conference") \ + .set_metadata({"participant_role": "moderator", "join_time": "2024-01-01T12:00:00Z"}) +``` + +**Parameters:** +- `name` (required): The name of the room to join + +**Variables Set:** +- `join_room_result`: "success" or "failed" + +#### `sip_refer(to_uri)` +**[HELPER METHOD]** - Send SIP REFER for call transfer using SWML. + +SIP REFER is used for call transfer in SIP environments, allowing one endpoint to request another to initiate a new connection. + +```python +# Basic SIP refer to transfer call +result.sip_refer("sip:support@company.com") + +# Transfer to specific SIP address with domain +result.sip_refer("sip:agent123@pbx.company.com:5060") + +# Chain with announcement +result.say("Transferring your call to our specialist") \ + .sip_refer("sip:specialist@company.com") +``` + +**Parameters:** +- `to_uri` (required): The SIP URI to send the REFER to + +**Variables Set:** +- `sip_refer_result`: "success" or "failed" + +#### `join_conference(name, **options)` +**[HELPER METHOD]** - Join an ad-hoc audio conference with RELAY and CXML calls using SWML. + +Provides extensive configuration options for conference call management and recording. + +```python +# Simple conference join +result.join_conference("my_conference") + +# Basic conference with recording +result.join_conference( + name="daily_standup", + record="record-from-start", + max_participants=10 +) + +# Advanced conference with callbacks and coaching +result.join_conference( + name="customer_support_conf", + muted=False, + beep="onEnter", + start_on_enter=True, + end_on_exit=False, + max_participants=50, + record="record-from-start", + region="us-east", + trim="trim-silence", + status_callback="https://api.company.com/conference-events", + status_callback_event="start end join leave", + recording_status_callback="https://api.company.com/recording-events" +) + +# Chain with other actions +result.say("Joining you to the team conference") \ + .join_conference("team_meeting") \ + .set_metadata({"meeting_type": "team_sync", "participant_role": "attendee"}) +``` + +**Core Parameters:** +- `name` (required): Name of conference to join +- `muted`: Join muted (default: False) +- `beep`: Beep configuration - "true", "false", "onEnter", "onExit" (default: "true") +- `start_on_enter`: Conference starts when this participant enters (default: True) +- `end_on_exit`: Conference ends when this participant exits (default: False) + +**Capacity & Region:** +- `max_participants`: Maximum participants `<=` 250 (default: 250) +- `region`: Conference region for optimization +- `wait_url`: SWML URL for custom hold music + +**Recording Options:** +- `record`: "do-not-record" or "record-from-start" (default: "do-not-record") +- `trim`: "trim-silence" or "do-not-trim" (default: "trim-silence") +- `recording_status_callback`: URL for recording status events +- `recording_status_callback_method`: "GET" or "POST" (default: "POST") +- `recording_status_callback_event`: "in-progress completed absent" (default: "completed") + +**Status & Coaching:** +- `coach`: SWML Call ID or CXML CallSid for coaching features +- `status_callback`: URL for conference status events +- `status_callback_method`: "GET" or "POST" (default: "POST") +- `status_callback_event`: Events to report - "start end join leave mute hold modify speaker announcement" + +**Control Flow:** +- `result`: Switch on return_value (object {} or array [] for conditional logic) + +**Variables Set:** +- `join_conference_result`: "completed", "answered", "no-answer", "failed", or "canceled" +- `return_value`: Same as `join_conference_result` + +#### `tap(uri, **options)` +**[HELPER METHOD]** - Start background call tap using SWML. + +Media is streamed over Websocket or RTP to customer controlled URI for real-time monitoring and analysis. + +```python +# Simple WebSocket tap +result.tap("wss://example.com/tap") + +# RTP tap with custom settings +result.tap( + uri="rtp://192.168.1.100:5004", + control_id="monitoring_tap_001", + direction="both", + codec="PCMA", + rtp_ptime=30 +) + +# Advanced tap with status callbacks +result.tap( + uri="wss://monitoring.company.com/audio-stream", + control_id="compliance_tap", + direction="speak", # Only what the party says + status_url="https://api.company.com/tap-status" +) \ +.set_metadata({"tap_purpose": "compliance", "session_id": "sess_123"}) +``` + +**Core Parameters:** +- `uri` (required): Destination of tap media stream + - WebSocket: `ws://example.com` or `wss://example.com` + - RTP: `rtp://IP:port` +- `control_id`: Identifier for this tap to use with stop_tap (optional, auto-generated if not provided) + +**Audio Configuration:** +- `direction`: Audio direction to tap (default: "both") + - `"speak"`: What party says + - `"hear"`: What party hears + - `"both"`: What party hears and says +- `codec`: Codec for tap stream - "PCMU" or "PCMA" (default: "PCMU") +- `rtp_ptime`: RTP packetization time in milliseconds (default: 20) + +**Status & Monitoring:** +- `status_url`: URL for tap status change requests + +**Variables Set:** +- `tap_uri`: Destination URI of the newly started tap +- `tap_result`: "success" or "failed" +- `tap_control_id`: Control ID of this tap +- `tap_rtp_src_addr`: If RTP, source address of the tap stream +- `tap_rtp_src_port`: If RTP, source port of the tap stream +- `tap_ptime`: Packetization time of the tap stream +- `tap_codec`: Codec in the tap stream +- `tap_rate`: Sample rate in the tap stream + +#### `stop_tap(control_id=None)` +**[HELPER METHOD]** - Stop an active tap stream using SWML. + +```python +# Stop the most recent tap +result.stop_tap() + +# Stop specific tap by ID +result.stop_tap("monitoring_tap_001") + +# Chain to stop tap and provide feedback +result.stop_tap("compliance_tap") \ + .say("Audio monitoring has been stopped") \ + .update_global_data({"tap_active": False}) +``` + +**Parameters:** +- `control_id` (optional): ID of the tap to stop. If not set, the last tap started will be stopped. + +**Variables Set:** +- `stop_tap_result`: "success" or "failed" + +#### `hangup()` +Terminate the call immediately. + +```python +result.hangup() +``` + +--- + +### Call Flow Control + +#### `hold(timeout=300)` +Put call on hold with timeout (max 900 seconds). + +```python +result.hold(60) # Hold for 1 minute +result.hold(600) # Hold for 10 minutes +``` + +#### `wait_for_user(enabled=None, timeout=None, answer_first=False)` +Control how agent waits for user input with flexible parameters. + +```python +result.wait_for_user(True) # Wait indefinitely +result.wait_for_user(timeout=30) # Wait 30 seconds +result.wait_for_user(answer_first=True) # Special answer_first mode +result.wait_for_user(False) # Disable waiting +``` + +#### `stop()` +Stop agent execution completely. + +```python +result.stop() +``` + +--- + +### Speech & Audio Control + +#### `say(text)` +Make the agent speak specific text immediately. + +```python +result.say("Please hold while I look that up for you") +``` + +#### `play_background_audio(filename, wait=False)` +Play audio file in background with attention control. + +```python +result.play_background_audio("hold_music.wav") # AI tries to get attention +result.play_background_audio("announcement.mp3", wait=True) # AI suppresses attention +``` + +#### `stop_background_audio()` +Stop currently playing background audio. + +```python +result.stop_background_audio() +``` + +--- + +### Speech Recognition Settings + +#### `set_end_of_speech_timeout(milliseconds)` +Set silence timeout after speech detection for finalizing recognition. + +```python +result.set_end_of_speech_timeout(2000) # 2 seconds of silence +``` + +#### `set_speech_event_timeout(milliseconds)` +Set timeout since last speech event - better for noisy environments. + +```python +result.set_speech_event_timeout(3000) # 3 seconds since last speech event +``` + +--- + +### Data Management + +#### `update_global_data(data)` +**[IMPLEMENTED]** - Update global agent data variables. + +```python +result.update_global_data({"user_name": "John", "step": 2}) +``` + +#### `remove_global_data(keys)` +Remove global data variables by key(s). + +```python +result.remove_global_data("temporary_data") # Single key +result.remove_global_data(["step", "temp_value"]) # Multiple keys +``` + +#### `set_metadata(data)` +Set metadata scoped to current function's meta_data_token. + +```python +result.set_metadata({"session_id": "abc123", "user_tier": "premium"}) +``` + +#### `remove_metadata(keys)` +Remove metadata from current function's scope. + +```python +result.remove_metadata("temp_session_data") # Single key +result.remove_metadata(["cache_key", "temp_flag"]) # Multiple keys +``` + +--- + +### Function & Behavior Control + +#### `toggle_functions(function_toggles)` +Enable/disable specific SWAIG functions dynamically. + +```python +result.toggle_functions([ + {"function": "transfer_call", "active": False}, + {"function": "lookup_info", "active": True} +]) +``` + +#### `enable_functions_on_timeout(enabled=True)` +Control whether functions can be called on speaker timeout. + +```python +result.enable_functions_on_timeout(True) +result.enable_functions_on_timeout(False) +``` + +#### `enable_extensive_data(enabled=True)` +Send full data to LLM for this turn only, then use smaller replacement. + +```python +result.enable_extensive_data(True) # Send extensive data this turn +result.enable_extensive_data(False) # Use normal data +``` + +--- + +### Agent Settings & Configuration + +#### `update_settings(settings)` +Update agent runtime settings with validation. + +```python +# AI model settings +result.update_settings({ + "temperature": 0.7, + "max-tokens": 2048, + "frequency-penalty": -0.5 +}) + +# Speech recognition settings +result.update_settings({ + "confidence": 0.8, + "barge-confidence": 0.7 +}) +``` + +**Supported Settings:** +- `frequency-penalty`: Float (-2.0 to 2.0) +- `presence-penalty`: Float (-2.0 to 2.0) +- `max-tokens`: Integer (0 to 4096) +- `top-p`: Float (0.0 to 1.0) +- `confidence`: Float (0.0 to 1.0) +- `barge-confidence`: Float (0.0 to 1.0) +- `temperature`: Float (0.0 to 2.0, clamped to 1.5) + +#### `switch_context(system_prompt=None, user_prompt=None, consolidate=False, full_reset=False)` +Change agent context/prompt during conversation. + +```python +# Simple context switch +result.switch_context("You are now a technical support agent") + +# Advanced context switch +result.switch_context( + system_prompt="You are a billing specialist", + user_prompt="The user needs help with their invoice", + consolidate=True +) +``` + +#### `simulate_user_input(text)` +Queue simulated user input for testing or flow control. + +```python +result.simulate_user_input("Yes, I'd like to speak to billing") +``` + +--- + +## Low-Level Methods + +### Manual Action Construction + +#### `add_action(name, data)` +Add a single action manually (for custom actions not covered by helper methods). + +```python +result.add_action("custom_action", {"param": "value"}) +``` + +#### `add_actions(actions)` +Add multiple actions at once. + +```python +result.add_actions([ + {"say": "Hello"}, + {"hold": 300} +]) +``` + +### Output Generation + +#### `to_dict()` +Convert result to dictionary format for JSON serialization. + +```python +result_dict = result.to_dict() +# Returns: {"response": "...", "action": [...], "post_process": true/false} +``` + +--- + +## Method Chaining + +All methods return `self` to enable fluent method chaining: + +```python +result = SwaigFunctionResult("Processing your request", post_process=True) \ + .update_global_data({"status": "processing"}) \ + .play_background_audio("processing.wav", wait=True) \ + .set_end_of_speech_timeout(2500) + +# Complex chaining example +result = SwaigFunctionResult("Let me transfer you to billing") \ + .set_metadata({"transfer_reason": "billing_inquiry"}) \ + .update_global_data({"last_action": "transfer_to_billing"}) \ + .connect("+15551234567", final=True) +``` + +--- + +## Implementation Status + +- **[IMPLEMENTED]**: `connect()`, `update_global_data()`, and all methods listed above +- **[HELPER METHODS]**: `send_sms()`, `pay()`, `record_call()`, `stop_record_call()`, `join_room()`, `sip_refer()`, `join_conference()`, `tap()`, `stop_tap()` - Additional convenience methods that generate SWML +- **[UTILITY METHODS]**: `create_payment_prompt()`, `create_payment_action()`, `create_payment_parameter()` +- **[EXTENSIBLE]**: Additional convenience methods for common SWML patterns + +## Best Practices + +1. **Use post_process=True** when you want the AI to speak before executing actions +2. **Chain methods** for cleaner, more readable code +3. **Use specific methods** instead of manual action construction when available +4. **Handle errors gracefully** - methods may raise TypeError for invalid inputs +5. **Validate settings** - update_settings() relies on server-side validation + +### Final State +The framework now includes **10 virtual helpers total**: +1. connect() - Call transfer/connect +2. send_sms() - SMS messaging +3. pay() - Payment processing +4. record_call() - Start background recording +5. stop_record_call() - Stop background recording +6. join_room() - Join RELAY room +7. sip_refer() - SIP REFER transfer +8. join_conference() - Join audio conference with extensive options +9. tap() - Start background call tap for monitoring +10. stop_tap() - Stop background call tap \ No newline at end of file diff --git a/docs/swml/agents-sdk/api/overview.mdx b/docs/swml/agents-sdk/api/overview.mdx new file mode 100644 index 00000000..82589671 --- /dev/null +++ b/docs/swml/agents-sdk/api/overview.mdx @@ -0,0 +1,3179 @@ +--- +id: agents-sdk-reference-api +slug: /agents-sdk/api +title: API Reference +sidebar_label: API Reference +--- + +# SignalWire AI Agents SDK - Complete API Reference + +This document provides a comprehensive reference for all public APIs in the SignalWire AI Agents SDK. + +--- + +## AgentBase Class + +The `AgentBase` class is the foundation for creating AI agents. It extends `SWMLService` and provides comprehensive functionality for building conversational AI agents. + +### Constructor + +```python +AgentBase( + name: str, + route: str = "/", + host: str = "0.0.0.0", + port: int = 3000, + basic_auth: Optional[Tuple[str, str]] = None, + use_pom: bool = True, + enable_state_tracking: bool = False, + token_expiry_secs: int = 3600, + auto_answer: bool = True, + record_call: bool = False, + record_format: str = "mp4", + record_stereo: bool = True, + state_manager: Optional[StateManager] = None, + default_webhook_url: Optional[str] = None, + agent_id: Optional[str] = None, + native_functions: Optional[List[str]] = None, + schema_path: Optional[str] = None, + suppress_logs: bool = False, + enable_post_prompt_override: bool = False, + check_for_input_override: bool = False +) +``` + +**Parameters:** +- `name` (str): Human-readable name for the agent +- `route` (str): HTTP route path for the agent (default: "/") +- `host` (str): Host address to bind to (default: "0.0.0.0") +- `port` (int): Port number to listen on (default: 3000) +- `basic_auth` (Optional[Tuple[str, str]]): Username/password for HTTP basic auth +- `use_pom` (bool): Whether to use Prompt Object Model (default: True) +- `enable_state_tracking` (bool): Enable persistent state management (default: False) +- `token_expiry_secs` (int): Security token expiration time (default: 3600) +- `auto_answer` (bool): Automatically answer incoming calls (default: True) +- `record_call` (bool): Record calls by default (default: False) +- `record_format` (str): Recording format: "mp4", "wav", "mp3" (default: "mp4") +- `record_stereo` (bool): Record in stereo (default: True) +- `state_manager` (Optional[StateManager]): Custom state manager instance +- `default_webhook_url` (Optional[str]): Default webhook URL for functions +- `agent_id` (Optional[str]): Unique identifier for the agent +- `native_functions` (Optional[List[str]]): List of native function names to enable +- `schema_path` (Optional[str]): Path to custom SWML schema file +- `suppress_logs` (bool): Suppress logging output (default: False) +- `enable_post_prompt_override` (bool): Allow post-prompt URL override (default: False) +- `check_for_input_override` (bool): Allow check-for-input URL override (default: False) + +### Core Methods + +#### Deployment and Execution + +##### `run(event=None, context=None, force_mode=None, host=None, port=None)` +Auto-detects deployment environment and runs the agent appropriately. + +**Parameters:** +- `event`: Event object for serverless environments +- `context`: Context object for serverless environments +- `force_mode` (str): Force specific mode: "server", "lambda", "cgi", "cloud_function" +- `host` (Optional[str]): Override host address +- `port` (Optional[int]): Override port number + +**Usage:** +```python +# Auto-detect environment +agent.run() + +# Force server mode +agent.run(force_mode="server", host="localhost", port=8080) + +# Lambda handler +def lambda_handler(event, context): + return agent.run(event, context) +``` + +##### `serve(host=None, port=None)` +Explicitly run as HTTP server using FastAPI/Uvicorn. + +**Parameters:** +- `host` (Optional[str]): Host address to bind to +- `port` (Optional[int]): Port number to listen on + +**Usage:** +```python +agent.serve() # Use constructor defaults +agent.serve(host="0.0.0.0", port=3000) +``` + +### Prompt Configuration + +#### Text-Based Prompts + +##### `set_prompt_text(text: str) -> AgentBase` +Set the agent's prompt as raw text. + +**Parameters:** +- `text` (str): The complete prompt text + +**Usage:** +```python +agent.set_prompt_text("You are a helpful customer service agent.") +``` + +##### `set_post_prompt(text: str) -> AgentBase` +Set additional text to append after the main prompt. + +**Parameters:** +- `text` (str): Text to append after main prompt + +**Usage:** +```python +agent.set_post_prompt("Always be polite and professional.") +``` + +#### Structured Prompts (POM) + +##### `prompt_add_section` + +```python +def prompt_add_section( + title: str, + body: str = "", + bullets: Optional[List[str]] = None, + numbered: bool = False, + numbered_bullets: bool = False, + subsections: Optional[List[Dict[str, Any]]] = None +) -> AgentBase +``` +Add a structured section to the prompt using Prompt Object Model. + +**Parameters:** +- `title` (str): Section title/heading +- `body` (str): Main section content (default: "") +- `bullets` (Optional[List[str]]): List of bullet points +- `numbered` (bool): Use numbered sections (default: False) +- `numbered_bullets` (bool): Use numbered bullet points (default: False) +- `subsections` (Optional[List[Dict]]): Nested subsections + +**Usage:** +```python +# Simple section +agent.prompt_add_section("Role", "You are a customer service representative.") + +# Section with bullets +agent.prompt_add_section( + "Guidelines", + "Follow these principles:", + bullets=["Be helpful", "Stay professional", "Listen carefully"] +) + +# Numbered bullets +agent.prompt_add_section( + "Process", + "Follow these steps:", + bullets=["Greet the customer", "Identify their need", "Provide solution"], + numbered_bullets=True +) +``` + +##### `prompt_add_to_section` + +```python +def prompt_add_to_section( + title: str, + body: Optional[str] = None, + bullet: Optional[str] = None, + bullets: Optional[List[str]] = None +) -> AgentBase +``` +Add content to an existing prompt section. + +**Parameters:** +- `title` (str): Title of existing section to modify +- `body` (Optional[str]): Additional body text to append +- `bullet` (Optional[str]): Single bullet point to add +- `bullets` (Optional[List[str]]): Multiple bullet points to add + +**Usage:** +```python +# Add body text to existing section +agent.prompt_add_to_section("Guidelines", "Remember to always verify customer identity.") + +# Add single bullet +agent.prompt_add_to_section("Process", bullet="Document the interaction") + +# Add multiple bullets +agent.prompt_add_to_section("Process", bullets=["Follow up", "Close ticket"]) +``` + +##### `prompt_add_subsection` + +```python +def prompt_add_subsection( + parent_title: str, + title: str, + body: str = "", + bullets: Optional[List[str]] = None +) -> AgentBase +``` +Add a subsection to an existing prompt section. + +**Parameters:** +- `parent_title` (str): Title of parent section +- `title` (str): Subsection title +- `body` (str): Subsection content (default: "") +- `bullets` (Optional[List[str]]): Subsection bullet points + +**Usage:** +```python +agent.prompt_add_subsection( + "Guidelines", + "Escalation Rules", + "Escalate when:", + bullets=["Customer is angry", "Technical issue beyond scope"] +) +``` + +### Voice and Language Configuration + +##### `add_language` + +```python +def add_language( + name: str, + code: str, + voice: str, + speech_fillers: Optional[List[str]] = None, + function_fillers: Optional[List[str]] = None, + engine: Optional[str] = None, + model: Optional[str] = None +) -> AgentBase +``` +Configure voice and language settings for the agent. + +**Parameters:** +- `name` (str): Human-readable language name +- `code` (str): Language code (e.g., "en-US", "es-ES") +- `voice` (str): Voice identifier (e.g., "rime.spore", "nova.luna") +- `speech_fillers` (Optional[List[str]]): Filler phrases during speech processing +- `function_fillers` (Optional[List[str]]): Filler phrases during function execution +- `engine` (Optional[str]): TTS engine to use +- `model` (Optional[str]): AI model to use + +**Usage:** +```python +# Basic language setup +agent.add_language("English", "en-US", "rime.spore") + +# With custom fillers +agent.add_language( + "English", + "en-US", + "nova.luna", + speech_fillers=["Let me think...", "One moment..."], + function_fillers=["Processing...", "Looking that up..."] +) +``` + +##### `set_languages(languages: List[Dict[str, Any]]) -> AgentBase` +Set multiple language configurations at once. + +**Parameters:** +- `languages` (List[Dict]): List of language configuration dictionaries + +**Usage:** +```python +agent.set_languages([ + {"name": "English", "code": "en-US", "voice": "rime.spore"}, + {"name": "Spanish", "code": "es-ES", "voice": "nova.luna"} +]) +``` + +### Speech Recognition Configuration + +##### `add_hint(hint: str) -> AgentBase` +Add a single speech recognition hint. + +**Parameters:** +- `hint` (str): Word or phrase to improve recognition accuracy + +**Usage:** +```python +agent.add_hint("SignalWire") +``` + +##### `add_hints(hints: List[str]) -> AgentBase` +Add multiple speech recognition hints. + +**Parameters:** +- `hints` (List[str]): List of words/phrases for better recognition + +**Usage:** +```python +agent.add_hints(["SignalWire", "SWML", "API", "webhook", "SIP"]) +``` + +##### `add_pattern_hint` + +```python +def add_pattern_hint( + hint: str, + pattern: str, + replace: str, + ignore_case: bool = False +) -> AgentBase +``` +Add a pattern-based hint for speech recognition. + +**Parameters:** +- `hint` (str): The hint phrase +- `pattern` (str): Regex pattern to match +- `replace` (str): Replacement text +- `ignore_case` (bool): Case-insensitive matching (default: False) + +**Usage:** +```python +agent.add_pattern_hint( + "phone number", + r"(\d{3})-(\d{3})-(\d{4})", + r"(\1) \2-\3" +) +``` + +##### `add_pronunciation` + +```python +def add_pronunciation( + replace: str, + with_text: str, + ignore_case: bool = False +) -> AgentBase +``` +Add pronunciation rules for text-to-speech. + +**Parameters:** +- `replace` (str): Text to replace +- `with_text` (str): Replacement pronunciation +- `ignore_case` (bool): Case-insensitive replacement (default: False) + +**Usage:** +```python +agent.add_pronunciation("API", "A P I") +agent.add_pronunciation("SWML", "swim-el") +``` + +##### `set_pronunciations` + +```python +def set_pronunciations( + pronunciations: List[Dict[str, Any]] +) -> AgentBase +``` +Set multiple pronunciation rules at once. + +**Parameters:** +- `pronunciations` (List[Dict]): List of pronunciation rule dictionaries + +**Usage:** +```python +agent.set_pronunciations([ + {"replace": "API", "with": "A P I"}, + {"replace": "SWML", "with": "swim-el", "ignore_case": True} +]) +``` + +### AI Parameters Configuration + +##### `set_param(key: str, value: Any) -> AgentBase` +Set a single AI parameter. + +**Parameters:** +- `key` (str): Parameter name +- `value` (Any): Parameter value + +**Usage:** +```python +agent.set_param("ai_model", "gpt-4.1-nano") +agent.set_param("end_of_speech_timeout", 500) +``` + +##### `set_params(params: Dict[str, Any]) -> AgentBase` +Set multiple AI parameters at once. + +**Parameters:** +- `params` (Dict[str, Any]): Dictionary of parameter key-value pairs + +**Common Parameters:** +- `ai_model`: AI model to use ("gpt-4.1-nano", "gpt-4.1-mini", etc.) +- `end_of_speech_timeout`: Milliseconds to wait for speech end (default: 1000) +- `attention_timeout`: Milliseconds before attention timeout (default: 30000) +- `background_file_volume`: Volume for background audio (-60 to 0 dB) +- `temperature`: AI creativity/randomness (0.0 to 2.0) +- `max_tokens`: Maximum response length +- `top_p`: Nucleus sampling parameter (0.0 to 1.0) + +**Usage:** +```python +agent.set_params({ + "ai_model": "gpt-4.1-nano", + "end_of_speech_timeout": 500, + "attention_timeout": 15000, + "background_file_volume": -20, + "temperature": 0.7 +}) +``` + +### Global Data Management + +##### `set_global_data(data: Dict[str, Any]) -> AgentBase` +Set global data available to the AI and functions. + +**Parameters:** +- `data` (Dict[str, Any]): Global data dictionary + +**Usage:** +```python +agent.set_global_data({ + "company_name": "Acme Corp", + "support_hours": "9 AM - 5 PM EST", + "escalation_number": "+1-555-0123" +}) +``` + +##### `update_global_data(data: Dict[str, Any]) -> AgentBase` +Update existing global data (merge with existing). + +**Parameters:** +- `data` (Dict[str, Any]): Data to merge with existing global data + +**Usage:** +```python +agent.update_global_data({ + "current_promotion": "20% off all services", + "promotion_expires": "2024-12-31" +}) +``` + +### Function Definition + +##### `define_tool` + +```python +def define_tool( + name: str, + description: str, + parameters: Dict[str, Any], + handler: Callable, + secure: bool = True, + fillers: Optional[Dict[str, List[str]]] = None, + webhook_url: Optional[str] = None, + **swaig_fields +) -> AgentBase +``` +Define a custom SWAIG function/tool. + +**Parameters:** +- `name` (str): Function name +- `description` (str): Function description for AI +- `parameters` (Dict[str, Any]): JSON schema for function parameters +- `handler` (Callable): Function to execute when called +- `secure` (bool): Require security token (default: True) +- `fillers` (Optional[Dict[str, List[str]]]): Language-specific filler phrases +- `webhook_url` (Optional[str]): Custom webhook URL +- `**swaig_fields`: Additional SWAIG function properties + +**Usage:** +```python +def get_weather(args, raw_data): + location = args.get("location", "Unknown") + return SwaigFunctionResult(f"The weather in {location} is sunny and 75°F") + +agent.define_tool( + name="get_weather", + description="Get current weather for a location", + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name" + } + }, + "required": ["location"] + }, + handler=get_weather, + fillers={"en-US": ["Checking weather...", "Looking up forecast..."]} +) +``` + +##### `@AgentBase.tool(name=None, **kwargs)` (Class Decorator) +Decorator for defining tools as class methods. + +**Parameters:** +- `name` (Optional[str]): Function name (defaults to method name) +- `**kwargs`: Same parameters as `define_tool()` + +**Usage:** +```python +class MyAgent(AgentBase): + @AgentBase.tool( + description="Get current time", + parameters={"type": "object", "properties": {}} + ) + def get_time(self, args, raw_data): + import datetime + return SwaigFunctionResult(f"Current time: {datetime.datetime.now()}") +``` + +##### `register_swaig_function` + +```python +def register_swaig_function( + function_dict: Dict[str, Any] +) -> AgentBase +``` +Register a pre-built SWAIG function dictionary. + +**Parameters:** +- `function_dict` (Dict[str, Any]): Complete SWAIG function definition + +**Usage:** +```python +# Register a DataMap tool +weather_tool = DataMap('get_weather').webhook('GET', 'https://api.weather.com/...') +agent.register_swaig_function(weather_tool.to_swaig_function()) +``` + +### Skills System + +##### `add_skill` + +```python +def add_skill( + skill_name: str, + params: Optional[Dict[str, Any]] = None +) -> AgentBase +``` +Add a modular skill to the agent. + +**Parameters:** +- `skill_name` (str): Name of the skill to add +- `params` (Optional[Dict[str, Any]]): Skill configuration parameters + +**Available Skills:** +- `datetime`: Current date/time information +- `math`: Mathematical calculations +- `web_search`: Google Custom Search integration +- `datasphere`: SignalWire DataSphere search +- `native_vector_search`: Local document search + +**Usage:** +```python +# Simple skill +agent.add_skill("datetime") +agent.add_skill("math") + +# Skill with configuration +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id", + "num_results": 3 +}) + +# Multiple instances with different names +agent.add_skill("web_search", { + "api_key": "your-api-key", + "search_engine_id": "general-engine", + "tool_name": "search_general" +}) + +agent.add_skill("web_search", { + "api_key": "your-api-key", + "search_engine_id": "news-engine", + "tool_name": "search_news" +}) +``` + +##### `remove_skill(skill_name: str) -> AgentBase` +Remove a skill from the agent. + +**Parameters:** +- `skill_name` (str): Name of skill to remove + +**Usage:** +```python +agent.remove_skill("web_search") +``` + +##### `list_skills() -> List[str]` +Get list of currently added skills. + +**Returns:** +- List[str]: Names of active skills + +**Usage:** +```python +active_skills = agent.list_skills() +print(f"Active skills: {active_skills}") +``` + +##### `has_skill(skill_name: str) -> bool` +Check if a skill is currently added. + +**Parameters:** +- `skill_name` (str): Name of skill to check + +**Returns:** +- bool: True if skill is active + +**Usage:** +```python +if agent.has_skill("web_search"): + print("Web search is available") +``` + +### Native Functions + +##### `set_native_functions` + +```python +def set_native_functions( + function_names: List[str] +) -> AgentBase +``` +Enable specific native SWML functions. + +**Parameters:** +- `function_names` (List[str]): List of native function names to enable + +**Available Native Functions:** +- `transfer`: Transfer calls +- `hangup`: End calls +- `play`: Play audio files +- `record`: Record audio +- `send_sms`: Send SMS messages + +**Usage:** +```python +agent.set_native_functions(["transfer", "hangup", "send_sms"]) +``` + +##### `set_internal_fillers` + +```python +def set_internal_fillers( + internal_fillers: Dict[str, Dict[str, List[str]]] +) -> AgentBase +``` +Set custom filler phrases for internal/native SWAIG functions. + +**Parameters:** +- `internal_fillers` (Dict[str, Dict[str, List[str]]]): Function name → language code → filler phrases + +**Available Internal Functions:** +- `next_step`: Moving between workflow steps (contexts system) +- `change_context`: Switching contexts in workflows +- `check_time`: Getting current time +- `wait_for_user`: Waiting for user input +- `wait_seconds`: Pausing for specified duration +- `get_visual_input`: Processing visual data + +**Usage:** +```python +agent.set_internal_fillers({ + "next_step": { + "en-US": ["Moving to the next step...", "Let's continue..."], + "es": ["Pasando al siguiente paso...", "Continuemos..."] + }, + "check_time": { + "en-US": ["Let me check the time...", "Getting current time..."] + } +}) +``` + +##### `add_internal_filler` + +```python +def add_internal_filler( + function_name: str, + language_code: str, + fillers: List[str] +) -> AgentBase +``` +Add internal fillers for a specific function and language. + +**Parameters:** +- `function_name` (str): Name of the internal function +- `language_code` (str): Language code (e.g., "en-US", "es", "fr") +- `fillers` (List[str]): List of filler phrases + +**Usage:** +```python +agent.add_internal_filler("next_step", "en-US", [ + "Great! Let's move to the next step...", + "Perfect! Moving forward..." +]) +``` + +### Function Includes + +##### `add_function_include` + +```python +def add_function_include( + url: str, + functions: List[str], + meta_data: Optional[Dict[str, Any]] = None +) -> AgentBase +``` +Include external SWAIG functions from another service. + +**Parameters:** +- `url` (str): URL of external SWAIG service +- `functions` (List[str]): List of function names to include +- `meta_data` (Optional[Dict[str, Any]]): Additional metadata + +**Usage:** +```python +agent.add_function_include( + "https://external-service.com/swaig", + ["external_function1", "external_function2"], + meta_data={"service": "external", "version": "1.0"} +) +``` + +##### `set_function_includes` + +```python +def set_function_includes( + includes: List[Dict[str, Any]] +) -> AgentBase +``` +Set multiple function includes at once. + +**Parameters:** +- `includes` (List[Dict[str, Any]]): List of function include configurations + +**Usage:** +```python +agent.set_function_includes([ + { + "url": "https://service1.com/swaig", + "functions": ["func1", "func2"] + }, + { + "url": "https://service2.com/swaig", + "functions": ["func3"], + "meta_data": {"priority": "high"} + } +]) +``` + +### Webhook Configuration + +##### `set_web_hook_url(url: str) -> AgentBase` +Set default webhook URL for SWAIG functions. + +**Parameters:** +- `url` (str): Default webhook URL + +**Usage:** +```python +agent.set_web_hook_url("https://myserver.com/webhook") +``` + +##### `set_post_prompt_url(url: str) -> AgentBase` +Set URL for post-prompt processing. + +**Parameters:** +- `url` (str): Post-prompt webhook URL + +**Usage:** +```python +agent.set_post_prompt_url("https://myserver.com/post-prompt") +``` + +### Dynamic Configuration + +##### `set_dynamic_config_callback` + +```python +def set_dynamic_config_callback( + callback: Callable[[dict, dict, dict, EphemeralAgentConfig], None] +) -> AgentBase +``` +Set callback for per-request dynamic configuration. + +**Parameters:** +- `callback` (Callable): Function that receives (query_params, headers, body, config) + +**Usage:** +```python +def configure_agent(query_params, headers, body, config): + # Configure based on request + if query_params.get("language") == "spanish": + config.add_language("Spanish", "es-ES", "nova.luna") + + # Set customer-specific data + customer_id = headers.get("X-Customer-ID") + if customer_id: + config.set_global_data({"customer_id": customer_id}) + +agent.set_dynamic_config_callback(configure_agent) +``` + +### SIP Integration + +##### `enable_sip_routing` + +```python +def enable_sip_routing( + auto_map: bool = True, + path: str = "/sip" +) -> AgentBase +``` +Enable SIP-based routing for voice calls. + +**Parameters:** +- `auto_map` (bool): Automatically map SIP usernames (default: True) +- `path` (str): SIP routing endpoint path (default: "/sip") + +**Usage:** +```python +agent.enable_sip_routing() +``` + +##### `register_sip_username(sip_username: str) -> AgentBase` +Register a specific SIP username for this agent. + +**Parameters:** +- `sip_username` (str): SIP username to register + +**Usage:** +```python +agent.register_sip_username("support") +agent.register_sip_username("sales") +``` + +##### `register_routing_callback` + +```python +def register_routing_callback( + callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]], + path: str = "/sip" +) -> None +``` +Register custom routing logic for SIP calls. + +**Parameters:** +- `callback_fn` (Callable): Function that returns agent route based on request +- `path` (str): Routing endpoint path (default: "/sip") + +**Usage:** +```python +def route_call(request, body): + sip_username = body.get("sip_username") + if sip_username == "support": + return "/support-agent" + elif sip_username == "sales": + return "/sales-agent" + return None + +agent.register_routing_callback(route_call) +``` + +### Utility Methods + +##### `get_name() -> str` +Get the agent's name. + +**Returns:** +- str: Agent name + +##### `get_app()` +Get the FastAPI application instance. + +**Returns:** +- FastAPI: The underlying FastAPI app + +##### `as_router() -> APIRouter` +Get the agent as a FastAPI router for embedding in larger applications. + +**Returns:** +- APIRouter: FastAPI router instance + +**Usage:** +```python +# Embed agent in larger FastAPI app +main_app = FastAPI() +agent_router = agent.as_router() +main_app.include_router(agent_router, prefix="/agent") +``` + +### Event Handlers + +##### `on_summary` + +```python +def on_summary( + summary: Optional[Dict[str, Any]], + raw_data: Optional[Dict[str, Any]] = None +) -> None +``` +Override to handle conversation summaries. + +**Parameters:** +- `summary` (Optional[Dict[str, Any]]): Summary data +- `raw_data` (Optional[Dict[str, Any]]): Raw request data + +**Usage:** +```python +class MyAgent(AgentBase): + def on_summary(self, summary, raw_data): + print(f"Conversation summary: {summary}") + # Save to database, send notification, etc. +``` + +##### `on_function_call` + +```python +def on_function_call( + name: str, + args: Dict[str, Any], + raw_data: Optional[Dict[str, Any]] = None +) -> Any +``` +Override to handle function calls with custom logic. + +**Parameters:** +- `name` (str): Function name being called +- `args` (Dict[str, Any]): Function arguments +- `raw_data` (Optional[Dict[str, Any]]): Raw request data + +**Returns:** +- Any: Function result (typically SwaigFunctionResult) + +**Usage:** +```python +class MyAgent(AgentBase): + def on_function_call(self, name, args, raw_data): + if name == "get_weather": + location = args.get("location") + # Custom weather logic + return SwaigFunctionResult(f"Weather in {location}: Sunny") + return super().on_function_call(name, args, raw_data) +``` + +##### `on_request` + +```python +def on_request( + request_data: Optional[dict] = None, + callback_path: Optional[str] = None +) -> Optional[dict] +``` +Override to handle general requests. + +**Parameters:** +- `request_data` (Optional[dict]): Request data +- `callback_path` (Optional[str]): Callback path + +**Returns:** +- Optional[dict]: Response modifications + +##### `on_swml_request` + +```python +def on_swml_request( + request_data: Optional[dict] = None, + callback_path: Optional[str] = None, + request: Optional[Request] = None +) -> Optional[dict] +``` +Override to handle SWML generation requests. + +**Parameters:** +- `request_data` (Optional[dict]): Request data +- `callback_path` (Optional[str]): Callback path +- `request` (Optional[Request]): FastAPI request object + +**Returns:** +- Optional[dict]: SWML modifications + +### Authentication + +##### `validate_basic_auth(username: str, password: str) -> bool` +Override to implement custom basic authentication logic. + +**Parameters:** +- `username` (str): Username from basic auth +- `password` (str): Password from basic auth + +**Returns:** +- bool: True if credentials are valid + +**Usage:** +```python +class MyAgent(AgentBase): + def validate_basic_auth(self, username, password): + # Custom auth logic + return username == "admin" and password == "secret" +``` + +##### `get_basic_auth_credentials` + +```python +def get_basic_auth_credentials( + include_source: bool = False +) -> Union[Tuple[str, str], Tuple[str, str, str]] +``` +Get basic auth credentials from environment or constructor. + +**Parameters:** +- `include_source` (bool): Include source information (default: False) + +**Returns:** +- Tuple: (username, password) or (username, password, source) + +### Context System + +##### `define_contexts() -> ContextBuilder` +Define structured workflow contexts for the agent. + +**Returns:** +- ContextBuilder: Builder for creating contexts and steps + +**Usage:** +```python +contexts = agent.define_contexts() +contexts.add_context("greeting") \ + .add_step("welcome", "Welcome! How can I help?") \ + .on_completion_go_to("main_menu") + +contexts.add_context("main_menu") \ + .add_step("menu", "Choose: 1) Support 2) Sales 3) Billing") \ + .allow_functions(["transfer_to_support", "transfer_to_sales"]) +``` + +This concludes Part 1 of the API reference covering the AgentBase class. The document will continue with SwaigFunctionResult, DataMap, and other components in subsequent parts. + +--- + +## SwaigFunctionResult Class + +The `SwaigFunctionResult` class is used to create structured responses from SWAIG functions. It handles both natural language responses and structured actions that the agent should execute. + +### Constructor + +```python +SwaigFunctionResult( + response: Optional[str] = None, + post_process: bool = False +) +``` + +**Parameters:** +- `response` (Optional[str]): Natural language response text for the AI to speak +- `post_process` (bool): Whether to let AI take another turn before executing actions (default: False) + +**Post-processing Behavior:** +- `post_process=False` (default): Execute actions immediately after AI response +- `post_process=True`: Let AI respond to user one more time, then execute actions + +**Usage:** +```python +# Simple response +result = SwaigFunctionResult("The weather is sunny and 75°F") + +# Response with post-processing enabled +result = SwaigFunctionResult("I'll transfer you now", post_process=True) + +# Empty response (actions only) +result = SwaigFunctionResult() +``` + +### Core Methods + +#### Response Configuration + +##### `set_response(response: str) -> SwaigFunctionResult` +Set or update the natural language response text. + +**Parameters:** +- `response` (str): The text the AI should speak + +**Usage:** +```python +result = SwaigFunctionResult() +result.set_response("I found your order information") +``` + +##### `set_post_process(post_process: bool) -> SwaigFunctionResult` +Enable or disable post-processing for this result. + +**Parameters:** +- `post_process` (bool): True to let AI respond once more before executing actions + +**Usage:** +```python +result = SwaigFunctionResult("I'll help you with that") +result.set_post_process(True) # Let AI handle follow-up questions first +``` + +#### Action Management + +##### `add_action(name: str, data: Any) -> SwaigFunctionResult` +Add a structured action to execute. + +**Parameters:** +- `name` (str): Action name/type (e.g., "play", "transfer", "set_global_data") +- `data` (Any): Action data - can be string, boolean, object, or array + +**Usage:** +```python +# Simple action with boolean +result.add_action("hangup", True) + +# Action with string data +result.add_action("play", "welcome.mp3") + +# Action with object data +result.add_action("set_global_data", {"customer_id": "12345", "status": "verified"}) + +# Action with array data +result.add_action("send_sms", ["+15551234567", "Your order is ready!"]) +``` + +##### `add_actions(actions: List[Dict[str, Any]]) -> SwaigFunctionResult` +Add multiple actions at once. + +**Parameters:** +- `actions` (List[Dict[str, Any]]): List of action dictionaries + +**Usage:** +```python +result.add_actions([ + {"play": "hold_music.mp3"}, + {"set_global_data": {"status": "on_hold"}}, + {"wait": 5000} +]) +``` + +### Call Control Actions + +#### Call Transfer and Connection + +##### `connect(destination: str, final: bool = True, from_addr: Optional[str] = None) -> SwaigFunctionResult` +Transfer or connect the call to another destination. + +**Parameters:** +- `destination` (str): Phone number, SIP address, or other destination +- `final` (bool): Permanent transfer (True) vs temporary transfer (False) (default: True) +- `from_addr` (Optional[str]): Override caller ID + +**Transfer Types:** +- `final=True`: Permanent transfer - call exits agent completely +- `final=False`: Temporary transfer - call returns to agent if far end hangs up + +**Usage:** +```python +# Permanent transfer to phone number +result.connect("+15551234567", final=True) + +# Temporary transfer to SIP address with custom caller ID +result.connect("support@company.com", final=False, from_addr="+15559876543") + +# Transfer with response +result = SwaigFunctionResult("Transferring you to our sales team") +result.connect("sales@company.com") +``` + +##### `swml_transfer(dest: str, ai_response: str) -> SwaigFunctionResult` +Create a SWML-based transfer with AI response setup. + +**Parameters:** +- `dest` (str): Transfer destination +- `ai_response` (str): AI response when transfer completes + +**Usage:** +```python +result.swml_transfer( + "+15551234567", + "You've been transferred back to me. How else can I help?" +) +``` + +##### `sip_refer(to_uri: str) -> SwaigFunctionResult` +Perform a SIP REFER transfer. + +**Parameters:** +- `to_uri` (str): SIP URI to transfer to + +**Usage:** +```python +result.sip_refer("sip:support@company.com") +``` + +#### Call Management + +##### `hangup() -> SwaigFunctionResult` +End the call immediately. + +**Usage:** +```python +result = SwaigFunctionResult("Thank you for calling. Goodbye!") +result.hangup() +``` + +##### `hold(timeout: int = 300) -> SwaigFunctionResult` +Put the call on hold. + +**Parameters:** +- `timeout` (int): Hold timeout in seconds (default: 300) + +**Usage:** +```python +result = SwaigFunctionResult("Please hold while I look that up") +result.hold(timeout=60) +``` + +##### `stop() -> SwaigFunctionResult` +Stop current audio playback or recording. + +**Usage:** +```python +result.stop() +``` + +#### Audio Control + +##### `say(text: str) -> SwaigFunctionResult` +Add text for the AI to speak. + +**Parameters:** +- `text` (str): Text to speak + +**Usage:** +```python +result.say("Please wait while I process your request") +``` + +##### `play_background_file(filename: str, wait: bool = False) -> SwaigFunctionResult` +Play an audio file in the background. + +**Parameters:** +- `filename` (str): Audio file path or URL +- `wait` (bool): Wait for file to finish before continuing (default: False) + +**Usage:** +```python +# Play hold music in background +result.play_background_file("hold_music.mp3") + +# Play announcement and wait for completion +result.play_background_file("important_announcement.wav", wait=True) +``` + +##### `stop_background_file() -> SwaigFunctionResult` +Stop background audio playback. + +**Usage:** +```python +result.stop_background_file() +``` + +### Data Management Actions + +##### `set_global_data(data: Dict[str, Any]) -> SwaigFunctionResult` +Set global data for the conversation. + +**Parameters:** +- `data` (Dict[str, Any]): Global data to set + +**Usage:** +```python +result.set_global_data({ + "customer_id": "12345", + "order_status": "shipped", + "tracking_number": "1Z999AA1234567890" +}) +``` + +##### `update_global_data(data: Dict[str, Any]) -> SwaigFunctionResult` +Update existing global data (merge with existing). + +**Parameters:** +- `data` (Dict[str, Any]): Data to merge + +**Usage:** +```python +result.update_global_data({ + "last_interaction": "2024-01-15T10:30:00Z", + "agent_notes": "Customer satisfied with resolution" +}) +``` + +##### `remove_global_data(keys: Union[str, List[str]]) -> SwaigFunctionResult` +Remove specific keys from global data. + +**Parameters:** +- `keys` (Union[str, List[str]]): Key name or list of key names to remove + +**Usage:** +```python +# Remove single key +result.remove_global_data("temporary_data") + +# Remove multiple keys +result.remove_global_data(["temp1", "temp2", "cache_data"]) +``` + +##### `set_metadata(data: Dict[str, Any]) -> SwaigFunctionResult` +Set metadata for the conversation. + +**Parameters:** +- `data` (Dict[str, Any]): Metadata to set + +**Usage:** +```python +result.set_metadata({ + "call_type": "support", + "priority": "high", + "department": "technical" +}) +``` + +##### `remove_metadata(keys: Union[str, List[str]]) -> SwaigFunctionResult` +Remove specific metadata keys. + +**Parameters:** +- `keys` (Union[str, List[str]]): Key name or list of key names to remove + +**Usage:** +```python +result.remove_metadata(["temporary_flag", "debug_info"]) +``` + +### AI Behavior Control + +##### `set_end_of_speech_timeout(milliseconds: int) -> SwaigFunctionResult` +Adjust how long to wait for speech to end. + +**Parameters:** +- `milliseconds` (int): Timeout in milliseconds + +**Usage:** +```python +# Shorter timeout for quick responses +result.set_end_of_speech_timeout(300) + +# Longer timeout for thoughtful responses +result.set_end_of_speech_timeout(2000) +``` + +##### `set_speech_event_timeout(milliseconds: int) -> SwaigFunctionResult` +Set timeout for speech events. + +**Parameters:** +- `milliseconds` (int): Timeout in milliseconds + +**Usage:** +```python +result.set_speech_event_timeout(5000) +``` + +##### `wait_for_user(enabled: Optional[bool] = None, timeout: Optional[int] = None, answer_first: bool = False) -> SwaigFunctionResult` +Control whether to wait for user input. + +**Parameters:** +- `enabled` (Optional[bool]): Enable/disable waiting for user +- `timeout` (Optional[int]): Timeout in milliseconds +- `answer_first` (bool): Answer call before waiting (default: False) + +**Usage:** +```python +# Wait for user input with 10 second timeout +result.wait_for_user(enabled=True, timeout=10000) + +# Don't wait for user (immediate response) +result.wait_for_user(enabled=False) +``` + +##### `toggle_functions(function_toggles: List[Dict[str, Any]]) -> SwaigFunctionResult` +Enable or disable specific functions. + +**Parameters:** +- `function_toggles` (List[Dict[str, Any]]): List of function toggle configurations + +**Usage:** +```python +result.toggle_functions([ + {"name": "transfer_to_sales", "enabled": True}, + {"name": "end_call", "enabled": False}, + {"name": "escalate", "enabled": True, "timeout": 30000} +]) +``` + +##### `enable_functions_on_timeout(enabled: bool = True) -> SwaigFunctionResult` +Control whether functions are enabled when timeout occurs. + +**Parameters:** +- `enabled` (bool): Enable functions on timeout (default: True) + +**Usage:** +```python +result.enable_functions_on_timeout(False) # Disable functions on timeout +``` + +##### `enable_extensive_data(enabled: bool = True) -> SwaigFunctionResult` +Enable extensive data collection. + +**Parameters:** +- `enabled` (bool): Enable extensive data (default: True) + +**Usage:** +```python +result.enable_extensive_data(True) +``` + +##### `update_settings(settings: Dict[str, Any]) -> SwaigFunctionResult` +Update various AI settings. + +**Parameters:** +- `settings` (Dict[str, Any]): Settings to update + +**Usage:** +```python +result.update_settings({ + "temperature": 0.8, + "max_tokens": 150, + "end_of_speech_timeout": 800 +}) +``` + +### Context and Conversation Control + +##### `switch_context(system_prompt: Optional[str] = None, user_prompt: Optional[str] = None, consolidate: bool = False, full_reset: bool = False) -> SwaigFunctionResult` +Switch conversation context or reset the conversation. + +**Parameters:** +- `system_prompt` (Optional[str]): New system prompt +- `user_prompt` (Optional[str]): New user prompt +- `consolidate` (bool): Consolidate conversation history (default: False) +- `full_reset` (bool): Completely reset conversation (default: False) + +**Usage:** +```python +# Switch to technical support context +result.switch_context( + system_prompt="You are now a technical support specialist", + user_prompt="The customer needs technical help" +) + +# Reset conversation completely +result.switch_context(full_reset=True) + +# Consolidate conversation history +result.switch_context(consolidate=True) +``` + +##### `simulate_user_input(text: str) -> SwaigFunctionResult` +Simulate user input for testing or automation. + +**Parameters:** +- `text` (str): Text to simulate as user input + +**Usage:** +```python +result.simulate_user_input("I need help with my order") +``` + +### Communication Actions + +##### `send_sms(to_number: str, from_number: str, body: Optional[str] = None, media: Optional[List[str]] = None, tags: Optional[List[str]] = None, region: Optional[str] = None) -> SwaigFunctionResult` +Send an SMS message. + +**Parameters:** +- `to_number` (str): Recipient phone number +- `from_number` (str): Sender phone number +- `body` (Optional[str]): SMS message text +- `media` (Optional[List[str]]): List of media URLs +- `tags` (Optional[List[str]]): Message tags +- `region` (Optional[str]): SignalWire region + +**Usage:** +```python +# Simple text message +result.send_sms( + to_number="+15551234567", + from_number="+15559876543", + body="Your order #12345 has shipped!" +) + +# Message with media and tags +result.send_sms( + to_number="+15551234567", + from_number="+15559876543", + body="Here's your receipt", + media=["https://example.com/receipt.pdf"], + tags=["receipt", "order_12345"] +) +``` + +### Recording and Media + +##### `record_call(control_id: Optional[str] = None, stereo: bool = False, format: str = "wav", direction: str = "both", terminators: Optional[str] = None, beep: bool = False, input_sensitivity: float = 44.0, initial_timeout: float = 0.0, end_silence_timeout: float = 0.0, max_length: Optional[float] = None, status_url: Optional[str] = None) -> SwaigFunctionResult` +Start call recording. + +**Parameters:** +- `control_id` (Optional[str]): Unique identifier for this recording +- `stereo` (bool): Record in stereo (default: False) +- `format` (str): Recording format: "wav", "mp3", "mp4" (default: "wav") +- `direction` (str): Recording direction: "both", "inbound", "outbound" (default: "both") +- `terminators` (Optional[str]): DTMF keys to stop recording +- `beep` (bool): Play beep before recording (default: False) +- `input_sensitivity` (float): Input sensitivity level (default: 44.0) +- `initial_timeout` (float): Initial timeout in seconds (default: 0.0) +- `end_silence_timeout` (float): End silence timeout in seconds (default: 0.0) +- `max_length` (Optional[float]): Maximum recording length in seconds +- `status_url` (Optional[str]): Webhook URL for recording status + +**Usage:** +```python +# Basic recording +result.record_call(format="mp3", direction="both") + +# Recording with control ID and settings +result.record_call( + control_id="customer_call_001", + stereo=True, + format="wav", + beep=True, + max_length=300.0, + terminators="#*" +) +``` + +##### `stop_record_call(control_id: Optional[str] = None) -> SwaigFunctionResult` +Stop call recording. + +**Parameters:** +- `control_id` (Optional[str]): Control ID of recording to stop + +**Usage:** +```python +result.stop_record_call() +result.stop_record_call(control_id="customer_call_001") +``` + +### Conference and Room Management + +##### `join_room(name: str) -> SwaigFunctionResult` +Join a SignalWire room. + +**Parameters:** +- `name` (str): Room name to join + +**Usage:** +```python +result.join_room("support_room_1") +``` + +##### `join_conference(name: str, muted: bool = False, beep: str = "true", start_on_enter: bool = True, end_on_exit: bool = False, wait_url: Optional[str] = None, max_participants: int = 250, record: str = "do-not-record", region: Optional[str] = None, trim: str = "trim-silence", coach: Optional[str] = None, status_callback_event: Optional[str] = None, status_callback: Optional[str] = None, status_callback_method: str = "POST", recording_status_callback: Optional[str] = None, recording_status_callback_method: str = "POST", recording_status_callback_event: str = "completed", result: Optional[Any] = None) -> SwaigFunctionResult` +Join a conference call. + +**Parameters:** +- `name` (str): Conference name +- `muted` (bool): Join muted (default: False) +- `beep` (str): Beep setting: "true", "false", "onEnter", "onExit" (default: "true") +- `start_on_enter` (bool): Start conference when this participant enters (default: True) +- `end_on_exit` (bool): End conference when this participant exits (default: False) +- `wait_url` (Optional[str]): URL for hold music/content +- `max_participants` (int): Maximum participants (default: 250) +- `record` (str): Recording setting (default: "do-not-record") +- `region` (Optional[str]): SignalWire region +- `trim` (str): Trim setting for recordings (default: "trim-silence") +- `coach` (Optional[str]): Coach participant identifier +- `status_callback_event` (Optional[str]): Status callback events +- `status_callback` (Optional[str]): Status callback URL +- `status_callback_method` (str): Status callback HTTP method (default: "POST") +- `recording_status_callback` (Optional[str]): Recording status callback URL +- `recording_status_callback_method` (str): Recording status callback method (default: "POST") +- `recording_status_callback_event` (str): Recording status callback events (default: "completed") + +**Usage:** +```python +# Basic conference join +result.join_conference("sales_meeting") + +# Conference with recording and settings +result.join_conference( + name="support_conference", + muted=False, + beep="onEnter", + record="record-from-start", + max_participants=10 +) +``` + +### Payment Processing + +##### `pay(payment_connector_url: str, input_method: str = "dtmf", status_url: Optional[str] = None, payment_method: str = "credit-card", timeout: int = 5, max_attempts: int = 1, security_code: bool = True, postal_code: Union[bool, str] = True, min_postal_code_length: int = 0, token_type: str = "reusable", charge_amount: Optional[str] = None, currency: str = "usd", language: str = "en-US", voice: str = "woman", description: Optional[str] = None, valid_card_types: str = "visa mastercard amex", parameters: Optional[List[Dict[str, str]]] = None, prompts: Optional[List[Dict[str, Any]]] = None) -> SwaigFunctionResult` +Process a payment through the call. + +**Parameters:** +- `payment_connector_url` (str): Payment processor webhook URL +- `input_method` (str): Input method: "dtmf", "speech" (default: "dtmf") +- `status_url` (Optional[str]): Payment status webhook URL +- `payment_method` (str): Payment method: "credit-card" (default: "credit-card") +- `timeout` (int): Input timeout in seconds (default: 5) +- `max_attempts` (int): Maximum retry attempts (default: 1) +- `security_code` (bool): Require security code (default: True) +- `postal_code` (Union[bool, str]): Require postal code (default: True) +- `min_postal_code_length` (int): Minimum postal code length (default: 0) +- `token_type` (str): Token type: "reusable", "one-time" (default: "reusable") +- `charge_amount` (Optional[str]): Amount to charge +- `currency` (str): Currency code (default: "usd") +- `language` (str): Language for prompts (default: "en-US") +- `voice` (str): Voice for prompts (default: "woman") +- `description` (Optional[str]): Payment description +- `valid_card_types` (str): Accepted card types (default: "visa mastercard amex") +- `parameters` (Optional[List[Dict[str, str]]]): Additional parameters +- `prompts` (Optional[List[Dict[str, Any]]]): Custom prompts + +**Usage:** +```python +# Basic payment processing +result.pay( + payment_connector_url="https://payment-processor.com/webhook", + charge_amount="29.99", + description="Monthly subscription" +) + +# Payment with custom settings +result.pay( + payment_connector_url="https://payment-processor.com/webhook", + input_method="speech", + timeout=10, + max_attempts=3, + security_code=True, + postal_code=True, + charge_amount="149.99", + currency="usd", + description="Premium service upgrade" +) +``` + +### Call Monitoring + +##### `tap(uri: str, control_id: Optional[str] = None, direction: str = "both", codec: str = "PCMU", rtp_ptime: int = 20, status_url: Optional[str] = None) -> SwaigFunctionResult` +Start call tapping/monitoring. + +**Parameters:** +- `uri` (str): URI to send tapped audio to +- `control_id` (Optional[str]): Unique identifier for this tap +- `direction` (str): Tap direction: "both", "inbound", "outbound" (default: "both") +- `codec` (str): Audio codec: "PCMU", "PCMA", "G722" (default: "PCMU") +- `rtp_ptime` (int): RTP packet time in milliseconds (default: 20) +- `status_url` (Optional[str]): Status webhook URL + +**Usage:** +```python +# Basic call tapping +result.tap("sip:monitor@company.com") + +# Tap with specific settings +result.tap( + uri="sip:quality@company.com", + control_id="quality_monitor_001", + direction="both", + codec="G722" +) +``` + +##### `stop_tap(control_id: Optional[str] = None) -> SwaigFunctionResult` +Stop call tapping. + +**Parameters:** +- `control_id` (Optional[str]): Control ID of tap to stop + +**Usage:** +```python +result.stop_tap() +result.stop_tap(control_id="quality_monitor_001") +``` + +### Advanced SWML Execution + +##### `execute_swml(swml_content, transfer: bool = False) -> SwaigFunctionResult` +Execute custom SWML content. + +**Parameters:** +- `swml_content`: SWML document or content to execute +- `transfer` (bool): Whether this is a transfer operation (default: False) + +**Usage:** +```python +# Execute custom SWML +custom_swml = { + "version": "1.0.0", + "sections": { + "main": [ + {"play": {"url": "https://example.com/custom.mp3"}}, + {"say": {"text": "Custom SWML execution"}} + ] + } +} +result.execute_swml(custom_swml) +``` + +### Utility Methods + +##### `to_dict() -> Dict[str, Any]` +Convert the result to a dictionary for serialization. + +**Returns:** +- Dict[str, Any]: Dictionary representation of the result + +**Usage:** +```python +result = SwaigFunctionResult("Hello world") +result.add_action("play", "music.mp3") +result_dict = result.to_dict() +print(result_dict) +# Output: {"response": "Hello world", "action": [{"play": "music.mp3"}]} +``` + +### Static Helper Methods + +##### `create_payment_prompt(for_situation: str, actions: List[Dict[str, str]], card_type: Optional[str] = None, error_type: Optional[str] = None) -> Dict[str, Any]` +Create a payment prompt configuration. + +**Parameters:** +- `for_situation` (str): Situation identifier +- `actions` (List[Dict[str, str]]): List of action configurations +- `card_type` (Optional[str]): Card type for prompts +- `error_type` (Optional[str]): Error type for error prompts + +**Usage:** +```python +prompt = SwaigFunctionResult.create_payment_prompt( + for_situation="card_number", + actions=[ + SwaigFunctionResult.create_payment_action("say", "Please enter your card number") + ] +) +``` + +##### `create_payment_action(action_type: str, phrase: str) -> Dict[str, str]` +Create a payment action configuration. + +**Parameters:** +- `action_type` (str): Action type +- `phrase` (str): Action phrase + +**Usage:** +```python +action = SwaigFunctionResult.create_payment_action("say", "Enter your card number") +``` + +##### `create_payment_parameter(name: str, value: str) -> Dict[str, str]` +Create a payment parameter configuration. + +**Parameters:** +- `name` (str): Parameter name +- `value` (str): Parameter value + +**Usage:** +```python +param = SwaigFunctionResult.create_payment_parameter("merchant_id", "12345") +``` + +### Method Chaining + +All methods return `self`, enabling fluent method chaining: + +```python +result = (SwaigFunctionResult("I'll help you with that") + .set_post_process(True) + .update_global_data({"status": "helping"}) + .set_end_of_speech_timeout(800) + .add_action("play", "thinking.mp3")) + +# Complex workflow +result = (SwaigFunctionResult("Processing your payment") + .set_post_process(True) + .update_global_data({"payment_status": "processing"}) + .pay( + payment_connector_url="https://payments.com/webhook", + charge_amount="99.99", + description="Service payment" + ) + .send_sms( + to_number="+15551234567", + from_number="+15559876543", + body="Payment confirmation will be sent shortly" + )) +``` + +This concludes Part 2 of the API reference covering the SwaigFunctionResult class. The document will continue with DataMap and other components in subsequent parts. + +--- + +## DataMap Class + +The `DataMap` class provides a declarative approach to creating SWAIG tools that integrate with REST APIs without requiring webhook infrastructure. DataMap tools execute on SignalWire's server infrastructure, eliminating the need to expose webhook endpoints. + +### Constructor + +```python +DataMap(function_name: str) +``` + +**Parameters:** +- `function_name` (str): Name of the SWAIG function this DataMap will create + +**Usage:** +```python +# Create a new DataMap tool +weather_map = DataMap('get_weather') +search_map = DataMap('search_docs') +``` + +### Core Configuration Methods + +#### Function Metadata + +##### `purpose(description: str) -> DataMap` +Set the function description/purpose. + +**Parameters:** +- `description` (str): Human-readable description of what this function does + +**Usage:** +```python +data_map = DataMap('get_weather').purpose('Get current weather information for any city') +``` + +##### `description(description: str) -> DataMap` +Alias for `purpose()` - set the function description. + +**Parameters:** +- `description` (str): Function description + +**Usage:** +```python +data_map = DataMap('search_api').description('Search our knowledge base for information') +``` + +#### Parameter Definition + +##### `parameter(name: str, param_type: str, description: str, required: bool = False, enum: Optional[List[str]] = None) -> DataMap` +Add a function parameter with JSON schema validation. + +**Parameters:** +- `name` (str): Parameter name +- `param_type` (str): JSON schema type: "string", "number", "boolean", "array", "object" +- `description` (str): Parameter description for the AI +- `required` (bool): Whether parameter is required (default: False) +- `enum` (Optional[List[str]]): List of allowed values for validation + +**Usage:** +```python +# Required string parameter +data_map.parameter('location', 'string', 'City name or ZIP code', required=True) + +# Optional number parameter +data_map.parameter('days', 'number', 'Number of forecast days', required=False) + +# Enum parameter with allowed values +data_map.parameter('units', 'string', 'Temperature units', + enum=['celsius', 'fahrenheit'], required=False) + +# Boolean parameter +data_map.parameter('include_alerts', 'boolean', 'Include weather alerts', required=False) + +# Array parameter +data_map.parameter('categories', 'array', 'Search categories to include') +``` + +### API Integration Methods + +#### HTTP Webhook Configuration + +##### `webhook(method: str, url: str, headers: Optional[Dict[str, str]] = None, form_param: Optional[str] = None, input_args_as_params: bool = False, require_args: Optional[List[str]] = None) -> DataMap` +Configure an HTTP API call. + +**Parameters:** +- `method` (str): HTTP method: "GET", "POST", "PUT", "DELETE", "PATCH" +- `url` (str): API endpoint URL (supports `${variable}` substitution) +- `headers` (Optional[Dict[str, str]]): HTTP headers to send +- `form_param` (Optional[str]): Send JSON body as single form parameter with this name +- `input_args_as_params` (bool): Merge function arguments into URL parameters (default: False) +- `require_args` (Optional[List[str]]): Only execute if these arguments are present + +**Variable Substitution in URLs:** +- `${args.parameter_name}`: Function argument values +- `${global_data.key}`: Call-wide data store (user info, call state - NOT credentials) +- `${meta_data.call_id}`: Call and function metadata + +**Usage:** +```python +# Simple GET request with parameter substitution +data_map.webhook('GET', 'https://api.weather.com/v1/current?key=API_KEY&q=${args.location}') + +# POST request with authentication headers +data_map.webhook( + 'POST', + 'https://api.company.com/search', + headers={ + 'Authorization': 'Bearer YOUR_TOKEN', + 'Content-Type': 'application/json' + } +) + +# Webhook that requires specific arguments +data_map.webhook( + 'GET', + 'https://api.service.com/data?id=${args.customer_id}', + require_args=['customer_id'] +) + +# Use global data for call-related info (NOT credentials) +data_map.webhook( + 'GET', + 'https://api.service.com/customer/${global_data.customer_id}/orders', + headers={'Authorization': 'Bearer YOUR_API_TOKEN'} # Use static credentials +) +``` + +##### `body(data: Dict[str, Any]) -> DataMap` +Set the JSON body for POST/PUT requests. + +**Parameters:** +- `data` (Dict[str, Any]): JSON body data (supports `${variable}` substitution) + +**Usage:** +```python +# Static body with parameter substitution +data_map.body({ + 'query': '${args.search_term}', + 'limit': 5, + 'filters': { + 'category': '${args.category}', + 'active': True + } +}) + +# Body with call-related data (NOT sensitive info) +data_map.body({ + 'customer_id': '${global_data.customer_id}', + 'request_id': '${meta_data.call_id}', + 'search': '${args.query}' +}) +``` + +##### `params(data: Dict[str, Any]) -> DataMap` +Set URL query parameters. + +**Parameters:** +- `data` (Dict[str, Any]): Query parameters (supports `${variable}` substitution) + +**Usage:** +```python +# URL parameters with substitution +data_map.params({ + 'api_key': 'YOUR_API_KEY', + 'q': '${args.location}', + 'units': '${args.units}', + 'lang': 'en' +}) +``` + +#### Multiple Webhooks and Fallbacks + +DataMap supports multiple webhook configurations for fallback scenarios: + +```python +# Primary API with fallback +data_map = (DataMap('search_with_fallback') + .purpose('Search with multiple API fallbacks') + .parameter('query', 'string', 'Search query', required=True) + + # Primary API + .webhook('GET', 'https://api.primary.com/search?q=${args.query}') + .output(SwaigFunctionResult('Primary result: ${response.title}')) + + # Fallback API + .webhook('GET', 'https://api.fallback.com/search?q=${args.query}') + .output(SwaigFunctionResult('Fallback result: ${response.title}')) + + # Final fallback if all APIs fail + .fallback_output(SwaigFunctionResult('Sorry, all search services are currently unavailable')) +) +``` + +### Response Processing + +#### Basic Output + +##### `output(result: SwaigFunctionResult) -> DataMap` +Set the response template for successful API calls. + +**Parameters:** +- `result` (SwaigFunctionResult): Response template with variable substitution + +**Variable Substitution in Outputs:** +- `${response.field}`: API response fields +- `${response.nested.field}`: Nested response fields +- `${response.array[0].field}`: Array element fields +- `${args.parameter}`: Original function arguments +- `${global_data.key}`: Call-wide data store (user info, call state) + +**Usage:** +```python +# Simple response template +data_map.output(SwaigFunctionResult('Weather in ${args.location}: ${response.current.condition.text}, ${response.current.temp_f}°F')) + +# Response with actions +data_map.output( + SwaigFunctionResult('Found ${response.total_results} results') + .update_global_data({'last_search': '${args.query}'}) + .add_action('play', 'search_complete.mp3') +) + +# Complex response with nested data +data_map.output( + SwaigFunctionResult('Order ${response.order.id} status: ${response.order.status}. Estimated delivery: ${response.order.delivery.estimated_date}') +) +``` + +##### `fallback_output(result: SwaigFunctionResult) -> DataMap` +Set the response when all webhooks fail. + +**Parameters:** +- `result` (SwaigFunctionResult): Fallback response + +**Usage:** +```python +data_map.fallback_output( + SwaigFunctionResult('Sorry, the service is temporarily unavailable. Please try again later.') + .add_action('play', 'service_unavailable.mp3') +) +``` + +#### Array Processing + +##### `foreach(foreach_config: Union[str, Dict[str, Any]]) -> DataMap` +Process array responses by iterating over elements. + +**Parameters:** +- `foreach_config` (Union[str, Dict]): Array path or configuration object + +**Simple Array Processing:** +```python +# Process array of search results +data_map = (DataMap('search_docs') + .webhook('GET', 'https://api.docs.com/search?q=${args.query}') + .foreach('${response.results}') # Iterate over results array + .output(SwaigFunctionResult('Found: ${foreach.title} - ${foreach.summary}')) +) +``` + +**Advanced Array Processing:** +```python +# Complex foreach configuration +data_map.foreach({ + 'array': '${response.items}', + 'limit': 3, # Process only first 3 items + 'filter': { + 'field': 'status', + 'value': 'active' + } +}) +``` + +**Foreach Variable Access:** +- `${foreach.field}`: Current array element field +- `${foreach.nested.field}`: Nested fields in current element +- `${foreach_index}`: Current iteration index (0-based) +- `${foreach_count}`: Total number of items being processed + +### Pattern-Based Processing + +#### Expression Matching + +##### `expression(test_value: str, pattern: Union[str, Pattern], output: SwaigFunctionResult, nomatch_output: Optional[SwaigFunctionResult] = None) -> DataMap` +Add pattern-based responses without API calls. + +**Parameters:** +- `test_value` (str): Template string to test against pattern +- `pattern` (Union[str, Pattern]): Regex pattern or compiled Pattern object +- `output` (SwaigFunctionResult): Response when pattern matches +- `nomatch_output` (Optional[SwaigFunctionResult]): Response when pattern doesn't match + +**Usage:** +```python +# Command-based responses +control_map = (DataMap('file_control') + .purpose('Control file playback') + .parameter('command', 'string', 'Playback command', required=True) + .parameter('filename', 'string', 'File to control') + + # Start commands + .expression( + '${args.command}', + r'start|play|begin', + SwaigFunctionResult('Starting playback') + .add_action('start_playback', {'file': '${args.filename}'}) + ) + + # Stop commands + .expression( + '${args.command}', + r'stop|pause|halt', + SwaigFunctionResult('Stopping playback') + .add_action('stop_playback', True) + ) + + # Volume commands + .expression( + '${args.command}', + r'volume (\d+)', + SwaigFunctionResult('Setting volume to ${match.1}') + .add_action('set_volume', '${match.1}') + ) +) +``` + +**Pattern Matching Variables:** +- `${match.0}`: Full match +- `${match.1}`, `${match.2}`, etc.: Capture groups +- `${match.group_name}`: Named capture groups + +### Error Handling + +##### `error_keys(keys: List[str]) -> DataMap` +Specify response fields that indicate errors. + +**Parameters:** +- `keys` (List[str]): List of field names that indicate API errors + +**Usage:** +```python +# Treat these response fields as errors +data_map.error_keys(['error', 'error_message', 'status_code']) + +# If API returns {"error": "Not found"}, DataMap will treat this as an error +``` + +##### `global_error_keys(keys: List[str]) -> DataMap` +Set global error keys for all webhooks in this DataMap. + +**Parameters:** +- `keys` (List[str]): Global error field names + +**Usage:** +```python +data_map.global_error_keys(['error', 'message', 'code']) +``` + +### Advanced Configuration + +##### `webhook_expressions(expressions: List[Dict[str, Any]]) -> DataMap` +Add expression-based webhook selection. + +**Parameters:** +- `expressions` (List[Dict[str, Any]]): List of expression configurations + +**Usage:** +```python +# Different APIs based on input +data_map.webhook_expressions([ + { + 'test': '${args.type}', + 'pattern': 'weather', + 'webhook': { + 'method': 'GET', + 'url': 'https://weather-api.com/current?q=${args.location}' + } + }, + { + 'test': '${args.type}', + 'pattern': 'news', + 'webhook': { + 'method': 'GET', + 'url': 'https://news-api.com/search?q=${args.query}' + } + } +]) +``` + +### Complete DataMap Examples + +#### Simple Weather API + +```python +weather_tool = (DataMap('get_weather') + .purpose('Get current weather information') + .parameter('location', 'string', 'City name or ZIP code', required=True) + .parameter('units', 'string', 'Temperature units', enum=['celsius', 'fahrenheit']) + .webhook('GET', 'https://api.weather.com/v1/current?key=API_KEY&q=${args.location}&units=${args.units}') + .output(SwaigFunctionResult('Weather in ${args.location}: ${response.current.condition.text}, ${response.current.temp_f}°F')) + .error_keys(['error']) +) + +# Register with agent +agent.register_swaig_function(weather_tool.to_swaig_function()) +``` + +#### Search with Array Processing + +```python +search_tool = (DataMap('search_knowledge') + .purpose('Search company knowledge base') + .parameter('query', 'string', 'Search query', required=True) + .parameter('category', 'string', 'Search category', enum=['docs', 'faq', 'policies']) + .webhook( + 'POST', + 'https://api.company.com/search', + headers={'Authorization': 'Bearer TOKEN'} + ) + .body({ + 'query': '${args.query}', + 'category': '${args.category}', + 'limit': 5 + }) + .foreach('${response.results}') + .output(SwaigFunctionResult('Found: ${foreach.title} - ${foreach.summary}')) + .fallback_output(SwaigFunctionResult('Search service is temporarily unavailable')) +) +``` + +#### Command Processing (No API) + +```python +control_tool = (DataMap('system_control') + .purpose('Control system functions') + .parameter('action', 'string', 'Action to perform', required=True) + .parameter('target', 'string', 'Target for the action') + + # Restart commands + .expression( + '${args.action}', + r'restart|reboot', + SwaigFunctionResult('Restarting ${args.target}') + .add_action('restart_service', {'service': '${args.target}'}) + ) + + # Status commands + .expression( + '${args.action}', + r'status|check', + SwaigFunctionResult('Checking status of ${args.target}') + .add_action('check_status', {'service': '${args.target}'}) + ) + + # Default for unrecognized commands + .expression( + '${args.action}', + r'.*', + SwaigFunctionResult('Unknown command: ${args.action}'), + nomatch_output=SwaigFunctionResult('Please specify a valid action') + ) +) +``` + +### Conversion and Registration + +##### `to_swaig_function() -> Dict[str, Any]` +Convert the DataMap to a SWAIG function dictionary for registration. + +**Returns:** +- Dict[str, Any]: Complete SWAIG function definition + +**Usage:** +```python +# Build DataMap +weather_map = DataMap('get_weather').purpose('Get weather').parameter('location', 'string', 'City', required=True) + +# Convert to SWAIG function and register +swaig_function = weather_map.to_swaig_function() +agent.register_swaig_function(swaig_function) +``` + +### Convenience Functions + +The SDK provides helper functions for common DataMap patterns: + +##### `create_simple_api_tool(name: str, url: str, response_template: str, parameters: Optional[Dict[str, Dict]] = None, method: str = "GET", headers: Optional[Dict[str, str]] = None, body: Optional[Dict[str, Any]] = None, error_keys: Optional[List[str]] = None) -> DataMap` + +Create a simple API integration tool. + +**Parameters:** +- `name` (str): Function name +- `url` (str): API endpoint URL +- `response_template` (str): Response template string +- `parameters` (Optional[Dict[str, Dict]]): Parameter definitions +- `method` (str): HTTP method (default: "GET") +- `headers` (Optional[Dict[str, str]]): HTTP headers +- `body` (Optional[Dict[str, Any]]): Request body +- `error_keys` (Optional[List[str]]): Error field names + +**Usage:** +```python +from signalwire_agents.core.data_map import create_simple_api_tool + +weather = create_simple_api_tool( + name='get_weather', + url='https://api.weather.com/v1/current?key=API_KEY&q=${location}', + response_template='Weather in ${location}: ${response.current.condition.text}', + parameters={ + 'location': { + 'type': 'string', + 'description': 'City name', + 'required': True + } + } +) + +agent.register_swaig_function(weather.to_swaig_function()) +``` + +##### `create_expression_tool(name: str, patterns: Dict[str, Tuple[str, SwaigFunctionResult]], parameters: Optional[Dict[str, Dict]] = None) -> DataMap` + +Create a pattern-based tool without API calls. + +**Parameters:** +- `name` (str): Function name +- `patterns` (Dict[str, Tuple[str, SwaigFunctionResult]]): Pattern mappings +- `parameters` (Optional[Dict[str, Dict]]): Parameter definitions + +**Usage:** +```python +from signalwire_agents.core.data_map import create_expression_tool + +file_control = create_expression_tool( + name='file_control', + patterns={ + r'start.*': ('${args.command}', SwaigFunctionResult().add_action('start_playback', True)), + r'stop.*': ('${args.command}', SwaigFunctionResult().add_action('stop_playback', True)) + }, + parameters={ + 'command': { + 'type': 'string', + 'description': 'Playback command', + 'required': True + } + } +) + +agent.register_swaig_function(file_control.to_swaig_function()) +``` + +### Method Chaining + +All DataMap methods return `self`, enabling fluent method chaining: + +```python +complete_tool = (DataMap('comprehensive_search') + .purpose('Comprehensive search with fallbacks') + .parameter('query', 'string', 'Search query', required=True) + .parameter('category', 'string', 'Search category', enum=['all', 'docs', 'faq']) + .webhook('GET', 'https://primary-api.com/search?q=${args.query}&cat=${args.category}') + .output(SwaigFunctionResult('Primary: ${response.title}')) + .webhook('GET', 'https://backup-api.com/search?q=${args.query}') + .output(SwaigFunctionResult('Backup: ${response.title}')) + .fallback_output(SwaigFunctionResult('All search services unavailable')) + .error_keys(['error', 'message']) +) +``` + +This concludes Part 3 of the API reference covering the DataMap class. The document will continue with Context System and other components in subsequent parts. + +--- + +## Context System + +The Context System enhances traditional prompt-based agents by adding structured workflows with sequential steps on top of a base prompt. Each step contains its own guidance, completion criteria, and function restrictions while building upon the agent's foundational prompt. + +### ContextBuilder Class + +The `ContextBuilder` is accessed via `agent.define_contexts()` and provides the main interface for creating structured workflows. + +#### Getting Started + +```python +# Access the context builder +contexts = agent.define_contexts() + +# Create contexts and steps +contexts.add_context("greeting") \ + .add_step("welcome") \ + .set_text("Welcome! How can I help you today?") \ + .set_step_criteria("User has stated their need") \ + .set_valid_steps(["next"]) +``` + +##### `add_context(name: str) -> Context` +Create a new context in the workflow. + +**Parameters:** +- `name` (str): Unique context name + +**Returns:** +- Context: Context object for method chaining + +**Usage:** +```python +# Create multiple contexts +greeting_context = contexts.add_context("greeting") +main_menu_context = contexts.add_context("main_menu") +support_context = contexts.add_context("support") +``` + +### Context Class + +The Context class represents a conversation context containing multiple steps with enhanced features: + +```python +class Context: + def add_step(self, name: str) -> Step + """Create a new step in this context""" + + def set_valid_contexts(self, contexts: List[str]) -> Context + """Set which contexts can be accessed from this context""" + + # Context entry parameters (for context switching behavior) + def set_post_prompt(self, post_prompt: str) -> Context + """Override agent's post prompt when this context is active""" + + def set_system_prompt(self, system_prompt: str) -> Context + """Trigger context switch with new system instructions (makes this a Context Switch Context)""" + + def set_consolidate(self, consolidate: bool) -> Context + """Whether to consolidate conversation history when entering this context""" + + def set_full_reset(self, full_reset: bool) -> Context + """Whether to do complete system prompt replacement vs injection""" + + def set_user_prompt(self, user_prompt: str) -> Context + """User message to inject when entering this context for AI context""" + + # Context prompts (guidance for all steps in context) + def set_prompt(self, prompt: str) -> Context + """Set simple string prompt that applies to all steps in this context""" + + def add_section(self, title: str, body: str) -> Context + """Add POM-style section to context prompt""" + + def add_bullets(self, title: str, bullets: List[str]) -> Context + """Add POM-style bullet section to context prompt""" +``` + +**Context Types:** + +1. **Workflow Container Context** (no `system_prompt`): Organizes steps without conversation state changes +2. **Context Switch Context** (has `system_prompt`): Triggers conversation state changes when entered, processing entry parameters like a `context_switch` SWAIG action + +**Prompt Hierarchy:** Base Agent Prompt → Context Prompt → Step Prompt + +#### Usage Examples + +```python +# Workflow container context (just organizes steps) +main_context = contexts.add_context("main") +main_context.set_prompt("Follow standard customer service protocols") + +# Context switch context (changes AI behavior) +billing_context = contexts.add_context("billing") +billing_context.set_system_prompt("You are now a billing specialist") \ + .set_consolidate(True) \ + .set_user_prompt("Customer needs billing assistance") \ + .add_section("Department", "Billing Department") \ + .add_bullets("Services", ["Account inquiries", "Payments", "Refunds"]) + +# Full reset context (complete conversation reset) +manager_context = contexts.add_context("manager") +manager_context.set_system_prompt("You are a senior manager") \ + .set_full_reset(True) \ + .set_consolidate(True) +``` + +--- + +## State Management + +The State Management system provides persistent storage for conversation data across calls and sessions. + +### StateManager (Abstract Base Class) + +The base interface that all state managers must implement. + +#### Core Methods + +##### `store(call_id: str, data: Dict[str, Any]) -> bool` +Store state data for a call. + +**Parameters:** +- `call_id` (str): Unique identifier for the call +- `data` (Dict[str, Any]): State data to store + +**Returns:** +- bool: True if successful, False otherwise + +##### `retrieve(call_id: str) -> Optional[Dict[str, Any]]` +Retrieve state data for a call. + +**Parameters:** +- `call_id` (str): Unique identifier for the call + +**Returns:** +- Optional[Dict[str, Any]]: State data or None if not found + +##### `update(call_id: str, data: Dict[str, Any]) -> bool` +Update existing state data (merges with existing). + +**Parameters:** +- `call_id` (str): Unique identifier for the call +- `data` (Dict[str, Any]): Data to merge with existing state + +**Returns:** +- bool: True if successful, False otherwise + +##### `delete(call_id: str) -> bool` +Delete state data for a call. + +**Parameters:** +- `call_id` (str): Unique identifier for the call + +**Returns:** +- bool: True if successful, False otherwise + +##### `cleanup_expired() -> int` +Clean up expired state data. + +**Returns:** +- int: Number of expired items cleaned up + +##### `exists(call_id: str) -> bool` +Check if state exists for a call. + +**Parameters:** +- `call_id` (str): Unique identifier for the call + +**Returns:** +- bool: True if state exists, False otherwise + +### FileStateManager + +File-based state manager implementation that stores state data in JSON files. + +#### Constructor + +```python +FileStateManager( + storage_dir: str = "./agent_state", + expiry_hours: int = 24, + auto_cleanup: bool = True +) +``` + +**Parameters:** +- `storage_dir` (str): Directory to store state files (default: "./agent_state") +- `expiry_hours` (int): Hours after which state expires (default: 24) +- `auto_cleanup` (bool): Automatically clean up expired files (default: True) + +#### Usage + +```python +from signalwire_agents.core.state import FileStateManager + +# Create file-based state manager +state_manager = FileStateManager( + storage_dir="/var/agent_state", + expiry_hours=48, # 2 days + auto_cleanup=True +) + +# Use with agent +agent = AgentBase( + name="Stateful Agent", + enable_state_tracking=True, + state_manager=state_manager +) + +# Manual state operations +state_manager.store("call_123", { + "customer_id": "12345", + "issue_type": "billing", + "status": "in_progress" +}) + +# Retrieve state +state = state_manager.retrieve("call_123") +if state: + print(f"Customer: {state['customer_id']}") + +# Update state +state_manager.update("call_123", { + "status": "resolved", + "resolution": "Account credited" +}) + +# Clean up expired state +cleaned = state_manager.cleanup_expired() +print(f"Cleaned up {cleaned} expired state files") +``` + +### Custom State Manager + +You can implement custom state managers for databases, Redis, etc.: + +```python +from signalwire_agents.core.state import StateManager +import redis + +class RedisStateManager(StateManager): + def __init__(self, redis_url: str, expiry_seconds: int = 86400): + self.redis = redis.from_url(redis_url) + self.expiry = expiry_seconds + + def store(self, call_id: str, data: Dict[str, Any]) -> bool: + try: + import json + self.redis.setex( + f"agent_state:{call_id}", + self.expiry, + json.dumps(data) + ) + return True + except Exception: + return False + + def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]: + try: + import json + data = self.redis.get(f"agent_state:{call_id}") + return json.loads(data) if data else None + except Exception: + return None + + def update(self, call_id: str, data: Dict[str, Any]) -> bool: + existing = self.retrieve(call_id) + if existing: + existing.update(data) + return self.store(call_id, existing) + return self.store(call_id, data) + + def delete(self, call_id: str) -> bool: + try: + self.redis.delete(f"agent_state:{call_id}") + return True + except Exception: + return False + + def cleanup_expired(self) -> int: + # Redis handles expiry automatically + return 0 + +# Use custom state manager +redis_state = RedisStateManager("redis://localhost:6379") +agent = AgentBase( + name="Redis Agent", + enable_state_tracking=True, + state_manager=redis_state +) +``` + +--- + +## Skills System + +The Skills System provides modular, reusable capabilities that can be easily added to any agent. + +### Available Built-in Skills + +#### `datetime` Skill +Provides current date and time information. + +**Parameters:** +- `timezone` (Optional[str]): Timezone for date/time (default: system timezone) +- `format` (Optional[str]): Custom date/time format string + +**Usage:** +```python +# Basic datetime skill +agent.add_skill("datetime") + +# With timezone +agent.add_skill("datetime", {"timezone": "America/New_York"}) + +# With custom format +agent.add_skill("datetime", { + "timezone": "UTC", + "format": "%Y-%m-%d %H:%M:%S %Z" +}) +``` + +#### `math` Skill +Safe mathematical expression evaluation. + +**Parameters:** +- `precision` (Optional[int]): Decimal precision for results (default: 2) +- `max_expression_length` (Optional[int]): Maximum expression length (default: 100) + +**Usage:** +```python +# Basic math skill +agent.add_skill("math") + +# With custom precision +agent.add_skill("math", {"precision": 4}) +``` + +#### `web_search` Skill +Google Custom Search API integration with web scraping. + +**Parameters:** +- `api_key` (str): Google Custom Search API key (required) +- `search_engine_id` (str): Google Custom Search Engine ID (required) +- `num_results` (Optional[int]): Number of results to return (default: 3) +- `tool_name` (Optional[str]): Custom tool name for multiple instances +- `delay` (Optional[float]): Delay between requests in seconds +- `no_results_message` (Optional[str]): Custom message when no results found + +**Usage:** +```python +# Basic web search +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id" +}) + +# Multiple search instances +agent.add_skill("web_search", { + "api_key": "your-api-key", + "search_engine_id": "general-engine-id", + "tool_name": "search_general", + "num_results": 5 +}) + +agent.add_skill("web_search", { + "api_key": "your-api-key", + "search_engine_id": "news-engine-id", + "tool_name": "search_news", + "num_results": 3, + "delay": 0.5 +}) +``` + +#### `datasphere` Skill +SignalWire DataSphere knowledge search integration. + +**Parameters:** +- `space_name` (str): DataSphere space name (required) +- `project_id` (str): DataSphere project ID (required) +- `token` (str): DataSphere access token (required) +- `document_id` (Optional[str]): Specific document to search +- `tool_name` (Optional[str]): Custom tool name for multiple instances +- `count` (Optional[int]): Number of results to return (default: 3) +- `tags` (Optional[List[str]]): Filter by document tags + +**Usage:** +```python +# Basic DataSphere search +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token" +}) + +# Multiple DataSphere instances +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token", + "document_id": "drinks-menu", + "tool_name": "search_drinks", + "count": 5 +}) + +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token", + "tool_name": "search_policies", + "tags": ["HR", "Policies"] +}) +``` + +#### `native_vector_search` Skill +Local document search with vector similarity and keyword search. + +**Parameters:** +- `index_path` (str): Path to search index file (required) +- `tool_name` (Optional[str]): Custom tool name (default: "search_documents") +- `max_results` (Optional[int]): Maximum results to return (default: 5) +- `similarity_threshold` (Optional[float]): Minimum similarity score (default: 0.1) + +**Usage:** +```python +# Basic local search +agent.add_skill("native_vector_search", { + "index_path": "./knowledge.swsearch" +}) + +# With custom settings +agent.add_skill("native_vector_search", { + "index_path": "./docs.swsearch", + "tool_name": "search_docs", + "max_results": 10, + "similarity_threshold": 0.3 +}) +``` + +### Creating Custom Skills + +#### Skill Structure + +Create a new skill by extending `SkillBase`: + +```python +from signalwire_agents.core.skill_base import SkillBase +from signalwire_agents.core.data_map import DataMap +from signalwire_agents.core.function_result import SwaigFunctionResult + +class CustomSkill(SkillBase): + SKILL_NAME = "custom_skill" + SKILL_DESCRIPTION = "Description of what this skill does" + SKILL_VERSION = "1.0.0" + REQUIRED_PACKAGES = ["requests"] # Python packages needed + REQUIRED_ENV_VARS = ["API_KEY"] # Environment variables needed + + def setup(self) -> bool: + """Validate and store configuration""" + if not self.params.get("api_key"): + self.logger.error("api_key parameter is required") + return False + + self.api_key = self.params["api_key"] + return True + + def register_tools(self) -> None: + """Register skill functions""" + # DataMap-based tool + tool = (DataMap("custom_function") + .description("Custom API integration") + .parameter("query", "string", "Search query", required=True) + .webhook("GET", f"https://api.example.com/search?key={self.api_key}&q=${{args.query}}") + .output(SwaigFunctionResult("Found: ${{response.title}}")) + ) + + self.agent.register_swaig_function(tool.to_swaig_function()) + + def get_hints(self) -> List[str]: + """Speech recognition hints""" + return ["custom search", "find information"] + + def get_global_data(self) -> Dict[str, Any]: + """Global data for DataMap""" + return {"skill_version": self.SKILL_VERSION} + + def get_prompt_sections(self) -> List[Dict[str, Any]]: + """Prompt sections to add""" + return [{ + "title": "Custom Search Capability", + "body": "You can search our custom database for information.", + "bullets": ["Use the custom_function to search", "Results are real-time"] + }] +``` + +#### Skill Registration + +Skills are automatically discovered from the `signalwire_agents/skills/` directory. To register a custom skill: + +1. Create directory: `signalwire_agents/skills/your_skill/` +2. Add `__init__.py`, `skill.py`, and `README.md` +3. Implement your skill class in `skill.py` +4. The skill will be automatically available + +--- + +## Utility Classes + +### SWAIGFunction Class + +Represents a SWAIG function definition with metadata and validation. + +#### Constructor + +```python +SWAIGFunction( + function: str, + description: str, + parameters: Dict[str, Any], + **kwargs +) +``` + +**Parameters:** +- `function` (str): Function name +- `description` (str): Function description +- `parameters` (Dict[str, Any]): JSON schema for parameters +- `**kwargs`: Additional SWAIG properties + +#### Usage + +```python +from signalwire_agents.core.swaig_function import SWAIGFunction + +# Create SWAIG function +swaig_func = SWAIGFunction( + function="get_weather", + description="Get current weather", + parameters={ + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"} + }, + "required": ["location"] + }, + secure=True, + fillers={"en-US": ["Checking weather..."]} +) + +# Register with agent +agent.register_swaig_function(swaig_func.to_dict()) +``` + +### SWMLService Class + +Base class providing SWML document generation and HTTP service capabilities. `AgentBase` extends this class. + +#### Key Methods + +##### `get_swml_document() -> Dict[str, Any]` +Generate the complete SWML document for the service. + +##### `handle_request(request_data: Dict[str, Any]) -> Dict[str, Any]` +Handle incoming HTTP requests and generate appropriate responses. + +### EphemeralAgentConfig Class + +Used in dynamic configuration callbacks to modify agent settings per-request. + +#### Available Methods + +All the same configuration methods as `AgentBase`: +- `add_language()`, `add_hint()`, `set_params()` +- `prompt_add_section()`, `set_global_data()` +- `add_function_include()`, `set_native_functions()` + +**Usage:** +```python +def dynamic_config(query_params, headers, body, config): + # Configure based on request + if query_params.get("lang") == "es": + config.add_language("Spanish", "es-ES", "nova.luna") + + # Customer-specific configuration + customer_id = headers.get("X-Customer-ID") + if customer_id: + config.set_global_data({"customer_id": customer_id}) + config.prompt_add_section("Customer Context", f"You are helping customer {customer_id}") + +agent.set_dynamic_config_callback(dynamic_config) +``` + +--- + +## Environment Variables + +The SDK supports various environment variables for configuration: + +### Authentication +- `SWML_BASIC_AUTH_USER`: Basic auth username +- `SWML_BASIC_AUTH_PASSWORD`: Basic auth password + +### SSL/HTTPS +- `SWML_SSL_ENABLED`: Enable SSL (true/false) +- `SWML_SSL_CERT_PATH`: Path to SSL certificate +- `SWML_SSL_KEY_PATH`: Path to SSL private key +- `SWML_DOMAIN`: Domain name for SSL + +### Proxy Support +- `SWML_PROXY_URL_BASE`: Base URL for proxy server + +### Skills Configuration +- `GOOGLE_SEARCH_API_KEY`: Google Custom Search API key +- `GOOGLE_SEARCH_ENGINE_ID`: Google Custom Search Engine ID +- `DATASPHERE_SPACE_NAME`: DataSphere space name +- `DATASPHERE_PROJECT_ID`: DataSphere project ID +- `DATASPHERE_TOKEN`: DataSphere access token + +### Usage + +```python +import os + +# Set environment variables +os.environ["SWML_BASIC_AUTH_USER"] = "admin" +os.environ["SWML_BASIC_AUTH_PASSWORD"] = "secret" +os.environ["GOOGLE_SEARCH_API_KEY"] = "your-api-key" + +# Agent will automatically use these +agent = AgentBase("My Agent") +agent.add_skill("web_search", { + "search_engine_id": "your-engine-id" + # api_key will be read from environment +}) +``` + +--- + +## Complete Example + +Here's a comprehensive example using multiple SDK components: + +```python +from signalwire_agents import AgentBase, SwaigFunctionResult, DataMap +from signalwire_agents.core.state import FileStateManager + +class ComprehensiveAgent(AgentBase): + def __init__(self): + # Initialize with state management + state_manager = FileStateManager( + storage_dir="./agent_state", + expiry_hours=48 + ) + + super().__init__( + name="Comprehensive Agent", + enable_state_tracking=True, + state_manager=state_manager, + auto_answer=True, + record_call=True + ) + + # Configure voice and language + self.add_language("English", "en-US", "rime.spore", + speech_fillers=["Let me check...", "One moment..."]) + + # Add speech recognition hints + self.add_hints(["SignalWire", "customer service", "technical support"]) + + # Configure AI parameters + self.set_params({ + "ai_model": "gpt-4.1-nano", + "end_of_speech_timeout": 800, + "temperature": 0.7 + }) + + # Add skills + self.add_skill("datetime") + self.add_skill("math") + self.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-engine-id", + "num_results": 3 + }) + + # Set up structured workflow + self._setup_contexts() + + # Add custom tools + self._register_custom_tools() + + # Set global data + self.set_global_data({ + "company_name": "Acme Corp", + "support_hours": "9 AM - 5 PM EST", + "version": "2.0" + }) + + def _setup_contexts(self): + """Set up structured workflow contexts""" + contexts = self.define_contexts() + + # Greeting context + greeting = contexts.add_context("greeting") + greeting.add_step("welcome") \ + .set_text("Hello! Welcome to Acme Corp support. How can I help you today?") \ + .set_step_criteria("Customer has explained their issue") \ + .set_valid_steps(["next"]) + + greeting.add_step("categorize") \ + .add_section("Current Task", "Categorize the customer's request") \ + .add_bullets("Categories", [ + "Technical issue - use diagnostic tools", + "Billing question - transfer to billing", + "General inquiry - handle directly" + ]) \ + .set_functions(["transfer_to_billing", "run_diagnostics"]) \ + .set_step_criteria("Request categorized and action taken") + + # Technical support context + tech = contexts.add_context("technical_support") + tech.add_step("diagnose") \ + .set_text("Let me run some diagnostics to identify the issue.") \ + .set_functions(["run_diagnostics", "check_system_status"]) \ + .set_step_criteria("Diagnostics completed") \ + .set_valid_steps(["resolve"]) + + tech.add_step("resolve") \ + .set_text("Based on the diagnostics, here's how we'll fix this.") \ + .set_functions(["apply_fix", "schedule_technician"]) \ + .set_step_criteria("Issue resolved or escalated") + + def _register_custom_tools(self): + """Register custom DataMap tools""" + + # Customer lookup tool + lookup_tool = (DataMap("lookup_customer") + .description("Look up customer information") + .parameter("customer_id", "string", "Customer ID", required=True) + .webhook("GET", "https://api.company.com/customers/${args.customer_id}", + headers={"Authorization": "Bearer YOUR_TOKEN"}) + .output(SwaigFunctionResult("Customer: ${response.name}, Status: ${response.status}")) + .error_keys(["error"]) + ) + + self.register_swaig_function(lookup_tool.to_swaig_function()) + + # System control tool + control_tool = (DataMap("system_control") + .description("Control system functions") + .parameter("action", "string", "Action to perform", required=True) + .parameter("target", "string", "Target system") + .expression("${args.action}", r"restart|reboot", + SwaigFunctionResult("Restarting ${args.target}") + .add_action("restart_system", {"target": "${args.target}"})) + .expression("${args.action}", r"status|check", + SwaigFunctionResult("Checking ${args.target} status") + .add_action("check_status", {"target": "${args.target}"})) + ) + + self.register_swaig_function(control_tool.to_swaig_function()) + + @AgentBase.tool( + description="Transfer call to billing department", + parameters={"type": "object", "properties": {}} + ) + def transfer_to_billing(self, args, raw_data): + """Transfer to billing with state tracking""" + return (SwaigFunctionResult("Transferring you to our billing department") + .update_global_data({"last_action": "transfer_to_billing"}) + .connect("billing@company.com", final=False)) + + def on_summary(self, summary, raw_data): + """Handle conversation summaries""" + print(f"Conversation completed: {summary}") + # Could save to database, send notifications, etc. + +# Run the agent +if __name__ == "__main__": + agent = ComprehensiveAgent() + agent.run() +``` + +This concludes the complete API reference for the SignalWire AI Agents SDK. The SDK provides a comprehensive framework for building sophisticated AI agents with modular capabilities, structured workflows, persistent state, and seamless deployment across multiple environments. \ No newline at end of file diff --git a/docs/swml/agents-sdk/api/post-data.md b/docs/swml/agents-sdk/api/post-data.md new file mode 100644 index 00000000..bd27dd12 --- /dev/null +++ b/docs/swml/agents-sdk/api/post-data.md @@ -0,0 +1,351 @@ +--- +id: agents-sdk-reference-post-data +slug: /agents-sdk/api/post-data +title: POST Data Reference +sidebar_label: POST Data +--- + +# SignalWire SWAIG post_data Complete Reference + +This document comprehensively details all possible keys that can be present in the `post_data` JSON object sent to SWAIG functions, based on analysis of the `execute_user_function` implementation in `server_code/mod_openai.c`. + +## Overview + +The `post_data` object is constructed differently depending on whether it's: +- **Webhook SWAIG Functions**: Traditional HTTP-based functions with Python handlers +- **DataMap Functions**: Serverless functions that execute on SignalWire servers + +## Base post_data Keys (All Functions) + +These keys are always present in `post_data` for both webhook and DataMap functions: + +### Core Identification +| Key | Type | Description | Example | +|-----|------|-------------|---------| +| `app_name` | string | Name of the AI application | `"my_ai_agent"` | +| `function` | string | Name of the SWAIG function being called | `"search_knowledge"` | +| `call_id` | string | Unique UUID of the current call session | `"12345678-1234-5678-9012-123456789abc"` | +| `ai_session_id` | string | Unique UUID of the AI session | `"87654321-4321-8765-2109-cba987654321"` | + +### Call Context +| Key | Type | Description | Example | +|-----|------|-------------|---------| +| `caller_id_name` | string | Caller ID name (if available) | `"John Doe"` | +| `caller_id_num` | string | Caller ID number (if available) | `"+15551234567"` | +| `channel_active` | boolean | Whether the channel is currently up | `true` | +| `channel_offhook` | boolean | Whether the channel is off-hook | `true` | +| `channel_ready` | boolean | Whether the AI session is ready | `true` | + +### Function Details +| Key | Type | Description | Example | +|-----|------|-------------|---------| +| `argument` | object | Parsed function arguments | `{"query": "test search"}` | +| `argument_desc` | object | Function argument schema/description | `{"type": "object", "properties": {...}}` | +| `purpose` | string | Description of what the function does | `"Search knowledge base"` | + +### Protocol Information +| Key | Type | Description | Example | +|-----|------|-------------|---------| +| `content_type` | string | Always "text/swaig" | `"text/swaig"` | +| `version` | string | SWAIG protocol version | `"1.0"` | +| `content_disposition` | string | Always "SWAIG Function" | `"SWAIG Function"` | + +### Optional Context Data +| Key | Type | Description | Present When | +|-----|------|-------------|--------------| +| `global_data` | object | Application-level global data | App has global data set | +| `conversation_id` | string | Conversation identifier | App has conversation tracking | +| `project_id` | string | SignalWire project ID | Available in channel vars | +| `space_id` | string | SignalWire space ID | Available in channel vars | + +## Conditional Keys (Webhook Functions Only) + +These keys are only added for traditional webhook SWAIG functions (not DataMap): + +### Metadata +| Key | Type | Description | Present When | +|-----|------|-------------|--------------| +| `meta_data_token` | string | Token for metadata access | Function has metadata token | +| `meta_data` | object | Function-level metadata object | Function has metadata token | + +**Metadata Details:** +- `meta_data_token`: Either specified in function definition or auto-generated (hash of function name + webhook URL) +- `meta_data`: Function-level key/value store, similar to `global_data` but scoped per function +- Functions sharing the same token share access to the same metadata +- Can be manipulated via SWAIG actions in responses +- Persists across function calls for the same token + +### SWML Integration +| Key | Type | Description | Present When | +|-----|------|-------------|--------------| +| `SWMLVars` | object | SWML variables | `swaig_post_swml_vars` parameter set | +| `SWMLCall` | object | SWML call state | `swaig_post_swml_vars` parameter set | + +**SWML Variable Filtering:** +- `swaig_post_swml_vars=true`: Includes all SWML variables +- `swaig_post_swml_vars=[array]`: Includes only specified variables from the array +- Variables may come from other SWML verbs executed before the AI verb +- Part of SWML's variable system for cross-verb communication + +### Conversation Data +| Key | Type | Description | Present When | +|-----|------|-------------|--------------| +| `call_log` | array | Processed conversation history | `swaig_post_conversation` parameter true | +| `raw_call_log` | array | Raw conversation history | `swaig_post_conversation` parameter true | + +**Conversation History Structure:** +Both arrays use OpenAI conversation format with additional SignalWire fields: + +```json +{ + "role": "assistant", + "content": "Response text", + "latency": 438, + "utterance_latency": 445, + "audio_latency": 599 +} +``` + +**Key Differences:** +- `call_log`: May shrink after conversation resets (consolidation) +- `raw_call_log`: Preserves full history from beginning regardless of resets +- Both include timing data (latency, utterance_latency, audio_latency) +- User messages include confidence scores when available + +## DataMap-Specific Additions + +For DataMap functions, additional keys are merged into `post_data`: + +### Enhanced Context Data +| Key | Type | Description | Source | +|-----|------|-------------|--------| +| `prompt_vars` | object | Template variables for AI prompts | Built from call context, SWML vars, and global_data | +| `global_data` | object | Application global data (same as base) | From SWML global_data section | + +### Parsed Arguments +| Key | Type | Description | Processing | +|-----|------|-------------|-----------| +| `args` | object | First parsed argument object | Extracted from `argument.parsed[0]` | + +**Argument Processing Details:** +- Each function has unique argument structure based on its JSON schema definition +- AI converts the schema to JSON string, engine parses and provides for expansion +- `args` contains the first parsed argument object for easy access in templates + +### Processing Context +| Key | Type | Description | Purpose | +|-----|------|-------------|---------| +| `input` | object | Copy of entire post_data | Variable expansion context | + +## prompt_vars Detailed Contents + +The `prompt_vars` object contains template variables built from multiple sources: + +### Call Information +| Key | Source | Description | Example | +|-----|--------|-------------|---------| +| `call_direction` | Call direction | "inbound" or "outbound" | `"inbound"` | +| `caller_id_name` | Channel variable | Caller's name | `"John Doe"` | +| `caller_id_number` | Channel variable | Caller's number | `"+15551234567"` | +| `local_date` | System time | Current date in local timezone | `"Friday, January 15, 2024"` | +| `spoken_date` | System time | Same as local_date | `"Friday, January 15, 2024"` | +| `local_time` | System time | Current time with timezone | `"02:30:15 PM -0500"` | +| `time_of_day` | Derived from hour | "morning", "afternoon", or "evening" | `"afternoon"` | +| `supported_languages` | App config | Available languages | `"English, Spanish, or French"` | +| `default_language` | App config | Primary language | `"English"` | + +### SWML State Variables +| Key | Source | Description | Present When | +|-----|--------|-------------|--------------| +| `vars` | SWML serialized state | All SWML variables (excluding system vars) | SWML vars exist | +| `call` | SWML serialized state | SWML call object | SWML state exists | + +### Global Data Overlay +All keys from the `global_data` object are merged into `prompt_vars`, with global_data taking precedence over built-in values. + +## global_data Evolution + +The `global_data` object has dynamic behavior: + +### Initial State +- Populated from SWML `global_data` section at call start +- Contains caller ID information automatically +- Available to both webhook and DataMap functions + +### Runtime Updates +- Can be modified by SWAIG actions during the call +- Changes persist for subsequent function calls +- Refreshed only when context switches occur +- Used as overlay for `prompt_vars` in DataMap functions + +## DataMap Processing Enhancements + +During DataMap processing, additional data may be added based on the processing path: + +### Webhook Processing Path +When DataMap uses webhooks, additional keys are added to the webhook response data: + +| Key | Type | Description | Added When | +|-----|------|-------------|-----------| +| `prompt_vars` | object | Prompt variables for expansion | Webhook processing | +| `global_data` | object | Global data for expansion | Webhook processing | +| `input` | object | Original input for context | Webhook processing | + +### Foreach Processing +During `foreach` operations, a temporary key is added: + +| Key | Type | Description | Scope | +|-----|------|-------------|-------| +| `this` | object | Current iteration item | During foreach loop only | + +### Expression Processing +For expression evaluation, data is enriched with context variables for template expansion. + +## Variable Expansion Context + +All DataMap processing uses variable expansion for template processing with access to: + +- All keys in the current data object +- Nested object access via dot notation: `${user.name}` +- Array access: `${items[0].value}` +- Encoding functions: `${enc:url:variable}` +- Built-in functions: `@{strftime %Y-%m-%d}`, `@{expr 2+2}`, etc. + +**Template Functions:** For a complete list of available `@{}` template functions, see the DataMap processing guide and `expand_jsonvars` implementation in `swaig.c`. + +## Error Handling + +### DataMap Webhook Errors +When DataMap webhook processing fails, special error keys may be added: + +| Key | Type | Description | When Present | +|-----|------|-------------|--------------| +| `parse_error` | boolean | JSON parsing failed | Webhook response not valid JSON | +| `protocol_error` | boolean | HTTP request failed | Network/connection issues | +| `http_code` | number | HTTP response code | Always present with errors | + +These errors are only set when the webhook didn't fetch properly, allowing alternate error responses to be provided to users. + +## Security and Permissions + +Several keys are only included based on SWML application parameters: + +### SWML Variables Permission +- **Parameter**: `swaig_post_swml_vars` (boolean or array) +- **Adds**: `SWMLVars`, `SWMLCall` +- **Behavior**: If true, includes all SWML vars; if array, includes only specified variables + +### Conversation Permission +- **Parameter**: `swaig_post_conversation` (boolean) +- **Adds**: `call_log`, `raw_call_log` +- **Default**: false (not included unless explicitly enabled) + +### SWML Execution Permission +- **Parameter**: `swaig_allow_swml` (boolean) +- **Controls**: Whether functions can execute SWML actions +- **Default**: true (enabled by default) + +### Global Data Modification Permission +- **Parameter**: `swaig_set_global_data` (boolean) +- **Controls**: Whether functions can modify global_data +- **Default**: true (enabled by default) + +## Implementation Notes + +1. **DataMap vs Webhook**: The key difference is DataMap functions merge prompt_vars and create an `input` copy for variable expansion +2. **Variable Expansion**: DataMap functions process templates using the entire post_data as context +3. **Memory Management**: post_data is created fresh for each function call and cleaned up afterward +4. **Error Handling**: Missing or invalid data results in empty objects rather than missing keys +5. **Dynamic Content**: prompt_vars and global_data can vary between function calls based on call state +6. **Metadata Scope**: Function metadata is isolated per token, enabling function-specific data storage + +## Example post_data Objects + +### Webhook Function Example +```json +{ + "app_name": "customer_service_agent", + "function": "search_knowledge", + "call_id": "12345678-1234-5678-9012-123456789abc", + "ai_session_id": "87654321-4321-8765-2109-cba987654321", + "caller_id_name": "John Doe", + "caller_id_num": "+15551234567", + "channel_active": true, + "channel_offhook": true, + "channel_ready": true, + "argument": {"query": "billing question"}, + "argument_desc": {"type": "object", "properties": {"query": {"type": "string"}}}, + "purpose": "Search the knowledge base for relevant information", + "content_type": "text/swaig", + "version": "1.0", + "content_disposition": "SWAIG Function", + "global_data": {"customer_tier": "premium"}, + "conversation_id": "conv_123", + "project_id": "proj_abc", + "space_id": "space_xyz", + "meta_data_token": "func_abc123", + "meta_data": {"search_count": 5, "last_query": "previous search"}, + "call_log": [ + { + "role": "user", + "content": "I have a billing question", + "confidence": 0.95 + }, + { + "role": "assistant", + "content": "I'd be happy to help with billing", + "latency": 250, + "utterance_latency": 300, + "audio_latency": 450 + } + ] +} +``` + +### DataMap Function Example +```json +{ + "app_name": "data_processor", + "function": "process_data", + "call_id": "12345678-1234-5678-9012-123456789abc", + "ai_session_id": "87654321-4321-8765-2109-cba987654321", + "channel_active": true, + "channel_offhook": true, + "channel_ready": true, + "argument": {"data": [{"name": "item1"}, {"name": "item2"}]}, + "argument_desc": {"type": "object", "properties": {"data": {"type": "array"}}}, + "purpose": "Process array data with template expansion", + "content_type": "text/swaig", + "version": "1.0", + "content_disposition": "SWAIG Function", + "global_data": {"prefix": "processed_", "customer_tier": "premium"}, + "prompt_vars": { + "call_direction": "inbound", + "caller_id_name": "John Doe", + "caller_id_number": "+15551234567", + "local_date": "Friday, January 15, 2024", + "local_time": "02:30:15 PM -0500", + "time_of_day": "afternoon", + "supported_languages": "English", + "default_language": "English", + "customer_tier": "premium", + "prefix": "processed_" + }, + "args": {"data": [{"name": "item1"}, {"name": "item2"}]}, + "input": { /* copy of entire post_data */ } +} +``` + +## SWML Parameter Reference + +The following SWML parameters control what data is included in post_data: + +| Parameter | Type | Default | Purpose | +|-----------|------|---------|---------| +| `swaig_allow_swml` | boolean | true | Allow functions to execute SWML actions | +| `swaig_allow_settings` | boolean | true | Allow functions to modify AI settings | +| `swaig_post_conversation` | boolean | false | Include conversation history in post_data | +| `swaig_set_global_data` | boolean | true | Allow functions to modify global_data | +| `swaig_post_swml_vars` | boolean/array | false | Include SWML variables in post_data | + +This reference covers all the keys that can appear in post_data based on the server implementation and configuration parameters. \ No newline at end of file diff --git a/docs/swml/agents-sdk/api/swaig-actions.md b/docs/swml/agents-sdk/api/swaig-actions.md new file mode 100644 index 00000000..bbaef9b6 --- /dev/null +++ b/docs/swml/agents-sdk/api/swaig-actions.md @@ -0,0 +1,730 @@ +--- +id: agents-sdk-reference-swaig-actions +slug: /agents-sdk/api/swaig-actions +title: SWAIG Actions Reference +sidebar_label: SWAIG Actions +--- + +# SWAIG Actions Reference + +This document describes all supported SWAIG actions that can be returned from function calls, their expected JSON parameters, and proposed Python helper methods for the `SwaigFunctionResult` class. + +## Core Call Control Actions + +### 1. SWML Execution +**Purpose**: Execute a SWML document or transfer to SWML + +**JSON Structure**: +```json +{ + "SWML": "", + "transfer": "true|false" // optional +} +``` + +**Parameters**: +- `SWML` (required): String (raw SWML JSON) or Object (structured SWML data) +- `transfer` (optional): String "true"/"false" or Boolean - controls if call exits agent + +**Proposed Python Method**: +```python +result.execute_swml(swml_content, transfer=False) +``` + +**Method Parameters**: +- `swml_content` (required): Flexible input supporting: + - **String**: Raw SWML JSON text + - **Dict**: SWML data structure (Python dictionary) + - **SWML Object**: SignalWire SWML SDK object with `.to_dict()` method +- `transfer` (optional): Boolean - whether call should exit agent after execution (default: False) + +**Usage Examples**: +```python +from signalwire.swml import SWML + +# 1. Raw SWML string (simple cases) +result.execute_swml('{"version":"1.0.0","sections":{"main":[{"say":"Hello World"}]}}') + +# 2. SWML dictionary (programmatic construction) +swml_dict = { + "version": "1.0.0", + "sections": { + "main": [ + {"say": "Please hold while I transfer you"}, + {"connect": {"to": "+15551234567"}} + ] + } +} +result.execute_swml(swml_dict, transfer=True) + +# 3. SWML SDK object (best developer experience with IDE support) +swml_doc = SWML() +swml_doc.add_application("main", "say", {"text": "Connecting you now"}) +swml_doc.add_application("main", "connect", {"to": "support@company.com"}) +result.execute_swml(swml_doc, transfer=True) + +# 4. Complex SWML with multiple sections +swml_doc = SWML() +swml_doc.add_application("main", "say", {"text": "Please choose an option"}) +swml_doc.add_application("main", "gather", { + "input": {"speech": {"timeout": 5}}, + "action_url": "/handle_choice" +}) +result.execute_swml(swml_doc) # No transfer, return to agent after execution +``` + +--- + +### 2. Call Hangup +**Purpose**: Terminate the call + +**JSON Structure**: +```json +{ + "hangup": true +} +``` + +**Parameters**: +- `hangup` (required): Boolean - must be true to hangup + +**Proposed Python Method**: +```python +result.hangup() +``` + +**Usage Examples**: +```python +result.hangup() +``` + +--- + +## Call Hold & Flow Control Actions + +### 3. Hold Call +**Purpose**: Put the call on hold with optional timeout + +**JSON Structure**: +```json +// Simple timeout +{ + "hold": 300 +} + +// String timeout (parsed) +{ + "hold": "5m" +} + +// Object with timeout +{ + "hold": { + "timeout": 300 + } +} +``` + +**Parameters**: +- `hold` (required): Number (seconds), String (time format), or Object + - `timeout` (required if object): Number - timeout in seconds (max 900) + +**Proposed Python Method**: +```python +result.hold(timeout=300) +``` + +**Usage Examples**: +```python +result.hold(60) # 60 seconds +result.hold("5m") # 5 minutes (if string parsing supported) +``` + +--- + +### 4. Wait for User Input +**Purpose**: Control how agent waits for user input + +**JSON Structure**: +```json +{ + "wait_for_user": true|false||"answer_first" +} +``` + +**Parameters**: +- `wait_for_user` (required): Boolean, Number (timeout), or String ("answer_first") + +**Proposed Python Method**: +```python +result.wait_for_user(enabled=True, timeout=None, answer_first=False) +``` + +**Usage Examples**: +```python +result.wait_for_user(True) # Wait indefinitely +result.wait_for_user(timeout=30) # Wait 30 seconds +result.wait_for_user(answer_first=True) # Special "answer_first" mode +``` + +--- + +### 5. Stop Agent +**Purpose**: Stop the agent execution + +**JSON Structure**: +```json +{ + "stop": true +} +``` + +**Parameters**: +- `stop` (required): Boolean - must be true to stop + +**Proposed Python Method**: +```python +result.stop() +``` + +**Usage Examples**: +```python +result.stop() +``` + +--- + +## Speech & Audio Control Actions + +### 6. Say Text +**Purpose**: Make the agent speak specific text + +**JSON Structure**: +```json +{ + "say": "" +} +``` + +**Parameters**: +- `say` (required): String - text for agent to speak + +**Proposed Python Method**: +```python +result.say(text) +``` + +**Usage Examples**: +```python +result.say("Hello, how can I help you today?") +``` + +--- + +### 7. Play Background Audio +**Purpose**: Play audio file in background + +**JSON Structure**: +```json +// Simple filename +{ + "playback_bg": "" +} + +// With options +{ + "playback_bg": { + "file": "", + "wait": true|false // optional + } +} +``` + +**Parameters**: +- `playback_bg` (required): String (filename) or Object + - `file` (required if object): String - audio filename/path + - `wait` (optional): Boolean - whether to suppress attention-getting behavior during playback + +**Proposed Python Method**: +```python +result.play_background_audio(filename, wait=False) +``` + +**Usage Examples**: +```python +# Play background audio, AI will try to get user attention per attention timeout +result.play_background_audio("hold_music.wav") + +# Play background audio, AI won't try to get user attention while playing +result.play_background_audio("announcement.mp3", wait=True) +``` + +**Behavior Notes**: +- Audio plays in background while AI can still hear and respond to user +- `wait=False` (default): AI will attempt to get user's attention according to attention timeout +- `wait=True`: AI suppresses attention-getting behavior during playback + +--- + +### 8. Stop Background Audio +**Purpose**: Stop currently playing background audio + +**JSON Structure**: +```json +{ + "stop_playback_bg": true +} +``` + +**Parameters**: +- `stop_playback_bg` (required): Boolean - must be true to stop playback + +**Proposed Python Method**: +```python +result.stop_background_audio() +``` + +**Usage Examples**: +```python +result.stop_background_audio() +``` + +--- + +## Speech Recognition Settings + +### 9. End of Speech Timeout +**Purpose**: Adjust speech detection timeout - milliseconds of silence after speaking has been detected to finalize speech recognition + +**JSON Structure**: +```json +{ + "end_of_speech_timeout": 1500 +} +``` + +**Parameters**: +- `end_of_speech_timeout` (required): Number or String - timeout in milliseconds of silence after speech detection to finalize recognition + +**Proposed Python Method**: +```python +result.set_end_of_speech_timeout(milliseconds) +``` + +**Usage Examples**: +```python +result.set_end_of_speech_timeout(2000) # Wait 2 seconds of silence to finalize speech +``` + +**Behavior Notes**: +- Mirrors the agent startup parameter of the same name +- Controls how long to wait for silence after speech is detected before finalizing recognition +- Higher values = more patient waiting for user to finish speaking +- Lower values = faster response but may cut off slow speakers + +--- + +### 10. Speech Event Timeout +**Purpose**: Adjust speech event timeout - milliseconds since last speech detection event to finalize recognition + +**JSON Structure**: +```json +{ + "speech_event_timeout": 5000 +} +``` + +**Parameters**: +- `speech_event_timeout` (required): Number or String - timeout in milliseconds since last speech detection event + +**Proposed Python Method**: +```python +result.set_speech_event_timeout(milliseconds) +``` + +**Usage Examples**: +```python +result.set_speech_event_timeout(3000) # Finalize after 3 seconds since last speech event +``` + +**Behavior Notes**: +- Mirrors the agent startup parameter of the same name +- Works better in noisy environments than `end_of_speech_timeout` +- Doesn't require silence - just lack of new speech detection events +- Useful when background noise prevents true silence detection +- More robust for real-world call environments + +--- + +## Data Management Actions + +### 11. Set Global Data +**Purpose**: Update global agent data variables + +**JSON Structure**: +```json +{ + "set_global_data": { + "key1": "value1", + "key2": "value2" + } +} +``` + +**Parameters**: +- `set_global_data` (required): Object - key-value pairs to set/update + +**Proposed Python Method**: +```python +result.update_global_data(data_dict) # Already implemented +``` + +**Usage Examples**: +```python +result.update_global_data({"user_name": "John", "step": 2}) +``` + +--- + +### 12. Unset Global Data +**Purpose**: Remove global agent data variables + +**JSON Structure**: +```json +// Single key +{ + "unset_global_data": "key_name" +} + +// Multiple keys +{ + "unset_global_data": ["key1", "key2"] +} +``` + +**Parameters**: +- `unset_global_data` (required): String (single key) or Array (multiple keys) + +**Proposed Python Method**: +```python +result.remove_global_data(keys) +``` + +**Usage Examples**: +```python +result.remove_global_data("temporary_data") +result.remove_global_data(["step", "temp_value"]) +``` + +--- + +### 13. Set Metadata +**Purpose**: Set metadata scoped to current function's meta_data_token + +**JSON Structure**: +```json +{ + "set_meta_data": { + "key1": "value1", + "key2": "value2" + } +} +``` + +**Parameters**: +- `set_meta_data` (required): Object - key-value pairs for metadata + +**Proposed Python Method**: +```python +result.set_metadata(data_dict) +``` + +**Usage Examples**: +```python +result.set_metadata({"session_id": "abc123", "user_tier": "premium"}) +``` + +**Scoping Behavior**: +- Metadata is scoped based on the `meta_data_token` parameter in the SWAIG function prototype +- If no `meta_data_token` is provided: scope defaults to function name/URL +- If `meta_data_token` is provided: functions with the same token share metadata scope +- This allows grouping related functions to share metadata + +--- + +### 14. Unset Metadata +**Purpose**: Remove metadata from current function's meta_data_token scope + +**JSON Structure**: +```json +// Single key +{ + "unset_meta_data": "key_name" +} + +// Multiple keys +{ + "unset_meta_data": ["key1", "key2"] +} +``` + +**Parameters**: +- `unset_meta_data` (required): String (single key) or Array (multiple keys) + +**Proposed Python Method**: +```python +result.remove_metadata(keys) +``` + +**Usage Examples**: +```python +result.remove_metadata("temp_session_data") +result.remove_metadata(["cache_key", "temp_flag"]) +``` + +**Scoping Behavior**: +- Removes metadata from the same scope as determined by `meta_data_token` in function prototype +- Scope follows same rules as `set_meta_data` action + +--- + +## Function & Behavior Control Actions + +### 15. Toggle Functions +**Purpose**: Enable/disable specific SWAIG functions + +**JSON Structure**: +```json +{ + "toggle_functions": [ + { + "function": "function_name", + "active": true|false + } + ] +} +``` + +**Parameters**: +- `toggle_functions` (required): Array of objects + - `function` (required): String - function name to toggle + - `active` (required): Boolean - whether function should be active + +**Proposed Python Method**: +```python +result.toggle_functions(function_toggles) +``` + +**Usage Examples**: +```python +result.toggle_functions([ + {"function": "transfer_call", "active": False}, + {"function": "lookup_info", "active": True} +]) +``` + +--- + +### 16. Functions on Timeout +**Purpose**: Enable function calls on speaker timeout + +**JSON Structure**: +```json +{ + "functions_on_speaker_timeout": true|false +} +``` + +**Parameters**: +- `functions_on_speaker_timeout` (required): Boolean + +**Proposed Python Method**: +```python +result.enable_functions_on_timeout(enabled=True) +``` + +**Usage Examples**: +```python +result.enable_functions_on_timeout(True) +``` + +--- + +### 17. Extensive Data +**Purpose**: Send full data to LLM for this turn only, then use smaller replacement in subsequent turns + +**JSON Structure**: +```json +{ + "extensive_data": true|false +} +``` + +**Parameters**: +- `extensive_data` (required): Boolean - whether to send extensive data this turn only + +**Proposed Python Method**: +```python +result.enable_extensive_data(enabled=True) +``` + +**Usage Examples**: +```python +result.enable_extensive_data(True) +``` + +**Behavior Notes**: +- When `true`: Sends full/detailed data to LLM for current response only +- After this turn: System automatically replaces extensive data with smaller version +- Useful for providing rich context without ongoing cost/latency impact +- Helps optimize token usage while maintaining context quality when needed + +--- + +## Agent Settings & Configuration Actions + +### 18. Update Settings +**Purpose**: Update agent runtime settings + +**JSON Structure**: +```json +{ + "settings": { + "frequency-penalty": -1.5, + "presence-penalty": 0.5, + "max-tokens": 1024, + "top-p": 0.9, + "confidence": 0.8, + "barge-confidence": 0.7, + "temperature": 0.7 + } +} +``` + +**Parameters**: +- `settings` (required): Object - settings key-value pairs with the following supported options: + - `frequency-penalty` (optional): Float (-2.0 to 2.0) - Penalizes repeated tokens + - `presence-penalty` (optional): Float (-2.0 to 2.0) - Penalizes tokens based on presence + - `max-tokens` (optional): Integer (0 to 4096) - Maximum tokens in response + - `top-p` (optional): Float (0.0 to 1.0) - Nucleus sampling parameter + - `confidence` (optional): Float (0.0 to 1.0) - Speech recognition confidence threshold + - `barge-confidence` (optional): Float (0.0 to 1.0) - Confidence threshold for barge-in + - `temperature` (optional): Float (0.0 to 2.0, clamped to 1.5) - Randomness in responses + +**Proposed Python Method**: +```python +result.update_settings(settings_dict) +``` + +**Usage Examples**: +```python +# Update AI model parameters +result.update_settings({ + "temperature": 0.7, + "max-tokens": 2048, + "frequency-penalty": -0.5 +}) + +# Update speech recognition settings +result.update_settings({ + "confidence": 0.8, + "barge-confidence": 0.7 +}) + +# Update all supported settings +result.update_settings({ + "frequency-penalty": 1.0, + "presence-penalty": 0.5, + "max-tokens": 1024, + "top-p": 0.9, + "confidence": 0.75, + "barge-confidence": 0.6, + "temperature": 0.8 +}) +``` + +--- + +### 19. Context Switch +**Purpose**: Change agent context/prompt during conversation + +**JSON Structure**: +```json +// Simple prompt change +{ + "context_switch": "" +} + +// Advanced context switching +{ + "context_switch": { + "system_prompt": "", + "user_prompt": "", + "system_pom": "", + "user_pom": "", + "consolidate": true|false, + "full_reset": true|false + } +} +``` + +**Parameters**: +- `context_switch` (required): String (simple prompt) or Object (advanced) + - `system_prompt` (optional): String - new system prompt + - `user_prompt` (optional): String - user message to add + - `system_pom` (optional): String - POM-based system prompt + - `user_pom` (optional): String - POM-based user prompt + - `consolidate` (optional): Boolean - summarize existing conversation + - `full_reset` (optional): Boolean - complete context reset + +**Proposed Python Method**: +```python +result.switch_context(system_prompt=None, user_prompt=None, consolidate=False, full_reset=False) +``` + +**Usage Examples**: +```python +# Simple context switch +result.switch_context("You are now a technical support agent") + +# Advanced context switch +result.switch_context( + system_prompt="You are a billing specialist", + user_prompt="The user needs help with their invoice", + consolidate=True +) +``` + +--- + +### 20. User Input Simulation +**Purpose**: Queue simulated user input + +**JSON Structure**: +```json +{ + "user_input": "" +} +``` + +**Parameters**: +- `user_input` (required): String - text to simulate as user input + +**Proposed Python Method**: +```python +result.simulate_user_input(text) +``` + +**Usage Examples**: +```python +result.simulate_user_input("Yes, I'd like to speak to billing") +``` + +--- + +## Notes + +- **Permissions**: Some actions require specific permissions (e.g., `swaig_allow_swml`, `swaig_allow_settings`, `swaig_set_global_data`) +- **Method Chaining**: All proposed methods should return `self` to enable method chaining +- **Type Safety**: Consider using type hints and validation for all parameters +- **Backward Compatibility**: Keep existing `add_action()` and `add_actions()` methods for custom use cases \ No newline at end of file diff --git a/docs/swml/agents-sdk/concepts/_category_.json b/docs/swml/agents-sdk/concepts/_category_.json new file mode 100644 index 00000000..66fd3428 --- /dev/null +++ b/docs/swml/agents-sdk/concepts/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Core concepts", + "collapsible": false, + "className": "menu-category", + "position": 2 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/concepts/architecture.md b/docs/swml/agents-sdk/concepts/architecture.md new file mode 100644 index 00000000..568c0bdb --- /dev/null +++ b/docs/swml/agents-sdk/concepts/architecture.md @@ -0,0 +1,198 @@ +--- +id: agents-sdk-architecture +slug: /agents-sdk/concepts/architecture +title: AI Agents SDK Architecture +sidebar_label: Architecture +--- + +# SignalWire AI Agents SDK Architecture + +## Overview + +The SignalWire AI Agents SDK provides a Python framework for building, deploying, and managing AI agents as microservices. These agents are self-contained web applications that expose HTTP endpoints to interact with the SignalWire platform. The SDK simplifies the creation of custom AI agents by handling common functionality like HTTP routing, prompt management, and tool execution. + +## Core Components + +### Class Hierarchy + +The SDK is built around a clear class hierarchy: + +- **SWMLService**: The foundation class providing SWML document creation and HTTP service capabilities + - **AgentBase**: Extends SWMLService with AI agent-specific functionality + - **Custom Agent Classes**: User implementations like SimpleAgent + - **Prefab Agents**: Ready-to-use agent types for common scenarios + +### Key Components + +1. **SWML Document Management** + - Schema validation for SWML documents + - Dynamic SWML verb creation and validation + - Document rendering and serving + +2. **Prompt Object Model (POM)** + - Structured format for defining AI prompts + - Section-based organization (Personality, Goal, Instructions, etc.) + - Programmatic prompt construction and manipulation + +3. **SWAIG Function Framework** + - Tool definition and registration system + - Parameter validation using JSON schema + - Security tokens for function execution + - Handler registry for function execution + +4. **HTTP Routing** + - FastAPI-based web service + - Endpoint routing for SWML, SWAIG, and other services + - Custom routing callbacks for dynamic endpoint handling + - SIP request routing for voice applications + - Basic authentication + +5. **State Management** + - Session-based state tracking + - Persistence options (file system, memory) + - State lifecycle hooks (startup, hangup) + +6. **Prefab Agents** + - Ready-to-use agent implementations + - Customizable configurations + - Extensible designs for common use cases + +7. **Skills System** + - Modular skill architecture for extending agent capabilities + - Automatic skill discovery from directory structure + - Parameter-configurable skills for customization + - Dependency validation (packages and environment variables) + - Built-in skills (web_search, datetime, math) + +## DataMap Tools + +The DataMap system provides a declarative approach to creating SWAIG tools that integrate with REST APIs without requiring custom webhook infrastructure. DataMap tools execute on SignalWire's server infrastructure, simplifying deployment and eliminating the need to expose webhook endpoints. + +### Architecture Overview + +DataMap tools follow a pipeline execution model on the SignalWire server: + +```mermaid +graph LR + A[Function Call
Arguments] --> B[Expression
Processing] + B --> C[Webhook
Execution] + C --> D[Response
Generation] + + A --> E[Variable
Expansion] + B --> F[Pattern
Matching] + C --> G[HTTP
Request] + D --> H[Template
Rendering] +``` + +### Core Components + +1. **Builder Pattern**: Fluent interface for constructing data_map configurations + ```python + tool = (DataMap('function_name') + .description('Function purpose') + .parameter('param', 'string', 'Description', required=True) + .webhook('GET', 'https://api.example.com/endpoint') + .output(SwaigFunctionResult('Response template')) + ) + ``` + +2. **Processing Pipeline**: Ordered execution with early termination + - **Expressions**: Pattern matching against arguments + - **Webhooks**: HTTP API calls with variable substitution + - **Foreach**: Array iteration for response processing + - **Output**: Final response generation using SwaigFunctionResult + +3. **Variable Expansion**: Dynamic substitution using `${variable}` syntax + - Function arguments: `${args.parameter_name}` + - API responses: `${response.field.nested_field}` + - Array elements: `${foreach.item_field}` + - Global data: `${global_data.key}` + - Metadata: `${meta_data.call_id}` + +### Tool Types + +The system supports different tool patterns: + +1. **API Integration Tools**: Direct REST API calls + ```python + weather_tool = (DataMap('get_weather') + .webhook('GET', 'https://api.weather.com/v1/current?q=${location}') + .output(SwaigFunctionResult('Weather: ${response.current.condition}')) + ) + ``` + +2. **Expression-Based Tools**: Pattern matching without API calls + ```python + control_tool = (DataMap('file_control') + .expression(r'start.*', SwaigFunctionResult().add_action('start', True)) + .expression(r'stop.*', SwaigFunctionResult().add_action('stop', True)) + ) + ``` + +3. **Array Processing Tools**: Handle list responses + ```python + search_tool = (DataMap('search_docs') + .webhook('GET', 'https://api.docs.com/search') + .foreach('${response.results}') + .output(SwaigFunctionResult('Found: ${foreach.title}')) + ) + ``` + +### Integration with Agent Architecture + +DataMap tools integrate seamlessly with the existing agent architecture: + +```mermaid +graph LR + A[AgentBase
.register_swaig_function] --> B[SWAIG Function
Registry] + B --> C[SignalWire Server
Execution] + + A --> D[DataMap
Builder] + B --> E[Function
Definition] + C --> F[Variable
Expansion] +``` + +1. **Registration**: DataMap tools are registered as SWAIG functions +2. **Execution**: Tools run on SignalWire infrastructure, not agent servers +3. **Response**: Results are returned to the agent as function responses + +### Configuration Architecture + +DataMap configurations use a hierarchical structure: + +```json +{ + "function": "tool_name", + "description": "Tool description", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + }, + "data_map": { + "expressions": [], + "webhooks": [], + "foreach": "path", + "output": {}, + "error_keys": [] + } +} +``` + +This structure separates: +- **Function Metadata**: Name, description, parameters +- **Processing Logic**: Expressions, webhooks, array handling +- **Output Definition**: Response templates and actions + +### Benefits and Trade-offs + +**Benefits:** +- No webhook infrastructure required +- Simplified deployment model +- Built-in authentication and error handling +- Server-side execution (no agent load) +- Automatic variable expansion + +**Trade-offs:** +- Limited to REST API patterns +- No complex processing logic \ No newline at end of file diff --git a/docs/swml/agents-sdk/concepts/contexts.md b/docs/swml/agents-sdk/concepts/contexts.md new file mode 100644 index 00000000..00a6345e --- /dev/null +++ b/docs/swml/agents-sdk/concepts/contexts.md @@ -0,0 +1,1220 @@ +--- +id: agents-sdk-concepts-contexts +slug: /agents-sdk/concepts/contexts +title: Contexts Guide +sidebar_label: Contexts +--- + +# Contexts and Steps Guide + +## Overview + +The **Contexts and Steps** system enhances traditional Prompt Object Model (POM) prompts in SignalWire AI agents by adding structured workflows on top of your base prompt. Instead of just defining a single prompt, you create workflows with explicit steps, navigation rules, and completion criteria. + +### Key Benefits + +- **Structured Workflows**: Define clear, step-by-step processes +- **Explicit Navigation**: Control exactly where users can go next +- **Function Restrictions**: Limit AI tool access per step +- **Completion Criteria**: Define clear progression requirements +- **Context Isolation**: Separate different conversation flows +- **Debugging**: Easier to trace and debug complex interactions + +### When to Use Contexts vs Traditional Prompts + +**Use Contexts and Steps when:** +- Building multi-step workflows (onboarding, support tickets, applications) +- Need explicit navigation control between conversation states +- Want to restrict function access based on conversation stage +- Building complex customer service or troubleshooting flows +- Creating guided experiences with clear progression + +**Use Traditional Prompts when:** +- Building simple, freeform conversational agents +- Want maximum flexibility in conversation flow +- Creating general-purpose assistants +- Prototyping or building simple proof-of-concepts + +## Core Concepts + +### Contexts + +A **Context** represents a conversation state or workflow area. Contexts can be: + +- **Workflow Container**: Simple step organization without state changes +- **Context Switch**: Triggers conversation state changes when entered + +Each context can define: + +- **Steps**: Individual workflow stages within the context +- **Context Prompts**: Guidance that applies to all steps in the context +- **Entry Parameters**: Control conversation state when context is entered +- **Navigation Rules**: Which other contexts can be accessed + +### Context Entry Parameters + +When entering a context, these parameters control conversation behavior: + +- **`post_prompt`**: Override the agent's post prompt for this context +- **`system_prompt`**: Trigger conversation reset with new instructions +- **`consolidate`**: Summarize previous conversation in new prompt +- **`full_reset`**: Complete system prompt replacement vs injection +- **`user_prompt`**: Inject user message for context establishment + +**Important**: If `system_prompt` is present, the context becomes a "Context Switch Context" that processes entry parameters like a `context_switch` SWAIG action. Without `system_prompt`, it's a "Workflow Container Context" that only organizes steps. + +### Context Prompts + +Contexts can have their own prompts (separate from entry parameters): + +```python +# Simple string prompt +context.set_prompt("Context-specific guidance") + +# POM-style sections +context.add_section("Department", "Billing Department") +context.add_bullets("Services", ["Payments", "Refunds", "Account inquiries"]) +``` + +Context prompts provide guidance that applies to all steps within that context, creating a prompt hierarchy: Base Agent Prompt → Context Prompt → Step Prompt. + +### Steps + +A **Step** is a specific stage within a context. Each step defines: + +- **Prompt Content**: What the AI says/does (text or POM sections) +- **Completion Criteria**: When the step is considered complete +- **Navigation Rules**: Where the user can go next +- **Function Access**: Which AI tools are available + +### Navigation Control + +The system provides fine-grained control over conversation flow: + +- **Valid Steps**: Control movement within a context +- **Valid Contexts**: Control switching between contexts +- **Implicit Navigation**: Automatic "next" step progression +- **Explicit Navigation**: User must explicitly choose next step + +## Getting Started + +### Basic Single-Context Workflow + +```python +from signalwire_agents import AgentBase + +class OnboardingAgent(AgentBase): + def __init__(self): + super().__init__(name="Onboarding Assistant", route="/onboarding") + + # Define contexts (replaces traditional prompt setup) + contexts = self.define_contexts() + + # Single context must be named "default" + workflow = contexts.add_context("default") + + # Step 1: Welcome + workflow.add_step("welcome") \ + .set_text("Welcome to our service! Let's get you set up. What's your name?") \ + .set_step_criteria("User has provided their name") \ + .set_valid_steps(["collect_email"]) + + # Step 2: Collect Email + workflow.add_step("collect_email") \ + .set_text("Thanks! Now I need your email address to create your account.") \ + .set_step_criteria("Valid email address has been provided") \ + .set_valid_steps(["confirm_details"]) + + # Step 3: Confirmation + workflow.add_step("confirm_details") \ + .set_text("Perfect! Let me confirm your details before we proceed.") \ + .set_step_criteria("User has confirmed their information") \ + .set_valid_steps(["complete"]) + + # Step 4: Completion + workflow.add_step("complete") \ + .set_text("All set! Your account has been created successfully.") + # No valid_steps = end of workflow + +agent = OnboardingAgent() +agent.run() + +if __name__ == "__main__": + main() +``` + +### Multi-Context Workflow + +```python +class CustomerServiceAgent(AgentBase): + def __init__(self): + super().__init__(name="Customer Service", route="/service") + + # Add skills for enhanced capabilities + self.add_skill("datetime") + self.add_skill("web_search", { + "api_key": "your-api-key", + "search_engine_id": "your-engine-id" + }) + + contexts = self.define_contexts() + + # Main triage context + triage = contexts.add_context("triage") + triage.add_step("greeting") \ + .add_section("Current Task", "Understand the customer's need and route appropriately") \ + .add_bullets("Required Information", [ + "Type of issue they're experiencing", + "Urgency level of the problem", + "Previous troubleshooting attempts" + ]) \ + .set_step_criteria("Customer's need has been identified") \ + .set_valid_contexts(["technical", "billing", "general"]) + + # Technical support context + tech = contexts.add_context("technical") + tech.add_step("technical_help") \ + .add_section("Current Task", "Help diagnose and resolve technical issues") \ + .add_section("Available Tools", "Use web search and datetime functions for technical solutions") \ + .set_functions(["web_search", "datetime"]) \ + .set_step_criteria("Issue is resolved or escalated") \ + .set_valid_contexts(["triage"]) + + # Billing context (restricted functions for security) + billing = contexts.add_context("billing") + billing.add_step("billing_help") \ + .set_text("I'll help with your billing question. For security, please provide your account verification.") \ + .set_functions("none") \ + .set_step_criteria("Billing issue is addressed") \ + .set_valid_contexts(["triage"]) + + # General inquiries context + general = contexts.add_context("general") + general.add_step("general_help") \ + .set_text("I'm here to help with general questions. What can I assist you with?") \ + .set_functions(["web_search", "datetime"]) \ + .set_step_criteria("Question has been answered") \ + .set_valid_contexts(["triage"]) + +agent = CustomerServiceAgent() +agent.run() + +if __name__ == "__main__": + main() +``` + +## API Reference + +### ContextBuilder + +The main entry point for defining contexts and steps. + +```python +# Get the builder +contexts = self.define_contexts() + +# Create contexts +context = contexts.add_context(name: str) -> Context +``` + +### Context + +Represents a conversation context or workflow state. + +```python +class Context: + def add_step(self, name: str) -> Step + """Create a new step in this context""" + + def set_valid_contexts(self, contexts: List[str]) -> Context + """Set which contexts can be accessed from this context""" + + # Context entry parameters + def set_post_prompt(self, post_prompt: str) -> Context + """Override post prompt for this context""" + + def set_system_prompt(self, system_prompt: str) -> Context + """Trigger context switch with new system prompt""" + + def set_consolidate(self, consolidate: bool) -> Context + """Consolidate conversation history when entering""" + + def set_full_reset(self, full_reset: bool) -> Context + """Full system prompt replacement vs injection""" + + def set_user_prompt(self, user_prompt: str) -> Context + """Inject user message for context""" + + # Context prompts + def set_prompt(self, prompt: str) -> Context + """Set simple string prompt for context""" + + def add_section(self, title: str, body: str) -> Context + """Add POM section to context prompt""" + + def add_bullets(self, title: str, bullets: List[str]) -> Context + """Add POM bullet section to context prompt""" +``` + +#### Methods + +- `add_step(name)`: Create and return a new Step +- `set_valid_contexts(contexts)`: Allow navigation to specified contexts +- `set_post_prompt(prompt)`: Override agent's post prompt for this context +- `set_system_prompt(prompt)`: Trigger context switch behavior (makes this a Context Switch Context) +- `set_consolidate(bool)`: Whether to consolidate conversation when entering +- `set_full_reset(bool)`: Complete vs partial context reset +- `set_user_prompt(prompt)`: User message to inject when entering context +- `set_prompt(text)`: Simple string prompt for context +- `add_section(title, body)`: Add POM section to context prompt +- `add_bullets(title, list)`: Add POM bullet section to context prompt + +### Step + +Represents a single step within a context workflow. + +```python +class Step: + # Content definition (choose one approach) + def set_text(self, text: str) -> Step + """Set direct text prompt (mutually exclusive with POM sections)""" + + def add_section(self, title: str, body: str = "") -> Step + """Add a POM-style section (mutually exclusive with set_text)""" + + def add_bullets(self, bullets: List[str], numbered: bool = False) -> Step + """Add bullets to the current or most recent section""" + + # Flow control + def set_step_criteria(self, criteria: str) -> Step + """Define completion criteria for this step""" + + def set_valid_steps(self, steps: List[str]) -> Step + """Set which steps can be accessed next in same context""" + + def set_valid_contexts(self, contexts: List[str]) -> Step + """Set which contexts can be accessed from this step""" + + # Function restrictions + def set_functions(self, functions: Union[List[str], str]) -> Step + """Restrict available functions ('none' or list of function names)""" +``` + +#### Content Methods + +**Option 1: Direct Text** +```python +step.set_text("Direct prompt text for the AI") +``` + +**Option 2: POM-Style Sections** +```python +step.add_section("Role", "You are a helpful assistant") \ + .add_section("Instructions", "Help users with their questions") \ + .add_bullets(["Be friendly", "Ask clarifying questions"]) +``` + +**Note**: You cannot mix `set_text()` with `add_section()` in the same step. + +#### Navigation Methods + +```python +# Control step progression within context +step.set_valid_steps(["step1", "step2"]) # Can go to step1 or step2 +step.set_valid_steps([]) # Cannot progress (dead end) +# No set_valid_steps() call = implicit "next" step + +# Control context switching +step.set_valid_contexts(["context1", "context2"]) # Can switch contexts +step.set_valid_contexts([]) # Trapped in current context +# No set_valid_contexts() call = inherit from context level +``` + +#### Function Restriction Methods + +```python +# Allow specific functions only +step.set_functions(["datetime", "math"]) + +# Block all functions +step.set_functions("none") + +# No restriction (default - all agent functions available) +# step.set_functions() # Don't call this method +``` + +## Navigation and Flow Control + +### Step Navigation Rules + +The `set_valid_steps()` method controls movement within a context: + +```python +# Explicit step list - can only go to these steps +step.set_valid_steps(["review", "edit", "cancel"]) + +# Empty list - dead end, cannot progress +step.set_valid_steps([]) + +# Not called - implicit "next" step progression +# (will go to the next step defined in the context) +``` + +### Context Navigation Rules + +The `set_valid_contexts()` method controls switching between contexts: + +```python +# Can switch to these contexts +step.set_valid_contexts(["billing", "technical", "general"]) + +# Trapped in current context +step.set_valid_contexts([]) + +# Not called - inherit from context-level settings +``` + +### Navigation Inheritance + +Context-level navigation settings are inherited by steps: + +```python +# Set at context level +context.set_valid_contexts(["main", "help"]) + +# All steps in this context can access main and help contexts +# unless overridden at step level +step.set_valid_contexts(["main"]) # Override - only main allowed +``` + +### Complete Navigation Example + +```python +contexts = self.define_contexts() + +# Main context +main = contexts.add_context("main") +main.set_valid_contexts(["help", "settings"]) # Context-level setting + +main.add_step("welcome") \ + .set_text("Welcome! How can I help you?") \ + .set_valid_steps(["menu"]) # Must go to menu + # Inherits context-level valid_contexts + +main.add_step("menu") \ + .set_text("Choose an option: 1) Help 2) Settings 3) Continue") \ + .set_valid_contexts(["help", "settings", "main"]) # Override context setting + # No valid_steps = this is a branching point + +# Help context +help_ctx = contexts.add_context("help") +help_ctx.add_step("help_info") \ + .set_text("Here's how to use the system...") \ + .set_valid_contexts(["main"]) # Can return to main + +# Settings context +settings = contexts.add_context("settings") +settings.add_step("settings_menu") \ + .set_text("Choose a setting to modify...") \ + .set_valid_contexts(["main"]) # Can return to main +``` + +## Function Restrictions + +Control which AI tools/functions are available in each step for enhanced security and user experience. + +### Function Restriction Levels + +```python +# No restrictions (default) - all agent functions available +step # Don't call set_functions() + +# Allow specific functions only +step.set_functions(["datetime", "math", "web_search"]) + +# Block all functions +step.set_functions("none") +``` + +### Security-Focused Example + +```python +class SecureBankingAgent(AgentBase): + def __init__(self): + super().__init__(name="Banking Assistant", route="/banking") + + # Add potentially sensitive functions + self.add_skill("web_search", {"api_key": "key", "search_engine_id": "id"}) + self.add_skill("datetime") + + contexts = self.define_contexts() + + # Public context - full access + public = contexts.add_context("public") + public.add_step("welcome") \ + .set_text("Welcome to banking support. Are you an existing customer?") \ + .set_functions(["datetime", "web_search"]) # Safe functions only \ + .set_valid_contexts(["authenticated", "public"]) + + # Authenticated context - restricted for security + auth = contexts.add_context("authenticated") + auth.add_step("account_access") \ + .set_text("I can help with your account. What do you need assistance with?") \ + .set_functions("none") # No external functions for account data \ + .set_valid_contexts(["public"]) # Can log out +``` + +### Function Access Patterns + +```python +# Progressive function access based on trust level +contexts = self.define_contexts() + +# Low trust - limited functions +public = contexts.add_context("public") +public.add_step("initial_contact") \ + .set_functions(["datetime"]) # Only safe functions + +# Medium trust - more functions +verified = contexts.add_context("verified") +verified.add_step("verified_user") \ + .set_functions(["datetime", "web_search"]) # Add search capability + +# High trust - full access +authenticated = contexts.add_context("authenticated") +authenticated.add_step("full_access") \ + # No set_functions() call = all functions available +``` + +## Real-World Examples + +### Example 1: Technical Support Troubleshooting + +```python +class TechnicalSupportAgent(AgentBase): + def __init__(self): + super().__init__(name="Tech Support", route="/tech-support") + + # Add diagnostic tools + self.add_skill("web_search", {"api_key": "key", "search_engine_id": "id"}) + self.add_skill("datetime") + + contexts = self.define_contexts() + + # Initial triage + triage = contexts.add_context("triage") + triage.add_step("problem_identification") \ + .add_section("Current Task", "Identify the type of technical issue") \ + .add_bullets("Information to Gather", [ + "Description of the specific problem", + "When did the issue start occurring?", + "What steps has the customer already tried?", + "Rate the severity level (critical/high/medium/low)" + ]) \ + .set_step_criteria("Issue type and severity determined") \ + .set_valid_contexts(["hardware", "software", "network"]) + + # Hardware troubleshooting + hardware = contexts.add_context("hardware") + hardware.add_step("hardware_diagnosis") \ + .add_section("Current Task", "Guide user through hardware diagnostics") \ + .add_section("Available Tools", "Use web search to find hardware specifications and troubleshooting guides") \ + .set_functions(["web_search"]) # Can search for hardware info \ + .set_step_criteria("Hardware issue diagnosed") \ + .set_valid_steps(["hardware_solution"]) + + hardware.add_step("hardware_solution") \ + .set_text("Based on the diagnosis, here's how to resolve the hardware issue...") \ + .set_step_criteria("Solution provided and tested") \ + .set_valid_contexts(["triage"]) # Can start over if needed + + # Software troubleshooting + software = contexts.add_context("software") + software.add_step("software_diagnosis") \ + .add_section("Current Task", "Diagnose software-related issues") \ + .add_section("Available Tools", "Use web search for software updates and datetime to check for recent changes") \ + .set_functions(["web_search", "datetime"]) # Can check for updates \ + .set_step_criteria("Software issue identified") \ + .set_valid_steps(["software_fix", "escalation"]) + + software.add_step("software_fix") \ + .set_text("Let's try these software troubleshooting steps...") \ + .set_step_criteria("Fix attempted and result confirmed") \ + .set_valid_steps(["escalation", "resolution"]) + + software.add_step("escalation") \ + .set_text("I'll escalate this to our specialist team.") \ + .set_functions("none") # No tools needed for escalation \ + .set_step_criteria("Escalation ticket created") + + software.add_step("resolution") \ + .set_text("Great! The issue has been resolved.") \ + .set_step_criteria("Customer confirms resolution") \ + .set_valid_contexts(["triage"]) + + # Network troubleshooting + network = contexts.add_context("network") + network.add_step("network_diagnosis") \ + .add_section("Current Task", "Diagnose network and connectivity issues") \ + .add_section("Available Tools", "Use web search to check service status and datetime for outage windows") \ + .set_functions(["web_search", "datetime"]) # Check service status \ + .set_step_criteria("Network issue diagnosed") \ + .set_valid_steps(["network_fix"]) + + network.add_step("network_fix") \ + .set_text("Let's resolve your connectivity issue with these steps...") \ + .set_step_criteria("Network connectivity restored") \ + .set_valid_contexts(["triage"]) + +agent = TechnicalSupportAgent() +agent.run() + +if __name__ == "__main__": + main() +``` + +### Example 2: Multi-Step Application Process + +```python +class LoanApplicationAgent(AgentBase): + def __init__(self): + super().__init__(name="Loan Application", route="/loan-app") + + # Add verification tools + self.add_skill("datetime") # For date validation + + contexts = self.define_contexts() + + # Single workflow context + application = contexts.add_context("default") + + # Step 1: Introduction and eligibility + application.add_step("introduction") \ + .add_section("Current Task", "Guide customers through the loan application process") \ + .add_bullets("Information to Provide", [ + "Explain the process clearly", + "Outline what information will be needed", + "Set expectations for timeline and next steps" + ]) \ + .set_step_criteria("Customer understands process and wants to continue") \ + .set_valid_steps(["personal_info"]) + + # Step 2: Personal information + application.add_step("personal_info") \ + .add_section("Instructions", "Collect personal information") \ + .add_bullets([ + "Full legal name", + "Date of birth", + "Social Security Number", + "Phone number and email" + ]) \ + .set_functions(["datetime"]) # Can validate dates \ + .set_step_criteria("All personal information collected and verified") \ + .set_valid_steps(["employment_info", "personal_info"]) # Can review/edit + + # Step 3: Employment information + application.add_step("employment_info") \ + .set_text("Now I need information about your employment and income.") \ + .set_step_criteria("Employment and income information complete") \ + .set_valid_steps(["financial_info", "personal_info"]) # Can go back + + # Step 4: Financial information + application.add_step("financial_info") \ + .set_text("Let's review your financial situation including assets and debts.") \ + .set_step_criteria("Financial information complete") \ + .set_valid_steps(["review", "employment_info"]) # Can go back + + # Step 5: Review all information + application.add_step("review") \ + .add_section("Instructions", "Review all collected information") \ + .add_bullets([ + "Confirm personal details", + "Verify employment information", + "Review financial data", + "Ensure accuracy before submission" + ]) \ + .set_step_criteria("Customer has reviewed and confirmed all information") \ + .set_valid_steps(["submit", "personal_info", "employment_info", "financial_info"]) + + # Step 6: Submission + application.add_step("submit") \ + .set_text("Thank you! Your loan application has been submitted successfully. You'll receive a decision within 2-3 business days.") \ + .set_functions("none") # No tools needed for final message \ + .set_step_criteria("Application submitted and confirmation provided") + # No valid_steps = end of process + +agent = LoanApplicationAgent() +agent.run() + +if __name__ == "__main__": + main() +``` + +### Example 3: E-commerce Customer Service + +```python +class EcommerceServiceAgent(AgentBase): + def __init__(self): + super().__init__(name="E-commerce Support", route="/ecommerce") + + # Add tools for order management + self.add_skill("web_search", {"api_key": "key", "search_engine_id": "id"}) + self.add_skill("datetime") + + contexts = self.define_contexts() + + # Main service menu + main = contexts.add_context("main") + main.add_step("service_menu") \ + .add_section("Current Task", "Help customers with their orders and questions") \ + .add_bullets("Service Areas Available", [ + "Order status, modifications, and tracking", + "Returns and refunds", + "Product information and specifications", + "Account-related questions" + ]) \ + .set_step_criteria("Customer's need has been identified") \ + .set_valid_contexts(["orders", "returns", "products", "account"]) + + # Order management context + orders = contexts.add_context("orders") + orders.add_step("order_assistance") \ + .add_section("Current Task", "Help with order status, modifications, and tracking") \ + .add_section("Available Tools", "Use datetime to check delivery dates and processing times") \ + .set_functions(["datetime"]) # Can check delivery dates \ + .set_step_criteria("Order issue resolved or escalated") \ + .set_valid_contexts(["main"]) + + # Returns and refunds context + returns = contexts.add_context("returns") + returns.add_step("return_process") \ + .add_section("Current Task", "Guide customers through return process") \ + .add_bullets("Return Process Steps", [ + "Verify return eligibility", + "Explain return policy", + "Provide return instructions", + "Process refund if applicable" + ]) \ + .set_functions("none") # Sensitive financial operations \ + .set_step_criteria("Return request processed") \ + .set_valid_contexts(["main"]) + + # Product information context + products = contexts.add_context("products") + products.add_step("product_help") \ + .add_section("Current Task", "Help customers with product questions") \ + .add_section("Available Tools", "Use web search to find detailed product information and specifications") \ + .set_functions(["web_search"]) # Can search for product info \ + .set_step_criteria("Product question answered") \ + .set_valid_contexts(["main"]) + + # Account management context + account = contexts.add_context("account") + account.add_step("account_help") \ + .set_text("I can help with account-related questions. Please verify your identity first.") \ + .set_functions("none") # Security-sensitive context \ + .set_step_criteria("Account issue resolved") \ + .set_valid_contexts(["main"]) + +agent = EcommerceServiceAgent() +agent.run() + +if __name__ == "__main__": + main() +``` + +## Best Practices + +### 1. Clear Step Naming + +Use descriptive step names that indicate purpose: + +```python +# Good +.add_step("collect_shipping_address") +.add_step("verify_payment_method") +.add_step("confirm_order_details") + +# Avoid +.add_step("step1") +.add_step("next") +.add_step("continue") +``` + +### 2. Meaningful Completion Criteria + +Define clear, testable completion criteria: + +```python +# Good - specific and measurable +.set_step_criteria("User has provided valid email address and confirmed subscription preferences") +.set_step_criteria("All required fields completed and payment method verified") + +# Avoid - vague or subjective +.set_step_criteria("User is ready") +.set_step_criteria("Everything is good") +``` + +### 3. Logical Navigation Flow + +Design intuitive navigation that matches user expectations: + +```python +# Allow users to go back and review +.set_valid_steps(["review_info", "edit_details", "confirm_submission"]) + +# Provide escape routes +.set_valid_contexts(["main_menu", "help"]) + +# Consider dead ends carefully +.set_valid_steps([]) # Only if this is truly the end +``` + +### 4. Progressive Function Access + +Restrict functions based on security and context needs: + +```python +# Public areas - limited functions +public_step.set_functions(["datetime", "web_search"]) + +# Authenticated areas - more functions allowed +auth_step.set_functions(["datetime", "web_search", "user_profile"]) + +# Sensitive operations - minimal functions +billing_step.set_functions("none") +``` + +### 5. Context Organization + +Organize contexts by functional area or user journey: + +```python +# By functional area +contexts = ["triage", "technical_support", "billing", "account_management"] + +# By user journey stage +contexts = ["onboarding", "verification", "configuration", "completion"] + +# By security level +contexts = ["public", "authenticated", "admin"] +``` + +### 6. Error Handling and Recovery + +Provide recovery paths for common issues: + +```python +# Allow users to retry failed steps +.set_valid_steps(["retry_payment", "choose_different_method", "contact_support"]) + +# Provide help context access +.set_valid_contexts(["help", "main"]) + +# Include validation steps +verification_step.add_step("validation") \ + .set_step_criteria("Data validation passed") \ + .set_valid_steps(["proceed", "edit_data"]) +``` + +### 7. Content Strategy + +Choose the right content approach for each step: + +```python +# Use set_text() for simple, direct instructions +step.set_text("Please provide your email address") + +# Use POM sections for complex, structured content +step.add_section("Role", "You are a technical specialist") \ + .add_section("Context", "Customer is experiencing network issues") \ + .add_section("Instructions", "Follow diagnostic protocol") \ + .add_bullets(["Check connectivity", "Test speed", "Verify settings"]) +``` + +## Troubleshooting + +### Common Issues + +#### 1. "Single context must be named 'default'" + +**Error**: When using a single context with a name other than "default" + +```python +# Wrong +context = contexts.add_context("main") # Error! + +# Correct +context = contexts.add_context("default") +``` + +#### 2. "Cannot mix set_text with add_section" + +**Error**: Using both direct text and POM sections in the same step + +```python +# Wrong +step.set_text("Welcome!") \ + .add_section("Role", "Assistant") # Error! + +# Correct - choose one approach +step.set_text("Welcome! I'm your assistant.") +# OR +step.add_section("Role", "Assistant") \ + .add_section("Message", "Welcome!") +``` + +#### 3. Navigation Issues + +**Problem**: Users getting stuck or unable to navigate + +```python +# Check your navigation rules +step.set_valid_steps([]) # Dead end - is this intended? +step.set_valid_contexts([]) # Trapped in context - is this intended? + +# Add appropriate navigation +step.set_valid_steps(["next_step", "previous_step"]) +step.set_valid_contexts(["main", "help"]) +``` + +#### 4. Function Access Problems + +**Problem**: Functions not available when expected + +```python +# Check function restrictions +step.set_functions("none") # All functions blocked +step.set_functions(["datetime"]) # Only datetime allowed + +# Verify function names match your agent's functions +self.add_skill("web_search") # Function name is "web_search" +step.set_functions(["web_search"]) # Must match exactly +``` + +### Debugging Tips + +#### 1. Trace Navigation Flow + +Add logging to understand flow: + +```python +def create_step_with_logging(self, name): + step = context.add_step(name) + print(f"Created step: {name}") + return step +``` + +#### 2. Validate Navigation Rules + +Check that all referenced steps/contexts exist: + +```python +# Ensure referenced steps exist +.set_valid_steps(["review", "edit"]) # Both "review" and "edit" steps must exist + +# Ensure referenced contexts exist +.set_valid_contexts(["main", "help"]) # Both "main" and "help" contexts must exist +``` + +#### 3. Test Function Restrictions + +Verify functions are properly restricted: + +```python +# Test with all functions +# step # No set_functions() call + +# Test with restrictions +step.set_functions(["datetime"]) + +# Test with no functions +step.set_functions("none") +``` + +## Migration from POM + +### Converting Traditional Prompts + +**Before (Traditional POM):** +```python +class TraditionalAgent(AgentBase): + def __init__(self): + super().__init__(name="assistant", route="/assistant") + + self.prompt_add_section("Role", "You are a helpful assistant") + self.prompt_add_section("Instructions", "Help users with questions") + self.prompt_add_section("Guidelines", bullets=[ + "Be friendly", + "Ask clarifying questions", + "Provide accurate information" + ]) +``` + +**After (Contexts and Steps):** +```python +class ContextsAgent(AgentBase): + def __init__(self): + super().__init__(name="assistant", route="/assistant") + + contexts = self.define_contexts() + main = contexts.add_context("default") + + main.add_step("assistance") \ + .add_section("Role", "You are a helpful assistant") \ + .add_section("Instructions", "Help users with questions") \ + .add_section("Guidelines", bullets=[ + "Be friendly", + "Ask clarifying questions", + "Provide accurate information" + ]) \ + .set_step_criteria("User's question has been answered") +``` + +### Hybrid Approach + +You can use both traditional prompts and contexts in the same agent: + +```python +class HybridAgent(AgentBase): + def __init__(self): + super().__init__(name="hybrid", route="/hybrid") + + # Traditional prompt sections (from skills, global settings, etc.) + # These will coexist with contexts + + # Define contexts for structured workflows + contexts = self.define_contexts() + workflow = contexts.add_context("default") + + workflow.add_step("structured_process") \ + .set_text("Following the structured workflow...") \ + .set_step_criteria("Workflow complete") +``` + +### Migration Strategy + +1. **Start Simple**: Convert one workflow at a time +2. **Preserve Existing**: Keep traditional prompts for simple interactions +3. **Add Structure**: Use contexts for complex, multi-step processes +4. **Test Thoroughly**: Verify navigation and function access work as expected +5. **Iterate**: Refine step criteria and navigation based on testing + +--- + +## Conclusion + +The Contexts and Steps system provides powerful workflow control for building sophisticated AI agents. By combining structured navigation, function restrictions, and clear completion criteria, you can create predictable, user-friendly agent experiences that guide users through complex processes while maintaining security and control. + +Start with simple single-context workflows and gradually build more complex multi-context systems as your requirements grow. The system is designed to be flexible and scalable, supporting both simple linear workflows and complex branching conversation trees. + +### Context Inheritance + +Contexts can inherit from other contexts to create hierarchical structures: + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.context import Context + +class CustomerServiceAgent(AgentBase): + def __init__(self): + super().__init__(name="customer-service", route="/support") + + # Base context for all customer interactions + base_context = Context( + name="customer_base", + description="Base context for all customer interactions", + instructions=[ + "Always be polite and professional", + "Verify customer identity before accessing account information", + "Document all interactions in the customer record" + ] + ) + + # Billing context inherits from base + billing_context = Context( + name="billing_support", + description="Handle billing inquiries and payment issues", + parent=base_context, # Inherits from base_context + instructions=[ + "Check payment history before suggesting solutions", + "Offer payment plan options for overdue accounts", + "Escalate disputes over $500 to billing manager" + ] + ) + + # Technical support context also inherits from base + tech_context = Context( + name="technical_support", + description="Provide technical assistance and troubleshooting", + parent=base_context, # Also inherits from base_context + instructions=[ + "Start with basic troubleshooting steps", + "Document error messages and symptoms", + "Create support tickets for unresolved issues" + ] + ) + + self.add_context(base_context) + self.add_context(billing_context) + self.add_context(tech_context) + +def main(): + agent = CustomerServiceAgent() + agent.run() + +if __name__ == "__main__": + main() +``` + +## Dynamic Context Switching + +Contexts can be switched dynamically during conversations based on user input or business logic: + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.context import Context +from signalwire_agents.core.function_result import SwaigFunctionResult + +class AdaptiveAgent(AgentBase): + def __init__(self): + super().__init__(name="adaptive", route="/adaptive") + + # Define multiple contexts + self.setup_contexts() + + # Start with general context + self.set_active_context("general") + + def setup_contexts(self): + general = Context( + name="general", + description="General conversation and routing", + instructions=[ + "Determine what the user needs help with", + "Route to appropriate specialized context", + "Be helpful and friendly" + ] + ) + + sales = Context( + name="sales", + description="Sales and product information", + instructions=[ + "Focus on product benefits and features", + "Understand customer needs and budget", + "Provide pricing and availability information" + ] + ) + + support = Context( + name="support", + description="Technical support and troubleshooting", + instructions=[ + "Diagnose technical issues systematically", + "Provide step-by-step solutions", + "Escalate complex problems to specialists" + ] + ) + + self.add_context(general) + self.add_context(sales) + self.add_context(support) + + @AgentBase.tool( + name="switch_to_sales", + description="Switch to sales context for product inquiries", + parameters={} + ) + def switch_to_sales(self, args, raw_data): + self.set_active_context("sales") + return SwaigFunctionResult("Switching to sales mode. How can I help you with our products?") + + @AgentBase.tool( + name="switch_to_support", + description="Switch to technical support context", + parameters={} + ) + def switch_to_support(self, args, raw_data): + self.set_active_context("support") + return SwaigFunctionResult("Switching to technical support. What issue are you experiencing?") + +def main(): + agent = AdaptiveAgent() + agent.run() + +if __name__ == "__main__": + main() +``` + +## Context-Aware Function Behavior + +Functions can behave differently based on the active context: + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.context import Context +from signalwire_agents.core.function_result import SwaigFunctionResult + +class ContextAwareAgent(AgentBase): + def __init__(self): + super().__init__(name="context-aware", route="/context") + + # Setup contexts + self.setup_contexts() + self.set_active_context("customer") + + def setup_contexts(self): + customer = Context( + name="customer", + description="Customer-facing interactions", + instructions=["Be friendly and helpful", "Use simple language"] + ) + + internal = Context( + name="internal", + description="Internal staff interactions", + instructions=["Be direct and technical", "Include detailed information"] + ) + + self.add_context(customer) + self.add_context(internal) + + @AgentBase.tool( + name="get_account_info", + description="Get account information", + parameters={ + "account_id": { + "type": "string", + "description": "Account identifier" + } + } + ) + def get_account_info(self, args, raw_data): + account_id = args.get("account_id") + + # Get the current context + current_context = self.get_active_context() + + if current_context.name == "customer": + # Customer-friendly response + return SwaigFunctionResult( + f"Your account {account_id} is in good standing. " + "Your next billing date is March 15th." + ) + elif current_context.name == "internal": + # Detailed internal response + return SwaigFunctionResult( + f"Account {account_id}: Status=ACTIVE, Balance=$125.50, " + "Last_Payment=2024-02-15, Next_Bill=2024-03-15, " + "Plan=Premium, Usage=85% of limit" + ) + else: + return SwaigFunctionResult("Account information retrieved.") + +def main(): + agent = ContextAwareAgent() + agent.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/swml/agents-sdk/concepts/datamap.md b/docs/swml/agents-sdk/concepts/datamap.md new file mode 100644 index 00000000..5ad2710e --- /dev/null +++ b/docs/swml/agents-sdk/concepts/datamap.md @@ -0,0 +1,3871 @@ +--- +id: agents-sdk-concepts-datamap +slug: /agents-sdk/concepts/datamap +title: DataMap System +sidebar_label: DataMap System +--- + +# DataMap Complete Guide: SWML Perspective and Development + +A comprehensive guide to understanding, implementing, and testing DataMap configurations in SignalWire AI Agents from the SWML (SignalWire Markup Language) perspective. + +--- + +*This guide provides comprehensive coverage of DataMap functionality within the SignalWire AI Agents framework, from basic concepts to advanced implementation patterns.* + +## 1. Introduction to DataMap in SWML + +### 1.1 What is DataMap + +DataMap is a serverless function execution system within SignalWire AI Agents that enables seamless integration with external APIs without the need for custom webhook endpoints. Unlike traditional webhook-based SWAIG functions that require you to host and maintain HTTP endpoints, DataMap functions are executed entirely within the SignalWire infrastructure. + +**Key Characteristics:** +- **Serverless Architecture**: No need to host webhook endpoints +- **Built-in HTTP Client**: Native HTTP request capabilities +- **Template-Based Configuration**: Declarative API integration using template expansion +- **Sequential Processing**: Multiple webhook fallback support +- **Response Transformation**: Built-in data processing and formatting +- **Error Handling**: Automatic failure detection and fallback mechanisms + +**DataMap Execution Flow:** +``` +Function Call → Template Expansion → HTTP Request → Response Processing → Output Generation +``` + +### 1.2 DataMap vs Traditional Webhooks + +| Aspect | Traditional Webhooks | DataMap | +|--------|---------------------|---------| +| **Infrastructure** | Requires hosted endpoints | Serverless execution | +| **Configuration** | Code-based handlers | Declarative JSON/YAML | +| **HTTP Requests** | Manual implementation | Built-in HTTP client | +| **Error Handling** | Custom error logic | Automatic failure detection | +| **Template Expansion** | Manual string formatting | Native template system | +| **Scalability** | Limited by hosting infrastructure | Auto-scaling serverless | +| **Maintenance** | Server maintenance required | Zero maintenance overhead | +| **Development Speed** | Slower (code + deploy) | Faster (configuration only) | + +**Traditional Webhook Example:** +```python +def search_knowledge(args, post_data): + # Custom HTTP request logic + response = requests.post("https://api.example.com/search", + json={"query": args["query"]}) + # Custom error handling + if response.status_code != 200: + return {"error": "API request failed"} + # Custom response processing + data = response.json() + return {"response": f"Found: {data['results'][0]['text']}"} +``` + +**DataMap Equivalent:** +```json +{ + "function": "search_knowledge", + "data_map": { + "webhooks": [{ + "url": "https://api.example.com/search", + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "params": {"query": "${args.query}"}, + "output": {"response": "Found: ${array[0].text}"}, + "error_keys": ["error"] + }], + "output": {"response": "Search service unavailable"} + } +} +``` + +### 1.3 SWML Integration Overview + +DataMap integrates seamlessly with SWML (SignalWire Markup Language) through the AI verb's function calling mechanism. When an AI agent needs to call a function, SWML automatically detects whether it's a traditional webhook or DataMap function and routes the execution appropriately. + +**SWML AI Verb Integration:** +```xml + + You can search knowledge using the search_knowledge function + + + + +``` + +**Execution Context:** +- DataMap functions have access to all SWML context variables +- Function arguments are automatically validated against parameter schemas +- Results can generate both response text and SWML actions +- Global data and prompt variables are available for template expansion + +**Integration Benefits:** +- **Declarative Configuration**: Define API integrations using configuration, not code +- **Automatic Validation**: Parameter validation based on JSON schema +- **Context Awareness**: Access to conversation state and SWML variables +- **Action Generation**: Can produce SWML actions for call control +- **Error Recovery**: Built-in fallback mechanisms maintain conversation flow + +### 1.4 When to Use DataMap + +**Ideal Use Cases:** +- **External API Integration**: REST API calls to third-party services +- **Knowledge Base Queries**: Search operations against document stores +- **Data Transformation**: Simple data processing and formatting +- **Service Aggregation**: Combining data from multiple sources +- **Rapid Prototyping**: Quick API integration without infrastructure + +**DataMap is Perfect For:** +- Simple to moderate API integration complexity +- Read-heavy operations (GET, POST with JSON) +- Services with predictable response formats +- Scenarios requiring fallback mechanisms +- Development teams without DevOps infrastructure + +**Consider Traditional Webhooks When:** +- Complex business logic is required +- Advanced error handling and retry mechanisms needed +- Custom authentication schemes beyond headers +- Heavy computational processing required +- Integration with non-HTTP protocols +- Need for persistent state or caching +- Complex response transformation logic + +**Hybrid Approach:** +Many applications benefit from using both DataMap and traditional webhooks: +- DataMap for simple API calls and data retrieval +- Traditional webhooks for complex processing and business logic +- DataMap for rapid prototyping, webhooks for production optimization + +## 2. DataMap Architecture and Processing Pipeline + +### 2.1 Server-Side Processing Flow + +DataMap execution occurs entirely within the SignalWire infrastructure, following a deterministic processing pipeline implemented in the server-side `mod_openai.c` module. Understanding this flow is crucial for effective DataMap configuration. + +**Server-Side Architecture:** +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ SWML Engine │────│ DataMap │────│ HTTP Client │ +│ Function Call │ │ Processor │ │ Request │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ┌──────▼──────┐ + │ Template │ + │ Expansion │ + │ Engine │ + └─────────────┘ +``` + +**Processing Modules:** +- **Function Router**: Determines if function is DataMap or webhook +- **Context Builder**: Assembles variable context from function arguments and SWML state +- **Template Engine**: Expands variables in URLs, headers, and request bodies +- **HTTP Client**: Executes HTTP requests with timeout and error handling +- **Response Processor**: Parses and validates HTTP responses +- **Foreach Engine**: Processes array data with template expansion +- **Output Generator**: Formats final results for SWML consumption + +### 2.2 Processing Order: Expressions → Webhooks → Foreach → Output + +DataMap processing follows a strict sequential order that ensures deterministic execution and proper error handling: + +**1. Expression Processing (Optional)** +```json +{ + "expressions": [ + { + "pattern": "simple query", + "output": {"response": "This is a simple response for: ${args.query}"} + } + ] +} +``` +- Pattern matching against function arguments +- Early exit if pattern matches +- Bypasses HTTP requests for known cases + +**2. Webhook Sequential Processing** +```json +{ + "webhooks": [ + {"url": "https://primary-api.com/search", "...": "..."}, + {"url": "https://fallback-api.com/search", "...": "..."} + ] +} +``` +- Process webhooks in array order +- Stop at first successful webhook +- Each webhook has independent configuration + +**3. Foreach Processing (Per Successful Webhook)** +```json +{ + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 5, + "append": "Result: ${this.title}\n" + } +} +``` +- Processes array data from successful webhook response +- Builds concatenated strings using template expansion +- Stores result in context for output templates + +**4. Output Generation** +```json +{ + "output": { + "response": "Found results: ${formatted_results}", + "action": [{"SWML": {"version": "1.0.0", "...": "..."}}] + } +} +``` +- Webhook-level output (if webhook succeeds) +- DataMap-level fallback output (if all webhooks fail) + +**Processing Flow Diagram:** +``` +Function Call + │ + ▼ +┌─────────────┐ Yes ┌──────────────┐ +│ Expressions │────────────│ Return Early │ +│ Match? │ │ Output │ +└─────────────┘ └──────────────┘ + │ No + ▼ +┌─────────────┐ +│ Webhook 1 │────┐ +│ Success? │ │ +└─────────────┘ │ + │ No │ Yes + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ Webhook 2 │ │ Foreach │ +│ Success? │ │ Processing │ +└─────────────┘ └─────────────┘ + │ No │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ Fallback │ │ Webhook │ +│ Output │ │ Output │ +└─────────────┘ └─────────────┘ +``` + +### 2.3 Context and Variable Scope + +DataMap maintains a hierarchical context system that provides access to various data sources during template expansion: + +**Context Hierarchy:** +``` +┌────────────────────────────────────────┐ +│ args │ ← Function arguments +│ ┌──────────────────────────────────┐ │ +│ │ response │ │ ← HTTP response object +│ │ ┌────────────────────────────┐ │ │ +│ │ │ this │ │ │ ← Current foreach item +│ │ │ │ │ │ +│ │ └────────────────────────────┘ │ │ +│ └──────────────────────────────────┘ │ +└────────────────────────────────────────┘ +``` + +**Variable Sources:** + +1. **Function Arguments** (`args.*`) + - Direct access to function call parameters + - Available throughout entire execution + - Example: `${args.query}`, `${args.filters}` + +2. **HTTP Response Data** (`response.*` or `array.*`) + - Response object for object responses + - Array data for array responses + - Available after successful webhook execution + +3. **Global Data** (`global_data.*`) + - Agent-level configuration and state + - SWML prompt variables + - Conversation context + +4. **Foreach Context** (`this.*`) + - Current item during foreach processing + - Only available within foreach append templates + - Dynamic scope based on array iteration + +**Context Evolution:** +```javascript +// Initial context +{ + "args": {"query": "SignalWire", "count": 3} +} + +// After webhook success (object response) +{ + "args": {"query": "SignalWire", "count": 3}, + "response": {"results": [...], "total": 25} +} + +// After webhook success (array response) +{ + "args": {"query": "SignalWire", "count": 3}, + "array": [{"title": "...", "text": "..."}, ...] +} + +// During foreach processing +{ + "args": {"query": "SignalWire", "count": 3}, + "array": [...], + "this": {"title": "Current Item", "text": "Current content"} +} +``` + +### 2.4 Serverless Execution Model + +DataMap functions execute in a serverless environment with specific characteristics and limitations: + +**Execution Environment:** +- **Stateless**: No persistent memory between function calls +- **Isolated**: Each function execution is independent +- **Time-Limited**: HTTP requests have built-in timeouts +- **Resource-Constrained**: Optimized for typical API integration scenarios + +**Execution Lifecycle:** +``` +1. Function Call Received + ├── Parse DataMap configuration + ├── Validate function arguments + └── Build initial context + +2. Template Expansion + ├── Expand webhook URLs and headers + ├── Expand request body parameters + └── Prepare HTTP request configuration + +3. HTTP Request Execution + ├── Make HTTP request with timeouts + ├── Handle network errors + └── Parse response (JSON/text) + +4. Response Processing + ├── Validate response structure + ├── Check for error conditions + └── Add response to context + +5. Foreach Processing (if configured) + ├── Extract array data + ├── Iterate with template expansion + └── Build concatenated result + +6. Output Generation + ├── Expand output templates + ├── Format final response + └── Return to SWML engine +``` + +**Performance Characteristics:** +- **Cold Start**: First execution may have slight latency +- **Warm Execution**: Subsequent calls are optimized +- **Concurrency**: Multiple functions can execute simultaneously +- **Scalability**: Automatic scaling based on demand + +**Resource Limits:** +- HTTP request timeout: ~30 seconds +- Response size limits: Reasonable API response sizes +- Memory constraints: Optimized for typical API responses +- Concurrent execution: Platform-managed scaling + +## 3. DataMap Configuration Structure + +### 3.1 Basic DataMap Schema + +DataMap configurations follow a specific JSON schema that defines how external APIs are integrated and how responses are processed. Understanding this schema is essential for creating effective DataMap functions. + +**Complete DataMap Function Structure:** +```json +{ + "function": "function_name", + "description": "Human-readable function description", + "parameters": { + "type": "object", + "properties": { + "param_name": { + "type": "string|number|boolean|array|object", + "description": "Parameter description", + "required": true, + "enum": ["optional", "enumeration", "values"] + } + }, + "required": ["param1", "param2"] + }, + "data_map": { + "expressions": [...], + "webhooks": [...], + "output": {...} + } +} +``` + +**Core Schema Elements:** + +1. **Function Metadata** + - `function`: Unique function identifier + - `description`: Human-readable description for AI understanding + - `parameters`: JSON Schema for function arguments validation + +2. **DataMap Configuration** (`data_map`) + - `expressions`: Optional pattern-based early return logic + - `webhooks`: Array of HTTP request configurations + - `output`: Fallback output when all webhooks fail + +**Minimal DataMap Example:** +```json +{ + "function": "simple_api_call", + "description": "Call external API", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [{ + "url": "https://api.example.com/search", + "method": "GET", + "headers": {"Authorization": "Bearer ${global_data.api_token}"}, + "params": {"q": "${args.query}"}, + "output": {"response": "Result: ${response.data}"} + }] + } +} +``` + +### 3.2 Function-Level Configuration + +Function-level configuration defines the interface between the AI agent and the DataMap execution engine: + +**Parameter Schema Definition:** +```json +{ + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query text", + "minLength": 1, + "maxLength": 500 + }, + "category": { + "type": "string", + "description": "Content category filter", + "enum": ["docs", "api", "tutorials", "blog"], + "default": "docs" + }, + "limit": { + "type": "integer", + "description": "Maximum number of results", + "minimum": 1, + "maximum": 20, + "default": 5 + }, + "filters": { + "type": "array", + "description": "Additional search filters", + "items": {"type": "string"}, + "maxItems": 10 + } + }, + "required": ["query"], + "additionalProperties": false + } +} +``` + +**Validation Features:** +- **Type Validation**: Ensures correct data types +- **Range Validation**: Min/max values for numbers and arrays +- **Enumeration**: Restricts to specific allowed values +- **Required Fields**: Ensures essential parameters are provided +- **Default Values**: Automatic parameter population +- **Additional Properties**: Controls extra parameter handling + +**AI Integration Benefits:** +```json +{ + "description": "Search the knowledge base for documentation and tutorials. Use specific keywords and categories for better results.", + "parameters": { + "properties": { + "query": { + "description": "Specific search terms - be precise for better results" + }, + "category": { + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides" + } + } + } +} +``` + +### 3.3 Nested DataMap Objects + +DataMap configurations can include nested objects and complex data structures for advanced use cases: + +**Complex Webhook Configuration:** +```json +{ + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +**Nested Structure Benefits:** +- **Complex API Integration**: Support for sophisticated API requirements +- **Conditional Logic**: Dynamic parameter construction based on arguments +- **Rich Response Processing**: Multiple output types and formatting +- **SWML Action Generation**: Create call control actions from API responses + +### 3.4 Parameter Validation + +Parameter validation ensures data integrity and provides clear error messages when function calls fail validation: + +**Comprehensive Validation Example:** +```json +{ + "parameters": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "Email address to validate", + "pattern": "^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,}$", + "maxLength": 254 + }, + "user_preferences": { + "type": "object", + "description": "User preference settings", + "properties": { + "language": { + "type": "string", + "enum": ["en", "es", "fr", "de"], + "default": "en" + }, + "notifications": { + "type": "object", + "properties": { + "email": {"type": "boolean", "default": true}, + "sms": {"type": "boolean", "default": false}, + "push": {"type": "boolean", "default": true} + } + } + } + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true, + "maxProperties": 20 + } + }, + "required": ["email"], + "dependencies": { + "user_preferences": { + "properties": { + "email": {"const": true} + } + } + } + } +} +``` + +**Validation Error Handling:** +When validation fails, the AI agent receives clear error messages: + +```json +{ + "error": "Parameter validation failed", + "details": [ + { + "parameter": "email", + "message": "Invalid email format", + "received": "invalid-email" + }, + { + "parameter": "limit", + "message": "Value must be between 1 and 20", + "received": 50 + } + ] +} +``` + +**Best Practices for Parameter Design:** +1. **Clear Descriptions**: Help the AI understand parameter purpose +2. **Appropriate Constraints**: Balance flexibility with validation +3. **Sensible Defaults**: Reduce required parameters where possible +4. **Enum Values**: Provide clear options for categorical parameters +5. **Nested Structure**: Organize complex parameters logically + +## 4. Template Expansion System + +### 4.1 Template Syntax Overview + +Template expansion is a powerful feature in DataMap that allows you to dynamically construct URLs, headers, and request bodies based on function arguments and context variables. Understanding the syntax and usage is essential for effective DataMap configuration. + +**Template Syntax:** +``` +${expression} +``` + +**Expression Types:** +- **Variable**: `${args.query}`, `${global_data.api_token}` +- **Function**: `${function_name(args)}` +- **Conditional**: `${if(condition, true_value, false_value)}` +- **Array Access**: `${array[index].property}` +- **Object Access**: `${object.property}` +- **Built-in Functions**: `${length(array)}`, `${now()}` + +### 4.2 Variable Types and Sources + +DataMap supports a variety of variable types and sources that can be accessed during template expansion: + +**Variable Sources:** +- **Function Arguments** (`args.*`) +- **HTTP Response Data** (`response.*` or `array.*`) +- **Global Data** (`global_data.*`) +- **Foreach Context** (`this.*`) + +### 4.3 Array and Object Access Patterns + +DataMap provides flexible access patterns for array and object data: + +**Array Access:** +``` +${array[index].property} +``` + +**Object Access:** +``` +${object.property} +``` + +### 4.4 Context-Specific Variables + +DataMap provides context-specific variables that can be used in template expansion: + +**Context-Specific Variables:** +- **Function Arguments**: `${args.query}`, `${args.filters}` +- **HTTP Response Data**: `${response.data}`, `${array[0].text}` +- **Global Data**: `${global_data.api_token}`, `${global_data.prompt_variables}` +- **Foreach Context**: `${this.title}`, `${this.text}` + +### 4.5 Template Expansion Examples + +**Simple Template Expansion:** +``` +{ + "function": "simple_api_call", + "description": "Call external API", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [{ + "url": "https://api.example.com/search", + "method": "GET", + "headers": {"Authorization": "Bearer ${global_data.api_token}"}, + "params": {"q": "${args.query}"}, + "output": {"response": "Result: ${response.data}"} + }] + } +} +``` + +**Complex Template Expansion:** +``` +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 5. Webhook Configuration and HTTP Processing + +### 5.1 Webhook Structure + +DataMap functions can be configured with multiple webhooks to handle different scenarios and provide fallback mechanisms: + +**Webhook Configuration:** +```json +{ + "webhooks": [ + {"url": "https://primary-api.com/search", "...": "..."}, + {"url": "https://fallback-api.com/search", "...": "..."} + ] +} +``` + +### 5.2 HTTP Methods and Headers + +DataMap functions can use various HTTP methods and headers to customize request configurations: + +**HTTP Method Examples:** +- **GET**: Retrieving data from a server +- **POST**: Sending data to a server for processing +- **PUT**: Updating existing data on a server +- **DELETE**: Removing data from a server + +**HTTP Header Examples:** +- **Authorization**: Used for authentication and access control +- **Content-Type**: Specifies the format of the request body +- **Accept**: Specifies the format of the response body +- **X-Request-ID**: Used for request tracking and correlation + +### 5.3 Request Body Construction + +DataMap functions can construct request bodies dynamically based on function arguments and context variables: + +**Request Body Examples:** +- **Simple Query**: `${args.query}` +- **Complex Query**: `${function_name(args)}` +- **JSON Object**: `${json_object}` +- **Formatted String**: `${formatted_string}` + +### 5.4 Sequential Webhook Processing + +DataMap functions can be configured to process multiple webhooks in sequence: + +**Sequential Webhook Configuration:** +```json +{ + "webhooks": [ + {"url": "https://primary-api.com/search", "...": "..."}, + {"url": "https://fallback-api.com/search", "...": "..."} + ] +} +``` + +### 5.5 Webhook Failure Detection + +DataMap functions can be configured to handle webhook failures and provide fallback mechanisms: + +**Webhook Failure Configuration:** +```json +{ + "webhooks": [ + {"url": "https://primary-api.com/search", "...": "..."}, + {"url": "https://fallback-api.com/search", "...": "..."} + ] +} +``` + +## 6. Response Processing and Data Handling + +### 6.1 Response Data Structure + +DataMap functions can return various types of response data: + +**Response Data Types:** +- **Text**: Simple text response +- **JSON**: Structured data in JSON format +- **Array**: List of data items +- **Object**: Key-value pairs + +### 6.2 Array vs Object Response Handling + +DataMap functions can handle both array and object responses: + +**Array Response Example:** +```json +{ + "response": { + "results": [{"title": "...", "text": "..."}, ...] + } +} +``` + +**Object Response Example:** +```json +{ + "response": { + "results": {"total": 25, "data": [...]} + } +} +``` + +### 6.3 Error Response Processing + +DataMap functions can handle errors and provide clear error messages: + +**Error Handling Example:** +```json +{ + "error": "API request failed", + "details": { + "status_code": 400, + "message": "Invalid request parameters" + } +} +``` + +### 6.4 Custom Error Keys + +DataMap functions can define custom error keys to provide more detailed error information: + +**Custom Error Keys Example:** +```json +{ + "error": "API request failed", + "details": { + "status_code": 400, + "message": "Invalid request parameters" + } +} +``` + +## 7. Foreach Processing and Array Iteration + +### 7.1 Foreach Configuration + +DataMap functions can be configured to process array data: + +**Foreach Configuration Example:** +```json +{ + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 5, + "append": "Result: ${this.title}\n" + } +} +``` + +### 7.2 Array Data Sources + +DataMap functions can use various array data sources: + +**Array Data Sources:** +- **Function Results**: `${response.results}` +- **Global Data**: `${global_data.array}` +- **Foreach Context**: `${this.array}` + +### 7.3 Template Expansion in Foreach + +DataMap functions can expand template variables within foreach append templates: + +**Foreach Template Expansion Example:** +```json +{ + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 5, + "append": "Result: ${this.title}\n" + } +} +``` + +### 7.4 String Concatenation and Formatting + +DataMap functions can concatenate and format string data: + +**String Concatenation Example:** +```json +{ + "response": { + "formatted_results": "Result: ${this.title}\n" + } +} +``` + +### 7.5 Foreach Limitations and Best Practices + +DataMap functions should be used cautiously when processing large arrays: + +**Foreach Limitations:** +- **Performance**: Processing large arrays can be slow +- **Memory**: Large arrays can consume significant memory + +**Best Practices:** +1. **Limit Array Size**: Use pagination or limit parameters +2. **Optimize Template Expansion**: Minimize array access in templates +3. **Use Foreach with Caution**: Only use when necessary + +## 8. Output Generation and Result Formatting + +### 8.1 Webhook-Level Output + +DataMap functions can return output directly from webhook responses: + +**Webhook Output Example:** +```json +{ + "response": { + "formatted_results": "Result: ${this.title}\n" + } +} +``` + +### 8.2 DataMap-Level Fallback Output + +DataMap functions can provide a fallback output when all webhooks fail: + +**Fallback Output Example:** +```json +{ + "output": { + "response": "Search service unavailable" + } +} +``` + +### 8.3 Response vs Action Outputs + +DataMap functions can return both response text and SWML actions: + +**Response vs Action Output Example:** +```json +{ + "response": { + "formatted_results": "Result: ${this.title}\n" + }, + "action": [{"SWML": {"version": "1.0.0", "...": "..."}}] +} +``` + +### 8.4 SWML Action Generation + +DataMap functions can generate SWML actions for call control: + +**SWML Action Generation Example:** +```json +{ + "action": [{"SWML": {"version": "1.0.0", "...": "..."}}] +} +``` + +## 9. Skills System Integration + +### 9.1 DataMap Skills vs Raw Configuration + +DataMap functions can be integrated with the skills system: + +**Skills System Integration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 9.2 Skill-Based DataMap Creation + +DataMap functions can be created based on skills: + +**Skill-Based DataMap Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 9.3 Skill Configuration Patterns + +DataMap functions can be configured based on skill patterns: + +**Skill Configuration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 9.4 Multi-Instance Skill Usage + +DataMap functions can be used across multiple instances: + +**Multi-Instance Skill Usage Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 10. Practical Examples and Use Cases + +### 10.1 API Integration Examples + +DataMap functions can be used for various API integration scenarios: + +**API Integration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 10.2 Knowledge Base Search + +DataMap functions can be used for knowledge base searches: + +**Knowledge Base Search Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 10.3 External Service Integration + +DataMap functions can be used for external service integrations: + +**External Service Integration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 10.4 Multi-Step Processing Workflows + +DataMap functions can be used for multi-step processing workflows: + +**Multi-Step Processing Workflow Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 11. Development and Testing + +### 11.1 Local Development Setup + +DataMap functions can be developed and tested locally: + +**Local Development Setup Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 11.2 Environment Variable Configuration + +DataMap functions can be configured with environment variables: + +**Environment Variable Configuration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 11.3 CLI Testing Tools + +DataMap functions can be tested using command-line tools: + +**CLI Testing Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 11.4 Debugging DataMap Execution + +DataMap functions can be debugged using various tools: + +**Debugging Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 12. Advanced Patterns and Techniques + +### 12.1 Multiple Webhook Fallback Chains + +DataMap functions can be configured with multiple webhooks to handle different scenarios and provide fallback mechanisms: + +**Multiple Webhook Configuration Example:** +```json +{ + "webhooks": [ + {"url": "https://primary-api.com/search", "...": "..."}, + {"url": "https://fallback-api.com/search", "...": "..."} + ] +} +``` + +### 12.2 Complex Template Expressions + +DataMap functions can use complex template expressions to handle advanced scenarios: + +**Complex Template Expression Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 12.3 Dynamic API Endpoint Selection + +DataMap functions can dynamically select API endpoints based on function arguments: + +**Dynamic API Endpoint Selection Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 12.4 Response Transformation Patterns + +DataMap functions can transform response data based on function arguments: + +**Response Transformation Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 13. Error Handling and Reliability + +### 13.1 HTTP Error Codes and Handling + +DataMap functions can handle various HTTP error codes: + +**HTTP Error Handling Example:** +```json +{ + "error": "API request failed", + "details": { + "status_code": 400, + "message": "Invalid request parameters" + } +} +``` + +### 13.2 Network Timeout and Retry Logic + +DataMap functions can handle network timeouts and implement retry logic: + +**Network Timeout and Retry Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 13.3 Graceful Degradation Strategies + +DataMap functions can implement graceful degradation strategies: + +**Graceful Degradation Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 13.4 Monitoring and Observability + +DataMap functions can be monitored and observed using various tools: + +**Monitoring and Observability Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 14. Security and Best Practices + +### 14.1 API Key Management + +DataMap functions can be configured with API keys: + +**API Key Management Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 14.2 Secure Header Configuration + +DataMap functions can be configured with secure headers: + +**Secure Header Configuration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 14.3 Input Validation and Sanitization + +DataMap functions should validate and sanitize input data: + +**Input Validation Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 14.4 Rate Limiting Considerations + +DataMap functions should consider rate limiting: + +**Rate Limiting Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 15. Performance Optimization + +### 15.1 Request Optimization + +DataMap functions can optimize request configurations: + +**Request Optimization Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 15.2 Response Size Management + +DataMap functions can manage response sizes: + +**Response Size Management Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 15.3 Caching Strategies + +DataMap functions can implement caching strategies: + +**Caching Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 15.4 Execution Time Considerations + +DataMap functions should consider execution time: + +**Execution Time Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +## 16. Migration and Upgrade Paths + +### 16.1 From Webhook to DataMap Migration + +DataMap functions can be migrated from traditional webhooks: + +**Migration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 16.2 Legacy Configuration Support + +DataMap functions can support legacy configurations: + +**Legacy Configuration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 16.3 Version Compatibility + +DataMap functions should be compatible with different versions: + +**Version Compatibility Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +### 16.4 Gradual Migration Strategies + +DataMap functions can implement gradual migration strategies: + +**Gradual Migration Example:** +```json +{ + "function": "search_knowledge", + "description": "Search the knowledge base for documentation and tutorials", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Specific search terms - be precise for better results" + }, + "category": { + "type": "string", + "description": "Content type: 'docs' for documentation, 'api' for API references, 'tutorials' for guides", + "enum": ["docs", "api", "tutorials"], + "default": "docs" + } + }, + "required": ["query"] + }, + "data_map": { + "webhooks": [ + { + "url": "https://api.primary.com/v2/search", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer ${global_data.primary_token}", + "X-Client-Version": "2.1", + "X-Request-ID": "${args.request_id}" + }, + "params": { + "query": { + "text": "${args.query}", + "filters": { + "category": "${args.category}", + "date_range": { + "start": "${args.start_date}", + "end": "${args.end_date}" + }, + "tags": "${args.tags}" + }, + "options": { + "highlight": true, + "max_results": "${args.limit}", + "include_metadata": true + } + } + }, + "foreach": { + "input_key": "results", + "output_key": "formatted_results", + "max": 10, + "append": "## ${this.title}\n${this.excerpt}\n**Score:** ${this.relevance_score}\n\n" + }, + "output": { + "response": "Found ${response.total} results:\n\n${formatted_results}", + "action": [ + { + "SWML": { + "version": "1.0.0", + "sections": { + "main": [ + { + "set": { + "last_search_query": "${args.query}", + "last_search_results": "${response.total}", + "search_timestamp": "${response.timestamp}" + } + } + ] + } + } + } + ] + }, + "error_keys": ["error", "message", "detail"] + } + ], + "output": { + "response": "I'm sorry, the search service is currently unavailable. Please try again later." + } + } +} +``` + +--- + +*This guide provides comprehensive coverage of DataMap functionality within the SignalWire AI Agents framework, from basic concepts to advanced implementation patterns.* \ No newline at end of file diff --git a/docs/swml/agents-sdk/concepts/index.md b/docs/swml/agents-sdk/concepts/index.md new file mode 100644 index 00000000..2d3cb605 --- /dev/null +++ b/docs/swml/agents-sdk/concepts/index.md @@ -0,0 +1,24 @@ +--- +id: agents-sdk-concepts +slug: /agents-sdk/concepts +title: AI Agents SDK Concepts +sidebar_label: Core Concepts +--- + +# AI Agents SDK Core Concepts + +Understand the fundamental concepts and architecture of the SignalWire AI Agents SDK. + +## Core Components + +- **Agents**: Self-contained AI personas with web service capabilities +- **SWAIG Functions**: AI-powered tools and functions +- **DataMap System**: Declarative API integration +- **Skills System**: Modular capabilities +- **State Management**: Conversation tracking and persistence + +## Architecture Overview + +The SignalWire AI Agents SDK is built on a modular architecture that combines AI capabilities with web service functionality. Each agent is a standalone microservice that can handle requests, maintain state, and interact with external services. + + \ No newline at end of file diff --git a/docs/swml/agents-sdk/get-started/_category_.json b/docs/swml/agents-sdk/get-started/_category_.json new file mode 100644 index 00000000..3e48f3fb --- /dev/null +++ b/docs/swml/agents-sdk/get-started/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Get started", + "collapsible": false, + "className": "menu-category", + "position": 1 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/get-started/installation.mdx b/docs/swml/agents-sdk/get-started/installation.mdx new file mode 100644 index 00000000..7e0bbb52 --- /dev/null +++ b/docs/swml/agents-sdk/get-started/installation.mdx @@ -0,0 +1,74 @@ +--- +slug: /agents-sdk/installation +--- + +# Installation + +Agents SDK + +## Basic Installation + +```bash +pip install signalwire-agents +``` + +## Optional Search Functionality + +The SDK includes optional local search capabilities that can be installed separately to avoid adding large dependencies to the base installation: + +### Search Installation Options + +```bash +# Basic search (vector search + keyword search) +pip install signalwire-agents[search] + +# Full search with document processing (PDF, DOCX, etc.) +pip install signalwire-agents[search-full] + +# Advanced NLP features (includes spaCy) +pip install signalwire-agents[search-nlp] + +# All search features +pip install signalwire-agents[search-all] +``` + +### What Each Option Includes + +| Option | Size | Features | +|--------|------|----------| +| `search` | ~500MB | Vector embeddings, keyword search, basic text processing | +| `search-full` | ~600MB | + PDF, DOCX, Excel, PowerPoint, HTML, Markdown processing | +| `search-nlp` | ~600MB | + Advanced spaCy NLP features | +| `search-all` | ~700MB | All search features combined | + +### Search Features + +- **Local/Offline Search**: No external API dependencies +- **Hybrid Search**: Vector similarity + keyword search +- **Smart Document Processing**: Markdown, Python, PDF, DOCX, etc. +- **Multiple Languages**: English, Spanish, with extensible framework +- **CLI Tools**: Build search indexes from document directories +- **HTTP API**: Standalone or embedded search service + +### Usage Example + +```python +# Only available with search extras installed +from signalwire_agents.search import IndexBuilder, SearchEngine + +# Build search index +builder = IndexBuilder() +builder.build_index( + source_dir="./docs", + output_file="knowledge.swsearch", + file_types=['md', 'txt', 'pdf'] +) + +# Search documents +engine = SearchEngine("knowledge.swsearch") +results = engine.search( + query_vector=embeddings, + enhanced_text="search query", + count=5 +) +``` \ No newline at end of file diff --git a/docs/swml/agents-sdk/get-started/overview.mdx b/docs/swml/agents-sdk/get-started/overview.mdx new file mode 100644 index 00000000..9bf1a3e6 --- /dev/null +++ b/docs/swml/agents-sdk/get-started/overview.mdx @@ -0,0 +1,88 @@ +--- +id: agents-sdk-getting-started +slug: /agents-sdk +hide_title: true +hide_table_of_contents: true +title: SignalWire Agents SDK +sidebar_label: Overview +sidebar_position: 0 +--- + +import InstallHero from '@site/src/components/InstallHero'; +import { Card, CardGroup } from '@site/src/components/Extras/Card'; +import { MdCode, MdSchemaValidation, MdGroup, MdExtension, MdArchitecture, MdStorage, MdBugReport } from 'react-icons/md'; + + + + +The SignalWire Agents SDK provides a comprehensive framework for building intelligent conversational agents that can handle voice and text interactions seamlessly. + +## Features + + + +} + href="/agents-sdk/guides/agent-guide#prompt-building" +> +Structured prompt composition for consistent AI interactions + + +} + href="/agents-sdk/guides/cli-testing" +> +Built-in validation to ensure data integrity and type safety + + +} + href="/agents-sdk/guides/agent-guide#prefab-agents" +> +Host multiple specialized agents on a single server + + +} + href="/agents-sdk/skills" +> +Modular capabilities that can be added with simple one-liner calls + + +} + href="/agents-sdk/guides/agent-guide#prefab-agents" +> +Ready-to-use agent archetypes for common scenarios + + +} + href="/agents-sdk/api#state-management" +> +Persistent conversation state with automatic tracking + + +} + href="/agents-sdk/guides/cli-testing" +> +Comprehensive testing framework for agent validation + + + diff --git a/docs/swml/agents-sdk/get-started/quickstart.mdx b/docs/swml/agents-sdk/get-started/quickstart.mdx new file mode 100644 index 00000000..2d04695b --- /dev/null +++ b/docs/swml/agents-sdk/get-started/quickstart.mdx @@ -0,0 +1,31 @@ +--- +slug: /agents-sdk/quickstart +--- + +# Quickstart + +Agents SDK

+ +Get up and running quickly with the SignalWire AI Agents SDK. This section covers installation, basic setup, and your first AI agent implementation. + +## Quick Start + +1. Install the SDK: +```bash +pip install signalwire-agents +``` + +2. Create your first agent: +```python +from signalwire_agents import AgentBase + +# Create an agent +agent = AgentBase("My Assistant", route="/assistant") + +# Add some basic capabilities +agent.add_skill("datetime") # Current date/time info +agent.add_skill("math") # Mathematical calculations + +# Start the agent +agent.serve() +``` \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/_category_.json b/docs/swml/agents-sdk/guides/_category_.json new file mode 100644 index 00000000..c6667f44 --- /dev/null +++ b/docs/swml/agents-sdk/guides/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Guides", + "collapsible": false, + "className": "menu-category", + "position": 3 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/agent-guide.md b/docs/swml/agents-sdk/guides/agent-guide.md new file mode 100644 index 00000000..ad378ff0 --- /dev/null +++ b/docs/swml/agents-sdk/guides/agent-guide.md @@ -0,0 +1,2785 @@ +--- +id: agents-sdk-guides-agent +slug: /agents-sdk/guides/agent-guide +title: Agent Guide +sidebar_label: Agent Guide +--- + +# SignalWire AI Agent Guide + +## Introduction + +The `AgentBase` class provides the foundation for creating AI-powered agents using the SignalWire AI Agent SDK. It extends the `SWMLService` class, inheriting all its SWML document creation and serving capabilities, while adding AI-specific functionality. + +Key features of `AgentBase` include: + +- Structured prompt building with POM (Prompt Object Model) +- SWAIG function definitions for AI tool access +- Multilingual support +- Agent configuration (hint handling, pronunciation rules, etc.) +- State management for conversations + +This guide explains how to create and customize your own AI agents, with examples based on the SDK's sample implementations. + +## Architecture Overview + +The Agent SDK architecture consists of several layers: + +1. **SWMLService**: The base layer for SWML document creation and serving +2. **AgentBase**: Extends SWMLService with AI agent functionality +3. **Custom Agents**: Your specific agent implementations that extend AgentBase + +Here's how these components relate to each other: + +``` +┌─────────────┐ +│ Your Agent │ (Extends AgentBase with your specific functionality) +└─────▲───────┘ + │ +┌─────┴───────┐ +│ AgentBase │ (Adds AI functionality to SWMLService) +└─────▲───────┘ + │ +┌─────┴───────┐ +│ SWMLService │ (Provides SWML document creation and web service) +└─────────────┘ +``` + +## Creating an Agent + +To create an agent, extend the `AgentBase` class and define your agent's behavior: + +```python +from signalwire_agents import AgentBase + +class MyAgent(AgentBase): + def __init__(self): + super().__init__( + name="my-agent", + route="/agent", + host="0.0.0.0", + port=3000, + use_pom=True # Enable Prompt Object Model + ) + + # Define agent personality and behavior + self.prompt_add_section("Personality", body="You are a helpful and friendly assistant.") + self.prompt_add_section("Goal", body="Help users with their questions and tasks.") + self.prompt_add_section("Instructions", bullets=[ + "Answer questions clearly and concisely", + "If you don't know, say so", + "Use the provided tools when appropriate" + ]) + + # Add a post-prompt for summary + self.set_post_prompt("Please summarize the key points of this conversation.") +``` + +## Running Your Agent + +The SignalWire AI Agent SDK provides a `run()` method that automatically detects the execution environment and configures the agent appropriately. This method works across all deployment modes: + +### Deployment with `run()` + +```python +def main(): + agent = MyAgent() + + print("Starting agent server...") + print("Note: Works in any deployment mode (server/CGI/Lambda)") + agent.run() # Auto-detects environment + +if __name__ == "__main__": + main() +``` + +The `run()` method automatically detects and configures for: + +- **HTTP Server**: When run directly, starts an HTTP server +- **CGI**: When CGI environment variables are detected, operates in CGI mode +- **AWS Lambda**: When Lambda environment is detected, configures for serverless execution + +### Deployment Modes + +#### HTTP Server Mode +When run directly (e.g., `python my_agent.py`), the agent starts an HTTP server: + +```python +# Automatically starts HTTP server when run directly +agent.run() +``` + +#### CGI Mode +When CGI environment variables are present, operates in CGI mode with clean HTTP output: + +```python +# Same code - automatically detects CGI environment +agent.run() +``` + +#### AWS Lambda Mode +When AWS Lambda environment is detected, configures for serverless execution: + +```python +# Same code - automatically detects Lambda environment +agent.run() +``` + +### Environment Detection + +The SDK automatically detects the execution environment: + +| Environment | Detection Method | Behavior | +|-------------|------------------|----------| +| **HTTP Server** | Default when no serverless environment detected | Starts FastAPI server on specified host/port | +| **CGI** | `GATEWAY_INTERFACE` environment variable present | Processes single CGI request and exits | +| **AWS Lambda** | `AWS_LAMBDA_FUNCTION_NAME` environment variable | Handles Lambda event/context | +| **Google Cloud** | `FUNCTION_NAME` or `K_SERVICE` variables | Processes Cloud Function request | +| **Azure Functions** | `AZURE_FUNCTIONS_*` variables | Handles Azure Function request | + +### Logging Configuration + +The SDK includes a central logging system that automatically configures based on the deployment environment: + +```python +# Logging is automatically configured based on environment +# No manual setup required in most cases + +# Optional: Override logging mode via environment variable +# SIGNALWIRE_LOG_MODE=off # Disable all logging +# SIGNALWIRE_LOG_MODE=stderr # Log to stderr +# SIGNALWIRE_LOG_MODE=default # Use default logging +# SIGNALWIRE_LOG_MODE=auto # Auto-detect (default) +``` + +The logging system automatically: +- **CGI Mode**: Sets logging to 'off' to avoid interfering with HTTP headers +- **Lambda Mode**: Configures appropriate logging for serverless environment +- **Server Mode**: Uses structured logging with timestamps and levels +- **Debug Mode**: Enhanced logging when debug flags are set + +## Prompt Building + +There are several ways to build prompts for your agent: + +### 1. Using Prompt Sections (POM) + +The Prompt Object Model (POM) provides a structured way to build prompts: + +```python +# Add a section with just body text +self.prompt_add_section("Personality", body="You are a friendly assistant.") + +# Add a section with bullet points +self.prompt_add_section("Instructions", bullets=[ + "Answer questions clearly", + "Be helpful and polite", + "Use functions when appropriate" +]) + +# Add a section with both body and bullets +self.prompt_add_section("Context", + body="The user is calling about technical support.", + bullets=["They may need help with their account", + "Check for existing tickets"]) +``` + +For convenience, the SDK also provides wrapper methods that some users may prefer: + +```python +# Convenience methods +self.setPersonality("You are a friendly assistant.") +self.setGoal("Help users with their questions.") +self.setInstructions([ + "Answer questions clearly", + "Be helpful and polite" +]) +``` + +These convenience methods call `prompt_add_section()` internally with the appropriate section titles. + +### 2. Using Raw Text Prompts + +For simpler agents, you can set the prompt directly as text: + +```python +self.set_prompt_text(""" +You are a helpful assistant. Your goal is to provide clear and concise information +to the user. Answer their questions to the best of your ability. +""") +``` + +### 3. Setting a Post-Prompt + +The post-prompt is sent to the AI after the conversation for summary or analysis: + +```python +self.set_post_prompt(""" +Analyze the conversation and extract: +1. Main topics discussed +2. Action items or follow-ups needed +3. Whether the user's questions were answered satisfactorily +""") +``` + +## SWAIG Functions + +SWAIG functions allow the AI agent to perform actions and access external systems. There are two types of SWAIG functions you can define: + +### 1. Local Webhook Functions (Standard) + +These are the traditional SWAIG functions that are handled locally by your agent: + +```python +from signalwire_agents.core.function_result import SwaigFunctionResult + +@AgentBase.tool( + name="get_weather", + description="Get the current weather for a location", + parameters={ + "location": { + "type": "string", + "description": "The city or location to get weather for" + } + }, + secure=True # Optional, defaults to True +) +def get_weather(self, args, raw_data): + # Extract the location parameter + location = args.get("location", "Unknown location") + + # Here you would typically call a weather API + # For this example, we'll return mock data + weather_data = f"It's sunny and 72°F in {location}." + + # Return a SwaigFunctionResult + return SwaigFunctionResult(weather_data) +``` + +### 2. External Webhook Functions + +External webhook functions allow you to delegate function execution to external services instead of handling them locally. This is useful when you want to: +- Use existing web services or APIs directly +- Distribute function processing across multiple servers +- Integrate with third-party systems that provide their own endpoints + +To create an external webhook function, add a `webhook_url` parameter to the decorator: + +```python +@AgentBase.tool( + name="get_weather_external", + description="Get weather from external service", + parameters={ + "location": { + "type": "string", + "description": "The city or location to get weather for" + } + }, + webhook_url="https://your-service.com/weather-endpoint" +) +def get_weather_external(self, args, raw_data): + # This function will never be called locally when webhook_url is provided + # The external service at webhook_url will receive the function call instead + return SwaigFunctionResult("This should not be reached for external webhooks") +``` + +#### How External Webhooks Work + +When you specify a `webhook_url`: + +1. **Function Registration**: The function is registered with your agent as usual +2. **SWML Generation**: The generated SWML includes the external webhook URL instead of your local endpoint +3. **SignalWire Processing**: When the AI calls the function, SignalWire makes an HTTP POST request directly to your external URL +4. **Payload Format**: The external service receives a JSON payload with the function call data: + +```json +{ + "function": "get_weather_external", + "argument": { + "parsed": [{"location": "New York"}], + "raw": "{\"location\": \"New York\"}" + }, + "call_id": "abc123-def456-ghi789", + "call": { /* call information */ }, + "vars": { /* call variables */ } +} +``` + +5. **Response Handling**: Your external service should return a JSON response that SignalWire will process. + +#### Mixing Local and External Functions + +You can mix both types of functions in the same agent: + +```python +class HybridAgent(AgentBase): + def __init__(self): + super().__init__(name="hybrid-agent", route="/hybrid") + + # Local function - handled by this agent + @AgentBase.tool( + name="get_help", + description="Get help information", + parameters={} + ) + def get_help(self, args, raw_data): + return SwaigFunctionResult("I can help you with weather and news!") + + # External function - handled by external service + @AgentBase.tool( + name="get_weather", + description="Get current weather", + parameters={ + "location": {"type": "string", "description": "City name"} + }, + webhook_url="https://weather-service.com/api/weather" + ) + def get_weather_external(self, args, raw_data): + # This won't be called for external webhooks + pass + + # Another external function - different service + @AgentBase.tool( + name="get_news", + description="Get latest news", + parameters={ + "topic": {"type": "string", "description": "News topic"} + }, + webhook_url="https://news-service.com/api/news" + ) + def get_news_external(self, args, raw_data): + # This won't be called for external webhooks + pass +``` + +#### Testing External Webhooks + +You can test external webhook functions using the CLI tool: + +```bash +# Test local function +swaig-test examples/my_agent.py --exec get_help + +# Test external webhook function +swaig-test examples/my_agent.py --verbose --exec get_weather --location "New York" + +# List all functions with their types +swaig-test examples/my_agent.py --list-tools +``` + +The CLI tool will automatically detect external webhook functions and make HTTP requests to the external services, simulating what SignalWire does in production. + +### Function Parameters + +The parameters for a SWAIG function are defined using JSON Schema: + +```python +parameters={ + "parameter_name": { + "type": "string", # Can be string, number, integer, boolean, array, object + "description": "Description of the parameter", + # Optional attributes: + "enum": ["option1", "option2"], # For enumerated values + "minimum": 0, # For numeric types + "maximum": 100, # For numeric types + "pattern": "^[A-Z]+$" # For string validation + } +} +``` + +### Function Results + +To return results from a SWAIG function, use the `SwaigFunctionResult` class: + +```python +# Basic result with just text +return SwaigFunctionResult("Here's the result") + +# Result with a single action +return SwaigFunctionResult("Here's the result with an action") + .add_action("say", "I found the information you requested.") + +# Result with multiple actions using add_actions +return SwaigFunctionResult("Multiple actions example") + .add_actions([ + {"playback_bg": {"file": "https://example.com/music.mp3"}}, + {"set_global_data": {"key": "value"}} + ]) + +# Alternative way to add multiple actions sequentially +return ( + SwaigFunctionResult("Sequential actions example") + .add_action("say", "I found the information you requested.") + .add_action("playback_bg", {"file": "https://example.com/music.mp3"}) +) +``` + +In the examples above: +- `add_action(name, data)` adds a single action with the given name and data +- `add_actions(actions)` adds multiple actions at once from a list of action objects + +### Native Functions + +The agent can use SignalWire's built-in functions: + +```python +# Enable native functions +self.set_native_functions([ + "check_time", + "wait_seconds" +]) +``` + +### Function Includes + +You can include functions from remote sources: + +```python +# Include remote functions +self.add_function_include( + url="https://api.example.com/functions", + functions=["get_weather", "get_news"], + meta_data={"session_id": "unique-session-123"} # Use for session tracking, NOT credentials +) +``` + +### SWAIG Function Security + +The SDK implements an automated security mechanism for SWAIG functions to ensure that only authorized calls can be made to your functions. This is important because SWAIG functions often provide access to sensitive operations or data. + +#### Token-Based Security + +By default, all SWAIG functions are marked as `secure=True`, which enables token-based security: + +```python +@agent.tool( + name="get_account_details", + description="Get customer account details", + parameters={"account_id": {"type": "string"}}, + secure=True # This is the default, can be omitted +) +def get_account_details(self, args, raw_data): + # Implementation +``` + +When a function is marked as secure: + +1. The SDK automatically generates a secure token for each function when rendering the SWML document +2. The token is added to the function's URL as a query parameter: `?token=X2FiY2RlZmcuZ2V0X3RpbWUuMTcxOTMxNDI1...` +3. When the function is called, the token is validated before executing the function + +These security tokens have important properties: +- **Completely stateless**: The system doesn't need to store tokens or track sessions +- **Self-contained**: Each token contains all information needed for validation +- **Function-specific**: A token for one function can't be used for another +- **Session-bound**: Tokens are tied to a specific call/session ID +- **Time-limited**: Tokens expire after a configurable duration (default: 60 minutes) +- **Cryptographically signed**: Tokens can't be tampered with or forged + +This stateless design provides several benefits: +- **Server resilience**: Tokens remain valid even if the server restarts +- **No memory consumption**: No need to track sessions or store tokens in memory +- **High scalability**: Multiple servers can validate tokens without shared state +- **Load balancing**: Requests can be distributed across multiple servers freely + +The token system secures both SWAIG functions and post-prompt endpoints: +- SWAIG function calls for interactive AI capabilities +- Post-prompt requests for receiving conversation summaries + +You can disable token security for specific functions when appropriate: + +```python +@agent.tool( + name="get_public_information", + description="Get public information that doesn't require security", + parameters={}, + secure=False # Disable token security for this function +) +def get_public_information(self, args, raw_data): + # Implementation +``` + +#### Token Expiration + +The default token expiration is 60 minutes (3600 seconds), but you can configure this when initializing your agent: + +```python +agent = MyAgent( + name="my_agent", + token_expiry_secs=1800 # Set token expiration to 30 minutes +) +``` + +The expiration timer resets each time a function is successfully called, so as long as there is activity at least once within the expiration period, the tokens will remain valid throughout the entire conversation. + +#### Custom Token Validation + +You can override the default token validation by implementing your own `validate_tool_token` method in your custom agent class. + +## Skills System + +The Skills System allows you to extend your agents with powerful capabilities using simple one-liner calls. Skills are modular, reusable components that can be easily added to any agent and configured with parameters. + +### Quick Start + +```python +from signalwire_agents import AgentBase + +class SkillfulAgent(AgentBase): + def __init__(self): + super().__init__(name="skillful-agent", route="/skillful") + + # Add skills with one-liners + self.add_skill("web_search") # Web search capability + self.add_skill("datetime") # Current date/time info + self.add_skill("math") # Mathematical calculations + + # Configure skills with parameters + self.add_skill("web_search", { + "num_results": 3, # Get 3 search results instead of default 1 + "delay": 0.5 # Add delay between requests + }) +``` + +### Available Built-in Skills + +#### Web Search Skill (`web_search`) +Provides web search capabilities using Google Custom Search API with web scraping. + +**Requirements:** +- Packages: `beautifulsoup4`, `requests` + +**Parameters:** +- `api_key` (required): Google Custom Search API key +- `search_engine_id` (required): Google Custom Search Engine ID +- `num_results` (default: 1): Number of search results to return +- `delay` (default: 0): Delay in seconds between requests +- `tool_name` (default: "web_search"): Custom name for the search tool +- `no_results_message` (default: "I couldn't find any results for '\{query}'. This might be due to a very specific query or temporary issues. Try rephrasing your search or asking about a different topic."): Custom message to return when no search results are found. Use `\{query}` as a placeholder for the search query. + +**Multiple Instance Support:** +The web_search skill supports multiple instances with different search engines and tool names, allowing you to search different data sources: + +**Example:** +```python +# Basic single instance +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id" +}) +# Creates tool: web_search + +# Fast single result (previous default) +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id", + "num_results": 1, + "delay": 0 +}) + +# Multiple results with delay +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id", + "num_results": 5, + "delay": 1.0 +}) + +# Multiple instances with different search engines +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "general-search-engine-id", + "tool_name": "search_general", + "num_results": 1 +}) +# Creates tool: search_general + +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "news-search-engine-id", + "tool_name": "search_news", + "num_results": 3, + "delay": 0.5 +}) +# Creates tool: search_news + +# Custom no results message +agent.add_skill("web_search", { + "api_key": "your-google-api-key", + "search_engine_id": "your-search-engine-id", + "no_results_message": "Sorry, I couldn't find information about '\{query}'. Please try a different search term." +}) +``` + +#### DateTime Skill (`datetime`) +Provides current date and time information with timezone support. + +**Requirements:** +- Packages: `pytz` + +**Tools Added:** +- `get_current_time`: Get current time with optional timezone +- `get_current_date`: Get current date with optional timezone + +**Example:** +```python +agent.add_skill("datetime") +# Agent can now tell users the current time and date +``` + +#### Math Skill (`math`) +Provides safe mathematical expression evaluation. + +**Requirements:** +- None (uses built-in Python functionality) + +**Tools Added:** +- `calculate`: Evaluate mathematical expressions safely + +**Example:** +```python +agent.add_skill("math") +# Agent can now perform calculations like "2 + 3 * 4" +``` + +#### DataSphere Skill (`datasphere`) +Provides knowledge search capabilities using SignalWire DataSphere RAG stack. + +**Requirements:** +- Packages: `requests` + +**Parameters:** +- `space_name` (required): SignalWire space name +- `project_id` (required): SignalWire project ID +- `token` (required): SignalWire authentication token +- `document_id` (required): DataSphere document ID to search +- `count` (default: 1): Number of search results to return +- `distance` (default: 3.0): Distance threshold for search matching +- `tags` (optional): List of tags to filter search results +- `language` (optional): Language code to limit search +- `pos_to_expand` (optional): List of parts of speech for synonym expansion (e.g., ["NOUN", "VERB"]) +- `max_synonyms` (optional): Maximum number of synonyms to use for each word +- `tool_name` (default: "search_knowledge"): Custom name for the search tool +- `no_results_message` (default: "I couldn't find any relevant information for '\{query}' in the knowledge base. Try rephrasing your question or asking about a different topic."): Custom message when no results found + +**Multiple Instance Support:** +The DataSphere skill supports multiple instances with different tool names, allowing you to search multiple knowledge bases: + +**Example:** +```python +# Basic single instance +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token", + "document_id": "general-knowledge" +}) +# Creates tool: search_knowledge + +# Multiple instances for different knowledge bases +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token", + "document_id": "product-docs", + "tool_name": "search_products", + "tags": ["Products", "Features"], + "count": 3 +}) +# Creates tool: search_products + +agent.add_skill("datasphere", { + "space_name": "my-space", + "project_id": "my-project", + "token": "my-token", + "document_id": "support-kb", + "tool_name": "search_support", + "no_results_message": "I couldn't find support information about '\{query}'. Try contacting our support team.", + "distance": 5.0 +}) +# Creates tool: search_support +``` + +#### Native Vector Search Skill (`native_vector_search`) +Provides local document search capabilities using vector similarity and keyword search. This skill works entirely offline with local `.swsearch` index files or can connect to remote search servers. + +**Requirements:** +- Packages: `sentence-transformers`, `scikit-learn`, `numpy` (install with `pip install signalwire-agents[search]`) + +**Parameters:** +- `tool_name` (default: "search_knowledge"): Custom name for the search tool +- `description` (default: "Search the local knowledge base for information"): Tool description +- `index_file` (optional): Path to local `.swsearch` index file +- `remote_url` (optional): URL of remote search server (e.g., `http://localhost:8001`) +- `index_name` (default: "default"): Index name on remote server (for remote mode) +- `build_index` (default: False): Auto-build index if missing +- `source_dir` (optional): Source directory for auto-building index +- `file_types` (default: ["md", "txt"]): File types to include when building index +- `count` (default: 3): Number of search results to return +- `distance_threshold` (default: 0.0): Minimum similarity score for results +- `tags` (optional): List of tags to filter search results +- `response_prefix` (optional): Text to prepend to all search responses +- `response_postfix` (optional): Text to append to all search responses +- `no_results_message` (default: "No information found for '\{query}'"): Custom message when no results found + +**Multiple Instance Support:** +The native vector search skill supports multiple instances with different indexes and tool names: + +**Example:** +```python +# Local mode with auto-build +agent.add_skill("native_vector_search", { + "tool_name": "search_docs", + "description": "Search SDK concepts guide", + "build_index": True, + "source_dir": "./docs", + "index_file": "concepts.swsearch", + "count": 5 +}) +# Creates tool: search_docs + +# Remote mode connecting to search server +agent.add_skill("native_vector_search", { + "tool_name": "search_knowledge", + "description": "Search the knowledge base", + "remote_url": "http://localhost:8001", + "index_name": "concepts", + "count": 3 +}) +# Creates tool: search_knowledge + +# Multiple local indexes +agent.add_skill("native_vector_search", { + "tool_name": "search_examples", + "description": "Search code examples", + "index_file": "examples.swsearch", + "response_prefix": "From the examples:" +}) +# Creates tool: search_examples + +# Voice-optimized responses using concepts guide +agent.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "concepts.swsearch", + "response_prefix": "Based on the comprehensive SDK guide:", + "response_postfix": "Would you like more specific information?", + "no_results_message": "I couldn't find information about '\{query}' in the concepts guide." +}) +``` + +**Building Search Indexes:** +Before using local mode, you need to build search indexes: + +```bash +# Build index from documentation +python -m signalwire_agents.cli.build_search docs --output docs.swsearch + +# Build with custom settings +python -m signalwire_agents.cli.build_search ./knowledge \ + --output knowledge.swsearch \ + --file-types md,txt,pdf \ + --chunk-size 500 \ + --verbose +``` + +For complete documentation on the search system, see [Local Search System](/agents-sdk/skills/search). + +### Skill Management + +```python +# Check what skills are loaded +loaded_skills = agent.list_skills() +print(f"Loaded skills: {', '.join(loaded_skills)}") + +# Check if a specific skill is loaded +if agent.has_skill("web_search"): + print("Web search is available") + +# Remove a skill (if needed) +agent.remove_skill("math") +``` + +### Advanced Skill Configuration with swaig_fields + +Skills support a special `swaig_fields` parameter that allows you to customize how SWAIG functions are registered. This parameter gets merged into the function decorator object, enabling the skill loader to add additional configuration to the tools. + +```python +# Add a skill with swaig_fields to customize SWAIG function properties +agent.add_skill("math", { + "precision": 2, # Regular skill parameter + "swaig_fields": { # Special fields merged into SWAIG function + "secure": False, # Override default security requirement + "fillers": { + "en-US": ["Let me calculate that...", "Computing the result..."], + "es-ES": ["Déjame calcular eso...", "Calculando el resultado..."] + } + } +}) + +# Add web search with custom security and fillers +agent.add_skill("web_search", { + "num_results": 3, + "delay": 0.5, + "swaig_fields": { + "secure": True, # Require authentication + "fillers": { + "en-US": ["Searching the web...", "Looking that up...", "Finding information..."] + } + } +}) +``` + +The `swaig_fields` can include any parameter accepted by `AgentBase.define_tool()`: +- `secure`: Boolean indicating if the function requires authentication +- `fillers`: Dictionary mapping language codes to arrays of filler phrases +- Any other fields supported by the SWAIG function system + +This feature enables advanced customization of how skills integrate with the agent's SWAIG system. + +### Error Handling + +The skills system provides detailed error messages for common issues: + +```python +try: + agent.add_skill("web_search") +except ValueError as e: + print(f"Failed to load skill: {e}") + # Output: "Failed to load skill 'web_search': Missing required environment variables: ['GOOGLE_SEARCH_API_KEY']" +``` + +### Creating Custom Skills + +You can create your own skills by extending the `SkillBase` class: + +```python +from signalwire_agents.core.skill_base import SkillBase +from signalwire_agents.core.function_result import SwaigFunctionResult + +class WeatherSkill(SkillBase): + """A custom skill for weather information""" + + SKILL_NAME = "weather" + SKILL_DESCRIPTION = "Get weather information for locations" + SKILL_VERSION = "1.0.0" + REQUIRED_PACKAGES = ["requests"] + REQUIRED_ENV_VARS = ["WEATHER_API_KEY"] + + def setup(self) -> bool: + """Setup the skill - validate dependencies and initialize""" + if not self.validate_env_vars() or not self.validate_packages(): + return False + + # Get configuration parameters + self.default_units = self.params.get('units', 'fahrenheit') + self.timeout = self.params.get('timeout', 10) + + return True + + def register_tools(self) -> None: + """Register tools with the agent""" + self.define_tool_with_swaig_fields( + name="get_weather", + description="Get current weather for a location", + parameters={ + "location": { + "type": "string", + "description": "City or location name" + }, + "units": { + "type": "string", + "description": "Temperature units (fahrenheit or celsius)", + "enum": ["fahrenheit", "celsius"] + } + }, + handler=self._get_weather_handler + ) + + def _get_weather_handler(self, args, raw_data): + """Handle weather requests""" + location = args.get("location", "") + units = args.get("units", self.default_units) + + if not location: + return SwaigFunctionResult("Please provide a location") + + # Your weather API integration here + weather_data = f"Weather for {location}: 72°F and sunny" + return SwaigFunctionResult(weather_data) + + def get_hints(self) -> List[str]: + """Return speech recognition hints""" + return ["weather", "temperature", "forecast", "conditions"] + + def get_prompt_sections(self) -> List[Dict[str, Any]]: + """Return prompt sections to add to agent""" + return [ + { + "title": "Weather Information", + "body": "You can provide current weather information for any location.", + "bullets": [ + "Use get_weather tool when users ask about weather", + "Always specify the location clearly", + "Include temperature and conditions in your response" + ] + } + ] +``` + +**Using the custom skill:** +```python +# Place the skill in signalwire_agents/skills/weather/skill.py +# Then use it in your agent: + +agent.add_skill("weather", { + "units": "celsius", + "timeout": 15 +}) +``` + +### Skills with Dynamic Configuration + +Skills work seamlessly with dynamic configuration: + +```python +class DynamicSkillAgent(AgentBase): + def __init__(self): + super().__init__(name="dynamic-skill-agent") + self.set_dynamic_config_callback(self.configure_per_request) + + def configure_per_request(self, query_params, body_params, headers, agent): + # Add different skills based on request parameters + tier = query_params.get('tier', 'basic') + + # Basic skills for all users + agent.add_skill("datetime") + agent.add_skill("math") + + # Premium skills for premium users + if tier == 'premium': + agent.add_skill("web_search", { + "num_results": 5, + "delay": 0.5 + }) + elif tier == 'basic': + agent.add_skill("web_search", { + "num_results": 1, + "delay": 0 + }) +``` + +### Best Practices + +1. **Choose appropriate parameters**: Configure skills for your use case + ```python + # For speed (customer service) + agent.add_skill("web_search", {"num_results": 1, "delay": 0}) + + # For research (detailed analysis) + agent.add_skill("web_search", {"num_results": 5, "delay": 1.0}) + ``` + +2. **Handle missing dependencies gracefully**: + ```python + try: + agent.add_skill("web_search") + except ValueError as e: + self.logger.warning(f"Web search unavailable: {e}") + # Continue without web search capability + ``` + +3. **Document your custom skills**: Include clear descriptions and parameter documentation + +4. **Test skills in isolation**: Create simple test scripts to verify skill functionality + +For more detailed information about the skills system architecture and advanced customization, see the [Skills System Overview](/agents-sdk/skills). + +## Multilingual Support + +Agents can support multiple languages: + +```python +# Add English language +self.add_language( + name="English", + code="en-US", + voice="en-US-Neural2-F", + speech_fillers=["Let me think...", "One moment please..."], + function_fillers=["I'm looking that up...", "Let me check that..."] +) + +# Add Spanish language +self.add_language( + name="Spanish", + code="es", + voice="rime.spore:multilingual", + speech_fillers=["Un momento por favor...", "Estoy pensando..."] +) +``` + +### Voice Formats + +There are different ways to specify voices: + +```python +# Simple format +self.add_language(name="English", code="en-US", voice="en-US-Neural2-F") + +# Explicit parameters with engine and model +self.add_language( + name="British English", + code="en-GB", + voice="spore", + engine="rime", + model="multilingual" +) + +# Combined string format +self.add_language( + name="Spanish", + code="es", + voice="rime.spore:multilingual" +) +``` + +## Agent Configuration + +### Adding Hints + +Hints help the AI understand certain terms better: + +```python +# Simple hints (list of words) +self.add_hints(["SignalWire", "SWML", "SWAIG"]) + +# Pattern hint with replacement +self.add_pattern_hint( + hint="AI Agent", + pattern="AI\\s+Agent", + replace="A.I. Agent", + ignore_case=True +) +``` + +### Adding Pronunciation Rules + +Pronunciation rules help the AI speak certain terms correctly: + +```python +# Add pronunciation rule +self.add_pronunciation("API", "A P I", ignore_case=False) +self.add_pronunciation("SIP", "sip", ignore_case=True) +``` + +### Setting AI Parameters + +Configure various AI behavior parameters: + +```python +# Set AI parameters +self.set_params({ + "wait_for_user": False, + "end_of_speech_timeout": 1000, + "ai_volume": 5, + "languages_enabled": True, + "local_tz": "America/Los_Angeles" +}) +``` + +### Setting Global Data + +Provide global data for the AI to reference: + +```python +# Set global data +self.set_global_data({ + "company_name": "SignalWire", + "product": "AI Agent SDK", + "supported_features": [ + "Voice AI", + "Telephone integration", + "SWAIG functions" + ] +}) +``` + +## Dynamic Agent Configuration + +Dynamic agent configuration allows you to configure agents per-request based on parameters from the HTTP request (query parameters, body data, headers). This enables powerful patterns like multi-tenant applications, A/B testing, personalization, and localization. + +### Overview + +There are two main approaches to agent configuration: + +#### Static Configuration (Traditional) +```python +class StaticAgent(AgentBase): + def __init__(self): + super().__init__(name="static-agent") + + # Configuration happens once at startup + self.add_language("English", "en-US", "rime.spore:mistv2") + self.set_params({"end_of_speech_timeout": 500}) + self.prompt_add_section("Role", "You are a customer service agent.") + self.set_global_data({"service_level": "standard"}) +``` + +**Pros**: Simple, fast, predictable +**Cons**: Same behavior for all users, requires separate agents for different configurations + +#### Dynamic Configuration (New) +```python +class DynamicAgent(AgentBase): + def __init__(self): + super().__init__(name="dynamic-agent") + + # No static configuration - set up dynamic callback instead + self.set_dynamic_config_callback(self.configure_per_request) + + def configure_per_request(self, query_params, body_params, headers, agent): + # Configuration happens fresh for each request + tier = query_params.get('tier', 'standard') + + if tier == 'premium': + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.set_params({"end_of_speech_timeout": 300}) # Faster + agent.prompt_add_section("Role", "You are a premium customer service agent.") + agent.set_global_data({"service_level": "premium"}) + else: + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.set_params({"end_of_speech_timeout": 500}) # Standard + agent.prompt_add_section("Role", "You are a customer service agent.") + agent.set_global_data({"service_level": "standard"}) +``` + +**Pros**: Highly flexible, single agent serves multiple configurations, enables advanced use cases +**Cons**: Slightly more complex, configuration overhead per request + +### Setting Up Dynamic Configuration + +Use the `set_dynamic_config_callback()` method to register a callback function that will be called for each request: + +```python +class MyDynamicAgent(AgentBase): + def __init__(self): + super().__init__(name="my-agent", route="/agent") + + # Register the dynamic configuration callback + self.set_dynamic_config_callback(self.configure_agent_dynamically) + + def configure_agent_dynamically(self, query_params, body_params, headers, agent): + """ + This method is called for every request to configure the agent + + Args: + query_params (dict): Query string parameters from the URL + body_params (dict): Parsed JSON body from POST requests + headers (dict): HTTP headers from the request + agent (EphemeralAgentConfig): Configuration object with familiar methods + """ + # Your dynamic configuration logic here + pass +``` + +The callback function receives four parameters: +- **query_params**: Dictionary of URL query parameters +- **body_params**: Dictionary of parsed JSON body (empty for GET requests) +- **headers**: Dictionary of HTTP headers +- **agent**: EphemeralAgentConfig object for configuration + +### EphemeralAgentConfig + +The `agent` parameter in your callback is an `EphemeralAgentConfig` object that provides the same familiar methods as `AgentBase`, but applies them per-request: + +#### Language Configuration +```python +# Add languages with voice configuration +agent.add_language("English", "en-US", "rime.spore:mistv2") +agent.add_language("Spanish", "es-ES", "rime.spore:mistv2") +``` + +#### Prompt Building +```python +# Add prompt sections +agent.prompt_add_section("Role", "You are a helpful assistant.") +agent.prompt_add_section("Guidelines", bullets=[ + "Be professional and courteous", + "Provide accurate information", + "Ask clarifying questions when needed" +]) + +# Set raw prompt text +agent.set_prompt_text("You are a specialized AI assistant...") + +# Set post-prompt for summary +agent.set_post_prompt("Summarize the key points of this conversation.") +``` + +#### AI Parameters +```python +# Configure AI behavior +agent.set_params({ + "end_of_speech_timeout": 300, + "attention_timeout": 20000, + "background_file_volume": -30 +}) +``` + +#### Global Data +```python +# Set data available to the AI +agent.set_global_data({ + "customer_tier": "premium", + "features_enabled": ["advanced_support", "priority_queue"], + "session_info": {"start_time": "2024-01-01T00:00:00Z"} +}) + +# Update existing global data +agent.update_global_data({"additional_info": "value"}) +``` + +#### Speech Recognition Hints +```python +# Add hints for better speech recognition +agent.add_hints(["SignalWire", "SWML", "API", "technical"]) +agent.add_pronunciation("API", "A P I") +``` + +#### Function Configuration +```python +# Set native functions +agent.set_native_functions(["transfer", "hangup"]) + +# Add function includes +agent.add_function_include( + url="https://api.example.com/functions", + functions=["get_account_info", "update_profile"] +) +``` + +### Request Data Access + +Your callback function receives detailed information about the incoming request: + +#### Query Parameters +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + # Extract query parameters + tier = query_params.get('tier', 'standard') + language = query_params.get('language', 'en') + customer_id = query_params.get('customer_id') + debug = query_params.get('debug', '').lower() == 'true' + + # Use parameters for configuration + if tier == 'premium': + agent.set_params({"end_of_speech_timeout": 300}) + + if customer_id: + agent.set_global_data({"customer_id": customer_id}) + +# Request: GET /agent?tier=premium&language=es&customer_id=12345&debug=true +``` + +#### POST Body Parameters +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + # Extract from POST body + user_profile = body_params.get('user_profile', {}) + preferences = body_params.get('preferences', {}) + + # Configure based on profile + if user_profile.get('language') == 'es': + agent.add_language("Spanish", "es-ES", "rime.spore:mistv2") + + if preferences.get('voice_speed') == 'fast': + agent.set_params({"end_of_speech_timeout": 200}) + +# Request: POST /agent with JSON body: +# { +# "user_profile": {"language": "es", "region": "mx"}, +# "preferences": {"voice_speed": "fast", "tone": "formal"} +# } +``` + +#### HTTP Headers +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + # Extract headers + user_agent = headers.get('user-agent', '') + auth_token = headers.get('authorization', '') + locale = headers.get('accept-language', 'en-US') + + # Configure based on headers + if 'mobile' in user_agent.lower(): + agent.set_params({"end_of_speech_timeout": 400}) # Longer for mobile + + if locale.startswith('es'): + agent.add_language("Spanish", "es-ES", "rime.spore:mistv2") +``` + +### Configuration Examples + +#### Simple Multi-Tenant Configuration +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + tenant = query_params.get('tenant', 'default') + + # Tenant-specific configuration + if tenant == 'healthcare': + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.prompt_add_section("Compliance", + "Follow HIPAA guidelines and maintain patient confidentiality.") + agent.set_global_data({ + "industry": "healthcare", + "compliance_level": "hipaa" + }) + elif tenant == 'finance': + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.prompt_add_section("Compliance", + "Follow financial regulations and protect sensitive data.") + agent.set_global_data({ + "industry": "finance", + "compliance_level": "pci" + }) +``` + +#### Language and Localization +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + language = query_params.get('language', 'en') + region = query_params.get('region', 'us') + + # Configure language and voice + if language == 'es': + if region == 'mx': + agent.add_language("Spanish (Mexico)", "es-MX", "rime.spore:mistv2") + else: + agent.add_language("Spanish", "es-ES", "rime.spore:mistv2") + + agent.prompt_add_section("Language", "Respond in Spanish.") + elif language == 'fr': + agent.add_language("French", "fr-FR", "rime.alois") + agent.prompt_add_section("Language", "Respond in French.") + else: + agent.add_language("English", "en-US", "rime.spore:mistv2") + + # Regional customization + agent.set_global_data({ + "language": language, + "region": region, + "currency": "USD" if region == "us" else "EUR" if region == "eu" else "MXN" + }) +``` + +#### A/B Testing Configuration +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + # Determine test group (could be from query param, user ID hash, etc.) + test_group = query_params.get('test_group', 'A') + + if test_group == 'A': + # Control group - standard configuration + agent.set_params({"end_of_speech_timeout": 500}) + agent.prompt_add_section("Style", "Use a standard conversational approach.") + agent.set_global_data({"test_group": "A", "features": ["basic"]}) + else: + # Test group B - experimental features + agent.set_params({"end_of_speech_timeout": 300}) + agent.prompt_add_section("Style", + "Use an enhanced, more interactive conversational approach.") + agent.set_global_data({"test_group": "B", "features": ["basic", "enhanced"]}) +``` + +#### Customer Tier-Based Configuration +```python +def configure_agent_dynamically(self, query_params, body_params, headers, agent): + customer_id = query_params.get('customer_id') + tier = query_params.get('tier', 'standard') + + # Base configuration + agent.add_language("English", "en-US", "rime.spore:mistv2") + + # Tier-specific configuration + if tier == 'enterprise': + agent.set_params({ + "end_of_speech_timeout": 200, # Fastest response + "attention_timeout": 30000 # Longest attention span + }) + agent.prompt_add_section("Service Level", + "You provide white-glove enterprise support with priority handling.") + features = ["all_features", "dedicated_support", "custom_integration"] + elif tier == 'premium': + agent.set_params({ + "end_of_speech_timeout": 300, + "attention_timeout": 20000 + }) + agent.prompt_add_section("Service Level", + "You provide premium support with enhanced features.") + features = ["premium_features", "priority_support"] + else: + agent.set_params({ + "end_of_speech_timeout": 500, + "attention_timeout": 15000 + }) + agent.prompt_add_section("Service Level", + "You provide standard customer support.") + features = ["basic_features"] + + # Set global data + global_data = {"tier": tier, "features": features} + if customer_id: + global_data["customer_id"] = customer_id + agent.set_global_data(global_data) +``` + +### Use Cases + +#### Multi-Tenant SaaS Applications +Perfect for SaaS platforms where each customer needs different agent behavior: + +```python +# Different tenants get different capabilities +# /agent?tenant=acme&industry=healthcare +# /agent?tenant=globex&industry=finance +``` + +Benefits: +- Single agent deployment serves all customers +- Tenant-specific branding and behavior +- Industry-specific compliance and terminology +- Custom feature sets per subscription level + +#### A/B Testing and Experimentation +Test different agent configurations with real users: + +```python +# Split traffic between different configurations +# /agent?test_group=A (control) +# /agent?test_group=B (experimental) +``` + +Benefits: +- Compare agent performance metrics +- Test new features with subset of users +- Gradual rollout of improvements +- Data-driven optimization + +#### Personalization and User Preferences +Adapt agent behavior to individual user preferences: + +```python +# Personalized based on user profile +# /agent?user_id=123&voice_speed=fast&formality=casual +``` + +Benefits: +- Improved user experience +- Accessibility support (voice speed, etc.) +- Cultural and linguistic adaptation +- Learning from user interactions + +#### Geographic and Cultural Localization +Adapt to different regions and cultures: + +```python +# Location-based configuration +# /agent?country=mx&language=es&timezone=America/Mexico_City +``` + +Benefits: +- Local language and dialect support +- Cultural appropriateness +- Regional business practices +- Time zone aware responses + +### Migration Guide + +#### Converting Static Agents to Dynamic + +**Step 1: Move Configuration to Callback** + +Before (Static): +```python +class MyAgent(AgentBase): + def __init__(self): + super().__init__(name="my-agent") + + # Static configuration + self.add_language("English", "en-US", "rime.spore:mistv2") + self.set_params({"end_of_speech_timeout": 500}) + self.prompt_add_section("Role", "You are a helpful assistant.") + self.set_global_data({"version": "1.0"}) +``` + +After (Dynamic): +```python +class MyAgent(AgentBase): + def __init__(self): + super().__init__(name="my-agent") + + # Set up dynamic configuration + self.set_dynamic_config_callback(self.configure_agent) + + def configure_agent(self, query_params, body_params, headers, agent): + # Same configuration, but now dynamic + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.set_params({"end_of_speech_timeout": 500}) + agent.prompt_add_section("Role", "You are a helpful assistant.") + agent.set_global_data({"version": "1.0"}) +``` + +**Step 2: Add Parameter-Based Logic** + +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Start with base configuration + agent.add_language("English", "en-US", "rime.spore:mistv2") + agent.prompt_add_section("Role", "You are a helpful assistant.") + + # Add parameter-based customization + timeout = int(query_params.get('timeout', '500')) + agent.set_params({"end_of_speech_timeout": timeout}) + + version = query_params.get('version', '1.0') + agent.set_global_data({"version": version}) +``` + +**Step 3: Test Both Approaches** + +You can support both static and dynamic patterns during migration: + +```python +class MyAgent(AgentBase): + def __init__(self, use_dynamic=False): + super().__init__(name="my-agent") + + if use_dynamic: + self.set_dynamic_config_callback(self.configure_agent) + else: + # Keep static configuration for backward compatibility + self._setup_static_config() + + def _setup_static_config(self): + # Original static configuration + self.add_language("English", "en-US", "rime.spore:mistv2") + # ... rest of static config + + def configure_agent(self, query_params, body_params, headers, agent): + # New dynamic configuration + # ... dynamic config logic +``` + +### Best Practices + +#### Performance Considerations + +1. **Keep Callbacks Lightweight** +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Good: Simple parameter extraction and configuration + tier = query_params.get('tier', 'standard') + agent.set_params(TIER_CONFIGS[tier]) + + # Avoid: Heavy computation or external API calls + # customer_data = expensive_api_call(customer_id) # Don't do this +``` + +2. **Cache Configuration Data** +```python +class MyAgent(AgentBase): + def __init__(self): + super().__init__(name="my-agent") + + # Pre-compute configuration templates + self.tier_configs = { + 'basic': {'end_of_speech_timeout': 500}, + 'premium': {'end_of_speech_timeout': 300}, + 'enterprise': {'end_of_speech_timeout': 200} + } + + self.set_dynamic_config_callback(self.configure_agent) + + def configure_agent(self, query_params, body_params, headers, agent): + tier = query_params.get('tier', 'basic') + agent.set_params(self.tier_configs[tier]) +``` + +3. **Use Default Values** +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Always provide defaults + language = query_params.get('language', 'en') + tier = query_params.get('tier', 'standard') + + # Handle invalid values gracefully + if language not in ['en', 'es', 'fr']: + language = 'en' +``` + +#### Security Considerations + +1. **Validate Input Parameters** +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Validate and sanitize inputs + tier = query_params.get('tier', 'standard') + if tier not in ['basic', 'premium', 'enterprise']: + tier = 'basic' # Safe default + + # Validate numeric parameters + try: + timeout = int(query_params.get('timeout', '500')) + timeout = max(100, min(timeout, 2000)) # Clamp to reasonable range + except ValueError: + timeout = 500 # Safe default +``` + +2. **Protect Sensitive Configuration** +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Don't expose internal configuration via parameters + # Bad: agent.set_global_data({"api_key": query_params.get('api_key')}) + + # Good: Use internal mapping for call-related data only + customer_id = query_params.get('customer_id') + if customer_id and self.is_valid_customer(customer_id): + # Store call-related customer info, NOT sensitive credentials + agent.set_global_data({ + "customer_id": customer_id, + "customer_tier": self.get_customer_tier(customer_id), + "account_type": "premium" + }) +``` + +3. **Rate Limiting for Complex Configurations** +```python +from functools import lru_cache + +class MyAgent(AgentBase): + @lru_cache(maxsize=100) + def get_customer_config(self, customer_id): + # Cache expensive lookups + return self.database.get_customer_settings(customer_id) + + def configure_agent(self, query_params, body_params, headers, agent): + customer_id = query_params.get('customer_id') + if customer_id: + config = self.get_customer_config(customer_id) + agent.set_global_data(config) +``` + +#### Error Handling + +1. **Graceful Degradation** +```python +def configure_agent(self, query_params, body_params, headers, agent): + try: + # Try custom configuration + self.apply_custom_config(query_params, agent) + except Exception as e: + # Log error but don't fail the request + self.log.error("config_error", error=str(e)) + + # Fall back to default configuration + self.apply_default_config(agent) +``` + +2. **Configuration Validation** +```python +def configure_agent(self, query_params, body_params, headers, agent): + # Validate required parameters + if not query_params.get('tenant'): + agent.set_global_data({"error": "Missing tenant parameter"}) + return + + # Validate configuration makes sense + language = query_params.get('language', 'en') + region = query_params.get('region', 'us') + + if language == 'es' and region == 'us': + # Adjust for Spanish speakers in US + agent.add_language("Spanish (US)", "es-US", "rime.spore:mistv2") +``` + +Dynamic agent configuration is a powerful feature that enables sophisticated, multi-tenant AI applications while maintaining the familiar AgentBase API. Start with simple parameter-based configuration and gradually add more complex logic as your use cases evolve. + +## Advanced Features + +### State Management + +Enable state tracking to persist information across interactions: + +```python +# Enable state tracking in the constructor +super().__init__( + name="stateful-agent", + enable_state_tracking=True, # Automatically registers startup_hook and hangup_hook + state_manager=FileStateManager(storage_dir="./state") # Optional custom state manager +) + +# Access and update state +@AgentBase.tool( + name="save_preference", + description="Save a user preference", + parameters={ + "key": { + "type": "string", + "description": "The preference key" + }, + "value": { + "type": "string", + "description": "The preference value" + } + } +) +def save_preference(self, args, raw_data): + # Get the call ID from the raw data + call_id = raw_data.get("call_id") + + if call_id: + # Get current state or empty dict if none exists + state = self.get_state(call_id) or {} + + # Update the state + preferences = state.get("preferences", {}) + preferences[args.get("key")] = args.get("value") + state["preferences"] = preferences + + # Save the updated state + self.update_state(call_id, state) + + return SwaigFunctionResult("Preference saved") + else: + return SwaigFunctionResult("Could not save preference: No call ID") +``` + +### SIP Routing + +SIP routing allows your agents to receive voice calls via SIP addresses. The SDK supports both individual agent-level routing and centralized server-level routing. + +#### Individual Agent SIP Routing + +Enable SIP routing on a single agent: + +```python +# Enable SIP routing with automatic username mapping based on agent name +agent.enable_sip_routing(auto_map=True) + +# Register additional SIP usernames for this agent +agent.register_sip_username("support_agent") +agent.register_sip_username("help_desk") +``` + +When `auto_map=True`, the agent automatically registers SIP usernames based on: +- The agent's name (e.g., `support@domain`) +- The agent's route path (e.g., `/support` becomes `support@domain`) +- Common variations (e.g., removing vowels for shorter dialing) + +#### Server-Level SIP Routing (Multi-Agent) + +For multi-agent setups, centralized routing is more efficient: + +```python +# Create an AgentServer +server = AgentServer(host="0.0.0.0", port=3000) + +# Register multiple agents +server.register(registration_agent) # Route: /register +server.register(support_agent) # Route: /support + +# Set up central SIP routing +server.setup_sip_routing(route="/sip", auto_map=True) + +# Register additional SIP username mappings +server.register_sip_username("signup", "/register") # signup@domain → registration agent +server.register_sip_username("help", "/support") # help@domain → support agent +``` + +With server-level routing: +- Each agent is reachable via its name (when `auto_map=True`) +- Additional SIP usernames can be mapped to specific agent routes +- All SIP routing is handled at a single endpoint (`/sip` by default) + +#### How SIP Routing Works + +1. A SIP call comes in with a username (e.g., `support@yourdomain`) +2. The SDK extracts the username part (`support`) +3. The system checks if this username is registered: + - In individual routing: The current agent checks its own username list + - In server routing: The server checks its central mapping table +4. If a match is found, the call is routed to the appropriate agent + +### Custom Routing + +You can dynamically handle requests to different paths using routing callbacks: + +```python +# Enable custom routing in the constructor or anytime after initialization +self.register_routing_callback(self.handle_customer_route, path="/customer") +self.register_routing_callback(self.handle_product_route, path="/product") + +# Define the routing handlers +def handle_customer_route(self, request, body): + """ + Process customer-related requests + + Args: + request: FastAPI Request object + body: Parsed JSON body as dictionary + + Returns: + Optional[str]: A URL to redirect to, or None to process normally + """ + # Extract any relevant data + customer_id = body.get("customer_id") + + # You can redirect to another agent/service if needed + if customer_id and customer_id.startswith("vip-"): + return f"/vip-handler/{customer_id}" + + # Or return None to process the request with on_swml_request + return None + +# Customize SWML based on the route in on_swml_request +def on_swml_request(self, request_data=None, callback_path=None): + """ + Customize SWML based on the request and path + + Args: + request_data: The request body data + callback_path: The path that triggered the routing callback + """ + if callback_path == "/customer": + # Serve customer-specific content + return { + "sections": { + "main": [ + {"answer": {}}, + {"play": {"url": "say:Welcome to customer service!"}} + ] + } + } + # Other path handling... + return None +``` + +### Customizing SWML Requests + +You can modify the SWML document based on request data by overriding the `on_swml_request` method: + +```python +def on_swml_request(self, request_data=None, callback_path=None): + """ + Customize the SWML document based on request data + + Args: + request_data: The request data (body for POST or query params for GET) + callback_path: The path that triggered the routing callback + + Returns: + Optional dict with modifications to apply to the document + """ + if request_data and "caller_type" in request_data: + # Example: Return modifications to change the AI behavior based on caller type + if request_data["caller_type"] == "vip": + return { + "sections": { + "main": [ + # Keep the first verb (answer) + # Modify the AI verb parameters + { + "ai": { + "params": { + "wait_for_user": False, + "end_of_speech_timeout": 500 # More responsive + } + } + } + ] + } + } + + # You can also use the callback_path to serve different content based on the route + if callback_path == "/customer": + return { + "sections": { + "main": [ + {"answer": {}}, + {"play": {"url": "say:Welcome to our customer service line."}} + ] + } + } + + # Return None to use the default document + return None +``` + +### Conversation Summary Handling + +Process conversation summaries: + +```python +def on_summary(self, summary, raw_data=None): + """ + Handle the conversation summary + + Args: + summary: The summary object or None if no summary was found + raw_data: The complete raw POST data from the request + """ + if summary: + # Log the summary + self.log.info("conversation_summary", summary=summary) + + # Save the summary to a database, send notifications, etc. + # ... +``` + +### Custom Webhook URLs + +You can override the default webhook URLs for SWAIG functions and post-prompt delivery: + +```python +# In your agent initialization or setup code: + +# Override the webhook URL for all SWAIG functions +agent.set_web_hook_url("https://external-service.example.com/handle-swaig") + +# Override the post-prompt delivery URL +agent.set_post_prompt_url("https://analytics.example.com/conversation-summaries") + +# These methods allow you to: +# 1. Send function calls to external services instead of handling them locally +# 2. Send conversation summaries to analytics services or other systems +# 3. Use special URLs with pre-configured authentication +``` + +### External Input Checking + +The SDK provides a check-for-input endpoint that allows agents to check for new input from external systems: + +```python +# Example client code that checks for new input +import requests +import json + +def check_for_new_input(agent_url, conversation_id, auth): + """ + Check if there's any new input for a conversation + + Args: + agent_url: Base URL for the agent + conversation_id: ID of the conversation to check + auth: (username, password) tuple for basic auth + + Returns: + New messages if any, None otherwise + """ + url = f"{agent_url}/check_for_input" + response = requests.post( + url, + json={"conversation_id": conversation_id}, + auth=auth + ) + + if response.status_code == 200: + data = response.json() + if data.get("new_input", False): + return data.get("messages", []) + + return None +``` + +By default, the check_for_input endpoint returns an empty response. To implement custom behavior, override the `_handle_check_for_input_request` method in your agent: + +```python +async def _handle_check_for_input_request(self, request): + # First do basic authentication check + if not self._check_basic_auth(request): + return Response( + content=json.dumps({"error": "Unauthorized"}), + status_code=401, + headers={"WWW-Authenticate": "Basic"}, + media_type="application/json" + ) + + # Get conversation_id from request + conversation_id = None + if request.method == "POST": + body = await request.json() + conversation_id = body.get("conversation_id") + else: + conversation_id = request.query_params.get("conversation_id") + + if not conversation_id: + return Response( + content=json.dumps({"error": "Missing conversation_id"}), + status_code=400, + media_type="application/json" + ) + + # Custom logic to check for new input + # For example, checking a database or external API + messages = self._get_new_messages(conversation_id) + + return { + "status": "success", + "conversation_id": conversation_id, + "new_input": len(messages) > 0, + "messages": messages + } +``` + +This endpoint is useful for implementing asynchronous conversations where users might send messages through different channels that need to be incorporated into the agent conversation. + +## Prefab Agents + +Prefab agents are pre-configured agent implementations designed for specific use cases. They provide ready-to-use functionality with customization options, saving development time and ensuring consistent patterns. + +### Built-in Prefabs + +The SDK includes several built-in prefab agents: + +#### InfoGathererAgent + +Collects structured information from users: + +```python +from signalwire_agents.prefabs import InfoGathererAgent + +agent = InfoGathererAgent( + fields=[ + {"name": "full_name", "prompt": "What is your full name?"}, + {"name": "email", "prompt": "What is your email address?", + "validation": r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"}, + {"name": "reason", "prompt": "How can I help you today?"} + ], + confirmation_template="Thanks {full_name}, I'll help you with {reason}. I'll send a confirmation to {email}.", + name="info-gatherer", + route="/info-gatherer" +) + +agent.serve(host="0.0.0.0", port=8000) +``` + +#### FAQBotAgent + +Answers questions based on a knowledge base: + +```python +from signalwire_agents.prefabs import FAQBotAgent + +agent = FAQBotAgent( + knowledge_base_path="./docs", + personality="I'm a product documentation assistant.", + citation_style="inline", + name="knowledge-base", + route="/knowledge-base" +) + +agent.serve(host="0.0.0.0", port=8000) +``` + +#### ConciergeAgent + +Routes users to specialized agents: + +```python +from signalwire_agents.prefabs import ConciergeAgent + +agent = ConciergeAgent( + routing_map={ + "technical_support": { + "url": "http://tech-support-agent:8001", + "criteria": ["error", "broken", "not working"] + }, + "sales": { + "url": "http://sales-agent:8002", + "criteria": ["pricing", "purchase", "subscribe"] + } + }, + greeting="Welcome to SignalWire. How can I help you today?", + name="concierge", + route="/concierge" +) + +agent.serve(host="0.0.0.0", port=8000) +``` + +#### SurveyAgent + +Conducts structured surveys with different question types: + +```python +from signalwire_agents.prefabs import SurveyAgent + +agent = SurveyAgent( + survey_name="Customer Satisfaction", + introduction="We'd like to know about your recent experience with our product.", + questions=[ + { + "id": "satisfaction", + "text": "How satisfied are you with our product?", + "type": "rating", + "scale": 5, + "labels": { + "1": "Very dissatisfied", + "5": "Very satisfied" + } + }, + { + "id": "feedback", + "text": "Do you have any specific feedback about how we can improve?", + "type": "text" + } + ], + name="satisfaction-survey", + route="/survey" +) + +agent.serve(host="0.0.0.0", port=8000) +``` + +#### ReceptionistAgent + +Handles call routing and department transfers: + +```python +from signalwire_agents.prefabs import ReceptionistAgent + +agent = ReceptionistAgent( + departments=[ + {"name": "sales", "description": "For product inquiries and pricing", "number": "+15551235555"}, + {"name": "support", "description": "For technical assistance", "number": "+15551236666"}, + {"name": "billing", "description": "For payment and invoice questions", "number": "+15551237777"} + ], + greeting="Thank you for calling ACME Corp. How may I direct your call?", + voice="rime.spore:mistv2", + name="acme-receptionist", + route="/reception" +) + +agent.serve(host="0.0.0.0", port=8000) +``` + +### Creating Your Own Prefabs + +You can create your own prefab agents by extending `AgentBase` or any existing prefab. Custom prefabs can be created directly within your project or packaged as reusable libraries. + +#### Basic Prefab Structure + +A well-designed prefab should: + +1. Extend `AgentBase` or another prefab +2. Take configuration parameters in the constructor +3. Apply configuration to set up the agent +4. Provide appropriate default values +5. Include domain-specific tools + +Example of a custom support agent prefab: + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.function_result import SwaigFunctionResult + +class CustomerSupportAgent(AgentBase): + def __init__( + self, + product_name, + knowledge_base_path=None, + support_email=None, + escalation_path=None, + **kwargs + ): + # Pass standard params to parent + super().__init__(**kwargs) + + # Store custom configuration + self._product_name = product_name + self._knowledge_base_path = knowledge_base_path + self._support_email = support_email + self._escalation_path = escalation_path + + # Configure prompt + self.prompt_add_section("Personality", + body=f"I am a customer support agent for {product_name}.") + self.prompt_add_section("Goal", + body="Help customers solve their problems effectively.") + + # Add standard instructions + self._configure_instructions() + + # Register default tools + self._register_default_tools() + + def _configure_instructions(self): + """Configure standard instructions based on settings""" + instructions = [ + "Be professional but friendly.", + "Verify the customer's identity before sharing account details." + ] + + if self._escalation_path: + instructions.append( + f"For complex issues, offer to escalate to {self._escalation_path}." + ) + + self.prompt_add_section("Instructions", bullets=instructions) + + def _register_default_tools(self): + """Register default tools if appropriate paths are configured""" + if self._knowledge_base_path: + self.register_knowledge_base_tool() + + def register_knowledge_base_tool(self): + """Register the knowledge base search tool if configured""" + # Implementation... + pass + + @AgentBase.tool( + name="escalate_issue", + description="Escalate a customer issue to a human agent", + parameters={ + "issue_summary": {"type": "string", "description": "Brief summary of the issue"}, + "customer_email": {"type": "string", "description": "Customer's email address"} + } + ) + def escalate_issue(self, args, raw_data): + # Implementation... + return SwaigFunctionResult("Issue escalated successfully.") + + @AgentBase.tool( + name="send_support_email", + description="Send a follow-up email to the customer", + parameters={ + "customer_email": {"type": "string"}, + "issue_summary": {"type": "string"}, + "resolution_steps": {"type": "string"} + } + ) + def send_support_email(self, args, raw_data): + # Implementation... + return SwaigFunctionResult("Follow-up email sent successfully.") +``` + +#### Using the Custom Prefab + +```python +# Create an instance of the custom prefab +support_agent = CustomerSupportAgent( + product_name="SignalWire Voice API", + knowledge_base_path="./product_docs", + support_email="support@example.com", + escalation_path="tier 2 support", + name="voice-support", + route="/voice-support" +) + +# Start the agent +support_agent.serve(host="0.0.0.0", port=8000) +``` + +#### Customizing Existing Prefabs + +You can also extend and customize the built-in prefabs: + +```python +from signalwire_agents.prefabs import InfoGathererAgent + +class EnhancedGatherer(InfoGathererAgent): + def __init__(self, fields, **kwargs): + super().__init__(fields=fields, **kwargs) + + # Add an additional instruction + self.prompt_add_section("Instructions", bullets=[ + "Verify all information carefully." + ]) + + # Add an additional custom tool + + @AgentBase.tool( + name="check_customer", + description="Check customer status in database", + parameters={"email": {"type": "string"}} + ) + def check_customer(self, args, raw_data): + # Implementation... + return SwaigFunctionResult("Customer status: Active") +``` + +### Best Practices for Prefab Design + +1. **Clear Documentation**: Document the purpose, parameters, and extension points +2. **Sensible Defaults**: Provide working defaults that make sense for the use case +3. **Error Handling**: Implement robust error handling with helpful messages +4. **Modular Design**: Keep prefabs focused on a specific use case +5. **Consistent Interface**: Maintain consistent patterns across related prefabs +6. **Extension Points**: Provide clear ways for others to extend your prefab +7. **Configuration Options**: Make all key behaviors configurable + +### Making Prefabs Distributable + +To create distributable prefabs that can be used across multiple projects: + +1. **Package Structure**: Create a proper Python package +2. **Documentation**: Include clear usage examples +3. **Configuration**: Support both code and file-based configuration +4. **Testing**: Include tests for your prefab +5. **Publishing**: Publish to PyPI or share via GitHub + +Example package structure: + +``` +my-prefab-agents/ +├── README.md +├── setup.py +├── examples/ +│ └── support_agent_example.py +└── my_prefab_agents/ + ├── __init__.py + ├── support.py + ├── retail.py + └── utils/ + ├── __init__.py + └── knowledge_base.py +``` + +## API Reference + +### Constructor Parameters + +- `name`: Agent name/identifier (required) +- `route`: HTTP route path (default: "/") +- `host`: Host to bind to (default: "0.0.0.0") +- `port`: Port to bind to (default: 3000) +- `basic_auth`: Optional (username, password) tuple +- `use_pom`: Whether to use POM for prompts (default: True) +- `enable_state_tracking`: Enable conversation state (default: False) +- `token_expiry_secs`: State token expiry time (default: 3600) +- `auto_answer`: Auto-answer calls (default: True) +- `record_call`: Record calls (default: False) +- `state_manager`: Custom state manager (default: None) +- `schema_path`: Optional path to schema.json file +- `suppress_logs`: Whether to suppress structured logs (default: False) + +### Prompt Methods + +- `prompt_add_section(title, body=None, bullets=None, numbered=False, numbered_bullets=False)` +- `prompt_add_subsection(parent_title, title, body=None, bullets=None)` +- `prompt_add_to_section(title, body=None, bullet=None, bullets=None)` +- `set_prompt_text(prompt_text)` or `set_prompt(prompt_text)` +- `set_post_prompt(prompt_text)` +- `setPersonality(text)` - Convenience method that calls prompt_add_section +- `setGoal(text)` - Convenience method that calls prompt_add_section +- `setInstructions(bullets)` - Convenience method that calls prompt_add_section + +### SWAIG Methods + +- `@AgentBase.tool(name, description, parameters={}, secure=True, fillers=None)` +- `define_tool(name, description, parameters, handler, secure=True, fillers=None)` +- `set_native_functions(function_names)` +- `add_native_function(function_name)` +- `remove_native_function(function_name)` +- `add_function_include(url, functions, meta_data=None)` + +### Configuration Methods + +- `add_hint(hint)` and `add_hints(hints)` +- `add_pattern_hint(hint, pattern, replace, ignore_case=False)` +- `add_pronunciation(replace, with_text, ignore_case=False)` +- `add_language(name, code, voice, speech_fillers=None, function_fillers=None, engine=None, model=None)` +- `set_param(key, value)` and `set_params(params_dict)` +- `set_global_data(data_dict)` and `update_global_data(data_dict)` + +### State Methods + +- `get_state(call_id)` +- `set_state(call_id, data)` +- `update_state(call_id, data)` +- `clear_state(call_id)` +- `cleanup_expired_state()` + +### SIP Routing Methods + +- `enable_sip_routing(auto_map=True, path="/sip")`: Enable SIP routing for an agent +- `register_sip_username(sip_username)`: Register a SIP username for an agent +- `auto_map_sip_usernames()`: Automatically register SIP usernames based on agent attributes + +#### AgentServer SIP Methods + +- `setup_sip_routing(route="/sip", auto_map=True)`: Set up central SIP routing for a server +- `register_sip_username(username, route)`: Map a SIP username to an agent route + +### Service Methods + +- `serve(host=None, port=None)`: Start the web server +- `as_router()`: Return a FastAPI router for this agent +- `on_swml_request(request_data=None, callback_path=None)`: Customize SWML based on request data and path +- `on_summary(summary, raw_data=None)`: Handle post-prompt summaries +- `on_function_call(name, args, raw_data=None)`: Process SWAIG function calls +- `register_routing_callback(callback_fn, path="/sip")`: Register a callback for custom path routing +- `set_web_hook_url(url)`: Override the default web_hook_url with a supplied URL string +- `set_post_prompt_url(url)`: Override the default post_prompt_url with a supplied URL string + +### Endpoint Methods + +The SDK provides several endpoints for different purposes: + +- Root endpoint (`/`): Serves the main SWML document +- SWAIG endpoint (`/swaig`): Handles SWAIG function calls +- Post-prompt endpoint (`/post_prompt`): Processes conversation summaries +- Check-for-input endpoint (`/check_for_input`): Supports checking for new input from external systems +- Debug endpoint (`/debug`): Serves the SWML document with debug headers +- SIP routing endpoint (configurable, default `/sip`): Handles SIP routing requests + +## Testing + +The SignalWire AI Agent SDK provides comprehensive testing capabilities through the `swaig-test` CLI tool, which allows you to test agents locally and simulate serverless environments without deployment. + +### Local Agent Testing + +Test your agents locally before deployment: + +```bash +# Discover agents in a file +swaig-test examples/my_agent.py + +# List available functions +swaig-test examples/my_agent.py --list-tools + +# Test SWAIG functions +swaig-test examples/my_agent.py --exec get_weather --location "New York" + +# Generate SWML documents +swaig-test examples/my_agent.py --dump-swml +``` + +### Serverless Environment Simulation + +Test your agents in simulated serverless environments to ensure they work correctly when deployed: + +#### AWS Lambda Testing + +```bash +# Basic Lambda environment simulation +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml + +# Test with custom Lambda configuration +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --aws-function-name my-production-function \ + --aws-region us-west-2 \ + --exec my_function --param value + +# Test function execution in Lambda context +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --exec get_weather --location "Miami" \ + --full-request +``` + +#### CGI Environment Testing + +```bash +# Test CGI environment +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host my-server.com \ + --cgi-https \ + --dump-swml + +# Test function in CGI context +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host example.com \ + --exec my_function --param value +``` + +#### Google Cloud Functions Testing + +```bash +# Test Cloud Functions environment +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project my-project \ + --gcp-function-url https://my-function.cloudfunctions.net \ + --dump-swml +``` + +#### Azure Functions Testing + +```bash +# Test Azure Functions environment +swaig-test examples/my_agent.py --simulate-serverless azure_function \ + --azure-env production \ + --azure-function-url https://my-function.azurewebsites.net \ + --exec my_function +``` + +### Environment Variable Management + +Use environment files for consistent testing across different platforms: + +```bash +# Create environment file for production testing +cat > production.env << EOF +AWS_LAMBDA_FUNCTION_NAME=prod-my-agent +AWS_REGION=us-east-1 +API_KEY=prod_api_key_123 +DEBUG=false +TIMEOUT=60 +EOF + +# Test with environment file +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env-file production.env \ + --exec critical_function --input "test" + +# Override specific variables +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env-file production.env \ + --env DEBUG=true \ + --dump-swml +``` + +### Cross-Platform Testing + +Test the same agent across multiple platforms to ensure compatibility: + +```bash +# Test across all platforms +for platform in lambda cgi cloud_function azure_function; do + echo "Testing $platform..." + swaig-test examples/my_agent.py --simulate-serverless $platform \ + --exec my_function --param value +done + +# Compare SWML generation across platforms +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml > lambda.swml +swaig-test examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml > cgi.swml +diff lambda.swml cgi.swml +``` + +### Webhook URL Verification + +The serverless simulation automatically generates platform-appropriate webhook URLs: + +| Platform | Example Webhook URL | +|----------|-------------------| +| Lambda (Function URL) | `https://abc123.lambda-url.us-east-1.on.aws/swaig/` | +| Lambda (API Gateway) | `https://api123.execute-api.us-east-1.amazonaws.com/prod/swaig/` | +| CGI | `https://example.com/cgi-bin/agent.cgi/swaig/` | +| Cloud Functions | `https://my-function-abc123.cloudfunctions.net/swaig/` | +| Azure Functions | `https://my-function.azurewebsites.net/swaig/` | + +Verify webhook URLs are generated correctly: + +```bash +# Check Lambda webhook URL +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' + +# Check CGI webhook URL +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host my-production-server.com \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' +``` + +### Testing Best Practices + +1. **Test locally first**: Always test your agent in local mode before serverless simulation +2. **Test target platforms**: Test on all platforms where you plan to deploy +3. **Use environment files**: Create reusable environment configurations for different stages +4. **Verify webhook URLs**: Ensure URLs are generated correctly for your target platform +5. **Test function execution**: Verify that functions work correctly in serverless context +6. **Use verbose mode**: Enable `--verbose` for debugging environment setup and execution + +### Multi-Agent Testing + +For files with multiple agents, specify which agent to test: + +```bash +# Discover available agents +swaig-test multi_agent_file.py --list-agents + +# Test specific agent +swaig-test multi_agent_file.py --agent-class MyAgent --simulate-serverless lambda --dump-swml + +# Test different agents across platforms +swaig-test multi_agent_file.py --agent-class AgentA --simulate-serverless lambda --exec function1 +swaig-test multi_agent_file.py --agent-class AgentB --simulate-serverless cgi --cgi-host example.com --exec function2 +``` + +For more detailed testing documentation, see the [CLI Testing Guide](/agents-sdk/guides/cli-testing). + +## Examples + +### Simple Question-Answering Agent + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.function_result import SwaigFunctionResult +from datetime import datetime + +class SimpleAgent(AgentBase): + def __init__(self): + super().__init__( + name="simple", + route="/simple", + use_pom=True + ) + + # Configure agent personality + self.prompt_add_section("Personality", body="You are a friendly and helpful assistant.") + self.prompt_add_section("Goal", body="Help users with basic tasks and answer questions.") + self.prompt_add_section("Instructions", bullets=[ + "Be concise and direct in your responses.", + "If you don't know something, say so clearly.", + "Use the get_time function when asked about the current time." + ]) + + @AgentBase.tool( + name="get_time", + description="Get the current time", + parameters={} + ) + def get_time(self, args, raw_data): + """Get the current time""" + now = datetime.now() + formatted_time = now.strftime("%H:%M:%S") + return SwaigFunctionResult(f"The current time is {formatted_time}") + +def main(): + agent = SimpleAgent() + print("Starting agent server...") + print("Note: Works in any deployment mode (server/CGI/Lambda)") + agent.run() + +if __name__ == "__main__": + main() +``` + +### Multi-Language Customer Service Agent + +```python +class CustomerServiceAgent(AgentBase): + def __init__(self): + super().__init__( + name="customer-service", + route="/support", + use_pom=True + ) + + # Configure agent personality + self.prompt_add_section("Personality", + body="You are a helpful customer service representative for SignalWire.") + self.prompt_add_section("Knowledge", + body="You can answer questions about SignalWire products and services.") + self.prompt_add_section("Instructions", bullets=[ + "Greet customers politely", + "Answer questions about SignalWire products", + "Use check_account_status when customer asks about their account", + "Use create_support_ticket for unresolved issues" + ]) + + # Add language support + self.add_language( + name="English", + code="en-US", + voice="en-US-Neural2-F", + speech_fillers=["Let me think...", "One moment please..."], + function_fillers=["I'm looking that up...", "Let me check that..."] + ) + + self.add_language( + name="Spanish", + code="es", + voice="rime.spore:multilingual", + speech_fillers=["Un momento por favor...", "Estoy pensando..."] + ) + + # Enable languages + self.set_params({"languages_enabled": True}) + + # Add company information + self.set_global_data({ + "company_name": "SignalWire", + "support_hours": "9am-5pm ET, Monday through Friday", + "support_email": "support@signalwire.com" + }) + + @AgentBase.tool( + name="check_account_status", + description="Check the status of a customer's account", + parameters={ + "account_id": { + "type": "string", + "description": "The customer's account ID" + } + } + ) + def check_account_status(self, args, raw_data): + account_id = args.get("account_id") + # In a real implementation, this would query a database + return SwaigFunctionResult(f"Account {account_id} is in good standing.") + + @AgentBase.tool( + name="create_support_ticket", + description="Create a support ticket for an unresolved issue", + parameters={ + "issue": { + "type": "string", + "description": "Brief description of the issue" + }, + "priority": { + "type": "string", + "description": "Ticket priority", + "enum": ["low", "medium", "high", "critical"] + } + } + ) + def create_support_ticket(self, args, raw_data): + issue = args.get("issue", "") + priority = args.get("priority", "medium") + + # Generate a ticket ID (in a real system, this would create a database entry) + ticket_id = f"TICKET-{hash(issue) % 10000:04d}" + + return SwaigFunctionResult( + f"Support ticket {ticket_id} has been created with {priority} priority. " + + "A support representative will contact you shortly." + ) + +def main(): + agent = CustomerServiceAgent() + print("Starting customer service agent...") + print("Note: Works in any deployment mode (server/CGI/Lambda)") + agent.run() + +if __name__ == "__main__": + main() +``` + +### Dynamic Agent Configuration Examples + +For working examples of dynamic agent configuration, see these files in the `examples` directory: + +- **`simple_static_agent.py`**: Traditional static configuration approach +- **`simple_dynamic_agent.py`**: Same agent but using dynamic configuration +- **`simple_dynamic_enhanced.py`**: Enhanced version that actually uses request parameters +- **`comprehensive_dynamic_agent.py`**: Advanced multi-tier, multi-industry dynamic agent +- **`custom_path_agent.py`**: Dynamic agent with custom routing path +- **`multi_agent_server.py`**: Multiple specialized dynamic agents on one server + +These examples demonstrate the progression from static to dynamic configuration and show real-world use cases like multi-tenant applications, A/B testing, and personalization. + +For more examples, see the `examples` directory in the SignalWire AI Agent SDK repository. + +# Build index from the comprehensive concepts guide +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch + +# Build from multiple sources +sw-search docs/signalwire_agents_concepts_guide.md examples README.md --output comprehensive.swsearch + +# Traditional directory approach with custom settings +sw-search ./knowledge \ + --output knowledge.swsearch \ + --file-types md,txt,pdf \ + --chunking-strategy sentence \ + --max-sentences-per-chunk 8 \ + --verbose \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/cli-testing.md b/docs/swml/agents-sdk/guides/cli-testing.md new file mode 100644 index 00000000..5696b911 --- /dev/null +++ b/docs/swml/agents-sdk/guides/cli-testing.md @@ -0,0 +1,2127 @@ +--- +id: agents-sdk-guides-cli-testing +slug: /agents-sdk/guides/cli-testing +title: CLI Testing Guide +sidebar_label: CLI Testing +--- + +# SWAIG CLI Testing Tool Guide + +A comprehensive command-line tool for testing SignalWire AI Agents SWAIG functions and SWML generation locally with complete environment simulation and real API execution. + +## Overview + +The `swaig-test` CLI tool provides a complete testing environment for: +- **SWAIG Functions**: Both webhook and DataMap functions with automatic type detection +- **SWML Generation**: Static and dynamic agent SWML document testing with realistic fake data +- **Mock Requests**: Complete FastAPI Request simulation for dynamic agent testing + +The tool automatically detects function types, provides appropriate execution environments, and simulates the SignalWire platform locally while making real HTTP requests for DataMap functions. + +## Key Features + +- **`--exec` Syntax**: Modern CLI-style function arguments +- **Agent Auto-Selection**: Automatically chooses agent when only one exists in file +- **Agent Discovery**: Lists available agents when no arguments provided +- **Auto-Detection**: Automatically detects webhook vs DataMap functions - no manual flags needed +- **Complete DataMap Simulation**: Full processing including URL templates, responses, and fallbacks +- **SWML Testing**: Generate and test SWML documents with realistic fake call data +- **Dynamic Agent Support**: Test request-dependent SWML generation with mock request objects +- **Real HTTP Execution**: DataMap functions make actual HTTP requests to real APIs +- **Comprehensive Simulation**: Generate realistic post_data with all SignalWire metadata +- **Advanced Template Engine**: Supports all DataMap variable syntax (`${args.param}`, `${response.field}`, `${array[0].property}`) +- **Flexible CLI Syntax**: Support both `--exec` and JSON argument styles +- **Override System**: Precise control over test data with dot notation paths +- **Mock Request Objects**: Complete FastAPI Request simulation for dynamic agents +- **Verbose Debugging**: Detailed execution tracing for both function types +- **Flexible Data Modes**: Choose between minimal, comprehensive, or custom post_data +- **Serverless Environment Simulation**: Complete platform simulation for Lambda, CGI, Cloud Functions, and Azure Functions with environment variable management + +## Installation + +Install as part of the signalwire_agents package: + +```bash +pip install -e . +swaig-test --help +``` + +## Quick Start + +### Agent Discovery + +The tool can automatically discover agents in Python files: + +```bash +# Discover all agents in a file (auto-runs when no other args provided) +swaig-test examples/joke_skill_demo.py + +# Explicitly list available agents +swaig-test matti_and_sigmond/dual_agent_app.py --list-agents + +# List agents with details +swaig-test matti_and_sigmond/dual_agent_app.py --list-agents --verbose +``` + +**Example Output:** +``` +Available agents in matti_and_sigmond/dual_agent_app.py: + + MattiAgent + Type: Ready instance + Name: Matti + Route: /matti-agent + Description: Advanced agent with custom tools and weather integration + + SigmondAgent + Type: Ready instance + Name: Sigmond + Route: /sigmond-agent + Description: Advanced conversational agent with data access + +To use a specific agent with this tool: + swaig-test matti_and_sigmond/dual_agent_app.py [tool_name] [args] --agent-class + +Examples: + swaig-test matti_and_sigmond/dual_agent_app.py --list-tools --agent-class MattiAgent + swaig-test matti_and_sigmond/dual_agent_app.py --dump-swml --agent-class SigmondAgent +``` + +### List Available Functions + +```bash +# List functions in single-agent file (auto-selected) +swaig-test examples/joke_skill_demo.py --list-tools + +# List functions for specific agent in multi-agent file +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --list-tools + +# Detailed function listing with schemas +swaig-test examples/joke_skill_demo.py --list-tools --verbose +``` + +**Example Output:** +``` +Available SWAIG functions: + get_joke - Get a random joke from API Ninjas (DataMap function - serverless) + Parameters: + type (string) (required): Type of joke to get + Config: {"data_map": {...}, "parameters": {...}} + + calculate - Perform mathematical calculations and return the result (LOCAL webhook) + Parameters: + expression (string) (required): Mathematical expression to evaluate + precision (integer): Number of decimal places (default: 2) +``` + +### Test SWML Generation + +```bash +# Basic SWML generation with fake call data +swaig-test examples/my_agent.py --dump-swml + +# Raw SWML JSON output for piping +swaig-test examples/my_agent.py --dump-swml --raw | jq '.' + +# Verbose SWML testing with detailed fake data +swaig-test examples/my_agent.py --dump-swml --verbose + +# Custom call types and scenarios +swaig-test examples/my_agent.py --dump-swml --call-type sip --call-direction outbound + +# Test SWML in serverless environments +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml +swaig-test examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml +``` + +## Serverless Environment Simulation + +The CLI tool provides comprehensive serverless platform simulation, allowing you to test your agents in Lambda, CGI, Cloud Functions, and Azure Functions environments locally without deployment. + +### Quick Start with Serverless Simulation + +```bash +# Test agent in Lambda environment +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml + +# Test function execution in Lambda context +swaig-test examples/my_agent.py --simulate-serverless lambda --exec my_function --param value + +# Test with custom Lambda configuration +swaig-test examples/my_agent.py --simulate-serverless lambda --aws-function-name my-func --aws-region us-west-2 --exec my_function + +# Test CGI environment with custom host +swaig-test examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml + +# Test with environment variables +swaig-test examples/my_agent.py --simulate-serverless lambda --env DEBUG=1 --env TEST_MODE=cli --exec my_function +``` + +### Supported Serverless Platforms + +| Platform | Simulation Flag | Key Features | +|----------|-----------------|--------------| +| **AWS Lambda** | `--simulate-serverless lambda` | Function URLs, API Gateway, environment detection | +| **CGI** | `--simulate-serverless cgi` | HTTP host, script paths, HTTPS simulation | +| **Google Cloud Functions** | `--simulate-serverless cloud_function` | Function URLs, project configuration | +| **Azure Functions** | `--simulate-serverless azure_function` | Function URLs, environment settings | + +### Platform-Specific Configuration + +#### AWS Lambda Simulation + +```bash +# Default Lambda simulation with auto-generated URLs +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml + +# Custom Lambda function configuration +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --aws-function-name my-custom-function \ + --aws-function-url https://abc123.lambda-url.us-west-2.on.aws/ \ + --aws-region us-west-2 \ + --dump-swml + +# API Gateway configuration +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --aws-api-gateway-id abc123def \ + --aws-region us-east-1 \ + --aws-stage prod \ + --exec my_function --param value + +# Test function execution in Lambda context +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --exec get_weather --location "San Francisco" \ + --full-request +``` + +**Lambda Environment Variables Set:** +- `AWS_LAMBDA_FUNCTION_NAME` +- `AWS_LAMBDA_FUNCTION_URL` (if using Function URLs) +- `AWS_API_GATEWAY_ID` (if using API Gateway) +- `AWS_REGION` +- `_HANDLER` + +#### CGI Simulation + +```bash +# Basic CGI simulation +swaig-test examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml + +# Custom CGI configuration +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host my-server.com \ + --cgi-script-name /cgi-bin/my-agent.cgi \ + --cgi-https \ + --cgi-path-info /custom/path \ + --exec my_function --param value + +# Test CGI with specific environment +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host production.example.com \ + --cgi-https \ + --env REMOTE_USER=admin \ + --dump-swml +``` + +**CGI Environment Variables Set:** +- `GATEWAY_INTERFACE=CGI/1.1` +- `HTTP_HOST` (from --cgi-host) +- `SCRIPT_NAME` (from --cgi-script-name) +- `HTTPS=on` (if --cgi-https) +- `PATH_INFO` (from --cgi-path-info) + +#### Google Cloud Functions Simulation + +```bash +# Basic Cloud Function simulation +swaig-test examples/my_agent.py --simulate-serverless cloud_function --dump-swml + +# Custom GCP configuration +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project my-project \ + --gcp-function-url https://my-function-abc123.cloudfunctions.net \ + --gcp-region us-central1 \ + --gcp-service my-service \ + --exec my_function --param value +``` + +**Cloud Function Environment Variables Set:** +- `GOOGLE_CLOUD_PROJECT` +- `FUNCTION_URL` (if provided) +- `GOOGLE_CLOUD_REGION` +- `K_SERVICE` (Knative service name) + +#### Azure Functions Simulation + +```bash +# Basic Azure Functions simulation +swaig-test examples/my_agent.py --simulate-serverless azure_function --dump-swml + +# Custom Azure configuration +swaig-test examples/my_agent.py --simulate-serverless azure_function \ + --azure-env production \ + --azure-function-url https://my-function.azurewebsites.net \ + --exec my_function --param value +``` + +**Azure Functions Environment Variables Set:** +- `AZURE_FUNCTIONS_ENVIRONMENT` +- `WEBSITE_SITE_NAME` +- Custom function URL (if provided) + +### Environment Variable Management + +#### Manual Environment Variables + +```bash +# Set custom environment variables +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env API_KEY=secret123 \ + --env DEBUG=true \ + --env TIMEOUT=30 \ + --exec my_function + +# Multiple environment variables +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --env DB_HOST=localhost \ + --env DB_PORT=5432 \ + --env LOG_LEVEL=info \ + --cgi-host example.com \ + --dump-swml +``` + +#### Environment Files + +Create environment files for reusable configurations: + +```bash +# Create environment file +cat > lambda.env << EOF +AWS_LAMBDA_FUNCTION_NAME=my-production-function +AWS_REGION=us-west-2 +API_KEY=prod_key_123 +DEBUG=false +TIMEOUT=60 +EOF + +# Use environment file +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env-file lambda.env \ + --exec my_function --param value + +# Override specific variables from file +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env-file lambda.env \ + --env DEBUG=true \ + --env AWS_REGION=us-east-1 \ + --dump-swml +``` + +### Webhook URL Generation + +The serverless simulation automatically generates appropriate webhook URLs for each platform: + +#### Platform-Specific URLs + +| Platform | Example Webhook URL | +|----------|-------------------| +| **Lambda (Function URL)** | `https://abc123.lambda-url.us-east-1.on.aws/swaig/` | +| **Lambda (API Gateway)** | `https://api123.execute-api.us-east-1.amazonaws.com/prod/swaig/` | +| **CGI** | `https://example.com/cgi-bin/agent.cgi/swaig/` | +| **Cloud Functions** | `https://my-function-abc123.cloudfunctions.net/swaig/` | +| **Azure Functions** | `https://my-function.azurewebsites.net/swaig/` | + +#### URL Generation Examples + +```bash +# Lambda Function URL +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --aws-function-url https://custom123.lambda-url.us-west-2.on.aws/ \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' + +# CGI with custom host +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host my-production-server.com \ + --cgi-https \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' + +# Cloud Functions with custom URL +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-function-url https://my-custom-function.cloudfunctions.net \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' +``` + +### Function Execution in Serverless Context + +Test function execution with platform-specific request/response formats: + +#### Lambda Function Execution + +```bash +# Test function in Lambda context +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --exec get_weather --location "Miami" \ + --full-request + +# Example output shows Lambda event format +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --exec calculate --expression "2+2" \ + --full-request --format-json +``` + +**Lambda Response Format:** +```json +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"result\": 4, \"expression\": \"2+2\"}" +} +``` + +#### CGI Function Execution + +```bash +# Test function in CGI context +swaig-test examples/my_agent.py --simulate-serverless cgi \ + --cgi-host example.com \ + --exec my_function --param value +``` + +### Advanced Serverless Features + +#### Environment Presets + +The tool includes built-in environment presets for each platform: + +```bash +# Use default Lambda preset +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml + +# Override preset values +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --aws-function-name custom-name \ + --env CUSTOM_VAR=value \ + --dump-swml +``` + +#### Environment Conflict Resolution + +The tool automatically clears conflicting environment variables between platforms: + +```bash +# Switching platforms clears previous environment +export AWS_LAMBDA_FUNCTION_NAME=old-function + +# This will clear AWS variables and set GCP variables +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project new-project \ + --dump-swml +``` + +#### Testing Multiple Platforms + +```bash +# Test the same agent across multiple platforms +for platform in lambda cgi cloud_function azure_function; do + echo "Testing $platform..." + swaig-test examples/my_agent.py --simulate-serverless $platform \ + --exec my_function --param value +done + +# Compare SWML generation across platforms +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml > lambda.swml +swaig-test examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml > cgi.swml +diff lambda.swml cgi.swml +``` + +### Debugging Serverless Simulation + +#### Verbose Mode + +```bash +# See detailed environment setup +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --verbose \ + --dump-swml + +# Debug function execution +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --verbose \ + --exec my_function --param value \ + --full-request +``` + +#### Environment Inspection + +```bash +# Show environment variables being set +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env DEBUG=1 \ + --exec get_status # Use a function that returns environment info +``` + +#### Format Options + +```bash +# Pretty-print JSON output +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --dump-swml --format-json + +# Raw JSON for piping +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.functions[0]' +``` + +### Serverless Best Practices + +#### Development Workflow + +1. **Start with local testing**: Test your agent normally first +2. **Test each platform**: Use serverless simulation for target platforms +3. **Verify webhook URLs**: Check that URLs are generated correctly for your platform +4. **Test environment variables**: Ensure your agent works with platform-specific variables +5. **Test function execution**: Verify functions work in serverless context + +#### Platform-Specific Testing + +```bash +# Lambda development workflow +swaig-test examples/my_agent.py --list-tools # First test locally +swaig-test examples/my_agent.py --simulate-serverless lambda --dump-swml # Check SWML +swaig-test examples/my_agent.py --simulate-serverless lambda --exec my_function --param value # Test functions + +# Production-like testing +swaig-test examples/my_agent.py --simulate-serverless lambda \ + --env-file production.env \ + --aws-function-name prod-my-agent \ + --aws-region us-east-1 \ + --exec critical_function --input "test" +``` + +#### Environment Management + +```bash +# Development environment +cat > dev.env << EOF +DEBUG=true +LOG_LEVEL=debug +API_TIMEOUT=10 +EOF + +# Production environment +cat > prod.env << EOF +DEBUG=false +LOG_LEVEL=info +API_TIMEOUT=30 +EOF + +# Test both environments +swaig-test examples/my_agent.py --simulate-serverless lambda --env-file dev.env --exec my_function +swaig-test examples/my_agent.py --simulate-serverless lambda --env-file prod.env --exec my_function +``` + +### Legacy Compatibility + +The tool maintains backward compatibility with existing serverless parameters: + +```bash +# Legacy syntax (still supported) +swaig-test examples/my_agent.py --serverless-mode lambda --function my_function --args '{"param":"value"}' + +# New syntax (recommended) +swaig-test examples/my_agent.py --simulate-serverless lambda --exec my_function --param value +``` + +### CLI Syntax with --exec + +The `--exec` syntax provides an intuitive way to test functions: + +```bash +# --exec syntax (recommended) - CLI flags BEFORE --exec +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type dadjokes +swaig-test examples/web_search_agent.py --exec web_search --query "AI agents" --limit 5 +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --exec get_weather --location "New York" + +# Multiple agents - specify which one to use +swaig-test matti_and_sigmond/dual_agent_app.py --verbose --agent-class SigmondAgent --exec search_knowledge --query "SignalWire" + +# Auto-agent selection (when only one agent in file) +swaig-test examples/joke_skill_demo.py --exec get_joke --type jokes + +# All CLI flags must come BEFORE --exec +swaig-test examples/agent.py --verbose --custom-data '{"test":"data"}' --exec my_function --param value +``` + +### JSON Syntax (Alternative) + +```bash +# JSON syntax (alternative approach) +swaig-test examples/joke_skill_demo.py get_joke '{"type":"dadjokes"}' +swaig-test examples/web_search_agent.py web_search '{"query":"AI agents","limit":5}' +``` + +## CLI Argument Syntax + +### Using --exec (Recommended) + +The `--exec` syntax separates CLI flags from function arguments: + +```bash +# Basic usage +swaig-test [--cli-flags] --exec [--function-args] + +# All CLI flags must come BEFORE --exec +swaig-test examples/agent.py --verbose --agent-class MyAgent --exec search --query "test" --limit 10 + +# Function arguments come AFTER --exec function-name +swaig-test examples/joke_skill_demo.py --exec get_joke --type dadjokes +# ^^^^ ^^^^^^^^ +# Wrong place! Wrong place! + +# Correct usage - CLI flags before --exec +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type dadjokes +# ^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^ +# CLI flag Function Function args +``` + +### Argument Type Handling + +The tool automatically converts arguments based on function schema: + +| Schema Type | CLI Input | Converted Value | +|-------------|-----------|-----------------| +| `string` | `--name "value"` | `"value"` | +| `integer` | `--count 42` | `42` | +| `number` | `--price 19.99` | `19.99` | +| `boolean` | `--verbose` or `--verbose true` | `true` | +| `array` | `--tags "tag1,tag2,tag3"` | `["tag1","tag2","tag3"]` | + +### CLI Syntax Examples + +```bash +# String parameters +swaig-test examples/agent.py --exec greet --name "Alice" + +# Multiple parameters with type conversion +swaig-test examples/agent.py --exec search --query "AI" --limit 5 --include-metadata + +# Boolean flags +swaig-test examples/agent.py --exec process --input "data" --verify --async false + +# Array parameters (comma-separated) +swaig-test examples/agent.py --exec filter --categories "tech,science,health" --max-results 20 + +# Complex example with multiple agent support +swaig-test matti_and_sigmond/dual_agent_app.py --verbose --agent-class SigmondAgent --exec get_trivia --category science +``` + +## Multi-Agent Support + +### Agent Auto-Selection + +When a file contains only one agent, it's automatically selected: + +```bash +# Auto-selects the single agent +swaig-test examples/joke_skill_demo.py --exec get_joke --type jokes +``` + +### Multi-Agent Files + +For files with multiple agents, specify which one to use: + +```bash +# Discover available agents +swaig-test matti_and_sigmond/dual_agent_app.py + +# Use specific agent +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --exec get_weather --location "San Francisco" +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --exec search_knowledge --query "AI" + +# Different operations with different agents +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --list-tools +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --dump-swml +``` + +## DataMap Function Testing + +### Automatic DataMap Detection + +DataMap functions are automatically detected and properly simulated: + +```bash +# DataMap function - automatically detected and simulated +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type dadjokes +``` + +**Complete DataMap Processing Pipeline:** +1. **URL Template Expansion**: `${args.type}` → `dadjokes` +2. **HTTP Request**: GET to `https://api.api-ninjas.com/v1/dadjokes` +3. **Response Processing**: Extract joke from API response array +4. **Output Template**: `${array[0].joke}` → actual joke text +5. **Fallback Handling**: If API fails, use fallback message + +**Example Output:** +``` +Executing DataMap function: get_joke +Arguments: {"type": "dadjokes"} +------------------------------------------------------------ +Simple DataMap structure with 1 webhook(s) +Processing 1 webhook(s)... + Webhook 1: GET https://api.api-ninjas.com/v1/${args.type} + Original URL: https://api.api-ninjas.com/v1/${args.type} + Template context: {'args': {'type': 'dadjokes'}, 'array': [], 'type': 'dadjokes'} + Expanded URL: https://api.api-ninjas.com/v1/dadjokes + ✓ Webhook succeeded: 200 + Response: [{"joke": "Why don't scientists trust atoms? Because they make up everything!"}] + Processed output: {'response': "Here's a joke: Why don't scientists trust atoms? Because they make up everything!"} + +RESULT: +Response: Here's a joke: Why don't scientists trust atoms? Because they make up everything! +``` + +### DataMap Template Expansion + +The tool properly handles all DataMap template syntax: + +- **Function Arguments**: `${args.type}`, `${args.location}` +- **Array Access**: `${array[0].joke}`, `${array[0].weather.temp}` +- **Nested Objects**: `${response.data.results[0].title}` +- **Fallback Values**: `${args.units || "metric"}` + +### DataMap Error Handling + +When APIs fail, DataMap functions gracefully fall back: + +```bash +# Test with invalid parameters to see fallback +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type invalid +``` + +**Fallback Output:** +``` + ✗ Webhook failed: 404 +All webhooks failed, using fallback output... +Fallback result = {'response': 'Sorry, there is a problem with the joke service right now. Please try again later.'} + +RESULT: +Response: Sorry, there is a problem with the joke service right now. Please try again later. +``` + +### Test Functions (Auto-Detection) + +The tool automatically detects whether a function is a local webhook, external webhook, or DataMap function: + +```bash +# Test local webhook function - auto-detected +swaig-test examples/datasphere_webhook_env_demo.py search_knowledge '{"query":"SignalWire"}' + +# Test DataMap function - auto-detected +swaig-test examples/datasphere_serverless_env_demo.py search_knowledge '{"query":"SignalWire"}' + +# Test external webhook function - auto-detected +swaig-test examples/external_webhook_weather_agent.py getWeather '{"location":"New York"}' --verbose + +# Test math skill function - auto-detected +swaig-test examples/datasphere_serverless_env_demo.py calculate '{"expression":"25 * 47"}' +``` + +#### External Webhook Function Testing + +External webhook functions are automatically detected and tested by making HTTP requests to the external service URL: + +```bash +# Test external webhook with verbose output +swaig-test examples/external_webhook_weather_agent.py getWeather '{"location":"San Francisco"}' --verbose + +# List functions with their types (local vs external) +swaig-test examples/external_webhook_weather_agent.py --list-tools +``` + +**Example Output for External Webhook:** +``` +Available SWAIG functions: + getHelp - Get help information about using the weather service (LOCAL webhook) + getWeather - Get current weather information for a specific location (EXTERNAL webhook) + External URL: https://api.example-weather-service.com/webhook + +Calling EXTERNAL webhook: getWeather +URL: https://api.example-weather-service.com/webhook +Arguments: {"location": "San Francisco"} + +Sending payload: { + "function": "getWeather", + "argument": { + "parsed": [{"location": "San Francisco"}], + "raw": "{\"location\": \"San Francisco\"}" + }, + "call_id": "test-call-123" +} +Making POST request to: https://api.example-weather-service.com/webhook +Response status: 200 +✓ External webhook succeeded +``` + +**How External Webhook Testing Works:** + +1. **Detection**: The CLI tool detects functions with `webhook_url` parameters as external webhooks +2. **HTTP Request**: Instead of calling the local function, it makes an HTTP POST to the external URL +3. **Payload Format**: Sends the same JSON payload that SignalWire would send: + ```json + { + "function": "function_name", + "argument": { + "parsed": [{"param": "value"}], + "raw": "{\"param\": \"value\"}" + }, + "call_id": "generated-call-id", + "call": { /* call information */ }, + "vars": { /* call variables */ } + } + ``` +4. **Response Handling**: Processes the HTTP response and displays the result +5. **Error Handling**: Shows connection errors, timeouts, and HTTP error responses + +**Testing Mixed Function Types:** + +You can test agents that have both local and external webhook functions: + +```bash +# Test local function +swaig-test examples/external_webhook_weather_agent.py getHelp '{}' + +# Test external function +swaig-test examples/external_webhook_weather_agent.py getWeather '{"location":"Tokyo"}' + +# Show all function types +swaig-test examples/external_webhook_weather_agent.py --list-tools +``` + +## SWML Generation and Testing + +### Realistic SWML Post Data + +The tool automatically generates realistic fake SWML post_data that matches SignalWire's structure: + +```bash +# Generate SWML with fake call data +swaig-test examples/my_agent.py --dump-swml --verbose +``` + +**Generated fake post_data structure:** +```json +{ + "call": { + "call_id": "550e8400-e29b-41d4-a716-446655440000", + "node_id": "test-node-a1b2c3d4", + "segment_id": "550e8400-e29b-41d4-a716-446655440001", + "call_session_id": "550e8400-e29b-41d4-a716-446655440002", + "tag": "550e8400-e29b-41d4-a716-446655440000", + "state": "created", + "direction": "inbound", + "type": "webrtc", + "from": "user-a1b2c3d4@test.domain", + "to": "agent-e5f6g7h8@test.domain", + "project_id": "550e8400-e29b-41d4-a716-446655440003", + "space_id": "550e8400-e29b-41d4-a716-446655440004" + }, + "vars": { + "userVariables": {} + }, + "envs": {} +} +``` + +### Call Type Simulation + +Support for different call types with appropriate metadata: + +```bash +# WebRTC call (default) +swaig-test examples/agent.py --dump-swml --call-type webrtc + +# SIP call with phone numbers +swaig-test examples/agent.py --dump-swml --call-type sip +``` + +**SIP vs WebRTC differences:** +- **SIP**: Uses phone numbers (+15551234567), includes SIP headers +- **WebRTC**: Uses domain addresses (user@domain), includes WebRTC headers + +### SWML Testing Options + +| Option | Description | Example | +|--------|-------------|---------| +| `--dump-swml` | Generate SWML document with fake call data | `--dump-swml` | +| `--raw` | Output raw JSON only (pipeable) | `--dump-swml --raw \| jq '.'` | +| `--call-type` | SIP or WebRTC call simulation | `--call-type sip` | +| `--call-direction` | Inbound or outbound call | `--call-direction outbound` | +| `--call-state` | Call state (created, answered, etc.) | `--call-state answered` | +| `--call-id` | Override call_id | `--call-id my-test-call` | +| `--project-id` | Override project_id | `--project-id my-project` | +| `--space-id` | Override space_id | `--space-id my-space` | +| `--from-number` | Override from address | `--from-number +15551234567` | +| `--to-extension` | Override to address | `--to-extension +15559876543` | + +### Data Override System + +Precise control over fake data using dot notation paths: + +```bash +# Simple value overrides +swaig-test examples/agent.py --dump-swml --override call.state=answered --override call.timeout=60 + +# JSON overrides for complex data +swaig-test examples/agent.py --dump-swml --override-json vars.userVariables='{"vip":true,"tier":"gold"}' + +# User variables and environment variables +swaig-test examples/agent.py --dump-swml --user-vars '{"customer_id":"12345","tier":"premium"}' + +# Query parameters (merged into userVariables) +swaig-test examples/agent.py --dump-swml --query-params '{"source":"api","debug":"true"}' +``` + +**Override Examples:** +```bash +# Multiple overrides +swaig-test examples/agent.py --dump-swml \ + --override call.project_id=my-project \ + --override call.direction=outbound \ + --override call.state=answered \ + --user-vars '{"vip_customer":true}' + +# Complex JSON overrides +swaig-test examples/agent.py --dump-swml \ + --override-json call.headers='{"X-Custom":"value"}' \ + --override-json vars.userVariables='{"settings":{"theme":"dark","lang":"en"}}' +``` + +### Dynamic Agent Testing + +Test dynamic agents that generate request-dependent SWML: + +```bash +# Basic dynamic agent testing +swaig-test examples/dynamic_agent.py --dump-swml --header "Authorization=Bearer token" + +# Custom request simulation +swaig-test examples/dynamic_agent.py --dump-swml \ + --method GET \ + --header "X-Source=api" \ + --header "Content-Type=application/json" \ + --query-params '{"source":"test","version":"v2"}' \ + --body '{"custom_data":"test"}' + +# Combined dynamic testing +swaig-test examples/dynamic_agent.py --dump-swml \ + --call-type sip \ + --call-direction outbound \ + --header "X-Call-Source=external" \ + --user-vars '{"priority":"high"}' \ + --verbose +``` + +**Mock Request Features:** +- **Headers**: Case-insensitive HTTP headers +- **Query Parameters**: Case-sensitive query parameters +- **Request Body**: JSON request body +- **HTTP Method**: GET, POST, PUT, etc. +- **URL**: Full URL with query string +- **Async Methods**: Compatible with FastAPI Request interface + +### SWML Output Formats + +```bash +# Standard output with headers +swaig-test examples/agent.py --dump-swml +# Output: Headers + formatted SWML + footers + +# Raw JSON for automation +swaig-test examples/agent.py --dump-swml --raw +# Output: Raw JSON only + +# Pipe to jq for processing +swaig-test examples/agent.py --dump-swml --raw | jq '.sections.main[1].ai.SWAIG.functions' + +# Verbose with fake data details +swaig-test examples/agent.py --dump-swml --verbose +# Output: Fake data details + agent info + SWML +``` + +## Alternative CLI Argument Syntax + +### Using --args Separator + +Instead of JSON strings, use CLI-style arguments: + +```bash +# Traditional JSON syntax +swaig-test examples/agent.py search_function '{"query":"test","limit":10,"verbose":true}' + +# Alternative CLI syntax +swaig-test examples/agent.py search_function --args --query "test" --limit 10 --verbose + +# Schema-based type conversion +swaig-test examples/agent.py calculate --args --expression "25 * 47" --precision 2 +``` + +### Argument Type Handling + +The tool automatically converts arguments based on function schema: + +| Schema Type | CLI Input | Converted Value | +|-------------|-----------|-----------------| +| `string` | `--name "value"` | `"value"` | +| `integer` | `--count 42` | `42` | +| `number` | `--price 19.99` | `19.99` | +| `boolean` | `--verbose` or `--verbose true` | `true` | +| `array` | `--tags "tag1,tag2,tag3"` | `["tag1","tag2","tag3"]` | + +### CLI Syntax Examples + +```bash +# Simple string parameter +swaig-test examples/agent.py greet --args --name "Alice" + +# Multiple parameters with type conversion +swaig-test examples/agent.py search --args --query "AI" --limit 5 --include-metadata + +# Boolean flags +swaig-test examples/agent.py process --args --input "data" --verify --async false + +# Array parameters (comma-separated) +swaig-test examples/agent.py filter --args --categories "tech,science,health" --max-results 20 +``` + +## DataMap Function Execution + +### Complete Processing Pipeline + +DataMap functions follow the SignalWire server-side processing pipeline: + +1. **Expression Processing**: Pattern matching against function arguments +2. **Webhook Execution**: Sequential HTTP requests until one succeeds +3. **Foreach Processing**: Array iteration with template expansion +4. **Output Generation**: Final result formatting using templates +5. **Fallback Handling**: Error recovery with fallback outputs + +### Real API Execution Example + +```bash +# Test DataSphere serverless search with verbose output +swaig-test examples/datasphere_serverless_env_demo.py search_knowledge '{"query":"SignalWire"}' --verbose +``` + +**Example Execution Flow:** +``` +=== DataMap Function Execution === +Config: { ... complete datamap configuration ... } + +--- Processing Webhooks --- +=== Webhook 1/1 === +Making POST request to: https://tony.signalwire.com/api/datasphere/documents/search +Headers: { + "Content-Type": "application/json", + "Authorization": "Basic ODQ2NTlmMjE..." +} +Request data: { + "document_id": "b888a1cc-1707-4902-9573-aa201a0c1086", + "query_string": "SignalWire", + "distance": "4.0", + "count": "1" +} +Response status: 200 +Webhook 1 succeeded! + +--- Processing Webhook Foreach --- +Found array data in response.chunks: 1 items +Processed 1 items +Foreach result (formatted_results): === RESULT === +SignalWire's competitive advantage comes from... + +--- Processing Webhook Output --- +Set response = I found results for "SignalWire": + +=== RESULT === +SignalWire's competitive advantage comes from... + +RESULT: +Response: I found results for "SignalWire": ... +``` + +### Template Expansion Support + +The tool supports all DataMap template syntax with both `${}` and `%{}` variations: + +| Syntax | Description | Example | +|--------|-------------|---------| +| `${args.param}` / `%{args.param}` | Function arguments | `${args.query}`, `%{args.type}` | +| `${response.field}` / `%{response.field}` | API response object | `${response.temperature}` | +| `${array[0].field}` / `%{array[0].field}` | API response array | `${array[0].joke}`, `%{array[0].text}` | +| `${this.property}` / `%{this.property}` | Current foreach item | `${this.title}`, `%{this.content}` | +| `${global_data.key}` / `%{global_data.key}` | Call-wide data store | `${global_data.customer_name}` | + +**Array Response Handling**: When a webhook returns a nameless array (like `[{"joke": "..."}]`), it's automatically stored as the `array` key, making it accessible via `${array[0].property}` syntax. + +**Template Expansion Examples:** +```json +{ + "url": "https://api.example.com/v1/%{args.type}", + "output": { + "response": "Here's a joke: ${array[0].joke}" + } +} +``` + +### Foreach Processing + +DataMap foreach loops concatenate strings from array elements: + +```json +{ + "foreach": { + "input_key": "chunks", + "output_key": "formatted_results", + "max": 3, + "append": "=== RESULT ===\n${this.text}\n====================\n\n" + } +} +``` + +This processes each item in `response.chunks` and builds a single concatenated string in `formatted_results`. + +## Webhook Function Testing + +The CLI tool supports testing three types of webhook functions: + +1. **Local Webhook Functions**: Traditional SWAIG functions handled by your local agent +2. **External Webhook Functions**: Functions that delegate to external HTTP services +3. **DataMap Functions**: Server-side functions that don't require local webhook infrastructure + +### External Webhook Functions + +External webhook functions are automatically detected when a function has a `webhook_url` parameter and are tested by making HTTP requests to the external service: + +```python +@AgentBase.tool( + name="get_weather", + description="Get weather from external service", + parameters={"location": {"type": "string"}}, + webhook_url="https://weather-api.example.com/current" +) +def get_weather_external(self, args, raw_data): + # This function body is never called for external webhooks + pass +``` + +**Testing External Webhooks:** + +```bash +# Test external webhook function +swaig-test examples/external_webhook_weather_agent.py getWeather '{"location":"Paris"}' --verbose + +# Compare with local function +swaig-test examples/external_webhook_weather_agent.py getHelp '{}' --verbose +``` + +**External Webhook Request Format:** + +The CLI tool sends the same payload format that SignalWire uses: + +```json +{ + "function": "getWeather", + "argument": { + "parsed": [{"location": "Paris"}], + "raw": "{\"location\": \"Paris\"}" + }, + "call_id": "test-call-uuid", + "call": { + "call_id": "test-call-uuid", + "project_id": "project-uuid", + "space_id": "space-uuid" + }, + "vars": { + "userVariables": {} + } +} +``` + +**External Webhook Error Handling:** + +```bash +# Test with unreachable external service +swaig-test examples/external_webhook_weather_agent.py testBrokenWebhook '{"message":"test"}' --verbose +``` + +Output shows connection errors and HTTP status codes: +``` +✗ Could not connect to external webhook: HTTPSConnectionPool(host='nonexistent.example.com', port=443) +RESULT: +Dict: { + "error": "Could not connect to external webhook: ...", + "status_code": null +} +``` + +### Post Data Simulation Modes + +#### 1. Default Mode (Minimal Data) +```bash +swaig-test my_agent.py my_function '{"param":"value"}' +``` +**Includes**: `function`, `argument`, `call_id`, `meta_data`, `global_data` + +#### 2. Comprehensive Mode (Full SignalWire Environment) +```bash +swaig-test my_agent.py my_function '{"param":"value"}' --fake-full-data +``` + +**Includes complete post_data with all SignalWire keys:** +- **Core identification**: `function`, `argument`, `call_id`, `call_session_id`, `node_id` +- **Metadata**: `meta_data_token`, `meta_data` (function-level shared data) +- **Global data**: `global_data` (agent configuration and state) +- **Conversation context**: `call_log`, `raw_call_log` (OpenAI conversation format) +- **SWML variables**: `prompt_vars` (includes SWML vars + global_data keys) +- **Permissions**: `swaig_allow_swml`, `swaig_post_conversation`, `swaig_post_swml_vars` +- **HTTP context**: `http_method`, `webhook_url`, `user_agent`, `request_headers` + +#### 3. Custom Data Mode +```bash +swaig-test my_agent.py my_function '{"param":"value"}' --custom-data '{"call_id":"test-123","global_data":{"environment":"production"}}' +``` + +### Comprehensive Post Data Example + +```json +{ + "function": "search_knowledge", + "argument": {"query": "SignalWire"}, + "call_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "call_session_id": "session-uuid", + "node_id": "test-node-001", + "meta_data_token": "func_hash_token", + "meta_data": { + "test_mode": true, + "function_name": "search_knowledge" + }, + "global_data": { + "app_name": "test_application", + "environment": "test", + "user_preferences": {"language": "en"} + }, + "call_log": [ + { + "role": "system", + "content": "You are a helpful AI assistant..." + }, + { + "role": "user", + "content": "Please call the search_knowledge function" + }, + { + "role": "assistant", + "content": "I'll call the search_knowledge function for you.", + "tool_calls": [ + { + "id": "call_12345678", + "type": "function", + "function": { + "name": "search_knowledge", + "arguments": "{\"query\":\"SignalWire\"}" + } + } + ] + } + ], + "raw_call_log": "... complete conversation history ...", + "prompt_vars": { + "ai_instructions": "You are a helpful assistant", + "temperature": 0.7, + "app_name": "test_application", + "current_timestamp": "2024-01-15T10:30:00Z" + }, + "swaig_allow_swml": true, + "swaig_post_conversation": true, + "swaig_post_swml_vars": true +} +``` + +### DataSphere Knowledge Search + +```bash +# Test DataSphere serverless function +swaig-test examples/datasphere_serverless_env_demo.py search_knowledge '{"query":"AI agents"}' --verbose +``` + +**Expected Output:** +``` +Executing DataMap function: search_knowledge +=== DataMap Function Execution === + +--- Processing Webhooks --- +Making POST request to: https://tony.signalwire.com/api/datasphere/documents/search +Response status: 200 +Webhook 1 succeeded! + +--- Processing Webhook Foreach --- +Found array data in response.chunks: 1 items +Processed 1 items + +--- Processing Webhook Output --- +Set response = I found results for "AI agents": + +=== RESULT === +[Actual knowledge base content about AI agents...] + +RESULT: +Response: I found results for "AI agents": ... +``` + +### Math Skill Function + +```bash +# Test webhook-style math function +swaig-test examples/datasphere_serverless_env_demo.py calculate '{"expression":"25 * 47"}' --verbose +``` + +**Expected Output:** +``` +Calling webhook function: calculate +Arguments: {"expression": "25 * 47"} +Function description: Perform mathematical calculations and return the result + +RESULT: +SwaigFunctionResult: The result of 25 * 47 is 1175. +``` + +### DateTime Skill Function + +```bash +# Test datetime function with comprehensive data +swaig-test examples/datasphere_serverless_env_demo.py get_datetime '{}' --fake-full-data +``` + +## Function Type Detection + +The tool automatically detects function types: + +- **DataMap Functions**: Stored as `dict` objects with `data_map` configuration +- **Webhook Functions**: Stored as `SWAIGFunction` objects with description and handler +- **Skill Functions**: Detected from loaded skills + +**Detection Example:** +```bash +swaig-test my_agent.py --list-tools --verbose + +Available SWAIG functions: + search_knowledge - DataMap function (serverless) + Config: {"webhooks": [...], "output": {...}} + calculate - Perform mathematical calculations and return the result + Function: +``` + +## Command Line Options + +### Core Options + +| Option | Description | +|--------|-------------| +| `--exec FUNCTION` | Execute function with CLI-style arguments (recommended) | +| `--agent-class CLASS` | Specify agent class for multi-agent files | +| `--list-agents` | List all available agents in the file | +| `--list-tools` | List all available SWAIG functions and their types | +| `--verbose`, `-v` | Enable detailed execution tracing and debugging | +| `--fake-full-data` | Generate comprehensive post_data with all SignalWire metadata | +| `--minimal` | Use minimal post_data (essential keys only) | +| `--custom-data` | JSON string with custom post_data overrides | + +### SWML Testing Options + +| Option | Description | +|--------|-------------| +| `--dump-swml` | Generate SWML document with fake call data | +| `--raw` | Output raw JSON only (no headers, pipeable) | +| `--call-type` | Call type: `sip` or `webrtc` (default: webrtc) | +| `--call-direction` | Call direction: `inbound` or `outbound` (default: inbound) | +| `--call-state` | Call state (default: created) | +| `--call-id` | Override call_id | +| `--project-id` | Override project_id | +| `--space-id` | Override space_id | +| `--from-number` | Override from address | +| `--to-extension` | Override to address | + +### Data Override Options + +| Option | Description | +|--------|-------------| +| `--user-vars` | JSON for vars.userVariables | +| `--query-params` | JSON for query parameters (merged into userVariables) | +| `--override` | Override values using dot notation (repeatable) | +| `--override-json` | Override with JSON values using dot notation (repeatable) | + +### Mock Request Options + +| Option | Description | +|--------|-------------| +| `--header` | Add HTTP headers for mock request (repeatable) | +| `--method` | HTTP method for mock request (default: POST) | +| `--body` | JSON string for mock request body | + +### Alternative Syntax + +| Option | Description | +|--------|-------------| +| `--args` | Separator for CLI-style function arguments | + +## Real-World Examples + +### Testing Joke Skill (DataMap) + +```bash +# Test dad jokes with verbose output +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type dadjokes + +# Test regular jokes +swaig-test examples/joke_skill_demo.py --exec get_joke --type jokes + +# Test error handling with invalid type +swaig-test examples/joke_skill_demo.py --verbose --exec get_joke --type invalid +``` + +### Testing Multi-Agent Applications + +```bash +# Discover available agents +swaig-test matti_and_sigmond/dual_agent_app.py + +# Test MattiAgent functions +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --list-tools +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --exec get_weather --location "Tokyo" +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --exec transfer --name "support" + +# Test SigmondAgent functions +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --list-tools +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --exec search_knowledge --query "SignalWire" +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --exec get_joke --type dadjokes + +# Generate SWML for different agents +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --dump-swml +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class SigmondAgent --dump-swml --raw | jq '.' +``` + +### Testing External Webhook Functions + +```bash +# Test external webhook with verbose output +swaig-test examples/external_webhook_weather_agent.py --verbose --exec getWeather --location "San Francisco" + +# List functions with their types (local vs external) +swaig-test examples/external_webhook_weather_agent.py --list-tools +``` + +### Advanced SWML Testing + +```bash +# Test dynamic agent with custom headers and data +swaig-test examples/dynamic_agent.py --dump-swml \ + --header "Authorization=Bearer test-token" \ + --header "X-User-ID=12345" \ + --method POST \ + --body '{"source":"api","environment":"test"}' \ + --user-vars '{"customer_tier":"premium"}' \ + --verbose + +# Test SIP vs WebRTC calls +swaig-test examples/agent.py --dump-swml --call-type sip --from-number "+15551234567" +swaig-test examples/agent.py --dump-swml --call-type webrtc --from-number "user@domain.com" + +# Test with multi-agent file +swaig-test matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --dump-swml --call-type sip --verbose +``` + +### SWML Generation Examples + +#### Basic Static Agent SWML + +```bash +# Generate SWML for static agent +swaig-test examples/simple_agent.py --dump-swml +``` + +**Expected Output:** +``` +Generating SWML document... +Agent: Simple Agent +Route: /swml + +SWML Document: +================================================== +{"version":"1.0","sections":{"main":[{"ai":{"SWAIG":{"functions":[...]}}}]}} +================================================== +``` + +#### Dynamic Agent with Mock Request + +```bash +# Test dynamic agent with custom headers and data +swaig-test examples/dynamic_agent.py --dump-swml \ + --header "Authorization=Bearer test-token" \ + --header "X-User-ID=12345" \ + --method POST \ + --body '{"source":"api","environment":"test"}' \ + --user-vars '{"customer_tier":"premium"}' \ + --verbose +``` + +**Expected Output:** +``` +Generating SWML document... +Agent: Dynamic Agent +Route: /swml + +Using fake SWML post_data: +{ + "call": { + "call_id": "550e8400-e29b-41d4-a716-446655440000", + ... + }, + "vars": { + "userVariables": {"customer_tier": "premium"} + } +} + +Mock request headers: {"authorization": "Bearer test-token", "x-user-id": "12345"} +Mock request method: POST +Dynamic agent modifications: {"ai_instructions": "Custom instructions for premium user"} + +SWML Document: +================================================== +{"version":"1.0","sections":{"main":[{"ai":{"SWAIG":{"functions":[...]},"params":{"ai_instructions":"Custom instructions for premium user"}}}]}} +================================================== +``` + +#### Call Type Differentiation + +```bash +# SIP call scenario +swaig-test examples/agent.py --dump-swml \ + --call-type sip \ + --call-direction outbound \ + --from-number "+15551234567" \ + --to-extension "+15559876543" \ + --verbose + +# WebRTC call scenario +swaig-test examples/agent.py --dump-swml \ + --call-type webrtc \ + --call-direction inbound \ + --from-number "customer@company.com" \ + --to-extension "support@myagent.com" \ + --header "Origin=https://company.com" \ + --verbose +``` + +#### Advanced Override Scenarios + +```bash +# Complex call state testing +swaig-test examples/agent.py --dump-swml \ + --call-state answered \ + --override call.timeout=120 \ + --override call.max_duration=7200 \ + --override-json call.record='{"enabled":true,"format":"mp3"}' \ + --user-vars '{"call_reason":"support","priority":"high","customer_id":"CUST-12345"}' \ + --verbose + +# Multi-environment testing +swaig-test examples/agent.py --dump-swml \ + --override call.project_id=prod-project-123 \ + --override call.space_id=prod-space-456 \ + --override-json vars.userVariables='{"environment":"production","region":"us-east-1","feature_flags":{"new_ui":true,"beta_features":false}}' \ + --override-json envs='{"DATABASE_URL":"prod-db","API_KEY":"prod-key"}' +``` + +### CLI Syntax Examples + +#### DataMap Function with CLI Arguments + +```bash +# Traditional JSON approach +swaig-test examples/datasphere_agent.py search_knowledge '{"query":"SignalWire features","count":"3","distance":"0.5"}' + +# CLI syntax approach +swaig-test examples/datasphere_agent.py search_knowledge --args \ + --query "SignalWire features" \ + --count 3 \ + --distance 0.5 +``` + +#### Math Function with Type Conversion + +```bash +# CLI syntax with automatic type conversion +swaig-test examples/math_agent.py calculate --args \ + --expression "sqrt(144) + log(100)" \ + --precision 4 \ + --scientific-notation false +``` + +#### Complex Function with Mixed Types + +```bash +# Function with string, number, boolean, and array parameters +swaig-test examples/complex_agent.py process_data --args \ + --input-text "Process this data" \ + --max-items 50 \ + --include-metadata \ + --categories "urgent,customer,support" \ + --confidence-threshold 0.85 \ + --async-processing false +``` + +## Advanced Usage + +### SWML Testing Workflows + +#### Testing Call Flow Scenarios + +```bash +# Test inbound call flow +swaig-test examples/ivr_agent.py --dump-swml \ + --call-type sip \ + --call-direction inbound \ + --call-state created \ + --from-number "+15551234567" \ + --user-vars '{"caller_history":"first_time","language":"en"}' \ + --verbose + +# Test transfer scenario +swaig-test examples/ivr_agent.py --dump-swml \ + --call-state answered \ + --override call.timeout=30 \ + --user-vars '{"transfer_reason":"escalation","agent_type":"supervisor"}' \ + --verbose + +# Test callback scenario +swaig-test examples/callback_agent.py --dump-swml \ + --call-direction outbound \ + --override call.state=created \ + --user-vars '{"callback_scheduled":"2024-01-15T14:30:00Z","customer_id":"CUST-789"}' \ + --verbose +``` + +#### Testing Agent Variations + +```bash +# Test with different project configurations +for project in test-proj staging-proj prod-proj; do + echo "Testing project: $project" + swaig-test examples/multi_tenant_agent.py --dump-swml \ + --project-id $project \ + --user-vars "{\"tenant\":\"$project\"}" \ + --raw | jq '.sections.main[0].ai.params.ai_instructions' +done + +# Test with different user types +for tier in basic premium enterprise; do + echo "Testing tier: $tier" + swaig-test examples/tiered_agent.py --dump-swml \ + --user-vars "{\"customer_tier\":\"$tier\"}" \ + --verbose +done +``` + +#### Pipeline Testing with jq + +```bash +# Extract specific SWML components +swaig-test examples/agent.py --dump-swml --raw | jq '.sections.main[0].ai.SWAIG.functions[].function' + +# Test multiple agents and compare +for agent in examples/agent*.py; do + echo "Agent: $agent" + swaig-test $agent --dump-swml --raw | jq '.sections.main[0].ai.params.ai_instructions' +done + +# Validate SWML structure +swaig-test examples/agent.py --dump-swml --raw | jq 'has("version") and has("sections")' +``` + +### Mock Request Testing + +#### Testing Request-Dependent Logic + +```bash +# Test API key validation +swaig-test examples/api_agent.py --dump-swml \ + --header "Authorization=Bearer valid-token" \ + --body '{"api_version":"v2"}' \ + --verbose + +# Test user authentication +swaig-test examples/auth_agent.py --dump-swml \ + --header "X-User-ID=user123" \ + --header "X-Session-Token=session456" \ + --query-params '{"authenticated":"true"}' \ + --verbose + +# Test webhook validation +swaig-test examples/webhook_agent.py --dump-swml \ + --method POST \ + --header "X-Webhook-Signature=sha256=..." \ + --body '{"event":"call.created","data":{"call_id":"test"}}' \ + --verbose +``` + +#### Testing Different Request Patterns + +```bash +# Test GET request handling +swaig-test examples/rest_agent.py --dump-swml \ + --method GET \ + --query-params '{"action":"get_config","format":"json"}' \ + --header "Accept=application/json" + +# Test form data handling +swaig-test examples/form_agent.py --dump-swml \ + --method POST \ + --header "Content-Type=application/x-www-form-urlencoded" \ + --body '{"form_field":"value","submit":"true"}' + +# Test file upload simulation +swaig-test examples/upload_agent.py --dump-swml \ + --method POST \ + --header "Content-Type=multipart/form-data" \ + --body '{"filename":"test.txt","content_type":"text/plain"}' +``` + +## Troubleshooting + +### Common Issues + +| Issue | Symptoms | Solution | +|-------|----------|----------| +| **Agent Loading** | "No AgentBase instance found" | Ensure file has `agent` variable or AgentBase subclass | +| **Function Missing** | "Function 'X' not found" | Use `--list-tools` to verify function registration | +| **DataMap HTTP Error** | "Webhook request failed" | Check network connectivity and API credentials | +| **Template Expansion** | "MISSING:variable" in output | Verify template variable names match data structure | +| **JSON Parsing** | "Invalid JSON in args" | Check JSON syntax in function arguments | +| **SWML Generation** | "Error generating SWML" | Check agent initialization and SWML template syntax | +| **Dynamic Agent** | "Dynamic agent callback failed" | Verify on_swml_request method signature and mock request handling | +| **Override Syntax** | "Override path not found" | Use `--verbose` to see generated data structure and verify paths | +| **Wrong Argument Order** | CLI flags not working | Put all CLI flags BEFORE `--exec` | +| **Template Expansion** | "MISSING:variable" in output | Verify template variable names match data structure | +| **JSON Parsing** | "Invalid JSON in args" | Check JSON syntax or use `--exec` syntax | +| **Serverless URL Issues** | Wrong webhook URLs in SWML | Verify platform-specific configuration and environment variables | +| **Environment Conflicts** | Unexpected behavior in serverless mode | Clear conflicting environment variables or restart shell | +| **Platform Detection** | Wrong platform detected | Use `--simulate-serverless` explicitly instead of relying on auto-detection | + +### Debug Strategies + +1. **Use `--verbose`**: Shows complete execution flow including fake data generation +2. **Check function list**: Use `--list-tools --verbose` to see configurations +3. **Test connectivity**: For DataMap functions, ensure API endpoints are reachable +4. **Validate JSON**: Use online JSON validators for complex arguments +5. **Check logs**: Agent initialization logs show skill loading status +6. **Test SWML incrementally**: Start with `--dump-swml` then add overrides gradually +7. **Verify mock requests**: Use `--verbose` to see mock request object details +8. **Pipeline with jq**: Use `--raw | jq '.'` to validate JSON structure + +### SWML-Specific Debugging + +For SWML generation issues: + +```bash +# Check basic SWML generation +swaig-test my_agent.py --dump-swml --verbose + +# Test with minimal overrides +swaig-test my_agent.py --dump-swml --override call.state=test --verbose + +# Validate JSON structure +swaig-test my_agent.py --dump-swml --raw | python -m json.tool + +# Check dynamic agent callback +swaig-test my_agent.py --dump-swml --header "test=value" --verbose +``` + +Look for: +- Fake post_data generation details +- Mock request object creation +- Dynamic agent callback execution +- Override application order +- Final SWML document structure + +### CLI Syntax Debugging + +For `--args` parsing issues: + +```bash +# Verify function schema +swaig-test my_agent.py --list-tools --verbose | grep -A 10 my_function + +# Test with simple parameters first +swaig-test my_agent.py my_function --args --simple-param "value" + +# Check type conversion +swaig-test my_agent.py my_function --args --number-param 42 --bool-param --verbose +``` + +Look for: +- Parameter schema definitions +- Type conversion results +- Required vs optional parameters +- Parsed argument values + +### Testing DataMap Error Handling + +Test how DataMap functions handle API failures: + +```bash +# Test with verbose output to see fallback processing +swaig-test my_agent.py my_datamap_func '{"input":"test"}' --verbose +``` + +If the primary webhook fails, you'll see: +``` +Webhook 1 request failed: Connection timeout +--- Using DataMap Fallback Output --- +Fallback result = Sorry, the service is temporarily unavailable. +``` + +### Custom Environment Testing + +Simulate different environments with custom data: + +```bash +# Simulate production environment +swaig-test my_agent.py my_function '{"input":"test"}' --fake-full-data --custom-data '{ + "global_data": { + "environment": "production", + "api_tier": "premium", + "user_id": "prod-user-123" + }, + "prompt_vars": { + "ai_instructions": "You are a premium production assistant", + "temperature": 0.3 + } +}' +``` + +### Testing Complex DataMap Configurations + +For DataMap functions with multiple webhooks and complex foreach processing: + +```bash +swaig-test my_agent.py complex_search '{"query":"test","filters":["type1","type2"]}' --verbose +``` + +This shows the complete processing pipeline: +- Template expansion in URLs and parameters +- Multiple webhook attempts with fallback +- Foreach processing of array responses +- Final output template expansion + +### DataMap-Specific Debugging + +For DataMap function issues: + +```bash +# Enable verbose to see HTTP details +swaig-test my_agent.py --verbose --exec my_datamap_func --input test + +# Check the complete configuration +swaig-test my_agent.py --list-tools --verbose | grep -A 20 my_datamap +``` + +Look for: +- Template expansion in request data +- HTTP response status and content +- Foreach processing details +- Output template expansion + +## Integration with Development + +### Pre-Deployment Testing + +```bash +# Test all functions systematically +functions=$(swaig-test my_agent.py --list-tools | grep " " | cut -d' ' -f3) +for func in $functions; do + echo "Testing $func..." + swaig-test my_agent.py $func '{"test":"data"}' --fake-full-data +done +``` + +### CI/CD Integration + +The tool returns appropriate exit codes: +- `0`: Success +- `1`: Error (function failed, invalid arguments, network issues, etc.) + +```yaml +# GitHub Actions example +- name: Test SWAIG Functions + run: | + swaig-test my_agent.py critical_function '{"input":"test"}' --fake-full-data + if [ $? -ne 0 ]; then + echo "Critical function test failed" + exit 1 + fi +``` + +## Performance and Limitations + +### Performance Considerations + +- **DataMap HTTP Requests**: Real network latency applies +- **Large Responses**: Processing large API responses takes time +- **Verbose Output**: Can generate substantial debugging information +- **Memory Usage**: Comprehensive post_data mode uses more memory + +### Current Limitations + +1. **SignalWire Infrastructure**: Cannot perfectly replicate the serverless environment +2. **Network Dependencies**: DataMap testing requires internet connectivity +3. **Authentication**: Uses real API credentials (ensure proper security) +4. **State Isolation**: No persistence between separate test runs +5. **Concurrency**: Single-threaded execution only + +### Best Practices + +1. **Use minimal data mode** for basic function validation +2. **Enable verbose mode** when debugging issues +3. **Test DataMap functions** with real API credentials in secure environments +4. **Validate JSON arguments** before testing +5. **Check network connectivity** before testing DataMap functions + +### Webhook Failure Detection + +DataMap webhooks are considered failed when any of these conditions occur: + +1. **HTTP Status Codes**: Status outside 200-299 range +2. **Explicit Error Keys**: `parse_error` or `protocol_error` in response +3. **Custom Error Keys**: Any keys specified in webhook `error_keys` configuration +4. **Network Errors**: Connection timeouts, DNS failures, etc. + +When a webhook fails, the tool: +- Tries the next webhook in sequence (if any) +- Uses fallback output if all webhooks fail +- Provides detailed error information in verbose mode + +## Troubleshooting + +### Common Issues + +| Issue | Symptoms | Solution | +|-------|----------|----------| +| **Multiple Agents** | "Multiple agents found" | Use `--agent-class ClassName` to specify which agent | +| **Agent Loading** | "No AgentBase instance found" | Ensure file has agent instance or AgentBase subclass | +| **Function Missing** | "Function 'X' not found" | Use `--list-tools` to verify function registration | +| **DataMap HTTP Error** | "Webhook request failed" | Check network connectivity and API credentials | +| **Wrong Argument Order** | CLI flags not working | Put all CLI flags BEFORE `--exec` | +| **Template Expansion** | "MISSING:variable" in output | Verify template variable names match data structure | +| **JSON Parsing** | "Invalid JSON in args" | Check JSON syntax or use `--exec` syntax | +| **Serverless URL Issues** | Wrong webhook URLs in SWML | Verify platform-specific configuration and environment variables | +| **Environment Conflicts** | Unexpected behavior in serverless mode | Clear conflicting environment variables or restart shell | +| **Platform Detection** | Wrong platform detected | Use `--simulate-serverless` explicitly instead of relying on auto-detection | + +### Debug Strategies + +1. **Use `--verbose`**: Shows complete execution flow including agent selection and fake data generation +2. **Check agent discovery**: Use `--list-agents` to see available agents +3. **Check function list**: Use `--list-tools --verbose` to see configurations +4. **Test connectivity**: For DataMap functions, ensure API endpoints are reachable +5. **Check argument order**: CLI flags before `--exec`, function args after function name +6. **Validate syntax**: Use `--exec` syntax to avoid JSON parsing issues + +### Serverless Debugging + +#### Environment Variable Issues + +```bash +# Debug environment variable setup +swaig-test my_agent.py --simulate-serverless lambda --verbose --exec get_status + +# Check what environment variables are set +swaig-test my_agent.py --simulate-serverless lambda --env DEBUG=1 --exec debug_env + +# Test environment file loading +swaig-test my_agent.py --simulate-serverless lambda --env-file my.env --verbose --dump-swml + +# Clear conflicting variables +unset AWS_LAMBDA_FUNCTION_NAME GOOGLE_CLOUD_PROJECT +swaig-test my_agent.py --simulate-serverless cloud_function --verbose --dump-swml +``` + +#### Platform-Specific Debugging + +```bash +# Debug Lambda configuration +swaig-test my_agent.py --simulate-serverless lambda \ + --aws-function-name my-function \ + --aws-region us-west-2 \ + --verbose --dump-swml + +# Debug CGI configuration +swaig-test my_agent.py --simulate-serverless cgi \ + --cgi-host example.com \ + --cgi-https \ + --verbose --dump-swml + +# Debug webhook URL generation +swaig-test my_agent.py --simulate-serverless lambda \ + --dump-swml --format-json | jq '.sections.main[1].ai.SWAIG.defaults.web_hook_url' +``` + +#### Function Execution Debugging + +```bash +# Debug function execution in serverless context +swaig-test my_agent.py --simulate-serverless lambda \ + --verbose \ + --exec my_function --param value \ + --full-request + +# Compare responses across platforms +swaig-test my_agent.py --exec my_function --param value > local.json +swaig-test my_agent.py --simulate-serverless lambda --exec my_function --param value > lambda.json +diff local.json lambda.json +``` + +### Agent Discovery Debugging + +```bash +# Debug agent discovery +swaig-test my_file.py --list-agents --verbose + +# Check if agent is auto-selected +swaig-test my_file.py --verbose --exec my_function --param value + +# Explicitly specify agent +swaig-test my_file.py --agent-class MyAgent --verbose --exec my_function --param value +``` + +### DataMap Debugging + +```bash +# Enable verbose to see complete DataMap processing +swaig-test my_agent.py --verbose --exec my_datamap_func --input test + +# Check URL template expansion +swaig-test my_agent.py --verbose --exec my_func --location "New York" +``` + +Look for: +- URL template expansion details +- HTTP request/response information +- Fallback processing when APIs fail +- Output template processing + +### Joke Agent Examples + +#### Working Joke API (Success Case) + +```bash +# Test with valid API key - shows successful DataMap processing +API_NINJAS_KEY=your_api_key swaig-test examples/joke_skill_demo.py get_joke '{"type": "jokes"}' --verbose +``` + +**Expected Output:** +``` +=== DataMap Function Execution === +--- Processing Webhooks --- +Making GET request to: https://api.api-ninjas.com/v1/jokes +Response status: 200 +Webhook 1 succeeded! +Array response: 1 items + +--- Processing Webhook Output --- +Set response = Here's a joke: What do you call a bear with no teeth? A gummy bear! + +RESULT: +Response: Here's a joke: What do you call a bear with no teeth? A gummy bear! +``` + +#### Invalid API Key (Failure Case) + +```bash +# Test with invalid API key - shows fallback output processing +swaig-test examples/joke_agent.py get_joke '{"type": "jokes"}' --verbose +``` + +**Expected Output (when API key is invalid):** +``` +=== DataMap Function Execution === +--- Processing Webhooks --- +Making GET request to: https://api.api-ninjas.com/v1/jokes +Response status: 400 +Response data: {"error": "Invalid API Key."} +Webhook failed: HTTP status 400 outside 200-299 range +Webhook 1 failed, trying next webhook... + +--- Using DataMap Fallback Output --- +Fallback result = Tell the user that the joke service is not working right now and just make up a joke on your own + +RESULT: +Response: Tell the user that the joke service is not working right now and just make up a joke on your own +``` + +This demonstrates both: +- **Successful webhook processing** with array response handling +- **Failure detection and fallback** when APIs return errors + +## Best Practices + +### Development Workflow + +1. **Start with discovery**: `swaig-test my_agent.py` to see available agents +2. **List functions**: `swaig-test my_agent.py --list-tools` to see available functions +3. **Test functions**: Use `--exec` syntax for cleaner testing +4. **Test SWML**: Use `--dump-swml` to verify agent configuration +5. **Use verbose mode**: Enable `--verbose` when debugging issues + +### Multi-Agent Files + +1. **Always use `--list-agents` first** to see what's available +2. **Use `--agent-class` consistently** for multi-agent files +3. **Test each agent separately** to isolate issues +4. **Use descriptive agent names** to make selection easier + +### DataMap Functions + +1. **Test with verbose mode** to see complete processing pipeline +2. **Verify API credentials** before testing DataMap functions +3. **Test error handling** with invalid parameters +4. **Check network connectivity** for external APIs + +### CLI Syntax + +1. **Prefer `--exec` syntax** for development +2. **Put CLI flags before `--exec`** for correct parsing +3. **Use `--verbose`** to see argument parsing results +4. **Use JSON syntax** when needed for complex argument structures + +### Serverless Testing + +1. **Test locally first** before using serverless simulation +2. **Use environment files** for consistent platform configuration +3. **Test all target platforms** to ensure compatibility +4. **Verify webhook URLs** are generated correctly for each platform +5. **Clear environment variables** between platform tests to avoid conflicts +6. **Use `--verbose`** to debug environment setup and URL generation + +## Conclusion + +The `swaig-test` tool provides a comprehensive testing experience with: + +- **Automatic agent discovery** and selection +- **Intuitive `--exec` CLI syntax** for easier function testing +- **Complete DataMap simulation** with real API execution +- **Multi-agent support** with simple selection +- **Comprehensive SWML testing** with realistic fake data + +The tool provides flexible interfaces for development and testing of SignalWire AI Agents. diff --git a/docs/swml/agents-sdk/guides/cloud-functions.md b/docs/swml/agents-sdk/guides/cloud-functions.md new file mode 100644 index 00000000..2b36b855 --- /dev/null +++ b/docs/swml/agents-sdk/guides/cloud-functions.md @@ -0,0 +1,484 @@ +--- +id: agents-sdk-guides-cloud-functions +slug: /agents-sdk/guides/cloud-functions +title: Cloud Functions Guide +sidebar_label: Cloud Functions +--- + +# SignalWire AI Agents - Cloud Functions Deployment Guide + +This guide covers deploying SignalWire AI Agents to Google Cloud Functions and Azure Functions. + +## Overview + +SignalWire AI Agents now support deployment to major cloud function platforms: + +- **Google Cloud Functions** - Serverless compute platform on Google Cloud +- **Azure Functions** - Serverless compute service on Microsoft Azure +- **AWS Lambda** - Already supported (see existing documentation) + +## Google Cloud Functions + +### Environment Detection + +The agent automatically detects Google Cloud Functions environment using these variables: +- `FUNCTION_TARGET` - The function entry point +- `K_SERVICE` - Knative service name (Cloud Run/Functions) +- `GOOGLE_CLOUD_PROJECT` - Google Cloud project ID + +### Deployment Steps + +1. **Create your agent file** (`main.py`): +```python +import functions_framework +from your_agent_module import YourAgent + +# Create agent instance +agent = YourAgent( + name="my-agent", + # Configure your agent parameters +) + +@functions_framework.http +def agent_handler(request): + """HTTP Cloud Function entry point""" + return agent.handle_serverless_request(event=request) +``` + +2. **Create requirements.txt**: +``` +functions-framework==3.* +signalwire-agents +# Add your other dependencies +``` + +3. **Deploy using gcloud**: +```bash +gcloud functions deploy my-agent \ + --runtime python39 \ + --trigger-http \ + --entry-point agent_handler \ + --allow-unauthenticated +``` + +### Environment Variables + +Set these environment variables for your function: + +```bash +# SignalWire credentials +export SIGNALWIRE_PROJECT_ID="your-project-id" +export SIGNALWIRE_TOKEN="your-token" + +# Agent configuration +export AGENT_USERNAME="your-username" +export AGENT_PASSWORD="your-password" + +# Optional: Custom region/project settings +export FUNCTION_REGION="us-central1" +export GOOGLE_CLOUD_PROJECT="your-project-id" +``` + +### URL Format + +Google Cloud Functions URLs follow this pattern: +``` +https://{region}-{project-id}.cloudfunctions.net/{function-name} +``` + +With authentication: +``` +https://username:password@{region}-{project-id}.cloudfunctions.net/{function-name} +``` + +## Azure Functions + +### Environment Detection + +The agent automatically detects Azure Functions environment using these variables: +- `AZURE_FUNCTIONS_ENVIRONMENT` - Azure Functions runtime environment +- `FUNCTIONS_WORKER_RUNTIME` - Runtime language (python, node, etc.) +- `AzureWebJobsStorage` - Azure storage connection string + +### Deployment Steps + +1. **Create your function app structure**: +``` +my-agent-function/ +├── __init__.py +├── function.json +└── requirements.txt +``` + +2. **Create `__init__.py`**: +```python +import azure.functions as func +from your_agent_module import YourAgent + +# Create agent instance +agent = YourAgent( + name="my-agent", + # Configure your agent parameters +) + +def main(req: func.HttpRequest) -> func.HttpResponse: + """Azure Function entry point""" + return agent.handle_serverless_request(event=req) +``` + +3. **Create `function.json`**: +```json +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": ["get", "post"] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} +``` + +4. **Create `requirements.txt`**: +``` +azure-functions +signalwire-agents +# Add your other dependencies +``` + +5. **Deploy using Azure CLI**: +```bash +# Create function app +az functionapp create \ + --resource-group myResourceGroup \ + --consumption-plan-location westus \ + --runtime python \ + --runtime-version 3.9 \ + --functions-version 4 \ + --name my-agent-function \ + --storage-account mystorageaccount + +# Deploy code +func azure functionapp publish my-agent-function +``` + +### Environment Variables + +Set these in your Azure Function App settings: + +```bash +# SignalWire credentials +SIGNALWIRE_PROJECT_ID="your-project-id" +SIGNALWIRE_TOKEN="your-token" + +# Agent configuration +AGENT_USERNAME="your-username" +AGENT_PASSWORD="your-password" + +# Azure-specific (usually auto-set) +AZURE_FUNCTIONS_ENVIRONMENT="Development" +WEBSITE_SITE_NAME="my-agent-function" +``` + +### URL Format + +Azure Functions URLs follow this pattern: +``` +https://{function-app-name}.azurewebsites.net/api/{function-name} +``` + +With authentication: +``` +https://username:password@{function-app-name}.azurewebsites.net/api/{function-name} +``` + +## Authentication + +Both platforms support HTTP Basic Authentication: + +### Automatic Authentication +The agent automatically validates credentials in cloud function environments: + +```python +agent = YourAgent( + name="my-agent", + username="your-username", + password="your-password" +) +``` + +### Authentication Flow +1. Client sends request with `Authorization: Basic ` header +2. Agent validates credentials against configured username/password +3. If invalid, returns 401 with `WWW-Authenticate` header +4. If valid, processes the request normally + +## Testing + +### SignalWire Agent Testing Tool + +The SignalWire AI Agents SDK includes a powerful testing tool (`swaig-test`) that can simulate cloud function environments for comprehensive testing before deployment. + +#### Cloud Function Environment Simulation + +**Google Cloud Functions:** +```bash +# Test SWML generation in GCP environment +swaig-test examples/my_agent.py --simulate-serverless cloud_function --gcp-project my-project --dump-swml + +# Test function execution +swaig-test examples/my_agent.py --simulate-serverless cloud_function --gcp-project my-project --exec my_function --param value + +# With custom region and service +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project my-project \ + --gcp-region us-west1 \ + --gcp-service my-service \ + --dump-swml +``` + +**Azure Functions:** +```bash +# Test SWML generation in Azure environment +swaig-test examples/my_agent.py --simulate-serverless azure_function --dump-swml + +# Test function execution +swaig-test examples/my_agent.py --simulate-serverless azure_function --exec my_function --param value + +# With custom environment and URL +swaig-test examples/my_agent.py --simulate-serverless azure_function \ + --azure-env Production \ + --azure-function-url https://myapp.azurewebsites.net/api/myfunction \ + --dump-swml +``` + +#### Environment Variable Testing + +Test with custom environment variables: +```bash +# Set individual environment variables +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --env GOOGLE_CLOUD_PROJECT=my-project \ + --env DEBUG=1 \ + --exec my_function + +# Load from environment file +swaig-test examples/my_agent.py --simulate-serverless azure_function \ + --env-file production.env \ + --dump-swml +``` + +#### Authentication Testing + +Test authentication in cloud function environments: +```bash +# Test with authentication (uses agent's configured credentials) +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project my-project \ + --dump-swml --verbose + +# The tool automatically tests: +# - Basic auth credential embedding in URLs +# - Authentication challenge responses +# - Platform-specific auth handling +``` + +#### URL Generation Testing + +Verify that URLs are generated correctly for each platform: +```bash +# Check URL generation with verbose output +swaig-test examples/my_agent.py --simulate-serverless cloud_function \ + --gcp-project my-project \ + --dump-swml --verbose + +# Extract webhook URLs from SWML +swaig-test examples/my_agent.py --simulate-serverless azure_function \ + --dump-swml --raw | jq '.sections.main[1].ai.SWAIG.functions[].web_hook_url' +``` + +#### Available Testing Options + +**Platform Selection:** +- `--simulate-serverless cloud_function` - Google Cloud Functions +- `--simulate-serverless azure_function` - Azure Functions +- `--simulate-serverless lambda` - AWS Lambda +- `--simulate-serverless cgi` - CGI environment + +**Google Cloud Platform Options:** +- `--gcp-project PROJECT_ID` - Set Google Cloud project ID +- `--gcp-region REGION` - Set Google Cloud region (default: us-central1) +- `--gcp-service SERVICE` - Set service name +- `--gcp-function-url URL` - Override function URL + +**Azure Functions Options:** +- `--azure-env ENVIRONMENT` - Set Azure environment (default: Development) +- `--azure-function-url URL` - Override Azure Function URL + +**Environment Variables:** +- `--env KEY=value` - Set individual environment variables +- `--env-file FILE` - Load environment variables from file + +**Output Options:** +- `--dump-swml` - Generate and display SWML document +- `--verbose` - Show detailed information +- `--raw` - Output raw JSON (useful for piping to jq) + +#### Complete Testing Workflow + +```bash +# 1. List available agents and tools +swaig-test examples/my_agent.py --list-agents +swaig-test examples/my_agent.py --list-tools + +# 2. Test SWML generation for each platform +swaig-test examples/my_agent.py --simulate-serverless cloud_function --gcp-project test-project --dump-swml +swaig-test examples/my_agent.py --simulate-serverless azure_function --dump-swml + +# 3. Test specific function execution +swaig-test examples/my_agent.py --simulate-serverless cloud_function --gcp-project test-project --exec search_knowledge --query "test" + +# 4. Test with production-like environment +swaig-test examples/my_agent.py --simulate-serverless azure_function --env-file production.env --exec my_function --param value + +# 5. Verify authentication and URL generation +swaig-test examples/my_agent.py --simulate-serverless cloud_function --gcp-project prod-project --dump-swml --verbose +``` + +### Local Testing + +**Google Cloud Functions:** +```bash +# Install Functions Framework +pip install functions-framework + +# Run locally +functions-framework --target=agent_handler --debug +``` + +**Azure Functions:** +```bash +# Install Azure Functions Core Tools +npm install -g azure-functions-core-tools@4 + +# Run locally +func start +``` + +### Testing Authentication + +```bash +# Test without auth (should return 401) +curl https://your-function-url/ + +# Test with valid auth +curl -u username:password https://your-function-url/ + +# Test SWAIG function call +curl -u username:password \ + -H "Content-Type: application/json" \ + -d '{"call_id": "test", "argument": {"parsed": [{"param": "value"}]}}' \ + https://your-function-url/your_function_name +``` + +## Best Practices + +### Performance +- Use connection pooling for database connections +- Implement proper caching strategies +- Minimize cold start times with smaller deployment packages + +### Security +- Always use HTTPS endpoints +- Implement proper authentication +- Use environment variables for sensitive data +- Consider using cloud-native secret management + +### Monitoring +- Enable cloud platform logging +- Monitor function execution times +- Set up alerts for errors and timeouts +- Use distributed tracing for complex workflows + +### Cost Optimization +- Right-size memory allocation +- Implement proper timeout settings +- Use reserved capacity for predictable workloads +- Monitor and optimize function execution patterns + +## Troubleshooting + +### Common Issues + +**Environment Detection:** +```python +# Check detected mode +from signalwire_agents.core.logging_config import get_execution_mode +print(f"Detected mode: {get_execution_mode()}") +``` + +**URL Generation:** +```python +# Check generated URLs +agent = YourAgent(name="test") +print(f"Base URL: {agent.get_full_url()}") +print(f"Auth URL: {agent.get_full_url(include_auth=True)}") +``` + +**Authentication Issues:** +- Verify username/password are set correctly +- Check that Authorization header is being sent +- Ensure credentials match exactly (case-sensitive) + +### Debugging + +Enable debug logging: +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +Check environment variables: +```python +import os +for key, value in os.environ.items(): + if 'FUNCTION' in key or 'AZURE' in key or 'GOOGLE' in key: + print(f"{key}: {value}") +``` + +## Migration from Other Platforms + +### From AWS Lambda +- Update environment variable names +- Modify request/response handling if needed +- Update deployment scripts + +### From Traditional Servers +- Add cloud function entry point +- Configure environment variables +- Update URL generation logic +- Test authentication flow + +## Examples + +See the `examples/cloud_functions/` directory for complete working examples: +- `google_cloud_function_example/` - Complete Google Cloud Functions deployment +- `azure_function_example/` - Complete Azure Functions deployment +- `multi_platform_agent/` - Agent that works across all platforms + +## Support + +For issues specific to cloud function deployment: +1. Check the troubleshooting section above +2. Verify environment variables are set correctly +3. Test authentication flow manually +4. Check cloud platform logs for detailed error messages +5. Refer to platform-specific documentation for deployment issues \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/datamap.md b/docs/swml/agents-sdk/guides/datamap.md new file mode 100644 index 00000000..ac788940 --- /dev/null +++ b/docs/swml/agents-sdk/guides/datamap.md @@ -0,0 +1,617 @@ +--- +id: agents-sdk-guides-datamap +slug: /agents-sdk/guides/datamap +title: DataMap Guide +sidebar_label: DataMap Guide +--- + +# DataMap Guide + +The DataMap system allows you to create SWAIG tools that integrate directly with REST APIs without requiring custom webhook endpoints. DataMap tools execute on the SignalWire server, making them simpler to deploy and manage than traditional webhook-based tools. + +## Overview + +DataMap tools provide a declarative way to define API integrations that run on SignalWire's infrastructure. Instead of creating webhook endpoints, you describe the API call and response processing using JSON configuration that gets executed serverlessly. + +### Key Benefits + +- **No webhook infrastructure**: Tools run on SignalWire servers +- **Simplified deployment**: No need to expose endpoints +- **Built-in authentication**: Support for API keys, Bearer tokens, Basic auth +- **Response processing**: Built-in JSON path traversal and array processing +- **Error handling**: Automatic error detection with `error_keys` +- **Pattern matching**: Expression-based responses without API calls + +### When to Use DataMap vs Skills vs Custom Tools + +- **DataMap**: Simple REST API integrations, no complex processing needed +- **Skills**: Reusable capabilities with complex logic or dependencies +- **Custom Tools**: Full control over webhook handling and processing + +## Basic Usage + +```python +from signalwire_agents.core.data_map import DataMap +from signalwire_agents.core.function_result import SwaigFunctionResult + +class MyAgent(AgentBase): + def setup_tools(self): + # Simple weather API integration + weather_tool = (DataMap('get_weather') + .description('Get current weather information') + .parameter('location', 'string', 'City name', required=True) + .webhook('GET', 'https://api.weather.com/v1/current?key=YOUR_API_KEY&q=${args.location}') + .output(SwaigFunctionResult('Weather in ${args.location}: ${response.current.condition.text}, ${response.current.temp_f}°F')) + .error_keys(['error']) + ) + + # Register with agent + self.register_swaig_function(weather_tool.to_swaig_function()) +``` + +## DataMap Builder Pattern + +DataMap uses a fluent interface where methods can be chained together: + +```python +tool = (DataMap('function_name') + .description('Tool description') + .parameter('param', 'string', 'Parameter description', required=True) + .webhook('POST', 'https://api.example.com/endpoint') + .body({'data': '${args.param}'}) + .output(SwaigFunctionResult('Result: ${response.value}')) +) +``` + +## Processing Pipeline + +DataMap tools follow this execution order: + +1. **Expressions**: Pattern matching against arguments (if defined) +2. **Webhooks**: API calls (if expressions don't match or aren't defined) +3. **Foreach**: Array processing (if webhook returns array and foreach is configured) +4. **Output**: Final response generation + +The pipeline stops at the first successful step. + +### Webhook Output Structure + +**Important**: Outputs are attached to individual webhooks, not at the top level. This allows for: + +- **Per-webhook responses**: Each API can have its own output template +- **Sequential fallback**: Try multiple APIs until one succeeds +- **Error handling**: Per-webhook error detection + +```python +# Correct: Output inside webhook +tool = (DataMap('get_data') + .webhook('GET', 'https://api.primary.com/data') + .output(SwaigFunctionResult('Primary: ${response.value}')) + .error_keys(['error']) +) + +# Multiple webhooks with fallback +tool = (DataMap('search_with_fallback') + .webhook('GET', 'https://api.fast.com/search?q=${args.query}') + .output(SwaigFunctionResult('Fast result: ${response.title}')) + .webhook('GET', 'https://api.comprehensive.com/search?q=${args.query}') + .output(SwaigFunctionResult('Comprehensive result: ${response.title}')) + .fallback_output(SwaigFunctionResult('Sorry, all search services are unavailable')) +) +``` + +### Execution Flow + +1. **Try first webhook**: If successful, use its output +2. **Try subsequent webhooks**: If first fails, try next webhook +3. **Fallback output**: If all webhooks fail, use top-level fallback (if defined) +4. **Generic error**: If no fallback defined, return generic error message + +## Variable Expansion + +DataMap supports powerful variable substitution using `${variable}` syntax: + +### Data Store Usage + +**global_data** - Call-wide data store that persists throughout the entire call: +- **Purpose**: Store user information, call state, preferences collected during conversation +- **Examples**: `${global_data.customer_name}`, `${global_data.account_type}`, `${global_data.preferred_language}` +- **Seeded by**: Initial SWML configuration, SWAIG actions during the call +- **Shared by**: All functions in the same call +- **NEVER use for**: API keys, passwords, secrets, or sensitive configuration + +**meta_data** - Function-scoped data store: +- **Purpose**: Function-specific state and metadata +- **Examples**: `${meta_data.call_id}`, `${meta_data.session_id}`, `${meta_data.retry_count}` +- **Seeded by**: Function definition, SWAIG actions +- **Shared by**: Functions with the same meta_data_token +- **NEVER use for**: Credentials, API keys, or sensitive data + +### Available Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `${args.param_name}` | Function arguments | `${args.location}` | +| `${array[0].field}` | API response data (array) | `${array[0].joke}` | +| `${response.field}` | API response data (object) | `${response.status}` | +| `${this.field}` | Current item in foreach | `${this.title}` | +| `${output_key}` | Built string from foreach | `${formatted_results}` | +| `${global_data.key}` | Agent global data | `${global_data.user_id}` | +| `${meta_data.call_id}` | Call metadata | `${meta_data.call_id}` | + +### JSON Path Traversal + +Variables support nested object access and array indexing: + +```python +# Nested objects +'${response.current.condition.text}' + +# Array indexing +'${response.results[0].title}' + +# Complex paths +'${response.data.users[2].profile.name}' +``` + +### Variable Scoping Rules + +Understanding when to use different variable types: + +| Context | Variable Type | Example | When to Use | +|---------|---------------|---------|-------------| +| **Array APIs** | `${array[0].field}` | `${array[0].joke}` | API returns JSON array `[{...}]` | +| **Object APIs** | `${response.field}` | `${response.temperature}` | API returns JSON object `{...}` | +| **Foreach processing** | `${this.field}` | `${this.title}` | Inside foreach append template | +| **Function args** | `${args.field}` | `${args.location}` | User-provided parameters | +| **Agent data** | `${global_data.field}` | `${global_data.api_key}` | Agent configuration | + +```python +# Example: Weather API returns object +{ + "current": {"temp_f": 72, "condition": {"text": "Sunny"}}, + "location": {"name": "New York"} +} +# Use: ${response.current.temp_f} + +# Example: Jokes API returns array +[ + {"joke": "Why did the chicken cross the road?", "category": "classic"} +] +# Use: ${array[0].joke} + +# Example: Search API with foreach +{ + "results": [ + {"title": "Result 1", "snippet": "..."}, + {"title": "Result 2", "snippet": "..."} + ] +} +# Configure foreach and use: ${this.title} in append template +``` + +### Examples + +```python +# URL parameter substitution +.webhook('GET', 'https://api.weather.com/v1/current?key=${global_data.api_key}&q=${args.location}') + +# Request body templating +.body({ + 'query': '${args.search_term}', + 'user_id': '${global_data.user_id}', + 'limit': 10 +}) + +# Response formatting +.output(SwaigFunctionResult('Found ${response.total_results} results for "${args.query}"')) +``` + +## Webhook Configuration + +### HTTP Methods + +```python +# GET request +.webhook('GET', 'https://api.example.com/data?param=${args.value}') + +# POST request with JSON body +.webhook('POST', 'https://api.example.com/create') +.body({'name': '${args.name}', 'value': '${args.value}'}) + +# PUT request with authentication +.webhook('PUT', 'https://api.example.com/update/${args.id}', + headers={'Authorization': 'Bearer ${global_data.token}'}) +.body({'status': '${args.status}'}) +``` + +### Authentication + +```python +# API key in URL +.webhook('GET', 'https://api.service.com/data?key=${global_data.api_key}&q=${args.query}') + +# Bearer token +.webhook('POST', 'https://api.service.com/search', + headers={'Authorization': 'Bearer ${global_data.token}'}) + +# Basic auth +.webhook('GET', 'https://api.service.com/data', + headers={'Authorization': 'Basic ${global_data.credentials}'}) + +# Custom headers +.webhook('GET', 'https://api.service.com/data', + headers={ + 'X-API-Key': '${global_data.api_key}', + 'X-Client-ID': '${global_data.client_id}', + 'Content-Type': 'application/json' + }) +``` + +## Array Processing with Foreach + +The `foreach` mechanism processes arrays by building a concatenated string. It does **not** iterate like JavaScript forEach - instead it builds a single output string from array elements. + +### How Foreach Works + +1. **input_key**: Specifies which key in the API response contains the array +2. **output_key**: Names the string variable that gets built up +3. **max**: Limits how many array items to process +4. **append**: Template string that gets evaluated for each item and concatenated + +```python +search_tool = (DataMap('search_docs') + .description('Search documentation') + .parameter('query', 'string', 'Search query', required=True) + .webhook('GET', 'https://api.docs.com/search?q=${args.query}') + .foreach({ + "input_key": "results", # API response key containing array + "output_key": "formatted_results", # Name for the built string + "max": 3, # Process max 3 items + "append": "Document: ${this.title} - ${this.summary}\n" # Template for each item + }) + .output(SwaigFunctionResult('Search results for "${args.query}":\n\n${formatted_results}')) +) +``` + +### Array Response Example + +If the API returns: +```json +{ + "results": [ + {"title": "Getting Started", "summary": "Basic setup"}, + {"title": "Advanced Features", "summary": "Complex workflows"} + ] +} +``` + +The foreach will build a string in `formatted_results`: +``` +Document: Getting Started - Basic setup +Document: Advanced Features - Complex workflows +``` + +### Foreach vs Direct Array Access + +| Approach | Use Case | Example | +|----------|----------|---------| +| **Direct access** | Single array item | `${array[0].joke}` | +| **Foreach** | Multiple items formatted as string | `${formatted_results}` after foreach | + +```python +# Direct access - single item from array response +joke_tool = (DataMap('get_joke') + .webhook('GET', 'https://api.jokes.com/random') + .output(SwaigFunctionResult('${array[0].joke}')) # Just first joke +) + +# Foreach - multiple items formatted +search_tool = (DataMap('search_all') + .webhook('GET', 'https://api.search.com/query') + .foreach({ + "input_key": "results", + "output_key": "all_results", + "max": 5, + "append": "- ${this.title}: ${this.description}\n" + }) + .output(SwaigFunctionResult('Found multiple results:\n${all_results}')) +) +``` + +## Expression-Based Tools + +For simple pattern matching without API calls, use expressions: + +```python +file_control = (DataMap('file_control') + .description('Control file playback') + .parameter('command', 'string', 'Playback command') + .parameter('filename', 'string', 'File to control', required=False) + .expression(r'start.*', SwaigFunctionResult().add_action('start_playback', {'file': '${args.filename}'})) + .expression(r'stop.*', SwaigFunctionResult().add_action('stop_playback', True)) + .expression(r'pause.*', SwaigFunctionResult().add_action('pause_playback', True)) +) +``` + +### Expression Patterns + +```python +# Exact match +.expression('hello', SwaigFunctionResult('Hello response')) + +# Case-insensitive regex +.expression(r'(?i)weather.*', SwaigFunctionResult('Weather info')) + +# Multiple patterns +.expression(r'start|begin|play', SwaigFunctionResult().add_action('start', True)) +.expression(r'stop|end|pause', SwaigFunctionResult().add_action('stop', True)) +``` + +## Error Handling + +Use `error_keys` to detect API errors: + +```python +api_tool = (DataMap('check_status') + .webhook('GET', 'https://api.service.com/status') + .error_keys(['error', 'message', 'errors']) # Check for these keys + .output(SwaigFunctionResult('Status: ${response.status}')) +) +``` + +If the response contains any of the error keys, the tool will fail gracefully. + +## Helper Functions + +For common patterns, use convenience functions: + +### Simple API Tool + +```python +from signalwire_agents.core.data_map import create_simple_api_tool + +weather = create_simple_api_tool( + name='get_weather', + url='https://api.weather.com/v1/current?key=API_KEY&q=${location}', + response_template='Weather: ${response.current.condition.text}, ${response.current.temp_f}°F', + parameters={ + 'location': { + 'type': 'string', + 'description': 'City name', + 'required': True + } + }, + headers={'X-API-Key': 'your-api-key'}, + error_keys=['error'] +) +``` + +### Expression Tool + +```python +from signalwire_agents.core.data_map import create_expression_tool + +control = create_expression_tool( + name='media_control', + patterns={ + r'start|play|begin': SwaigFunctionResult().add_action('start', True), + r'stop|end|pause': SwaigFunctionResult().add_action('stop', True), + r'next|skip': SwaigFunctionResult().add_action('next', True) + }, + parameters={ + 'command': {'type': 'string', 'description': 'Control command'} + } +) +``` + +## Real-World Examples + +### Weather Service + +```python +weather_tool = (DataMap('get_weather') + .description('Get current weather conditions') + .parameter('location', 'string', 'City and state/country', required=True) + .parameter('units', 'string', 'Temperature units', enum=['fahrenheit', 'celsius']) + .webhook('GET', 'https://api.openweathermap.org/data/2.5/weather?q=${args.location}&appid=${global_data.api_key}&units=${"imperial" if args.units == "fahrenheit" else "metric"}') + .error_keys(['cod', 'message']) + .output(SwaigFunctionResult('Weather in ${args.location}: ${response.weather[0].description}, ${response.main.temp}°${"F" if args.units == "fahrenheit" else "C"}. Feels like ${response.main.feels_like}°.')) +) +``` + +### Knowledge Search with Foreach + +```python +knowledge_tool = (DataMap('search_knowledge') + .description('Search company knowledge base') + .parameter('query', 'string', 'Search query', required=True) + .parameter('category', 'string', 'Knowledge category', enum=['support', 'sales', 'technical']) + .webhook('POST', 'https://api.company.com/knowledge/search', + headers={'Authorization': 'Bearer ${global_data.knowledge_token}'}) + .body({ + 'query': '${args.query}', + 'category': '${args.category}', + 'max_results': 5 + }) + .foreach({ + "input_key": "articles", + "output_key": "article_summaries", + "max": 3, + "append": "Article: ${this.title}\nSummary: ${this.summary}\nRelevance: ${this.score}\n\n" + }) + .output(SwaigFunctionResult('Found articles for "${args.query}":\n\n${article_summaries}')) +) +``` + +### Joke Service + +```python +joke_tool = (DataMap('get_joke') + .description('Get a random joke') + .parameter('category', 'string', 'Joke category', + enum=['programming', 'dad', 'pun', 'random']) + .webhook('GET', 'https://api.jokes.com/v1/joke?category=${args.category}&format=json') + .output(SwaigFunctionResult('Here\'s a ${args.category} joke: ${response.setup} ... ${response.punchline}')) + .error_keys(['error']) + .fallback_output(SwaigFunctionResult('Sorry, the joke service is currently unavailable. Please try again later.')) +) +``` + +### API with Array Response + +```python +# For APIs that return arrays, use ${array[0].field} syntax for single items +joke_ninja_tool = (DataMap('get_joke') + .description('Get a random joke from API Ninjas') + .parameter('type', 'string', 'Type of joke', enum=['jokes', 'dadjokes']) + .webhook('GET', 'https://api.api-ninjas.com/v1/${args.type}', + headers={'X-Api-Key': '${global_data.api_key}'}) + .output(SwaigFunctionResult('Here\'s a joke: ${array[0].joke}')) + .error_keys(['error']) + .fallback_output(SwaigFunctionResult('Sorry, there is a problem with the joke service right now. Please try again later.')) +) +``` + +### Multi-Step Fallback + +```python +# Try multiple APIs with fallback +search_tool = (DataMap('web_search') + .description('Search the web') + .parameter('query', 'string', 'Search query', required=True) + # Primary API + .webhook('GET', 'https://api.primary.com/search?q=${args.query}&key=${global_data.primary_key}') + # Fallback API + .webhook('GET', 'https://api.fallback.com/search?query=${args.query}&token=${global_data.fallback_token}') + .output(SwaigFunctionResult('Search results for "${args.query}": ${response.results[0].title} - ${response.results[0].snippet}')) +) +``` + +## Best Practices + +### 1. Keep It Simple + +DataMap is best for straightforward API integrations. For complex logic, use Skills or custom tools: + +```python +# Good: Simple API call +.webhook('GET', 'https://api.service.com/data?id=${args.id}') + +# Consider alternatives: Complex multi-step processing +# (Better handled by Skills or custom tools) +``` + +### 2. Use Global Data for Secrets + +Store API keys and tokens in global data, not hardcoded: + +```python +# Good +.webhook('GET', 'https://api.service.com/data?key=${global_data.api_key}') + +# Bad +.webhook('GET', 'https://api.service.com/data?key=hardcoded-key') +``` + +### 3. Provide Clear Parameter Descriptions + +```python +# Good +.parameter('location', 'string', 'City name or ZIP code (e.g., "New York" or "10001")', required=True) + +# Bad +.parameter('location', 'string', 'location', required=True) +``` + +### 4. Handle Errors Gracefully + +```python +# Always include error handling +.error_keys(['error', 'message', 'status']) +.fallback_output(SwaigFunctionResult('Service temporarily unavailable')) +``` + +### 5. Use Foreach for Multiple Items + +```python +# Good: Use foreach for multiple formatted results +.foreach({ + "input_key": "results", + "output_key": "formatted_list", + "append": "- ${this.title}: ${this.summary}\n" +}) + +# Less optimal: Only showing first result +.output(SwaigFunctionResult('First result: ${response.results[0].title}')) +``` + +## Troubleshooting + +### Common Issues + +#### 1. Variable Not Expanding + +**Problem**: Variables like `${args.location}` showing up as literal text. + +**Solutions**: +- Check variable name matches parameter name exactly +- Ensure proper `${variable}` syntax +- Verify the variable is in scope for that context + +#### 2. Array Access Not Working + +**Problem**: `${array[0].field}` returns undefined. + +**Solutions**: +- Verify API actually returns an array `[{...}]` +- Check if API returns object with array property: use `${response.arrayfield[0].field}` +- For multiple items, consider using foreach instead + +#### 3. Foreach Not Processing Arrays + +**Problem**: Foreach output is empty or not working. + +**Solutions**: +- Check `input_key` matches the actual API response structure +- Verify array exists and has items: `"input_key": "results"` for `{"results": [...]}` +- Ensure `append` template uses `${this.property}` syntax +- Check `max` value isn't zero + +#### 4. Authentication Failures + +**Problem**: API returns 401/403 errors. + +**Solutions**: +- Verify API key/token is correct in global_data +- Check header format matches API requirements +- Test API credentials outside of DataMap first + +#### 5. Error Keys Not Working + +**Problem**: Tool doesn't detect API errors properly. + +**Solutions**: +- Check actual API error response structure +- Add all possible error field names to `error_keys` +- Use `fallback_output` for generic error handling + +### Debug Tools + +Enable debug mode to see variable expansion: + +```python +debug_tool = (DataMap('debug_echo') + .parameter('test', 'string', 'Test parameter') + .output(SwaigFunctionResult('Input: ${args.test}, All args: ${args}')) +) + +# Test variable expansion +test_variables = (DataMap('test_vars') + .parameter('location', 'string', 'Location') + .webhook('GET', 'https://httpbin.org/get?location=${args.location}') + .output(SwaigFunctionResult('URL was: ${response.url}, Args: ${response.args}')) +) +``` + +This comprehensive guide should help you understand and effectively use the DataMap system for creating REST API integrations in your SignalWire agents. \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/developer-guide.md b/docs/swml/agents-sdk/guides/developer-guide.md new file mode 100644 index 00000000..248be97d --- /dev/null +++ b/docs/swml/agents-sdk/guides/developer-guide.md @@ -0,0 +1,9 @@ +--- +slug: /agents-sdk/guides/developer-guide +--- + +- **Latency**: `<`100ms end-to-end for voice +- **Jitter**: `<`30ms variation +- **Packet Loss**: `<`0.1% under normal conditions +- **Availability**: 99.999% uptime SLA +- **Capacity**: Millions of concurrent connections \ No newline at end of file diff --git a/docs/swml/agents-sdk/guides/swml-service.md b/docs/swml/agents-sdk/guides/swml-service.md new file mode 100644 index 00000000..4e45c102 --- /dev/null +++ b/docs/swml/agents-sdk/guides/swml-service.md @@ -0,0 +1,599 @@ +--- +id: agents-sdk-guides-swml-service +slug: /agents-sdk/guides/swml-service +title: SWML Service Guide +sidebar_label: SWML Service +--- + +# SignalWire SWML Service Guide + + +## Introduction + +The `SWMLService` class provides a foundation for creating and serving SignalWire Markup Language (SWML) documents. It serves as the base class for all SignalWire services, including AI Agents, and handles common tasks such as: + +- SWML document creation and manipulation +- Schema validation +- Web service functionality +- Authentication +- Centralized logging + +The class is designed to be extended for specific use cases, while providing powerful capabilities out of the box. + +## Installation + +The `SWMLService` class is part of the SignalWire AI Agent SDK. Install it using pip: + +```bash +pip install signalwire-agents +``` + +## Basic Usage + +Here's a simple example of creating an SWML service: + +```python +from signalwire_agents.core.swml_service import SWMLService + +class SimpleVoiceService(SWMLService): + def __init__(self, host="0.0.0.0", port=3000): + super().__init__( + name="voice-service", + route="/voice", + host=host, + port=port + ) + + # Build the SWML document + self.build_document() + + def build_document(self): + # Reset the document to start fresh + self.reset_document() + + # Add answer verb + self.add_answer_verb() + + # Add play verb for greeting + self.add_verb("play", { + "url": "say:Hello, thank you for calling our service." + }) + + # Add hangup verb + self.add_hangup_verb() + +# Create and start the service +service = SimpleVoiceService() +service.run() +``` + +## Centralized Logging System + +The `SWMLService` class includes a centralized logging system based on `structlog` that provides structured, JSON-formatted logs. This logging system is automatically set up when you import the module, so you don't need to configure it in each service or example. + +### How It Works + +1. When `swml_service.py` is imported, it configures `structlog` (if not already configured) +2. Each `SWMLService` instance gets a logger bound to its service name +3. All logs include contextual information like service name, timestamp, and log level +4. Logs are formatted as JSON for easy parsing and analysis + +### Using the Logger + +Every `SWMLService` instance has a `log` attribute that can be used for logging: + +```python +# Basic logging +self.log.info("service_started") + +# Logging with context +self.log.debug("document_created", size=len(document)) + +# Error logging +try: + # Some operation + pass +except Exception as e: + self.log.error("operation_failed", error=str(e)) +``` + +### Log Levels + +The following log levels are available (in increasing order of severity): +- `debug`: Detailed information for debugging +- `info`: General information about operation +- `warning`: Warning about potential issues +- `error`: Error information when operations fail +- `critical`: Critical error that might cause the application to terminate + +### Suppressing Logs + +To suppress logs when running a service, you can set the log level: + +```python +import logging +logging.getLogger().setLevel(logging.WARNING) # Only show warnings and above +``` + +You can also pass `suppress_logs=True` when initializing an agent or service: + +```python +service = SWMLService( + name="my-service", + suppress_logs=True +) +``` + +## SWML Document Creation + +The `SWMLService` class provides methods for creating and manipulating SWML documents. + +### Document Structure + +SWML documents have the following basic structure: + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "verb1": { /* configuration */ } }, + { "verb2": { /* configuration */ } } + ], + "section1": [ + { "verb3": { /* configuration */ } } + ] + } +} +``` + +### Document Methods + +- `reset_document()`: Reset the document to an empty state +- `add_verb(verb_name, config)`: Add a verb to the main section +- `add_section(section_name)`: Add a new section +- `add_verb_to_section(section_name, verb_name, config)`: Add a verb to a specific section +- `get_document()`: Get the current document as a dictionary +- `render_document()`: Get the current document as a JSON string + +### Common Verb Shortcuts + +- `add_answer_verb(max_duration=None, codecs=None)`: Add an answer verb +- `add_hangup_verb(reason=None)`: Add a hangup verb +- `add_ai_verb(prompt_text=None, prompt_pom=None, post_prompt=None, post_prompt_url=None, swaig=None, params=None)`: Add an AI verb + +## Verb Handling + +The `SWMLService` class provides validation for SWML verbs using the SignalWire schema. + +### Verb Validation + +When adding a verb, the service validates it against the schema to ensure it has the correct structure and parameters. + +```python +# This will validate the configuration against the schema +self.add_verb("play", { + "url": "say:Hello, world!", + "volume": 5 +}) + +# This would fail validation (invalid parameter) +self.add_verb("play", { + "invalid_param": "value" +}) +``` + +### Custom Verb Handlers + +You can register custom verb handlers for specialized verb processing: + +```python +from signalwire_agents.core.swml_handler import SWMLVerbHandler + +class CustomPlayHandler(SWMLVerbHandler): + def __init__(self): + super().__init__("play") + + def validate_config(self, config): + # Custom validation logic + return True, [] + + def build_config(self, **kwargs): + # Custom configuration building + return kwargs + +service.register_verb_handler(CustomPlayHandler()) +``` + +## Web Service Features + +The `SWMLService` class includes built-in web service capabilities for serving SWML documents. + +### Endpoints + +By default, a service provides the following endpoints: + +- `GET /route`: Return the SWML document +- `POST /route`: Process request data and return the SWML document +- `GET /route/`: Same as above but with trailing slash +- `POST /route/`: Same as above but with trailing slash + +Where `route` is the route path specified when creating the service. + +### Authentication + +Basic authentication is automatically set up for all endpoints. Credentials are generated if not provided, or can be specified: + +```python +service = SWMLService( + name="my-service", + basic_auth=("username", "password") +) +``` + +You can also set credentials using environment variables: +- `SWML_BASIC_AUTH_USER` +- `SWML_BASIC_AUTH_PASSWORD` + +### Dynamic SWML Generation + +You can override the `on_swml_request` method to customize SWML documents based on request data: + +```python +def on_swml_request(self, request_data=None): + if not request_data: + return None + + # Customize document based on request_data + self.reset_document() + self.add_answer_verb() + + # Add custom verbs based on request_data + if request_data.get("caller_type") == "vip": + self.add_verb("play", { + "url": "say:Welcome VIP caller!" + }) + else: + self.add_verb("play", { + "url": "say:Welcome caller!" + }) + + # Return modifications to the document + # or None to use the document we've built without modifications + return None +``` + +## Custom Routing Callbacks + +The `SWMLService` class allows you to register custom routing callbacks that can examine incoming requests and determine where they should be routed. + +### Registering a Routing Callback + +You can use the `register_routing_callback` method to register a function that will be called to process requests to a specific path: + +```python +def my_routing_callback(request, body): + """ + Process incoming requests and determine routing + + Args: + request: FastAPI Request object + body: Parsed JSON body as a dictionary + + Returns: + Optional[str]: If a string is returned, the request will be redirected to that URL. + If None is returned, the request will be processed normally. + """ + # Example: Route based on a field in the request body + if "customer_id" in body: + customer_id = body["customer_id"] + return f"/customer/{customer_id}" + + # Process request normally + return None + +# Register the callback for a specific path +service.register_routing_callback(my_routing_callback, path="/customer") +``` + +### How Routing Works + +1. When a request is received at the registered path, the routing callback is executed +2. The callback inspects the request and can decide whether to redirect it +3. If the callback returns a URL string, the request is redirected with HTTP 307 (temporary redirect) +4. If the callback returns `None`, the request is processed normally by the `on_request` method + +### Serving Different Content for Different Paths + +You can use the `callback_path` parameter passed to `on_request` to serve different content for different paths: + +```python +def on_request(self, request_data=None, callback_path=None): + """ + Called when SWML is requested + + Args: + request_data: Optional dictionary containing the parsed POST body + callback_path: Optional callback path from the request + + Returns: + Optional dict to modify/augment the SWML document + """ + # Serve different content based on the callback path + if callback_path == "/customer": + return { + "sections": { + "main": [ + {"answer": {}}, + {"play": {"url": "say:Welcome to customer service!"}} + ] + } + } + elif callback_path == "/product": + return { + "sections": { + "main": [ + {"answer": {}}, + {"play": {"url": "say:Welcome to product support!"}} + ] + } + } + + # Default content + return None +``` + +### Example: Multi-Section Service + +Here's an example of a service that uses routing callbacks to handle different types of requests: + +```python +from signalwire_agents.core.swml_service import SWMLService +from fastapi import Request +from typing import Dict, Any, Optional + +class MultiSectionService(SWMLService): + def __init__(self): + super().__init__( + name="multi-section", + route="/main" + ) + + # Create the main document + self.reset_document() + self.add_answer_verb() + self.add_verb("play", {"url": "say:Hello from the main service!"}) + self.add_verb("hangup", {}) + + # Register customer and product routes + self.register_customer_route() + self.register_product_route() + + def register_customer_route(self): + def customer_callback(request: Request, body: Dict[str, Any]) -> Optional[str]: + # Check if we need to route to a specific customer ID + if "customer_id" in body: + customer_id = body["customer_id"] + # In a real implementation, you might redirect to another service + # Here we just log it and process normally + print(f"Processing request for customer ID: {customer_id}") + return None + + # Register the callback at the /customer path + self.register_routing_callback(customer_callback, path="/customer") + + # Create the customer SWML section + self.add_section("customer_section") + self.add_verb_to_section("customer_section", "answer", {}) + self.add_verb_to_section("customer_section", "play", + {"url": "say:Welcome to customer service!"}) + self.add_verb_to_section("customer_section", "hangup", {}) + + def register_product_route(self): + def product_callback(request: Request, body: Dict[str, Any]) -> Optional[str]: + # Check if we need to route to a specific product ID + if "product_id" in body: + product_id = body["product_id"] + print(f"Processing request for product ID: {product_id}") + return None + + # Register the callback at the /product path + self.register_routing_callback(product_callback, path="/product") + + # Create the product SWML section + self.add_section("product_section") + self.add_verb_to_section("product_section", "answer", {}) + self.add_verb_to_section("product_section", "play", + {"url": "say:Welcome to product support!"}) + self.add_verb_to_section("product_section", "hangup", {}) + + def on_request(self, request_data=None, callback_path=None): + # Serve different content based on the callback path + if callback_path == "/customer": + return { + "sections": { + "main": self.get_document()["sections"]["customer_section"] + } + } + elif callback_path == "/product": + return { + "sections": { + "main": self.get_document()["sections"]["product_section"] + } + } + return None +``` + +In this example: +1. The service registers two custom route paths: `/customer` and `/product` +2. Each path has its own callback function to handle routing decisions +3. The `on_request` method uses the `callback_path` to determine which content to serve +4. Different SWML sections are served for different paths + +## Advanced Usage + +### Creating a FastAPI Router + +You can get a FastAPI router for the service to include in a larger application: + +```python +from fastapi import FastAPI + +app = FastAPI() +service = SWMLService(name="my-service") +router = service.as_router() +app.include_router(router, prefix="/voice") +``` + +### Schema Path Customization + +You can specify a custom path to the schema file: + +```python +service = SWMLService( + name="my-service", + schema_path="/path/to/schema.json" +) +``` + +## API Reference + +### Constructor Parameters + +- `name`: Service name/identifier (required) +- `route`: HTTP route path (default: "/") +- `host`: Host to bind to (default: "0.0.0.0") +- `port`: Port to bind to (default: 3000) +- `basic_auth`: Optional tuple of (username, password) +- `schema_path`: Optional path to schema.json +- `suppress_logs`: Whether to suppress structured logs (default: False) + +### Document Methods + +- `reset_document()` +- `add_verb(verb_name, config)` +- `add_section(section_name)` +- `add_verb_to_section(section_name, verb_name, config)` +- `get_document()` +- `render_document()` + +### Service Methods + +- `as_router()`: Get a FastAPI router for the service +- `run()`: Start the service +- `stop()`: Stop the service +- `get_basic_auth_credentials(include_source=False)`: Get the basic auth credentials +- `on_swml_request(request_data=None)`: Called when SWML is requested +- `register_routing_callback(callback_fn, path="/sip")`: Register a callback for request routing + +### Verb Helper Methods + +- `add_answer_verb(max_duration=None, codecs=None)` +- `add_hangup_verb(reason=None)` +- `add_ai_verb(prompt_text=None, prompt_pom=None, post_prompt=None, post_prompt_url=None, swaig=None, params=None)` + +## Examples + +### Basic Voicemail Service + +```python +from signalwire_agents.core.swml_service import SWMLService + +class VoicemailService(SWMLService): + def __init__(self, host="0.0.0.0", port=3000): + super().__init__( + name="voicemail", + route="/voicemail", + host=host, + port=port + ) + + # Build the SWML document + self.build_voicemail_document() + + def build_voicemail_document(self): + """Build the voicemail SWML document""" + # Reset the document + self.reset_document() + + # Add answer verb + self.add_answer_verb() + + # Add play verb for greeting + self.add_verb("play", { + "url": "say:Hello, you've reached the voicemail service. Please leave a message after the beep." + }) + + # Play a beep + self.add_verb("play", { + "url": "https://example.com/beep.wav" + }) + + # Record the message + self.add_verb("record", { + "format": "mp3", + "stereo": False, + "max_length": 120, # 2 minutes max + "terminators": "#" + }) + + # Thank the caller + self.add_verb("play", { + "url": "say:Thank you for your message. Goodbye!" + }) + + # Hang up + self.add_hangup_verb() + + self.log.debug("voicemail_document_built") +``` + +### Dynamic Call Routing Service + +```python +class CallRouterService(SWMLService): + def on_swml_request(self, request_data=None): + # If there's no request data, use default routing + if not request_data: + self.log.debug("no_request_data_using_default") + return None + + # Create a new document + self.reset_document() + self.add_answer_verb() + + # Get routing parameters + department = request_data.get("department", "").lower() + + # Add play verb for greeting + self.add_verb("play", { + "url": f"say:Thank you for calling our {department} department. Please hold." + }) + + # Route based on department + phone_numbers = { + "sales": "+15551112222", + "support": "+15553334444", + "billing": "+15555556666" + } + + # Get the appropriate number or use default + to_number = phone_numbers.get(department, "+15559990000") + + # Connect to the department + self.add_verb("connect", { + "to": to_number, + "timeout": 30, + "answer_on_bridge": True + }) + + # Add fallback message and hangup + self.add_verb("play", { + "url": "say:We're sorry, but all of our agents are currently busy. Please try again later." + }) + self.add_hangup_verb() + + return None # Use the document we've built +``` + +For more examples, see the `examples` directory in the SignalWire AI Agent SDK repository. \ No newline at end of file diff --git a/docs/swml/agents-sdk/skills/_category_.json b/docs/swml/agents-sdk/skills/_category_.json new file mode 100644 index 00000000..9cf7d581 --- /dev/null +++ b/docs/swml/agents-sdk/skills/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Skills System", + "collapsible": false, + "className": "menu-category", + "position": 4 +} \ No newline at end of file diff --git a/docs/swml/agents-sdk/skills/overview.md b/docs/swml/agents-sdk/skills/overview.md new file mode 100644 index 00000000..b1dff927 --- /dev/null +++ b/docs/swml/agents-sdk/skills/overview.md @@ -0,0 +1,401 @@ +--- +id: agents-sdk-skills-overview +slug: /agents-sdk/skills +title: Skills System +sidebar_label: Overview +--- + +# SignalWire Agents Skills System + +The SignalWire Agents SDK now includes a powerful, modular skills system that allows you to add capabilities to your agents with simple one-liner calls and configurable parameters. + +## What's New + +Instead of manually implementing every agent capability, you can now: + +```python +from signalwire_agents import AgentBase + +# Create an agent +agent = AgentBase("My Assistant") + +# Add skills with one-liners! +agent.add_skill("web_search") # Web search capability with default settings +agent.add_skill("datetime") # Current date/time info +agent.add_skill("math") # Mathematical calculations + +# Add skills with custom parameters! +agent.add_skill("web_search", { + "num_results": 3, # Get 3 search results instead of default 1 + "delay": 0.5 # Add 0.5s delay between requests instead of default 0 +}) + +# Your agent now has all these capabilities automatically +``` + +## Architecture + +The skills system consists of: + +### Core Infrastructure +- **`SkillBase`** - Abstract base class for all skills with parameter support +- **`SkillManager`** - Handles loading/unloading and lifecycle management with parameters +- **`AgentBase.add_skill()`** - Simple method to add skills to agents with optional parameters + +### Discovery & Registry +- **`SkillRegistry`** - Auto-discovers skills from the `skills/` directory +- **Auto-discovery** - Skills are found automatically on import +- **Validation** - Checks dependencies and environment variables + +### Built-in Skills +- **`web_search`** - Google Custom Search API integration with web scraping +- **`datetime`** - Current date/time information with timezone support +- **`math`** - Basic mathematical calculations + +## Available Skills + +### Web Search (`web_search`) +Search the internet and extract content from web pages. + +**Requirements:** +- Environment variables: `GOOGLE_SEARCH_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` +- Packages: `beautifulsoup4`, `requests` + +**Parameters:** +- `num_results` (default: 1) - Number of search results to retrieve (1-10) +- `delay` (default: 0) - Delay in seconds between web requests + +**Tools provided:** +- `web_search(query, num_results)` - Search and scrape web content + +**Usage examples:** +```python +# Default: fast single result +agent.add_skill("web_search") + +# Custom: multiple results with delay +agent.add_skill("web_search", { + "num_results": 3, + "delay": 0.5 +}) + +# Speed optimized: single result, no delay +agent.add_skill("web_search", { + "num_results": 1, + "delay": 0 +}) +``` + +### Date/Time (`datetime`) +Get current date and time information. + +**Requirements:** +- Packages: `pytz` + +**Parameters:** None (no configurable parameters) + +**Tools provided:** +- `get_current_time(timezone)` - Current time in any timezone +- `get_current_date(timezone)` - Current date in any timezone + +### Math (`math`) +Perform mathematical calculations. + +**Requirements:** None + +**Parameters:** None (no configurable parameters) + +**Tools provided:** +- `calculate(expression)` - Evaluate mathematical expressions safely + +### Native Vector Search (`native_vector_search`) +Search local document collections using vector similarity and keyword search. + +**Requirements:** +- Packages: `sentence-transformers`, `scikit-learn`, `numpy` +- Install with: `pip install signalwire-agents[search]` + +**Parameters:** +- `tool_name` (default: "search_knowledge") - Custom name for the search tool +- `index_file` (optional) - Path to local `.swsearch` index file +- `remote_url` (optional) - URL of remote search server +- `index_name` (default: "default") - Index name on remote server +- `build_index` (default: False) - Auto-build index if missing +- `source_dir` (optional) - Source directory for auto-building +- `count` (default: 3) - Number of search results to return +- `distance_threshold` (default: 0.0) - Minimum similarity score +- `response_prefix` (optional) - Text to prepend to responses +- `response_postfix` (optional) - Text to append to responses + +**Tools provided:** +- `search_knowledge(query, count)` - Search documents with hybrid vector/keyword search + +**Usage examples:** +```python +# Local mode with auto-build from concepts guide +agent.add_skill("native_vector_search", { + "tool_name": "search_docs", + "build_index": True, + "source_dir": "./docs", # Will build from directory + "index_file": "concepts.swsearch" +}) + +# Or build from specific concepts guide file +agent.add_skill("native_vector_search", { + "tool_name": "search_concepts", + "index_file": "concepts.swsearch" # Pre-built from concepts guide +}) + +# Remote mode +agent.add_skill("native_vector_search", { + "remote_url": "http://localhost:8001", + "index_name": "knowledge" +}) + +# Multiple instances for different document collections +agent.add_skill("native_vector_search", { + "tool_name": "search_examples", + "index_file": "examples.swsearch" +}) +``` + +For complete documentation, see [Local Search System](search.md). + +## Usage Examples + +### Basic Usage +```python +from signalwire_agents import AgentBase + +# Create agent and add skills +agent = AgentBase("Assistant", route="/assistant") +agent.add_skill("datetime") +agent.add_skill("math") +agent.add_skill("web_search") # Uses defaults: 1 result, no delay + +# Start the agent +agent.run() +``` + +### Skills with Custom Parameters +```python +from signalwire_agents import AgentBase + +# Create agent +agent = AgentBase("Research Assistant", route="/research") + +# Add web search optimized for research (more results) +agent.add_skill("web_search", { + "num_results": 5, # Get more comprehensive results + "delay": 1.0 # Be respectful to websites +}) + +# Add other skills without parameters +agent.add_skill("datetime") +agent.add_skill("math") + +# Start the agent +agent.run() +``` + +### Different Parameter Configurations +```python +# Speed-optimized for quick responses +agent.add_skill("web_search", { + "num_results": 1, + "delay": 0 +}) + +# Comprehensive research mode +agent.add_skill("web_search", { + "num_results": 5, + "delay": 1.0 +}) + +# Balanced approach +agent.add_skill("web_search", { + "num_results": 3, + "delay": 0.5 +}) +``` + +### Check Available Skills +```python +from signalwire_agents.skills.registry import skill_registry + +# List all discovered skills +for skill in skill_registry.list_skills(): + print(f"- {skill['name']}: {skill['description']}") + if skill['required_env_vars']: + print(f" Requires: {', '.join(skill['required_env_vars'])}") +``` + +### Runtime Skill Management +```python +agent = AgentBase("Dynamic Agent") + +# Add skills with different configurations +agent.add_skill("math") +agent.add_skill("datetime") +agent.add_skill("web_search", {"num_results": 2, "delay": 0.3}) + +# Check what's loaded +print("Loaded skills:", agent.list_skills()) + +# Remove a skill +agent.remove_skill("math") + +# Check if specific skill is loaded +if agent.has_skill("datetime"): + print("Date/time capabilities available") +``` + +## Creating Custom Skills + +Create a new skill by extending `SkillBase` with parameter support: + +```python +# signalwire_agents/skills/my_skill/skill.py +from signalwire_agents.core.skill_base import SkillBase +from signalwire_agents.core.function_result import SwaigFunctionResult + +class MyCustomSkill(SkillBase): + SKILL_NAME = "my_skill" + SKILL_DESCRIPTION = "Does something awesome with configurable parameters" + SKILL_VERSION = "1.0.0" + REQUIRED_PACKAGES = ["requests"] # Optional + REQUIRED_ENV_VARS = ["API_KEY"] # Optional + + def setup(self) -> bool: + """Initialize the skill with parameters""" + if not self.validate_env_vars() or not self.validate_packages(): + return False + + # Use parameters with defaults + self.max_items = self.params.get('max_items', 10) + self.timeout = self.params.get('timeout', 30) + self.retry_count = self.params.get('retry_count', 3) + + return True + + def register_tools(self) -> None: + """Register SWAIG tools with the agent""" + self.agent.define_tool( + name="my_function", + description=f"Does something cool (max {self.max_items} items)", + parameters={ + "input": { + "type": "string", + "description": "Input parameter" + } + }, + handler=self._my_handler + ) + + def _my_handler(self, args, raw_data): + """Handle the tool call using configured parameters""" + # Use self.max_items, self.timeout, self.retry_count in your logic + return SwaigFunctionResult(f"Processed with max_items={self.max_items}") + + def get_hints(self): + """Speech recognition hints""" + return ["custom", "skill", "awesome"] + + def get_prompt_sections(self): + """Prompt sections to add to agent""" + return [{ + "title": "Custom Capability", + "body": f"You can do custom things with my_skill (configured for {self.max_items} items)." + }] +``` + +The skill will be automatically discovered and available as: +```python +# Use defaults +agent.add_skill("my_skill") + +# Use custom parameters +agent.add_skill("my_skill", { + "max_items": 20, + "timeout": 60, + "retry_count": 5 +}) +``` + +## Quick Start + +1. **Install dependencies:** + ```bash + pip install pytz beautifulsoup4 requests + ``` + +2. **Run the demo:** + ```bash + python examples/skills_demo.py + ``` + +3. **For web search, set environment variables:** + ```bash + export GOOGLE_SEARCH_API_KEY="your_api_key" + export GOOGLE_SEARCH_ENGINE_ID="your_engine_id" + ``` + +## Testing + +Test the skills system with parameters: + +```bash +python3 -c " +from signalwire_agents import AgentBase +from signalwire_agents.skills.registry import skill_registry + +# Show discovered skills +print('Available skills:', [s['name'] for s in skill_registry.list_skills()]) + +# Create agent and load skills with parameters +agent = AgentBase('Test', route='/test') +agent.add_skill('datetime') +agent.add_skill('math') +agent.add_skill('web_search', {'num_results': 2, 'delay': 0.5}) + +print('Loaded skills:', agent.list_skills()) +print('Skills system with parameters working!') +" +``` + +## Benefits + +- **One-liner integration** - `agent.add_skill("skill_name")` +- **Configurable parameters** - `agent.add_skill("skill_name", {"param": "value"})` +- **Automatic discovery** - Drop skills in the directory and they're available +- **Dependency validation** - Checks packages and environment variables +- **Modular architecture** - Skills are self-contained and reusable +- **Extensible** - Easy to create custom skills with parameters +- **Clean separation** - Skills don't interfere with each other +- **Performance tuning** - Configure skills for speed vs. comprehensiveness + +## Migration Guide + +**Before (manual implementation):** +```python +# Had to manually implement every capability +class WebSearchAgent(AgentBase): + def __init__(self): + super().__init__("WebSearchAgent") + self.setup_google_search() + self.define_tool("web_search", ...) + # Lots of manual code... +``` + +**After (skills system with parameters):** +```python +# Simple one-liner with custom configuration +agent = AgentBase("WebSearchAgent") +agent.add_skill("web_search", { + "num_results": 3, # Get more results + "delay": 0.5 # Be respectful to servers +}) +# Done! Full web search capability with custom settings. +``` + +The skills system makes SignalWire agents more modular, maintainable, configurable, and powerful than ever before! \ No newline at end of file diff --git a/docs/swml/agents-sdk/skills/search-installation.md b/docs/swml/agents-sdk/skills/search-installation.md new file mode 100644 index 00000000..6361f5fc --- /dev/null +++ b/docs/swml/agents-sdk/skills/search-installation.md @@ -0,0 +1,312 @@ +--- +id: agents-sdk-skills-search-installation +slug: /agents-sdk/skills/search-installation +title: Search System Installation +sidebar_label: Search Installation +--- + +# SignalWire Agents Search Installation Guide + +The SignalWire Agents SDK includes optional local search capabilities that can be installed separately to avoid adding large dependencies to the base installation. + +## Which Installation Should I Choose? + +### For Development and Testing +- **`search`** - Fast, lightweight, good for development and testing +- No additional setup required +- Best for: Local development, CI/CD, resource-constrained environments + +### For Production with Document Processing +- **`search-full`** - Comprehensive document support without NLP overhead +- Handles PDF, DOCX, Excel, PowerPoint files +- Best for: Production systems that need document processing but prioritize speed + +### For Advanced Search Quality +- **`search-nlp`** - Better search relevance with advanced query processing +- Requires spaCy model download: `python -m spacy download en_core_web_sm` +- 2-3x slower than basic search +- Best for: Applications where search quality is more important than speed + +### For Everything +- **`search-all`** - Complete feature set +- Requires spaCy model download: `python -m spacy download en_core_web_sm` +- Largest installation, slowest performance +- Best for: Full-featured applications with powerful hardware + +## Installation Options + +### Basic Search +For vector embeddings and keyword search with minimal dependencies: + +```bash +pip install signalwire-agents[search] +``` + +**Size:** ~500MB +**Includes:** sentence-transformers, scikit-learn, nltk, numpy + +### Full Document Processing +For comprehensive document processing including PDF, DOCX, Excel, PowerPoint: + +```bash +pip install signalwire-agents[search-full] +``` + +**Size:** ~600MB +**Includes:** Basic search + pdfplumber, python-docx, openpyxl, python-pptx, markdown, striprtf, python-magic + +### Advanced NLP +For advanced natural language processing with spaCy: + +```bash +pip install signalwire-agents[search-nlp] +``` + +**Size:** ~600MB +**Includes:** Basic search + spaCy + +**⚠️ Additional Setup Required:** +After installation, you must download the spaCy language model: + +```bash +python -m spacy download en_core_web_sm +``` + +**Performance Note:** Advanced NLP features provide better query understanding and synonym expansion, but are significantly slower than basic search. You can control which NLP backend to use: + +- **NLTK (default)**: Fast processing, good for most use cases +- **spaCy**: Better quality but 2-3x slower, requires model download + +Use the `nlp_backend` parameter to choose: +```python +# Fast NLTK processing (default) +self.add_skill("native_vector_search", { + "nlp_backend": "nltk" # or omit for default +}) + +# Better quality spaCy processing +self.add_skill("native_vector_search", { + "nlp_backend": "spacy" # requires spaCy model download +}) +``` + +### All Features +For complete search functionality: + +```bash +pip install signalwire-agents[search-all] +``` + +**Size:** ~700MB +**Includes:** All search features combined + +**⚠️ Additional Setup Required:** +After installation, you must download the spaCy language model: + +```bash +python -m spacy download en_core_web_sm +``` + +**Performance Note:** This includes advanced NLP features which are slower but provide better search quality. + +You can control which NLP backend to use with the `nlp_backend` parameter: +- `"nltk"` (default): Fast processing +- `"spacy"`: Better quality but slower, requires model download + +## Feature Comparison + +| Feature | Basic | Full | NLP | All | +|---------|-------|------|-----|-----| +| Vector embeddings | ✅ | ✅ | ✅ | ✅ | +| Keyword search | ✅ | ✅ | ✅ | ✅ | +| Text files (txt, md) | ✅ | ✅ | ✅ | ✅ | +| PDF processing | ❌ | ✅ | ❌ | ✅ | +| DOCX processing | ❌ | ✅ | ❌ | ✅ | +| Excel/PowerPoint | ❌ | ✅ | ❌ | ✅ | +| Advanced NLP | ❌ | ❌ | ✅ | ✅ | +| POS tagging | ❌ | ❌ | ✅ | ✅ | +| Named entity recognition | ❌ | ❌ | ✅ | ✅ | + +## Checking Installation + +You can check if search functionality is available in your code: + +```python +try: + from signalwire_agents.search import IndexBuilder, SearchEngine + print("✅ Search functionality is available") +except ImportError as e: + print(f"❌ Search not available: {e}") + print("Install with: pip install signalwire-agents[search]") +``` + +## Quick Start + +Once installed, you can start using search functionality: + +### 1. Build a Search Index + +```bash +# Using the CLI tool with the comprehensive concepts guide +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch + +# Build from multiple sources (files and directories) +sw-search docs/signalwire_agents_concepts_guide.md examples README.md --file-types md,py,txt --output comprehensive.swsearch + +# Traditional directory approach +sw-search ./docs --output knowledge.swsearch --file-types md,txt,pdf + +# Or in Python +from signalwire_agents.search import IndexBuilder +from pathlib import Path + +builder = IndexBuilder() +# Build from specific file +builder.build_index_from_sources( + sources=[Path("docs/signalwire_agents_concepts_guide.md")], + output_file="concepts.swsearch", + file_types=['md'] +) + +# Build from multiple sources +builder.build_index_from_sources( + sources=[Path("docs/signalwire_agents_concepts_guide.md"), Path("examples"), Path("README.md")], + output_file="comprehensive.swsearch", + file_types=['md', 'py', 'txt'] +) +``` + +### 2. Search Documents + +```python +from signalwire_agents.search import SearchEngine +from signalwire_agents.search.query_processor import preprocess_query + +# Load search engine with concepts guide +engine = SearchEngine("concepts.swsearch") + +# Preprocess query +enhanced = preprocess_query("How do I build agents?", vector=True) + +# Search +results = engine.search( + query_vector=enhanced['vector'], + enhanced_text=enhanced['enhanced_text'], + count=5 +) + +for result in results: + print(f"Score: {result['score']:.2f}") + print(f"File: {result['metadata']['filename']}") + print(f"Content: {result['content'][:200]}...") + print("---") +``` + +### 3. Use in an Agent + +```python +from signalwire_agents import AgentBase +from signalwire_agents.core.function_result import SwaigFunctionResult + +class SearchAgent(AgentBase): + def __init__(self): + super().__init__(name="search-agent") + + # Check if search is available + try: + from signalwire_agents.search import SearchEngine + self.search_engine = SearchEngine("concepts.swsearch") + self.search_available = True + except ImportError: + self.search_available = False + + @AgentBase.tool( + name="search_knowledge", + description="Search the knowledge base", + parameters={ + "query": {"type": "string", "description": "Search query"} + } + ) + def search_knowledge(self, args, raw_data): + if not self.search_available: + return SwaigFunctionResult( + "Search not available. Install with: pip install signalwire-agents[search]" + ) + + # Perform search... + return SwaigFunctionResult("Search results...") +``` + +## Troubleshooting + +### Common Issues + +1. **ImportError: No module named 'sentence_transformers'** + ```bash + pip install signalwire-agents[search] + ``` + +2. **ImportError: No module named 'pdfplumber'** + ```bash + pip install signalwire-agents[search-full] + ``` + +3. **NLTK data not found** + ```python + import nltk + nltk.download('punkt') + nltk.download('averaged_perceptron_tagger') + ``` + +4. **spaCy model not found** + ```bash + python -m spacy download en_core_web_sm + ``` + + If you see "spaCy model 'en_core_web_sm' not found. Falling back to NLTK", this means the spaCy language model wasn't installed. This is required for `search-nlp` and `search-all` installations. + +5. **Large download sizes** + - The sentence-transformers library downloads pre-trained models (~400MB) + - This happens on first use and is cached locally + - Use `search` instead of `search-all` if you don't need document processing + +### Performance Tips + +1. **Model Selection**: Use smaller models for faster inference: + ```python + builder = IndexBuilder(model_name='sentence-transformers/all-MiniLM-L6-v2') + ``` + +2. **Chunk Size**: Adjust chunk size based on your documents: + ```python + builder = IndexBuilder(chunk_size=300, chunk_overlap=30) # Smaller chunks + ``` + +3. **File Filtering**: Only index relevant file types: + ```python + builder.build_index( + source_dir="./docs", + file_types=['md', 'txt'], # Skip heavy formats like PDF + exclude_patterns=['**/test/**', '**/__pycache__/**'] + ) + ``` + +## Uninstalling + +To remove search dependencies: + +```bash +pip uninstall sentence-transformers scikit-learn nltk +# Add other packages as needed +``` + +The core SignalWire Agents SDK will continue to work without search functionality. + +## Support + +For issues with search functionality: + +1. Check the [GitHub Issues](https://github.com/signalwire/signalwire-ai-agents/issues) +2. Verify your Python version (3.7+ required) +3. Try reinstalling with `--force-reinstall` +4. Check available disk space (models require ~1GB total) \ No newline at end of file diff --git a/docs/swml/agents-sdk/skills/search.md b/docs/swml/agents-sdk/skills/search.md new file mode 100644 index 00000000..6e5775a1 --- /dev/null +++ b/docs/swml/agents-sdk/skills/search.md @@ -0,0 +1,618 @@ +--- +id: agents-sdk-skills-search +slug: /agents-sdk/skills/search +title: Search System +sidebar_label: Search System +--- + +# Local Search System + +The SignalWire Agents SDK includes a powerful local search system that provides DataSphere-compatible search functionality without external dependencies. This system uses advanced query preprocessing, local embeddings, and hybrid search techniques to enable agents to search through document collections offline. + + +## Overview + +The local search system provides: + +- **Offline Search**: No external API calls or internet required +- **Hybrid Search**: Combines vector similarity and keyword search +- **Document Processing**: Supports multiple file formats (Markdown, PDF, DOCX, etc.) +- **Smart Chunking**: Intelligent document segmentation with context preservation +- **Advanced Query Processing**: NLP-enhanced query understanding +- **Flexible Deployment**: Local embedded mode or remote server mode +- **SQLite Storage**: Portable `.swsearch` index files + +### Architecture + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Documents │───▶│ Index Builder │───▶│ .swsearch DB │ +│ (MD, PDF, etc.) │ │ │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Agent │───▶│ Search Skill │───▶│ Search Engine │ +│ │ │ │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ +``` + +## Installation Options + +The search system uses optional dependencies to keep the base SDK lightweight. Choose the installation option that fits your needs: + +### Basic Search (~500MB) +```bash +pip install signalwire-agents[search] +``` +**Includes:** +- Core search functionality +- Sentence transformers for embeddings +- SQLite FTS5 for keyword search +- Basic document processing (text, markdown) + +### Full Document Processing (~600MB) +```bash +pip install signalwire-agents[search-full] +``` +**Adds:** +- PDF processing (PyPDF2) +- DOCX processing (python-docx) +- HTML processing (BeautifulSoup4) +- Additional file format support + +### Advanced NLP Features (~700MB) +```bash +pip install signalwire-agents[search-nlp] +``` +**Adds:** +- spaCy for advanced text processing +- NLTK for linguistic analysis +- Enhanced query preprocessing +- Language detection + +**⚠️ Additional Setup Required:** +```bash +python -m spacy download en_core_web_sm +``` + +**Performance Note:** Advanced NLP features provide significantly better query understanding, synonym expansion, and search relevance, but are 2-3x slower than basic search. Only recommended if you have sufficient CPU power and can tolerate longer response times. + +**NLP Backend Control:** You can choose which NLP backend to use: +- **NLTK (default)**: Fast processing, good for most use cases +- **spaCy**: Better quality but slower, requires model download + +Configure via the `nlp_backend` parameter in your search skill. + +### All Search Features (~700MB) +```bash +pip install signalwire-agents[search-all] +``` +**Includes everything above** + +**⚠️ Additional Setup Required:** +```bash +python -m spacy download en_core_web_sm +``` + +**Performance Note:** This includes advanced NLP features which improve search quality but increase response times. + +### Minimal Installation (Base SDK only) +```bash +pip install signalwire-agents +``` +Search functionality will show helpful error messages when dependencies are missing. + +## Quick Start + +### 1. Install Dependencies +```bash +pip install signalwire-agents[search-full] +``` + +### 2. Build a Search Index +```bash +# Build from the comprehensive concepts guide +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch + +# Build from multiple individual files +sw-search README.md docs/agent_guide.md docs/architecture.md --output knowledge.swsearch + +# Build from mixed sources (files and directories) +sw-search docs/signalwire_agents_concepts_guide.md examples --file-types md,py --output comprehensive.swsearch + +# Build from a directory (traditional approach) +sw-search docs --output docs.swsearch + +# Include specific file types +sw-search docs --file-types md,txt,py + +# Exclude patterns +sw-search docs --exclude "**/test/**,**/__pycache__/**" +``` + +### 3. Use in Your Agent +```python +from signalwire_agents import AgentBase + +class MyAgent(AgentBase): + def __init__(self): + super().__init__() + + # Add search capability using the concepts guide + self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "description": "Search the comprehensive SDK concepts guide for information", + "index_file": "concepts.swsearch", + "count": 5 + }) + +agent = MyAgent() +agent.serve() +``` + +### 4. Test the Search +Ask your agent: *"How do I create a new agent?"* and it will search the comprehensive concepts guide to provide detailed answers. + +## Building Search Indexes + +Search indexes are SQLite databases with the `.swsearch` extension that contain processed documents, embeddings, and search metadata. + +### Basic Index Building + +```bash +# Build index from the comprehensive concepts guide +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch + +# Build from multiple individual files +sw-search README.md docs/agent_guide.md docs/architecture.md --output knowledge.swsearch + +# Build from mixed sources (files and directories) +sw-search docs/signalwire_agents_concepts_guide.md examples --file-types md,py --output comprehensive.swsearch + +# Build from a directory (traditional approach) +sw-search docs --output docs.swsearch + +# Include specific file types +sw-search docs --file-types md,txt,py + +# Exclude patterns +sw-search docs --exclude "**/test/**,**/__pycache__/**" +``` + +### Advanced Index Building + +```bash +# Full configuration example with multiple sources +sw-search docs/signalwire_agents_concepts_guide.md ./examples README.md \ + --output ./knowledge.swsearch \ + --chunking-strategy sentence \ + --max-sentences-per-chunk 8 \ + --file-types md,txt,rst,py \ + --exclude "**/test/**,**/__pycache__/**" \ + --model sentence-transformers/all-mpnet-base-v2 \ + --tags documentation,api \ + --verbose +``` + +### Supported File Types + +| Format | Extension | Requirements | +|--------|-----------|--------------| +| Markdown | `.md` | Built-in | +| Text | `.txt` | Built-in | +| Python | `.py` | Built-in | +| reStructuredText | `.rst` | Built-in | +| PDF | `.pdf` | `search-full` | +| Word Documents | `.docx` | `search-full` | +| HTML | `.html` | `search-full` | +| JSON | `.json` | Built-in | + +### Index Structure + +Each `.swsearch` file contains: + +- **Document chunks** with embeddings and metadata +- **Full-text search index** (SQLite FTS5) +- **Configuration** and model information +- **Synonym cache** for query expansion + +## Using the Search Skill + +The `native_vector_search` skill provides search functionality to your agents. + +### Basic Configuration + +```python +self.add_skill("native_vector_search", { + "tool_name": "search_knowledge", + "description": "Search the knowledge base", + "index_file": "knowledge.swsearch" +}) +``` + +### Advanced Configuration + +### NLP Backend Selection + +Choose between NLTK (fast) and spaCy (better quality) for query processing: + +```python +# Fast NLTK processing (default) +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", + "nlp_backend": "nltk" # Fast, good for most use cases +}) + +# Better quality spaCy processing +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", + "nlp_backend": "spacy" # Slower but better quality, requires model download +}) +``` + +**Performance Comparison:** +- **NLTK**: ~50-100ms query processing, good synonym expansion +- **spaCy**: ~150-300ms query processing, better POS tagging and entity recognition + +### Custom Embedding Models + +```python +# Use a different embedding model +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", + "model": "sentence-transformers/all-MiniLM-L6-v2" # Smaller, faster model +}) +``` + +### Query Enhancement + +The system automatically enhances queries using: +- **Language detection** +- **POS tagging** (with NLP dependencies) +- **Synonym expansion** using WordNet +- **Keyword extraction** +- **Vector embeddings** + +### Response Customization + +```python +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", + + # Customize responses for voice calls + "response_prefix": "Based on the documentation, here's what I found:", + "response_postfix": "Would you like me to search for more specific information?", + + # Custom no-results message + "no_results_message": "I couldn't find information about '{query}'. Try rephrasing your question.", + + # SWAIG function fillers for natural conversation + "swaig_fields": { + "fillers": { + "en-US": [ + "Let me search the documentation", + "Checking our knowledge base", + "Looking that up for you" + ] + } + } +}) +``` + +### Tag-Based Filtering + +```python +# Only search documents tagged with specific categories +self.add_skill("native_vector_search", { + "tool_name": "search_api_docs", + "index_file": "docs.swsearch", + "tags": ["api", "reference"], # Only search API docs + "description": "Search API reference documentation" +}) +``` + +### Complete Configuration Example + +```python +self.add_skill("native_vector_search", { + # Tool configuration + "tool_name": "search_docs", + "description": "Search SDK documentation for detailed information", + + # Index configuration + "index_file": "docs.swsearch", + "build_index": True, # Auto-build if missing + "source_dir": "./docs", # Source for auto-build + "file_types": ["md", "txt"], + + # Search parameters + "count": 5, # Number of results + "distance_threshold": 0.1, # Similarity threshold + "tags": ["documentation"], # Filter by tags + + # NLP backend selection + "nlp_backend": "nltk", # or "spacy" for better quality + + # Response formatting + "response_prefix": "Based on the documentation:", + "response_postfix": "Would you like more details?", + "no_results_message": "No information found for '{query}'", + + # SWAIG configuration + "swaig_fields": { + "fillers": { + "en-US": ["Let me search for that", "Checking the docs"] + } + } +}) +``` + +### Multiple Search Instances + +You can add multiple search instances for different document collections: + +```python +# Documentation search with spaCy for better quality +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", + "nlp_backend": "spacy", + "description": "Search SDK documentation" +}) + +# Code examples search with NLTK for speed +self.add_skill("native_vector_search", { + "tool_name": "search_examples", + "index_file": "examples.swsearch", + "nlp_backend": "nltk", + "description": "Search code examples" +}) +``` + +## Local vs Remote Modes + +The search skill supports both local and remote operation modes. + +### Local Mode (Default) + +**Pros:** +- Faster (no network latency) +- Works offline +- Simple deployment +- Lower operational complexity + +**Cons:** +- Higher memory usage per agent +- Index files must be distributed with each agent +- Updates require redeploying agents + +**Configuration:** +```python +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "index_file": "docs.swsearch", # Local file + "nlp_backend": "nltk" # Choose NLP backend +}) +``` + +### Remote Mode + +**Pros:** +- Lower memory usage per agent +- Centralized index management +- Easy updates without redeploying agents +- Better scalability for multiple agents +- Shared resources + +**Cons:** +- Network dependency +- Additional infrastructure complexity +- Potential latency + +**Configuration:** +```python +self.add_skill("native_vector_search", { + "tool_name": "search_docs", + "remote_url": "http://localhost:8001", # Search server + "index_name": "docs", # Index name on server + "nlp_backend": "nltk" # NLP backend for query preprocessing +}) +``` + +### Running a Remote Search Server + +1. **Start the search server:** +```bash +python examples/search_server_standalone.py +``` + +2. **The server provides HTTP API:** +- `POST /search` - Search the indexes +- `GET /health` - Health check and available indexes +- `POST /reload_index` - Add or reload an index + +3. **Test the API:** +```bash +curl -X POST "http://localhost:8001/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "how to create an agent", "index_name": "docs", "count": 3}' +``` + +### Automatic Mode Detection + +The skill automatically detects which mode to use: +- If `remote_url` is provided → Remote mode +- If `index_file` is provided → Local mode +- Remote mode takes priority if both are specified + +## Advanced Configuration + +### Custom Embedding Models + +## CLI Reference + +### sw-search Command + +```bash +sw-search [options] +``` + +**Arguments:** +- `source_dir` - Directory containing documents to index + +**Options:** +- `--output FILE` - Output .swsearch file (default: `.swsearch`) +- `--chunk-size SIZE` - Chunk size in characters (default: 500) +- `--chunk-overlap SIZE` - Overlap between chunks (default: 50) +- `--file-types TYPES` - Comma-separated file extensions (default: md,txt,rst) +- `--exclude PATTERNS` - Comma-separated glob patterns to exclude +- `--languages LANGS` - Comma-separated language codes (default: en) +- `--model MODEL` - Embedding model name (default: sentence-transformers/all-mpnet-base-v2) +- `--tags TAGS` - Comma-separated tags to add to all chunks +- `--verbose` - Show detailed progress information +- `--validate` - Validate the created index after building + +**Subcommands:** + +#### validate - Validate Search Index + +```bash +sw-search validate [--verbose] +``` + +Validates an existing .swsearch index file and shows statistics. + +#### search - Search Within Index + +```bash +sw-search search [options] +``` + +Search within an existing .swsearch index file. This is useful for: +- Testing search quality and relevance +- Exploring index contents +- Debugging search results +- Scripting and automation + +**Search Options:** +- `--count COUNT` - Number of results to return (default: 5) +- `--distance-threshold FLOAT` - Minimum similarity score (default: 0.0) +- `--tags TAGS` - Comma-separated tags to filter by +- `--nlp-backend {nltk,spacy}` - NLP backend to use (default: nltk) +- `--verbose` - Show detailed information including index stats +- `--json` - Output results as JSON for scripting +- `--no-content` - Hide content in results (show only metadata) + +**Examples:** + +```bash +# Build from the comprehensive concepts guide +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch + +# Build from multiple sources (files and directories) +sw-search docs/signalwire_agents_concepts_guide.md examples README.md \ + --output comprehensive.swsearch \ + --file-types md,py,txt \ + --verbose + +# Traditional directory-based approach +sw-search ./documentation \ + --output knowledge.swsearch \ + --chunking-strategy sentence \ + --max-sentences-per-chunk 8 \ + --file-types md,rst,txt \ + --exclude "**/drafts/**" \ + --tags documentation,help \ + --verbose + +# Validate an existing index +sw-search validate concepts.swsearch --verbose + +# Search within an index +sw-search search concepts.swsearch "how to create an agent" +sw-search search concepts.swsearch "API reference" --count 3 --verbose +sw-search search concepts.swsearch "configuration" --tags documentation --json + +# Use different NLP backends +sw-search search concepts.swsearch "deployment options" --nlp-backend nltk # Fast +sw-search search concepts.swsearch "deployment options" --nlp-backend spacy # Better quality + +# Advanced search with filtering +sw-search search concepts.swsearch "deployment options" \ + --count 10 \ + --distance-threshold 0.1 \ + --tags "deployment,production" \ + --nlp-backend spacy \ + --verbose + +# JSON output for scripting +sw-search search concepts.swsearch "error handling" --json | jq '.results[0].content' + +# Build multiple specialized indexes +sw-search docs/signalwire_agents_concepts_guide.md --output concepts.swsearch +sw-search examples --output examples.swsearch --file-types py,md +``` + +### Index Validation + +```bash +# Validate an existing index +python -c " +from signalwire_agents.search import SearchEngine +engine = SearchEngine('docs.swsearch') +print(f'Index stats: {engine.get_stats()}') +" +``` + +## API Reference + +### SearchEngine Class + +```python +from signalwire_agents.search import SearchEngine + +# Load an index +engine = SearchEngine("docs.swsearch") + +# Perform search +results = engine.search( + query_vector=[...], # Optional: pre-computed query vector + enhanced_text="search query", # Enhanced query text + count=5, # Number of results + distance_threshold=0.0, # Minimum similarity score + tags=["documentation"] # Filter by tags +) + +# Get index statistics +stats = engine.get_stats() +print(f"Total chunks: {stats['total_chunks']}") +print(f"Total files: {stats['total_files']}") +``` + +### IndexBuilder Class + +```python +from signalwire_agents.search import IndexBuilder + +# Create index builder +builder = IndexBuilder( + model_name="sentence-transformers/all-mpnet-base-v2", + chunk_size=500, + chunk_overlap=50, + verbose=True +) + +# Build index +builder.build_index( + source_dir="./docs", + output_file="docs.swsearch", + file_types=["md", "txt"], + exclude_patterns=["**/test/**"], + tags=["documentation"] +) +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0f20bcb9..62222600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "docusaurus-plugin-markdown-printer": "./plugins/markdown-printer", "docusaurus-plugin-showcase": "./plugins/docusaurus-plugin-showcase", "linkinator": "^5.0.2", + "mdx2vast": "^0.3.0", "prettier": "^3.3.3", "sharp": "^0.33.4", "shx": "^0.3.4", @@ -6836,6 +6837,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/acorn": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", + "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -7340,6 +7351,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -14736,6 +14754,13 @@ "node": ">= 0.6" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "dev": true, + "license": "ISC" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -14896,6 +14921,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -16586,6 +16624,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -17743,6 +17791,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", @@ -17895,181 +17963,1467 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "license": "CC0-1.0" }, - "node_modules/media-typer": { + "node_modules/mdx2vast": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "resolved": "https://registry.npmjs.org/mdx2vast/-/mdx2vast-0.3.0.tgz", + "integrity": "sha512-xRfRHp6h7bho6esXxKzPA5r3mSmjrKFGyaEBeYwqtK/eSdEYAeAqsG7BJJaxLb/EoIbvgM9VUIdPN4vOyTtf5g==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/medium-zoom": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz", - "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==", - "license": "MIT" - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.4" + "@mdx-js/mdx": "^2.3.0", + "commander": "^11.0.0", + "fs": "^0.0.1-security", + "get-stdin": "^9.0.0", + "hast-util-to-html": "^9.0.5", + "hastscript": "^9.0.1", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-math": "^3.0.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-hast": "^13.2.0", + "micromark-extension-math": "^3.1.0", + "micromark-extension-mdxjs": "^3.0.0", + "path": "^0.12.7" + }, + "bin": { + "mdx2vast": "bin/cli.js" }, "engines": { - "node": ">= 4.0.0" + "node": ">=18" } }, - "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "node_modules/mdx2vast/node_modules/@mdx-js/mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", + "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=16.10" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/mdx": "^2.0.0", + "estree-util-build-jsx": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-util-to-js": "^1.1.0", + "estree-walker": "^3.0.0", + "hast-util-to-estree": "^2.0.0", + "markdown-extensions": "^1.0.0", + "periscopic": "^3.0.0", + "remark-mdx": "^2.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "unified": "^10.0.0", + "unist-util-position-from-estree": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "node_modules/mdx2vast/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@types/unist": "^2" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "node_modules/mdx2vast/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdx2vast/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdx2vast/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=16" } }, - "node_modules/mermaid": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz", - "integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==", + "node_modules/mdx2vast/node_modules/estree-util-attach-comments": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", + "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==", + "dev": true, "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.0.4", - "@iconify/utils": "^2.1.33", - "@mermaid-js/parser": "^0.4.0", - "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.2.0", - "d3": "^7.9.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.11", - "dayjs": "^1.11.13", - "dompurify": "^3.2.4", - "katex": "^0.16.9", - "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^15.0.7", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mermaid/node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "node_modules/mdx2vast/node_modules/estree-util-build-jsx": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz", + "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==", + "dev": true, "license": "MIT", - "bin": { - "marked": "bin/marked.js" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-walker": "^3.0.0" }, - "engines": { - "node": ">= 18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mermaid/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/mdx2vast/node_modules/estree-util-is-identifier-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz", + "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==", + "dev": true, "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/mdx2vast/node_modules/estree-util-to-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", + "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mdx2vast/node_modules/estree-util-visit": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz", + "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mdx2vast/node_modules/hast-util-to-estree": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", + "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "estree-util-attach-comments": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.1", + "unist-util-position": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdx2vast/node_modules/markdown-extensions": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", + "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdx-expression": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", + "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdx-expression/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdx-jsx": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz", + "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "mdast-util-from-markdown": "^1.1.0", + "mdast-util-to-markdown": "^1.3.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdx-jsx/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdxjs-esm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz", + "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-mdxjs-esm/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-extension-mdx-expression": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz", + "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-extension-mdx-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz", + "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/micromark-extension-mdx-md": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz", + "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/micromark-extension-mdxjs-esm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz", + "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-core-commonmark": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.1.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-factory-mdx-expression": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz", + "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdx2vast/node_modules/micromark-util-events-to-acorn": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz", + "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "@types/unist": "^2.0.0", + "estree-util-visit": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdx2vast/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mdx2vast/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdx2vast/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdx2vast/node_modules/remark-mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz", + "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^2.0.0", + "micromark-extension-mdxjs": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-mdx/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-mdx/node_modules/mdast-util-mdx": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz", + "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-mdx/node_modules/micromark-extension-mdxjs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz", + "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "micromark-extension-mdx-jsx": "^1.0.0", + "micromark-extension-mdx-md": "^1.0.0", + "micromark-extension-mdxjs-esm": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/remark-rehype/node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/mdx2vast/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-position-from-estree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz", + "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-remove-position": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", + "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx2vast/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/medium-zoom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz", + "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==", + "license": "MIT" + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz", + "integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.4", + "@iconify/utils": "^2.1.33", + "@mermaid-js/parser": "^0.4.0", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.13", + "dompurify": "^3.2.4", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^15.0.7", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", @@ -18614,6 +19968,85 @@ ], "license": "MIT" }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -21254,6 +22687,18 @@ "node": ">=0.12" } }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -27047,6 +28492,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", diff --git a/package.json b/package.json index f4206cbf..910b0515 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@mdx-js/react": "^3.0.1", "@niravcodes/call-widget": "^2.1.0", "@popperjs/core": "^2.11.8", + "@signalwire/docusaurus-plugin-llms-txt": "1.0.1", "@typespec/compiler": "1.0.0", "@typespec/http": "1.0.0", "@typespec/openapi": "1.0.0", @@ -60,7 +61,6 @@ "docusaurus-plugin-sass": "0.2.6", "docusaurus-theme-openapi-docs": "4.3.0", "docusaurus-theme-search-typesense": "^0.24.0", - "@signalwire/docusaurus-plugin-llms-txt": "1.0.1", "dompurify": "^3.2.4", "dotenv": "^16.4.5", "plugin-image-zoom": "https://github.com/flexanalytics/plugin-image-zoom.git", @@ -99,6 +99,7 @@ "docusaurus-plugin-markdown-printer": "./plugins/markdown-printer", "docusaurus-plugin-showcase": "./plugins/docusaurus-plugin-showcase", "linkinator": "^5.0.2", + "mdx2vast": "^0.3.0", "prettier": "^3.3.3", "sharp": "^0.33.4", "shx": "^0.3.4", diff --git a/src/components/InstallHero/example.mdx b/src/components/InstallHero/example.mdx new file mode 100644 index 00000000..dc362021 --- /dev/null +++ b/src/components/InstallHero/example.mdx @@ -0,0 +1,45 @@ +--- +title: InstallHero Example +--- + +import InstallHero from './index'; + +# InstallHero Component Example + +The InstallHero component provides a beautiful and consistent way to display installation instructions for your SDK or package. + +## Basic Usage + + + +## Beta Version Example + + + +## Minimal Example + + \ No newline at end of file diff --git a/src/components/InstallHero/index.tsx b/src/components/InstallHero/index.tsx new file mode 100644 index 00000000..a924f297 --- /dev/null +++ b/src/components/InstallHero/index.tsx @@ -0,0 +1,109 @@ +import React, { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; +import { FaGithub, FaBook, FaRocket } from 'react-icons/fa'; +import { SiPypi } from 'react-icons/si'; +import { AlphaBadge, BetaBadge } from '../AlphaBetaBadges'; +import CodeBlock from '@theme/CodeBlock'; + +interface InstallHeroProps { + title: string; + installCommand: string; + language?: string; + githubUrl?: string; + docsUrl?: string; + gettingStartedUrl?: string; + status?: 'stable' | 'beta' | 'alpha'; + pypiPackage?: string; + className?: string; + children?: ReactNode; +} + +export default function InstallHero({ + title, + installCommand, + language = 'bash', + githubUrl, + docsUrl, + gettingStartedUrl, + status = 'stable', + pypiPackage, + className, + children, +}: InstallHeroProps): JSX.Element { + const renderStatusBadge = () => { + if (status === 'alpha') return ; + if (status === 'beta') return ; + return null; + }; + + const getCodeBlockWidth = () => { + const commandLength = installCommand.length; + if (commandLength < 30) return '400px'; + if (commandLength < 50) return '500px'; + if (commandLength < 80) return '600px'; + return '100%'; + }; + + const getGitHubRepo = (url: string) => { + return url.replace('https://github.com/', ''); + }; + + return ( +
+
+
+

{title}

+ {status !== 'stable' && renderStatusBadge()} +
+ + {children && ( +
+ {children} +
+ )} + +
+
+ + {installCommand} + +
+
+ +
+ {githubUrl && ( + + GitHub + GitHub stars + + )} + {pypiPackage && ( + + PyPI + PyPI version + + )} + {docsUrl && ( + + Documentation + + )} + {gettingStartedUrl && ( + + Getting Started + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/InstallHero/styles.module.css b/src/components/InstallHero/styles.module.css new file mode 100644 index 00000000..91727b94 --- /dev/null +++ b/src/components/InstallHero/styles.module.css @@ -0,0 +1,117 @@ +.installHero { + background: var(--ifm-background-color); + padding: 2rem 0; + margin: 2rem 0; + border-radius: var(--ifm-card-border-radius); + border: 1px solid var(--ifm-color-emphasis-200); +} + +.content { + max-width: var(--ifm-container-width); + margin: 0 auto; + padding: 0 var(--ifm-spacing-horizontal); +} + +.header { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.title { + margin: 0; + font-size: var(--ifm-h1-font-size); + font-weight: var(--ifm-h1-font-weight); + line-height: var(--ifm-h1-line-height); + color: var(--ifm-heading-color); +} + +.description { + margin-bottom: 2rem; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +.installSection { + margin: 2rem 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.codeBlockWrapper { + width: 100%; + margin: 0 auto; +} + +/* Center text within the installation code block only */ +.codeBlockWrapper :global(.theme-code-block) code { + text-align: center; +} + +.links { + display: flex; + gap: 2rem; + margin-top: 2rem; + justify-content: center; + align-items: center; +} + +.link { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--ifm-color-primary); + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.link:hover { + color: var(--ifm-color-primary-darker); + text-decoration: none; +} + +.linkShield { + height: 20px; + margin-left: 0.5rem; +} + +/* Responsive styles */ +@media (max-width: 996px) { + .title { + font-size: 2rem; + } + + .links { + gap: 1.5rem; + } +} + +@media (max-width: 768px) { + .installHero { + padding: 1.5rem 0; + } + + .title { + font-size: 1.75rem; + } + + .header { + flex-direction: column; + gap: 0.5rem; + } + + .links { + flex-direction: column; + gap: 1rem; + } + + .codeBlockWrapper { + max-width: 100% !important; + } +} \ No newline at end of file diff --git a/src/css/_signalwire.scss b/src/css/_signalwire.scss index 080174fb..53d7bbd2 100644 --- a/src/css/_signalwire.scss +++ b/src/css/_signalwire.scss @@ -17,9 +17,10 @@ This includes gradients for Navbar and links. font-size: 14px; box-shadow: none; // remove default shadow - .navbar__items a > svg { - display: none; - } +// Uncomment this selector to hide the 'external link' icons in the navbar +// .navbar__items a > svg { +// display: none; +// } a { transition: color 100ms ease; // Smooth transition for text color diff --git a/src/pages/internal/extras/InstallHero.mdx b/src/pages/internal/extras/InstallHero.mdx new file mode 100644 index 00000000..f46ae1ce --- /dev/null +++ b/src/pages/internal/extras/InstallHero.mdx @@ -0,0 +1,162 @@ +# InstallHero Component + +import InstallHero from "@site/src/components/InstallHero"; + +The `InstallHero` component provides a beautiful and consistent way to display installation instructions for SDKs and packages. It includes features like copy-to-clipboard functionality, version display, status badges, and prerequisites. + +## Basic Usage + + + +### InstallHero Props + +| Prop | Type | Description | +|------|------|-------------| +| title | string | The SDK/package name | +| description | string | Optional description of the SDK/package | +| installCommand | string | The command to install the package | +| language | string | The language of the install command (default: "bash") | +| version | string | Optional version number to display | +| githubUrl | string | Optional link to GitHub repository | +| docsUrl | string | Optional link to documentation | +| gettingStartedUrl | string | Optional link to getting started guide | +| status | "stable" \| "beta" \| "alpha" | Optional status badge (default: "stable") | +| prerequisites | string[] | Optional list of prerequisites | +| className | string | Additional CSS classes | + +## Examples + +### Beta Version + + + +### Alpha Version + + + +### Minimal Example + + + +### With Custom Language + + + +## Example Usage + +```jsx +import InstallHero from '@site/src/components/InstallHero'; + + + A powerful SDK for building AI agents with SignalWire's platform. + + ## Prerequisites + + - Python 3.8 or higher + - A SignalWire account + - pip package manager + +``` + +## Live Example + + + A powerful SDK for building AI agents with SignalWire's platform. + + ## Prerequisites + + - Python 3.8 or higher + - A SignalWire account + - pip package manager + + +## Props + +- `title`: The main title +- `installCommand`: The installation command to display +- `language`: Code block language (default: 'bash') +- `githubUrl`: GitHub repository URL +- `docsUrl`: Documentation URL +- `gettingStartedUrl`: Getting started guide URL +- `status`: 'stable', 'beta', or 'alpha' +- `pypiPackage`: PyPI package name for version fetching +- `children`: Markdown content to display between title and code block \ No newline at end of file diff --git a/src/pages/internal/index.mdx b/src/pages/internal/index.mdx new file mode 100644 index 00000000..3dfdba95 --- /dev/null +++ b/src/pages/internal/index.mdx @@ -0,0 +1,116 @@ +--- +title: Internal Components +description: A collection of reusable components for the SignalWire documentation +--- + +import { Card, CardGroup } from "@site/src/components/Extras/Card"; +import { + MdViewModule, + MdCode, + MdPhotoLibrary, + MdExpandMore, + MdViewCarousel, + MdInstallDesktop, + MdNewReleases, + MdImage +} from "react-icons/md"; + +# Internal Components + +This directory contains reusable components that can be used throughout the SignalWire documentation. + + + +} + href="/internal/extras/InstallHero" +> + A beautiful and consistent way to display installation instructions for SDKs and packages. + + +} + href="/internal/extras/Card" +> + A versatile UI element that can display content in a structured format with optional title, icon, and image. + + +} + href="/internal/extras/CodeGroup" +> + Display multiple code blocks in a tabbed interface, making it easy to showcase different implementations. + + +} + href="/internal/extras/Accordion" +> + Create collapsible content sections that can be expanded and collapsed by the user. + + +} + href="/internal/extras/Slideshow" +> + Display a series of images or content in a carousel format. + + +} + href="/internal/extras/ReleaseCard" +> + Showcase release information in a structured and visually appealing way. + + +} + href="/internal/extras/Frame" +> + Create bordered containers for content with optional styling options. + + +} + href="/internal/extras/Icons" +> + A collection of icons available for use throughout the documentation. + + + + +## Usage + +To use these components in your documentation: + +1. Import the component from its source +2. Use the component with the appropriate props +3. Refer to the component's documentation page for detailed usage examples + +For example: + +```jsx +import { Card } from "@site/src/components/Extras/Card"; + + +``` + +## Contributing + +When adding new components: + +1. Create the component in the appropriate directory +2. Add comprehensive documentation +3. Include usage examples +4. Add the component to this directory page \ No newline at end of file