Recipes← all recipesView on GitHub

Create a hosted voice AI agent with one REST call

AI Agentshosted voice AI agent (no server)

Turn the agent definition you would serve yourself into a resource SignalWire hosts, with one POST, then put a phone number on it with one more.

resthosted

The claim

Every agent recipe here serves a document from your host. This one hands the same definition to the platform instead. AgentBase renders the ai verb as it would for a call. Its prompt, params and post_prompt become the body of a create request, and the response is a resource with an id.

Why it holds

The vendored REST spec, tools/openapi/rest.json, is the authority.

  • POST /api/fabric/resources/ai_agents requires name and prompt. The spec describes prompt, params and post_prompt by pointing at the SWML ai reference, so the objects the SDK renders are the objects the endpoint wants.
  • POST /api/fabric/resources/{id}/phone_routes requires phone_route_id and a handler, whose enum is calling or messaging.
  • The number’s id comes from GET /api/relay/rest/phone_numbers with filter_number, which is a contains match, so the recipe compares the number exactly and refuses one the project does not hold.

A hosted agent has no tool webhooks of yours to call, because there is no host of yours. This one has none. An agent with tools stays a served agent, or uses serverless tools. That pattern is [Let the agent call an API with no server of yours](../call-an-api-without-a-backend/).

How it works

class FrontDesk(AgentBase):
    def __init__(self):
        super().__init__(name=NAME, route="/front-desk")
        self.prompt_add_section("Role", "You answer the phone for Ridgeline Cycles...")
        self.set_post_prompt("Summarise the call in one sentence.")
        self.set_params({"end_of_speech_timeout": 700})

def definition():
    doc = json.loads(FrontDesk()._render_swml())
    (ai,) = [step["ai"] for step in doc["sections"]["main"] if "ai" in step]
    return {"name": NAME, "prompt": ai["prompt"], "params": ai["params"],
            "post_prompt": ai["post_prompt"]}

client.fabric.ai_agents.create(**definition())

What the platform receives, from the Python SDK:

{"name": "ridgeline-front-desk",
 "prompt": {"pom": [{"title": "Role", "body": "You answer the phone for ..."},
                    {"title": "Hours", "body": "Open Monday to Friday, ..."},
                    {"title": "Limits", "body": "You cannot book repairs. ..."}]},
 "params": {"end_of_speech_timeout": 700},
 "post_prompt": {"text": "Summarise the call in one sentence."}}

The two SDKs render the same sections in different forms. Python 3.0.1 emits a pom list; @signalwire/sdk 2.0.5 emits them as markdown text with one ## heading per section. The ai.prompt schema allows both, and the verifier validates each as SWML before trusting it as a request body.

The second call is the one every no-server recipe shares: client.fabric.resources.assign_phone_route(agent_id, phone_route_id=..., handler="calling").

Limitations

The verifier proves the requests and the documents, not the call. What the hosted agent says when the number rings is live behaviour.

A hosted agent cannot call tool webhooks on a host you do not have. Keep tools serverless, or keep the agent served.

What to change first

Add self.define_tool(...) with a webhook handler to FrontDesk and run the verifier. The rendered ai now carries a SWAIG object pointing at your host, and definition() drops it. That is the point: the fields this recipe posts are the ones a hosted agent can honour.