Skip to main content

knowledge_runtime/evidence/
support.rs

1use semantic_memory::SearchResult;
2use serde::{Deserialize, Serialize};
3
4/// A bundle of supporting evidence assembled from search results.
5///
6/// A `SearchEvidenceBundle` gathers the results that back an answer or
7/// projection. It does NOT contain the answer itself — it is the
8/// "show your work" component.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SearchEvidenceBundle {
11    /// Unique bundle identifier.
12    pub id: String,
13    /// Supporting search results, ordered by relevance.
14    pub items: Vec<EvidenceItem>,
15    /// Overall confidence across all items (0.0 to 1.0).
16    pub aggregate_confidence: f32,
17}
18
19/// A single evidence item derived from a search result.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct EvidenceItem {
22    /// Content of the evidence.
23    pub content: String,
24    /// Which semantic-memory source produced this.
25    pub source: semantic_memory::SearchSource,
26    /// Relevance score from the search pipeline.
27    pub score: f64,
28    /// How this evidence relates to the query.
29    pub relevance: EvidenceRelevance,
30}
31
32/// How a piece of evidence relates to the query.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum EvidenceRelevance {
36    /// Directly answers the query.
37    Direct,
38    /// Provides supporting context.
39    Supporting,
40    /// Provides contrasting/refuting information.
41    Contrasting,
42    /// Tangentially related.
43    Tangential,
44}
45
46/// An assembled answer bundle with evidence.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AnswerBundle {
49    /// The answer or summary text (assembled by the caller, not by this crate).
50    pub answer: String,
51    /// Evidence backing the answer.
52    pub evidence: SearchEvidenceBundle,
53    /// Confidence in the answer given the evidence (0.0 to 1.0).
54    pub confidence: f32,
55}
56
57/// Build a `SearchEvidenceBundle` from search results.
58///
59/// All items are initially tagged as `Direct` relevance. Callers should
60/// reclassify relevance as appropriate for their use case.
61pub fn bundle_from_results(results: &[SearchResult]) -> SearchEvidenceBundle {
62    let items: Vec<EvidenceItem> = results
63        .iter()
64        .map(|r| EvidenceItem {
65            content: r.content.clone(),
66            source: r.source.clone(),
67            score: r.score,
68            relevance: EvidenceRelevance::Direct,
69        })
70        .collect();
71
72    let aggregate_confidence = if items.is_empty() {
73        0.0
74    } else {
75        let sum: f64 = items.iter().map(|i| i.score).sum();
76        (sum / items.len() as f64).min(1.0) as f32
77    };
78
79    SearchEvidenceBundle {
80        id: uuid::Uuid::new_v4().to_string(),
81        items,
82        aggregate_confidence,
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use semantic_memory::{SearchResult, SearchSource};
90
91    #[test]
92    fn bundle_from_empty_results() {
93        let bundle = bundle_from_results(&[]);
94        assert!(bundle.items.is_empty());
95        assert_eq!(bundle.aggregate_confidence, 0.0);
96    }
97
98    #[test]
99    fn bundle_preserves_order() {
100        let results = vec![
101            SearchResult {
102                content: "first".into(),
103                source: SearchSource::Fact {
104                    fact_id: "f1".into(),
105                    namespace: "test".into(),
106                },
107                score: 0.9,
108                bm25_rank: None,
109                vector_rank: None,
110                cosine_similarity: None,
111            },
112            SearchResult {
113                content: "second".into(),
114                source: SearchSource::Fact {
115                    fact_id: "f2".into(),
116                    namespace: "test".into(),
117                },
118                score: 0.7,
119                bm25_rank: None,
120                vector_rank: None,
121                cosine_similarity: None,
122            },
123        ];
124        let bundle = bundle_from_results(&results);
125        assert_eq!(bundle.items.len(), 2);
126        assert_eq!(bundle.items[0].content, "first");
127        assert_eq!(bundle.items[1].content, "second");
128    }
129}