use std::io;
use choreo_proto::Turn;
use redb::{ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition};
use tracing::debug;
use super::{db_err, session_range_end};
pub(super) const SESSION_ATTACHMENTS: TableDefinition<(u64, u32, String), &[u8]> =
TableDefinition::new("session_attachments");
fn display_slot(index: u32) -> String {
format!("d{index}")
}
fn result_slot(call_id: &str) -> String {
format!("r{call_id}")
}
type ReadOnlyAttachments<'a> = ReadOnlyTable<(u64, u32, String), &'a [u8]>;
pub(super) fn delete_session_attachments(
write_txn: &redb::WriteTransaction,
session_id: u64,
) -> io::Result<()> {
let mut att_table = write_txn
.open_table(SESSION_ATTACHMENTS)
.map_err(|e| db_err(format!("redb open session_attachments: {e}")))?;
let att_keys: Vec<(u64, u32, String)> = att_table
.range::<(u64, u32, String)>(
(session_id, 0u32, String::new())..(session_range_end(session_id), 0u32, String::new()),
)
.map_err(|e| db_err(format!("redb range session_attachments: {e}")))?
.filter_map(std::result::Result::ok)
.map(|(k, _)| k.value())
.collect();
for key in att_keys {
att_table
.remove(key)
.map_err(|e| db_err(format!("redb remove session_attachment: {e}")))?;
}
Ok(())
}
fn delete_turn_attachments(
write_txn: &redb::WriteTransaction,
session_id: u64,
turn_id: u32,
) -> io::Result<()> {
let mut att_table = write_txn
.open_table(SESSION_ATTACHMENTS)
.map_err(|e| db_err(format!("redb open session_attachments: {e}")))?;
let att_keys: Vec<(u64, u32, String)> = att_table
.range::<(u64, u32, String)>(
(session_id, turn_id, String::new())
..(session_id, turn_id.saturating_add(1), String::new()),
)
.map_err(|e| db_err(format!("redb range session_attachments: {e}")))?
.filter_map(std::result::Result::ok)
.map(|(k, _)| k.value())
.collect();
for key in att_keys {
att_table
.remove(key)
.map_err(|e| db_err(format!("redb remove session_attachment: {e}")))?;
}
Ok(())
}
pub(super) fn write_turn_attachments(
write_txn: &redb::WriteTransaction,
session_id: u64,
turn_id: u32,
turn: &Turn,
) -> io::Result<()> {
delete_turn_attachments(write_txn, session_id, turn_id)?;
let mut attachments = write_txn
.open_table(SESSION_ATTACHMENTS)
.map_err(|e| db_err(format!("redb open session_attachments: {e}")))?;
for (i, img) in turn.displayed_images.iter().enumerate() {
if img.data.is_empty() {
continue; }
let slot = display_slot(u32::try_from(i).unwrap_or(u32::MAX));
attachments
.insert((session_id, turn_id, slot), img.data.as_slice())
.map_err(|e| db_err(format!("redb insert display attachment: {e}")))?;
}
for tr in &turn.tool_results {
if let Some(image) = &tr.image
&& !image.data.is_empty()
{
let slot = result_slot(&tr.call_id);
attachments
.insert((session_id, turn_id, slot), image.data.as_slice())
.map_err(|e| db_err(format!("redb insert result attachment: {e}")))?;
}
}
Ok(())
}
pub(super) fn reattach_turn_attachments(
attachments: Option<&ReadOnlyAttachments<'_>>,
session_id: u64,
turn_id: u32,
turn: &mut Turn,
) -> io::Result<()> {
let Some(attachments) = attachments else {
return Ok(());
};
for (i, img) in turn.displayed_images.iter_mut().enumerate() {
if img.data.is_empty() {
let slot = display_slot(u32::try_from(i).unwrap_or(u32::MAX));
if let Some(guard) = attachments
.get((session_id, turn_id, slot))
.map_err(|e| db_err(format!("redb get display attachment: {e}")))?
{
img.data = guard.value().to_vec();
}
}
}
for tr in &mut turn.tool_results {
if let Some(image) = &mut tr.image
&& image.data.is_empty()
{
let slot = result_slot(&tr.call_id);
if let Some(guard) = attachments
.get((session_id, turn_id, slot))
.map_err(|e| db_err(format!("redb get result attachment: {e}")))?
{
image.data = guard.value().to_vec();
}
}
}
debug!(session_id, turn_id, "re-attached turn image attachments");
Ok(())
}
pub fn read_display_image(
db: &redb::Database,
session_id: u64,
turn_id: u32,
image_index: u32,
) -> io::Result<Option<Vec<u8>>> {
let read_txn = db
.begin_read()
.map_err(|e| db_err(format!("redb read txn (display image): {e}")))?;
let table = match read_txn.open_table(SESSION_ATTACHMENTS) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => {
return Err(db_err(format!(
"redb open session_attachments (display image): {e}"
)));
}
};
let slot = display_slot(image_index);
match table
.get((session_id, turn_id, slot))
.map_err(|e| db_err(format!("redb get display attachment: {e}")))?
{
Some(guard) => Ok(Some(guard.value().to_vec())),
None => Ok(None),
}
}
pub fn write_display_image_attachment(
db: &redb::Database,
session_id: u64,
turn_id: u32,
image_index: u32,
data: &[u8],
) -> io::Result<()> {
if data.is_empty() {
return Ok(()); }
let write_txn = db
.begin_write()
.map_err(|e| db_err(format!("redb write txn (display image): {e}")))?;
{
let mut table = write_txn.open_table(SESSION_ATTACHMENTS).map_err(|e| {
db_err(format!(
"redb open session_attachments (display image): {e}"
))
})?;
table
.insert((session_id, turn_id, display_slot(image_index)), data)
.map_err(|e| db_err(format!("redb insert display attachment (emit): {e}")))?;
}
write_txn
.commit()
.map_err(|e| db_err(format!("redb commit display attachment (emit): {e}")))?;
Ok(())
}