1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static XMPP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "xmpp_connected",
18 "XMPP client successfully connected and authenticated",
19 json!({
20 "type": "send_message",
21 "to": "friend@example.com",
22 "body": "Hello!"
23 }),
24 )
25 .with_parameters(vec![Parameter {
26 name: "jid".to_string(),
27 type_hint: "string".to_string(),
28 description: "The JID (Jabber ID) of the connected client".to_string(),
29 required: true,
30 }])
31});
32
33pub static XMPP_CLIENT_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "xmpp_message_received",
37 "XMPP message received from another user",
38 json!({
39 "type": "send_message",
40 "to": "friend@example.com",
41 "body": "Thanks for your message!"
42 }),
43 )
44 .with_parameters(vec![
45 Parameter {
46 name: "from".to_string(),
47 type_hint: "string".to_string(),
48 description: "JID of the message sender".to_string(),
49 required: true,
50 },
51 Parameter {
52 name: "to".to_string(),
53 type_hint: "string".to_string(),
54 description: "JID of the message recipient".to_string(),
55 required: true,
56 },
57 Parameter {
58 name: "body".to_string(),
59 type_hint: "string".to_string(),
60 description: "Message body text".to_string(),
61 required: true,
62 },
63 Parameter {
64 name: "message_type".to_string(),
65 type_hint: "string".to_string(),
66 description: "Type of message (Chat, Groupchat, etc.)".to_string(),
67 required: true,
68 },
69 ])
70});
71
72pub static XMPP_CLIENT_PRESENCE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
74 EventType::new(
75 "xmpp_presence_received",
76 "XMPP presence update received from a contact",
77 json!({
78 "type": "wait_for_more"
79 }),
80 )
81 .with_parameters(vec![
82 Parameter {
83 name: "from".to_string(),
84 type_hint: "string".to_string(),
85 description: "JID of the contact".to_string(),
86 required: true,
87 },
88 Parameter {
89 name: "presence_type".to_string(),
90 type_hint: "string".to_string(),
91 description: "Type of presence (Available, Unavailable, etc.)".to_string(),
92 required: true,
93 },
94 Parameter {
95 name: "show".to_string(),
96 type_hint: "string".to_string(),
97 description: "Availability indicator (away, chat, dnd, xa)".to_string(),
98 required: false,
99 },
100 Parameter {
101 name: "status".to_string(),
102 type_hint: "string".to_string(),
103 description: "Status message".to_string(),
104 required: false,
105 },
106 ])
107});
108
109pub struct XmppClientProtocol;
111
112impl XmppClientProtocol {
113 pub fn new() -> Self {
114 Self
115 }
116}
117
118impl Protocol for XmppClientProtocol {
120 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
121 vec![
122 crate::llm::actions::ParameterDefinition {
123 name: "jid".to_string(),
124 type_hint: "string".to_string(),
125 description: "JID (Jabber ID) to connect as (e.g., user@example.com). Names \
126 the account only: the client connects to remote_addr, never to \
127 a server looked up from the JID's domain"
128 .to_string(),
129 required: true,
130 example: serde_json::json!("alice@example.com"),
131 default: None,
132 },
133 crate::llm::actions::ParameterDefinition {
134 name: "password".to_string(),
135 type_hint: "string".to_string(),
136 description: "Password for authentication".to_string(),
137 required: true,
138 example: serde_json::json!("secret"),
139 default: None,
140 },
141 crate::llm::actions::ParameterDefinition {
142 name: "session_timeout_secs".to_string(),
143 type_hint: "integer".to_string(),
144 description: format!(
145 "Seconds to wait for the server to accept the session (TCP connect, \
146 STARTTLS, SASL authentication, resource binding) before the client fails, \
147 1..{}",
148 super::MAX_SESSION_TIMEOUT_SECS
149 ),
150 required: false,
151 example: serde_json::json!(30),
152 default: Some(serde_json::json!(super::SESSION_TIMEOUT.as_secs())),
153 },
154 ]
155 }
156 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
157 vec![
158 ActionDefinition {
159 name: "send_message".to_string(),
160 description: "Send a message to a JID".to_string(),
161 parameters: vec![
162 Parameter {
163 name: "to".to_string(),
164 type_hint: "string".to_string(),
165 description: "JID of the recipient".to_string(),
166 required: true,
167 },
168 Parameter {
169 name: "body".to_string(),
170 type_hint: "string".to_string(),
171 description: "Message body text".to_string(),
172 required: true,
173 },
174 ],
175 example: json!({
176 "type": "send_message",
177 "to": "friend@example.com",
178 "body": "Hello from NetGet!"
179 }),
180 log_template: None,
181 },
182 ActionDefinition {
183 name: "send_presence".to_string(),
184 description: "Send presence update".to_string(),
185 parameters: vec![
186 Parameter {
187 name: "show".to_string(),
188 type_hint: "string".to_string(),
189 description: "Availability (away, chat, dnd, xa)".to_string(),
190 required: false,
191 },
192 Parameter {
193 name: "status".to_string(),
194 type_hint: "string".to_string(),
195 description: "Status message".to_string(),
196 required: false,
197 },
198 ],
199 example: json!({
200 "type": "send_presence",
201 "show": "away",
202 "status": "Out for lunch"
203 }),
204 log_template: None,
205 },
206 ActionDefinition {
207 name: "disconnect".to_string(),
208 description: "Disconnect from the XMPP server".to_string(),
209 parameters: vec![],
210 example: json!({
211 "type": "disconnect"
212 }),
213 log_template: None,
214 },
215 ]
216 }
217 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
218 vec![
219 ActionDefinition {
220 name: "send_message".to_string(),
221 description: "Send a message in response to a received message or event"
222 .to_string(),
223 parameters: vec![
224 Parameter {
225 name: "to".to_string(),
226 type_hint: "string".to_string(),
227 description: "JID of the recipient".to_string(),
228 required: true,
229 },
230 Parameter {
231 name: "body".to_string(),
232 type_hint: "string".to_string(),
233 description: "Message body text".to_string(),
234 required: true,
235 },
236 ],
237 example: json!({
238 "type": "send_message",
239 "to": "friend@example.com",
240 "body": "Thanks for your message!"
241 }),
242 log_template: None,
243 },
244 ActionDefinition {
245 name: "wait_for_more".to_string(),
246 description: "Wait for more events before responding".to_string(),
247 parameters: vec![],
248 example: json!({
249 "type": "wait_for_more"
250 }),
251 log_template: None,
252 },
253 ]
254 }
255 fn protocol_name(&self) -> &'static str {
256 "XMPP"
257 }
258 fn get_event_types(&self) -> Vec<EventType> {
259 vec![
260 EventType::new(
261 "xmpp_connected",
262 "Triggered when XMPP client connects and authenticates",
263 json!({"type": "placeholder", "event_id": "xmpp_connected"}),
264 ),
265 EventType::new(
266 "xmpp_message_received",
267 "Triggered when XMPP message is received",
268 json!({"type": "placeholder", "event_id": "xmpp_message_received"}),
269 ),
270 EventType::new(
271 "xmpp_presence_received",
272 "Triggered when presence update is received",
273 json!({"type": "placeholder", "event_id": "xmpp_presence_received"}),
274 ),
275 ]
276 }
277 fn stack_name(&self) -> &'static str {
278 "ETH>IP>TCP>TLS>XMPP"
279 }
280 fn keywords(&self) -> Vec<&'static str> {
281 vec![
282 "xmpp",
283 "xmpp client",
284 "jabber",
285 "connect to xmpp",
286 "connect to jabber",
287 ]
288 }
289 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
290 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
291
292 ProtocolMetadataV2::builder()
293 .state(DevelopmentState::Experimental)
294 .implementation("tokio-xmpp library for async XMPP client")
295 .llm_control("Send messages, presence updates, respond to incoming stanzas")
296 .e2e_testing(
297 "Loopback listeners pin the connect target, the pending-session status and the \
298 command channel; a session against a real prosody/ejabberd exists only in \
299 #[ignore]d tests",
300 )
301 .build()
302 }
303 fn description(&self) -> &'static str {
304 "XMPP/Jabber client for instant messaging"
305 }
306 fn example_prompt(&self) -> &'static str {
307 "Connect to XMPP as user@example.com and respond to incoming messages"
308 }
309 fn group_name(&self) -> &'static str {
310 "Application"
311 }
312
313 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
314 use crate::llm::actions::StartupExamples;
315 use serde_json::json;
316
317 StartupExamples::new(
318 json!({
320 "type": "open_client",
321 "remote_addr": "xmpp.example.com:5222",
322 "base_stack": "xmpp",
323 "startup_params": {"jid": "alice@example.com", "password": "secret"},
324 "instruction": "Send presence and auto-reply to all incoming messages"
325 }),
326 json!({
328 "type": "open_client",
329 "remote_addr": "xmpp.example.com:5222",
330 "base_stack": "xmpp",
331 "startup_params": {"jid": "alice@example.com", "password": "secret"},
332 "event_handlers": [{
333 "event_pattern": "xmpp_message_received",
334 "handler": {
335 "type": "script",
336 "language": "python",
337 "code": "<xmpp_client_handler>"
338 }
339 }]
340 }),
341 json!({
343 "type": "open_client",
344 "remote_addr": "xmpp.example.com:5222",
345 "base_stack": "xmpp",
346 "startup_params": {"jid": "alice@example.com", "password": "secret"},
347 "event_handlers": [
348 {
349 "event_pattern": "xmpp_connected",
350 "handler": {
351 "type": "static",
352 "actions": [{
353 "type": "send_presence",
354 "show": "chat",
355 "status": "Available"
356 }]
357 }
358 },
359 {
360 "event_pattern": "xmpp_message_received",
361 "handler": {
362 "type": "static",
363 "actions": [{
364 "type": "wait_for_more"
365 }]
366 }
367 }
368 ]
369 }),
370 )
371 }
372}
373
374impl Client for XmppClientProtocol {
376 fn connect(
377 &self,
378 ctx: crate::protocol::ConnectContext,
379 ) -> std::pin::Pin<
380 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
381 > {
382 Box::pin(async move {
383 use crate::client::xmpp::XmppClientConnection;
384 XmppClientConnection::connect_with_llm_actions(
385 ctx.remote_addr,
386 ctx.llm_client,
387 ctx.state,
388 ctx.status_tx,
389 ctx.client_id,
390 ctx.startup_params,
391 )
392 .await
393 })
394 }
395 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
396 let action_type = action
397 .get("type")
398 .and_then(|v| v.as_str())
399 .context("Missing 'type' field in action")?;
400
401 match action_type {
402 "send_message" => {
403 let to = action
404 .get("to")
405 .and_then(|v| v.as_str())
406 .context("Missing 'to' field")?;
407
408 let body = action
409 .get("body")
410 .and_then(|v| v.as_str())
411 .context("Missing 'body' field")?;
412
413 Ok(ClientActionResult::Custom {
414 name: "send_message".to_string(),
415 data: json!({
416 "to": to,
417 "body": body,
418 }),
419 })
420 }
421 "send_presence" => {
422 let show = action.get("show").and_then(|v| v.as_str());
423 let status = action.get("status").and_then(|v| v.as_str());
424
425 Ok(ClientActionResult::Custom {
426 name: "send_presence".to_string(),
427 data: json!({
428 "show": show,
429 "status": status,
430 }),
431 })
432 }
433 "disconnect" => Ok(ClientActionResult::Disconnect),
434 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
435 _ => Err(anyhow::anyhow!(
436 "Unknown XMPP client action: {}",
437 action_type
438 )),
439 }
440 }
441}