Skip to main content

agentic_core/storage/models/
response.rs

1//! LLM API response stored in the database.
2
3use super::super::pool::{DbPool, DbResult, DbTransaction};
4use crate::utils::common::{deserialize_from_string_opt, deserialize_from_string_opt_or_default, utcnow_str};
5
6/// LLM API response stored in the database.
7///
8/// Maps to the `responses` table and represents a single API response
9/// with its metadata and history chain.
10#[derive(Debug, Clone, sqlx::FromRow)]
11pub struct Response {
12    /// Unique response identifier.
13    pub id: String,
14
15    /// Optional conversation this response belongs to.
16    pub conversation_id: Option<String>,
17
18    /// Optional reference to previous response for chaining.
19    pub previous_response_id: Option<String>,
20
21    /// History item IDs as JSON array string.
22    pub history_item_ids: Option<String>,
23
24    /// Response metadata as JSON object string.
25    pub metadata: Option<String>,
26
27    /// Creation timestamp as Unix timestamp in seconds.
28    pub created_at: i64,
29}
30
31/// Create a response in a transaction and return it.
32///
33/// # Errors
34/// Returns `DbResult::Err` if the database insertion fails.
35pub async fn create_in_tx(
36    tx: &mut DbTransaction<'_>,
37    id: &str,
38    conversation_id: Option<&str>,
39    previous_response_id: Option<&str>,
40    history_item_ids: Option<&str>,
41    metadata: Option<&str>,
42) -> DbResult<Response> {
43    let now = utcnow_str();
44    sqlx::query_as::<_, Response>(
45        "INSERT INTO responses \
46         (id, conversation_id, previous_response_id, history_item_ids, metadata, created_at) \
47         VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",
48    )
49    .bind(id)
50    .bind(conversation_id)
51    .bind(previous_response_id)
52    .bind(history_item_ids)
53    .bind(metadata)
54    .bind(now)
55    .fetch_one(&mut **tx)
56    .await
57}
58
59/// Get a response by ID.
60///
61/// # Errors
62/// Returns `DbResult::Err` if the database query fails.
63pub async fn get(pool: &DbPool, id: &str) -> DbResult<Option<Response>> {
64    sqlx::query_as::<_, Response>("SELECT * FROM responses WHERE id = $1")
65        .bind(id)
66        .fetch_optional(pool)
67        .await
68}
69
70impl Response {
71    /// Deserialize `history_item_ids` from JSON string to Vec<String>.
72    #[must_use]
73    pub fn history_item_ids_vec(&self) -> Vec<String> {
74        deserialize_from_string_opt_or_default(&self.history_item_ids)
75    }
76
77    /// Deserialize metadata from JSON string to the given type.
78    #[must_use]
79    pub fn metadata_as<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
80        deserialize_from_string_opt(&self.metadata)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_response_history_ids_empty() {
90        let response = Response {
91            id: "test".to_string(),
92            conversation_id: None,
93            previous_response_id: None,
94            history_item_ids: None,
95            metadata: None,
96            created_at: 1_704_067_200,
97        };
98
99        let ids: Vec<String> = response.history_item_ids_vec();
100        assert!(ids.is_empty());
101    }
102
103    #[test]
104    fn test_response_history_ids_valid() {
105        let response = Response {
106            id: "test".to_string(),
107            conversation_id: None,
108            previous_response_id: None,
109            history_item_ids: Some(r#"["item_1", "item_2"]"#.to_string()),
110            metadata: None,
111            created_at: 1_704_067_200,
112        };
113
114        let ids = response.history_item_ids_vec();
115        assert_eq!(ids.len(), 2);
116        assert_eq!(ids[0], "item_1");
117    }
118
119    #[test]
120    fn test_response_metadata_deserialize() {
121        #[derive(serde::Deserialize, PartialEq, Debug)]
122        struct TestMeta {
123            model: String,
124        }
125
126        let response = Response {
127            id: "resp_1".to_string(),
128            conversation_id: None,
129            previous_response_id: None,
130            history_item_ids: None,
131            metadata: Some(r#"{"model":"gpt-4"}"#.to_string()),
132            created_at: 1_704_067_200,
133        };
134
135        let meta: Option<TestMeta> = response.metadata_as();
136        assert!(meta.is_some());
137    }
138}