1pub mod actions;
3
4pub use actions::DynamoDbClientProtocol;
5
6use anyhow::{Context, Result};
7use aws_smithy_types::Blob;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use tracing::{error, info};
12
13use crate::client::dynamodb::actions::DYNAMODB_CLIENT_RESPONSE_RECEIVED_EVENT;
14use crate::client::llm_budget::call_llm_for_client;
15use crate::llm::actions::client_trait::{Client, ClientActionResult};
16use crate::llm::actions::protocol_trait::Protocol;
17use crate::llm::ollama_client::OllamaClient;
18use crate::llm::ClientLlmResult;
19use crate::protocol::{Event, StartupParams};
20use crate::state::app_state::AppState;
21use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
22use crate::state::{AccessLogOwner, ClientId, ClientStatus};
23use crate::utils::truncate::truncate_for_log;
24
25fn endpoint_url_from_remote_addr(remote_addr: &str) -> Option<String> {
30 let trimmed = remote_addr.trim();
31 if trimmed.is_empty() {
32 return None;
33 }
34 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
35 Some(trimmed.to_string())
36 } else {
37 Some(format!("http://{trimmed}"))
38 }
39}
40
41pub struct DynamoDbClient;
43
44impl DynamoDbClient {
45 pub async fn connect_with_llm_actions(
47 remote_addr: String,
48 _llm_client: OllamaClient,
49 app_state: Arc<AppState>,
50 status_tx: mpsc::UnboundedSender<String>,
51 client_id: ClientId,
52 startup_params: Option<StartupParams>,
53 ) -> Result<SocketAddr> {
54 let region = startup_params
60 .as_ref()
61 .map(|p| p.get_optional_string("region"))
62 .transpose()?
63 .flatten()
64 .unwrap_or_else(|| "us-east-1".to_string());
65 crate::client::aws_support::validate_region("DynamoDB", ®ion)?;
67
68 let endpoint_url = startup_params
69 .as_ref()
70 .map(|p| p.get_optional_string("endpoint_url"))
71 .transpose()?
72 .flatten();
73
74 let access_key_id = startup_params
75 .as_ref()
76 .map(|p| p.get_optional_string("access_key_id"))
77 .transpose()?
78 .flatten();
79
80 let secret_access_key = startup_params
81 .as_ref()
82 .map(|p| p.get_optional_string("secret_access_key"))
83 .transpose()?
84 .flatten();
85
86 let endpoint_url = endpoint_url.or_else(|| endpoint_url_from_remote_addr(&remote_addr));
95
96 crate::client::aws_support::refuse_ambient_credentials(
99 "DynamoDB",
100 endpoint_url.as_deref(),
101 access_key_id.is_some() && secret_access_key.is_some(),
102 )?;
103
104 info!(
105 "DynamoDB client {} initializing for region {}",
106 client_id, region
107 );
108
109 app_state
111 .with_client_mut(client_id, |client| {
112 client.set_protocol_field("region".to_string(), serde_json::json!(region.clone()));
113 if let Some(endpoint) = &endpoint_url {
114 client.set_protocol_field(
115 "endpoint_url".to_string(),
116 serde_json::json!(endpoint),
117 );
118 }
119 if let Some(key_id) = &access_key_id {
120 client
121 .set_protocol_field("access_key_id".to_string(), serde_json::json!(key_id));
122 }
123 if let Some(secret_key) = &secret_access_key {
124 client.set_protocol_field(
125 "secret_access_key".to_string(),
126 serde_json::json!(secret_key),
127 );
128 }
129 })
130 .await;
131
132 app_state
134 .update_client_status(client_id, ClientStatus::Connected)
135 .await;
136 let _ = status_tx.send(format!(
137 "[CLIENT] DynamoDB client {} ready for region {}{}",
138 client_id,
139 region,
140 endpoint_url
141 .as_ref()
142 .map(|e| format!(" (endpoint: {})", e))
143 .unwrap_or_default()
144 ));
145 let _ = status_tx.send("__UPDATE_UI__".to_string());
146
147 let command_rx =
156 crate::client::command_support::register_command_channel(&app_state, client_id).await;
157 let task_registrar = app_state.clone();
158 let conn_llm = _llm_client.clone();
159 let task_handle = tokio::spawn(Self::command_loop(
160 command_rx,
161 client_id,
162 app_state.clone(),
163 _llm_client.clone(),
164 status_tx.clone(),
165 ));
166 task_registrar
167 .register_client_task(client_id, task_handle)
168 .await;
169
170 let conn_state = app_state.clone();
181 let conn_status = status_tx.clone();
182 let conn_region = region.clone();
183 let conn_endpoint = endpoint_url.clone();
184 let conn_task = tokio::spawn(async move {
185 let Some(instruction) = conn_state.get_instruction_for_client(client_id).await else {
186 return;
187 };
188 let protocol = crate::client::dynamodb::actions::DynamoDbClientProtocol::new();
189 let event = Event::new(
193 &crate::client::dynamodb::actions::DYNAMODB_CLIENT_CONNECTED_EVENT,
194 serde_json::json!({
195 "region": conn_region,
196 "endpoint": conn_endpoint,
197 }),
198 );
199 match crate::client::llm_budget::call_llm_for_client(
200 &conn_llm,
201 &conn_state,
202 client_id.to_string(),
203 &instruction,
204 "",
205 Some(&event),
206 &protocol,
207 &conn_status,
208 )
209 .await
210 {
211 Ok(result) => {
212 if let Some(mem) = result.memory_updates {
213 conn_state.set_memory_for_client(client_id, mem).await;
214 }
215 use crate::llm::actions::client_trait::{Client, ClientActionResult};
216 for action in result.actions {
217 match protocol.execute_action(action.clone()) {
223 Ok(ClientActionResult::Custom { name, data }) => {
224 if let Err(e) =
225 Self::run_operation_once(client_id, &name, &data, &conn_state)
226 .await
227 {
228 error!(
229 "DynamoDB client {} connect-time {} failed: {}",
230 client_id, name, e
231 );
232 }
233 }
234 Ok(other) => info!(
235 "DynamoDB client {} connect-time action {:?} needs no DynamoDB \
236 operation",
237 client_id, other
238 ),
239 Err(e) => error!(
240 "DynamoDB client {} could not execute connect-time action {}: {}",
241 client_id, action, e
242 ),
243 }
244 }
245 }
246 Err(e) => error!(
247 "DynamoDB client {} LLM error on connected event: {}",
248 client_id, e
249 ),
250 }
251 });
252 task_registrar
253 .register_client_task(client_id, conn_task)
254 .await;
255
256 Ok("0.0.0.0:0".parse().unwrap())
258 }
259
260 pub async fn execute_operation(
267 client_id: ClientId,
268 operation: String,
269 data: serde_json::Value,
270 app_state: Arc<AppState>,
271 llm_client: OllamaClient,
272 status_tx: mpsc::UnboundedSender<String>,
273 ) -> Result<serde_json::Value> {
274 Self::execute_operation_inner(
275 client_id, operation, data, app_state, llm_client, status_tx, true,
276 )
277 .await
278 }
279
280 async fn execute_operation_inner(
289 client_id: ClientId,
290 operation: String,
291 data: serde_json::Value,
292 app_state: Arc<AppState>,
293 llm_client: OllamaClient,
294 status_tx: mpsc::UnboundedSender<String>,
295 notify: bool,
296 ) -> Result<serde_json::Value> {
297 info!(
298 "DynamoDB client {} executing operation: {}",
299 client_id, operation
300 );
301
302 let (region, endpoint_url, access_key_id, secret_access_key) =
303 Self::get_config(&app_state, client_id).await?;
304
305 let config = Self::build_aws_config(
306 ®ion,
307 endpoint_url.as_deref(),
308 access_key_id.as_deref(),
309 secret_access_key.as_deref(),
310 )
311 .await?;
312
313 let ddb = aws_sdk_dynamodb::Client::new(&config);
314
315 let result = match operation.as_str() {
316 "put_item" => Self::put_item(&ddb, &data).await,
317 "get_item" => Self::get_item(&ddb, &data).await,
318 "query" => Self::query(&ddb, &data).await,
319 "scan" => Self::scan(&ddb, &data).await,
320 "update_item" => Self::update_item(&ddb, &data).await,
321 "delete_item" => Self::delete_item(&ddb, &data).await,
322 other => Err(anyhow::anyhow!("Unknown DynamoDB operation: {}", other)),
323 };
324
325 let (success, data, error_text) = match &result {
326 Ok(value) => {
327 info!("DynamoDB client {} {} succeeded", client_id, operation);
328 (true, Some(value.clone()), None)
329 }
330 Err(e) => {
331 error!("DynamoDB client {} {} failed: {}", client_id, operation, e);
332 (false, None, Some(e.to_string()))
333 }
334 };
335
336 if !notify {
342 return result;
343 }
344
345 let notify_state = app_state.clone();
346 let notify = tokio::spawn(async move {
347 if let Err(e) = Self::call_llm_with_response(
348 client_id,
349 &operation,
350 success,
351 data,
352 error_text,
353 &app_state,
354 &llm_client,
355 &status_tx,
356 )
357 .await
358 {
359 error!(
360 "DynamoDB client {} response notification failed: {}",
361 client_id, e
362 );
363 }
364 });
365 notify_state.register_client_task(client_id, notify).await;
366
367 result
368 }
369
370 async fn run_operation_once(
378 client_id: ClientId,
379 operation: &str,
380 data: &serde_json::Value,
381 app_state: &Arc<AppState>,
382 ) -> Result<serde_json::Value> {
383 let (region, endpoint_url, access_key_id, secret_access_key) =
384 Self::get_config(app_state, client_id).await?;
385
386 let config = Self::build_aws_config(
387 ®ion,
388 endpoint_url.as_deref(),
389 access_key_id.as_deref(),
390 secret_access_key.as_deref(),
391 )
392 .await?;
393
394 let ddb = aws_sdk_dynamodb::Client::new(&config);
395
396 match operation {
397 "put_item" => Self::put_item(&ddb, data).await,
398 "get_item" => Self::get_item(&ddb, data).await,
399 "query" => Self::query(&ddb, data).await,
400 "scan" => Self::scan(&ddb, data).await,
401 "update_item" => Self::update_item(&ddb, data).await,
402 "delete_item" => Self::delete_item(&ddb, data).await,
403 other => Err(anyhow::anyhow!("Unknown DynamoDB operation: {}", other)),
404 }
405 }
406
407 fn attribute_values_from(
409 data: &serde_json::Value,
410 field: &str,
411 ) -> Result<Option<std::collections::HashMap<String, aws_sdk_dynamodb::types::AttributeValue>>>
412 {
413 let Some(map) = data.get(field).and_then(|v| v.as_object()) else {
414 return Ok(None);
415 };
416 let mut out = std::collections::HashMap::new();
417 for (key, value) in map {
418 out.insert(key.clone(), Self::json_to_attribute_value(key, value)?);
419 }
420 Ok(Some(out))
421 }
422
423 fn attribute_map_from(
425 data: &serde_json::Value,
426 field: &str,
427 ) -> Result<std::collections::HashMap<String, aws_sdk_dynamodb::types::AttributeValue>> {
428 let map = data
429 .get(field)
430 .and_then(|v| v.as_object())
431 .with_context(|| format!("Missing '{}' in DynamoDB action data", field))?;
432 let mut out = std::collections::HashMap::new();
433 for (key, value) in map {
434 out.insert(key.clone(), Self::json_to_attribute_value(key, value)?);
435 }
436 Ok(out)
437 }
438
439 fn table_name_from(data: &serde_json::Value) -> Result<String> {
440 Ok(data
441 .get("table_name")
442 .and_then(|v| v.as_str())
443 .context("Missing 'table_name' in DynamoDB action data")?
444 .to_string())
445 }
446
447 async fn put_item(
449 ddb: &aws_sdk_dynamodb::Client,
450 data: &serde_json::Value,
451 ) -> Result<serde_json::Value> {
452 let table_name = Self::table_name_from(data)?;
453 let item = Self::attribute_map_from(data, "item")?;
454
455 ddb.put_item()
456 .table_name(&table_name)
457 .set_item(Some(item))
458 .send()
459 .await
460 .context("PutItem failed")?;
461
462 Ok(serde_json::json!({ "table_name": table_name, "written": true }))
463 }
464
465 async fn get_item(
467 ddb: &aws_sdk_dynamodb::Client,
468 data: &serde_json::Value,
469 ) -> Result<serde_json::Value> {
470 let table_name = Self::table_name_from(data)?;
471 let key = Self::attribute_map_from(data, "key")?;
472
473 let output = ddb
474 .get_item()
475 .table_name(&table_name)
476 .set_key(Some(key))
477 .send()
478 .await
479 .context("GetItem failed")?;
480
481 let item_json = match output.item {
482 Some(item) => Self::attribute_map_to_json(&item),
483 None => serde_json::json!(null),
484 };
485
486 Ok(serde_json::json!({ "table_name": table_name, "item": item_json }))
487 }
488
489 async fn query(
491 ddb: &aws_sdk_dynamodb::Client,
492 data: &serde_json::Value,
493 ) -> Result<serde_json::Value> {
494 let table_name = Self::table_name_from(data)?;
495 let key_condition_expression = data
496 .get("key_condition_expression")
497 .and_then(|v| v.as_str())
498 .context("Missing 'key_condition_expression' in DynamoDB action data")?;
499
500 let output = ddb
501 .query()
502 .table_name(&table_name)
503 .key_condition_expression(key_condition_expression)
504 .set_expression_attribute_values(Self::attribute_values_from(
505 data,
506 "expression_attribute_values",
507 )?)
508 .send()
509 .await
510 .context("Query failed")?;
511
512 let items: Vec<serde_json::Value> = output
513 .items()
514 .iter()
515 .map(Self::attribute_map_to_json)
516 .collect();
517
518 Ok(serde_json::json!({
519 "table_name": table_name,
520 "count": items.len(),
521 "items": items,
522 }))
523 }
524
525 async fn scan(
527 ddb: &aws_sdk_dynamodb::Client,
528 data: &serde_json::Value,
529 ) -> Result<serde_json::Value> {
530 let table_name = Self::table_name_from(data)?;
531
532 let mut request = ddb
533 .scan()
534 .table_name(&table_name)
535 .set_expression_attribute_values(Self::attribute_values_from(
536 data,
537 "expression_attribute_values",
538 )?);
539
540 if let Some(filter) = data.get("filter_expression").and_then(|v| v.as_str()) {
541 request = request.filter_expression(filter);
542 }
543
544 let output = request.send().await.context("Scan failed")?;
545
546 let items: Vec<serde_json::Value> = output
547 .items()
548 .iter()
549 .map(Self::attribute_map_to_json)
550 .collect();
551
552 Ok(serde_json::json!({
553 "table_name": table_name,
554 "count": items.len(),
555 "items": items,
556 }))
557 }
558
559 async fn update_item(
561 ddb: &aws_sdk_dynamodb::Client,
562 data: &serde_json::Value,
563 ) -> Result<serde_json::Value> {
564 let table_name = Self::table_name_from(data)?;
565 let key = Self::attribute_map_from(data, "key")?;
566 let update_expression = data
567 .get("update_expression")
568 .and_then(|v| v.as_str())
569 .context("Missing 'update_expression' in DynamoDB action data")?;
570
571 let output = ddb
572 .update_item()
573 .table_name(&table_name)
574 .set_key(Some(key))
575 .update_expression(update_expression)
576 .set_expression_attribute_values(Self::attribute_values_from(
577 data,
578 "expression_attribute_values",
579 )?)
580 .return_values(aws_sdk_dynamodb::types::ReturnValue::AllNew)
581 .send()
582 .await
583 .context("UpdateItem failed")?;
584
585 let attributes = match output.attributes {
586 Some(attrs) => Self::attribute_map_to_json(&attrs),
587 None => serde_json::json!(null),
588 };
589
590 Ok(serde_json::json!({
591 "table_name": table_name,
592 "updated": true,
593 "attributes": attributes,
594 }))
595 }
596
597 async fn delete_item(
599 ddb: &aws_sdk_dynamodb::Client,
600 data: &serde_json::Value,
601 ) -> Result<serde_json::Value> {
602 let table_name = Self::table_name_from(data)?;
603 let key = Self::attribute_map_from(data, "key")?;
604
605 ddb.delete_item()
606 .table_name(&table_name)
607 .set_key(Some(key))
608 .send()
609 .await
610 .context("DeleteItem failed")?;
611
612 Ok(serde_json::json!({ "table_name": table_name, "deleted": true }))
613 }
614
615 async fn apply_action(
622 result: ClientActionResult,
623 client_id: ClientId,
624 app_state: &Arc<AppState>,
625 llm_client: &OllamaClient,
626 status_tx: &mpsc::UnboundedSender<String>,
627 ) -> ClientSendOutcome {
628 match result {
629 ClientActionResult::Custom { name, data } => {
630 match Self::execute_operation(
631 client_id,
632 name.clone(),
633 data,
634 app_state.clone(),
635 llm_client.clone(),
636 status_tx.clone(),
637 )
638 .await
639 {
640 Ok(value) => ClientSendOutcome::Executed {
641 detail: format!(
642 "{} completed: {}",
643 name,
644 truncate_for_log(&value.to_string(), 200)
645 ),
646 },
647 Err(e) => {
648 let _ = status_tx
649 .send(format!("[ERROR] DynamoDB operation {} failed: {}", name, e));
650 ClientSendOutcome::Rejected {
658 error: format!(
659 "{} failed: {}",
660 name,
661 truncate_for_log(&e.to_string(), 200)
662 ),
663 }
664 }
665 }
666 }
667 ClientActionResult::Disconnect => ClientSendOutcome::Disconnected,
668 ClientActionResult::WaitForMore => ClientSendOutcome::Executed {
669 detail: "wait_for_more".to_string(),
670 },
671 ClientActionResult::NoAction => ClientSendOutcome::Executed {
672 detail: "no_action".to_string(),
673 },
674 ClientActionResult::SendData(_) => ClientSendOutcome::Executed {
675 detail: "send_data has no meaning for the DynamoDB client: it speaks the \
676 DynamoDB API through the AWS SDK, not a socket this client owns"
677 .to_string(),
678 },
679 ClientActionResult::Multiple(_) => ClientSendOutcome::Executed {
680 detail: "the DynamoDB client's own verbs never produce Multiple; nothing was \
681 applied"
682 .to_string(),
683 },
684 }
685 }
686
687 async fn command_loop(
695 mut command_rx: mpsc::Receiver<ClientCommand>,
696 client_id: ClientId,
697 app_state: Arc<AppState>,
698 llm_client: OllamaClient,
699 status_tx: mpsc::UnboundedSender<String>,
700 ) {
701 let protocol = crate::client::dynamodb::actions::DynamoDbClientProtocol::new();
702
703 while let Some(command) = command_rx.recv().await {
704 let action = command.action.clone();
705 let outcome = match protocol.execute_action(action.clone()) {
706 Err(e) => ClientSendOutcome::Rejected {
707 error: e.to_string(),
708 },
709 Ok(result) => {
710 Self::apply_action(result, client_id, &app_state, &llm_client, &status_tx).await
711 }
712 };
713 let disconnected = matches!(outcome, ClientSendOutcome::Disconnected);
714
715 app_state
716 .record_access_log(
717 AccessLogOwner::Client(client_id.as_u32()),
718 protocol.protocol_name(),
719 None,
720 "injected_action",
721 action,
722 vec![serde_json::to_value(&outcome).unwrap_or(serde_json::Value::Null)],
723 )
724 .await;
725 let _ = status_tx.send("__UPDATE_UI__".to_string());
726 crate::client::command_support::reply(command, Ok(outcome));
727
728 if disconnected {
729 info!(
730 "DynamoDB client {} disconnecting on injected action",
731 client_id
732 );
733 app_state
734 .update_client_status(client_id, ClientStatus::Disconnected)
735 .await;
736 break;
737 }
738 }
739
740 info!("DynamoDB client {} command loop stopped", client_id);
741 app_state.remove_client_handle(client_id).await;
743 let _ = status_tx.send("__UPDATE_UI__".to_string());
744 }
745
746 async fn get_config(
748 app_state: &AppState,
749 client_id: ClientId,
750 ) -> Result<(String, Option<String>, Option<String>, Option<String>)> {
751 let region = app_state
752 .with_client_mut(client_id, |client| {
753 client
754 .get_protocol_field("region")
755 .and_then(|v| v.as_str())
756 .map(|s| s.to_string())
757 })
758 .await
759 .flatten()
760 .context("No region found")?;
761
762 let endpoint_url = app_state
763 .with_client_mut(client_id, |client| {
764 client
765 .get_protocol_field("endpoint_url")
766 .and_then(|v| v.as_str())
767 .map(|s| s.to_string())
768 })
769 .await
770 .flatten();
771
772 let access_key_id = app_state
773 .with_client_mut(client_id, |client| {
774 client
775 .get_protocol_field("access_key_id")
776 .and_then(|v| v.as_str())
777 .map(|s| s.to_string())
778 })
779 .await
780 .flatten();
781
782 let secret_access_key = app_state
783 .with_client_mut(client_id, |client| {
784 client
785 .get_protocol_field("secret_access_key")
786 .and_then(|v| v.as_str())
787 .map(|s| s.to_string())
788 })
789 .await
790 .flatten();
791
792 Ok((region, endpoint_url, access_key_id, secret_access_key))
793 }
794
795 async fn build_aws_config(
797 region: &str,
798 endpoint_url: Option<&str>,
799 access_key_id: Option<&str>,
800 secret_access_key: Option<&str>,
801 ) -> Result<aws_config::SdkConfig> {
802 use aws_config::BehaviorVersion;
803
804 crate::client::aws_support::validate_region("DynamoDB", region)?;
805 let mut config_loader = aws_config::defaults(BehaviorVersion::latest())
806 .region(aws_config::Region::new(region.to_string()));
807
808 if let Some(endpoint) = endpoint_url {
810 config_loader = config_loader.endpoint_url(endpoint);
811 }
812
813 if let (Some(key_id), Some(secret_key)) = (access_key_id, secret_access_key) {
815 use aws_config::meta::credentials::CredentialsProviderChain;
816 use aws_credential_types::Credentials;
817
818 let credentials = Credentials::new(
819 key_id,
820 secret_key,
821 None, None, "netget_dynamodb_client",
824 );
825
826 let provider = CredentialsProviderChain::first_try(
827 "Static",
828 aws_credential_types::provider::SharedCredentialsProvider::new(credentials),
829 );
830
831 config_loader = config_loader.credentials_provider(provider);
832 }
833
834 Ok(config_loader.load().await)
835 }
836
837 fn json_to_attribute_value(
845 name: &str,
846 json: &serde_json::Value,
847 ) -> Result<aws_sdk_dynamodb::types::AttributeValue> {
848 use aws_sdk_dynamodb::types::AttributeValue;
849
850 let map = json.as_object().with_context(|| {
851 format!(
852 "Attribute '{name}' must be a DynamoDB typed value such as {{\"S\": \"text\"}}, \
853 got {json}"
854 )
855 })?;
856 if map.len() != 1 {
857 anyhow::bail!(
858 "Attribute '{name}' must carry exactly one type key (\"S\", \"N\", \"B\", \
859 \"BOOL\" or \"NULL\"), got {json}"
860 );
861 }
862 let (type_key, value) = map.iter().next().expect("length checked above");
863
864 let wrong_shape = |expected: &str| {
865 anyhow::anyhow!("Attribute '{name}' has type {type_key} so its value must be {expected}, got {value}")
866 };
867
868 match type_key.as_str() {
869 "S" => Ok(AttributeValue::S(
870 value
871 .as_str()
872 .ok_or_else(|| wrong_shape("a string"))?
873 .to_string(),
874 )),
875 "N" => Ok(AttributeValue::N(
876 value
877 .as_str()
878 .ok_or_else(|| wrong_shape("a string holding a number, e.g. \"123\""))?
879 .to_string(),
880 )),
881 "B" => {
882 let encoded = value
883 .as_str()
884 .ok_or_else(|| wrong_shape("a base64 string"))?;
885 let bytes = base64::Engine::decode(
888 &base64::engine::general_purpose::STANDARD,
889 encoded,
890 )
891 .with_context(|| {
892 format!("Attribute '{name}' is type B, so its value must be standard base64")
893 })?;
894 Ok(AttributeValue::B(Blob::new(bytes)))
895 }
896 "BOOL" => Ok(AttributeValue::Bool(
897 value
898 .as_bool()
899 .ok_or_else(|| wrong_shape("true or false"))?,
900 )),
901 "NULL" => Ok(AttributeValue::Null(true)),
902 other => anyhow::bail!(
903 "Attribute '{name}' uses DynamoDB type {other:?}, which this client does not \
904 support. Supported: S (string), N (number as string), B (base64 bytes), BOOL, \
905 NULL. Maps (M), lists (L) and sets (SS/NS/BS) are not implemented — do not \
906 send them rather than expecting them to be dropped."
907 ),
908 }
909 }
910
911 fn attribute_map_to_json(
913 map: &std::collections::HashMap<String, aws_sdk_dynamodb::types::AttributeValue>,
914 ) -> serde_json::Value {
915 use aws_sdk_dynamodb::types::AttributeValue;
916
917 let mut json_map = serde_json::Map::new();
918 for (key, value) in map {
919 let json_value = match value {
920 AttributeValue::S(s) => serde_json::json!({"S": s}),
921 AttributeValue::N(n) => serde_json::json!({"N": n}),
922 AttributeValue::B(b) => {
923 let b64 = base64::Engine::encode(
924 &base64::engine::general_purpose::STANDARD,
925 b.as_ref(),
926 );
927 serde_json::json!({"B": b64})
928 }
929 AttributeValue::Bool(b) => serde_json::json!({"BOOL": b}),
930 AttributeValue::Null(_) => serde_json::json!({"NULL": true}),
931 other => serde_json::json!({
935 "netget_unsupported_attribute_type": format!("{other:?}")
936 }),
937 };
938 json_map.insert(key.clone(), json_value);
939 }
940 serde_json::Value::Object(json_map)
941 }
942
943 async fn call_llm_with_response(
945 client_id: ClientId,
946 operation: &str,
947 success: bool,
948 data: Option<serde_json::Value>,
949 error: Option<String>,
950 app_state: &Arc<AppState>,
953 llm_client: &OllamaClient,
954 status_tx: &mpsc::UnboundedSender<String>,
955 ) -> Result<()> {
956 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
957 let protocol =
958 Arc::new(crate::client::dynamodb::actions::DynamoDbClientProtocol::new());
959
960 let mut event_data = serde_json::json!({
961 "operation": operation,
962 "success": success,
963 });
964
965 if let Some(d) = data {
966 event_data["data"] = d;
967 }
968 if let Some(e) = error {
969 event_data["error"] = serde_json::json!(e);
970 }
971
972 let event = Event::new(&DYNAMODB_CLIENT_RESPONSE_RECEIVED_EVENT, event_data);
973
974 let memory = app_state
975 .get_memory_for_client(client_id)
976 .await
977 .unwrap_or_default();
978
979 match call_llm_for_client(
980 llm_client,
981 app_state,
982 client_id.to_string(),
983 &instruction,
984 &memory,
985 Some(&event),
986 protocol.as_ref(),
987 status_tx,
988 )
989 .await
990 {
991 Ok(ClientLlmResult {
992 actions,
993 memory_updates,
994 }) => {
995 if let Some(mem) = memory_updates {
997 app_state.set_memory_for_client(client_id, mem).await;
998 }
999
1000 use crate::llm::actions::client_trait::Client;
1008 for action in actions {
1009 match protocol.execute_action(action.clone()) {
1010 Ok(crate::llm::actions::client_trait::ClientActionResult::Custom {
1011 name,
1012 data,
1013 }) => {
1014 let outcome =
1021 Self::run_operation_once(client_id, &name, &data, app_state)
1022 .await;
1023 info!(
1024 "DynamoDB client {} follow-up action {:?} -> {:?}",
1025 client_id, action, outcome
1026 );
1027 }
1028 Ok(other) => info!(
1029 "DynamoDB client {} follow-up produced no operation: {:?}",
1030 client_id, other
1031 ),
1032 Err(e) => error!(
1033 "DynamoDB client {} rejected its own follow-up action: {}",
1034 client_id, e
1035 ),
1036 }
1037 }
1038 }
1039 Err(e) => {
1040 error!("LLM error for DynamoDB client {}: {}", client_id, e);
1041 }
1042 }
1043 }
1044
1045 Ok(())
1046 }
1047}