1pub mod actions;
3
4pub use actions::PostgresqlClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10use tracing::{error, info, trace, warn};
11
12use crate::client::llm_budget::call_llm_for_client;
13use crate::client::postgresql::actions::{
14 POSTGRESQL_CLIENT_CONNECTED_EVENT, POSTGRESQL_CLIENT_QUERY_RESULT_EVENT,
15};
16use crate::llm::actions::client_trait::{Client, ClientActionResult};
17use crate::llm::ollama_client::OllamaClient;
18use crate::llm::ClientLlmResult;
19use crate::protocol::Event;
20use crate::state::app_state::AppState;
21use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
22use crate::state::{ClientId, ClientStatus};
23
24enum Applied {
26 Query {
28 query: String,
29 rows: Vec<serde_json::Value>,
30 },
31 Disconnect,
33 Nothing(&'static str),
35}
36
37const MAX_FOLLOWUP_DEPTH: usize = 4;
44
45pub struct PostgresqlClient;
47
48impl PostgresqlClient {
49 pub async fn connect_with_llm_actions(
51 remote_addr: String,
52 llm_client: OllamaClient,
53 app_state: Arc<AppState>,
54 status_tx: mpsc::UnboundedSender<String>,
55 client_id: ClientId,
56 startup_params: Option<crate::protocol::StartupParams>,
57 ) -> Result<SocketAddr> {
58 let (database, user, password) = if let Some(params) = &startup_params {
60 let database = params
61 .get_optional_string("database")?
62 .unwrap_or_else(|| "postgres".to_string());
63 let user = params
64 .get_optional_string("user")?
65 .unwrap_or_else(|| "postgres".to_string());
66 let password = params
67 .get_optional_string("password")?
68 .unwrap_or_else(|| "".to_string());
69 (database, user, password)
70 } else {
71 (
72 "postgres".to_string(),
73 "postgres".to_string(),
74 "".to_string(),
75 )
76 };
77
78 let (host, port) = match remote_addr.rsplit_once(':') {
82 Some((h, p)) => match p.parse::<u16>() {
83 Ok(p) => (h.to_string(), p),
84 Err(_) => (remote_addr.clone(), 5432),
85 },
86 None => (remote_addr.clone(), 5432),
87 };
88 let conn_str = if password.is_empty() {
89 format!(
90 "host={} port={} user={} dbname={}",
91 host, port, user, database
92 )
93 } else {
94 format!(
95 "host={} port={} user={} password={} dbname={}",
96 host, port, user, password, database
97 )
98 };
99
100 info!(
101 "PostgreSQL client {} connecting to {} (user={}, database={})",
102 client_id, remote_addr, user, database
103 );
104
105 let (client, connection) = tokio_postgres::connect(&conn_str, tokio_postgres::NoTls)
107 .await
108 .context(format!(
109 "Failed to connect to PostgreSQL at {}",
110 remote_addr
111 ))?;
112
113 let local_addr: SocketAddr = format!("{}:0", host)
116 .parse()
117 .unwrap_or_else(|_| "127.0.0.1:0".parse().unwrap());
118
119 info!(
120 "PostgreSQL client {} connected to {}",
121 client_id, remote_addr
122 );
123
124 app_state
126 .update_client_status(client_id, ClientStatus::Connected)
127 .await;
128 let _ = status_tx.send(format!(
129 "[CLIENT] PostgreSQL client {} connected",
130 client_id
131 ));
132 let _ = status_tx.send("__UPDATE_UI__".to_string());
133
134 let status_tx_clone = status_tx.clone();
136 let connection_handle = tokio::spawn(async move {
137 if let Err(e) = connection.await {
138 error!("PostgreSQL client {} connection error: {}", client_id, e);
139 let _ = status_tx_clone.send(format!(
140 "[CLIENT] PostgreSQL client {} connection error: {}",
141 client_id, e
142 ));
143 }
144 });
145 app_state
146 .register_client_task(client_id, connection_handle)
147 .await;
148
149 let client_arc = Arc::new(client);
153 let protocol = Arc::new(PostgresqlClientProtocol::new());
154
155 let command_rx =
159 crate::client::command_support::register_command_channel(&app_state, client_id).await;
160 let cmd_task = tokio::spawn(Self::command_loop(
161 command_rx,
162 protocol.clone(),
163 client_arc.clone(),
164 client_id,
165 llm_client.clone(),
166 app_state.clone(),
167 status_tx.clone(),
168 ));
169 app_state.register_client_task(client_id, cmd_task).await;
170
171 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
173 let event = Event::new(
174 &POSTGRESQL_CLIENT_CONNECTED_EVENT,
175 serde_json::json!({
176 "remote_addr": remote_addr,
177 "database": database,
178 "user": user,
179 }),
180 );
181
182 let memory = app_state
183 .get_memory_for_client(client_id)
184 .await
185 .unwrap_or_default();
186
187 let protocol_clone = protocol.clone();
188 let client_arc_clone = client_arc.clone();
189 let llm_client_clone = llm_client.clone();
190 let app_state_clone = app_state.clone();
191 let status_tx_clone = status_tx.clone();
192
193 let task_registrar = app_state.clone();
194 let handle = tokio::spawn(async move {
195 match call_llm_for_client(
196 &llm_client_clone,
197 &app_state_clone,
198 client_id.to_string(),
199 &instruction,
200 &memory,
201 Some(&event),
202 protocol_clone.as_ref(),
203 &status_tx_clone,
204 )
205 .await
206 {
207 Ok(ClientLlmResult {
208 actions,
209 memory_updates,
210 }) => {
211 if let Some(mem) = memory_updates {
213 app_state_clone.set_memory_for_client(client_id, mem).await;
214 }
215
216 for action in actions {
218 if let Err(e) = Self::execute_llm_action(
219 client_id,
220 action,
221 &protocol_clone,
222 &client_arc_clone,
223 &app_state_clone,
224 &llm_client_clone,
225 &status_tx_clone,
226 0,
227 )
228 .await
229 {
230 error!("Error executing PostgreSQL action: {}", e);
231 }
232 }
233 }
234 Err(e) => {
235 error!("LLM error for PostgreSQL client {}: {}", client_id, e);
236 }
237 }
238 });
239 task_registrar.register_client_task(client_id, handle).await;
240 }
241
242 Ok(local_addr)
243 }
244
245 #[allow(clippy::too_many_arguments)]
252 async fn command_loop(
253 mut command_rx: mpsc::Receiver<ClientCommand>,
254 protocol: Arc<PostgresqlClientProtocol>,
255 pg_client: Arc<tokio_postgres::Client>,
256 client_id: ClientId,
257 llm_client: OllamaClient,
258 app_state: Arc<AppState>,
259 status_tx: mpsc::UnboundedSender<String>,
260 ) {
261 use crate::llm::actions::protocol_trait::Protocol;
262 use crate::state::AccessLogOwner;
263
264 while let Some(command) = command_rx.recv().await {
265 let action = command.action.clone();
266
267 let mut follow_up: Option<(String, Vec<serde_json::Value>)> = None;
268 let outcome = match protocol.execute_action(action.clone()) {
269 Err(e) => Ok(ClientSendOutcome::Rejected {
270 error: e.to_string(),
271 }),
272 Ok(result) => match Self::apply_action(result, &pg_client, client_id).await {
273 Err(e) => Err(e),
274 Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
275 Ok(Applied::Nothing(what)) => Ok(ClientSendOutcome::Executed {
276 detail: what.to_string(),
277 }),
278 Ok(Applied::Query { query, rows }) => {
279 let detail = format!(
280 "query executed by tokio_postgres ({} rows); \
281 no byte count — the driver owns the socket: {}",
282 rows.len(),
283 crate::utils::truncate::truncate_for_log(&query, 80)
284 );
285 follow_up = Some((query, rows));
286 Ok(ClientSendOutcome::Executed { detail })
287 }
288 },
289 };
290
291 let outcome_json = match &outcome {
292 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
293 Err(e) => serde_json::json!({"error": e.to_string()}),
294 };
295 app_state
296 .record_access_log(
297 AccessLogOwner::Client(client_id.as_u32()),
298 protocol.protocol_name(),
299 None,
300 "injected_action",
301 action,
302 vec![outcome_json],
303 )
304 .await;
305
306 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
307 if let Err(e) = &outcome {
308 error!(
309 "PostgreSQL client {} injected action failed: {}",
310 client_id, e
311 );
312 let _ = status_tx.send(format!(
313 "[WARN] Client {} injected action failed: {}",
314 client_id, e
315 ));
316 }
317 let _ = status_tx.send("__UPDATE_UI__".to_string());
318 crate::client::command_support::reply(command, outcome);
319
320 if disconnect {
321 Self::mark_disconnected(client_id, &app_state, &status_tx).await;
322 break;
323 }
324
325 if let Some((query, rows)) = follow_up {
328 Self::report_query_result(
329 client_id,
330 &query,
331 rows,
332 &pg_client,
333 &protocol,
334 &app_state,
335 &llm_client,
336 &status_tx,
337 0,
338 )
339 .await;
340 }
341 }
342
343 app_state.remove_client_handle(client_id).await;
344 let _ = status_tx.send("__UPDATE_UI__".to_string());
345 }
346
347 #[allow(clippy::too_many_arguments)]
355 fn execute_llm_action<'a>(
356 client_id: ClientId,
357 action: serde_json::Value,
358 protocol: &'a Arc<PostgresqlClientProtocol>,
359 pg_client: &'a Arc<tokio_postgres::Client>,
360 app_state: &'a Arc<AppState>,
361 llm_client: &'a OllamaClient,
362 status_tx: &'a mpsc::UnboundedSender<String>,
363 depth: usize,
364 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
365 Box::pin(async move {
366 match Self::apply_action(protocol.execute_action(action)?, pg_client, client_id).await {
367 Ok(Applied::Query { query, rows }) => {
368 Self::report_query_result(
369 client_id, &query, rows, pg_client, protocol, app_state, llm_client,
370 status_tx, depth,
371 )
372 .await;
373 }
374 Ok(Applied::Disconnect) => {
375 info!("PostgreSQL client {} disconnecting", client_id);
376 Self::mark_disconnected(client_id, app_state, status_tx).await;
377 }
378 Ok(Applied::Nothing(what)) => {
379 trace!(
380 "PostgreSQL client {} action had no effect: {}",
381 client_id,
382 what
383 );
384 }
385 Err(e) => {
386 error!("PostgreSQL client {} query error: {}", client_id, e);
387 let _ = status_tx.send(format!(
388 "[CLIENT] PostgreSQL client {} query error: {}",
389 client_id, e
390 ));
391 }
392 }
393 Ok(())
394 })
395 }
396
397 async fn apply_action(
400 result: ClientActionResult,
401 pg_client: &Arc<tokio_postgres::Client>,
402 client_id: ClientId,
403 ) -> Result<Applied> {
404 match result {
405 ClientActionResult::Custom { name, data } if name == "pg_query" => {
406 let query = data
407 .get("query")
408 .and_then(|v| v.as_str())
409 .context("Missing 'query' in pg_query action data")?
410 .to_string();
411
412 trace!("PostgreSQL client {} executing: {}", client_id, query);
413
414 let rows = pg_client
418 .query(query.as_str(), &[])
419 .await
420 .context("Failed to execute query")?;
421
422 let result = rows
423 .iter()
424 .map(|row| {
425 let mut obj = serde_json::Map::new();
426 for (idx, col) in row.columns().iter().enumerate() {
427 obj.insert(col.name().to_string(), pg_cell_to_json(row, idx));
428 }
429 serde_json::Value::Object(obj)
430 })
431 .collect::<Vec<_>>();
432
433 info!(
434 "PostgreSQL client {} query returned {} rows",
435 client_id,
436 result.len()
437 );
438
439 Ok(Applied::Query {
440 query,
441 rows: result,
442 })
443 }
444 ClientActionResult::Custom { name, .. } => {
445 trace!(
446 "PostgreSQL client {} ignoring custom result '{}'",
447 client_id,
448 name
449 );
450 Ok(Applied::Nothing("custom result not handled by this client"))
451 }
452 ClientActionResult::Disconnect => Ok(Applied::Disconnect),
453 ClientActionResult::WaitForMore => Ok(Applied::Nothing("wait_for_more")),
454 ClientActionResult::NoAction => Ok(Applied::Nothing("no_action")),
455 ClientActionResult::SendData(_) => Ok(Applied::Nothing(
456 "send_data is not meaningful for a tokio_postgres connection",
457 )),
458 ClientActionResult::Multiple(_) => Ok(Applied::Nothing(
459 "multiple results not handled by this client",
460 )),
461 }
462 }
463
464 #[allow(clippy::too_many_arguments)]
472 async fn report_query_result(
473 client_id: ClientId,
474 query: &str,
475 rows: Vec<serde_json::Value>,
476 pg_client: &Arc<tokio_postgres::Client>,
477 protocol: &Arc<PostgresqlClientProtocol>,
478 app_state: &Arc<AppState>,
479 llm_client: &OllamaClient,
480 status_tx: &mpsc::UnboundedSender<String>,
481 depth: usize,
482 ) {
483 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
484 return;
485 };
486
487 let event = Event::new(
488 &POSTGRESQL_CLIENT_QUERY_RESULT_EVENT,
489 serde_json::json!({
490 "query": query,
491 "rows": rows,
492 "row_count": rows.len(),
493 }),
494 );
495
496 let memory = app_state
497 .get_memory_for_client(client_id)
498 .await
499 .unwrap_or_default();
500
501 let (actions, memory_updates) = match call_llm_for_client(
504 llm_client,
505 app_state,
506 client_id.to_string(),
507 &instruction,
508 &memory,
509 Some(&event),
510 protocol.as_ref(),
511 status_tx,
512 )
513 .await
514 {
515 Ok(ClientLlmResult {
516 actions,
517 memory_updates,
518 }) => (actions, memory_updates),
519 Err(e) => {
520 error!(
521 "LLM error for PostgreSQL client {} on a query result: {}",
522 client_id, e
523 );
524 return;
525 }
526 };
527 if let Some(mem) = memory_updates {
528 app_state.set_memory_for_client(client_id, mem).await;
529 }
530 if actions.is_empty() {
531 return;
532 }
533 if depth >= MAX_FOLLOWUP_DEPTH {
534 warn!(
535 "PostgreSQL client {} reached the follow-up depth bound ({}); dropping {} \
536 action(s) rather than looping",
537 client_id,
538 MAX_FOLLOWUP_DEPTH,
539 actions.len()
540 );
541 return;
542 }
543 for action in actions {
544 if let Err(e) = Self::execute_llm_action(
545 client_id,
546 action,
547 protocol,
548 pg_client,
549 app_state,
550 llm_client,
551 status_tx,
552 depth + 1,
553 )
554 .await
555 {
556 error!(
557 "PostgreSQL client {} rejected its own follow-up action: {}",
558 client_id, e
559 );
560 }
561 }
562 }
563
564 async fn mark_disconnected(
574 client_id: ClientId,
575 app_state: &Arc<AppState>,
576 status_tx: &mpsc::UnboundedSender<String>,
577 ) {
578 app_state.remove_client_handle(client_id).await;
579 app_state
580 .update_client_status(client_id, ClientStatus::Disconnected)
581 .await;
582 let _ = status_tx.send(format!(
583 "[CLIENT] PostgreSQL client {} disconnected",
584 client_id
585 ));
586 let _ = status_tx.send("__UPDATE_UI__".to_string());
587 }
588}
589
590fn pg_cell_to_json(row: &tokio_postgres::Row, idx: usize) -> serde_json::Value {
605 use tokio_postgres::types::Type;
606
607 macro_rules! try_as {
608 ($t:ty) => {
609 if let Ok(v) = row.try_get::<_, Option<$t>>(idx) {
610 return serde_json::json!(v);
611 }
612 };
613 }
614
615 match *row.columns()[idx].type_() {
616 Type::BOOL => try_as!(bool),
617 Type::INT2 => try_as!(i16),
618 Type::INT4 => try_as!(i32),
619 Type::INT8 => try_as!(i64),
620 Type::FLOAT4 => try_as!(f32),
621 Type::FLOAT8 => try_as!(f64),
622 _ => {}
623 }
624 try_as!(String);
626 serde_json::Value::Null
627}