use std::collections::{BTreeMap, BTreeSet};
use std::io;
use rig_core::completion::Message;
use rig_core::conversation::EntryRef;
use rig_core::message::{AssistantContent, UserContent};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{Files, Session, main_order};
use rig_core::now_ms;
pub const META: &str = "meta.json";
const TITLE_CHARS: usize = 60;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SessionMeta {
pub title: String,
pub labels: BTreeMap<String, String>,
pub updated: u64,
pub model: String,
pub messages: usize,
#[serde(rename = "data", skip_serializing_if = "BTreeMap::is_empty")]
pub listed: BTreeMap<String, Value>,
}
impl SessionMeta {
pub fn read(files: &(impl Files + ?Sized)) -> Option<Self> {
serde_json::from_slice(&files.read_file(META).ok()?).ok()
}
pub fn write(&self, files: &(impl Files + ?Sized)) -> io::Result<()> {
files.write_file(META, &serde_json::to_vec_pretty(self)?)
}
pub fn relist(&mut self, session: &Session, listed: &BTreeSet<String>) {
let mut agents: Vec<_> = session.agents.iter().filter(|a| !a.retired).collect();
agents.sort_by(|a, b| {
main_order(a.parent.is_some(), &a.id).cmp(&main_order(b.parent.is_some(), &b.id))
});
let main = agents.first();
self.messages = main.map_or(0, |main| main.conversation.messages().len());
self.model = main.and_then(|main| main.model()).unwrap_or_default();
let keys: BTreeSet<String> = listed.iter().chain(self.listed.keys()).cloned().collect();
self.listed = keys
.into_iter()
.filter_map(|key| {
let mut holders = agents.iter().map(|agent| &agent.saved);
let value = session
.saved
.get(&key)
.or_else(|| holders.find_map(|state| state.get(&key)))?;
Some((key, value.clone()))
})
.collect();
self.updated = now_ms();
}
}
pub fn title_of(text: &str) -> String {
let line = text.split_whitespace().collect::<Vec<_>>().join(" ");
match line.char_indices().nth(TITLE_CHARS) {
None => line,
Some(_) => {
let kept = line.chars().take(TITLE_CHARS - 1);
kept.chain(['…']).collect()
}
}
}
pub fn excerpt(entry: &EntryRef<'_>) -> String {
let (who, text) = match entry.message {
Message::System { content } => ("system", content.clone()),
Message::User { content } => {
let text: Vec<&str> = content
.iter()
.filter_map(|part| match part {
UserContent::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect();
match text.is_empty() {
true => ("tool results", String::new()),
false => ("user", text.join(" ")),
}
}
Message::Assistant(turn) => {
let text = turn.text();
let calls: Vec<&str> = turn
.content
.iter()
.filter_map(|part| match part {
AssistantContent::ToolCall(call) => Some(call.function.name.as_str()),
_ => None,
})
.collect();
match (text.trim().is_empty(), calls.is_empty()) {
(true, false) => ("calls", calls.join(", ")),
_ => ("assistant", text),
}
}
};
format!("{who}: {}", title_of(&text)).trim_end().to_owned()
}
#[cfg(test)]
mod tests;