1#[cfg(feature = "medical")]
83pub mod medical_types;
84#[cfg(feature = "medical")]
85pub use medical_types::*;
86
87#[cfg(feature = "hgnc")]
89pub mod hgnc;
90
91pub mod capability;
93pub use capability::*;
94
95pub mod mcp_tool;
97pub use mcp_tool::*;
98
99pub mod procedure;
101pub use procedure::*;
102
103pub mod persona;
105pub use persona::{CharacteristicDef, PersonaDefinition, PersonaLoadError, SfiaSkillDef};
106
107use ahash::AHashMap;
108use serde::{Deserialize, Deserializer, Serialize, Serializer};
109use std::collections::HashSet;
110use std::collections::hash_map::Iter;
111use std::fmt::{self, Display, Formatter};
112use std::iter::IntoIterator;
113use std::ops::{Deref, DerefMut};
114use std::sync::atomic::{AtomicU64, Ordering};
115static INT_SEQ: AtomicU64 = AtomicU64::new(1);
116fn get_int_id() -> u64 {
117 INT_SEQ.fetch_add(1, Ordering::SeqCst)
118}
119
120use schemars::JsonSchema;
121use std::str::FromStr;
122#[cfg(feature = "typescript")]
123use tsify::Tsify;
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, JsonSchema)]
149#[cfg_attr(feature = "typescript", derive(Tsify))]
150#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
151pub struct RoleName {
152 pub original: String,
154 pub lowercase: String,
156}
157
158impl RoleName {
159 pub fn new(name: &str) -> Self {
173 RoleName {
174 original: name.to_string(),
175 lowercase: name.to_lowercase(),
176 }
177 }
178
179 pub fn as_lowercase(&self) -> &str {
183 &self.lowercase
184 }
185
186 pub fn as_str(&self) -> &str {
188 &self.original
189 }
190}
191
192impl fmt::Display for RoleName {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "{}", self.original)
195 }
196}
197
198impl FromStr for RoleName {
199 type Err = ();
200
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
202 Ok(RoleName::new(s))
203 }
204}
205
206impl From<&str> for RoleName {
207 fn from(s: &str) -> Self {
208 RoleName::new(s)
209 }
210}
211
212impl From<String> for RoleName {
213 fn from(s: String) -> Self {
214 RoleName::new(&s)
215 }
216}
217
218impl Serialize for RoleName {
219 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
220 where
221 S: Serializer,
222 {
223 serializer.serialize_str(&self.original)
224 }
225}
226
227impl<'de> Deserialize<'de> for RoleName {
228 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229 where
230 D: Deserializer<'de>,
231 {
232 let s = String::deserialize(deserializer)?;
233 Ok(RoleName::new(&s))
234 }
235}
236#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
240#[cfg_attr(feature = "typescript", derive(Tsify))]
241#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
242pub struct NormalizedTermValue(String);
243
244impl NormalizedTermValue {
245 pub fn new(term: String) -> Self {
246 let value = term.trim().to_lowercase();
247 Self(value)
248 }
249 pub fn as_str(&self) -> &str {
251 &self.0
252 }
253}
254
255impl From<String> for NormalizedTermValue {
256 fn from(term: String) -> Self {
257 Self::new(term)
258 }
259}
260
261impl From<&str> for NormalizedTermValue {
262 fn from(term: &str) -> Self {
263 Self::new(term.to_string())
264 }
265}
266
267impl Display for NormalizedTermValue {
268 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
269 write!(f, "{}", self.0)
270 }
271}
272
273impl AsRef<[u8]> for NormalizedTermValue {
274 fn as_ref(&self) -> &[u8] {
275 self.0.as_bytes()
276 }
277}
278
279#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
284pub struct NormalizedTerm {
285 pub id: u64,
287 #[serde(rename = "nterm")]
290 pub value: NormalizedTermValue,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub display_value: Option<String>,
295 pub url: Option<String>,
297}
298
299impl NormalizedTerm {
300 pub fn new(id: u64, value: NormalizedTermValue) -> Self {
303 Self {
304 id,
305 value,
306 display_value: None,
307 url: None,
308 }
309 }
310
311 pub fn with_auto_id(value: NormalizedTermValue) -> Self {
314 Self {
315 id: get_int_id(),
316 value,
317 display_value: None,
318 url: None,
319 }
320 }
321
322 pub fn with_display_value(mut self, display_value: String) -> Self {
325 self.display_value = Some(display_value);
326 self
327 }
328
329 pub fn with_url(mut self, url: String) -> Self {
331 self.url = Some(url);
332 self
333 }
334
335 pub fn display(&self) -> &str {
338 self.display_value
339 .as_deref()
340 .unwrap_or_else(|| self.value.as_str())
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
351pub struct Concept {
352 pub id: u64,
354 pub value: NormalizedTermValue,
356}
357
358impl Concept {
359 pub fn new(value: NormalizedTermValue) -> Self {
361 Self {
362 id: get_int_id(),
363 value,
364 }
365 }
366
367 pub fn with_id(id: u64, value: NormalizedTermValue) -> Self {
369 Self { id, value }
370 }
371}
372
373impl From<String> for Concept {
374 fn from(concept: String) -> Self {
375 let concept = NormalizedTermValue::new(concept);
376 Self::new(concept)
377 }
378}
379
380impl Display for Concept {
381 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
382 write!(f, "{}", self.value)
383 }
384}
385
386#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum DocumentType {
389 #[default]
390 KgEntry,
391 Document,
392 ConfigDocument,
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct RouteDirective {
397 pub provider: String,
398 pub model: String,
399}
400
401#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
402pub struct MarkdownDirectives {
403 #[serde(default)]
404 pub doc_type: DocumentType,
405 #[serde(default)]
406 pub synonyms: Vec<String>,
407 #[serde(default)]
408 pub route: Option<RouteDirective>,
409 #[serde(default)]
410 pub priority: Option<u8>,
411 #[serde(default)]
412 pub trigger: Option<String>,
413 #[serde(default)]
414 pub pinned: bool,
415 #[serde(default)]
417 pub heading: Option<String>,
418}
419
420#[derive(Deserialize, Serialize, Debug, Clone, Default)]
462#[cfg_attr(feature = "typescript", derive(Tsify))]
463#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
464pub struct Document {
465 pub id: String,
467 pub url: String,
469 pub title: String,
471 pub body: String,
473
474 pub description: Option<String>,
476 pub summarization: Option<String>,
478 pub stub: Option<String>,
480 pub tags: Option<Vec<String>>,
482 pub rank: Option<u64>,
484 pub source_haystack: Option<String>,
486 #[serde(default)]
488 pub doc_type: DocumentType,
489 #[serde(default)]
491 pub synonyms: Option<Vec<String>>,
492 #[serde(default)]
494 pub route: Option<RouteDirective>,
495 #[serde(default)]
497 pub priority: Option<u8>,
498}
499
500impl fmt::Display for Document {
501 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502 write!(f, "{} {}", self.title, self.body)?;
504
505 if let Some(ref description) = self.description {
507 write!(f, " {}", description)?;
508 }
509
510 if let Some(ref summarization) = self.summarization {
512 if Some(summarization) != self.description.as_ref() {
513 write!(f, " {}", summarization)?;
514 }
515 }
516
517 Ok(())
518 }
519}
520
521impl Document {
522 pub fn with_source_haystack(mut self, haystack_location: String) -> Self {
524 self.source_haystack = Some(haystack_location);
525 self
526 }
527
528 pub fn get_source_haystack(&self) -> Option<&String> {
530 self.source_haystack.as_ref()
531 }
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535pub struct Edge {
536 pub id: u64,
538 pub rank: u64,
540 pub doc_hash: AHashMap<String, u64>,
542 #[cfg(feature = "medical")]
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub edge_type: Option<medical_types::MedicalEdgeType>,
546}
547
548impl Edge {
549 pub fn new(id: u64, document_id: String) -> Self {
550 let mut doc_hash = AHashMap::new();
551 doc_hash.insert(document_id, 1);
552 Self {
553 id,
554 rank: 1,
555 doc_hash,
556 #[cfg(feature = "medical")]
557 edge_type: None,
558 }
559 }
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
566pub struct Node {
567 pub id: u64,
569 pub rank: u64,
571 pub connected_with: HashSet<u64>,
573 #[cfg(feature = "medical")]
575 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub node_type: Option<medical_types::MedicalNodeType>,
577 #[cfg(feature = "medical")]
579 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub term: Option<String>,
581 #[cfg(feature = "medical")]
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub snomed_id: Option<u64>,
585}
586
587impl Node {
588 pub fn new(id: u64, edge: Edge) -> Self {
590 let mut connected_with = HashSet::new();
591 connected_with.insert(edge.id);
592 Self {
593 id,
594 rank: 1,
595 connected_with,
596 #[cfg(feature = "medical")]
597 node_type: None,
598 #[cfg(feature = "medical")]
599 term: None,
600 #[cfg(feature = "medical")]
601 snomed_id: None,
602 }
603 }
604
605 }
614
615#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
617pub struct Thesaurus {
618 name: String,
620 data: AHashMap<NormalizedTermValue, NormalizedTerm>,
622}
623
624impl Thesaurus {
625 pub fn new(name: String) -> Self {
627 Self {
628 name,
629 data: AHashMap::new(),
630 }
631 }
632
633 pub fn name(&self) -> &str {
635 &self.name
636 }
637
638 pub fn insert(&mut self, key: NormalizedTermValue, value: NormalizedTerm) {
640 self.data.insert(key, value);
641 }
642
643 pub fn len(&self) -> usize {
645 self.data.len()
646 }
647
648 pub fn is_empty(&self) -> bool {
650 self.data.is_empty()
651 }
652
653 pub fn get(&self, key: &NormalizedTermValue) -> Option<&NormalizedTerm> {
657 self.data.get(key)
658 }
659
660 pub fn keys(
661 &self,
662 ) -> std::collections::hash_map::Keys<'_, NormalizedTermValue, NormalizedTerm> {
663 self.data.keys()
664 }
665}
666
667impl<'a> IntoIterator for &'a Thesaurus {
669 type Item = (&'a NormalizedTermValue, &'a NormalizedTerm);
670 type IntoIter = Iter<'a, NormalizedTermValue, NormalizedTerm>;
671
672 fn into_iter(self) -> Self::IntoIter {
673 self.data.iter()
674 }
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct Index {
683 inner: AHashMap<String, Document>,
684}
685
686impl Default for Index {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692impl Index {
693 pub fn new() -> Self {
695 Self {
696 inner: AHashMap::new(),
697 }
698 }
699
700 pub fn get_documents(&self, docs: Vec<IndexedDocument>) -> Vec<Document> {
704 let mut documents: Vec<Document> = Vec::new();
705 for doc in docs {
706 log::trace!("doc: {:#?}", doc);
707 if let Some(document) = self.get_document(&doc) {
708 let mut document = document;
710 document.tags = Some(doc.tags.clone());
711 document.rank = Some(doc.rank);
714 documents.push(document.clone());
715 } else {
716 log::warn!("Document not found in cache. Cannot convert.");
717 }
718 }
719 documents
720 }
721 pub fn get_all_documents(&self) -> Vec<Document> {
723 let documents: Vec<Document> = self.values().cloned().collect::<Vec<Document>>();
724 documents
725 }
726
727 pub fn get_document(&self, doc: &IndexedDocument) -> Option<Document> {
729 if let Some(document) = self.inner.get(&doc.id).cloned() {
730 let mut document = document;
732 document.tags = Some(doc.tags.clone());
733 document.rank = Some(doc.rank);
736 Some(document)
737 } else {
738 None
739 }
740 }
741}
742
743impl Deref for Index {
744 type Target = AHashMap<String, Document>;
745
746 fn deref(&self) -> &Self::Target {
747 &self.inner
748 }
749}
750
751impl DerefMut for Index {
752 fn deref_mut(&mut self) -> &mut Self::Target {
753 &mut self.inner
754 }
755}
756
757impl IntoIterator for Index {
758 type Item = (String, Document);
759 type IntoIter = std::collections::hash_map::IntoIter<String, Document>;
760
761 fn into_iter(self) -> Self::IntoIter {
762 self.inner.into_iter()
763 }
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
775pub struct QualityScore {
776 pub knowledge: Option<f64>,
778 pub learning: Option<f64>,
780 pub synthesis: Option<f64>,
782}
783
784impl QualityScore {
785 pub fn composite(&self) -> f64 {
805 let mut sum = 0.0;
806 let mut count = 0;
807
808 if let Some(k) = self.knowledge {
809 sum += k;
810 count += 1;
811 }
812 if let Some(l) = self.learning {
813 sum += l;
814 count += 1;
815 }
816 if let Some(s) = self.synthesis {
817 sum += s;
818 count += 1;
819 }
820
821 if count == 0 { 0.0 } else { sum / count as f64 }
822 }
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
827pub struct IndexedDocument {
828 pub id: String,
830 pub matched_edges: Vec<Edge>,
832 pub rank: u64,
835 pub tags: Vec<String>,
837 pub nodes: Vec<u64>,
839 #[serde(default)]
841 pub quality_score: Option<QualityScore>,
842}
843
844impl IndexedDocument {
845 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
846 serde_json::to_string(&self)
847 }
848 pub fn from_document(document: Document) -> Self {
849 IndexedDocument {
850 id: document.id,
851 matched_edges: Vec::new(),
852 rank: 0,
853 tags: document.tags.unwrap_or_default(),
854 nodes: Vec::new(),
855 quality_score: None,
856 }
857 }
858}
859
860#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
862#[cfg_attr(feature = "typescript", derive(Tsify))]
863#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
864pub enum LogicalOperator {
865 #[serde(rename = "and")]
867 And,
868 #[serde(rename = "or")]
870 Or,
871}
872
873#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, JsonSchema)]
880#[cfg_attr(feature = "typescript", derive(Tsify))]
881#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
882pub enum Layer {
883 #[serde(rename = "1")]
885 #[default]
886 One,
887 #[serde(rename = "2")]
889 Two,
890 #[serde(rename = "3")]
892 Three,
893}
894
895impl Layer {
896 pub fn from_u8(value: u8) -> Option<Self> {
898 match value {
899 1 => Some(Layer::One),
900 2 => Some(Layer::Two),
901 3 => Some(Layer::Three),
902 _ => None,
903 }
904 }
905
906 pub fn includes_content(&self) -> bool {
908 matches!(self, Layer::Two | Layer::Three)
909 }
910
911 pub fn includes_full_content(&self) -> bool {
913 matches!(self, Layer::Three)
914 }
915}
916
917impl std::fmt::Display for Layer {
918 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
919 match self {
920 Layer::One => write!(f, "1"),
921 Layer::Two => write!(f, "2"),
922 Layer::Three => write!(f, "3"),
923 }
924 }
925}
926
927pub fn extract_first_paragraph(body: &str) -> String {
932 let content = if body.trim_start().starts_with("---") {
934 if let Some(end_pos) = body[3..].find("---") {
936 &body[end_pos + 6..] } else {
938 body
939 }
940 } else {
941 body
942 };
943
944 for line in content.lines() {
946 let trimmed = line.trim();
947 if !trimmed.is_empty() {
948 return trimmed.to_string();
949 }
950 }
951
952 String::new()
954}
955
956#[derive(Debug, Serialize, Deserialize, Clone, Default)]
994#[cfg_attr(feature = "typescript", derive(Tsify))]
995#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
996pub struct SearchQuery {
997 #[serde(alias = "query")]
999 pub search_term: NormalizedTermValue,
1000 pub search_terms: Option<Vec<NormalizedTermValue>>,
1002 pub operator: Option<LogicalOperator>,
1004 pub skip: Option<usize>,
1006 pub limit: Option<usize>,
1008 pub role: Option<RoleName>,
1010 #[serde(default)]
1012 pub layer: Layer,
1013}
1014
1015impl SearchQuery {
1016 pub fn get_all_terms(&self) -> Vec<&NormalizedTermValue> {
1018 if let Some(ref multiple_terms) = self.search_terms {
1019 let mut all_terms: Vec<&NormalizedTermValue> =
1022 Vec::with_capacity(1 + multiple_terms.len());
1023 all_terms.push(&self.search_term);
1024
1025 for term in multiple_terms.iter() {
1026 if term.as_str() != self.search_term.as_str() {
1027 all_terms.push(term);
1028 }
1029 }
1030
1031 all_terms
1032 } else {
1033 vec![&self.search_term]
1035 }
1036 }
1037
1038 pub fn is_multi_term_query(&self) -> bool {
1040 self.search_terms.is_some() && !self.search_terms.as_ref().unwrap().is_empty()
1041 }
1042
1043 pub fn get_operator(&self) -> LogicalOperator {
1045 self.operator
1046 .as_ref()
1047 .unwrap_or(&LogicalOperator::Or)
1048 .clone()
1049 }
1050
1051 pub fn with_terms_and_operator(
1053 primary_term: NormalizedTermValue,
1054 additional_terms: Vec<NormalizedTermValue>,
1055 operator: LogicalOperator,
1056 role: Option<RoleName>,
1057 ) -> Self {
1058 Self {
1059 search_term: primary_term,
1060 search_terms: Some(additional_terms),
1061 operator: Some(operator),
1062 skip: None,
1063 limit: None,
1064 role,
1065 layer: Layer::default(),
1066 }
1067 }
1068}
1069
1070#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy, JsonSchema, Default)]
1073#[cfg_attr(feature = "typescript", derive(Tsify))]
1074#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1075pub enum RelevanceFunction {
1076 #[serde(rename = "terraphim-graph")]
1082 TerraphimGraph,
1083 #[default]
1085 #[serde(rename = "title-scorer")]
1086 TitleScorer,
1087 #[serde(rename = "bm25")]
1089 BM25,
1090 #[serde(rename = "bm25f")]
1092 BM25F,
1093 #[serde(rename = "bm25plus")]
1095 BM25Plus,
1096}
1097
1098#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
1103#[cfg_attr(feature = "typescript", derive(Tsify))]
1104#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1105pub enum KnowledgeGraphInputType {
1106 #[serde(rename = "markdown")]
1108 Markdown,
1109 #[serde(rename = "json")]
1111 Json,
1112}
1113
1114#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1118#[cfg_attr(feature = "typescript", derive(Tsify))]
1119#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1120pub struct ConversationId(pub String);
1121
1122impl ConversationId {
1123 pub fn new() -> Self {
1124 Self(uuid::Uuid::new_v4().to_string())
1125 }
1126
1127 pub fn from_string(id: String) -> Self {
1128 Self(id)
1129 }
1130
1131 pub fn as_str(&self) -> &str {
1132 &self.0
1133 }
1134}
1135
1136impl Default for ConversationId {
1137 fn default() -> Self {
1138 Self::new()
1139 }
1140}
1141
1142impl Display for ConversationId {
1143 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1144 write!(f, "{}", self.0)
1145 }
1146}
1147
1148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1150#[cfg_attr(feature = "typescript", derive(Tsify))]
1151#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1152pub enum ContextType {
1153 System,
1155 UserInput,
1157 Document,
1159 SearchResult,
1161 External,
1163 KGTermDefinition,
1165 KGIndex,
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1171#[cfg_attr(feature = "typescript", derive(Tsify))]
1172#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1173pub struct MessageId(pub String);
1174
1175impl MessageId {
1176 pub fn new() -> Self {
1177 Self(uuid::Uuid::new_v4().to_string())
1178 }
1179
1180 pub fn from_string(id: String) -> Self {
1181 Self(id)
1182 }
1183
1184 pub fn as_str(&self) -> &str {
1185 &self.0
1186 }
1187}
1188
1189impl Default for MessageId {
1190 fn default() -> Self {
1191 Self::new()
1192 }
1193}
1194
1195impl Display for MessageId {
1196 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1197 write!(f, "{}", self.0)
1198 }
1199}
1200
1201#[derive(Debug, Clone, Serialize, Deserialize)]
1203#[cfg_attr(feature = "typescript", derive(Tsify))]
1204#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1205pub struct ContextItem {
1206 pub id: String,
1208 pub context_type: ContextType,
1210 pub title: String,
1212 pub summary: Option<String>,
1214 pub content: String,
1216 pub metadata: AHashMap<String, String>,
1218 pub created_at: chrono::DateTime<chrono::Utc>,
1220 pub relevance_score: Option<f64>,
1222}
1223
1224impl ContextItem {
1225 pub fn from_document(document: &Document) -> Self {
1227 let mut metadata = AHashMap::new();
1228 metadata.insert("source_type".to_string(), "document".to_string());
1229 metadata.insert("document_id".to_string(), document.id.clone());
1230 if !document.url.is_empty() {
1231 metadata.insert("url".to_string(), document.url.clone());
1232 }
1233 if let Some(tags) = &document.tags {
1234 metadata.insert("tags".to_string(), tags.join(", "));
1235 }
1236 if let Some(rank) = document.rank {
1237 metadata.insert("rank".to_string(), rank.to_string());
1238 }
1239
1240 Self {
1241 id: uuid::Uuid::new_v4().to_string(),
1242 context_type: ContextType::Document,
1243 title: if document.title.is_empty() {
1244 document.id.clone()
1245 } else {
1246 document.title.clone()
1247 },
1248 summary: document.description.clone(),
1249 content: format!(
1250 "Title: {}\n\n{}\n\n{}",
1251 document.title,
1252 document.description.as_deref().unwrap_or(""),
1253 document.body
1254 ),
1255 metadata,
1256 created_at: chrono::Utc::now(),
1257 relevance_score: document.rank.map(|r| r as f64),
1258 }
1259 }
1260
1261 pub fn from_search_result(query: &str, documents: &[Document]) -> Self {
1263 let mut metadata = AHashMap::new();
1264 metadata.insert("source_type".to_string(), "search_result".to_string());
1265 metadata.insert("query".to_string(), query.to_string());
1266 metadata.insert("result_count".to_string(), documents.len().to_string());
1267
1268 let content = if documents.is_empty() {
1269 format!("Search query: '{}'\nNo results found.", query)
1270 } else {
1271 let mut content = format!("Search query: '{}'\nResults:\n\n", query);
1272 for (i, doc) in documents.iter().take(5).enumerate() {
1273 content.push_str(&format!(
1274 "{}. {}\n {}\n Rank: {}\n\n",
1275 i + 1,
1276 doc.title,
1277 doc.description.as_deref().unwrap_or("No description"),
1278 doc.rank.unwrap_or(0)
1279 ));
1280 }
1281 if documents.len() > 5 {
1282 content.push_str(&format!("... and {} more results\n", documents.len() - 5));
1283 }
1284 content
1285 };
1286
1287 Self {
1288 id: uuid::Uuid::new_v4().to_string(),
1289 context_type: ContextType::Document, title: format!("Search: {}", query),
1291 summary: Some(format!(
1292 "Search results for '{}' - {} documents found",
1293 query,
1294 documents.len()
1295 )),
1296 content,
1297 metadata,
1298 created_at: chrono::Utc::now(),
1299 relevance_score: documents.first().and_then(|d| d.rank.map(|r| r as f64)),
1300 }
1301 }
1302
1303 pub fn from_kg_term_definition(kg_term: &KGTermDefinition) -> Self {
1305 let mut metadata = AHashMap::new();
1306 metadata.insert("source_type".to_string(), "kg_term".to_string());
1307 metadata.insert("term_id".to_string(), kg_term.id.to_string());
1308 metadata.insert(
1309 "normalized_term".to_string(),
1310 kg_term.normalized_term.to_string(),
1311 );
1312 metadata.insert(
1313 "synonyms_count".to_string(),
1314 kg_term.synonyms.len().to_string(),
1315 );
1316 metadata.insert(
1317 "related_terms_count".to_string(),
1318 kg_term.related_terms.len().to_string(),
1319 );
1320 metadata.insert(
1321 "usage_examples_count".to_string(),
1322 kg_term.usage_examples.len().to_string(),
1323 );
1324
1325 if let Some(ref url) = kg_term.url {
1326 metadata.insert("url".to_string(), url.clone());
1327 }
1328
1329 for (key, value) in &kg_term.metadata {
1331 metadata.insert(format!("kg_{}", key), value.clone());
1332 }
1333
1334 let mut content = format!("**Term:** {}\n", kg_term.term);
1335
1336 if let Some(ref definition) = kg_term.definition {
1337 content.push_str(&format!("**Definition:** {}\n", definition));
1338 }
1339
1340 if !kg_term.synonyms.is_empty() {
1341 content.push_str(&format!("**Synonyms:** {}\n", kg_term.synonyms.join(", ")));
1342 }
1343
1344 if !kg_term.related_terms.is_empty() {
1345 content.push_str(&format!(
1346 "**Related Terms:** {}\n",
1347 kg_term.related_terms.join(", ")
1348 ));
1349 }
1350
1351 if !kg_term.usage_examples.is_empty() {
1352 content.push_str("**Usage Examples:**\n");
1353 for (i, example) in kg_term.usage_examples.iter().enumerate() {
1354 content.push_str(&format!("{}. {}\n", i + 1, example));
1355 }
1356 }
1357
1358 Self {
1359 id: uuid::Uuid::new_v4().to_string(),
1360 context_type: ContextType::KGTermDefinition,
1361 title: format!("KG Term: {}", kg_term.term),
1362 summary: Some(format!(
1363 "Knowledge Graph term '{}' with {} synonyms and {} related terms",
1364 kg_term.term,
1365 kg_term.synonyms.len(),
1366 kg_term.related_terms.len()
1367 )),
1368 content,
1369 metadata,
1370 created_at: chrono::Utc::now(),
1371 relevance_score: kg_term.relevance_score,
1372 }
1373 }
1374
1375 pub fn from_kg_index(kg_index: &KGIndexInfo) -> Self {
1377 let mut metadata = AHashMap::new();
1378 metadata.insert("source_type".to_string(), "kg_index".to_string());
1379 metadata.insert("kg_name".to_string(), kg_index.name.clone());
1380 metadata.insert("total_terms".to_string(), kg_index.total_terms.to_string());
1381 metadata.insert("total_nodes".to_string(), kg_index.total_nodes.to_string());
1382 metadata.insert("total_edges".to_string(), kg_index.total_edges.to_string());
1383 metadata.insert("source".to_string(), kg_index.source.clone());
1384 metadata.insert(
1385 "last_updated".to_string(),
1386 kg_index.last_updated.to_rfc3339(),
1387 );
1388
1389 if let Some(ref version) = kg_index.version {
1390 metadata.insert("version".to_string(), version.clone());
1391 }
1392
1393 let content = format!(
1394 "**Knowledge Graph Index: {}**\n\n\
1395 **Statistics:**\n\
1396 - Total Terms: {}\n\
1397 - Total Nodes: {}\n\
1398 - Total Edges: {}\n\
1399 - Source: {}\n\
1400 - Last Updated: {}\n\
1401 - Version: {}\n\n\
1402 This context includes the complete knowledge graph index with all terms, \
1403 relationships, and metadata available for reference.",
1404 kg_index.name,
1405 kg_index.total_terms,
1406 kg_index.total_nodes,
1407 kg_index.total_edges,
1408 kg_index.source,
1409 kg_index.last_updated.format("%Y-%m-%d %H:%M:%S UTC"),
1410 kg_index.version.as_deref().unwrap_or("N/A")
1411 );
1412
1413 Self {
1414 id: uuid::Uuid::new_v4().to_string(),
1415 context_type: ContextType::KGIndex,
1416 title: format!("KG Index: {}", kg_index.name),
1417 summary: Some(format!(
1418 "Complete knowledge graph index with {} terms, {} nodes, and {} edges",
1419 kg_index.total_terms, kg_index.total_nodes, kg_index.total_edges
1420 )),
1421 content,
1422 metadata,
1423 created_at: chrono::Utc::now(),
1424 relevance_score: Some(1.0), }
1426 }
1427}
1428
1429#[derive(Debug, Clone, Serialize, Deserialize)]
1431#[cfg_attr(feature = "typescript", derive(Tsify))]
1432#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1433pub struct KGTermDefinition {
1434 pub term: String,
1436 pub normalized_term: NormalizedTermValue,
1438 pub id: u64,
1440 pub definition: Option<String>,
1442 pub synonyms: Vec<String>,
1444 pub related_terms: Vec<String>,
1446 pub usage_examples: Vec<String>,
1448 pub url: Option<String>,
1450 pub metadata: AHashMap<String, String>,
1452 pub relevance_score: Option<f64>,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1458#[cfg_attr(feature = "typescript", derive(Tsify))]
1459#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1460pub struct KGIndexInfo {
1461 pub name: String,
1463 pub total_terms: usize,
1465 pub total_nodes: usize,
1467 pub total_edges: usize,
1469 pub last_updated: chrono::DateTime<chrono::Utc>,
1471 pub source: String,
1473 pub version: Option<String>,
1475}
1476
1477#[derive(Debug, Clone, Serialize, Deserialize)]
1478#[cfg_attr(feature = "typescript", derive(Tsify))]
1479#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1480pub struct ChatMessage {
1481 pub id: MessageId,
1483 pub role: String, pub content: String,
1487 pub context_items: Vec<ContextItem>,
1489 pub created_at: chrono::DateTime<chrono::Utc>,
1491 pub token_count: Option<u32>,
1493 pub model: Option<String>,
1495}
1496
1497impl ChatMessage {
1498 pub fn user(content: String) -> Self {
1500 Self {
1501 id: MessageId::new(),
1502 role: "user".to_string(),
1503 content,
1504 context_items: Vec::new(),
1505 created_at: chrono::Utc::now(),
1506 token_count: None,
1507 model: None,
1508 }
1509 }
1510
1511 pub fn assistant(content: String, model: Option<String>) -> Self {
1513 Self {
1514 id: MessageId::new(),
1515 role: "assistant".to_string(),
1516 content,
1517 context_items: Vec::new(),
1518 created_at: chrono::Utc::now(),
1519 token_count: None,
1520 model,
1521 }
1522 }
1523
1524 pub fn system(content: String) -> Self {
1526 Self {
1527 id: MessageId::new(),
1528 role: "system".to_string(),
1529 content,
1530 context_items: Vec::new(),
1531 created_at: chrono::Utc::now(),
1532 token_count: None,
1533 model: None,
1534 }
1535 }
1536
1537 pub fn add_context(&mut self, context: ContextItem) {
1539 self.context_items.push(context);
1540 }
1541
1542 pub fn add_contexts(&mut self, contexts: Vec<ContextItem>) {
1544 self.context_items.extend(contexts);
1545 }
1546}
1547
1548#[derive(Debug, Clone, Serialize, Deserialize)]
1550#[cfg_attr(feature = "typescript", derive(Tsify))]
1551#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1552pub struct Conversation {
1553 pub id: ConversationId,
1555 pub title: String,
1557 pub messages: Vec<ChatMessage>,
1559 pub global_context: Vec<ContextItem>,
1561 pub role: RoleName,
1563 pub created_at: chrono::DateTime<chrono::Utc>,
1565 pub updated_at: chrono::DateTime<chrono::Utc>,
1567 pub metadata: AHashMap<String, String>,
1569}
1570
1571impl Conversation {
1572 pub fn new(title: String, role: RoleName) -> Self {
1574 let now = chrono::Utc::now();
1575 Self {
1576 id: ConversationId::new(),
1577 title,
1578 messages: Vec::new(),
1579 global_context: Vec::new(),
1580 role,
1581 created_at: now,
1582 updated_at: now,
1583 metadata: AHashMap::new(),
1584 }
1585 }
1586
1587 pub fn add_message(&mut self, message: ChatMessage) {
1589 self.messages.push(message);
1590 self.updated_at = chrono::Utc::now();
1591 }
1592
1593 pub fn add_global_context(&mut self, context: ContextItem) {
1595 self.global_context.push(context);
1596 self.updated_at = chrono::Utc::now();
1597 }
1598
1599 pub fn estimated_context_length(&self) -> usize {
1601 let message_length: usize = self
1602 .messages
1603 .iter()
1604 .map(|m| {
1605 m.content.len()
1606 + m.context_items
1607 .iter()
1608 .map(|c| c.content.len())
1609 .sum::<usize>()
1610 })
1611 .sum();
1612 let global_context_length: usize =
1613 self.global_context.iter().map(|c| c.content.len()).sum();
1614 message_length + global_context_length
1615 }
1616}
1617
1618#[derive(Debug, Clone, Serialize, Deserialize)]
1620#[cfg_attr(feature = "typescript", derive(Tsify))]
1621#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1622pub struct ConversationSummary {
1623 pub id: ConversationId,
1625 pub title: String,
1627 pub role: RoleName,
1629 pub message_count: usize,
1631 pub context_count: usize,
1633 pub created_at: chrono::DateTime<chrono::Utc>,
1635 pub updated_at: chrono::DateTime<chrono::Utc>,
1637 pub preview: Option<String>,
1639}
1640
1641impl From<&Conversation> for ConversationSummary {
1645 fn from(conversation: &Conversation) -> Self {
1646 let context_count = conversation.global_context.len()
1647 + conversation
1648 .messages
1649 .iter()
1650 .map(|m| m.context_items.len())
1651 .sum::<usize>();
1652
1653 let preview = conversation
1654 .messages
1655 .iter()
1656 .find(|m| m.role == "user")
1657 .map(|m| {
1658 if m.content.len() > 100 {
1659 format!("{}...", &m.content[..100])
1660 } else {
1661 m.content.clone()
1662 }
1663 });
1664
1665 Self {
1666 id: conversation.id.clone(),
1667 title: conversation.title.clone(),
1668 role: conversation.role.clone(),
1669 message_count: conversation.messages.len(),
1670 context_count,
1671 created_at: conversation.created_at,
1672 updated_at: conversation.updated_at,
1673 preview,
1674 }
1675 }
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize)]
1680#[cfg_attr(feature = "typescript", derive(Tsify))]
1681#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1682pub struct ContextHistory {
1683 pub used_contexts: Vec<ContextHistoryEntry>,
1685 pub max_entries: usize,
1687}
1688
1689impl ContextHistory {
1690 pub fn new(max_entries: usize) -> Self {
1691 Self {
1692 used_contexts: Vec::new(),
1693 max_entries,
1694 }
1695 }
1696
1697 pub fn record_usage(
1699 &mut self,
1700 context_id: &str,
1701 conversation_id: &ConversationId,
1702 usage_type: ContextUsageType,
1703 ) {
1704 let entry = ContextHistoryEntry {
1705 context_id: context_id.to_string(),
1706 conversation_id: conversation_id.clone(),
1707 usage_type,
1708 used_at: chrono::Utc::now(),
1709 usage_count: 1,
1710 };
1711
1712 if let Some(existing) = self
1714 .used_contexts
1715 .iter_mut()
1716 .find(|e| e.context_id == context_id && e.conversation_id == *conversation_id)
1717 {
1718 existing.usage_count += 1;
1719 existing.used_at = chrono::Utc::now();
1720 } else {
1721 self.used_contexts.push(entry);
1722 }
1723
1724 if self.used_contexts.len() > self.max_entries {
1726 self.used_contexts.sort_by_key(|e| e.used_at);
1727 self.used_contexts
1728 .drain(0..self.used_contexts.len() - self.max_entries);
1729 }
1730 }
1731
1732 pub fn get_frequent_contexts(&self, limit: usize) -> Vec<&ContextHistoryEntry> {
1734 let mut entries = self.used_contexts.iter().collect::<Vec<_>>();
1735 entries.sort_by_key(|e| std::cmp::Reverse(e.usage_count));
1736 entries.into_iter().take(limit).collect()
1737 }
1738}
1739
1740#[derive(Debug, Clone, Serialize, Deserialize)]
1742#[cfg_attr(feature = "typescript", derive(Tsify))]
1743#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1744pub struct ContextHistoryEntry {
1745 pub context_id: String,
1747 pub conversation_id: ConversationId,
1749 pub usage_type: ContextUsageType,
1751 pub used_at: chrono::DateTime<chrono::Utc>,
1753 pub usage_count: usize,
1755}
1756
1757#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1759#[cfg_attr(feature = "typescript", derive(Tsify))]
1760#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1761pub enum ContextUsageType {
1762 Manual,
1764 Automatic,
1766 SearchResult,
1768 DocumentReference,
1770}
1771
1772#[derive(
1777 Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, JsonSchema, Default,
1778)]
1779#[cfg_attr(feature = "typescript", derive(Tsify))]
1780#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1781pub struct Priority(pub u8);
1782
1783impl Priority {
1784 pub fn new(value: u8) -> Self {
1786 Self(value.clamp(0, 100))
1787 }
1788
1789 pub fn value(&self) -> u8 {
1791 self.0
1792 }
1793
1794 pub fn is_high(&self) -> bool {
1796 self.0 >= 80
1797 }
1798
1799 pub fn is_medium(&self) -> bool {
1801 self.0 >= 40 && self.0 < 80
1802 }
1803
1804 pub fn is_low(&self) -> bool {
1806 self.0 < 40
1807 }
1808
1809 pub const MAX: Self = Self(100);
1811
1812 pub const HIGH: Self = Self(80);
1814
1815 pub const MEDIUM: Self = Self(50);
1817
1818 pub const LOW: Self = Self(20);
1820
1821 pub const MIN: Self = Self(0);
1823}
1824
1825impl fmt::Display for Priority {
1826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1827 write!(f, "{}", self.0)
1828 }
1829}
1830
1831impl From<u8> for Priority {
1832 fn from(value: u8) -> Self {
1833 Self::new(value)
1834 }
1835}
1836
1837impl From<i32> for Priority {
1838 fn from(value: i32) -> Self {
1839 Self::new(value as u8)
1840 }
1841}
1842
1843#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1845#[cfg_attr(feature = "typescript", derive(Tsify))]
1846#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1847pub struct RoutingRule {
1848 pub id: String,
1850
1851 pub name: String,
1853
1854 pub pattern: String,
1856
1857 pub priority: Priority,
1859
1860 pub provider: String,
1862
1863 pub model: String,
1865
1866 pub description: Option<String>,
1868
1869 pub tags: Vec<String>,
1871
1872 pub enabled: bool,
1874
1875 pub created_at: chrono::DateTime<chrono::Utc>,
1877
1878 pub updated_at: chrono::DateTime<chrono::Utc>,
1880}
1881
1882impl RoutingRule {
1883 pub fn new(
1885 id: String,
1886 name: String,
1887 pattern: String,
1888 priority: Priority,
1889 provider: String,
1890 model: String,
1891 ) -> Self {
1892 let now = chrono::Utc::now();
1893 Self {
1894 id,
1895 name,
1896 pattern,
1897 priority,
1898 provider,
1899 model,
1900 description: None,
1901 tags: Vec::new(),
1902 enabled: true,
1903 created_at: now,
1904 updated_at: now,
1905 }
1906 }
1907
1908 pub fn with_defaults(
1910 id: String,
1911 name: String,
1912 pattern: String,
1913 provider: String,
1914 model: String,
1915 ) -> Self {
1916 Self::new(id, name, pattern, Priority::MEDIUM, provider, model)
1917 }
1918
1919 pub fn with_description(mut self, description: String) -> Self {
1921 self.description = Some(description);
1922 self
1923 }
1924
1925 pub fn with_tag(mut self, tag: String) -> Self {
1927 self.tags.push(tag);
1928 self
1929 }
1930
1931 pub fn with_enabled(mut self, enabled: bool) -> Self {
1933 self.enabled = enabled;
1934 self
1935 }
1936
1937 pub fn touch(&mut self) {
1939 self.updated_at = chrono::Utc::now();
1940 }
1941}
1942
1943#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1945#[cfg_attr(feature = "typescript", derive(Tsify))]
1946#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
1947pub struct PatternMatch {
1948 pub concept: String,
1950
1951 pub provider: String,
1953
1954 pub model: String,
1956
1957 pub score: f64,
1959
1960 pub priority: Priority,
1962
1963 pub weighted_score: f64,
1965
1966 pub rule_id: String,
1968}
1969
1970impl PatternMatch {
1971 pub fn new(
1973 concept: String,
1974 provider: String,
1975 model: String,
1976 score: f64,
1977 priority: Priority,
1978 rule_id: String,
1979 ) -> Self {
1980 let priority_factor = priority.value() as f64 / 100.0;
1981 let weighted_score = score * priority_factor;
1982
1983 Self {
1984 concept,
1985 provider,
1986 model,
1987 score,
1988 priority,
1989 weighted_score,
1990 rule_id,
1991 }
1992 }
1993
1994 pub fn simple(concept: String, provider: String, model: String, score: f64) -> Self {
1996 Self::new(
1997 concept,
1998 provider,
1999 model,
2000 score,
2001 Priority::MEDIUM,
2002 "default".to_string(),
2003 )
2004 }
2005}
2006
2007#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2009#[cfg_attr(feature = "typescript", derive(Tsify))]
2010#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
2011pub struct RoutingDecision {
2012 pub provider: String,
2014
2015 pub model: String,
2017
2018 pub scenario: RoutingScenario,
2020
2021 pub priority: Priority,
2023
2024 pub confidence: f64,
2026
2027 pub rule_id: Option<String>,
2029
2030 pub reason: String,
2032}
2033
2034impl RoutingDecision {
2035 pub fn new(
2037 provider: String,
2038 model: String,
2039 scenario: RoutingScenario,
2040 priority: Priority,
2041 confidence: f64,
2042 reason: String,
2043 ) -> Self {
2044 Self {
2045 provider,
2046 model,
2047 scenario,
2048 priority,
2049 confidence,
2050 rule_id: None,
2051 reason,
2052 }
2053 }
2054
2055 pub fn with_rule(
2057 provider: String,
2058 model: String,
2059 scenario: RoutingScenario,
2060 priority: Priority,
2061 confidence: f64,
2062 rule_id: String,
2063 reason: String,
2064 ) -> Self {
2065 Self {
2066 provider,
2067 model,
2068 scenario,
2069 priority,
2070 confidence,
2071 rule_id: Some(rule_id),
2072 reason,
2073 }
2074 }
2075
2076 pub fn default(provider: String, model: String) -> Self {
2078 Self::new(
2079 provider,
2080 model,
2081 RoutingScenario::Default,
2082 Priority::LOW,
2083 0.5,
2084 "Default routing".to_string(),
2085 )
2086 }
2087}
2088
2089#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Default)]
2091#[cfg_attr(feature = "typescript", derive(Tsify))]
2092#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
2093pub enum RoutingScenario {
2094 #[serde(rename = "default")]
2096 #[default]
2097 Default,
2098
2099 #[serde(rename = "background")]
2101 Background,
2102
2103 #[serde(rename = "think")]
2105 Think,
2106
2107 #[serde(rename = "long_context")]
2109 LongContext,
2110
2111 #[serde(rename = "web_search")]
2113 WebSearch,
2114
2115 #[serde(rename = "image")]
2117 Image,
2118
2119 #[serde(rename = "pattern")]
2121 Pattern(String),
2122
2123 #[serde(rename = "priority")]
2125 Priority,
2126
2127 #[serde(rename = "custom")]
2129 Custom(String),
2130}
2131
2132impl fmt::Display for RoutingScenario {
2133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2134 match self {
2135 Self::Default => write!(f, "default"),
2136 Self::Background => write!(f, "background"),
2137 Self::Think => write!(f, "think"),
2138 Self::LongContext => write!(f, "long_context"),
2139 Self::WebSearch => write!(f, "web_search"),
2140 Self::Image => write!(f, "image"),
2141 Self::Pattern(concept) => write!(f, "pattern:{}", concept),
2142 Self::Priority => write!(f, "priority"),
2143 Self::Custom(name) => write!(f, "custom:{}", name),
2144 }
2145 }
2146}
2147
2148#[derive(Debug, Clone, Serialize, Deserialize)]
2150#[cfg_attr(feature = "typescript", derive(Tsify))]
2151#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
2152pub struct MultiAgentContext {
2153 pub session_id: String,
2155 pub agents: Vec<AgentInfo>,
2157 pub shared_context: Vec<ContextItem>,
2159 pub agent_contexts: AHashMap<String, Vec<ContextItem>>,
2161 pub agent_communications: Vec<AgentCommunication>,
2163 pub created_at: chrono::DateTime<chrono::Utc>,
2165 pub updated_at: chrono::DateTime<chrono::Utc>,
2167}
2168
2169impl MultiAgentContext {
2170 pub fn new() -> Self {
2171 let now = chrono::Utc::now();
2172 Self {
2173 session_id: uuid::Uuid::new_v4().to_string(),
2174 agents: Vec::new(),
2175 shared_context: Vec::new(),
2176 agent_contexts: AHashMap::new(),
2177 agent_communications: Vec::new(),
2178 created_at: now,
2179 updated_at: now,
2180 }
2181 }
2182
2183 pub fn add_agent(&mut self, agent: AgentInfo) {
2185 self.agents.push(agent.clone());
2186 self.agent_contexts.insert(agent.id, Vec::new());
2187 self.updated_at = chrono::Utc::now();
2188 }
2189
2190 pub fn add_agent_context(&mut self, agent_id: &str, context: ContextItem) {
2192 if let Some(contexts) = self.agent_contexts.get_mut(agent_id) {
2193 contexts.push(context);
2194 self.updated_at = chrono::Utc::now();
2195 }
2196 }
2197
2198 pub fn record_communication(
2200 &mut self,
2201 from_agent: &str,
2202 to_agent: Option<&str>,
2203 message: String,
2204 ) {
2205 let communication = AgentCommunication {
2206 from_agent: from_agent.to_string(),
2207 to_agent: to_agent.map(|s| s.to_string()),
2208 message,
2209 timestamp: chrono::Utc::now(),
2210 };
2211 self.agent_communications.push(communication);
2212 self.updated_at = chrono::Utc::now();
2213 }
2214}
2215
2216impl Default for MultiAgentContext {
2217 fn default() -> Self {
2218 Self::new()
2219 }
2220}
2221
2222#[derive(Debug, Clone, Serialize, Deserialize)]
2224#[cfg_attr(feature = "typescript", derive(Tsify))]
2225#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
2226pub struct AgentInfo {
2227 pub id: String,
2229 pub name: String,
2231 pub role: String,
2233 pub capabilities: Vec<String>,
2235 pub model: Option<String>,
2237}
2238
2239#[derive(Debug, Clone, Serialize, Deserialize)]
2241#[cfg_attr(feature = "typescript", derive(Tsify))]
2242#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
2243pub struct AgentCommunication {
2244 pub from_agent: String,
2246 pub to_agent: Option<String>,
2248 pub message: String,
2250 pub timestamp: chrono::DateTime<chrono::Utc>,
2252}
2253
2254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2260#[serde(rename_all = "snake_case")]
2261pub enum NormalizationMethod {
2262 #[default]
2264 Exact,
2265 Fuzzy,
2267 GraphRank,
2269}
2270
2271#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2273pub struct GroundingMetadata {
2274 pub normalized_uri: Option<String>,
2276 pub normalized_label: Option<String>,
2278 pub normalized_prov: Option<String>,
2280 pub normalized_score: Option<f32>,
2282 pub normalized_method: Option<NormalizationMethod>,
2284}
2285
2286impl GroundingMetadata {
2287 pub fn new(
2289 uri: String,
2290 label: String,
2291 prov: String,
2292 score: f32,
2293 method: NormalizationMethod,
2294 ) -> Self {
2295 Self {
2296 normalized_uri: Some(uri),
2297 normalized_label: Some(label),
2298 normalized_prov: Some(prov),
2299 normalized_score: Some(score),
2300 normalized_method: Some(method),
2301 }
2302 }
2303}
2304
2305#[derive(Debug, Clone, Serialize, Deserialize)]
2307pub struct CoverageSignal {
2308 pub total_categories: usize,
2310 pub matched_categories: usize,
2312 pub coverage_ratio: f32,
2314 pub threshold: f32,
2316 pub needs_review: bool,
2318}
2319
2320impl CoverageSignal {
2321 pub fn compute(categories: &[String], matched: usize, threshold: f32) -> Self {
2323 let total = categories.len();
2324 let ratio = if total > 0 {
2325 matched as f32 / total as f32
2326 } else {
2327 0.0
2328 };
2329 Self {
2330 total_categories: total,
2331 matched_categories: matched,
2332 coverage_ratio: ratio,
2333 threshold,
2334 needs_review: ratio < threshold,
2335 }
2336 }
2337}
2338
2339#[cfg(feature = "medical")]
2341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2342#[serde(rename_all = "snake_case")]
2343pub enum EntityType {
2344 CancerDiagnosis,
2345 Tumor,
2346 GenomicVariant,
2347 Biomarker,
2348 Drug,
2349 Treatment,
2350 SideEffect,
2351}
2352
2353#[cfg(feature = "medical")]
2355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2356#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2357pub enum RelationshipType {
2358 HasTumor,
2359 HasVariant,
2360 HasBiomarker,
2361 TreatedWith,
2362 Causes,
2363 HasDiagnosis,
2364}
2365
2366#[derive(Debug, Clone, Serialize, Deserialize)]
2368pub struct ExtractedEntity {
2369 pub entity_type: String,
2371 pub raw_value: String,
2373 pub normalized_value: Option<String>,
2375 pub grounding: Option<GroundingMetadata>,
2377}
2378
2379#[derive(Debug, Clone, Serialize, Deserialize)]
2381pub struct ExtractedRelationship {
2382 pub relationship_type: String,
2384 pub source: String,
2386 pub target: String,
2388 pub confidence: f32,
2390}
2391
2392#[derive(Debug, Clone, Serialize, Deserialize)]
2394pub struct SchemaSignal {
2395 pub entities: Vec<ExtractedEntity>,
2397 pub relationships: Vec<ExtractedRelationship>,
2399 pub confidence: f32,
2401}
2402
2403#[derive(Debug, Clone, Serialize, Deserialize)]
2409pub struct OntologyEntityType {
2410 pub id: String,
2412 pub label: String,
2414 #[serde(default)]
2416 pub uri_prefix: Option<String>,
2417 #[serde(default)]
2419 pub aliases: Vec<String>,
2420 #[serde(default)]
2422 pub category: Option<String>,
2423}
2424
2425#[derive(Debug, Clone, Serialize, Deserialize)]
2427pub struct OntologyRelationshipType {
2428 pub id: String,
2430 pub label: String,
2432 pub source_type: String,
2434 pub target_type: String,
2436}
2437
2438#[derive(Debug, Clone, Serialize, Deserialize)]
2440pub struct OntologyAntiPattern {
2441 pub id: String,
2443 pub description: String,
2445 pub indicators: Vec<String>,
2447}
2448
2449#[derive(Debug, Clone, Serialize, Deserialize)]
2453pub struct OntologySchema {
2454 pub name: String,
2456 pub version: String,
2458 pub entity_types: Vec<OntologyEntityType>,
2460 #[serde(default)]
2462 pub relationship_types: Vec<OntologyRelationshipType>,
2463 #[serde(default)]
2465 pub anti_patterns: Vec<OntologyAntiPattern>,
2466}
2467
2468impl OntologySchema {
2469 pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
2471 let content = std::fs::read_to_string(path)?;
2472 let schema: Self = serde_json::from_str(&content)?;
2473 Ok(schema)
2474 }
2475
2476 pub fn to_thesaurus_entries(&self) -> Vec<(String, String, Option<String>)> {
2482 let mut entries = Vec::new();
2483 for entity_type in &self.entity_types {
2484 let url = entity_type
2485 .uri_prefix
2486 .clone()
2487 .unwrap_or_else(|| format!("kg://{}", entity_type.id));
2488 entries.push((
2490 entity_type.id.clone(),
2491 entity_type.label.clone(),
2492 Some(url.clone()),
2493 ));
2494 for alias in &entity_type.aliases {
2496 entries.push((entity_type.id.clone(), alias.clone(), Some(url.clone())));
2497 }
2498 }
2499 entries
2500 }
2501
2502 pub fn category_ids(&self) -> Vec<String> {
2504 self.entity_types.iter().map(|e| e.id.clone()).collect()
2505 }
2506
2507 pub fn uri_for(&self, entity_type_id: &str) -> Option<String> {
2509 self.entity_types
2510 .iter()
2511 .find(|e| e.id == entity_type_id)
2512 .and_then(|e| e.uri_prefix.clone())
2513 }
2514}
2515
2516#[cfg(test)]
2517mod tests {
2518 use super::*;
2519
2520 #[test]
2521 fn test_search_query_logical_operators() {
2522 let single_query = SearchQuery {
2524 search_term: NormalizedTermValue::new("rust".to_string()),
2525 search_terms: None,
2526 operator: None,
2527 skip: None,
2528 limit: Some(10),
2529 role: Some(RoleName::new("test")),
2530 layer: Layer::default(),
2531 };
2532
2533 assert!(!single_query.is_multi_term_query());
2534 assert_eq!(single_query.get_all_terms().len(), 1);
2535 assert_eq!(single_query.get_operator(), LogicalOperator::Or); let and_query = SearchQuery::with_terms_and_operator(
2539 NormalizedTermValue::new("machine".to_string()),
2540 vec![NormalizedTermValue::new("learning".to_string())],
2541 LogicalOperator::And,
2542 Some(RoleName::new("test")),
2543 );
2544
2545 assert!(and_query.is_multi_term_query());
2546 assert_eq!(and_query.get_all_terms().len(), 2);
2547 assert_eq!(and_query.get_operator(), LogicalOperator::And);
2548
2549 let or_query = SearchQuery::with_terms_and_operator(
2551 NormalizedTermValue::new("neural".to_string()),
2552 vec![NormalizedTermValue::new("networks".to_string())],
2553 LogicalOperator::Or,
2554 Some(RoleName::new("test")),
2555 );
2556
2557 assert!(or_query.is_multi_term_query());
2558 assert_eq!(or_query.get_all_terms().len(), 2);
2559 assert_eq!(or_query.get_operator(), LogicalOperator::Or);
2560 }
2561
2562 #[test]
2563 fn test_logical_operator_serialization() {
2564 let and_op = LogicalOperator::And;
2566 let or_op = LogicalOperator::Or;
2567
2568 let and_json = serde_json::to_string(&and_op).unwrap();
2569 let or_json = serde_json::to_string(&or_op).unwrap();
2570
2571 assert_eq!(and_json, "\"and\"");
2572 assert_eq!(or_json, "\"or\"");
2573
2574 let and_deser: LogicalOperator = serde_json::from_str("\"and\"").unwrap();
2576 let or_deser: LogicalOperator = serde_json::from_str("\"or\"").unwrap();
2577
2578 assert_eq!(and_deser, LogicalOperator::And);
2579 assert_eq!(or_deser, LogicalOperator::Or);
2580 }
2581
2582 #[test]
2583 fn test_search_query_serialization() {
2584 let query = SearchQuery {
2585 search_term: NormalizedTermValue::new("test".to_string()),
2586 search_terms: Some(vec![
2587 NormalizedTermValue::new("additional".to_string()),
2588 NormalizedTermValue::new("terms".to_string()),
2589 ]),
2590 operator: Some(LogicalOperator::And),
2591 skip: Some(0),
2592 limit: Some(10),
2593 role: Some(RoleName::new("test_role")),
2594 layer: Layer::default(),
2595 };
2596
2597 let json = serde_json::to_string(&query).unwrap();
2598 let deserialized: SearchQuery = serde_json::from_str(&json).unwrap();
2599
2600 assert_eq!(query.search_term, deserialized.search_term);
2601 assert_eq!(query.search_terms, deserialized.search_terms);
2602 assert_eq!(query.operator, deserialized.operator);
2603 assert_eq!(query.skip, deserialized.skip);
2604 assert_eq!(query.limit, deserialized.limit);
2605 assert_eq!(query.role, deserialized.role);
2606 }
2607
2608 #[test]
2609 fn test_priority_creation_and_comparison() {
2610 let high = Priority::HIGH;
2611 let medium = Priority::MEDIUM;
2612 let low = Priority::LOW;
2613 let custom = Priority::new(75);
2614
2615 assert_eq!(high.value(), 80);
2616 assert_eq!(medium.value(), 50);
2617 assert_eq!(low.value(), 20);
2618 assert_eq!(custom.value(), 75);
2619
2620 assert!(high.is_high());
2621 assert!(!medium.is_high());
2622 assert!(medium.is_medium());
2623 assert!(low.is_low());
2624
2625 assert!(high > medium);
2627 assert!(medium > low);
2628 assert!(custom > medium);
2629 assert!(custom < high);
2630
2631 let max = Priority::new(150);
2633 assert_eq!(max.value(), 100);
2634 let min = Priority::new(0);
2635 assert_eq!(min.value(), 0);
2636 }
2637
2638 #[test]
2639 fn test_routing_rule_creation() {
2640 let rule = RoutingRule::new(
2641 "test-rule".to_string(),
2642 "Test Rule".to_string(),
2643 "test.*pattern".to_string(),
2644 Priority::HIGH,
2645 "openai".to_string(),
2646 "gpt-4".to_string(),
2647 )
2648 .with_description("A test rule for unit testing".to_string())
2649 .with_tag("test".to_string())
2650 .with_tag("example".to_string());
2651
2652 assert_eq!(rule.id, "test-rule");
2653 assert_eq!(rule.name, "Test Rule");
2654 assert_eq!(rule.pattern, "test.*pattern");
2655 assert_eq!(rule.priority, Priority::HIGH);
2656 assert_eq!(rule.provider, "openai");
2657 assert_eq!(rule.model, "gpt-4");
2658 assert_eq!(
2659 rule.description,
2660 Some("A test rule for unit testing".to_string())
2661 );
2662 assert_eq!(rule.tags, vec!["test", "example"]);
2663 assert!(rule.enabled);
2664 }
2665
2666 #[test]
2667 fn test_routing_rule_defaults() {
2668 let rule = RoutingRule::with_defaults(
2669 "default-rule".to_string(),
2670 "Default Rule".to_string(),
2671 "default".to_string(),
2672 "anthropic".to_string(),
2673 "claude-3-sonnet".to_string(),
2674 );
2675
2676 assert_eq!(rule.priority, Priority::MEDIUM);
2677 assert!(rule.enabled);
2678 assert!(rule.tags.is_empty());
2679 assert!(rule.description.is_none());
2680 }
2681
2682 #[test]
2683 fn test_pattern_match() {
2684 let pattern_match = PatternMatch::new(
2685 "machine-learning".to_string(),
2686 "openai".to_string(),
2687 "gpt-4".to_string(),
2688 0.95,
2689 Priority::HIGH,
2690 "ml-rule".to_string(),
2691 );
2692
2693 assert_eq!(pattern_match.concept, "machine-learning");
2694 assert_eq!(pattern_match.provider, "openai");
2695 assert_eq!(pattern_match.model, "gpt-4");
2696 assert_eq!(pattern_match.score, 0.95);
2697 assert_eq!(pattern_match.priority, Priority::HIGH);
2698 assert_eq!(pattern_match.rule_id, "ml-rule");
2699
2700 assert_eq!(pattern_match.weighted_score, 0.95 * 0.8);
2702 }
2703
2704 #[test]
2705 fn test_pattern_match_simple() {
2706 let simple = PatternMatch::simple(
2707 "test".to_string(),
2708 "anthropic".to_string(),
2709 "claude-3-haiku".to_string(),
2710 0.8,
2711 );
2712
2713 assert_eq!(simple.priority, Priority::MEDIUM);
2714 assert_eq!(simple.rule_id, "default");
2715 assert_eq!(simple.weighted_score, 0.8 * 0.5);
2716 }
2717
2718 #[test]
2719 fn test_routing_decision() {
2720 let decision = RoutingDecision::new(
2721 "openai".to_string(),
2722 "gpt-4".to_string(),
2723 RoutingScenario::Think,
2724 Priority::HIGH,
2725 0.9,
2726 "High priority thinking task".to_string(),
2727 );
2728
2729 assert_eq!(decision.provider, "openai");
2730 assert_eq!(decision.model, "gpt-4");
2731 assert_eq!(decision.scenario, RoutingScenario::Think);
2732 assert_eq!(decision.priority, Priority::HIGH);
2733 assert_eq!(decision.confidence, 0.9);
2734 assert_eq!(decision.reason, "High priority thinking task");
2735 assert!(decision.rule_id.is_none());
2736 }
2737
2738 #[test]
2739 fn test_routing_decision_with_rule() {
2740 let decision = RoutingDecision::with_rule(
2741 "anthropic".to_string(),
2742 "claude-3-sonnet".to_string(),
2743 RoutingScenario::Pattern("web-search".to_string()),
2744 Priority::MEDIUM,
2745 0.85,
2746 "web-rule".to_string(),
2747 "Web search pattern matched".to_string(),
2748 );
2749
2750 assert_eq!(decision.rule_id, Some("web-rule".to_string()));
2751 assert_eq!(
2752 decision.scenario,
2753 RoutingScenario::Pattern("web-search".to_string())
2754 );
2755 }
2756
2757 #[test]
2758 fn test_routing_decision_default() {
2759 let default = RoutingDecision::default("openai".to_string(), "gpt-3.5-turbo".to_string());
2760
2761 assert_eq!(default.provider, "openai");
2762 assert_eq!(default.model, "gpt-3.5-turbo");
2763 assert_eq!(default.scenario, RoutingScenario::Default);
2764 assert_eq!(default.priority, Priority::LOW);
2765 assert_eq!(default.confidence, 0.5);
2766 assert_eq!(default.reason, "Default routing");
2767 }
2768
2769 #[test]
2770 fn test_routing_scenario_serialization() {
2771 let scenarios = vec![
2772 RoutingScenario::Default,
2773 RoutingScenario::Background,
2774 RoutingScenario::Think,
2775 RoutingScenario::LongContext,
2776 RoutingScenario::WebSearch,
2777 RoutingScenario::Image,
2778 RoutingScenario::Pattern("test".to_string()),
2779 RoutingScenario::Priority,
2780 RoutingScenario::Custom("special".to_string()),
2781 ];
2782
2783 for scenario in scenarios {
2784 let json = serde_json::to_string(&scenario).unwrap();
2785 let deserialized: RoutingScenario = serde_json::from_str(&json).unwrap();
2786 assert_eq!(scenario, deserialized);
2787 }
2788 }
2789
2790 #[test]
2791 fn test_routing_scenario_display() {
2792 assert_eq!(format!("{}", RoutingScenario::Default), "default");
2793 assert_eq!(format!("{}", RoutingScenario::Think), "think");
2794 assert_eq!(
2795 format!("{}", RoutingScenario::Pattern("ml".to_string())),
2796 "pattern:ml"
2797 );
2798 assert_eq!(
2799 format!("{}", RoutingScenario::Custom("test".to_string())),
2800 "custom:test"
2801 );
2802 }
2803
2804 #[test]
2805 fn test_priority_serialization() {
2806 let priority = Priority::new(75);
2807 let json = serde_json::to_string(&priority).unwrap();
2808 let deserialized: Priority = serde_json::from_str(&json).unwrap();
2809 assert_eq!(priority, deserialized);
2810 assert_eq!(deserialized.value(), 75);
2811 }
2812
2813 #[test]
2814 fn test_routing_rule_serialization() {
2815 let rule = RoutingRule::new(
2816 "serialize-test".to_string(),
2817 "Serialize Test".to_string(),
2818 "test-pattern".to_string(),
2819 Priority::MEDIUM,
2820 "provider".to_string(),
2821 "model".to_string(),
2822 );
2823
2824 let json = serde_json::to_string(&rule).unwrap();
2825 let deserialized: RoutingRule = serde_json::from_str(&json).unwrap();
2826 assert_eq!(rule.id, deserialized.id);
2827 assert_eq!(rule.name, deserialized.name);
2828 assert_eq!(rule.priority, deserialized.priority);
2829 assert_eq!(rule.provider, deserialized.provider);
2830 assert_eq!(rule.model, deserialized.model);
2831 }
2832
2833 #[test]
2834 fn test_document_type_serialization() {
2835 let types = vec![
2836 DocumentType::KgEntry,
2837 DocumentType::Document,
2838 DocumentType::ConfigDocument,
2839 ];
2840
2841 for doc_type in types {
2842 let json = serde_json::to_string(&doc_type).unwrap();
2843 let deserialized: DocumentType = serde_json::from_str(&json).unwrap();
2844 assert_eq!(doc_type, deserialized);
2845 }
2846 }
2847
2848 #[test]
2849 fn test_document_defaults_for_new_fields() {
2850 let json = r#"{
2851 "id":"doc-1",
2852 "url":"file:///tmp/doc.md",
2853 "title":"Doc",
2854 "body":"Body"
2855 }"#;
2856
2857 let doc: Document = serde_json::from_str(json).unwrap();
2858 assert_eq!(doc.doc_type, DocumentType::KgEntry);
2859 assert!(doc.synonyms.is_none());
2860 assert!(doc.route.is_none());
2861 assert!(doc.priority.is_none());
2862 }
2863
2864 #[test]
2865 fn test_ontology_schema_deserialize() {
2866 let json = include_str!("../test-fixtures/sample_ontology_schema.json");
2867 let schema: OntologySchema = serde_json::from_str(json).unwrap();
2868 assert_eq!(schema.name, "Publishing Domain Model");
2869 assert_eq!(schema.version, "1.0.0");
2870 assert_eq!(schema.entity_types.len(), 3);
2871 assert_eq!(schema.relationship_types.len(), 1);
2872 assert_eq!(schema.anti_patterns.len(), 1);
2873 }
2874
2875 #[test]
2876 fn test_ontology_schema_to_thesaurus_entries() {
2877 let json = include_str!("../test-fixtures/sample_ontology_schema.json");
2878 let schema: OntologySchema = serde_json::from_str(json).unwrap();
2879 let entries = schema.to_thesaurus_entries();
2880 assert_eq!(entries.len(), 10);
2882 assert!(entries.iter().any(|(_, term, _)| term == "Chapter"));
2884 assert!(entries.iter().any(|(_, term, _)| term == "Concept"));
2885 assert!(entries.iter().any(|(_, term, _)| term == "Knowledge Graph"));
2886 assert!(entries.iter().any(|(_, term, _)| term == "section"));
2888 assert!(entries.iter().any(|(_, term, _)| term == "KG"));
2889 assert!(entries.iter().all(|(_, _, url)| url.is_some()));
2891 }
2892
2893 #[test]
2894 fn test_ontology_schema_category_ids() {
2895 let json = include_str!("../test-fixtures/sample_ontology_schema.json");
2896 let schema: OntologySchema = serde_json::from_str(json).unwrap();
2897 let ids = schema.category_ids();
2898 assert_eq!(ids.len(), 3);
2899 assert!(ids.contains(&"chapter".to_string()));
2900 assert!(ids.contains(&"concept".to_string()));
2901 assert!(ids.contains(&"knowledge_graph".to_string()));
2902 }
2903
2904 #[test]
2905 fn test_ontology_schema_uri_for() {
2906 let json = include_str!("../test-fixtures/sample_ontology_schema.json");
2907 let schema: OntologySchema = serde_json::from_str(json).unwrap();
2908 assert_eq!(
2909 schema.uri_for("chapter"),
2910 Some("https://schema.org/Chapter".to_string())
2911 );
2912 assert_eq!(
2913 schema.uri_for("concept"),
2914 Some("https://schema.org/DefinedTerm".to_string())
2915 );
2916 assert_eq!(schema.uri_for("nonexistent"), None);
2917 }
2918
2919 #[test]
2920 fn test_ontology_schema_minimal() {
2921 let json = r#"{
2923 "name": "Minimal",
2924 "version": "0.1.0",
2925 "entity_types": [
2926 {"id": "item", "label": "Item"}
2927 ]
2928 }"#;
2929 let schema: OntologySchema = serde_json::from_str(json).unwrap();
2930 assert_eq!(schema.name, "Minimal");
2931 assert_eq!(schema.entity_types.len(), 1);
2932 assert!(schema.relationship_types.is_empty());
2933 assert!(schema.anti_patterns.is_empty());
2934 assert!(schema.entity_types[0].aliases.is_empty());
2935 assert!(schema.entity_types[0].uri_prefix.is_none());
2936 }
2937
2938 #[test]
2939 fn test_layer_enum() {
2940 let default: Layer = Default::default();
2942 assert_eq!(default, Layer::One);
2943
2944 assert_eq!(Layer::from_u8(1), Some(Layer::One));
2946 assert_eq!(Layer::from_u8(2), Some(Layer::Two));
2947 assert_eq!(Layer::from_u8(3), Some(Layer::Three));
2948 assert_eq!(Layer::from_u8(0), None);
2949 assert_eq!(Layer::from_u8(4), None);
2950
2951 assert_eq!(format!("{}", Layer::One), "1");
2953 assert_eq!(format!("{}", Layer::Two), "2");
2954 assert_eq!(format!("{}", Layer::Three), "3");
2955
2956 assert!(!Layer::One.includes_content());
2958 assert!(Layer::Two.includes_content());
2959 assert!(Layer::Three.includes_content());
2960
2961 assert!(!Layer::One.includes_full_content());
2963 assert!(!Layer::Two.includes_full_content());
2964 assert!(Layer::Three.includes_full_content());
2965 }
2966
2967 #[test]
2968 fn test_extract_first_paragraph_simple() {
2969 let body = "First paragraph here.\n\nSecond paragraph here.";
2970 assert_eq!(extract_first_paragraph(body), "First paragraph here.");
2971 }
2972
2973 #[test]
2974 fn test_extract_first_paragraph_with_yaml_frontmatter() {
2975 let body = "---\ntitle: My Document\ntags: [rust, programming]\n---\n\nThis is the actual first paragraph.\nMore content here.";
2976 assert_eq!(
2977 extract_first_paragraph(body),
2978 "This is the actual first paragraph."
2979 );
2980 }
2981
2982 #[test]
2983 fn test_extract_first_paragraph_empty_lines() {
2984 let body = "\n\n\nFirst paragraph after empty lines.";
2985 assert_eq!(
2986 extract_first_paragraph(body),
2987 "First paragraph after empty lines."
2988 );
2989 }
2990
2991 #[test]
2992 fn test_extract_first_paragraph_single_line() {
2993 let body = "Just one line";
2994 assert_eq!(extract_first_paragraph(body), "Just one line");
2995 }
2996
2997 #[test]
2998 fn test_layer_serialization() {
2999 let query = SearchQuery {
3001 search_term: NormalizedTermValue::new("test".to_string()),
3002 search_terms: None,
3003 operator: None,
3004 skip: None,
3005 limit: None,
3006 role: None,
3007 layer: Layer::Two,
3008 };
3009
3010 let json = serde_json::to_string(&query).unwrap();
3011 assert!(json.contains("\"layer\""));
3012
3013 let deserialized: SearchQuery = serde_json::from_str(&json).unwrap();
3015 assert_eq!(deserialized.layer, Layer::Two);
3016 }
3017
3018 #[test]
3019 fn test_quality_score_composite() {
3020 let full_score = QualityScore {
3022 knowledge: Some(0.8),
3023 learning: Some(0.6),
3024 synthesis: Some(0.7),
3025 };
3026 assert!((full_score.composite() - 0.7).abs() < f64::EPSILON); let partial_score = QualityScore {
3030 knowledge: Some(0.9),
3031 learning: None,
3032 synthesis: Some(0.5),
3033 };
3034 assert!((partial_score.composite() - 0.7).abs() < f64::EPSILON); let single_score = QualityScore {
3038 knowledge: Some(0.8),
3039 learning: None,
3040 synthesis: None,
3041 };
3042 assert!((single_score.composite() - 0.8).abs() < f64::EPSILON);
3043
3044 let empty_score = QualityScore::default();
3046 assert_eq!(empty_score.composite(), 0.0);
3047 }
3048
3049 #[test]
3050 fn test_quality_score_serialization() {
3051 let score = QualityScore {
3052 knowledge: Some(0.8),
3053 learning: Some(0.6),
3054 synthesis: Some(0.7),
3055 };
3056
3057 let json = serde_json::to_string(&score).unwrap();
3058 assert!(json.contains("0.8"));
3059 assert!(json.contains("0.6"));
3060 assert!(json.contains("0.7"));
3061
3062 let deserialized: QualityScore = serde_json::from_str(&json).unwrap();
3063 assert_eq!(deserialized.knowledge, Some(0.8));
3064 assert_eq!(deserialized.learning, Some(0.6));
3065 assert_eq!(deserialized.synthesis, Some(0.7));
3066 }
3067
3068 #[test]
3069 fn test_quality_score_default_serialization() {
3070 let score = QualityScore::default();
3072 let json = serde_json::to_string(&score).unwrap();
3073 let deserialized: QualityScore = serde_json::from_str(&json).unwrap();
3074 assert!(deserialized.knowledge.is_none());
3075 assert!(deserialized.learning.is_none());
3076 assert!(deserialized.synthesis.is_none());
3077 }
3078
3079 #[test]
3080 fn test_indexed_document_with_quality_score() {
3081 let doc = IndexedDocument {
3082 id: "test-doc-1".to_string(),
3083 matched_edges: vec![],
3084 rank: 10,
3085 tags: vec!["rust".to_string()],
3086 nodes: vec![1, 2],
3087 quality_score: Some(QualityScore {
3088 knowledge: Some(0.8),
3089 learning: Some(0.6),
3090 synthesis: Some(0.7),
3091 }),
3092 };
3093
3094 assert_eq!(doc.id, "test-doc-1");
3095 assert!((doc.quality_score.as_ref().unwrap().composite() - 0.7).abs() < f64::EPSILON);
3096 }
3097
3098 #[test]
3099 fn test_indexed_document_from_document_quality_score_none() {
3100 let doc = Document {
3101 id: "doc-1".to_string(),
3102 url: "https://example.com".to_string(),
3103 title: "Test".to_string(),
3104 body: "Body".to_string(),
3105 description: None,
3106 summarization: None,
3107 stub: None,
3108 tags: None,
3109 rank: None,
3110 source_haystack: None,
3111 doc_type: DocumentType::Document,
3112 synonyms: None,
3113 route: None,
3114 priority: None,
3115 };
3116
3117 let indexed = IndexedDocument::from_document(doc);
3118 assert!(indexed.quality_score.is_none());
3119 }
3120
3121 #[test]
3122 fn test_indexed_document_serialization_backward_compat() {
3123 let json = r#"{
3127 "id": "doc-1",
3128 "matched_edges": [],
3129 "rank": 5,
3130 "tags": ["test"],
3131 "nodes": [1]
3132 }"#;
3133
3134 let doc: IndexedDocument = serde_json::from_str(json).unwrap();
3135 assert_eq!(doc.id, "doc-1");
3136 assert!(doc.quality_score.is_none());
3137 }
3138}