Skip to main content

voice_echo/pipeline/
bridge.rs

1use serde_json::json;
2
3/// HTTP client for bridge-echo. Sends transcribed speech to the multiplexer
4/// and receives Claude's response. All session management and trust context
5/// wrapping is handled by bridge-echo.
6pub struct BridgeClient {
7    url: String,
8    caller_name: String,
9    client: reqwest::Client,
10}
11
12impl BridgeClient {
13    pub fn new(bridge_url: &str, caller_name: String) -> Self {
14        Self {
15            url: format!("{}/chat", bridge_url.trim_end_matches('/')),
16            caller_name,
17            client: reqwest::Client::new(),
18        }
19    }
20
21    /// Send a voice transcript to bridge-echo and get the response.
22    ///
23    /// The `context` parameter is used for outbound calls — it tells Claude
24    /// why it initiated the call. Consumed on first utterance.
25    pub async fn send(
26        &self,
27        call_sid: &str,
28        transcript: &str,
29        context: Option<&str>,
30    ) -> Result<String, BridgeError> {
31        let mut metadata = json!({
32            "call_sid": call_sid,
33        });
34        if let Some(ctx) = context {
35            metadata["context"] = json!(ctx);
36        }
37
38        let body = json!({
39            "channel": "voice",
40            "sender": self.caller_name,
41            "message": transcript,
42            "metadata": metadata,
43        });
44
45        let resp = self
46            .client
47            .post(&self.url)
48            .json(&body)
49            .send()
50            .await
51            .map_err(|e| BridgeError::Request(e.to_string()))?;
52
53        if !resp.status().is_success() {
54            let status = resp.status();
55            let body = resp.text().await.unwrap_or_default();
56            return Err(BridgeError::Response(format!("HTTP {status}: {body}")));
57        }
58
59        let parsed: serde_json::Value = resp
60            .json()
61            .await
62            .map_err(|e| BridgeError::Parse(e.to_string()))?;
63
64        parsed
65            .get("response")
66            .and_then(|v| v.as_str())
67            .map(String::from)
68            .ok_or_else(|| BridgeError::Parse("Missing 'response' field".into()))
69    }
70}
71
72#[derive(Debug, thiserror::Error)]
73pub enum BridgeError {
74    #[error("Bridge request failed: {0}")]
75    Request(String),
76    #[error("Bridge returned error: {0}")]
77    Response(String),
78    #[error("Failed to parse bridge response: {0}")]
79    Parse(String),
80}