[router] conversation item API: create, retrieve and delete (#11369)
This commit is contained in:
@@ -142,6 +142,50 @@ impl ConversationItemStorage for MemoryConversationItemStorage {
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn get_item(&self, item_id: &ConversationItemId) -> Result<Option<ConversationItem>> {
|
||||
let items = self.items.read().unwrap();
|
||||
Ok(items.get(item_id).cloned())
|
||||
}
|
||||
|
||||
async fn is_item_linked(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> Result<bool> {
|
||||
let rev = self.rev_index.read().unwrap();
|
||||
if let Some(conv_idx) = rev.get(conversation_id) {
|
||||
Ok(conv_idx.contains_key(&item_id.0))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_item(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> Result<()> {
|
||||
// Get the key from rev_index and remove the entry at the same time
|
||||
let key_to_remove = {
|
||||
let mut rev = self.rev_index.write().unwrap();
|
||||
if let Some(conv_idx) = rev.get_mut(conversation_id) {
|
||||
conv_idx.remove(&item_id.0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// If the item was in rev_index, remove it from links as well
|
||||
if let Some(key) = key_to_remove {
|
||||
let mut links = self.links.write().unwrap();
|
||||
if let Some(conv_links) = links.get_mut(conversation_id) {
|
||||
conv_links.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -243,6 +243,92 @@ impl ConversationItemStorage for OracleConversationItemStorage {
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn get_item(&self, item_id: &ConversationItemId) -> ItemResult<Option<ConversationItem>> {
|
||||
let iid = item_id.0.clone();
|
||||
|
||||
self.with_connection(move |conn| {
|
||||
let mut stmt = conn
|
||||
.statement(
|
||||
"SELECT id, response_id, item_type, role, content, status, created_at \
|
||||
FROM conversation_items WHERE id = :1",
|
||||
)
|
||||
.build()
|
||||
.map_err(map_oracle_error)?;
|
||||
|
||||
let mut rows = stmt.query(&[&iid]).map_err(map_oracle_error)?;
|
||||
|
||||
if let Some(row_res) = rows.next() {
|
||||
let row = row_res.map_err(map_oracle_error)?;
|
||||
let id: String = row.get(0).map_err(map_oracle_error)?;
|
||||
let response_id: Option<String> = row.get(1).map_err(map_oracle_error)?;
|
||||
let item_type: String = row.get(2).map_err(map_oracle_error)?;
|
||||
let role: Option<String> = row.get(3).map_err(map_oracle_error)?;
|
||||
let content_raw: Option<String> = row.get(4).map_err(map_oracle_error)?;
|
||||
let status: Option<String> = row.get(5).map_err(map_oracle_error)?;
|
||||
let created_at: DateTime<Utc> = row.get(6).map_err(map_oracle_error)?;
|
||||
|
||||
let content = match content_raw {
|
||||
Some(s) => serde_json::from_str(&s)?,
|
||||
None => Value::Null,
|
||||
};
|
||||
|
||||
Ok(Some(ConversationItem {
|
||||
id: ConversationItemId(id),
|
||||
response_id,
|
||||
item_type,
|
||||
role,
|
||||
content,
|
||||
status,
|
||||
created_at,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn is_item_linked(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> ItemResult<bool> {
|
||||
let cid = conversation_id.0.clone();
|
||||
let iid = item_id.0.clone();
|
||||
|
||||
self.with_connection(move |conn| {
|
||||
let count: i64 = conn
|
||||
.query_row_as(
|
||||
"SELECT COUNT(*) FROM conversation_item_links WHERE conversation_id = :1 AND item_id = :2",
|
||||
&[&cid, &iid],
|
||||
)
|
||||
.map_err(map_oracle_error)?;
|
||||
Ok(count > 0)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_item(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> ItemResult<()> {
|
||||
let cid = conversation_id.0.clone();
|
||||
let iid = item_id.0.clone();
|
||||
|
||||
self.with_connection(move |conn| {
|
||||
// Delete ONLY the link (do not delete the item itself)
|
||||
conn.execute(
|
||||
"DELETE FROM conversation_item_links WHERE conversation_id = :1 AND item_id = :2",
|
||||
&[&cid, &iid],
|
||||
)
|
||||
.map_err(map_oracle_error)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -94,15 +94,32 @@ pub trait ConversationItemStorage: Send + Sync + 'static {
|
||||
conversation_id: &ConversationId,
|
||||
params: ListParams,
|
||||
) -> Result<Vec<ConversationItem>>;
|
||||
|
||||
/// Get a single item by ID
|
||||
async fn get_item(&self, item_id: &ConversationItemId) -> Result<Option<ConversationItem>>;
|
||||
|
||||
/// Check if an item is linked to a conversation
|
||||
async fn is_item_linked(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> Result<bool>;
|
||||
|
||||
/// Delete an item link from a conversation (does not delete the item itself)
|
||||
async fn delete_item(
|
||||
&self,
|
||||
conversation_id: &ConversationId,
|
||||
item_id: &ConversationItemId,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
pub type SharedConversationItemStorage = Arc<dyn ConversationItemStorage>;
|
||||
|
||||
/// Helper to build id prefix based on item_type
|
||||
pub fn make_item_id(item_type: &str) -> ConversationItemId {
|
||||
// Generate a 24-byte random hex string (48 hex chars), consistent with conversation id style
|
||||
// Generate exactly 50 hex characters (25 bytes) for the part after the underscore
|
||||
let mut rng = rand::rng();
|
||||
let mut bytes = [0u8; 24];
|
||||
let mut bytes = [0u8; 25];
|
||||
rng.fill_bytes(&mut bytes);
|
||||
let hex_string: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user