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::Result;
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static COUCHDB_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "couchdb_connected",
18 "CouchDB client successfully connected to server",
19 json!({"type": "list_databases"}),
20 )
21 .with_parameters(vec![
22 Parameter {
23 name: "remote_addr".to_string(),
24 type_hint: "string".to_string(),
25 description: "CouchDB server address".to_string(),
26 required: true,
27 },
28 Parameter {
29 name: "server_info".to_string(),
30 type_hint: "object".to_string(),
31 description: "Server welcome information (version, etc.)".to_string(),
32 required: false,
33 },
34 ])
35});
36
37pub static COUCHDB_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
39 EventType::new(
40 "couchdb_response_received",
41 "Response received from CouchDB server",
42 json!({"type": "get_document", "database": "mydb", "doc_id": "user1"}),
43 )
44 .with_parameters(vec![
45 Parameter {
46 name: "operation".to_string(),
47 type_hint: "string".to_string(),
48 description: "The operation that was performed".to_string(),
49 required: true,
50 },
51 Parameter {
52 name: "success".to_string(),
53 type_hint: "boolean".to_string(),
54 description: "Whether the operation succeeded".to_string(),
55 required: true,
56 },
57 Parameter {
58 name: "data".to_string(),
59 type_hint: "object".to_string(),
60 description: "Response data from the operation".to_string(),
61 required: false,
62 },
63 Parameter {
64 name: "error".to_string(),
65 type_hint: "string".to_string(),
66 description: "Error message if operation failed".to_string(),
67 required: false,
68 },
69 ])
70});
71
72pub static COUCHDB_CLIENT_CONFLICT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
74 EventType::new(
75 "couchdb_conflict",
76 "Document update conflict detected (revision mismatch)",
77 json!({"type": "get_document", "database": "mydb", "doc_id": "user1"}),
78 )
79 .with_parameters(vec![
80 Parameter {
81 name: "database".to_string(),
82 type_hint: "string".to_string(),
83 description: "Database name".to_string(),
84 required: true,
85 },
86 Parameter {
87 name: "doc_id".to_string(),
88 type_hint: "string".to_string(),
89 description: "Document ID".to_string(),
90 required: true,
91 },
92 Parameter {
93 name: "expected_rev".to_string(),
94 type_hint: "string".to_string(),
95 description: "The revision that was expected".to_string(),
96 required: false,
97 },
98 ])
99});
100
101pub static COUCHDB_CLIENT_CHANGE_DETECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
103 EventType::new(
104 "couchdb_change_detected",
105 "Document change detected in changes feed",
106 json!({"type": "get_document", "database": "mydb", "doc_id": "user1"}),
107 )
108 .with_parameters(vec![
109 Parameter {
110 name: "database".to_string(),
111 type_hint: "string".to_string(),
112 description: "Database name".to_string(),
113 required: true,
114 },
115 Parameter {
116 name: "seq".to_string(),
117 type_hint: "string".to_string(),
118 description: "Sequence number".to_string(),
119 required: true,
120 },
121 Parameter {
122 name: "doc_id".to_string(),
123 type_hint: "string".to_string(),
124 description: "Changed document ID".to_string(),
125 required: true,
126 },
127 Parameter {
128 name: "changes".to_string(),
129 type_hint: "array".to_string(),
130 description: "Array of revision changes".to_string(),
131 required: true,
132 },
133 Parameter {
134 name: "deleted".to_string(),
135 type_hint: "boolean".to_string(),
136 description: "Whether the document was deleted".to_string(),
137 required: false,
138 },
139 ])
140});
141
142pub struct CouchDbClientProtocol;
144
145impl CouchDbClientProtocol {
146 pub fn new() -> Self {
147 Self
148 }
149}
150
151impl Protocol for CouchDbClientProtocol {
153 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
154 vec![
155 ActionDefinition {
156 name: "create_database".to_string(),
157 description: "Create a new database".to_string(),
158 parameters: vec![Parameter {
159 name: "database".to_string(),
160 type_hint: "string".to_string(),
161 description: "Database name".to_string(),
162 required: true,
163 }],
164 example: json!({
165 "type": "create_database",
166 "database": "mydb"
167 }),
168 log_template: None,
169 },
170 ActionDefinition {
171 name: "delete_database".to_string(),
172 description: "Delete a database".to_string(),
173 parameters: vec![Parameter {
174 name: "database".to_string(),
175 type_hint: "string".to_string(),
176 description: "Database name".to_string(),
177 required: true,
178 }],
179 example: json!({
180 "type": "delete_database",
181 "database": "mydb"
182 }),
183 log_template: None,
184 },
185 ActionDefinition {
186 name: "list_databases".to_string(),
187 description: "List all databases".to_string(),
188 parameters: vec![],
189 example: json!({
190 "type": "list_databases"
191 }),
192 log_template: None,
193 },
194 ActionDefinition {
195 name: "create_document".to_string(),
196 description: "Create a new document".to_string(),
197 parameters: vec![
198 Parameter {
199 name: "database".to_string(),
200 type_hint: "string".to_string(),
201 description: "Database name".to_string(),
202 required: true,
203 },
204 Parameter {
205 name: "doc_id".to_string(),
206 type_hint: "string".to_string(),
207 description: "Document ID (optional, auto-generated if not provided)"
208 .to_string(),
209 required: false,
210 },
211 Parameter {
212 name: "document".to_string(),
213 type_hint: "object".to_string(),
214 description: "Document data as JSON object".to_string(),
215 required: true,
216 },
217 ],
218 example: json!({
219 "type": "create_document",
220 "database": "mydb",
221 "doc_id": "user1",
222 "document": {"name": "Alice", "age": 30}
223 }),
224 log_template: None,
225 },
226 ActionDefinition {
227 name: "get_document".to_string(),
228 description: "Retrieve a document".to_string(),
229 parameters: vec![
230 Parameter {
231 name: "database".to_string(),
232 type_hint: "string".to_string(),
233 description: "Database name".to_string(),
234 required: true,
235 },
236 Parameter {
237 name: "doc_id".to_string(),
238 type_hint: "string".to_string(),
239 description: "Document ID".to_string(),
240 required: true,
241 },
242 ],
243 example: json!({
244 "type": "get_document",
245 "database": "mydb",
246 "doc_id": "user1"
247 }),
248 log_template: None,
249 },
250 ActionDefinition {
251 name: "update_document".to_string(),
252 description: "Update an existing document (requires current revision)".to_string(),
253 parameters: vec![
254 Parameter {
255 name: "database".to_string(),
256 type_hint: "string".to_string(),
257 description: "Database name".to_string(),
258 required: true,
259 },
260 Parameter {
261 name: "doc_id".to_string(),
262 type_hint: "string".to_string(),
263 description: "Document ID".to_string(),
264 required: true,
265 },
266 Parameter {
267 name: "document".to_string(),
268 type_hint: "object".to_string(),
269 description: "Updated document data (must include _rev)".to_string(),
270 required: true,
271 },
272 ],
273 example: json!({
274 "type": "update_document",
275 "database": "mydb",
276 "doc_id": "user1",
277 "document": {"_rev": "1-abc", "name": "Alice", "age": 31}
278 }),
279 log_template: None,
280 },
281 ActionDefinition {
282 name: "delete_document".to_string(),
283 description: "Delete a document (requires current revision)".to_string(),
284 parameters: vec![
285 Parameter {
286 name: "database".to_string(),
287 type_hint: "string".to_string(),
288 description: "Database name".to_string(),
289 required: true,
290 },
291 Parameter {
292 name: "doc_id".to_string(),
293 type_hint: "string".to_string(),
294 description: "Document ID".to_string(),
295 required: true,
296 },
297 Parameter {
298 name: "rev".to_string(),
299 type_hint: "string".to_string(),
300 description: "Current document revision".to_string(),
301 required: true,
302 },
303 ],
304 example: json!({
305 "type": "delete_document",
306 "database": "mydb",
307 "doc_id": "user1",
308 "rev": "2-abc"
309 }),
310 log_template: None,
311 },
312 ActionDefinition {
313 name: "bulk_docs".to_string(),
314 description: "Perform bulk document operations".to_string(),
315 parameters: vec![
316 Parameter {
317 name: "database".to_string(),
318 type_hint: "string".to_string(),
319 description: "Database name".to_string(),
320 required: true,
321 },
322 Parameter {
323 name: "docs".to_string(),
324 type_hint: "array".to_string(),
325 description: "Array of documents to create/update".to_string(),
326 required: true,
327 },
328 ],
329 example: json!({
330 "type": "bulk_docs",
331 "database": "mydb",
332 "docs": [
333 {"_id": "doc1", "name": "Alice"},
334 {"_id": "doc2", "name": "Bob"}
335 ]
336 }),
337 log_template: None,
338 },
339 ActionDefinition {
340 name: "list_documents".to_string(),
341 description: "List all documents in a database".to_string(),
342 parameters: vec![
343 Parameter {
344 name: "database".to_string(),
345 type_hint: "string".to_string(),
346 description: "Database name".to_string(),
347 required: true,
348 },
349 Parameter {
350 name: "include_docs".to_string(),
351 type_hint: "boolean".to_string(),
352 description: "Whether to include full document content".to_string(),
353 required: false,
354 },
355 ],
356 example: json!({
357 "type": "list_documents",
358 "database": "mydb",
359 "include_docs": false
360 }),
361 log_template: None,
362 },
363 ActionDefinition {
364 name: "query_view".to_string(),
365 description: "Query a MapReduce view".to_string(),
366 parameters: vec![
367 Parameter {
368 name: "database".to_string(),
369 type_hint: "string".to_string(),
370 description: "Database name".to_string(),
371 required: true,
372 },
373 Parameter {
374 name: "design_doc".to_string(),
375 type_hint: "string".to_string(),
376 description: "Design document name".to_string(),
377 required: true,
378 },
379 Parameter {
380 name: "view_name".to_string(),
381 type_hint: "string".to_string(),
382 description: "View name".to_string(),
383 required: true,
384 },
385 Parameter {
386 name: "params".to_string(),
387 type_hint: "object".to_string(),
388 description: "Query parameters (key, limit, skip, etc.)".to_string(),
389 required: false,
390 },
391 ],
392 example: json!({
393 "type": "query_view",
394 "database": "mydb",
395 "design_doc": "users",
396 "view_name": "by_age",
397 "params": {"limit": 10}
398 }),
399 log_template: None,
400 },
401 ActionDefinition {
402 name: "watch_changes".to_string(),
403 description: "Start watching changes feed".to_string(),
404 parameters: vec![
405 Parameter {
406 name: "database".to_string(),
407 type_hint: "string".to_string(),
408 description: "Database name".to_string(),
409 required: true,
410 },
411 Parameter {
412 name: "since".to_string(),
413 type_hint: "string".to_string(),
414 description: "Start sequence (or 'now')".to_string(),
415 required: false,
416 },
417 Parameter {
418 name: "feed".to_string(),
419 type_hint: "string".to_string(),
420 description: "Feed type: normal, longpoll, or continuous".to_string(),
421 required: false,
422 },
423 ],
424 example: json!({
425 "type": "watch_changes",
426 "database": "mydb",
427 "since": "now",
428 "feed": "longpoll"
429 }),
430 log_template: None,
431 },
432 ActionDefinition {
433 name: "disconnect".to_string(),
434 description: "Disconnect from the CouchDB server".to_string(),
435 parameters: vec![],
436 example: json!({
437 "type": "disconnect"
438 }),
439 log_template: None,
440 },
441 ActionDefinition {
445 name: "wait_for_more".to_string(),
446 description: "Do nothing and wait for the next response. Correct when the \
447 previous operation needs no follow-up."
448 .to_string(),
449 parameters: vec![],
450 example: json!({
451 "type": "wait_for_more"
452 }),
453 log_template: None,
454 },
455 ]
456 }
457
458 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
459 self.get_async_actions(&AppState::new())
461 }
462
463 fn protocol_name(&self) -> &'static str {
464 "CouchDB"
465 }
466
467 fn get_event_types(&self) -> Vec<EventType> {
468 vec![
469 COUCHDB_CLIENT_CONNECTED_EVENT.clone(),
470 COUCHDB_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
471 COUCHDB_CLIENT_CONFLICT_EVENT.clone(),
472 COUCHDB_CLIENT_CHANGE_DETECTED_EVENT.clone(),
473 ]
474 }
475
476 fn stack_name(&self) -> &'static str {
477 "ETH>IP>TCP>HTTP>COUCHDB"
478 }
479
480 fn keywords(&self) -> Vec<&'static str> {
481 vec!["couchdb client", "connect to couchdb", "couchdb"]
482 }
483
484 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
485 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
486
487 ProtocolMetadataV2::builder()
488 .state(DevelopmentState::Experimental)
489 .implementation("couch_rs library with LLM action control")
490 .llm_control("Full control over CouchDB operations")
491 .e2e_testing("NetGet CouchDB server")
492 .build()
493 }
494
495 fn description(&self) -> &'static str {
496 "CouchDB client for document database operations"
497 }
498
499 fn example_prompt(&self) -> &'static str {
500 "Connect to CouchDB at localhost:5984 and create a database called 'myapp'"
501 }
502
503 fn group_name(&self) -> &'static str {
504 "Database"
505 }
506
507 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
508 use crate::llm::actions::StartupExamples;
509 use serde_json::json;
510
511 StartupExamples::new(
512 json!({
514 "type": "open_client",
515 "remote_addr": "localhost:5984",
516 "base_stack": "couchdb",
517 "instruction": "List all databases and then get documents from the 'users' database"
518 }),
519 json!({
521 "type": "open_client",
522 "remote_addr": "localhost:5984",
523 "base_stack": "couchdb",
524 "event_handlers": [{
525 "event_pattern": "couchdb_response_received",
526 "handler": {
527 "type": "script",
528 "language": "python",
529 "code": "<couchdb_client_handler>"
530 }
531 }]
532 }),
533 json!({
535 "type": "open_client",
536 "remote_addr": "localhost:5984",
537 "base_stack": "couchdb",
538 "event_handlers": [
539 {
540 "event_pattern": "couchdb_connected",
541 "handler": {
542 "type": "static",
543 "actions": [{
544 "type": "list_databases"
545 }]
546 }
547 },
548 {
549 "event_pattern": "couchdb_response_received",
550 "handler": {
551 "type": "static",
552 "actions": [{
553 "type": "disconnect"
554 }]
555 }
556 }
557 ]
558 }),
559 )
560 }
561
562 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
563 vec![
564 crate::llm::actions::ParameterDefinition {
565 name: "username".to_string(),
566 type_hint: "string".to_string(),
567 description: "CouchDB username (for basic auth)".to_string(),
568 required: false,
569 example: json!("admin"),
570 default: None,
571 },
572 crate::llm::actions::ParameterDefinition {
573 name: "password".to_string(),
574 type_hint: "string".to_string(),
575 description: "CouchDB password (for basic auth)".to_string(),
576 required: false,
577 example: json!("password"),
578 default: None,
579 },
580 ]
581 }
582}
583
584impl Client for CouchDbClientProtocol {
586 fn connect(
587 &self,
588 ctx: crate::protocol::ConnectContext,
589 ) -> std::pin::Pin<
590 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
591 > {
592 Box::pin(async move {
593 use crate::client::couchdb::CouchDbClient;
594
595 let username = ctx
596 .startup_params
597 .as_ref()
598 .map(|p| p.get_optional_string("username"))
599 .transpose()?
600 .flatten();
601
602 let password = ctx
603 .startup_params
604 .as_ref()
605 .map(|p| p.get_optional_string("password"))
606 .transpose()?
607 .flatten();
608
609 CouchDbClient::connect_with_llm_actions(
610 ctx.remote_addr,
611 ctx.llm_client,
612 ctx.state,
613 ctx.status_tx,
614 ctx.client_id,
615 username,
616 password,
617 )
618 .await
619 })
620 }
621
622 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
623 let action_type = action
624 .get("type")
625 .and_then(|v| v.as_str())
626 .ok_or_else(|| anyhow::anyhow!("Missing action type"))?;
627
628 const COUCHDB_CLIENT_ACTIONS: &[&str] = &[
634 "create_database",
635 "delete_database",
636 "list_databases",
637 "create_document",
638 "get_document",
639 "update_document",
640 "delete_document",
641 "list_documents",
642 "bulk_docs",
643 "query_view",
644 "watch_changes",
645 "disconnect",
646 "wait_for_more",
647 ];
648
649 if !COUCHDB_CLIENT_ACTIONS.contains(&action_type) {
650 return Err(anyhow::anyhow!(
651 "Unknown CouchDB client action '{}'. Valid actions: {}",
652 action_type,
653 COUCHDB_CLIENT_ACTIONS.join(", ")
654 ));
655 }
656
657 Ok(ClientActionResult::Custom {
658 name: action_type.to_string(),
659 data: action,
660 })
661 }
662}