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 MONGODB_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "mongodb_connected",
18 "MongoDB client successfully connected to server",
19 json!({
20 "type": "find_documents",
21 "collection": "users",
22 "filter": {}
23 }),
24 )
25 .with_parameters(vec![
26 Parameter {
27 name: "remote_addr".to_string(),
28 type_hint: "string".to_string(),
29 description: "MongoDB server address".to_string(),
30 required: true,
31 },
32 Parameter {
33 name: "database".to_string(),
34 type_hint: "string".to_string(),
35 description: "Connected database name".to_string(),
36 required: true,
37 },
38 ])
39});
40
41pub static MONGODB_CLIENT_RESULT_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
43 EventType::new(
44 "mongodb_result_received",
45 "Query result received from MongoDB server",
46 json!({
47 "type": "wait_for_more"
48 }),
49 )
50 .with_parameters(vec![
51 Parameter {
52 name: "result_type".to_string(),
53 type_hint: "string".to_string(),
54 description: "Type of result (find, insert, update, delete)".to_string(),
55 required: true,
56 },
57 Parameter {
58 name: "documents".to_string(),
59 type_hint: "array".to_string(),
60 description: "Result documents (for find queries)".to_string(),
61 required: false,
62 },
63 Parameter {
64 name: "count".to_string(),
65 type_hint: "number".to_string(),
66 description: "Count of affected documents (for insert/update/delete)".to_string(),
67 required: false,
68 },
69 ])
70});
71
72pub struct MongodbClientProtocol;
74
75impl MongodbClientProtocol {
76 pub fn new() -> Self {
77 Self
78 }
79}
80
81impl Protocol for MongodbClientProtocol {
83 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
84 vec![
85 ActionDefinition {
86 name: "find_documents".to_string(),
87 description: "Find documents matching a filter".to_string(),
88 parameters: vec![
89 Parameter {
90 name: "collection".to_string(),
91 type_hint: "string".to_string(),
92 description: "Collection name".to_string(),
93 required: true,
94 },
95 Parameter {
96 name: "filter".to_string(),
97 type_hint: "object".to_string(),
98 description: "MongoDB filter query (default: {})".to_string(),
99 required: false,
100 },
101 Parameter {
102 name: "projection".to_string(),
103 type_hint: "object".to_string(),
104 description: "Fields to include/exclude".to_string(),
105 required: false,
106 },
107 Parameter {
108 name: "limit".to_string(),
109 type_hint: "integer".to_string(),
110 description: "Maximum number of documents to return".to_string(),
111 required: false,
112 },
113 ],
114 example: json!({
115 "type": "find_documents",
116 "collection": "users",
117 "filter": {"age": {"$gte": 18}},
118 "limit": 10
119 }),
120 log_template: None,
121 },
122 ActionDefinition {
123 name: "insert_document".to_string(),
124 description: "Insert a document into a collection".to_string(),
125 parameters: vec![
126 Parameter {
127 name: "collection".to_string(),
128 type_hint: "string".to_string(),
129 description: "Collection name".to_string(),
130 required: true,
131 },
132 Parameter {
133 name: "document".to_string(),
134 type_hint: "object".to_string(),
135 description: "Document to insert".to_string(),
136 required: true,
137 },
138 ],
139 example: json!({
140 "type": "insert_document",
141 "collection": "users",
142 "document": {"name": "Alice", "age": 30}
143 }),
144 log_template: None,
145 },
146 ActionDefinition {
147 name: "update_documents".to_string(),
148 description: "Update documents matching a filter".to_string(),
149 parameters: vec![
150 Parameter {
151 name: "collection".to_string(),
152 type_hint: "string".to_string(),
153 description: "Collection name".to_string(),
154 required: true,
155 },
156 Parameter {
157 name: "filter".to_string(),
158 type_hint: "object".to_string(),
159 description: "MongoDB filter query".to_string(),
160 required: true,
161 },
162 Parameter {
163 name: "update".to_string(),
164 type_hint: "object".to_string(),
165 description: "Update operations (e.g., {\"$set\": {...}})".to_string(),
166 required: true,
167 },
168 ],
169 example: json!({
170 "type": "update_documents",
171 "collection": "users",
172 "filter": {"name": "Alice"},
173 "update": {"$set": {"age": 31}}
174 }),
175 log_template: None,
176 },
177 ActionDefinition {
178 name: "delete_documents".to_string(),
179 description: "Delete documents matching a filter".to_string(),
180 parameters: vec![
181 Parameter {
182 name: "collection".to_string(),
183 type_hint: "string".to_string(),
184 description: "Collection name".to_string(),
185 required: true,
186 },
187 Parameter {
188 name: "filter".to_string(),
189 type_hint: "object".to_string(),
190 description: "MongoDB filter query".to_string(),
191 required: true,
192 },
193 ],
194 example: json!({
195 "type": "delete_documents",
196 "collection": "users",
197 "filter": {"age": {"$lt": 18}}
198 }),
199 log_template: None,
200 },
201 ActionDefinition {
202 name: "disconnect".to_string(),
203 description: "Disconnect from the MongoDB server".to_string(),
204 parameters: vec![],
205 example: json!({
206 "type": "disconnect"
207 }),
208 log_template: None,
209 },
210 ]
211 }
212
213 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
214 vec![
215 ActionDefinition {
216 name: "find_documents".to_string(),
217 description: "Find documents in response to received data".to_string(),
218 parameters: vec![
219 Parameter {
220 name: "collection".to_string(),
221 type_hint: "string".to_string(),
222 description: "Collection name".to_string(),
223 required: true,
224 },
225 Parameter {
226 name: "filter".to_string(),
227 type_hint: "object".to_string(),
228 description: "MongoDB filter query".to_string(),
229 required: false,
230 },
231 ],
232 example: json!({
233 "type": "find_documents",
234 "collection": "logs",
235 "filter": {}
236 }),
237 log_template: None,
238 },
239 ActionDefinition {
240 name: "wait_for_more".to_string(),
241 description: "Wait for more results without executing new operations".to_string(),
242 parameters: vec![],
243 example: json!({
244 "type": "wait_for_more"
245 }),
246 log_template: None,
247 },
248 ]
249 }
250
251 fn protocol_name(&self) -> &'static str {
252 "MongoDB"
253 }
254
255 fn get_event_types(&self) -> Vec<EventType> {
256 vec![
257 MONGODB_CLIENT_CONNECTED_EVENT.clone(),
258 MONGODB_CLIENT_RESULT_RECEIVED_EVENT.clone(),
259 ]
260 }
261
262 fn stack_name(&self) -> &'static str {
263 "ETH>IP>TCP>MongoDB"
264 }
265
266 fn keywords(&self) -> Vec<&'static str> {
267 vec![
268 "mongodb",
269 "mongo",
270 "mongodb client",
271 "connect to mongodb",
272 "nosql",
273 "database",
274 ]
275 }
276
277 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
278 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
279
280 ProtocolMetadataV2::builder()
281 .state(DevelopmentState::Experimental)
282 .implementation("Official mongodb v3.3 driver with async support")
283 .llm_control("Full control over CRUD operations and queries")
284 .e2e_testing("NetGet's own MongoDB server; never against a real mongod")
289 .notes(
290 "Follow-up chains are bounded by MAX_FOLLOWUP_DEPTH (4): every operation's \
291 result is reported to the model, and past the bound an action still runs but \
292 its result is not reported. One operation at a time - no aggregation \
293 pipeline, GridFS, change streams or transactions.",
294 )
295 .build()
296 }
297
298 fn description(&self) -> &'static str {
299 "MongoDB client for NoSQL database operations"
300 }
301
302 fn example_prompt(&self) -> &'static str {
303 "Connect to MongoDB at localhost:27017 database testdb and find all users"
304 }
305
306 fn group_name(&self) -> &'static str {
307 "Database"
308 }
309
310 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
311 use crate::llm::actions::StartupExamples;
312 use serde_json::json;
313
314 StartupExamples::new(
315 json!({
317 "type": "open_client",
318 "remote_addr": "localhost:27017",
319 "base_stack": "mongodb",
320 "instruction": "Find all users with age greater than 25 and summarize the results"
321 }),
322 json!({
324 "type": "open_client",
325 "remote_addr": "localhost:27017",
326 "base_stack": "mongodb",
327 "event_handlers": [{
328 "event_pattern": "mongodb_result_received",
329 "handler": {
330 "type": "script",
331 "language": "python",
332 "code": "<mongodb_client_handler>"
333 }
334 }]
335 }),
336 json!({
338 "type": "open_client",
339 "remote_addr": "localhost:27017",
340 "base_stack": "mongodb",
341 "event_handlers": [
342 {
343 "event_pattern": "mongodb_connected",
344 "handler": {
345 "type": "static",
346 "actions": [{
347 "type": "find_documents",
348 "collection": "users",
349 "filter": {},
350 "limit": 10
351 }]
352 }
353 },
354 {
355 "event_pattern": "mongodb_result_received",
356 "handler": {
357 "type": "static",
358 "actions": [{
359 "type": "disconnect"
360 }]
361 }
362 }
363 ]
364 }),
365 )
366 }
367
368 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
369 vec![
370 crate::llm::actions::ParameterDefinition {
371 name: "database".to_string(),
372 type_hint: "string".to_string(),
373 description: "MongoDB database name (default: admin)".to_string(),
374 required: false,
375 example: json!("testdb"),
376 default: None,
377 },
378 crate::llm::actions::ParameterDefinition {
379 name: "username".to_string(),
380 type_hint: "string".to_string(),
381 description: "MongoDB username (if authentication required)".to_string(),
382 required: false,
383 example: json!("myuser"),
384 default: None,
385 },
386 crate::llm::actions::ParameterDefinition {
387 name: "password".to_string(),
388 type_hint: "string".to_string(),
389 description: "MongoDB password (if authentication required)".to_string(),
390 required: false,
391 example: json!("mypassword"),
392 default: None,
393 },
394 ]
395 }
396}
397
398impl Client for MongodbClientProtocol {
400 fn connect(
401 &self,
402 ctx: crate::protocol::ConnectContext,
403 ) -> std::pin::Pin<
404 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
405 > {
406 Box::pin(async move {
407 crate::client::mongodb::MongodbClient::connect_with_llm_actions(
408 ctx.remote_addr,
409 ctx.llm_client,
410 ctx.state,
411 ctx.status_tx,
412 ctx.client_id,
413 ctx.startup_params,
414 )
415 .await
416 })
417 }
418
419 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
420 let action_type = action
421 .get("type")
422 .and_then(|v| v.as_str())
423 .context("Missing 'type' field in action")?;
424
425 match action_type {
426 "find_documents" => {
427 let collection = action
428 .get("collection")
429 .and_then(|v| v.as_str())
430 .context("Missing 'collection' field")?
431 .to_string();
432 let filter = action.get("filter").cloned().unwrap_or(json!({}));
433 let projection = action.get("projection").cloned();
434 let limit = action.get("limit").and_then(|v| v.as_i64());
435
436 Ok(ClientActionResult::Custom {
437 name: "mongodb_find".to_string(),
438 data: json!({
439 "collection": collection,
440 "filter": filter,
441 "projection": projection,
442 "limit": limit
443 }),
444 })
445 }
446 "insert_document" => {
447 let collection = action
448 .get("collection")
449 .and_then(|v| v.as_str())
450 .context("Missing 'collection' field")?
451 .to_string();
452 let document = action
453 .get("document")
454 .context("Missing 'document' field")?
455 .clone();
456
457 Ok(ClientActionResult::Custom {
458 name: "mongodb_insert".to_string(),
459 data: json!({
460 "collection": collection,
461 "document": document
462 }),
463 })
464 }
465 "update_documents" => {
466 let collection = action
467 .get("collection")
468 .and_then(|v| v.as_str())
469 .context("Missing 'collection' field")?
470 .to_string();
471 let filter = action
472 .get("filter")
473 .context("Missing 'filter' field")?
474 .clone();
475 let update = action
476 .get("update")
477 .context("Missing 'update' field")?
478 .clone();
479
480 Ok(ClientActionResult::Custom {
481 name: "mongodb_update".to_string(),
482 data: json!({
483 "collection": collection,
484 "filter": filter,
485 "update": update
486 }),
487 })
488 }
489 "delete_documents" => {
490 let collection = action
491 .get("collection")
492 .and_then(|v| v.as_str())
493 .context("Missing 'collection' field")?
494 .to_string();
495 let filter = action
496 .get("filter")
497 .context("Missing 'filter' field")?
498 .clone();
499
500 Ok(ClientActionResult::Custom {
501 name: "mongodb_delete".to_string(),
502 data: json!({
503 "collection": collection,
504 "filter": filter
505 }),
506 })
507 }
508 "disconnect" => Ok(ClientActionResult::Disconnect),
509 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
510 _ => Err(anyhow::anyhow!(
511 "Unknown MongoDB client action: {}",
512 action_type
513 )),
514 }
515 }
516}