1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::{ConnectContext, EventType};
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static POSTGRESQL_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "postgresql_connected",
18 "PostgreSQL client successfully connected to server",
19 json!({
20 "type": "execute_query",
21 "query": "SELECT * FROM users WHERE id = 1"
22 }),
23 )
24 .with_parameters(vec![
25 Parameter {
26 name: "remote_addr".to_string(),
27 type_hint: "string".to_string(),
28 description: "PostgreSQL server address".to_string(),
29 required: true,
30 },
31 Parameter {
32 name: "database".to_string(),
33 type_hint: "string".to_string(),
34 description: "Database name".to_string(),
35 required: true,
36 },
37 Parameter {
38 name: "user".to_string(),
39 type_hint: "string".to_string(),
40 description: "Username".to_string(),
41 required: true,
42 },
43 ])
44});
45
46pub static POSTGRESQL_CLIENT_QUERY_RESULT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
48 EventType::new(
49 "postgresql_query_result",
50 "Query result received from PostgreSQL server",
51 json!({
52 "type": "execute_query",
53 "query": "INSERT INTO logs VALUES ('result processed')"
54 }),
55 )
56 .with_parameters(vec![
57 Parameter {
58 name: "query".to_string(),
59 type_hint: "string".to_string(),
60 description: "The SQL query executed".to_string(),
61 required: true,
62 },
63 Parameter {
64 name: "rows".to_string(),
65 type_hint: "array".to_string(),
66 description: "Array of row objects".to_string(),
67 required: true,
68 },
69 Parameter {
70 name: "row_count".to_string(),
71 type_hint: "number".to_string(),
72 description: "Number of rows returned".to_string(),
73 required: true,
74 },
75 ])
76});
77
78#[derive(Default)]
80pub struct PostgresqlClientProtocol;
81
82impl PostgresqlClientProtocol {
83 pub fn new() -> Self {
84 Self
85 }
86}
87
88impl Protocol for PostgresqlClientProtocol {
90 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
91 vec![
92 ActionDefinition {
93 name: "execute_query".to_string(),
94 description: "Execute a SQL query (SELECT, INSERT, UPDATE, DELETE, etc.)"
95 .to_string(),
96 parameters: vec![Parameter {
97 name: "query".to_string(),
98 type_hint: "string".to_string(),
99 description: "SQL query to execute".to_string(),
100 required: true,
101 }],
102 example: json!({
103 "type": "execute_query",
104 "query": "SELECT * FROM users WHERE id = 1"
105 }),
106 log_template: None,
107 },
108 ActionDefinition {
109 name: "begin_transaction".to_string(),
110 description: "Begin a new transaction".to_string(),
111 parameters: vec![],
112 example: json!({
113 "type": "begin_transaction"
114 }),
115 log_template: None,
116 },
117 ActionDefinition {
118 name: "commit_transaction".to_string(),
119 description: "Commit the current transaction".to_string(),
120 parameters: vec![],
121 example: json!({
122 "type": "commit_transaction"
123 }),
124 log_template: None,
125 },
126 ActionDefinition {
127 name: "rollback_transaction".to_string(),
128 description: "Roll back the current transaction".to_string(),
129 parameters: vec![],
130 example: json!({
131 "type": "rollback_transaction"
132 }),
133 log_template: None,
134 },
135 ActionDefinition {
136 name: "disconnect".to_string(),
137 description: "Disconnect from the PostgreSQL server".to_string(),
138 parameters: vec![],
139 example: json!({
140 "type": "disconnect"
141 }),
142 log_template: None,
143 },
144 ]
145 }
146 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
147 vec![ActionDefinition {
148 name: "execute_query".to_string(),
149 description: "Execute a SQL query in response to query results".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 VALUES ('result processed')"
159 }),
160 log_template: None,
161 }]
162 }
163 fn protocol_name(&self) -> &'static str {
164 "PostgreSQL"
165 }
166 fn get_event_types(&self) -> Vec<EventType> {
173 vec![
174 POSTGRESQL_CLIENT_CONNECTED_EVENT.clone(),
175 POSTGRESQL_CLIENT_QUERY_RESULT_EVENT.clone(),
176 ]
177 }
178 fn stack_name(&self) -> &'static str {
179 "ETH>IP>TCP>PostgreSQL"
180 }
181 fn keywords(&self) -> Vec<&'static str> {
182 vec![
183 "postgresql",
184 "postgres",
185 "postgresql client",
186 "postgres client",
187 "connect to postgresql",
188 "connect to postgres",
189 ]
190 }
191 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
192 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
193
194 ProtocolMetadataV2::builder()
195 .state(DevelopmentState::Beta)
196 .implementation(
197 "tokio-postgres (extended query protocol, NoTls). Each query's rows go back to \
198 the model as postgresql_query_result, and the model's answer is executed in \
199 turn, bounded at four follow-ups",
200 )
201 .llm_control("Full control over SQL queries and transactions")
202 .e2e_testing(
203 "tests/client/postgresql/real_server_test.rs, 6 LLM calls, against a real \
204 PostgreSQL server (initdb + postgres on a loopback port) read back with psql \
205 (libpq). The model creates a table, inserts a row, selects it, and inserts a \
206 second row built from the SELECT's rows; each result event is matched on its \
207 query and the SELECT's on the row it carries, and psql must read both rows \
208 back exactly. Not #[ignore]d; a missing initdb, postgres or psql fails the \
209 test rather than skipping it. e2e_test.rs pins the follow-up depth bound \
210 against NetGet's own server.",
211 )
212 .notes(
213 "No TLS (NoTls), and only trust authentication is exercised against a real \
214 server; password and SCRAM are untested. A query the server rejects is logged and raises no \
215 event, so the model does not learn of the error. Cells of types other than \
216 bool, integers, floats and text-like types reach the model as null.",
217 )
218 .build()
219 }
220 fn description(&self) -> &'static str {
221 "PostgreSQL client for relational database operations"
222 }
223 fn example_prompt(&self) -> &'static str {
224 "Connect to PostgreSQL at localhost:5432 and select all users from the users table"
225 }
226 fn group_name(&self) -> &'static str {
227 "Database"
228 }
229
230 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
231 use crate::llm::actions::StartupExamples;
232 use serde_json::json;
233
234 StartupExamples::new(
235 json!({
237 "type": "open_client",
238 "remote_addr": "localhost:5432",
239 "base_stack": "postgresql",
240 "instruction": "Query all users from the users table and analyze the data structure"
241 }),
242 json!({
244 "type": "open_client",
245 "remote_addr": "localhost:5432",
246 "base_stack": "postgresql",
247 "event_handlers": [{
248 "event_pattern": "postgresql_query_result",
249 "handler": {
250 "type": "script",
251 "language": "python",
252 "code": "<postgresql_client_handler>"
253 }
254 }]
255 }),
256 json!({
258 "type": "open_client",
259 "remote_addr": "localhost:5432",
260 "base_stack": "postgresql",
261 "event_handlers": [
262 {
263 "event_pattern": "postgresql_connected",
264 "handler": {
265 "type": "static",
266 "actions": [{
267 "type": "execute_query",
268 "query": "SELECT * FROM users WHERE active = true"
269 }]
270 }
271 },
272 {
273 "event_pattern": "postgresql_query_result",
274 "handler": {
275 "type": "static",
276 "actions": [{
277 "type": "disconnect"
278 }]
279 }
280 }
281 ]
282 }),
283 )
284 }
285
286 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
287 vec![
288 ParameterDefinition {
289 name: "database".to_string(),
290 type_hint: "string".to_string(),
291 description: "Database name (default: postgres)".to_string(),
292 required: false,
293 example: json!("mydb"),
294 default: None,
295 },
296 ParameterDefinition {
297 name: "user".to_string(),
298 type_hint: "string".to_string(),
299 description: "Username (default: postgres)".to_string(),
300 required: false,
301 example: json!("admin"),
302 default: None,
303 },
304 ParameterDefinition {
305 name: "password".to_string(),
306 type_hint: "string".to_string(),
307 description: "Password (default: empty)".to_string(),
308 required: false,
309 example: json!("secret123"),
310 default: None,
311 },
312 ]
313 }
314}
315
316impl Client for PostgresqlClientProtocol {
318 fn connect(
319 &self,
320 ctx: ConnectContext,
321 ) -> std::pin::Pin<
322 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
323 > {
324 Box::pin(async move {
325 use crate::client::postgresql::PostgresqlClient;
326 PostgresqlClient::connect_with_llm_actions(
327 ctx.remote_addr,
328 ctx.llm_client,
329 ctx.state,
330 ctx.status_tx,
331 ctx.client_id,
332 ctx.startup_params,
333 )
334 .await
335 })
336 }
337 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
338 let action_type = action
339 .get("type")
340 .and_then(|v| v.as_str())
341 .context("Missing 'type' field in action")?;
342
343 match action_type {
344 "execute_query" => {
345 let query = action
346 .get("query")
347 .and_then(|v| v.as_str())
348 .context("Missing 'query' field")?
349 .to_string();
350
351 Ok(ClientActionResult::Custom {
352 name: "pg_query".to_string(),
353 data: json!({
354 "query": query,
355 }),
356 })
357 }
358 "begin_transaction" => Ok(ClientActionResult::Custom {
359 name: "pg_query".to_string(),
360 data: json!({
361 "query": "BEGIN",
362 }),
363 }),
364 "commit_transaction" => Ok(ClientActionResult::Custom {
365 name: "pg_query".to_string(),
366 data: json!({
367 "query": "COMMIT",
368 }),
369 }),
370 "rollback_transaction" => Ok(ClientActionResult::Custom {
371 name: "pg_query".to_string(),
372 data: json!({
373 "query": "ROLLBACK",
374 }),
375 }),
376 "disconnect" => Ok(ClientActionResult::Disconnect),
377 _ => Err(anyhow::anyhow!(
378 "Unknown PostgreSQL client action: {}",
379 action_type
380 )),
381 }
382 }
383}