1pub mod actions;
3
4pub use actions::MongodbClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10use tracing::{error, info, trace, warn};
11
12#[cfg(not(target_arch = "wasm32"))]
13use mongodb::{
14 bson::Document,
15 options::{ClientOptions, FindOptions},
16 Client as MongoClient, Database,
17};
18
19use crate::client::llm_budget::call_llm_for_client;
20use crate::client::mongodb::actions::{
21 MONGODB_CLIENT_CONNECTED_EVENT, MONGODB_CLIENT_RESULT_RECEIVED_EVENT,
22};
23use crate::llm::actions::client_trait::Client;
24use crate::llm::ollama_client::OllamaClient;
25use crate::llm::ClientLlmResult;
26use crate::protocol::Event;
27use crate::state::app_state::AppState;
28use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
29use crate::state::{ClientId, ClientStatus};
30
31#[cfg(not(target_arch = "wasm32"))]
36enum Applied {
37 Ran { detail: String, event: Event },
40 Disconnect,
42 Nothing(&'static str),
44}
45
46const MAX_FOLLOWUP_DEPTH: u8 = 4;
59
60pub struct MongodbClient;
62
63impl MongodbClient {
64 pub async fn connect_with_llm_actions(
66 remote_addr: String,
67 llm_client: OllamaClient,
68 app_state: Arc<AppState>,
69 status_tx: mpsc::UnboundedSender<String>,
70 client_id: ClientId,
71 startup_params: Option<crate::protocol::StartupParams>,
72 ) -> Result<SocketAddr> {
73 Self::connect_impl(
74 remote_addr,
75 llm_client,
76 app_state,
77 status_tx,
78 client_id,
79 startup_params,
80 )
81 .await
82 }
83
84 #[cfg(not(target_arch = "wasm32"))]
85 async fn connect_impl(
86 remote_addr: String,
87 llm_client: OllamaClient,
88 app_state: Arc<AppState>,
89 status_tx: mpsc::UnboundedSender<String>,
90 client_id: ClientId,
91 startup_params: Option<crate::protocol::StartupParams>,
92 ) -> Result<SocketAddr> {
93 let database_name = startup_params
95 .as_ref()
96 .map(|p| p.get_optional_string("database"))
97 .transpose()?
98 .flatten()
99 .unwrap_or_else(|| "admin".to_string());
100
101 let username = startup_params
102 .as_ref()
103 .map(|p| p.get_optional_string("username"))
104 .transpose()?
105 .flatten();
106
107 let password = startup_params
108 .as_ref()
109 .map(|p| p.get_optional_string("password"))
110 .transpose()?
111 .flatten();
112
113 let connection_string = if let (Some(user), Some(pass)) = (username, password) {
115 format!("mongodb://{}:{}@{}", user, pass, remote_addr)
116 } else {
117 format!("mongodb://{}", remote_addr)
118 };
119
120 let client_options = ClientOptions::parse(&connection_string)
122 .await
123 .context(format!(
124 "Failed to parse MongoDB connection string for {}",
125 remote_addr
126 ))?;
127
128 let mongo_client = MongoClient::with_options(client_options)
130 .context(format!("Failed to connect to MongoDB at {}", remote_addr))?;
131
132 info!("MongoDB client {} connected to {}", client_id, remote_addr);
133
134 let db = mongo_client.database(&database_name);
136
137 let socket_addr: SocketAddr = if remote_addr.contains(':') {
139 remote_addr
140 .parse()
141 .context("Failed to parse socket address")?
142 } else {
143 format!("{}:27017", remote_addr)
144 .parse()
145 .context("Failed to parse socket address")?
146 };
147
148 app_state
150 .update_client_status(client_id, ClientStatus::Connected)
151 .await;
152 let _ = status_tx.send(format!(
153 "[CLIENT] MongoDB client {} connected to {}",
154 client_id, remote_addr
155 ));
156 let _ = status_tx.send("__UPDATE_UI__".to_string());
157
158 let db_arc = Arc::new(db);
162 let protocol = Arc::new(MongodbClientProtocol::new());
163
164 let command_rx =
168 crate::client::command_support::register_command_channel(&app_state, client_id).await;
169 let cmd_task = tokio::spawn(Self::command_loop(
170 command_rx,
171 protocol.clone(),
172 db_arc.clone(),
173 client_id,
174 llm_client.clone(),
175 app_state.clone(),
176 status_tx.clone(),
177 ));
178 app_state.register_client_task(client_id, cmd_task).await;
179
180 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
182 let event = Event::new(
183 &MONGODB_CLIENT_CONNECTED_EVENT,
184 serde_json::json!({
185 "remote_addr": remote_addr,
186 "database": database_name,
187 }),
188 );
189
190 let memory = app_state
191 .get_memory_for_client(client_id)
192 .await
193 .unwrap_or_default();
194
195 let protocol_clone = protocol.clone();
196 let db_clone = db_arc.clone();
197 let app_state_clone = app_state.clone();
198 let status_tx_clone = status_tx.clone();
199
200 let task_registrar = app_state.clone();
203 let task_handle = tokio::spawn(async move {
204 match call_llm_for_client(
205 &llm_client,
206 &app_state_clone,
207 client_id.to_string(),
208 &instruction,
209 &memory,
210 Some(&event),
211 protocol_clone.as_ref(),
212 &status_tx_clone,
213 )
214 .await
215 {
216 Ok(ClientLlmResult {
217 actions,
218 memory_updates,
219 }) => {
220 if let Some(mem) = memory_updates {
222 app_state_clone.set_memory_for_client(client_id, mem).await;
223 }
224
225 for action in actions {
227 match Self::execute_llm_action(
228 client_id,
229 action,
230 &protocol_clone,
231 &db_clone,
232 &app_state_clone,
233 &llm_client,
234 &status_tx_clone,
235 0,
236 )
237 .await
238 {
239 Ok(true) => {}
240 Ok(false) => break,
241 Err(e) => error!("Error executing MongoDB action: {}", e),
242 }
243 }
244 }
245 Err(e) => {
246 error!("LLM error for MongoDB client {}: {}", client_id, e);
247 }
248 }
249 });
250 task_registrar
251 .register_client_task(client_id, task_handle)
252 .await;
253 }
254
255 Ok(socket_addr)
256 }
257
258 #[cfg(target_arch = "wasm32")]
259 async fn connect_impl(
260 _remote_addr: String,
261 _llm_client: OllamaClient,
262 _app_state: Arc<AppState>,
263 _status_tx: mpsc::UnboundedSender<String>,
264 _client_id: ClientId,
265 _startup_params: Option<crate::protocol::StartupParams>,
266 ) -> Result<SocketAddr> {
267 Err(anyhow::anyhow!("MongoDB client feature not enabled"))
268 }
269
270 #[cfg(not(target_arch = "wasm32"))]
277 #[allow(clippy::too_many_arguments)]
278 async fn command_loop(
279 mut command_rx: mpsc::Receiver<ClientCommand>,
280 protocol: Arc<MongodbClientProtocol>,
281 db: Arc<Database>,
282 client_id: ClientId,
283 llm_client: OllamaClient,
284 app_state: Arc<AppState>,
285 status_tx: mpsc::UnboundedSender<String>,
286 ) {
287 use crate::llm::actions::protocol_trait::Protocol;
288 use crate::state::AccessLogOwner;
289
290 while let Some(command) = command_rx.recv().await {
291 let action = command.action.clone();
292
293 let mut follow_up: Option<Event> = None;
294 let outcome = match protocol.execute_action(action.clone()) {
295 Err(e) => Ok(ClientSendOutcome::Rejected {
296 error: e.to_string(),
297 }),
298 Ok(result) => match Self::apply_action(client_id, result, &db, &status_tx).await {
299 Err(e) => Err(e),
300 Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
301 Ok(Applied::Nothing(what)) => Ok(ClientSendOutcome::Executed {
302 detail: what.to_string(),
303 }),
304 Ok(Applied::Ran { detail, event }) => {
305 follow_up = Some(event);
306 Ok(ClientSendOutcome::Executed { detail })
307 }
308 },
309 };
310
311 let outcome_json = match &outcome {
312 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
313 Err(e) => serde_json::json!({"error": e.to_string()}),
314 };
315 app_state
316 .record_access_log(
317 AccessLogOwner::Client(client_id.as_u32()),
318 protocol.protocol_name(),
319 None,
320 "injected_action",
321 action,
322 vec![outcome_json],
323 )
324 .await;
325
326 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
327 if let Err(e) = &outcome {
328 error!("MongoDB client {} injected action failed: {}", client_id, e);
329 let _ = status_tx.send(format!(
330 "[WARN] Client {} injected action failed: {}",
331 client_id, e
332 ));
333 }
334 let _ = status_tx.send("__UPDATE_UI__".to_string());
335 crate::client::command_support::reply(command, outcome);
336
337 if disconnect {
338 app_state
339 .update_client_status(client_id, ClientStatus::Disconnected)
340 .await;
341 let _ = status_tx.send("__UPDATE_UI__".to_string());
342 break;
343 }
344
345 if let Some(event) = follow_up {
348 if let Err(e) = Self::raise_result_event(
349 client_id,
350 event,
351 &protocol,
352 &db,
353 &app_state,
354 &llm_client,
355 &status_tx,
356 0,
357 )
358 .await
359 {
360 error!("MongoDB client {} result event failed: {}", client_id, e);
361 }
362 }
363 }
364
365 app_state.remove_client_handle(client_id).await;
368 let _ = status_tx.send("__UPDATE_UI__".to_string());
369 }
370
371 #[cfg(not(target_arch = "wasm32"))]
379 #[allow(clippy::too_many_arguments)]
380 async fn execute_llm_action(
381 client_id: ClientId,
382 action: serde_json::Value,
383 protocol: &Arc<MongodbClientProtocol>,
384 db: &Arc<Database>,
385 app_state: &Arc<AppState>,
386 llm_client: &OllamaClient,
387 status_tx: &mpsc::UnboundedSender<String>,
388 depth: u8,
389 ) -> Result<bool> {
390 match Self::apply_action(client_id, protocol.execute_action(action)?, db, status_tx).await?
391 {
392 Applied::Ran { event, detail } => {
393 if depth >= MAX_FOLLOWUP_DEPTH {
394 warn!(
395 "MongoDB client {} stopped a follow-up chain at depth {}: '{}' ran but \
396 its result is not being reported, so the model will not answer it",
397 client_id, depth, detail
398 );
399 let _ = status_tx.send(format!(
400 "[WARN] MongoDB client {} hit the follow-up depth limit ({}); the last \
401 operation ran but its result was not reported",
402 client_id, MAX_FOLLOWUP_DEPTH
403 ));
404 } else {
405 Self::raise_result_event(
406 client_id, event, protocol, db, app_state, llm_client, status_tx, depth,
407 )
408 .await?;
409 }
410 }
411 Applied::Disconnect => {
412 info!("MongoDB client {} disconnecting", client_id);
413 app_state
414 .update_client_status(client_id, ClientStatus::Disconnected)
415 .await;
416 return Ok(false);
417 }
418 Applied::Nothing(what) => {
419 trace!(
420 "MongoDB client {} action had no effect: {}",
421 client_id,
422 what
423 );
424 }
425 }
426
427 Ok(true)
428 }
429
430 #[cfg(not(target_arch = "wasm32"))]
433 async fn apply_action(
434 client_id: ClientId,
435 result: crate::llm::actions::client_trait::ClientActionResult,
436 db: &Arc<Database>,
437 status_tx: &mpsc::UnboundedSender<String>,
438 ) -> Result<Applied> {
439 use crate::llm::actions::client_trait::ClientActionResult;
440
441 match result {
442 ClientActionResult::Custom { name, data } if name == "mongodb_find" => {
443 let collection_name = data
444 .get("collection")
445 .and_then(|v| v.as_str())
446 .context("Missing collection")?;
447 let filter_json = data.get("filter").cloned().unwrap_or(serde_json::json!({}));
448 let projection_json = data.get("projection").cloned();
449 let limit = data.get("limit").and_then(|v| v.as_i64());
450
451 trace!(
452 "MongoDB client {} finding in collection: {}",
453 client_id,
454 collection_name
455 );
456
457 let collection = db.collection::<Document>(collection_name);
458
459 let filter = serde_json::from_value::<Document>(filter_json.clone())
461 .context("Failed to convert filter to BSON")?;
462
463 let mut find_options = FindOptions::default();
465 if let Some(proj_json) = projection_json {
466 let projection = serde_json::from_value::<Document>(proj_json).ok();
467 find_options.projection = projection;
468 }
469 if let Some(lim) = limit {
470 find_options.limit = Some(lim);
471 }
472
473 let cursor = collection
475 .find(filter)
476 .with_options(find_options)
477 .await
478 .context("Failed to execute find")?;
479
480 use futures::stream::StreamExt;
482 let documents: Vec<Document> = cursor
483 .collect::<Vec<Result<Document, mongodb::error::Error>>>()
484 .await
485 .into_iter()
486 .filter_map(|r| r.ok())
487 .collect();
488
489 let _ = status_tx.send(format!(
490 "[MongoDB] Found {} documents in {}",
491 documents.len(),
492 collection_name
493 ));
494
495 let detail = format!(
496 "find on '{}' returned {} documents; no byte count — the driver owns the socket",
497 collection_name,
498 documents.len()
499 );
500 let event = result_event("find", Some(documents), None);
501 Ok(Applied::Ran { detail, event })
502 }
503 ClientActionResult::Custom { name, data } if name == "mongodb_insert" => {
504 let collection_name = data
505 .get("collection")
506 .and_then(|v| v.as_str())
507 .context("Missing collection")?;
508 let doc_json = data.get("document").context("Missing document")?;
509
510 trace!(
511 "MongoDB client {} inserting into collection: {}",
512 client_id,
513 collection_name
514 );
515
516 let collection = db.collection::<Document>(collection_name);
517
518 let document = serde_json::from_value::<Document>(doc_json.clone())
520 .context("Failed to convert document to BSON")?;
521
522 let result = collection
524 .insert_one(document)
525 .await
526 .context("Failed to insert document")?;
527
528 let _ = status_tx.send(format!(
529 "[MongoDB] Inserted document into {} (id: {:?})",
530 collection_name, result.inserted_id
531 ));
532
533 let detail = format!(
534 "insert into '{}' acknowledged (id {:?}); no byte count — the driver owns the socket",
535 collection_name, result.inserted_id
536 );
537 let event = result_event("insert", None, Some(1));
538 Ok(Applied::Ran { detail, event })
539 }
540 ClientActionResult::Custom { name, data } if name == "mongodb_update" => {
541 let collection_name = data
542 .get("collection")
543 .and_then(|v| v.as_str())
544 .context("Missing collection")?;
545 let filter_json = data.get("filter").context("Missing filter")?;
546 let update_json = data.get("update").context("Missing update")?;
547
548 trace!(
549 "MongoDB client {} updating collection: {}",
550 client_id,
551 collection_name
552 );
553
554 let collection = db.collection::<Document>(collection_name);
555
556 let filter = serde_json::from_value::<Document>(filter_json.clone())
558 .context("Failed to convert filter to BSON")?;
559 let update = serde_json::from_value::<Document>(update_json.clone())
560 .context("Failed to convert update to BSON")?;
561
562 let result = collection
564 .update_many(filter, update)
565 .await
566 .context("Failed to update documents")?;
567
568 let _ = status_tx.send(format!(
569 "[MongoDB] Updated {} documents in {}",
570 result.modified_count, collection_name
571 ));
572
573 let detail = format!(
574 "update on '{}' modified {} documents; no byte count — the driver owns the socket",
575 collection_name, result.modified_count
576 );
577 let event = result_event("update", None, Some(result.modified_count));
578 Ok(Applied::Ran { detail, event })
579 }
580 ClientActionResult::Custom { name, data } if name == "mongodb_delete" => {
581 let collection_name = data
582 .get("collection")
583 .and_then(|v| v.as_str())
584 .context("Missing collection")?;
585 let filter_json = data.get("filter").context("Missing filter")?;
586
587 trace!(
588 "MongoDB client {} deleting from collection: {}",
589 client_id,
590 collection_name
591 );
592
593 let collection = db.collection::<Document>(collection_name);
594
595 let filter = serde_json::from_value::<Document>(filter_json.clone())
597 .context("Failed to convert filter to BSON")?;
598
599 let result = collection
601 .delete_many(filter)
602 .await
603 .context("Failed to delete documents")?;
604
605 let _ = status_tx.send(format!(
606 "[MongoDB] Deleted {} documents from {}",
607 result.deleted_count, collection_name
608 ));
609
610 let detail = format!(
611 "delete on '{}' removed {} documents; no byte count — the driver owns the socket",
612 collection_name, result.deleted_count
613 );
614 let event = result_event("delete", None, Some(result.deleted_count));
615 Ok(Applied::Ran { detail, event })
616 }
617 ClientActionResult::Custom { name, .. } => {
618 trace!(
619 "MongoDB client {} ignoring custom result '{}'",
620 client_id,
621 name
622 );
623 Ok(Applied::Nothing("custom result not handled by this client"))
624 }
625 ClientActionResult::Disconnect => Ok(Applied::Disconnect),
626 ClientActionResult::WaitForMore => Ok(Applied::Nothing("wait_for_more")),
627 ClientActionResult::NoAction => Ok(Applied::Nothing("no_action")),
628 ClientActionResult::SendData(_) => Ok(Applied::Nothing(
629 "send_data is not meaningful for a mongodb driver handle",
630 )),
631 ClientActionResult::Multiple(_) => Ok(Applied::Nothing(
632 "multiple results not handled by this client",
633 )),
634 }
635 }
636
637 #[cfg(target_arch = "wasm32")]
638 async fn execute_llm_action(
639 _client_id: ClientId,
640 _action: serde_json::Value,
641 _protocol: &Arc<MongodbClientProtocol>,
642 _db: &Arc<()>,
643 _app_state: &Arc<AppState>,
644 _llm_client: &OllamaClient,
645 _status_tx: &mpsc::UnboundedSender<String>,
646 ) -> Result<()> {
647 Err(anyhow::anyhow!("MongoDB client feature not enabled"))
648 }
649
650 #[cfg(not(target_arch = "wasm32"))]
652 #[allow(clippy::too_many_arguments)]
653 async fn raise_result_event(
654 client_id: ClientId,
655 event: Event,
656 protocol: &Arc<MongodbClientProtocol>,
657 db: &Arc<Database>,
658 app_state: &Arc<AppState>,
659 llm_client: &OllamaClient,
660 status_tx: &mpsc::UnboundedSender<String>,
661 depth: u8,
662 ) -> Result<()> {
663 let memory = app_state
664 .get_memory_for_client(client_id)
665 .await
666 .unwrap_or_default();
667 let instruction = app_state
668 .get_instruction_for_client(client_id)
669 .await
670 .unwrap_or_default();
671
672 match call_llm_for_client(
673 llm_client,
674 app_state,
675 client_id.to_string(),
676 &instruction,
677 &memory,
678 Some(&event),
679 protocol.as_ref(),
680 status_tx,
681 )
682 .await
683 {
684 Ok(ClientLlmResult {
685 actions,
686 memory_updates,
687 }) => {
688 if let Some(mem) = memory_updates {
690 app_state.set_memory_for_client(client_id, mem).await;
691 }
692
693 for action in actions {
703 match Box::pin(Self::execute_llm_action(
704 client_id,
705 action.clone(),
706 protocol,
707 db,
708 app_state,
709 llm_client,
710 status_tx,
711 depth + 1,
712 ))
713 .await
714 {
715 Ok(true) => {}
716 Ok(false) => {
717 info!("MongoDB client {} follow-up ended the session", client_id);
718 break;
719 }
720 Err(e) => {
721 error!(
722 "MongoDB client {} follow-up action failed: {}",
723 client_id, e
724 );
725 }
726 }
727 }
728 }
729 Err(e) => {
730 error!("LLM error for MongoDB result: {}", e);
731 }
732 }
733
734 Ok(())
735 }
736}
737
738#[cfg(not(target_arch = "wasm32"))]
740fn result_event(result_type: &str, documents: Option<Vec<Document>>, count: Option<u64>) -> Event {
741 let mut event_data = serde_json::json!({
742 "result_type": result_type,
743 });
744
745 if let Some(docs) = documents {
746 let json_docs: Vec<serde_json::Value> = docs
748 .iter()
749 .filter_map(|doc| serde_json::to_value(doc).ok())
750 .collect();
751 event_data["documents"] = serde_json::json!(json_docs);
752 }
753
754 if let Some(c) = count {
755 event_data["count"] = serde_json::json!(c);
756 }
757
758 Event::new(&MONGODB_CLIENT_RESULT_RECEIVED_EVENT, event_data)
759}