{"slug":"signalwire-python-agents-sdk","title":"SignalWire Python Agents SDK","tags":["signalwire","python","agents-sdk","voice-ai","swaig","swml"],"agent_summary":"AgentBase setup, fluent prompt and parameter configuration, the @tool decorator for SWAIG, FunctionResult chaining, and the AgentServer pattern for serving multiple agents on one port.","trigger_phrases":["how do I build a SignalWire AI agent in python","AgentBase setup","register SWAIG tool decorator","FunctionResult connect hangup","AgentServer multi-agent","post_prompt url callback","Python voice AI SignalWire"],"runnable":true,"markdown":"\n# SignalWire Python Agents SDK\n\nPython SDK that auto-generates SWML, registers SWAIG endpoints, and runs a FastAPI/uvicorn server. The primary class is `AgentBase`. Nine mixins compose the feature set: prompt, tool, skill, AI config, web, auth, serverless, state, MCP server. Every setter returns `self` for fluent chaining.\n\n## Install\n\n```bash\npip install signalwire-agents\npip install signalwire-pom  # only when using PomBuilder directly\n```\n\n## Key imports\n\n```python\nfrom signalwire import AgentBase, AgentServer, SWMLService\nfrom signalwire import SWAIGFunction, FunctionResult, DataMap\nfrom signalwire import (\n    create_simple_context, create_simple_api_tool,\n    create_expression_tool, register_skill, add_skill_directory,\n)\nfrom signalwire.core.pom_builder import PomBuilder\nfrom signalwire.core.skill_base import SkillBase\nfrom signalwire.mcp_gateway import MCPGateway\nfrom signalwire.prefabs import (\n    ConciergeAgent, FAQBotAgent, InfoGathererAgent,\n    ReceptionistAgent, SurveyAgent,\n)\n```\n\n## AgentBase constructor\n\n| Parameter | Type | Default | Notes |\n|---|---|---|---|\n| `name` | str | required | Display name, used in logging and SIP username mapping |\n| `route` | str | `\"/\"` | HTTP route path |\n| `host` | str | `\"0.0.0.0\"` | Bind address |\n| `port` | int | `PORT` env or `3000` | Listen port |\n| `agent_id` | str | auto UUID | Unique instance ID |\n| `use_pom` | bool | `True` | Enable Prompt Object Model |\n| `auto_answer` | bool | `True` | Add `answer` verb before `ai` in SWML |\n| `record_call` | bool | `False` | Enable call recording |\n| `record_format` | str | `\"mp4\"` | `\"mp4\"` or `\"wav\"` |\n| `record_stereo` | bool | `True` | Separate channel per party |\n| `basic_auth` | tuple[str,str] | env or auto | `(username, password)` |\n| `token_expiry_secs` | int | `3600` | SWAIG auth token TTL |\n| `native_functions` | list[str] | None | Built-in platform functions, e.g. `[\"check_time\"]` |\n| `config_file` | str | None | Path to JSON config |\n| `schema_validation` | bool | `True` | Validate generated SWML |\n| `suppress_logs` | bool | `False` | Silence SDK output |\n\n## Entry points\n\n```python\nagent.run()    # auto-detects serverless vs uvicorn\nagent.serve()  # always starts FastAPI/uvicorn\n```\n\n## Subclass pattern\n\n```python\nfrom signalwire import AgentBase, FunctionResult\n\nclass SupportAgent(AgentBase):\n    def __init__(self):\n        super().__init__(name=\"support-agent\", route=\"/support\")\n        self.add_language(\"English\", \"en-US\", \"rime.spore\")\n        self.set_prompt_text(\"You are a friendly customer support agent.\")\n        self.add_hints([\"SignalWire\", \"SWML\", \"SWAIG\"])\n        self.set_params({\"temperature\": 0.7, \"end_of_speech_timeout\": 1000})\n\nif __name__ == \"__main__\":\n    SupportAgent().run()\n```\n\n## Instance pattern (no subclass)\n\n```python\nagent = AgentBase(name=\"assistant\", route=\"/assistant\")\nagent.set_prompt_text(\"You are a helpful assistant.\")\nagent.add_language(\"English\", \"en-US\", \"rime.spore\")\nagent.serve()\n```\n\n## Defining the prompt\n\n### Plain text\n\n```python\nagent.set_prompt_text(\"You are a helpful assistant.\")\n```\n\n### Structured POM sections\n\n```python\nagent.prompt_add_section(\"Role\", \"You are a customer service agent.\")\nagent.prompt_add_section(\"Guidelines\", bullets=[\n    \"Be concise\",\n    \"Never promise timelines you can't keep\",\n])\nagent.prompt_add_subsection(\"Guidelines\", \"Escalation\", \"Transfer if unresolved after 2 attempts.\")\nagent.prompt_add_to_section(\"Guidelines\", bullet=\"Always verify caller identity first.\")\n```\n\n### AI parameters\n\n```python\nagent.set_params({\n    \"temperature\": 0.7,\n    \"end_of_speech_timeout\": 1000,\n    \"attention_timeout\": 10000,\n    \"max_speech_timeout\": 30000,\n})\nagent.set_param(\"temperature\", 0.5)\n```\n\n### Post-prompt for call summary\n\n```python\nagent.set_post_prompt(\"Summarize the call outcome in one sentence.\")\nagent.set_post_prompt_url(\"https://yourserver.com/summaries\")\n```\n\n### Language and voice\n\n```python\nagent.add_language(\"English\", \"en-US\", \"rime.spore\")\nagent.add_language(\"Spanish\", \"es-MX\", \"rime.luna\")\n```\n\n### Hints, patterns, pronunciation\n\n```python\nagent.add_hint(\"SignalWire\")\nagent.add_hints([\"SWML\", \"SWAIG\", \"webhook\"])\nagent.add_pattern_hint(r\"\\b\\d{3}-\\d{4}\\b\", \"phone number\")\nagent.add_pronunciation(\"GHL\", \"G H L\")\n```\n\n## @tool decorator — preferred SWAIG registration\n\nThe `@tool` decorator is the canonical way to register SWAIG functions. It works on standalone functions and on methods inside subclasses.\n\n### Instance decorator with type inference\n\n```python\nagent = AgentBase(name=\"assistant\", route=\"/assistant\")\nagent.set_prompt_text(\"You are a helpful assistant.\")\n\n@agent.tool(description=\"Look up a customer's order status\")\ndef check_order(args, raw_data=None):\n    order_id = args.get(\"order_id\")\n    return FunctionResult(f\"Order {order_id} shipped March 28.\")\n\nagent.serve()\n```\n\n### Instance decorator with explicit schema\n\n```python\n@agent.tool(\n    name=\"search_products\",\n    description=\"Search the product catalog\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"query\": {\"type\": \"string\", \"description\": \"Search query\"},\n            \"category\": {\"type\": \"string\", \"description\": \"Product category\"},\n        },\n    },\n    required=[\"query\"],\n    fillers={\"en-US\": [\"Searching...\", \"Let me find that...\"]},\n)\ndef search_products(args, raw_data=None):\n    query = args.get(\"query\")\n    return FunctionResult(f\"Found 3 results for '{query}'.\")\n```\n\n### Class decorator (subclass)\n\n```python\nclass SupportAgent(AgentBase):\n    @AgentBase.tool(description=\"Transfer to a human agent\")\n    def transfer_to_human(self, args, raw_data=None):\n        return FunctionResult(\"Transferring now.\").connect(\"+15551234567\")\n```\n\n### Typed parameters auto-inferred\n\n```python\n@agent.tool(description=\"Calculate shipping cost\")\ndef calculate_shipping(weight_kg: float, destination: str, express: bool = False):\n    cost = weight_kg * (5.00 if express else 2.50)\n    return FunctionResult(f\"Shipping to {destination}: ${cost:.2f}\")\n```\n\n### Decorator parameters\n\n| Parameter | Type | Default | Notes |\n|---|---|---|---|\n| `name` | str | function `__name__` | Exposed to the AI |\n| `description` | str | docstring or `\"Function {name}\"` | AI reads to decide when to call |\n| `parameters` | dict | inferred from type hints | Full JSON Schema object |\n| `secure` | bool | `True` | Token-validate calls |\n| `fillers` | dict[str, list[str]] | None | e.g. `{\"en-US\": [\"One moment...\"]}` |\n| `webhook_url` | str | None | Forward to external URL instead of local handler |\n| `required` | list[str] | inferred | Required parameter names |\n\n## FunctionResult — required return type\n\nEvery tool handler must return `FunctionResult`. All methods return `self` so calls can be chained.\n\n```python\nreturn FunctionResult(\"Done.\")\nreturn FunctionResult(\"I'll transfer you. Anything else?\", post_process=True)\n```\n\n`post_process=True` makes the AI speak the response, take one more turn, then execute actions.\n\n### Call control\n\n```python\n.connect(\"+15551234567\")                  # transfer\n.connect(\"+15551234567\", final=True)      # transfer, end conversation\n.hangup()                                  # end immediately\n.hold(timeout=30)                          # hold with timeout\n.swml_transfer(\"https://your.api/flow\", return_message=\"Transfer failed\")\n```\n\n### Data\n\n```python\n.update_global_data({\"key\": \"value\"})     # merge into session global_data\n.remove_global_data([\"key\"])              # remove keys\n.set_metadata({\"local_key\": \"val\"})       # function-scoped only\n```\n\n### Context navigation\n\n```python\n.swml_change_step(\"step_name\")\n.swml_change_context(\"context_name\")\n.switch_context(\"context_name\", system_prompt=\"New prompt\", consolidate=True)\n```\n\n### Media\n\n```python\n.play_background_file(\"https://example.com/hold.mp3\")\n.stop_background_file()\n.record_call(format=\"mp4\", stereo=True)\n```\n\n### Messaging, speech, function control\n\n```python\n.send_sms(to_number=\"+15551234567\", from_number=\"+15559876543\", body=\"Your order shipped.\")\n.say(\"Please hold while I look that up.\")\n.wait_for_user(True)\n.stop()\n.toggle_functions({\"lookup_order\": False, \"escalate\": True})\n```\n\n### Chaining example\n\n```python\n@agent.tool(name=\"transfer_to_billing\", description=\"Transfer caller to billing\")\ndef transfer_to_billing(args, raw_data):\n    return (\n        FunctionResult(\"I'll transfer you to billing. Anything else first?\", post_process=True)\n        .update_global_data({\"transferred\": True})\n        .send_sms(to_number=\"+15551234567\", from_number=\"+15559876543\",\n                  body=\"You are being transferred to billing.\")\n        .connect(\"+15551234567\", final=True)   # terminal — always last\n    )\n```\n\n## AgentServer — multiple agents, one port\n\nMount any number of agents on different routes and serve them from one uvicorn process.\n\n```python\nfrom signalwire import AgentServer\n\nserver = AgentServer(host=\"0.0.0.0\", port=3000)\nserver.mount(SalesAgent(), route=\"/sales\")\nserver.mount(SupportAgent(), route=\"/support\")\nserver.run()\n```\n\nEndpoints exposed: `/sales`, `/support`, `/health`, `/ready`.\n\n## Anti-patterns\n\n- Returning a string instead of `FunctionResult` from a tool — handler will error.\n- Placing `.connect(..., final=True)` mid-chain — `final=True` is terminal. Always last.\n- Forgetting `post_process=True` when you want the AI to speak before acting.\n- Hardcoding `port` when deploying to serverless — use `agent.run()` so the SDK auto-detects.\n- Skipping `add_language` — without an explicit language and voice, the agent uses a low-quality default.\n\n## See also\n\n- [SWAIG functions deep-dive](/topic/swaig-functions)\n- [SWML AI verb reference](/topic/swml-ai-verb)\n- [Context Builder for multi-step flows](/topic/agents-context-builder)\n- [POM Builder for structured prompts](/topic/agents-pom-builder)\n- [MCP Gateway for tool federation](/topic/agents-mcp-gateway)\n","html":"<h1>SignalWire Python Agents SDK</h1>\n<p>Python SDK that auto-generates SWML, registers SWAIG endpoints, and runs a FastAPI/uvicorn server. The primary class is <code>AgentBase</code>. Nine mixins compose the feature set: prompt, tool, skill, AI config, web, auth, serverless, state, MCP server. Every setter returns <code>self</code> for fluent chaining.</p>\n<h2>Install</h2>\n<pre><code class=\"language-bash\">pip install signalwire-agents\npip install signalwire-pom  # only when using PomBuilder directly\n</code></pre>\n<h2>Key imports</h2>\n<pre><code class=\"language-python\">from signalwire import AgentBase, AgentServer, SWMLService\nfrom signalwire import SWAIGFunction, FunctionResult, DataMap\nfrom signalwire import (\n    create_simple_context, create_simple_api_tool,\n    create_expression_tool, register_skill, add_skill_directory,\n)\nfrom signalwire.core.pom_builder import PomBuilder\nfrom signalwire.core.skill_base import SkillBase\nfrom signalwire.mcp_gateway import MCPGateway\nfrom signalwire.prefabs import (\n    ConciergeAgent, FAQBotAgent, InfoGathererAgent,\n    ReceptionistAgent, SurveyAgent,\n)\n</code></pre>\n<h2>AgentBase constructor</h2>\n<p>| Parameter | Type | Default | Notes |\n|---|---|---|---|\n| <code>name</code> | str | required | Display name, used in logging and SIP username mapping |\n| <code>route</code> | str | <code>\"/\"</code> | HTTP route path |\n| <code>host</code> | str | <code>\"0.0.0.0\"</code> | Bind address |\n| <code>port</code> | int | <code>PORT</code> env or <code>3000</code> | Listen port |\n| <code>agent_id</code> | str | auto UUID | Unique instance ID |\n| <code>use_pom</code> | bool | <code>True</code> | Enable Prompt Object Model |\n| <code>auto_answer</code> | bool | <code>True</code> | Add <code>answer</code> verb before <code>ai</code> in SWML |\n| <code>record_call</code> | bool | <code>False</code> | Enable call recording |\n| <code>record_format</code> | str | <code>\"mp4\"</code> | <code>\"mp4\"</code> or <code>\"wav\"</code> |\n| <code>record_stereo</code> | bool | <code>True</code> | Separate channel per party |\n| <code>basic_auth</code> | tuple[str,str] | env or auto | <code>(username, password)</code> |\n| <code>token_expiry_secs</code> | int | <code>3600</code> | SWAIG auth token TTL |\n| <code>native_functions</code> | list[str] | None | Built-in platform functions, e.g. <code>[\"check_time\"]</code> |\n| <code>config_file</code> | str | None | Path to JSON config |\n| <code>schema_validation</code> | bool | <code>True</code> | Validate generated SWML |\n| <code>suppress_logs</code> | bool | <code>False</code> | Silence SDK output |</p>\n<h2>Entry points</h2>\n<pre><code class=\"language-python\">agent.run()    # auto-detects serverless vs uvicorn\nagent.serve()  # always starts FastAPI/uvicorn\n</code></pre>\n<h2>Subclass pattern</h2>\n<pre><code class=\"language-python\">from signalwire import AgentBase, FunctionResult\n\nclass SupportAgent(AgentBase):\n    def __init__(self):\n        super().__init__(name=\"support-agent\", route=\"/support\")\n        self.add_language(\"English\", \"en-US\", \"rime.spore\")\n        self.set_prompt_text(\"You are a friendly customer support agent.\")\n        self.add_hints([\"SignalWire\", \"SWML\", \"SWAIG\"])\n        self.set_params({\"temperature\": 0.7, \"end_of_speech_timeout\": 1000})\n\nif __name__ == \"__main__\":\n    SupportAgent().run()\n</code></pre>\n<h2>Instance pattern (no subclass)</h2>\n<pre><code class=\"language-python\">agent = AgentBase(name=\"assistant\", route=\"/assistant\")\nagent.set_prompt_text(\"You are a helpful assistant.\")\nagent.add_language(\"English\", \"en-US\", \"rime.spore\")\nagent.serve()\n</code></pre>\n<h2>Defining the prompt</h2>\n<h3>Plain text</h3>\n<pre><code class=\"language-python\">agent.set_prompt_text(\"You are a helpful assistant.\")\n</code></pre>\n<h3>Structured POM sections</h3>\n<pre><code class=\"language-python\">agent.prompt_add_section(\"Role\", \"You are a customer service agent.\")\nagent.prompt_add_section(\"Guidelines\", bullets=[\n    \"Be concise\",\n    \"Never promise timelines you can't keep\",\n])\nagent.prompt_add_subsection(\"Guidelines\", \"Escalation\", \"Transfer if unresolved after 2 attempts.\")\nagent.prompt_add_to_section(\"Guidelines\", bullet=\"Always verify caller identity first.\")\n</code></pre>\n<h3>AI parameters</h3>\n<pre><code class=\"language-python\">agent.set_params({\n    \"temperature\": 0.7,\n    \"end_of_speech_timeout\": 1000,\n    \"attention_timeout\": 10000,\n    \"max_speech_timeout\": 30000,\n})\nagent.set_param(\"temperature\", 0.5)\n</code></pre>\n<h3>Post-prompt for call summary</h3>\n<pre><code class=\"language-python\">agent.set_post_prompt(\"Summarize the call outcome in one sentence.\")\nagent.set_post_prompt_url(\"https://yourserver.com/summaries\")\n</code></pre>\n<h3>Language and voice</h3>\n<pre><code class=\"language-python\">agent.add_language(\"English\", \"en-US\", \"rime.spore\")\nagent.add_language(\"Spanish\", \"es-MX\", \"rime.luna\")\n</code></pre>\n<h3>Hints, patterns, pronunciation</h3>\n<pre><code class=\"language-python\">agent.add_hint(\"SignalWire\")\nagent.add_hints([\"SWML\", \"SWAIG\", \"webhook\"])\nagent.add_pattern_hint(r\"\\b\\d{3}-\\d{4}\\b\", \"phone number\")\nagent.add_pronunciation(\"GHL\", \"G H L\")\n</code></pre>\n<h2>@tool decorator — preferred SWAIG registration</h2>\n<p>The <code>@tool</code> decorator is the canonical way to register SWAIG functions. It works on standalone functions and on methods inside subclasses.</p>\n<h3>Instance decorator with type inference</h3>\n<pre><code class=\"language-python\">agent = AgentBase(name=\"assistant\", route=\"/assistant\")\nagent.set_prompt_text(\"You are a helpful assistant.\")\n\n@agent.tool(description=\"Look up a customer's order status\")\ndef check_order(args, raw_data=None):\n    order_id = args.get(\"order_id\")\n    return FunctionResult(f\"Order {order_id} shipped March 28.\")\n\nagent.serve()\n</code></pre>\n<h3>Instance decorator with explicit schema</h3>\n<pre><code class=\"language-python\">@agent.tool(\n    name=\"search_products\",\n    description=\"Search the product catalog\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"query\": {\"type\": \"string\", \"description\": \"Search query\"},\n            \"category\": {\"type\": \"string\", \"description\": \"Product category\"},\n        },\n    },\n    required=[\"query\"],\n    fillers={\"en-US\": [\"Searching...\", \"Let me find that...\"]},\n)\ndef search_products(args, raw_data=None):\n    query = args.get(\"query\")\n    return FunctionResult(f\"Found 3 results for '{query}'.\")\n</code></pre>\n<h3>Class decorator (subclass)</h3>\n<pre><code class=\"language-python\">class SupportAgent(AgentBase):\n    @AgentBase.tool(description=\"Transfer to a human agent\")\n    def transfer_to_human(self, args, raw_data=None):\n        return FunctionResult(\"Transferring now.\").connect(\"+15551234567\")\n</code></pre>\n<h3>Typed parameters auto-inferred</h3>\n<pre><code class=\"language-python\">@agent.tool(description=\"Calculate shipping cost\")\ndef calculate_shipping(weight_kg: float, destination: str, express: bool = False):\n    cost = weight_kg * (5.00 if express else 2.50)\n    return FunctionResult(f\"Shipping to {destination}: ${cost:.2f}\")\n</code></pre>\n<h3>Decorator parameters</h3>\n<p>| Parameter | Type | Default | Notes |\n|---|---|---|---|\n| <code>name</code> | str | function <code>__name__</code> | Exposed to the AI |\n| <code>description</code> | str | docstring or <code>\"Function {name}\"</code> | AI reads to decide when to call |\n| <code>parameters</code> | dict | inferred from type hints | Full JSON Schema object |\n| <code>secure</code> | bool | <code>True</code> | Token-validate calls |\n| <code>fillers</code> | dict[str, list[str]] | None | e.g. <code>{\"en-US\": [\"One moment...\"]}</code> |\n| <code>webhook_url</code> | str | None | Forward to external URL instead of local handler |\n| <code>required</code> | list[str] | inferred | Required parameter names |</p>\n<h2>FunctionResult — required return type</h2>\n<p>Every tool handler must return <code>FunctionResult</code>. All methods return <code>self</code> so calls can be chained.</p>\n<pre><code class=\"language-python\">return FunctionResult(\"Done.\")\nreturn FunctionResult(\"I'll transfer you. Anything else?\", post_process=True)\n</code></pre>\n<p><code>post_process=True</code> makes the AI speak the response, take one more turn, then execute actions.</p>\n<h3>Call control</h3>\n<pre><code class=\"language-python\">.connect(\"+15551234567\")                  # transfer\n.connect(\"+15551234567\", final=True)      # transfer, end conversation\n.hangup()                                  # end immediately\n.hold(timeout=30)                          # hold with timeout\n.swml_transfer(\"https://your.api/flow\", return_message=\"Transfer failed\")\n</code></pre>\n<h3>Data</h3>\n<pre><code class=\"language-python\">.update_global_data({\"key\": \"value\"})     # merge into session global_data\n.remove_global_data([\"key\"])              # remove keys\n.set_metadata({\"local_key\": \"val\"})       # function-scoped only\n</code></pre>\n<h3>Context navigation</h3>\n<pre><code class=\"language-python\">.swml_change_step(\"step_name\")\n.swml_change_context(\"context_name\")\n.switch_context(\"context_name\", system_prompt=\"New prompt\", consolidate=True)\n</code></pre>\n<h3>Media</h3>\n<pre><code class=\"language-python\">.play_background_file(\"https://example.com/hold.mp3\")\n.stop_background_file()\n.record_call(format=\"mp4\", stereo=True)\n</code></pre>\n<h3>Messaging, speech, function control</h3>\n<pre><code class=\"language-python\">.send_sms(to_number=\"+15551234567\", from_number=\"+15559876543\", body=\"Your order shipped.\")\n.say(\"Please hold while I look that up.\")\n.wait_for_user(True)\n.stop()\n.toggle_functions({\"lookup_order\": False, \"escalate\": True})\n</code></pre>\n<h3>Chaining example</h3>\n<pre><code class=\"language-python\">@agent.tool(name=\"transfer_to_billing\", description=\"Transfer caller to billing\")\ndef transfer_to_billing(args, raw_data):\n    return (\n        FunctionResult(\"I'll transfer you to billing. Anything else first?\", post_process=True)\n        .update_global_data({\"transferred\": True})\n        .send_sms(to_number=\"+15551234567\", from_number=\"+15559876543\",\n                  body=\"You are being transferred to billing.\")\n        .connect(\"+15551234567\", final=True)   # terminal — always last\n    )\n</code></pre>\n<h2>AgentServer — multiple agents, one port</h2>\n<p>Mount any number of agents on different routes and serve them from one uvicorn process.</p>\n<pre><code class=\"language-python\">from signalwire import AgentServer\n\nserver = AgentServer(host=\"0.0.0.0\", port=3000)\nserver.mount(SalesAgent(), route=\"/sales\")\nserver.mount(SupportAgent(), route=\"/support\")\nserver.run()\n</code></pre>\n<p>Endpoints exposed: <code>/sales</code>, <code>/support</code>, <code>/health</code>, <code>/ready</code>.</p>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Returning a string instead of <code>FunctionResult</code> from a tool — handler will error.</li>\n<li>Placing <code>.connect(..., final=True)</code> mid-chain — <code>final=True</code> is terminal. Always last.</li>\n<li>Forgetting <code>post_process=True</code> when you want the AI to speak before acting.</li>\n<li>Hardcoding <code>port</code> when deploying to serverless — use <code>agent.run()</code> so the SDK auto-detects.</li>\n<li>Skipping <code>add_language</code> — without an explicit language and voice, the agent uses a low-quality default.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/swaig-functions\">SWAIG functions deep-dive</a></li>\n<li><a href=\"/topic/swml-ai-verb\">SWML AI verb reference</a></li>\n<li><a href=\"/topic/agents-context-builder\">Context Builder for multi-step flows</a></li>\n<li><a href=\"/topic/agents-pom-builder\">POM Builder for structured prompts</a></li>\n<li><a href=\"/topic/agents-mcp-gateway\">MCP Gateway for tool federation</a></li>\n</ul>\n"}