use ahash::AHashMap;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::iter::IntoIterator;
use std::ops::{Deref, DerefMut};
#[cfg(feature = "typescript")]
use tsify::Tsify;
use crate::graph::Edge;
use crate::route::RouteDirective;
use crate::validation::{ValidationError, validate_score};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DocumentType {
#[default]
KgEntry,
Document,
ConfigDocument,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkdownDirectives {
#[serde(default)]
pub doc_type: DocumentType,
#[serde(default)]
pub synonyms: Vec<String>,
#[serde(default)]
pub route: Option<RouteDirective>,
#[serde(default)]
pub routes: Vec<RouteDirective>,
#[serde(default)]
pub priority: Option<u8>,
#[serde(default)]
pub trigger: Option<String>,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub heading: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Document {
pub id: String,
pub url: String,
pub title: String,
pub body: String,
pub description: Option<String>,
pub summarization: Option<String>,
pub stub: Option<String>,
pub tags: Option<Vec<String>>,
pub rank: Option<u64>,
pub source_haystack: Option<String>,
#[serde(default)]
pub doc_type: DocumentType,
#[serde(default)]
pub synonyms: Option<Vec<String>>,
#[serde(default)]
pub route: Option<RouteDirective>,
#[serde(default)]
pub priority: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quality_score: Option<QualityScore>,
}
impl fmt::Display for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.title, self.body)?;
if let Some(ref description) = self.description {
write!(f, " {}", description)?;
}
if let Some(ref summarization) = self.summarization
&& Some(summarization) != self.description.as_ref()
{
write!(f, " {}", summarization)?;
}
Ok(())
}
}
impl Document {
pub fn with_source_haystack(mut self, haystack_location: String) -> Self {
self.source_haystack = Some(haystack_location);
self
}
pub fn get_source_haystack(&self) -> Option<&String> {
self.source_haystack.as_ref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Index {
inner: AHashMap<String, Document>,
}
impl Default for Index {
fn default() -> Self {
Self::new()
}
}
impl Index {
pub fn new() -> Self {
Self {
inner: AHashMap::new(),
}
}
pub fn get_documents(&self, docs: Vec<IndexedDocument>) -> Vec<Document> {
let mut documents: Vec<Document> = Vec::new();
for doc in docs {
log::trace!("doc: {:#?}", doc);
if let Some(document) = self.get_document(&doc) {
let mut document = document;
document.tags = Some(doc.tags.clone());
document.rank = Some(doc.rank);
document.quality_score = doc.quality_score.clone();
documents.push(document);
} else {
log::warn!("Document not found in cache. Cannot convert.");
}
}
documents
}
pub fn get_all_documents(&self) -> Vec<Document> {
let documents: Vec<Document> = self.values().cloned().collect::<Vec<Document>>();
documents
}
pub fn get_document(&self, doc: &IndexedDocument) -> Option<Document> {
if let Some(document) = self.inner.get(&doc.id).cloned() {
let mut document = document;
document.tags = Some(doc.tags.clone());
document.rank = Some(doc.rank);
document.quality_score = doc.quality_score.clone();
Some(document)
} else {
None
}
}
}
impl Deref for Index {
type Target = AHashMap<String, Document>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Index {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl IntoIterator for Index {
type Item = (String, Document);
type IntoIter = std::collections::hash_map::IntoIter<String, Document>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct QualityScore {
pub knowledge: Option<f64>,
pub logic: Option<f64>,
pub structure: Option<f64>,
pub last_evaluated: Option<chrono::DateTime<chrono::Utc>>,
}
impl QualityScore {
pub fn try_new(
knowledge: Option<f64>,
logic: Option<f64>,
structure: Option<f64>,
last_evaluated: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<Self, ValidationError> {
Ok(Self {
knowledge: knowledge
.map(|v| validate_score("knowledge", v))
.transpose()?,
logic: logic.map(|v| validate_score("logic", v)).transpose()?,
structure: structure
.map(|v| validate_score("structure", v))
.transpose()?,
last_evaluated,
})
}
pub fn composite(&self) -> f64 {
let mut sum = 0.0;
let mut count = 0;
if let Some(k) = self.knowledge {
sum += k;
count += 1;
}
if let Some(l) = self.logic {
sum += l;
count += 1;
}
if let Some(s) = self.structure {
sum += s;
count += 1;
}
if count == 0 { 0.0 } else { sum / count as f64 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IndexedDocument {
pub id: String,
pub matched_edges: Vec<Edge>,
pub rank: u64,
pub tags: Vec<String>,
pub nodes: Vec<u64>,
#[serde(default)]
pub quality_score: Option<QualityScore>,
}
impl IndexedDocument {
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
}
pub fn from_document(document: Document) -> Self {
IndexedDocument {
id: document.id,
matched_edges: Vec::new(),
rank: 0,
tags: document.tags.unwrap_or_default(),
nodes: Vec::new(),
quality_score: None,
}
}
}
pub fn extract_first_paragraph(body: &str) -> String {
let content = if body.trim_start().starts_with("---") {
if let Some(end_pos) = body[3..].find("---") {
&body[end_pos + 6..] } else {
body
}
} else {
body
};
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
String::new()
}