1use 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
14pub 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
29pub 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
52pub struct CassandraClientProtocol;
54
55impl CassandraClientProtocol {
56 pub fn new() -> Self {
57 Self
58 }
59}
60
61impl 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 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 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 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 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
296impl 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}