Skip to main content

netget/client/etcd/
actions.rs

1//! etcd 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/// etcd client connected event
15pub static ETCD_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "etcd_connected",
18        "etcd client successfully connected to server",
19        json!({
20            "type": "etcd_get",
21            "key": "/config/database"
22        }),
23    )
24    .with_parameters(vec![Parameter {
25        name: "remote_addr".to_string(),
26        type_hint: "string".to_string(),
27        description: "etcd server address".to_string(),
28        required: true,
29    }])
30});
31
32/// etcd client response received event
33pub static ETCD_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
34    EventType::new(
35        "etcd_response_received",
36        "Response received from etcd server",
37        json!({
38            "type": "etcd_put",
39            "key": "/config/database",
40            "value": "postgresql://localhost:5432/mydb"
41        }),
42    )
43    .with_parameters(vec![
44        Parameter {
45            name: "operation".to_string(),
46            type_hint: "string".to_string(),
47            description: "The operation type (get, put, delete)".to_string(),
48            required: true,
49        },
50        Parameter {
51            name: "key".to_string(),
52            type_hint: "string".to_string(),
53            description: "The key that was operated on".to_string(),
54            required: true,
55        },
56    ])
57});
58
59/// etcd client protocol action handler
60pub struct EtcdClientProtocol;
61
62impl EtcdClientProtocol {
63    pub fn new() -> Self {
64        Self
65    }
66}
67
68// Implement Protocol trait (common functionality)
69impl Protocol for EtcdClientProtocol {
70    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
71        vec![
72            ActionDefinition {
73                name: "etcd_get".to_string(),
74                description: "Get a key-value pair from etcd".to_string(),
75                parameters: vec![Parameter {
76                    name: "key".to_string(),
77                    type_hint: "string".to_string(),
78                    description: "Key to retrieve".to_string(),
79                    required: true,
80                }],
81                example: json!({
82                    "type": "etcd_get",
83                    "key": "/config/database"
84                }),
85                log_template: None,
86            },
87            ActionDefinition {
88                name: "etcd_put".to_string(),
89                description: "Put a key-value pair into etcd".to_string(),
90                parameters: vec![
91                    Parameter {
92                        name: "key".to_string(),
93                        type_hint: "string".to_string(),
94                        description: "Key to set".to_string(),
95                        required: true,
96                    },
97                    Parameter {
98                        name: "value".to_string(),
99                        type_hint: "string".to_string(),
100                        description: "Value to set".to_string(),
101                        required: true,
102                    },
103                ],
104                example: json!({
105                    "type": "etcd_put",
106                    "key": "/config/database",
107                    "value": "postgresql://localhost:5432/mydb"
108                }),
109                log_template: None,
110            },
111            ActionDefinition {
112                name: "etcd_delete".to_string(),
113                description: "Delete a key from etcd".to_string(),
114                parameters: vec![Parameter {
115                    name: "key".to_string(),
116                    type_hint: "string".to_string(),
117                    description: "Key to delete".to_string(),
118                    required: true,
119                }],
120                example: json!({
121                    "type": "etcd_delete",
122                    "key": "/config/database"
123                }),
124                log_template: None,
125            },
126            ActionDefinition {
127                name: "disconnect".to_string(),
128                description: "Disconnect from the etcd server".to_string(),
129                parameters: vec![],
130                example: json!({
131                    "type": "disconnect"
132                }),
133                log_template: None,
134            },
135        ]
136    }
137    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
138        vec![
139            ActionDefinition {
140                name: "etcd_get".to_string(),
141                description: "Get a key in response to received data".to_string(),
142                parameters: vec![Parameter {
143                    name: "key".to_string(),
144                    type_hint: "string".to_string(),
145                    description: "Key to retrieve".to_string(),
146                    required: true,
147                }],
148                example: json!({
149                    "type": "etcd_get",
150                    "key": "/config/database"
151                }),
152                log_template: None,
153            },
154            ActionDefinition {
155                name: "etcd_put".to_string(),
156                description: "Put a key in response to received data".to_string(),
157                parameters: vec![
158                    Parameter {
159                        name: "key".to_string(),
160                        type_hint: "string".to_string(),
161                        description: "Key to set".to_string(),
162                        required: true,
163                    },
164                    Parameter {
165                        name: "value".to_string(),
166                        type_hint: "string".to_string(),
167                        description: "Value to set".to_string(),
168                        required: true,
169                    },
170                ],
171                example: json!({
172                    "type": "etcd_put",
173                    "key": "/config/database",
174                    "value": "postgresql://localhost:5432/mydb"
175                }),
176                log_template: None,
177            },
178        ]
179    }
180    fn protocol_name(&self) -> &'static str {
181        "etcd"
182    }
183    fn get_event_types(&self) -> Vec<EventType> {
184        vec![
185            EventType::new(
186                "etcd_connected",
187                "Triggered when etcd client connects to server",
188                json!({"type": "placeholder", "event_id": "etcd_connected"}),
189            ),
190            EventType::new(
191                "etcd_response_received",
192                "Triggered when etcd client receives a response",
193                json!({"type": "placeholder", "event_id": "etcd_response_received"}),
194            ),
195        ]
196    }
197    fn stack_name(&self) -> &'static str {
198        "ETH>IP>TCP>gRPC>etcd"
199    }
200    fn keywords(&self) -> Vec<&'static str> {
201        vec!["etcd", "etcd client", "connect to etcd"]
202    }
203    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
204        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
205
206        ProtocolMetadataV2::builder()
207            .state(DevelopmentState::Beta)
208            .implementation("etcd-client crate for gRPC-based KV operations")
209            .llm_control("Full control over get/put/delete operations")
210            .e2e_testing(
211                "tests/client/etcd/real_server_test.rs, 6 LLM calls, against the official Go \
212                 etcd (grpc-go; NetGet's client is etcd-client on tonic, so no code is shared) \
213                 read back with etcdctl. The model puts a key, gets it, puts a second value \
214                 built from the kvs entry etcd returned, and deletes the first; etcdctl must \
215                 find exactly that value and the deleted key gone. Checked against etcd 3.5.17 \
216                 and 3.7.1. Not #[ignore]d, and a missing etcd or etcdctl fails the test rather \
217                 than skipping it.",
218            )
219            .notes(
220                "Single-key get, put and delete only: no range or prefix reads, transactions, \
221                 watches, leases, authentication or TLS.",
222            )
223            .build()
224    }
225    fn description(&self) -> &'static str {
226        "etcd client for distributed key-value operations"
227    }
228    fn example_prompt(&self) -> &'static str {
229        "Connect to etcd at localhost:2379 and get the value of '/config/database'"
230    }
231    fn group_name(&self) -> &'static str {
232        "Database"
233    }
234    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
235        use crate::llm::actions::StartupExamples;
236        use serde_json::json;
237
238        StartupExamples::new(
239            // LLM mode: LLM controls etcd operations
240            json!({
241                "type": "open_client",
242                "remote_addr": "localhost:2379",
243                "base_stack": "etcd",
244                "instruction": "Store a configuration key and retrieve it back"
245            }),
246            // Script mode: Code-based key-value operations
247            json!({
248                "type": "open_client",
249                "remote_addr": "localhost:2379",
250                "base_stack": "etcd",
251                "event_handlers": [{
252                    "event_pattern": "etcd_response_received",
253                    "handler": {
254                        "type": "script",
255                        "language": "python",
256                        "code": "<etcd_client_handler>"
257                    }
258                }]
259            }),
260            // Static mode: Fixed get/put operations
261            json!({
262                "type": "open_client",
263                "remote_addr": "localhost:2379",
264                "base_stack": "etcd",
265                "event_handlers": [
266                    {
267                        "event_pattern": "etcd_connected",
268                        "handler": {
269                            "type": "static",
270                            "actions": [{
271                                "type": "etcd_put",
272                                "key": "/config/app",
273                                "value": "production"
274                            }]
275                        }
276                    },
277                    {
278                        "event_pattern": "etcd_response_received",
279                        "handler": {
280                            "type": "static",
281                            "actions": [{
282                                "type": "disconnect"
283                            }]
284                        }
285                    }
286                ]
287            }),
288        )
289    }
290}
291
292// Implement Client trait (client-specific functionality)
293impl Client for EtcdClientProtocol {
294    fn connect(
295        &self,
296        ctx: crate::protocol::ConnectContext,
297    ) -> std::pin::Pin<
298        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
299    > {
300        Box::pin(async move {
301            use crate::client::etcd::EtcdClient;
302            EtcdClient::connect_with_llm_actions(
303                ctx.remote_addr,
304                ctx.llm_client,
305                ctx.state,
306                ctx.status_tx,
307                ctx.client_id,
308            )
309            .await
310        })
311    }
312    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
313        let action_type = action
314            .get("type")
315            .and_then(|v| v.as_str())
316            .context("Missing 'type' field in action")?;
317
318        match action_type {
319            "etcd_get" => {
320                let key = action
321                    .get("key")
322                    .and_then(|v| v.as_str())
323                    .context("Missing 'key' field")?
324                    .to_string();
325
326                Ok(ClientActionResult::Custom {
327                    name: "etcd_get".to_string(),
328                    data: json!({
329                        "key": key,
330                    }),
331                })
332            }
333            "etcd_put" => {
334                let key = action
335                    .get("key")
336                    .and_then(|v| v.as_str())
337                    .context("Missing 'key' field")?
338                    .to_string();
339
340                let value = action
341                    .get("value")
342                    .and_then(|v| v.as_str())
343                    .context("Missing 'value' field")?
344                    .to_string();
345
346                Ok(ClientActionResult::Custom {
347                    name: "etcd_put".to_string(),
348                    data: json!({
349                        "key": key,
350                        "value": value,
351                    }),
352                })
353            }
354            "etcd_delete" => {
355                let key = action
356                    .get("key")
357                    .and_then(|v| v.as_str())
358                    .context("Missing 'key' field")?
359                    .to_string();
360
361                Ok(ClientActionResult::Custom {
362                    name: "etcd_delete".to_string(),
363                    data: json!({
364                        "key": key,
365                    }),
366                })
367            }
368            "disconnect" => Ok(ClientActionResult::Disconnect),
369            _ => Err(anyhow::anyhow!(
370                "Unknown etcd client action: {}",
371                action_type
372            )),
373        }
374    }
375}