use ahash::AHashMap;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
#[cfg(feature = "typescript")]
use tsify::Tsify;
use crate::document::Document;
use crate::role::RoleName;
use crate::term::NormalizedTermValue;
use crate::validation::preview;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ConversationId(pub String);
impl ConversationId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn from_string(id: String) -> Self {
Self(id)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ConversationId {
fn default() -> Self {
Self::new()
}
}
impl Display for ConversationId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub enum ContextType {
System,
UserInput,
Document,
SearchResult,
External,
KGTermDefinition,
KGIndex,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct MessageId(pub String);
impl MessageId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn from_string(id: String) -> Self {
Self(id)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for MessageId {
fn default() -> Self {
Self::new()
}
}
impl Display for MessageId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContextItem {
pub id: String,
pub context_type: ContextType,
pub title: String,
pub summary: Option<String>,
pub content: String,
pub metadata: AHashMap<String, String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub relevance_score: Option<f64>,
}
impl ContextItem {
pub fn from_document(document: &Document) -> Self {
let mut metadata = AHashMap::new();
metadata.insert("source_type".to_string(), "document".to_string());
metadata.insert("document_id".to_string(), document.id.clone());
if !document.url.is_empty() {
metadata.insert("url".to_string(), document.url.clone());
}
if let Some(tags) = &document.tags {
metadata.insert("tags".to_string(), tags.join(", "));
}
if let Some(rank) = document.rank {
metadata.insert("rank".to_string(), rank.to_string());
}
Self {
id: uuid::Uuid::new_v4().to_string(),
context_type: ContextType::Document,
title: if document.title.is_empty() {
document.id.clone()
} else {
document.title.clone()
},
summary: document.description.clone(),
content: format!(
"Title: {}\n\n{}\n\n{}",
document.title,
document.description.as_deref().unwrap_or(""),
document.body
),
metadata,
created_at: chrono::Utc::now(),
relevance_score: document.rank.map(|r| r as f64),
}
}
pub fn from_search_result(query: &str, documents: &[Document]) -> Self {
let mut metadata = AHashMap::new();
metadata.insert("source_type".to_string(), "search_result".to_string());
metadata.insert("query".to_string(), query.to_string());
metadata.insert("result_count".to_string(), documents.len().to_string());
let content = if documents.is_empty() {
format!("Search query: '{}'\nNo results found.", query)
} else {
let mut content = format!("Search query: '{}'\nResults:\n\n", query);
for (i, doc) in documents.iter().take(5).enumerate() {
content.push_str(&format!(
"{}. {}\n {}\n Rank: {}\n\n",
i + 1,
doc.title,
doc.description.as_deref().unwrap_or("No description"),
doc.rank.unwrap_or(0)
));
}
if documents.len() > 5 {
content.push_str(&format!("... and {} more results\n", documents.len() - 5));
}
content
};
Self {
id: uuid::Uuid::new_v4().to_string(),
context_type: ContextType::Document, title: format!("Search: {}", query),
summary: Some(format!(
"Search results for '{}' - {} documents found",
query,
documents.len()
)),
content,
metadata,
created_at: chrono::Utc::now(),
relevance_score: documents.first().and_then(|d| d.rank.map(|r| r as f64)),
}
}
pub fn from_kg_term_definition(kg_term: &KGTermDefinition) -> Self {
let mut metadata = AHashMap::new();
metadata.insert("source_type".to_string(), "kg_term".to_string());
metadata.insert("term_id".to_string(), kg_term.id.to_string());
metadata.insert(
"normalized_term".to_string(),
kg_term.normalized_term.to_string(),
);
metadata.insert(
"synonyms_count".to_string(),
kg_term.synonyms.len().to_string(),
);
metadata.insert(
"related_terms_count".to_string(),
kg_term.related_terms.len().to_string(),
);
metadata.insert(
"usage_examples_count".to_string(),
kg_term.usage_examples.len().to_string(),
);
if let Some(ref url) = kg_term.url {
metadata.insert("url".to_string(), url.clone());
}
for (key, value) in &kg_term.metadata {
metadata.insert(format!("kg_{}", key), value.clone());
}
let mut content = format!("**Term:** {}\n", kg_term.term);
if let Some(ref definition) = kg_term.definition {
content.push_str(&format!("**Definition:** {}\n", definition));
}
if !kg_term.synonyms.is_empty() {
content.push_str(&format!("**Synonyms:** {}\n", kg_term.synonyms.join(", ")));
}
if !kg_term.related_terms.is_empty() {
content.push_str(&format!(
"**Related Terms:** {}\n",
kg_term.related_terms.join(", ")
));
}
if !kg_term.usage_examples.is_empty() {
content.push_str("**Usage Examples:**\n");
for (i, example) in kg_term.usage_examples.iter().enumerate() {
content.push_str(&format!("{}. {}\n", i + 1, example));
}
}
Self {
id: uuid::Uuid::new_v4().to_string(),
context_type: ContextType::KGTermDefinition,
title: format!("KG Term: {}", kg_term.term),
summary: Some(format!(
"Knowledge Graph term '{}' with {} synonyms and {} related terms",
kg_term.term,
kg_term.synonyms.len(),
kg_term.related_terms.len()
)),
content,
metadata,
created_at: chrono::Utc::now(),
relevance_score: kg_term.relevance_score,
}
}
pub fn from_kg_index(kg_index: &KGIndexInfo) -> Self {
let mut metadata = AHashMap::new();
metadata.insert("source_type".to_string(), "kg_index".to_string());
metadata.insert("kg_name".to_string(), kg_index.name.clone());
metadata.insert("total_terms".to_string(), kg_index.total_terms.to_string());
metadata.insert("total_nodes".to_string(), kg_index.total_nodes.to_string());
metadata.insert("total_edges".to_string(), kg_index.total_edges.to_string());
metadata.insert("source".to_string(), kg_index.source.clone());
metadata.insert(
"last_updated".to_string(),
kg_index.last_updated.to_rfc3339(),
);
if let Some(ref version) = kg_index.version {
metadata.insert("version".to_string(), version.clone());
}
let content = format!(
"**Knowledge Graph Index: {}**\n\n\
**Statistics:**\n\
- Total Terms: {}\n\
- Total Nodes: {}\n\
- Total Edges: {}\n\
- Source: {}\n\
- Last Updated: {}\n\
- Version: {}\n\n\
This context includes the complete knowledge graph index with all terms, \
relationships, and metadata available for reference.",
kg_index.name,
kg_index.total_terms,
kg_index.total_nodes,
kg_index.total_edges,
kg_index.source,
kg_index.last_updated.format("%Y-%m-%d %H:%M:%S UTC"),
kg_index.version.as_deref().unwrap_or("N/A")
);
Self {
id: uuid::Uuid::new_v4().to_string(),
context_type: ContextType::KGIndex,
title: format!("KG Index: {}", kg_index.name),
summary: Some(format!(
"Complete knowledge graph index with {} terms, {} nodes, and {} edges",
kg_index.total_terms, kg_index.total_nodes, kg_index.total_edges
)),
content,
metadata,
created_at: chrono::Utc::now(),
relevance_score: Some(1.0), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct KGTermDefinition {
pub term: String,
pub normalized_term: NormalizedTermValue,
pub id: u64,
pub definition: Option<String>,
pub synonyms: Vec<String>,
pub related_terms: Vec<String>,
pub usage_examples: Vec<String>,
pub url: Option<String>,
pub metadata: AHashMap<String, String>,
pub relevance_score: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct KGIndexInfo {
pub name: String,
pub total_terms: usize,
pub total_nodes: usize,
pub total_edges: usize,
pub last_updated: chrono::DateTime<chrono::Utc>,
pub source: String,
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ChatMessage {
pub id: MessageId,
pub role: String, pub content: String,
pub context_items: Vec<ContextItem>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub token_count: Option<u32>,
pub model: Option<String>,
}
impl ChatMessage {
pub fn user(content: String) -> Self {
Self {
id: MessageId::new(),
role: "user".to_string(),
content,
context_items: Vec::new(),
created_at: chrono::Utc::now(),
token_count: None,
model: None,
}
}
pub fn assistant(content: String, model: Option<String>) -> Self {
Self {
id: MessageId::new(),
role: "assistant".to_string(),
content,
context_items: Vec::new(),
created_at: chrono::Utc::now(),
token_count: None,
model,
}
}
pub fn system(content: String) -> Self {
Self {
id: MessageId::new(),
role: "system".to_string(),
content,
context_items: Vec::new(),
created_at: chrono::Utc::now(),
token_count: None,
model: None,
}
}
pub fn add_context(&mut self, context: ContextItem) {
self.context_items.push(context);
}
pub fn add_contexts(&mut self, contexts: Vec<ContextItem>) {
self.context_items.extend(contexts);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "snake_case")]
pub enum RotStatus {
Fresh,
Warning,
Critical,
}
impl std::fmt::Display for RotStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RotStatus::Fresh => write!(f, "fresh"),
RotStatus::Warning => write!(f, "warning"),
RotStatus::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Conversation {
pub id: ConversationId,
pub title: String,
pub messages: Vec<ChatMessage>,
pub global_context: Vec<ContextItem>,
pub role: RoleName,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub metadata: AHashMap<String, String>,
#[serde(default)]
pub token_budget: Option<usize>,
}
impl Conversation {
pub fn new(title: String, role: RoleName) -> Self {
let now = chrono::Utc::now();
Self {
id: ConversationId::new(),
title,
messages: Vec::new(),
global_context: Vec::new(),
role,
created_at: now,
updated_at: now,
metadata: AHashMap::new(),
token_budget: None,
}
}
pub fn with_token_budget(mut self, budget: usize) -> Self {
self.token_budget = Some(budget);
self
}
pub fn check_rot(&self) -> Option<RotStatus> {
let budget = self.token_budget?;
if budget == 0 {
return Some(RotStatus::Critical);
}
let current_size = self.estimated_context_length();
let ratio = current_size as f32 / budget as f32;
if ratio > 0.9 {
Some(RotStatus::Critical)
} else if ratio > 0.75 {
Some(RotStatus::Warning)
} else {
Some(RotStatus::Fresh)
}
}
pub fn add_message(&mut self, message: ChatMessage) {
self.messages.push(message);
self.updated_at = chrono::Utc::now();
}
pub fn add_global_context(&mut self, context: ContextItem) {
self.global_context.push(context);
self.updated_at = chrono::Utc::now();
}
pub fn estimated_context_length(&self) -> usize {
let message_length: usize = self
.messages
.iter()
.map(|m| {
m.content.len()
+ m.context_items
.iter()
.map(|c| c.content.len())
.sum::<usize>()
})
.sum();
let global_context_length: usize =
self.global_context.iter().map(|c| c.content.len()).sum();
message_length + global_context_length
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ConversationSummary {
pub id: ConversationId,
pub title: String,
pub role: RoleName,
pub message_count: usize,
pub context_count: usize,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub preview: Option<String>,
}
impl From<&Conversation> for ConversationSummary {
fn from(conversation: &Conversation) -> Self {
let context_count = conversation.global_context.len()
+ conversation
.messages
.iter()
.map(|m| m.context_items.len())
.sum::<usize>();
let preview = conversation
.messages
.iter()
.find(|m| m.role == "user")
.map(|m| preview(&m.content, 100, "..."));
Self {
id: conversation.id.clone(),
title: conversation.title.clone(),
role: conversation.role.clone(),
message_count: conversation.messages.len(),
context_count,
created_at: conversation.created_at,
updated_at: conversation.updated_at,
preview,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContextHistory {
pub used_contexts: Vec<ContextHistoryEntry>,
pub max_entries: usize,
}
impl ContextHistory {
pub fn new(max_entries: usize) -> Self {
Self {
used_contexts: Vec::new(),
max_entries,
}
}
pub fn record_usage(
&mut self,
context_id: &str,
conversation_id: &ConversationId,
usage_type: ContextUsageType,
) {
let entry = ContextHistoryEntry {
context_id: context_id.to_string(),
conversation_id: conversation_id.clone(),
usage_type,
used_at: chrono::Utc::now(),
usage_count: 1,
};
if let Some(existing) = self
.used_contexts
.iter_mut()
.find(|e| e.context_id == context_id && e.conversation_id == *conversation_id)
{
existing.usage_count += 1;
existing.used_at = chrono::Utc::now();
} else {
self.used_contexts.push(entry);
}
if self.used_contexts.len() > self.max_entries {
self.used_contexts.sort_by_key(|e| e.used_at);
self.used_contexts
.drain(0..self.used_contexts.len() - self.max_entries);
}
}
pub fn get_frequent_contexts(&self, limit: usize) -> Vec<&ContextHistoryEntry> {
let mut entries = self.used_contexts.iter().collect::<Vec<_>>();
entries.sort_by_key(|e| std::cmp::Reverse(e.usage_count));
entries.into_iter().take(limit).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ContextHistoryEntry {
pub context_id: String,
pub conversation_id: ConversationId,
pub usage_type: ContextUsageType,
pub used_at: chrono::DateTime<chrono::Utc>,
pub usage_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub enum ContextUsageType {
Manual,
Automatic,
SearchResult,
DocumentReference,
}