WIP push-to-talk Letta chat frontend
1use std::sync::Arc;
2
3use futures_util::StreamExt;
4use reqwest::Client;
5use reqwest_eventsource::{Event, EventSource};
6use serde_json::{from_str, json};
7use tauri::async_runtime::{channel, spawn, Mutex, Receiver, Sender};
8use tauri::Wry;
9use tauri_plugin_store::Store;
10
11use crate::secrets::SecretsManager;
12use types::{LettaAgentInfo, LettaCompletionMessage, LettaConfigKey, LettaError};
13
14pub mod commands;
15pub mod types;
16
17pub struct LettaManager {
18 http_client: Client,
19 secrets_manager: Arc<SecretsManager>,
20 store: Arc<Store<Wry>>,
21 base_url: Mutex<String>,
22 agent_id: Mutex<String>,
23}
24impl LettaManager {
25 pub fn new(
26 http_client: Client,
27 store: Arc<Store<Wry>>,
28 secrets_manager: Arc<SecretsManager>,
29 ) -> Self {
30 let base_url = match store.get(LettaConfigKey::BaseUrl.to_string()) {
31 Some(url) => url
32 .as_str()
33 .expect("Letta base URL in config is not a string")
34 .to_owned(),
35 None => {
36 let url = "https://api.letta.com".to_owned();
37
38 store.set(LettaConfigKey::BaseUrl.to_string(), json!(url));
39
40 url
41 }
42 };
43 let agent_id = match store.get(LettaConfigKey::AgentId.to_string()) {
44 Some(id) => id
45 .as_str()
46 .expect("Letta agent ID in config is not a string")
47 .to_owned(),
48 None => "".to_owned(),
49 };
50
51 Self {
52 http_client,
53 secrets_manager,
54 store,
55 base_url: Mutex::new(base_url),
56 agent_id: Mutex::new(agent_id),
57 }
58 }
59
60 pub async fn list_agents(&self) -> Result<Vec<LettaAgentInfo>, LettaError> {
61 let api_key = self
62 .secrets_manager
63 .get_secret(crate::secrets::SecretName::LettaApiKey)?;
64 let base_url = self.base_url.lock().await.to_owned();
65 let req = self
66 .http_client
67 .get(format!("{base_url}/v1/agents/"))
68 .header("Authorization", format!("Bearer {api_key}"));
69
70 let res = req.send().await.expect("failed to list Letta agents");
71 let out = res.json::<Vec<LettaAgentInfo>>().await?;
72
73 Ok(out)
74 }
75
76 pub async fn start_completion(
77 &self,
78 msg: String,
79 ) -> Result<Receiver<LettaCompletionMessage>, LettaError> {
80 let api_key = self
81 .secrets_manager
82 .get_secret(crate::secrets::SecretName::LettaApiKey)?;
83 let base_url = self.base_url.lock().await.to_owned();
84 let agent_id = self.agent_id.lock().await.to_owned();
85 let body = &json!({
86 "messages": [
87 {
88 "role": "user",
89 "content": [{
90 "type": "text",
91 "text": msg,
92 }]
93 }
94 ],
95 "stream_tokens": true
96 });
97
98 println!("body: {:?}", body);
99
100 let req = self
101 .http_client
102 .post(format!("{base_url}/v1/agents/{agent_id}/messages/stream"))
103 .header("Authorization", format!("Bearer {api_key}"))
104 .header("Content-Type", "application/json")
105 .json(body);
106
107 let source = EventSource::new(req).expect("failed to clone request");
108
109 let (tx, rx) = channel::<LettaCompletionMessage>(100);
110 let handler = handle_completion_messages(source, tx);
111
112 spawn(handler);
113
114 return Ok(rx);
115 }
116}
117
118async fn handle_completion_messages(
119 mut source: EventSource,
120 sender: Sender<LettaCompletionMessage>,
121) {
122 while let Some(event) = source.next().await {
123 match event {
124 Ok(Event::Open) => println!("stream opened"),
125 Ok(Event::Message(msg)) => {
126 if msg.data == "[DONE]" {
127 continue;
128 };
129
130 match from_str::<LettaCompletionMessage>(&msg.data) {
131 Ok(content) => {
132 sender.send(content).await.expect("failed to forward event");
133 }
134 Err(err) => {
135 eprintln!(
136 "failed to parse message: {:?}, err: {}",
137 msg.data.clone(),
138 err
139 )
140 }
141 }
142 }
143 Err(err) => {
144 eprintln!("got stream error: {}", err);
145 source.close();
146 }
147 }
148 }
149}