Skip to main content

netget/client/cassandra/
actions.rs

1//! Cassandra client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter, ParameterDefinition,
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/// Cassandra client connected event
15pub static CASSANDRA_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "cassandra_connected",
18        "Cassandra client successfully connected to server",
19        json!({"type": "execute_cql_query", "query": "SELECT * FROM system.local"}),
20    )
21    .with_parameters(vec![Parameter {
22        name: "remote_addr".to_string(),
23        type_hint: "string".to_string(),
24        description: "Cassandra server address".to_string(),
25        required: true,
26    }])
27});
28
29/// Cassandra client query result received event
30pub static CASSANDRA_CLIENT_RESULT_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31    EventType::new(
32        "cassandra_result_received",
33        "Query result received from Cassandra server",
34        json!({"type": "execute_cql_query", "query": "SELECT * FROM users WHERE id = 1"}),
35    )
36    .with_parameters(vec![
37        Parameter {
38            name: "rows".to_string(),
39            type_hint: "array".to_string(),
40            description: "Query result rows".to_string(),
41            required: true,
42        },
43        Parameter {
44            name: "row_count".to_string(),
45            type_hint: "number".to_string(),
46            description: "Number of rows returned".to_string(),
47            required: true,
48        },
49    ])
50});
51
52/// Cassandra client protocol action handler
53pub struct CassandraClientProtocol;
54
55impl CassandraClientProtocol {
56    pub fn new() -> Self {
57        Self
58    }
59}
60
61// Implement Protocol trait (common functionality)
62impl Protocol for CassandraClientProtocol {
63    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
64        vec![
65            ActionDefinition {
66                name: "execute_cql_query".to_string(),
67                description: "Execute a CQL query".to_string(),
68                parameters: vec![
69                    Parameter {
70                        name: "query".to_string(),
71                        type_hint: "string".to_string(),
72                        description: "CQL query (e.g., SELECT * FROM keyspace.table WHERE id = 1)"
73                            .to_string(),
74                        required: true,
75                    },
76                    Parameter {
77                        name: "consistency".to_string(),
78                        type_hint: "string".to_string(),
79                        // This is applied to the statement, not merely logged: it used to be
80                        // read and discarded, so a request for QUORUM ran at the session
81                        // default (LOCAL_QUORUM in scylla) and the log claimed otherwise.
82                        // An unrecognised name is refused rather than downgraded.
83                        description: "Consistency level for this statement: ANY, ONE, TWO, \
84                                      THREE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM, SERIAL, \
85                                      LOCAL_SERIAL or LOCAL_ONE. Omit to use the session \
86                                      default; an unrecognised value is rejected rather than \
87                                      silently downgraded"
88                            .to_string(),
89                        required: false,
90                    },
91                ],
92                example: json!({
93                    "type": "execute_cql_query",
94                    "query": "SELECT * FROM system.local",
95                    "consistency": "ONE"
96                }),
97                log_template: None,
98            },
99            ActionDefinition {
100                name: "disconnect".to_string(),
101                description: "Disconnect from the Cassandra server".to_string(),
102                parameters: vec![],
103                example: json!({
104                    "type": "disconnect"
105                }),
106                log_template: None,
107            },
108            ActionDefinition {
109                name: "wait_for_more".to_string(),
110                description: "Wait for more results or do nothing".to_string(),
111                parameters: vec![],
112                example: json!({
113                    "type": "wait_for_more"
114                }),
115                log_template: None,
116            },
117        ]
118    }
119    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
120        vec![
121            ActionDefinition {
122                name: "execute_cql_query".to_string(),
123                description: "Execute a CQL query in response to received data".to_string(),
124                parameters: vec![
125                    Parameter {
126                        name: "query".to_string(),
127                        type_hint: "string".to_string(),
128                        description: "CQL query".to_string(),
129                        required: true,
130                    },
131                    Parameter {
132                        name: "consistency".to_string(),
133                        type_hint: "string".to_string(),
134                        description: "Consistency level for this statement: ANY, ONE, TWO, \
135                                      THREE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM, SERIAL, \
136                                      LOCAL_SERIAL or LOCAL_ONE. Omit to use the session \
137                                      default; an unrecognised value is rejected rather than \
138                                      silently downgraded"
139                            .to_string(),
140                        required: false,
141                    },
142                ],
143                example: json!({
144                    "type": "execute_cql_query",
145                    "query": "SELECT * FROM users WHERE id = 1"
146                }),
147                log_template: None,
148            },
149            ActionDefinition {
150                name: "wait_for_more".to_string(),
151                description: "Wait for more results".to_string(),
152                parameters: vec![],
153                example: json!({
154                    "type": "wait_for_more"
155                }),
156                log_template: None,
157            },
158        ]
159    }
160    fn protocol_name(&self) -> &'static str {
161        "Cassandra"
162    }
163    fn get_event_types(&self) -> Vec<EventType> {
164        vec![
165            EventType::new(
166                "cassandra_connected",
167                "Triggered when Cassandra client connects to server",
168                json!({"type": "execute_cql_query", "query": "SELECT * FROM system.local"}),
169            ),
170            EventType::new(
171                "cassandra_result_received",
172                "Triggered when Cassandra client receives query results",
173                json!({"type": "execute_cql_query", "query": "SELECT * FROM users WHERE id = 1"}),
174            ),
175        ]
176    }
177    fn stack_name(&self) -> &'static str {
178        "ETH>IP>TCP>CASSANDRA"
179    }
180    fn keywords(&self) -> Vec<&'static str> {
181        vec![
182            "cassandra",
183            "cassandra client",
184            "connect to cassandra",
185            "cql",
186            "scylla",
187        ]
188    }
189    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
190        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
191
192        ProtocolMetadataV2::builder()
193            .state(DevelopmentState::Experimental)
194            .implementation("ScyllaDB Rust driver (scylla crate)")
195            .llm_control("Full control over CQL queries with consistency levels")
196            .e2e_testing("Docker Cassandra container")
197            .build()
198    }
199    fn description(&self) -> &'static str {
200        "Cassandra/ScyllaDB client for CQL queries"
201    }
202    fn example_prompt(&self) -> &'static str {
203        "Connect to Cassandra at localhost:9042 and query system.local table"
204    }
205    fn group_name(&self) -> &'static str {
206        "Database"
207    }
208
209    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
210        use crate::llm::actions::StartupExamples;
211        use serde_json::json;
212
213        StartupExamples::new(
214            // LLM mode: LLM controls Cassandra CQL queries
215            json!({
216                "type": "open_client",
217                "remote_addr": "localhost:9042",
218                "base_stack": "cassandra",
219                "instruction": "Query the system.local table and report the cluster information"
220            }),
221            // Script mode: Code-based deterministic responses
222            json!({
223                "type": "open_client",
224                "remote_addr": "localhost:9042",
225                "base_stack": "cassandra",
226                "event_handlers": [{
227                    "event_pattern": "cassandra_result_received",
228                    "handler": {
229                        "type": "script",
230                        "language": "python",
231                        "code": "<cassandra_client_handler>"
232                    }
233                }]
234            }),
235            // Static mode: Fixed CQL query on connect
236            json!({
237                "type": "open_client",
238                "remote_addr": "localhost:9042",
239                "base_stack": "cassandra",
240                "event_handlers": [
241                    {
242                        "event_pattern": "cassandra_connected",
243                        "handler": {
244                            "type": "static",
245                            "actions": [{
246                                "type": "execute_cql_query",
247                                "query": "SELECT * FROM system.local",
248                                "consistency": "ONE"
249                            }]
250                        }
251                    },
252                    {
253                        "event_pattern": "cassandra_result_received",
254                        "handler": {
255                            "type": "static",
256                            "actions": [{
257                                "type": "disconnect"
258                            }]
259                        }
260                    }
261                ]
262            }),
263        )
264    }
265
266    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
267        vec![
268            ParameterDefinition {
269                name: "keyspace".to_string(),
270                type_hint: "string".to_string(),
271                description: "Default keyspace to use".to_string(),
272                required: false,
273                example: json!("my_keyspace"),
274                default: None,
275            },
276            ParameterDefinition {
277                name: "username".to_string(),
278                type_hint: "string".to_string(),
279                description: "Username for authentication".to_string(),
280                required: false,
281                example: json!("cassandra"),
282                default: None,
283            },
284            ParameterDefinition {
285                name: "password".to_string(),
286                type_hint: "string".to_string(),
287                description: "Password for authentication".to_string(),
288                required: false,
289                example: json!("cassandra"),
290                default: None,
291            },
292        ]
293    }
294}
295
296// Implement Client trait (client-specific functionality)
297impl Client for CassandraClientProtocol {
298    fn connect(
299        &self,
300        ctx: crate::protocol::ConnectContext,
301    ) -> std::pin::Pin<
302        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
303    > {
304        Box::pin(async move {
305            use crate::client::cassandra::CassandraClient;
306            CassandraClient::connect_with_llm_actions(
307                ctx.remote_addr,
308                ctx.llm_client,
309                ctx.state,
310                ctx.status_tx,
311                ctx.client_id,
312                ctx.startup_params,
313            )
314            .await
315        })
316    }
317    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
318        let action_type = action
319            .get("type")
320            .and_then(|v| v.as_str())
321            .context("Missing 'type' field in action")?;
322
323        match action_type {
324            "execute_cql_query" => {
325                let query = action
326                    .get("query")
327                    .and_then(|v| v.as_str())
328                    .context("Missing 'query' field")?
329                    .to_string();
330
331                let consistency = action
332                    .get("consistency")
333                    .and_then(|v| v.as_str())
334                    .map(|s| s.to_string());
335
336                Ok(ClientActionResult::Custom {
337                    name: "cql_query".to_string(),
338                    data: json!({
339                        "query": query,
340                        "consistency": consistency,
341                    }),
342                })
343            }
344            "disconnect" => Ok(ClientActionResult::Disconnect),
345            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
346            _ => Err(anyhow::anyhow!(
347                "Unknown Cassandra client action: {}",
348                action_type
349            )),
350        }
351    }
352}