1#![allow(deprecated)]
2
3use crate::error::MemoryError;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use stack_ids::{
7 ClaimId, ClaimVersionId, EntityId, EnvelopeId, EpisodeId, RelationVersionId, ScopeKey,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct CompatTraceId(pub String);
22
23#[deprecated(since = "0.5.0", note = "Use stack_ids::TraceCtx instead")]
24pub type TraceId = CompatTraceId;
25
26impl CompatTraceId {
27 pub fn new(value: impl Into<String>) -> Self {
29 Self(value.into())
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36}
37
38impl std::fmt::Display for CompatTraceId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(&self.0)
41 }
42}
43
44impl From<String> for CompatTraceId {
45 fn from(value: String) -> Self {
46 Self(value)
47 }
48}
49
50impl From<&str> for CompatTraceId {
51 fn from(value: &str) -> Self {
52 Self(value.to_string())
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum Role {
60 System,
62 User,
64 Assistant,
66 Tool,
68}
69
70impl Role {
71 pub fn as_str(&self) -> &'static str {
73 match self {
74 Role::System => "system",
75 Role::User => "user",
76 Role::Assistant => "assistant",
77 Role::Tool => "tool",
78 }
79 }
80
81 pub fn from_str_value(s: &str) -> Option<Self> {
83 match s {
84 "system" => Some(Role::System),
85 "user" => Some(Role::User),
86 "assistant" => Some(Role::Assistant),
87 "tool" => Some(Role::Tool),
88 _ => None,
89 }
90 }
91}
92
93impl std::fmt::Display for Role {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str(self.as_str())
96 }
97}
98
99impl std::str::FromStr for Role {
100 type Err = MemoryError;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::from_str_value(s).ok_or_else(|| MemoryError::Other(format!("Unknown role: '{}'", s)))
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum SearchSourceType {
110 Facts,
112 Chunks,
114 Messages,
116 Episodes,
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ReceiptMode {
124 #[default]
126 Disabled,
127 ExplainOnly,
129 ReturnReceipt,
131}
132
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ExactnessProfile {
137 #[default]
139 Default,
140 PreferExact,
142 AllowApproximate,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SearchContext {
149 pub evaluation_time: DateTime<Utc>,
151 pub receipt_mode: ReceiptMode,
153 pub exactness_profile: ExactnessProfile,
155 pub request_id: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub trace_id: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub attempt_family_id: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub attempt_id: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub replay_of: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub query_text_digest: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub query_input_digest: Option<String>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub filter_digest: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub redaction_state: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub budget_id: Option<String>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub deadline_at: Option<DateTime<Utc>>,
187}
188
189impl SearchContext {
190 pub fn default_now() -> Self {
192 Self {
193 evaluation_time: Utc::now(),
194 receipt_mode: ReceiptMode::Disabled,
195 exactness_profile: ExactnessProfile::Default,
196 request_id: None,
197 trace_id: None,
198 attempt_family_id: None,
199 attempt_id: None,
200 replay_of: None,
201 query_text_digest: None,
202 query_input_digest: None,
203 filter_digest: None,
204 redaction_state: None,
205 budget_id: None,
206 deadline_at: None,
207 }
208 }
209
210 pub fn at(evaluation_time: DateTime<Utc>) -> Self {
212 Self {
213 evaluation_time,
214 ..Self::default_now()
215 }
216 }
217
218 pub fn receipts_enabled(&self) -> bool {
220 self.receipt_mode != ReceiptMode::Disabled
221 }
222}
223
224impl Default for SearchContext {
225 fn default() -> Self {
226 Self::default_now()
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct VectorSearchReceiptV1 {
233 #[serde(default = "default_vector_search_receipt_schema")]
235 pub schema_version: String,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub receipt_digest: Option<String>,
239 pub receipt_id: String,
241 pub evaluation_time: DateTime<Utc>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub trace_id: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub attempt_family_id: Option<String>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub attempt_id: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub replay_of: Option<String>,
255 pub query_embedding_digest: Option<String>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub query_text_digest: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub query_input_digest: Option<String>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub filter_digest: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub redaction_state: Option<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub budget_id: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub deadline_at: Option<DateTime<Utc>>,
275 pub search_profile: String,
277 pub candidate_backend: String,
279 pub codec_family: Option<String>,
281 pub codec_profile_digest: Option<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub artifact_profile_digest: Option<String>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub artifact_count: Option<usize>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub artifact_corruption_count: Option<usize>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub artifact_missing_count: Option<usize>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub vector_artifact_manifest_digest: Option<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub artifact_generation_id: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub approximate_scanned_count: Option<usize>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub approximate_returned_count: Option<usize>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub raw_rows_loaded_count: Option<usize>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub filter_strategy: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub vector_artifact_count: Option<usize>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub vector_artifact_missing_count: Option<usize>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub vector_artifact_stale_count: Option<usize>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub exact_rerank_count: Option<usize>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub approximate_candidate_count: Option<usize>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub fallback_reason: Option<String>,
331 pub approximate: bool,
333 pub requested_candidates: usize,
335 pub returned_candidates: usize,
337 pub post_filter_candidates: usize,
339 pub fallback: Option<String>,
341 pub exact_rerank: bool,
343 pub result_ids: Vec<String>,
345 pub degradations: Vec<String>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct DerivedVectorArtifactGenerationV1 {
352 pub schema_version: String,
354 pub generation_id: String,
356 pub codec_family: String,
358 pub codec_profile_digest: String,
360 pub source_snapshot_digest: String,
362 pub source_row_count: usize,
364 pub artifact_count: usize,
366 pub source_tables: Vec<String>,
368 pub dim: usize,
370 pub encoding: String,
372 pub created_at: DateTime<Utc>,
374 pub build_receipt_id: Option<String>,
376 pub artifact_manifest_digest: String,
378 pub status: String,
380 pub degradations: Vec<String>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct VectorArtifactBuildReceiptV1 {
387 pub schema_version: String,
389 pub codec_family: String,
391 pub codec_profile_digest: String,
393 pub source_row_count: usize,
395 pub artifact_count: usize,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub generation_id: Option<String>,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub source_snapshot_digest: Option<String>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub artifact_manifest_digest: Option<String>,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub build_receipt_id: Option<String>,
409 pub skipped_row_count: usize,
411 pub elapsed_ms: u128,
413 pub created_at: DateTime<Utc>,
415 pub degradations: Vec<String>,
417}
418
419fn default_vector_search_receipt_schema() -> String {
420 "vector_search_receipt_v1".to_string()
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct SearchReceiptAnswersV1 {
426 pub receipt_id: String,
428 pub replay_receipt_id: String,
430 pub evaluation_time: DateTime<Utc>,
432 pub search_profile: String,
434 pub candidate_backend: String,
436 pub codec_family: Option<String>,
438 pub codec_profile_digest: Option<String>,
440 pub exactness: String,
442 pub approximate: bool,
444 pub exact_rerank: bool,
446 pub fallback: Option<String>,
448 pub degraded: bool,
450 pub replay_ready: bool,
452 pub rebuild_ready: bool,
454 pub result_ids: Vec<String>,
456 pub result_count: usize,
458 pub degradations: Vec<String>,
460 pub why_results_appeared: Vec<String>,
462}
463
464impl VectorSearchReceiptV1 {
465 pub fn answers(&self) -> SearchReceiptAnswersV1 {
467 let exactness = match (self.approximate, self.exact_rerank) {
468 (true, true) => "approximate_candidate_generation_with_exact_rerank",
469 (true, false) => "approximate",
470 (false, true) => "exact_reference_with_rerank",
471 (false, false) => "exact_reference",
472 }
473 .to_string();
474
475 let mut why_results_appeared = Vec::new();
476 why_results_appeared.push(format!(
477 "retrieval used candidate backend '{}'",
478 self.candidate_backend
479 ));
480 if self.exact_rerank {
481 why_results_appeared.push("final vector ordering used exact f32 scoring".to_string());
482 }
483 if let Some(fallback) = &self.fallback {
484 why_results_appeared.push(format!("fallback path '{}' was used", fallback));
485 }
486 if let Some(codec_profile_digest) = &self.codec_profile_digest {
487 why_results_appeared.push(format!(
488 "derived vector artifacts used codec profile '{}'",
489 codec_profile_digest
490 ));
491 } else {
492 why_results_appeared.push("no derived codec profile was used".to_string());
493 }
494 if let Some(query_embedding_digest) = &self.query_embedding_digest {
495 why_results_appeared.push(format!(
496 "query embedding digest '{}' is recorded for replay checks",
497 query_embedding_digest
498 ));
499 }
500
501 SearchReceiptAnswersV1 {
502 receipt_id: self.receipt_id.clone(),
503 replay_receipt_id: self.receipt_id.clone(),
504 evaluation_time: self.evaluation_time,
505 search_profile: self.search_profile.clone(),
506 candidate_backend: self.candidate_backend.clone(),
507 codec_family: self.codec_family.clone(),
508 codec_profile_digest: self.codec_profile_digest.clone(),
509 exactness,
510 approximate: self.approximate,
511 exact_rerank: self.exact_rerank,
512 fallback: self.fallback.clone(),
513 degraded: self.fallback.is_some() || !self.degradations.is_empty(),
514 replay_ready: self.query_embedding_digest.is_some(),
515 rebuild_ready: self.query_embedding_digest.is_some()
516 && self.exact_rerank
517 && self.fallback.is_none()
518 && (self
519 .vector_artifact_count
520 .or(self.artifact_count)
521 .is_some_and(|count| count > 0)
522 || (self.codec_family.is_none()
523 && self.candidate_backend.contains("brute_force_f32")
524 && !self.result_ids.is_empty())),
525 result_ids: self.result_ids.clone(),
526 result_count: self.result_ids.len(),
527 degradations: self.degradations.clone(),
528 why_results_appeared,
529 }
530 }
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct SearchResponse {
536 pub results: Vec<SearchResult>,
538 pub receipt: Option<VectorSearchReceiptV1>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct ChunkManifestEntry {
548 pub external_chunk_id: String,
550 pub content: String,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub token_count_estimate: Option<usize>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub content_digest: Option<String>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub metadata: Option<serde_json::Value>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct ChunkManifestIngestOptions {
566 pub title: String,
568 pub namespace: String,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub source_path: Option<String>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub metadata: Option<serde_json::Value>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct ChunkManifestChunkMapping {
581 pub external_chunk_id: String,
583 pub sm_document_id: String,
585 pub sm_chunk_id: String,
587 pub chunk_index: usize,
589 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub content_digest: Option<String>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub metadata: Option<serde_json::Value>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct ChunkManifestIngestResult {
600 pub sm_document_id: String,
602 pub namespace: String,
604 pub receipt_id: String,
606 pub chunks: Vec<ChunkManifestChunkMapping>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct ExplainedSearchResponse {
613 pub results: Vec<ExplainedResult>,
615 pub receipt: Option<VectorSearchReceiptV1>,
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct SearchReplayReportV1 {
622 pub receipt_id: String,
624 pub replay_receipt_id: String,
626 pub original_receipt: VectorSearchReceiptV1,
628 pub replay_receipt: VectorSearchReceiptV1,
630 pub query_embedding_digest_matches: bool,
632 pub result_ids_match: bool,
634 pub missing_result_ids: Vec<String>,
636 pub added_result_ids: Vec<String>,
638 pub vector_only: bool,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct ProjectionQuery {
645 pub scope: ScopeKey,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub text_query: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub valid_at: Option<String>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub recorded_at_or_before: Option<String>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub subject_entity_id: Option<EntityId>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub canonical_entity_id: Option<EntityId>,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub claim_state: Option<String>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub claim_id: Option<ClaimId>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub claim_version_id: Option<ClaimVersionId>,
671 pub limit: usize,
673}
674
675impl ProjectionQuery {
676 pub fn new(scope: ScopeKey) -> Self {
677 Self {
678 scope,
679 text_query: None,
680 valid_at: None,
681 recorded_at_or_before: None,
682 subject_entity_id: None,
683 canonical_entity_id: None,
684 claim_state: None,
685 claim_id: None,
686 claim_version_id: None,
687 limit: 10,
688 }
689 }
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct ProjectionClaimVersion {
695 pub claim_version_id: ClaimVersionId,
696 pub claim_id: ClaimId,
697 pub claim_state: String,
698 pub projection_family: String,
699 pub subject_entity_id: EntityId,
700 pub predicate: String,
701 pub object_anchor: serde_json::Value,
702 pub scope_key: ScopeKey,
703 pub valid_from: Option<String>,
704 pub valid_to: Option<String>,
705 pub recorded_at: String,
706 pub preferred_open: bool,
707 pub source_envelope_id: EnvelopeId,
708 pub source_authority: String,
709 pub trace_id: Option<String>,
710 pub freshness: String,
711 pub contradiction_status: String,
712 pub supersedes_claim_version_id: Option<ClaimVersionId>,
713 pub content: String,
714 pub confidence: f32,
715 pub metadata: Option<serde_json::Value>,
716 pub source_exported_at: Option<String>,
717 pub transformed_at: Option<String>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct ProjectionRelationVersion {
723 pub relation_version_id: RelationVersionId,
724 pub subject_entity_id: EntityId,
725 pub predicate: String,
726 pub object_anchor: serde_json::Value,
727 pub scope_key: ScopeKey,
728 pub claim_id: Option<ClaimId>,
729 pub source_episode_id: Option<EpisodeId>,
730 pub valid_from: Option<String>,
731 pub valid_to: Option<String>,
732 pub recorded_at: String,
733 pub preferred_open: bool,
734 pub supersedes_relation_version_id: Option<RelationVersionId>,
735 pub contradiction_status: String,
736 pub source_confidence: f32,
737 pub projection_family: String,
738 pub source_envelope_id: EnvelopeId,
739 pub source_authority: String,
740 pub trace_id: Option<String>,
741 pub freshness: String,
742 pub metadata: Option<serde_json::Value>,
743 pub source_exported_at: Option<String>,
744 pub transformed_at: Option<String>,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
749pub struct ProjectionEpisode {
750 pub episode_id: EpisodeId,
751 pub document_id: String,
752 pub cause_ids: Vec<String>,
753 pub effect_type: String,
754 pub outcome: String,
755 pub confidence: f32,
756 pub experiment_id: Option<String>,
757 pub scope_key: ScopeKey,
758 pub source_envelope_id: EnvelopeId,
759 pub source_authority: String,
760 pub trace_id: Option<String>,
761 pub recorded_at: String,
762 pub metadata: Option<serde_json::Value>,
763 pub source_exported_at: Option<String>,
764 pub transformed_at: Option<String>,
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
769pub struct ProjectionEntityAlias {
770 pub canonical_entity_id: EntityId,
771 pub alias_text: String,
772 pub alias_source: String,
773 pub match_evidence: Option<serde_json::Value>,
774 pub confidence: f32,
775 pub merge_decision: String,
776 pub scope_key: ScopeKey,
777 pub review_state: String,
778 pub is_human_confirmed: bool,
779 pub is_human_confirmed_final: bool,
780 pub superseded_by_entity_id: Option<EntityId>,
781 pub split_from_entity_id: Option<EntityId>,
782 pub source_envelope_id: EnvelopeId,
783 pub recorded_at: String,
784 pub source_exported_at: Option<String>,
785 pub transformed_at: Option<String>,
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct ProjectionEvidenceRef {
791 pub claim_id: ClaimId,
792 pub claim_version_id: Option<ClaimVersionId>,
793 pub fetch_handle: String,
794 pub source_authority: String,
795 pub source_envelope_id: EnvelopeId,
796 pub scope_key: ScopeKey,
797 pub recorded_at: String,
798 pub metadata: Option<serde_json::Value>,
799 pub source_exported_at: Option<String>,
800 pub transformed_at: Option<String>,
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct Session {
806 pub id: String,
808 pub channel: String,
810 pub created_at: String,
812 pub updated_at: String,
814 pub metadata: Option<serde_json::Value>,
816 pub message_count: u32,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct Message {
823 pub id: i64,
825 pub session_id: String,
827 pub role: Role,
829 pub content: String,
831 pub token_count: Option<u32>,
833 pub created_at: String,
835 pub metadata: Option<serde_json::Value>,
837}
838
839#[derive(Debug, Clone, Serialize, Deserialize)]
841pub struct Fact {
842 pub id: String,
844 pub namespace: String,
846 pub content: String,
848 pub source: Option<String>,
850 pub created_at: String,
852 pub updated_at: String,
854 pub metadata: Option<serde_json::Value>,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct Document {
861 pub id: String,
863 pub title: String,
865 pub source_path: Option<String>,
867 pub namespace: String,
869 pub created_at: String,
871 pub metadata: Option<serde_json::Value>,
873 pub chunk_count: u32,
875}
876
877#[derive(Debug, Clone, Serialize, Deserialize)]
879pub struct TextChunk {
880 pub index: usize,
882 pub content: String,
884 pub token_count_estimate: usize,
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct SearchResult {
891 pub content: String,
893
894 pub source: SearchSource,
896
897 pub score: f64,
899
900 pub bm25_rank: Option<usize>,
902
903 pub vector_rank: Option<usize>,
905
906 pub cosine_similarity: Option<f64>,
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize)]
912#[serde(rename_all = "snake_case")]
913pub enum SearchSource {
914 Fact {
916 fact_id: String,
918 namespace: String,
920 },
921 Chunk {
923 chunk_id: String,
925 document_id: String,
927 document_title: String,
929 chunk_index: usize,
931 },
932 Message {
934 message_id: i64,
936 session_id: String,
938 role: String,
940 },
941 Episode {
943 episode_id: String,
946 document_id: String,
948 effect_type: String,
950 outcome: String,
952 },
953 Projection {
955 projection_kind: String,
957 projection_id: String,
959 scope_key: ScopeKey,
961 valid_from: Option<String>,
963 valid_to: Option<String>,
965 recorded_at: String,
967 source_envelope_id: String,
969 source_authority: String,
971 },
972}
973
974impl SearchSource {
975 pub fn result_id(&self) -> String {
977 match self {
978 Self::Fact { fact_id, .. } => format!("fact:{fact_id}"),
979 Self::Chunk { chunk_id, .. } => format!("chunk:{chunk_id}"),
980 Self::Message { message_id, .. } => format!("msg:{message_id}"),
981 Self::Episode { episode_id, .. } => format!("episode:{episode_id}"),
982 Self::Projection { projection_id, .. } => format!("projection:{projection_id}"),
983 }
984 }
985
986 pub fn source_kind(&self) -> &'static str {
988 match self {
989 Self::Fact { .. } => "fact",
990 Self::Chunk { .. } => "chunk",
991 Self::Message { .. } => "message",
992 Self::Episode { .. } => "episode",
993 Self::Projection { .. } => "projection",
994 }
995 }
996
997 pub fn source_id(&self) -> String {
999 match self {
1000 Self::Fact { fact_id, .. } => fact_id.clone(),
1001 Self::Chunk { chunk_id, .. } => chunk_id.clone(),
1002 Self::Message { message_id, .. } => message_id.to_string(),
1003 Self::Episode { episode_id, .. } => episode_id.clone(),
1004 Self::Projection { projection_id, .. } => projection_id.clone(),
1005 }
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct EpisodeMeta {
1014 pub cause_ids: Vec<String>,
1016 pub effect_type: String,
1018 pub outcome: EpisodeOutcome,
1020 pub confidence: f32,
1022 pub verification_status: VerificationStatus,
1024 pub experiment_id: Option<String>,
1026 pub valid_time: Option<chrono::DateTime<chrono::Utc>>,
1028 pub fact_digest: Option<String>,
1030}
1031
1032#[derive(Debug, Clone, Serialize, Deserialize)]
1034pub struct EpisodeAsOfReceiptV1 {
1035 pub query_id: String,
1036 pub as_of_valid: chrono::DateTime<chrono::Utc>,
1037 pub as_of_recorded: chrono::DateTime<chrono::Utc>,
1038 pub episode_count: usize,
1039 pub episode_ids: Vec<String>,
1040 pub excluded_superseded: usize,
1041}
1042
1043#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1045#[serde(rename_all = "lowercase")]
1046pub enum EpisodeOutcome {
1047 Confirmed,
1049 Refuted,
1051 Inconclusive,
1053 Pending,
1055}
1056
1057impl EpisodeOutcome {
1058 pub fn as_str(&self) -> &'static str {
1060 match self {
1061 Self::Confirmed => "confirmed",
1062 Self::Refuted => "refuted",
1063 Self::Inconclusive => "inconclusive",
1064 Self::Pending => "pending",
1065 }
1066 }
1067
1068 pub fn from_str_value(s: &str) -> Option<Self> {
1070 match s {
1071 "confirmed" => Some(Self::Confirmed),
1072 "refuted" => Some(Self::Refuted),
1073 "inconclusive" => Some(Self::Inconclusive),
1074 "pending" => Some(Self::Pending),
1075 _ => None,
1076 }
1077 }
1078}
1079
1080impl std::fmt::Display for EpisodeOutcome {
1081 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082 f.write_str(self.as_str())
1083 }
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1088#[serde(tag = "status", rename_all = "lowercase")]
1089pub enum VerificationStatus {
1090 Unverified,
1092 Verified {
1094 method: String,
1096 at: String,
1098 },
1099 Failed {
1101 reason: String,
1103 at: String,
1105 },
1106}
1107
1108#[derive(Debug, Clone, Serialize, Deserialize)]
1112pub struct ScoreBreakdown {
1113 pub rrf_score: f64,
1115 pub bm25_score: Option<f64>,
1117 pub vector_score: Option<f64>,
1119 pub recency_score: Option<f64>,
1121 pub bm25_rank: Option<usize>,
1123 pub vector_rank: Option<usize>,
1125 pub vector_source_rank: Option<usize>,
1127 pub vector_source_score: Option<f64>,
1129 pub bm25_contribution: Option<f64>,
1131 pub vector_contribution: Option<f64>,
1133 pub vector_reranked_from_f32: bool,
1135 pub bm25_weight: f64,
1137 pub vector_weight: f64,
1139 pub recency_weight: Option<f64>,
1141 pub rrf_k: f64,
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1147pub struct ExplainedResult {
1148 pub result: SearchResult,
1150 pub breakdown: ScoreBreakdown,
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct ExplainedResultAnswerV1 {
1157 pub result_id: String,
1159 pub source_kind: String,
1161 pub source_id: String,
1163 pub why_this_result: Vec<String>,
1165 pub text_match: bool,
1167 pub vector_match: bool,
1169 pub recency_applied: bool,
1171 pub exact_vector_rerank: bool,
1173 pub final_score: f64,
1175}
1176
1177impl ExplainedResult {
1178 pub fn answer(&self) -> ExplainedResultAnswerV1 {
1180 let text_match = self.breakdown.bm25_rank.is_some();
1181 let vector_match = self.breakdown.vector_rank.is_some();
1182 let recency_applied = self.breakdown.recency_score.is_some();
1183 let mut why_this_result = Vec::new();
1184
1185 if let Some(rank) = self.breakdown.bm25_rank {
1186 why_this_result.push(format!("text match rank {rank} contributed to fusion"));
1187 }
1188 if let Some(rank) = self.breakdown.vector_rank {
1189 why_this_result.push(format!("vector match rank {rank} contributed to fusion"));
1190 }
1191 if recency_applied {
1192 why_this_result.push("recency contributed to the fused score".to_string());
1193 }
1194 if self.breakdown.vector_reranked_from_f32 {
1195 why_this_result.push("vector score was checked with exact f32 rerank".to_string());
1196 }
1197 if why_this_result.is_empty() {
1198 why_this_result.push("result survived filtering and deterministic ranking".to_string());
1199 }
1200
1201 ExplainedResultAnswerV1 {
1202 result_id: self.result.source.result_id(),
1203 source_kind: self.result.source.source_kind().to_string(),
1204 source_id: self.result.source.source_id(),
1205 why_this_result,
1206 text_match,
1207 vector_match,
1208 recency_applied,
1209 exact_vector_rerank: self.breakdown.vector_reranked_from_f32,
1210 final_score: self.result.score,
1211 }
1212 }
1213}
1214
1215pub trait GraphView: Send + Sync {
1219 fn neighbors(
1221 &self,
1222 node_id: &str,
1223 direction: GraphDirection,
1224 max_depth: usize,
1225 ) -> Result<Vec<GraphEdge>, MemoryError>;
1226
1227 fn path(
1229 &self,
1230 from: &str,
1231 to: &str,
1232 max_depth: usize,
1233 ) -> Result<Option<Vec<String>>, MemoryError>;
1234}
1235
1236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1238pub enum GraphDirection {
1239 Outgoing,
1241 Incoming,
1243 Both,
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1249pub struct GraphEdge {
1250 pub source: String,
1252 pub target: String,
1254 pub edge_type: GraphEdgeType,
1256 pub weight: f64,
1258 pub metadata: Option<serde_json::Value>,
1260}
1261
1262#[derive(Debug, Clone, Serialize, Deserialize)]
1264#[serde(rename_all = "snake_case")]
1265pub enum GraphEdgeType {
1266 Semantic {
1268 cosine_similarity: f32,
1270 },
1271 Temporal {
1273 delta_secs: u64,
1275 },
1276 Causal {
1278 confidence: f32,
1280 evidence_ids: Vec<String>,
1282 },
1283 Entity {
1285 relation: String,
1287 },
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct EmbeddingDisplacement {
1293 pub cosine_similarity: f32,
1295 pub euclidean_distance: f32,
1297 pub magnitude_a: f32,
1299 pub magnitude_b: f32,
1301}
1302
1303#[derive(Debug, Clone, Serialize, Deserialize)]
1305pub struct MemoryStats {
1306 pub total_facts: u64,
1308 pub total_documents: u64,
1310 pub total_chunks: u64,
1312 pub total_sessions: u64,
1314 pub total_messages: u64,
1316 pub database_size_bytes: u64,
1318 pub embedding_model: Option<String>,
1320 pub embedding_dimensions: Option<usize>,
1322}
1323
1324#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1326pub struct NamespaceDeleteReport {
1327 pub facts: usize,
1329 pub documents: usize,
1331 pub chunks: usize,
1333 pub messages: usize,
1335 pub sessions: usize,
1337 pub episodes: usize,
1339 pub projection_rows: usize,
1341 pub hnsw_ops: usize,
1343}