1use std::collections::{BTreeMap, HashSet};
9use std::rc::Rc;
10
11use crate::graph::{Graph, GraphNodeOpts};
12use crate::json::JsonValue;
13use crate::node::{Node, NodeOpts};
14use crate::operators::Operator;
15
16pub type FactId = String;
18
19#[derive(Clone, Debug, PartialEq)]
22pub struct MemoryFragment<T> {
23 pub id: FactId,
25 pub payload: T,
27 pub t_ns: u128,
29 pub valid_from: Option<u128>,
31 pub valid_to: Option<u128>,
33 pub confidence: f64,
35 pub tags: Vec<String>,
37 pub sources: Vec<FactId>,
39 pub embedding: Option<Vec<f64>>,
41 pub parent_fragment_id: Option<FactId>,
43 pub provenance: Option<String>,
45}
46
47impl<T> MemoryFragment<T> {
48 pub fn new(id: impl Into<String>, payload: T, t_ns: u128) -> Self {
50 Self {
51 id: id.into(),
52 payload,
53 t_ns,
54 valid_from: None,
55 valid_to: None,
56 confidence: 1.0,
57 tags: Vec::new(),
58 sources: Vec::new(),
59 embedding: None,
60 parent_fragment_id: None,
61 provenance: None,
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq)]
72pub enum KnowledgeAssertionObject {
73 Entity {
75 entity_id: FactId,
77 },
78 Literal {
80 value: JsonValue,
82 },
83}
84
85#[derive(Clone, Debug, PartialEq)]
87pub struct KnowledgeAssertion {
88 pub id: FactId,
90 pub subject_id: FactId,
92 pub predicate: String,
94 pub object: KnowledgeAssertionObject,
96 pub sources: Vec<FactId>,
98 pub confidence: f64,
100 pub t_ns: u128,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
105pub struct KnowledgeGraphPolicy {
107 pub allowed_predicates: Vec<String>,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct KnowledgeGraphEntity {
114 pub id: FactId,
116 pub assertion_ids: Vec<FactId>,
118 pub subject_assertion_ids: Vec<FactId>,
120 pub object_assertion_ids: Vec<FactId>,
122}
123
124#[derive(Clone, Debug, PartialEq)]
125pub struct KnowledgeGraphRelation {
127 pub assertion_id: FactId,
129 pub subject_id: FactId,
131 pub predicate: String,
133 pub object: KnowledgeAssertionObject,
135 pub sources: Vec<FactId>,
137 pub confidence: f64,
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct KnowledgeGraphTopic {
144 pub predicate: String,
146 pub assertion_ids: Vec<FactId>,
148 pub entity_ids: Vec<FactId>,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct KnowledgeGraphIndex {
155 pub assertion_ids: Vec<FactId>,
157 pub entity_ids: Vec<FactId>,
159 pub relation_ids: Vec<FactId>,
161 pub predicates: Vec<String>,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct KnowledgeGraphCursor {
168 pub evaluation: u64,
170 pub valid_assertions: usize,
172 pub invalid_assertions: usize,
174 pub entity_count: usize,
176 pub relation_count: usize,
178 pub predicate_count: usize,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum KnowledgeGraphStatusState {
185 Ready,
187 Empty,
189 Partial,
191 Error,
193}
194
195#[derive(Clone, Debug, PartialEq, Eq)]
196pub struct KnowledgeGraphStatus {
198 pub state: KnowledgeGraphStatusState,
200 pub cursor: KnowledgeGraphCursor,
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq)]
205pub enum KnowledgeGraphErrorCode {
207 InvalidAssertion,
209 DuplicateAssertionId,
211 PolicyConflict,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct KnowledgeGraphError {
218 pub code: KnowledgeGraphErrorCode,
220 pub message: String,
222 pub index: Option<usize>,
224 pub assertion_id: Option<FactId>,
226 pub validation_errors: Vec<String>,
228 pub cursor: KnowledgeGraphCursor,
230}
231
232#[derive(Clone, Debug, PartialEq)]
233pub struct KnowledgeGraphSnapshot {
235 pub assertions: Vec<KnowledgeAssertion>,
237 pub entities: Vec<KnowledgeGraphEntity>,
239 pub relations: Vec<KnowledgeGraphRelation>,
241 pub topics: Vec<KnowledgeGraphTopic>,
243 pub index: KnowledgeGraphIndex,
245 pub status: KnowledgeGraphStatus,
247 pub errors: Vec<KnowledgeGraphError>,
249 pub cursor: KnowledgeGraphCursor,
251}
252
253#[derive(Clone)]
254pub struct KnowledgeGraphReducerBundleOptions {
256 pub name: Option<String>,
258 pub assertions: Node<Vec<KnowledgeAssertion>>,
260 pub policy: Option<Node<KnowledgeGraphPolicy>>,
262}
263
264impl KnowledgeGraphReducerBundleOptions {
265 pub fn new(assertions: Node<Vec<KnowledgeAssertion>>) -> Self {
267 Self {
268 name: None,
269 assertions,
270 policy: None,
271 }
272 }
273
274 pub fn named(mut self, name: impl Into<String>) -> Self {
276 self.name = Some(name.into());
277 self
278 }
279
280 pub fn with_policy(mut self, policy: Node<KnowledgeGraphPolicy>) -> Self {
282 self.policy = Some(policy);
283 self
284 }
285}
286
287#[derive(Clone)]
288pub struct KnowledgeGraphReducerBundle {
290 pub assertions_input: Node<Vec<KnowledgeAssertion>>,
292 pub policy_input: Option<Node<KnowledgeGraphPolicy>>,
294 pub snapshot: Node<KnowledgeGraphSnapshot>,
296 pub assertions: Node<Vec<KnowledgeAssertion>>,
298 pub entities: Node<Vec<KnowledgeGraphEntity>>,
300 pub relations: Node<Vec<KnowledgeGraphRelation>>,
302 pub topics: Node<Vec<KnowledgeGraphTopic>>,
304 pub index: Node<KnowledgeGraphIndex>,
306 pub status: Node<KnowledgeGraphStatus>,
308 pub errors: Node<Vec<KnowledgeGraphError>>,
310 pub cursor: Node<KnowledgeGraphCursor>,
312}
313
314pub type ShardKey = String;
316
317#[derive(Clone, Debug, Default, PartialEq)]
318pub struct FactStore<T> {
320 pub by_id: BTreeMap<FactId, MemoryFragment<T>>,
322}
323
324impl<T> FactStore<T> {
325 pub fn read_handle(&self) -> StoreReadHandle<'_, T> {
327 StoreReadHandle { by_id: &self.by_id }
328 }
329}
330
331#[derive(Clone, Copy, Debug)]
332pub struct StoreReadHandle<'a, T> {
334 by_id: &'a BTreeMap<FactId, MemoryFragment<T>>,
335}
336
337impl<'a, T> StoreReadHandle<'a, T> {
338 pub fn get(&self, id: &str) -> Option<&'a MemoryFragment<T>> {
340 self.by_id.get(id)
341 }
342
343 pub fn has(&self, id: &str) -> bool {
345 self.by_id.contains_key(id)
346 }
347
348 pub fn size(&self) -> usize {
350 self.by_id.len()
351 }
352
353 pub fn values(&self) -> impl Iterator<Item = &'a MemoryFragment<T>> {
355 self.by_id.values()
356 }
357}
358
359#[derive(Clone, Debug, Default, PartialEq)]
360pub struct MemoryQuery {
362 pub tags: Vec<String>,
364 pub as_of: Option<u128>,
366 pub min_confidence: Option<f64>,
368 pub limit: Option<usize>,
370}
371
372#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct MemoryFragmentValidation {
375 pub ok: bool,
377 pub errors: Vec<String>,
379}
380
381#[derive(Clone, Debug, PartialEq)]
382pub struct OutcomeSignal {
384 pub fact_id: FactId,
386 pub reward: f64,
388}
389
390#[derive(Clone, Debug, PartialEq)]
391pub struct CollectionEntry<T> {
393 pub id: String,
395 pub value: T,
397 pub created_at_ns: u128,
399 pub last_access_ns: u128,
401 pub base_score: f64,
403}
404
405#[derive(Clone, Debug, PartialEq)]
406pub struct RankedCollectionEntry<T> {
408 pub entry: CollectionEntry<T>,
410 pub score: f64,
412}
413
414#[derive(Clone, Debug, Default, PartialEq)]
415pub struct RetrievalQuery {
417 pub text: Option<String>,
419 pub vector: Option<Vec<f64>>,
421 pub entity_ids: Vec<String>,
423 pub context: Vec<String>,
425}
426
427#[derive(Clone, Debug, PartialEq)]
428pub struct VectorSearchResult<TMeta> {
430 pub id: String,
432 pub score: f64,
434 pub meta: Option<TMeta>,
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub enum RetrievalEntrySource {
441 Vector,
443 Graph,
445 Store,
447}
448
449#[derive(Clone, Debug, PartialEq)]
450pub struct RetrievalEntry<TMem> {
452 pub key: String,
454 pub value: TMem,
456 pub score: f64,
458 pub sources: Vec<RetrievalEntrySource>,
460 pub context: Vec<String>,
462}
463
464#[derive(Clone, Debug, PartialEq)]
465pub struct RetrievalTrace<TMem> {
467 pub vector_candidates: Vec<VectorSearchResult<TMem>>,
469 pub graph_expanded: Vec<String>,
471 pub ranked: Vec<RetrievalEntry<TMem>>,
473 pub packed: Vec<RetrievalEntry<TMem>>,
475}
476
477pub type AdmissionThresholds = BTreeMap<String, f64>;
479pub type AdmissionScoreFn<TRaw> = Rc<dyn Fn(&TRaw) -> BTreeMap<String, f64>>;
481pub type AdmissionScore3DFn<TRaw> = Rc<dyn Fn(&TRaw) -> AdmissionScores>;
483pub type TenantShardFn<T> = Rc<dyn Fn(&MemoryFragment<T>) -> String>;
485pub type ShardByFn<T> = Rc<dyn Fn(&MemoryFragment<T>) -> ShardKey>;
487
488pub struct AdmissionScoredOptions<TRaw> {
490 pub score_fn: AdmissionScoreFn<TRaw>,
492 pub thresholds: AdmissionThresholds,
494}
495
496#[derive(Clone, Copy, Debug, PartialEq)]
497pub struct AdmissionScores {
499 pub persistence: f64,
501 pub structure: f64,
503 pub personal_value: f64,
505}
506
507pub struct AdmissionScore3DOptions<TRaw> {
509 pub score_fn: AdmissionScore3DFn<TRaw>,
511 pub persistence_threshold: f64,
513 pub personal_value_threshold: f64,
515 pub require_structured: bool,
517}
518
519impl<TRaw> AdmissionScore3DOptions<TRaw> {
520 pub fn new(score_fn: AdmissionScore3DFn<TRaw>) -> Self {
522 Self {
523 score_fn,
524 persistence_threshold: 0.3,
525 personal_value_threshold: 0.3,
526 require_structured: false,
527 }
528 }
529}
530
531#[derive(Clone, Debug, Default, PartialEq, Eq)]
532pub struct ShardByTenantOptions {
534 pub tenants: Vec<String>,
536 pub shard_count: Option<usize>,
538}
539
540pub struct ShardByTenantConfig<T> {
542 pub shard_by: ShardByFn<T>,
544 pub shard_count: usize,
546}
547
548pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
550 let n = a.len().max(b.len());
551 let mut dot = 0.0;
552 let mut na = 0.0;
553 let mut nb = 0.0;
554 for i in 0..n {
555 let av = a.get(i).copied().unwrap_or(0.0);
556 let bv = b.get(i).copied().unwrap_or(0.0);
557 dot += av * bv;
558 na += av * av;
559 nb += bv * bv;
560 }
561 if na == 0.0 || nb == 0.0 {
562 return 0.0;
563 }
564 let score = dot / (na.sqrt() * nb.sqrt());
565 if score.is_finite() {
566 score
567 } else {
568 0.0
569 }
570}
571
572pub fn memory_fragment_valid_at<T>(fragment: &MemoryFragment<T>, as_of: Option<u128>) -> bool {
574 match as_of {
575 None => fragment.valid_from.is_none() && fragment.valid_to.is_none(),
576 Some(as_of) => {
577 if fragment
578 .valid_from
579 .is_some_and(|valid_from| valid_from > as_of)
580 {
581 return false;
582 }
583 if fragment.valid_to.is_some_and(|valid_to| valid_to <= as_of) {
584 return false;
585 }
586 true
587 }
588 }
589}
590
591pub fn memory_fragment_matches_query<T>(fragment: &MemoryFragment<T>, query: &MemoryQuery) -> bool {
593 if !memory_fragment_valid_at(fragment, query.as_of) {
594 return false;
595 }
596 if query
597 .min_confidence
598 .is_some_and(|min_confidence| fragment.confidence < min_confidence)
599 {
600 return false;
601 }
602 if !query.tags.is_empty()
603 && !query
604 .tags
605 .iter()
606 .any(|tag| fragment.tags.iter().any(|fragment_tag| fragment_tag == tag))
607 {
608 return false;
609 }
610 true
611}
612
613pub fn filter_memory_fragments<T: Clone>(
615 fragments: impl IntoIterator<Item = MemoryFragment<T>>,
616 query: &MemoryQuery,
617) -> Vec<MemoryFragment<T>> {
618 let mut ranked: Vec<_> = fragments
619 .into_iter()
620 .filter(|fragment| memory_fragment_matches_query(fragment, query))
621 .collect();
622 ranked.sort_by(|a, b| {
623 b.confidence
624 .total_cmp(&a.confidence)
625 .then_with(|| b.t_ns.cmp(&a.t_ns))
626 });
627 if let Some(limit) = query.limit {
628 ranked.truncate(limit);
629 }
630 ranked
631}
632
633pub fn validate_memory_fragment<T>(fragment: &MemoryFragment<T>) -> MemoryFragmentValidation {
635 let errors = validate_fragment(fragment);
636 MemoryFragmentValidation {
637 ok: errors.is_empty(),
638 errors,
639 }
640}
641
642pub fn admission_scored<TRaw>(opts: AdmissionScoredOptions<TRaw>) -> impl Fn(&TRaw) -> bool {
644 move |raw| {
645 let scores = (opts.score_fn)(raw);
646 opts.thresholds.iter().all(|(dimension, threshold)| {
647 scores
648 .get(dimension)
649 .copied()
650 .filter(|score| score.is_finite())
651 .is_some_and(|score| score >= *threshold)
652 })
653 }
654}
655
656pub fn admission_filter_3d<TRaw>(opts: AdmissionScore3DOptions<TRaw>) -> impl Fn(&TRaw) -> bool {
658 move |raw| {
659 let scores = (opts.score_fn)(raw);
660 score_at_least(scores.persistence, opts.persistence_threshold)
661 && score_at_least(scores.personal_value, opts.personal_value_threshold)
662 && (!opts.require_structured || score_at_least(scores.structure, f64::MIN_POSITIVE))
663 }
664}
665
666pub fn shard_by_tenant<T: 'static>(
668 tenant_of: TenantShardFn<T>,
669 opts: ShardByTenantOptions,
670) -> ShardByTenantConfig<T> {
671 if !opts.tenants.is_empty() {
672 let mut tenants = Vec::<String>::new();
673 for tenant in opts.tenants {
674 if !tenants.iter().any(|known| known == &tenant) {
675 tenants.push(tenant);
676 }
677 }
678 let index: BTreeMap<_, _> = tenants
679 .iter()
680 .enumerate()
681 .map(|(i, tenant)| (tenant.clone(), i.to_string()))
682 .collect();
683 let overflow = tenants.len().to_string();
684 return ShardByTenantConfig {
685 shard_count: tenants.len() + 1,
686 shard_by: Rc::new(move |fragment| {
687 index
688 .get(&(tenant_of)(fragment))
689 .cloned()
690 .unwrap_or_else(|| overflow.clone())
691 }),
692 };
693 }
694 let shard_count = opts.shard_count.unwrap_or(4).max(1);
695 ShardByTenantConfig {
696 shard_count,
697 shard_by: Rc::new(move |fragment| (tenant_of)(fragment)),
698 }
699}
700
701fn score_at_least(score: f64, min: f64) -> bool {
702 score.is_finite() && score >= min
703}
704
705#[derive(Clone, Debug, Default, PartialEq)]
707pub struct MemoryRetrievalQuery {
708 pub tags: Vec<String>,
710 pub as_of: Option<u128>,
712 pub min_confidence: Option<f64>,
714 pub limit: Option<usize>,
716 pub vector: Option<Vec<f64>>,
718}
719
720impl MemoryRetrievalQuery {
721 pub fn memory_query(&self) -> MemoryQuery {
723 MemoryQuery {
724 tags: self.tags.clone(),
725 as_of: self.as_of,
726 min_confidence: self.min_confidence,
727 limit: self.limit,
728 }
729 }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq)]
733pub struct MemoryRetrievalCursor {
735 pub evaluation: u64,
737 pub valid_fragments: usize,
739 pub invalid_fragments: usize,
741 pub result_count: usize,
743}
744
745#[derive(Clone, Copy, Debug, PartialEq, Eq)]
746pub enum MemoryRetrievalStatusState {
748 Ready,
750 Empty,
752 Partial,
754 Error,
756}
757
758#[derive(Clone, Debug, PartialEq)]
759pub struct MemoryRetrievalStatus {
761 pub state: MemoryRetrievalStatusState,
763 pub query: MemoryRetrievalQuery,
765 pub cursor: MemoryRetrievalCursor,
767}
768
769#[derive(Clone, Debug, PartialEq)]
770pub struct MemoryRetrievalIndex<T> {
772 pub ids: Vec<FactId>,
774 pub by_id: BTreeMap<FactId, MemoryFragment<T>>,
776 pub cursor: MemoryRetrievalCursor,
778}
779
780#[derive(Clone, Copy, Debug, PartialEq, Eq)]
781pub enum MemoryRetrievalErrorCode {
783 DuplicateFragmentId,
785 InvalidFragment,
787 InvalidQuery,
789 InvalidQueryVector,
791}
792
793#[derive(Clone, Debug, PartialEq, Eq)]
794pub struct MemoryRetrievalError {
796 pub code: MemoryRetrievalErrorCode,
798 pub message: String,
800 pub index: Option<usize>,
802 pub fragment_id: Option<FactId>,
804 pub validation_errors: Vec<String>,
806 pub cursor: MemoryRetrievalCursor,
808}
809
810#[derive(Clone, Debug, PartialEq)]
811pub struct MemoryAnswer<T> {
813 pub query: MemoryRetrievalQuery,
815 pub results: Vec<MemoryFragment<T>>,
817}
818
819#[derive(Clone, Debug, PartialEq)]
820pub struct MemoryRetrievalSnapshot<T> {
822 pub fragments: Vec<MemoryFragment<T>>,
824 pub indexed: MemoryRetrievalIndex<T>,
826 pub ranked: MemoryAnswer<T>,
828 pub status: MemoryRetrievalStatus,
830 pub errors: Vec<MemoryRetrievalError>,
832 pub cursor: MemoryRetrievalCursor,
834}
835
836pub type MemoryRetrievalFact<T> = MemoryRetrievalSnapshot<T>;
838
839#[derive(Clone)]
840pub struct MemoryRetrievalBundleOptions<T> {
842 pub name: Option<String>,
844 pub fragments: Node<Vec<MemoryFragment<T>>>,
846 pub query: Node<MemoryRetrievalQuery>,
848}
849
850impl<T> MemoryRetrievalBundleOptions<T> {
851 pub fn new(fragments: Node<Vec<MemoryFragment<T>>>, query: Node<MemoryRetrievalQuery>) -> Self {
853 Self {
854 name: None,
855 fragments,
856 query,
857 }
858 }
859
860 pub fn named(mut self, name: impl Into<String>) -> Self {
862 self.name = Some(name.into());
863 self
864 }
865}
866
867#[derive(Clone)]
868pub struct MemoryRetrievalBundle<T> {
870 pub fragments_input: Node<Vec<MemoryFragment<T>>>,
872 pub query_input: Node<MemoryRetrievalQuery>,
874 pub snapshot: Node<MemoryRetrievalSnapshot<T>>,
876 pub fragments: Node<Vec<MemoryFragment<T>>>,
878 pub indexed: Node<MemoryRetrievalIndex<T>>,
880 pub ranked: Node<MemoryAnswer<T>>,
882 pub status: Node<MemoryRetrievalStatus>,
884 pub errors: Node<Vec<MemoryRetrievalError>>,
886 pub cursor: Node<MemoryRetrievalCursor>,
888}
889
890#[derive(Clone, Debug)]
891struct PendingMemoryRetrievalError {
892 code: MemoryRetrievalErrorCode,
893 message: String,
894 index: Option<usize>,
895 fragment_id: Option<FactId>,
896 validation_errors: Vec<String>,
897}
898
899pub fn memory_retrieval_bundle<T: Clone + 'static>(
905 graph: &Graph,
906 opts: MemoryRetrievalBundleOptions<T>,
907) -> MemoryRetrievalBundle<T> {
908 let name = opts.name.unwrap_or_else(|| "memoryRetrieval".to_owned());
909 let fragments = opts.fragments;
910 let query = opts.query;
911 let snapshot = graph.init_node(
912 Operator::with_opts("memoryRetrievalSnapshot", pattern_node_config(), |ctx| {
913 let evaluation = ctx
914 .state_get::<u64>()
915 .map(|evaluation| *evaluation + 1)
916 .unwrap_or(1);
917 let raw_fragments = ctx
918 .data::<Vec<MemoryFragment<T>>>(0)
919 .map(|fragments| (*fragments).clone())
920 .unwrap_or_default();
921 let raw_query = ctx
922 .data::<MemoryRetrievalQuery>(1)
923 .map(|query| (*query).clone())
924 .unwrap_or_default();
925 let query_errors = validate_query(&raw_query);
926 let current_query = if query_errors.is_empty() {
927 raw_query
928 } else {
929 MemoryRetrievalQuery::default()
930 };
931 let mut valid = Vec::new();
932 let mut seen = HashSet::new();
933 let mut errors = query_errors;
934
935 for (index, fragment) in raw_fragments.into_iter().enumerate() {
936 let fragment_errors = validate_fragment(&fragment);
937 if !fragment_errors.is_empty() {
938 errors.push(PendingMemoryRetrievalError {
939 code: MemoryRetrievalErrorCode::InvalidFragment,
940 message: "memory_retrieval_bundle: invalid memory fragment".to_owned(),
941 index: Some(index),
942 fragment_id: Some(fragment.id.clone()),
943 validation_errors: fragment_errors,
944 });
945 continue;
946 }
947 if !seen.insert(fragment.id.clone()) {
948 errors.push(PendingMemoryRetrievalError {
949 code: MemoryRetrievalErrorCode::DuplicateFragmentId,
950 message: "memory_retrieval_bundle: duplicate fragment id".to_owned(),
951 index: Some(index),
952 fragment_id: Some(fragment.id.clone()),
953 validation_errors: vec![format!("duplicate fragment id '{}'", fragment.id)],
954 });
955 continue;
956 }
957 valid.push(fragment);
958 }
959
960 let ranked = if errors
961 .iter()
962 .any(|error| !is_recoverable_fragment_error(error))
963 {
964 Vec::new()
965 } else {
966 rank_fragments(&valid, ¤t_query)
967 };
968 let cursor = MemoryRetrievalCursor {
969 evaluation,
970 valid_fragments: valid.len(),
971 invalid_fragments: errors
972 .iter()
973 .filter(|error| is_recoverable_fragment_error(error))
974 .count(),
975 result_count: ranked.len(),
976 };
977 ctx.state_set(evaluation);
978 let status = MemoryRetrievalStatus {
979 state: status_state(&errors, ranked.len()),
980 query: current_query.clone(),
981 cursor: cursor.clone(),
982 };
983 let indexed = MemoryRetrievalIndex {
984 ids: valid.iter().map(|fragment| fragment.id.clone()).collect(),
985 by_id: valid
986 .iter()
987 .map(|fragment| (fragment.id.clone(), fragment.clone()))
988 .collect(),
989 cursor: cursor.clone(),
990 };
991 let errors = errors
992 .into_iter()
993 .map(|error| MemoryRetrievalError {
994 code: error.code,
995 message: error.message,
996 index: error.index,
997 fragment_id: error.fragment_id,
998 validation_errors: error.validation_errors,
999 cursor: cursor.clone(),
1000 })
1001 .collect();
1002 ctx.emit(MemoryRetrievalSnapshot {
1003 fragments: valid,
1004 indexed,
1005 ranked: MemoryAnswer {
1006 query: current_query,
1007 results: ranked,
1008 },
1009 status,
1010 errors,
1011 cursor,
1012 });
1013 }),
1014 vec![fragments.erased(), query.erased()],
1015 named_graph_node_opts(format!("{name}/snapshot")),
1016 );
1017
1018 MemoryRetrievalBundle {
1019 fragments_input: fragments,
1020 query_input: query,
1021 fragments: retrieval_projection(
1022 graph,
1023 &snapshot,
1024 format!("{name}/fragments"),
1025 "memoryRetrievalFragments",
1026 |snapshot| snapshot.fragments.clone(),
1027 ),
1028 indexed: retrieval_projection(
1029 graph,
1030 &snapshot,
1031 format!("{name}/indexed"),
1032 "memoryRetrievalIndexed",
1033 |snapshot| snapshot.indexed.clone(),
1034 ),
1035 ranked: retrieval_projection(
1036 graph,
1037 &snapshot,
1038 format!("{name}/ranked"),
1039 "memoryRetrievalRanked",
1040 |snapshot| snapshot.ranked.clone(),
1041 ),
1042 status: retrieval_projection(
1043 graph,
1044 &snapshot,
1045 format!("{name}/status"),
1046 "memoryRetrievalStatus",
1047 |snapshot| snapshot.status.clone(),
1048 ),
1049 errors: retrieval_projection(
1050 graph,
1051 &snapshot,
1052 format!("{name}/errors"),
1053 "memoryRetrievalErrors",
1054 |snapshot| snapshot.errors.clone(),
1055 ),
1056 cursor: retrieval_projection(
1057 graph,
1058 &snapshot,
1059 format!("{name}/cursor"),
1060 "memoryRetrievalCursor",
1061 |snapshot| snapshot.cursor.clone(),
1062 ),
1063 snapshot,
1064 }
1065}
1066
1067pub fn knowledge_graph_reducer_bundle(
1069 graph: &Graph,
1070 opts: KnowledgeGraphReducerBundleOptions,
1071) -> KnowledgeGraphReducerBundle {
1072 let name = opts.name.unwrap_or_else(|| "knowledgeGraph".to_owned());
1073 let assertions = opts.assertions;
1074 let policy = opts.policy;
1075 let mut deps = vec![assertions.erased()];
1076 if let Some(policy) = &policy {
1077 deps.push(policy.erased());
1078 }
1079 let has_policy = policy.is_some();
1080 let snapshot = graph.init_node(
1081 Operator::with_opts(
1082 "knowledgeGraphReducerSnapshot",
1083 pattern_node_config(),
1084 move |ctx| {
1085 let evaluation = ctx
1086 .state_get::<u64>()
1087 .map(|evaluation| *evaluation + 1)
1088 .unwrap_or(1);
1089 let raw_assertions = ctx
1090 .data::<Vec<KnowledgeAssertion>>(0)
1091 .map(|assertions| (*assertions).clone())
1092 .unwrap_or_default();
1093 let policy = if has_policy {
1094 ctx.data::<KnowledgeGraphPolicy>(1)
1095 .map(|policy| (*policy).clone())
1096 .unwrap_or_default()
1097 } else {
1098 KnowledgeGraphPolicy::default()
1099 };
1100 let reduced = reduce_knowledge_assertions(raw_assertions, &policy);
1101 let cursor = KnowledgeGraphCursor {
1102 evaluation,
1103 valid_assertions: reduced.assertions.len(),
1104 invalid_assertions: reduced.errors.len(),
1105 entity_count: reduced.entities.len(),
1106 relation_count: reduced.relations.len(),
1107 predicate_count: reduced.topics.len(),
1108 };
1109 let errors = reduced
1110 .errors
1111 .into_iter()
1112 .map(|pending| KnowledgeGraphError {
1113 code: pending.code,
1114 message: pending.message,
1115 index: pending.index,
1116 assertion_id: pending.assertion_id,
1117 validation_errors: pending.validation_errors,
1118 cursor: cursor.clone(),
1119 })
1120 .collect::<Vec<_>>();
1121 let status = KnowledgeGraphStatus {
1122 state: if reduced.assertions.is_empty() && errors.is_empty() {
1123 KnowledgeGraphStatusState::Empty
1124 } else if errors.is_empty() {
1125 KnowledgeGraphStatusState::Ready
1126 } else if !reduced.assertions.is_empty() {
1127 KnowledgeGraphStatusState::Partial
1128 } else {
1129 KnowledgeGraphStatusState::Error
1130 },
1131 cursor: cursor.clone(),
1132 };
1133 ctx.state_set(evaluation);
1134 ctx.emit(KnowledgeGraphSnapshot {
1135 assertions: reduced.assertions,
1136 entities: reduced.entities,
1137 relations: reduced.relations,
1138 topics: reduced.topics,
1139 index: reduced.index,
1140 status,
1141 errors,
1142 cursor,
1143 });
1144 },
1145 ),
1146 deps,
1147 named_graph_node_opts(format!("{name}/snapshot")),
1148 );
1149
1150 KnowledgeGraphReducerBundle {
1151 assertions_input: assertions,
1152 policy_input: policy,
1153 assertions: kg_projection(
1154 graph,
1155 &snapshot,
1156 format!("{name}/assertions"),
1157 "knowledgeGraphAssertions",
1158 |snapshot| snapshot.assertions.clone(),
1159 ),
1160 entities: kg_projection(
1161 graph,
1162 &snapshot,
1163 format!("{name}/entities"),
1164 "knowledgeGraphEntities",
1165 |snapshot| snapshot.entities.clone(),
1166 ),
1167 relations: kg_projection(
1168 graph,
1169 &snapshot,
1170 format!("{name}/relations"),
1171 "knowledgeGraphRelations",
1172 |snapshot| snapshot.relations.clone(),
1173 ),
1174 topics: kg_projection(
1175 graph,
1176 &snapshot,
1177 format!("{name}/topics"),
1178 "knowledgeGraphTopics",
1179 |snapshot| snapshot.topics.clone(),
1180 ),
1181 index: kg_projection(
1182 graph,
1183 &snapshot,
1184 format!("{name}/index"),
1185 "knowledgeGraphIndex",
1186 |snapshot| snapshot.index.clone(),
1187 ),
1188 status: kg_projection(
1189 graph,
1190 &snapshot,
1191 format!("{name}/status"),
1192 "knowledgeGraphStatus",
1193 |snapshot| snapshot.status.clone(),
1194 ),
1195 errors: kg_projection(
1196 graph,
1197 &snapshot,
1198 format!("{name}/errors"),
1199 "knowledgeGraphErrors",
1200 |snapshot| snapshot.errors.clone(),
1201 ),
1202 cursor: kg_projection(
1203 graph,
1204 &snapshot,
1205 format!("{name}/cursor"),
1206 "knowledgeGraphCursor",
1207 |snapshot| snapshot.cursor.clone(),
1208 ),
1209 snapshot,
1210 }
1211}
1212
1213fn kg_projection<U, F>(
1214 graph: &Graph,
1215 snapshot: &Node<KnowledgeGraphSnapshot>,
1216 name: String,
1217 factory: &'static str,
1218 select: F,
1219) -> Node<U>
1220where
1221 U: 'static,
1222 F: Fn(&KnowledgeGraphSnapshot) -> U + 'static,
1223{
1224 graph.init_node(
1225 Operator::with_opts(factory, pattern_node_config(), move |ctx| {
1226 for snapshot in ctx.batch::<KnowledgeGraphSnapshot>(0) {
1227 ctx.emit(select(snapshot.as_ref()));
1228 }
1229 }),
1230 vec![snapshot.erased()],
1231 named_graph_node_opts(name),
1232 )
1233}
1234
1235#[derive(Clone, Debug)]
1236struct PendingKnowledgeGraphError {
1237 code: KnowledgeGraphErrorCode,
1238 message: String,
1239 index: Option<usize>,
1240 assertion_id: Option<FactId>,
1241 validation_errors: Vec<String>,
1242}
1243
1244struct ReducedKnowledgeGraph {
1245 assertions: Vec<KnowledgeAssertion>,
1246 entities: Vec<KnowledgeGraphEntity>,
1247 relations: Vec<KnowledgeGraphRelation>,
1248 topics: Vec<KnowledgeGraphTopic>,
1249 index: KnowledgeGraphIndex,
1250 errors: Vec<PendingKnowledgeGraphError>,
1251}
1252
1253fn reduce_knowledge_assertions(
1254 raw_assertions: Vec<KnowledgeAssertion>,
1255 policy: &KnowledgeGraphPolicy,
1256) -> ReducedKnowledgeGraph {
1257 let mut assertions = Vec::new();
1258 let mut errors = Vec::new();
1259 let mut seen = HashSet::new();
1260 for (index, assertion) in raw_assertions.into_iter().enumerate() {
1261 let validation = validate_knowledge_assertion(&assertion, policy);
1262 if !validation.is_empty() {
1263 errors.push(PendingKnowledgeGraphError {
1264 code: if validation.iter().any(|error| error.contains("policy")) {
1265 KnowledgeGraphErrorCode::PolicyConflict
1266 } else {
1267 KnowledgeGraphErrorCode::InvalidAssertion
1268 },
1269 message: "knowledge_graph_reducer_bundle: assertion is invalid".to_owned(),
1270 index: Some(index),
1271 assertion_id: Some(assertion.id.clone()),
1272 validation_errors: validation,
1273 });
1274 continue;
1275 }
1276 if !seen.insert(assertion.id.clone()) {
1277 errors.push(PendingKnowledgeGraphError {
1278 code: KnowledgeGraphErrorCode::DuplicateAssertionId,
1279 message: "knowledge_graph_reducer_bundle: duplicate assertion id".to_owned(),
1280 index: Some(index),
1281 assertion_id: Some(assertion.id.clone()),
1282 validation_errors: vec![format!("duplicate assertion id '{}'", assertion.id)],
1283 });
1284 continue;
1285 }
1286 assertions.push(assertion);
1287 }
1288 let (entities, relations, topics, index) = materialize_knowledge_graph(&assertions);
1289 ReducedKnowledgeGraph {
1290 assertions,
1291 entities,
1292 relations,
1293 topics,
1294 index,
1295 errors,
1296 }
1297}
1298
1299fn validate_knowledge_assertion(
1300 assertion: &KnowledgeAssertion,
1301 policy: &KnowledgeGraphPolicy,
1302) -> Vec<String> {
1303 let mut errors = Vec::new();
1304 if assertion.id.is_empty() {
1305 errors.push("id must be a non-empty string".to_owned());
1306 }
1307 if assertion.subject_id.is_empty() {
1308 errors.push("subject_id must be a non-empty string".to_owned());
1309 }
1310 if assertion.predicate.is_empty() {
1311 errors.push("predicate must be a non-empty string".to_owned());
1312 }
1313 if !policy.allowed_predicates.is_empty()
1314 && !policy.allowed_predicates.contains(&assertion.predicate)
1315 {
1316 errors.push(format!(
1317 "predicate '{}' is rejected by policy",
1318 assertion.predicate
1319 ));
1320 }
1321 if let KnowledgeAssertionObject::Entity { entity_id } = &assertion.object {
1322 if entity_id.is_empty() {
1323 errors.push("object entity_id must be a non-empty string".to_owned());
1324 }
1325 }
1326 if !assertion.confidence.is_finite() || !(0.0..=1.0).contains(&assertion.confidence) {
1327 errors.push("confidence must be finite in [0, 1]".to_owned());
1328 }
1329 errors
1330}
1331
1332fn materialize_knowledge_graph(
1333 assertions: &[KnowledgeAssertion],
1334) -> (
1335 Vec<KnowledgeGraphEntity>,
1336 Vec<KnowledgeGraphRelation>,
1337 Vec<KnowledgeGraphTopic>,
1338 KnowledgeGraphIndex,
1339) {
1340 type EntityBuckets = (HashSet<FactId>, HashSet<FactId>, HashSet<FactId>);
1341 type TopicBuckets = (HashSet<FactId>, HashSet<FactId>);
1342
1343 let mut entity_map: BTreeMap<FactId, EntityBuckets> = BTreeMap::new();
1344 let mut topic_map: BTreeMap<String, TopicBuckets> = BTreeMap::new();
1345 let mut relations = Vec::new();
1346 for assertion in assertions {
1347 let subject_entry = entity_map.entry(assertion.subject_id.clone()).or_default();
1348 subject_entry.0.insert(assertion.id.clone());
1349 subject_entry.1.insert(assertion.id.clone());
1350 let topic_entry = topic_map.entry(assertion.predicate.clone()).or_default();
1351 topic_entry.0.insert(assertion.id.clone());
1352 topic_entry.1.insert(assertion.subject_id.clone());
1353 if let KnowledgeAssertionObject::Entity { entity_id } = &assertion.object {
1354 let object_entry = entity_map.entry(entity_id.clone()).or_default();
1355 object_entry.0.insert(assertion.id.clone());
1356 object_entry.2.insert(assertion.id.clone());
1357 topic_entry.1.insert(entity_id.clone());
1358 }
1359 relations.push(KnowledgeGraphRelation {
1360 assertion_id: assertion.id.clone(),
1361 subject_id: assertion.subject_id.clone(),
1362 predicate: assertion.predicate.clone(),
1363 object: assertion.object.clone(),
1364 sources: assertion.sources.clone(),
1365 confidence: assertion.confidence,
1366 });
1367 }
1368 relations.sort_by(|a, b| a.assertion_id.cmp(&b.assertion_id));
1369 let entities = entity_map
1370 .into_iter()
1371 .map(
1372 |(id, (assertions, subjects, objects))| KnowledgeGraphEntity {
1373 id,
1374 assertion_ids: sorted_set(assertions),
1375 subject_assertion_ids: sorted_set(subjects),
1376 object_assertion_ids: sorted_set(objects),
1377 },
1378 )
1379 .collect::<Vec<_>>();
1380 let topics = topic_map
1381 .into_iter()
1382 .map(|(predicate, (assertions, entities))| KnowledgeGraphTopic {
1383 predicate,
1384 assertion_ids: sorted_set(assertions),
1385 entity_ids: sorted_set(entities),
1386 })
1387 .collect::<Vec<_>>();
1388 let index = KnowledgeGraphIndex {
1389 assertion_ids: {
1390 let mut ids = assertions
1391 .iter()
1392 .map(|assertion| assertion.id.clone())
1393 .collect::<Vec<_>>();
1394 ids.sort();
1395 ids
1396 },
1397 entity_ids: entities.iter().map(|entity| entity.id.clone()).collect(),
1398 relation_ids: relations
1399 .iter()
1400 .map(|relation| relation.assertion_id.clone())
1401 .collect(),
1402 predicates: topics.iter().map(|topic| topic.predicate.clone()).collect(),
1403 };
1404 (entities, relations, topics, index)
1405}
1406
1407fn sorted_set(set: HashSet<FactId>) -> Vec<FactId> {
1408 let mut out = set.into_iter().collect::<Vec<_>>();
1409 out.sort();
1410 out
1411}
1412
1413fn retrieval_projection<T, U, F>(
1414 graph: &Graph,
1415 snapshot: &Node<MemoryRetrievalSnapshot<T>>,
1416 name: String,
1417 factory: &'static str,
1418 select: F,
1419) -> Node<U>
1420where
1421 T: Clone + 'static,
1422 U: 'static,
1423 F: Fn(&MemoryRetrievalSnapshot<T>) -> U + 'static,
1424{
1425 graph.init_node(
1426 Operator::with_opts(factory, pattern_node_config(), move |ctx| {
1427 for snapshot in ctx.batch::<MemoryRetrievalSnapshot<T>>(0) {
1428 ctx.emit(select(snapshot.as_ref()));
1429 }
1430 }),
1431 vec![snapshot.erased()],
1432 named_graph_node_opts(name),
1433 )
1434}
1435
1436fn pattern_node_config() -> NodeOpts {
1437 NodeOpts {
1438 complete_when_deps_complete: false,
1439 error_when_deps_error: false,
1440 ..NodeOpts::default()
1441 }
1442}
1443
1444fn named_graph_node_opts(name: String) -> GraphNodeOpts {
1445 GraphNodeOpts {
1446 name: Some(name),
1447 ..GraphNodeOpts::default()
1448 }
1449}
1450
1451fn validate_query(query: &MemoryRetrievalQuery) -> Vec<PendingMemoryRetrievalError> {
1452 let mut errors = Vec::new();
1453 if let Some(min_confidence) = query.min_confidence {
1454 if !min_confidence.is_finite() || !(0.0..=1.0).contains(&min_confidence) {
1455 errors.push(PendingMemoryRetrievalError {
1456 code: MemoryRetrievalErrorCode::InvalidQuery,
1457 message: "memory_retrieval_bundle: query.min_confidence must be finite in [0, 1]"
1458 .to_owned(),
1459 index: None,
1460 fragment_id: None,
1461 validation_errors: vec!["min_confidence must be finite in [0, 1]".to_owned()],
1462 });
1463 }
1464 }
1465 if let Some(vector) = &query.vector {
1466 if vector.iter().any(|component| !component.is_finite()) {
1467 errors.push(PendingMemoryRetrievalError {
1468 code: MemoryRetrievalErrorCode::InvalidQueryVector,
1469 message: "memory_retrieval_bundle: query.vector must be a finite number array"
1470 .to_owned(),
1471 index: None,
1472 fragment_id: None,
1473 validation_errors: vec!["vector must be a finite number array".to_owned()],
1474 });
1475 }
1476 }
1477 errors
1478}
1479
1480fn validate_fragment<T>(fragment: &MemoryFragment<T>) -> Vec<String> {
1481 let mut errors = Vec::new();
1482 if fragment.id.is_empty() {
1483 errors.push("id must be a non-empty string".to_owned());
1484 }
1485 if !fragment.confidence.is_finite() || !(0.0..=1.0).contains(&fragment.confidence) {
1486 errors.push("confidence must be finite in [0, 1]".to_owned());
1487 }
1488 if let (Some(valid_from), Some(valid_to)) = (fragment.valid_from, fragment.valid_to) {
1489 if valid_from >= valid_to {
1490 errors.push("valid_from must be earlier than valid_to".to_owned());
1491 }
1492 }
1493 if fragment
1494 .embedding
1495 .as_ref()
1496 .is_some_and(|embedding| embedding.iter().any(|component| !component.is_finite()))
1497 {
1498 errors.push("embedding must be a finite number array when present".to_owned());
1499 }
1500 errors
1501}
1502
1503fn is_recoverable_fragment_error(error: &PendingMemoryRetrievalError) -> bool {
1504 matches!(
1505 error.code,
1506 MemoryRetrievalErrorCode::InvalidFragment | MemoryRetrievalErrorCode::DuplicateFragmentId
1507 )
1508}
1509
1510fn status_state(
1511 errors: &[PendingMemoryRetrievalError],
1512 result_count: usize,
1513) -> MemoryRetrievalStatusState {
1514 if errors
1515 .iter()
1516 .any(|error| !is_recoverable_fragment_error(error))
1517 {
1518 MemoryRetrievalStatusState::Error
1519 } else if !errors.is_empty() {
1520 MemoryRetrievalStatusState::Partial
1521 } else if result_count > 0 {
1522 MemoryRetrievalStatusState::Ready
1523 } else {
1524 MemoryRetrievalStatusState::Empty
1525 }
1526}
1527
1528fn rank_fragments<T: Clone>(
1529 fragments: &[MemoryFragment<T>],
1530 query: &MemoryRetrievalQuery,
1531) -> Vec<MemoryFragment<T>> {
1532 let mut ranked: Vec<_> = fragments
1533 .iter()
1534 .filter(|fragment| memory_fragment_matches_query(fragment, &query.memory_query()))
1535 .cloned()
1536 .collect();
1537 if query.vector.is_some() {
1538 ranked.sort_by(|a, b| {
1539 vector_score(b, query)
1540 .total_cmp(&vector_score(a, query))
1541 .then_with(|| b.confidence.total_cmp(&a.confidence))
1542 .then_with(|| b.t_ns.cmp(&a.t_ns))
1543 });
1544 } else {
1545 ranked.sort_by(|a, b| {
1546 b.confidence
1547 .total_cmp(&a.confidence)
1548 .then_with(|| b.t_ns.cmp(&a.t_ns))
1549 });
1550 }
1551 if let Some(limit) = query.limit {
1552 ranked.truncate(limit);
1553 }
1554 ranked
1555}
1556
1557fn vector_score<T>(fragment: &MemoryFragment<T>, query: &MemoryRetrievalQuery) -> f64 {
1558 match (&query.vector, &fragment.embedding) {
1559 (Some(query), Some(embedding)) => cosine_similarity(query, embedding),
1560 _ => 0.0,
1561 }
1562}