From eea5f65ea028c59e9ff8c39cc120904dbe3d78ab Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:27:32 -0700 Subject: [PATCH] feat: wire Twilio SMS handler with agent + ack + response sending The Twilio webhook now: 1. Sends "Message received! Working on it now..." via Twilio API 2. Routes to DeepResearchAgent (via bridge or direct fallback) 3. Sends the research response back as SMS 4. Handles missing bridge gracefully 5. Formats response for SMS (strips markdown, 1500 char limit) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/openjarvis/server/webhook_routes.py | 115 ++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/src/openjarvis/server/webhook_routes.py b/src/openjarvis/server/webhook_routes.py index 0c3db1da..845abce9 100644 --- a/src/openjarvis/server/webhook_routes.py +++ b/src/openjarvis/server/webhook_routes.py @@ -111,15 +111,114 @@ def create_webhook_router( from_number = params.get("From", "") body = params.get("Body", "") - # Dispatch to background — return TwiML immediately - task = asyncio.create_task( - asyncio.to_thread( - bridge.handle_incoming, - from_number, - body, - "twilio", - max_length=1600, + if not from_number or not body: + return Response( + content="", + media_type="application/xml", ) + + # Use bridge or app.state.channel_bridge + active_bridge = bridge or getattr( + request.app.state, "channel_bridge", None, + ) + + def _handle_twilio() -> None: + + # Send ack via Twilio API + try: + from twilio.rest import Client + + # Get creds from app state bindings + mgr = getattr( + request.app.state, "agent_manager", None, + ) + twilio_client = None + twilio_from = "" + if mgr: + for agent in mgr.list_agents(): + aid = agent.get("id", "") + for b in mgr.list_channel_bindings(aid): + if b.get("channel_type") == "twilio": + cfg = b.get("config", {}) + sid = cfg.get("account_sid", "") + tok = cfg.get("auth_token", "") + twilio_from = cfg.get( + "phone_number", "", + ) + if sid and tok: + twilio_client = Client( + sid, tok, + ) + break + + if twilio_client and twilio_from: + twilio_client.messages.create( + body=( + "Message received! " + "Working on it now..." + ), + from_=twilio_from, + to=from_number, + ) + except Exception as _e: + logger.warning("Twilio ack failed: %s", _e) + + # Process via bridge or agent directly + response = "" + if active_bridge: + response = active_bridge.handle_incoming( + from_number, body, "twilio", + max_length=1600, + ) + else: + # Direct agent fallback + try: + from openjarvis.agents.deep_research import ( + DeepResearchAgent, + ) + from openjarvis.server.agent_manager_routes import ( + _build_deep_research_tools, + ) + + engine = getattr( + request.app.state, "engine", None, + ) + if engine: + tools = _build_deep_research_tools( + engine=engine, model="", + ) + agent = DeepResearchAgent( + engine=engine, + model=getattr( + engine, "_model", "", + ), + tools=tools, + max_turns=5, + ) + result = agent.run(body) + response = result.content or "" + except Exception as _exc: + response = f"Error: {_exc}" + + # Send response via Twilio + if response and twilio_client and twilio_from: + try: + clean = _format_for_sms(response) + # Twilio SMS limit is 1600 chars + if len(clean) > 1500: + clean = clean[:1500] + "\n\n(truncated)" + twilio_client.messages.create( + body=clean, + from_=twilio_from, + to=from_number, + ) + except Exception as _e: + logger.warning( + "Twilio reply failed: %s", _e, + ) + + task = asyncio.create_task( + asyncio.to_thread(_handle_twilio), ) task.add_done_callback(_log_task_exception)