Skip to main content

netget/client/mongodb/
actions.rs

1//! MongoDB client protocol actions implementation
2
3use 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
14/// MongoDB client connected event
15pub 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
41/// MongoDB client result received event
42pub 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
72/// MongoDB client protocol action handler
73pub struct MongodbClientProtocol;
74
75impl MongodbClientProtocol {
76    pub fn new() -> Self {
77        Self
78    }
79}
80
81// Implement Protocol trait (common functionality)
82impl 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            // Not a real MongoDB server, which is what this said. The e2e tests point the
285            // driver at NetGet's *own* MongoDB server (feature `mongodb-server`), so the
286            // official driver is the thing under test rather than the independent peer
287            // corroborating it. No test has ever run this client against mongod.
288            .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            // LLM mode: LLM controls MongoDB operations
316            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            // Script mode: Code-based deterministic responses
323            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            // Static mode: Fixed MongoDB query on connect
337            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
398// Implement Client trait (client-specific functionality)
399impl 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}