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::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static MYSQL_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "mysql_connected",
18 "MySQL client successfully connected to server",
19 json!({
20 "type": "execute_query",
21 "query": "SELECT * FROM users LIMIT 10"
22 }),
23 )
24 .with_parameters(vec![Parameter {
25 name: "remote_addr".to_string(),
26 type_hint: "string".to_string(),
27 description: "MySQL server address".to_string(),
28 required: true,
29 }])
30});
31
32pub static MYSQL_CLIENT_RESULT_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
34 EventType::new(
35 "mysql_result_received",
36 "Query result received from MySQL server",
37 json!({
38 "type": "wait_for_more"
39 }),
40 )
41 .with_parameters(vec![
42 Parameter {
43 name: "query".to_string(),
44 type_hint: "string".to_string(),
45 description: "The SQL query this result belongs to".to_string(),
46 required: true,
47 },
48 Parameter {
49 name: "result".to_string(),
50 type_hint: "array".to_string(),
51 description: "The rows returned, one object per row keyed by column name".to_string(),
52 required: true,
53 },
54 Parameter {
55 name: "row_count".to_string(),
56 type_hint: "number".to_string(),
57 description: "Number of rows returned".to_string(),
58 required: true,
59 },
60 Parameter {
61 name: "affected_rows".to_string(),
62 type_hint: "number".to_string(),
63 description: "Rows the server reports as affected (INSERT/UPDATE/DELETE); 0 for a \
64 SELECT"
65 .to_string(),
66 required: true,
67 },
68 Parameter {
69 name: "last_insert_id".to_string(),
70 type_hint: "number".to_string(),
71 description: "The AUTO_INCREMENT id the statement generated, when it generated one"
72 .to_string(),
73 required: false,
74 },
75 ])
76});
77
78pub struct MysqlClientProtocol;
80
81impl MysqlClientProtocol {
82 pub fn new() -> Self {
83 Self
84 }
85}
86
87impl Protocol for MysqlClientProtocol {
89 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
90 vec![
91 ActionDefinition {
92 name: "execute_query".to_string(),
93 description: "Execute a SQL query (SELECT, INSERT, UPDATE, DELETE, etc.)"
94 .to_string(),
95 parameters: vec![Parameter {
96 name: "query".to_string(),
97 type_hint: "string".to_string(),
98 description: "SQL query to execute".to_string(),
99 required: true,
100 }],
101 example: json!({
102 "type": "execute_query",
103 "query": "SELECT * FROM users WHERE id = 1"
104 }),
105 log_template: None,
106 },
107 ActionDefinition {
108 name: "begin_transaction".to_string(),
109 description: "Begin a transaction".to_string(),
110 parameters: vec![],
111 example: json!({
112 "type": "begin_transaction"
113 }),
114 log_template: None,
115 },
116 ActionDefinition {
117 name: "commit_transaction".to_string(),
118 description: "Commit the current transaction".to_string(),
119 parameters: vec![],
120 example: json!({
121 "type": "commit_transaction"
122 }),
123 log_template: None,
124 },
125 ActionDefinition {
126 name: "rollback_transaction".to_string(),
127 description: "Rollback the current transaction".to_string(),
128 parameters: vec![],
129 example: json!({
130 "type": "rollback_transaction"
131 }),
132 log_template: None,
133 },
134 ActionDefinition {
135 name: "disconnect".to_string(),
136 description: "Disconnect from the MySQL server".to_string(),
137 parameters: vec![],
138 example: json!({
139 "type": "disconnect"
140 }),
141 log_template: None,
142 },
143 ]
144 }
145 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
146 vec![
147 ActionDefinition {
148 name: "execute_query".to_string(),
149 description: "Execute a SQL query in response to received data".to_string(),
150 parameters: vec![Parameter {
151 name: "query".to_string(),
152 type_hint: "string".to_string(),
153 description: "SQL query to execute".to_string(),
154 required: true,
155 }],
156 example: json!({
157 "type": "execute_query",
158 "query": "INSERT INTO logs (message) VALUES ('processed')"
159 }),
160 log_template: None,
161 },
162 ActionDefinition {
163 name: "wait_for_more".to_string(),
164 description: "Wait for more results without executing new queries".to_string(),
165 parameters: vec![],
166 example: json!({
167 "type": "wait_for_more"
168 }),
169 log_template: None,
170 },
171 ]
172 }
173 fn protocol_name(&self) -> &'static str {
174 "MySQL"
175 }
176 fn get_event_types(&self) -> Vec<EventType> {
183 vec![
184 MYSQL_CLIENT_CONNECTED_EVENT.clone(),
185 MYSQL_CLIENT_RESULT_RECEIVED_EVENT.clone(),
186 ]
187 }
188 fn stack_name(&self) -> &'static str {
189 "ETH>IP>TCP>MySQL"
190 }
191 fn keywords(&self) -> Vec<&'static str> {
192 vec![
193 "mysql",
194 "mysql client",
195 "connect to mysql",
196 "sql",
197 "database",
198 ]
199 }
200 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
201 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
202
203 ProtocolMetadataV2::builder()
204 .state(DevelopmentState::Beta)
205 .implementation(
206 "mysql_async, one connection, text protocol. Each query's rows and the server's \
207 affected_rows / last_insert_id go back to the model as mysql_result_received, \
208 and the model's answer is executed in turn, bounded at four follow-ups",
209 )
210 .llm_control("Full control over SQL queries and transactions")
211 .e2e_testing(
212 "tests/client/mysql/real_server_test.rs, 6 LLM calls, against a real mysqld \
213 (--initialize-insecure, then mysqld on a loopback port) read back with the \
214 mysql CLI. The client connects with the database startup parameter; the model \
215 creates a table, inserts a row, selects it, and inserts a second row built \
216 from the SELECT's rows. The INSERT's result is matched on affected_rows 1 and \
217 last_insert_id 1, the SELECT's on the row it carries, and the CLI must read \
218 both rows back exactly. Not #[ignore]d; a missing mysqld or mysql fails the \
219 test rather than skipping it.",
220 )
221 .notes(
222 "No TLS, and only a passwordless account is exercised against a real server. A \
223 query the server rejects marks the client Error and raises no event, so the \
224 model does not learn of the error. Text protocol only; no prepared \
225 statements.",
226 )
227 .build()
228 }
229 fn description(&self) -> &'static str {
230 "MySQL client for database operations"
231 }
232 fn example_prompt(&self) -> &'static str {
233 "Connect to MySQL at localhost:3306 as root and query the users table"
234 }
235 fn group_name(&self) -> &'static str {
236 "Database"
237 }
238
239 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
240 use crate::llm::actions::StartupExamples;
241 use serde_json::json;
242
243 StartupExamples::new(
244 json!({
246 "type": "open_client",
247 "remote_addr": "localhost:3306",
248 "base_stack": "mysql",
249 "instruction": "Query the users table and show all active users, then summarize the results"
250 }),
251 json!({
253 "type": "open_client",
254 "remote_addr": "localhost:3306",
255 "base_stack": "mysql",
256 "event_handlers": [{
257 "event_pattern": "mysql_result_received",
258 "handler": {
259 "type": "script",
260 "language": "python",
261 "code": "<mysql_client_handler>"
262 }
263 }]
264 }),
265 json!({
267 "type": "open_client",
268 "remote_addr": "localhost:3306",
269 "base_stack": "mysql",
270 "event_handlers": [
271 {
272 "event_pattern": "mysql_connected",
273 "handler": {
274 "type": "static",
275 "actions": [{
276 "type": "execute_query",
277 "query": "SELECT * FROM users LIMIT 10"
278 }]
279 }
280 },
281 {
282 "event_pattern": "mysql_result_received",
283 "handler": {
284 "type": "static",
285 "actions": [{
286 "type": "disconnect"
287 }]
288 }
289 }
290 ]
291 }),
292 )
293 }
294
295 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
296 vec![
297 crate::llm::actions::ParameterDefinition {
298 name: "username".to_string(),
299 type_hint: "string".to_string(),
300 description: "MySQL username (default: root)".to_string(),
301 required: false,
302 example: json!("myuser"),
303 default: None,
304 },
305 crate::llm::actions::ParameterDefinition {
306 name: "password".to_string(),
307 type_hint: "string".to_string(),
308 description: "MySQL password (default: empty)".to_string(),
309 required: false,
310 example: json!("mypassword"),
311 default: None,
312 },
313 crate::llm::actions::ParameterDefinition {
314 name: "database".to_string(),
315 type_hint: "string".to_string(),
316 description: "Database name to connect to (default: none)".to_string(),
317 required: false,
318 example: json!("mydb"),
319 default: None,
320 },
321 ]
322 }
323}
324
325impl Client for MysqlClientProtocol {
327 fn connect(
328 &self,
329 ctx: crate::protocol::ConnectContext,
330 ) -> std::pin::Pin<
331 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
332 > {
333 Box::pin(async move {
334 use crate::client::mysql::MysqlClient;
335 MysqlClient::connect_with_llm_actions(
336 ctx.remote_addr,
337 ctx.llm_client,
338 ctx.state,
339 ctx.status_tx,
340 ctx.client_id,
341 ctx.startup_params,
342 )
343 .await
344 })
345 }
346 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
347 let action_type = action
348 .get("type")
349 .and_then(|v| v.as_str())
350 .context("Missing 'type' field in action")?;
351
352 match action_type {
353 "execute_query" => {
354 let query = action
355 .get("query")
356 .and_then(|v| v.as_str())
357 .context("Missing 'query' field")?
358 .to_string();
359
360 Ok(ClientActionResult::Custom {
361 name: "mysql_query".to_string(),
362 data: json!({
363 "query": query,
364 }),
365 })
366 }
367 "begin_transaction" => Ok(ClientActionResult::Custom {
368 name: "mysql_query".to_string(),
369 data: json!({
370 "query": "BEGIN",
371 }),
372 }),
373 "commit_transaction" => Ok(ClientActionResult::Custom {
374 name: "mysql_query".to_string(),
375 data: json!({
376 "query": "COMMIT",
377 }),
378 }),
379 "rollback_transaction" => Ok(ClientActionResult::Custom {
380 name: "mysql_query".to_string(),
381 data: json!({
382 "query": "ROLLBACK",
383 }),
384 }),
385 "disconnect" => Ok(ClientActionResult::Disconnect),
386 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
387 _ => Err(anyhow::anyhow!(
388 "Unknown MySQL client action: {}",
389 action_type
390 )),
391 }
392 }
393}