1use chrono::NaiveDate;
7use serde::{Deserialize, Serialize};
8
9use crate::case::CaseRef;
10use crate::ids::AccountId;
11use crate::locale::Locale;
12use crate::read::{DataSensitivity, TrustLevel};
13use crate::reduce::SourcePolicy;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct Citation {
18 pub source_id: String,
20 pub label: String,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub uri: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub locator: Option<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub version: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct KnowledgeRequest {
36 pub account_id: AccountId,
38 pub query: String,
40 pub locale: Locale,
42 pub case_refs: Vec<CaseRef>,
44 pub source_policy: SourcePolicy,
46 pub max_chunks: Option<usize>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub as_of: Option<NaiveDate>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct KnowledgeChunk {
60 pub chunk_id: String,
62 pub source_id: String,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub source_version: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub effective_from: Option<NaiveDate>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub effective_to: Option<NaiveDate>,
73 #[serde(default)]
75 pub permissions: Vec<String>,
76 pub citation: Citation,
78 pub text: String,
80 pub trust: TrustLevel,
82 pub sensitivity: DataSensitivity,
84}
85
86impl KnowledgeChunk {
87 #[must_use]
89 pub fn is_effective_on(&self, date: NaiveDate) -> bool {
90 self.effective_from.is_none_or(|from| from <= date)
91 && self.effective_to.is_none_or(|to| date <= to)
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
97#[serde(tag = "kind", rename_all = "snake_case")]
98#[non_exhaustive]
99pub enum KnowledgeError {
100 #[error("knowledge source {source_id} unavailable")]
102 Unavailable {
103 source_id: String,
105 },
106 #[error("knowledge access unauthorized")]
108 Unauthorized,
109 #[error("knowledge retrieval timed out")]
111 Timeout,
112 #[error("knowledge provider returned malformed data")]
114 Malformed,
115 #[error("knowledge failure {code}")]
117 Other {
118 code: String,
120 },
121}
122
123#[async_trait::async_trait]
125pub trait KnowledgeProvider: Send + Sync {
126 async fn retrieve(
128 &self,
129 request: KnowledgeRequest,
130 ) -> Result<Vec<KnowledgeChunk>, KnowledgeError>;
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn effective_window() {
139 let chunk = KnowledgeChunk {
140 chunk_id: "c".into(),
141 source_id: "s".into(),
142 source_version: None,
143 effective_from: NaiveDate::from_ymd_opt(2026, 1, 1),
144 effective_to: NaiveDate::from_ymd_opt(2026, 12, 31),
145 permissions: vec![],
146 citation: Citation {
147 source_id: "s".into(),
148 label: "Source".into(),
149 uri: None,
150 locator: None,
151 version: None,
152 },
153 text: "t".into(),
154 trust: TrustLevel::Retrieved,
155 sensitivity: DataSensitivity::Public,
156 };
157 assert!(chunk.is_effective_on(NaiveDate::from_ymd_opt(2026, 6, 1).unwrap()));
158 assert!(!chunk.is_effective_on(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()));
159 }
160}