1use crate::fact::MemoryTier;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Message {
12 pub role: String,
13 pub content: String,
14}
15
16impl Message {
17 pub fn user(content: impl Into<String>) -> Self {
18 Self {
19 role: "user".into(),
20 content: content.into(),
21 }
22 }
23 pub fn assistant(content: impl Into<String>) -> Self {
24 Self {
25 role: "assistant".into(),
26 content: content.into(),
27 }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ExtractedFact {
34 pub text: String,
36 #[serde(default)]
38 pub entities: Vec<ExtractedEntity>,
39 #[serde(default)]
41 pub relationships: Vec<ExtractedRelationship>,
42 #[serde(default = "default_confidence")]
44 pub confidence: f64,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub category: Option<String>,
48}
49
50fn default_confidence() -> f64 {
51 1.0
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ExtractedEntity {
57 pub name: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub entity_type: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ExtractedRelationship {
67 pub source: String,
69 pub relation: String,
71 pub target: String,
73}
74
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct ExtractionResult {
78 pub facts: Vec<ExtractedFact>,
80}
81
82#[derive(Debug, Clone)]
84pub struct ExtractionConfig {
85 pub custom_prompt: Option<String>,
87 pub skip_categories: Vec<String>,
89 pub rules: Vec<ExtractionRule>,
91 pub dedup_threshold: f32,
93 pub default_tier: MemoryTier,
95}
96
97impl Default for ExtractionConfig {
98 fn default() -> Self {
99 Self {
100 custom_prompt: None,
101 skip_categories: Vec::new(),
102 rules: Vec::new(),
103 dedup_threshold: 0.92,
104 default_tier: MemoryTier::Conversation,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ExtractionRule {
112 pub category: String,
113 #[serde(default = "default_priority")]
115 pub priority: f64,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub ttl: Option<String>,
119 #[serde(default)]
121 pub pii: bool,
122}
123
124fn default_priority() -> f64 {
125 1.0
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ConflictVerdict {
131 Duplicate,
133 Contradicts,
135 Refines,
137 NoConflict,
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn extraction_config_defaults() {
147 let cfg = ExtractionConfig::default();
148 assert!((cfg.dedup_threshold - 0.92).abs() < f32::EPSILON);
149 assert_eq!(cfg.default_tier, MemoryTier::Conversation);
150 assert!(cfg.skip_categories.is_empty());
151 }
152
153 #[test]
154 fn message_constructors() {
155 let m = Message::user("hello");
156 assert_eq!(m.role, "user");
157 let m = Message::assistant("hi");
158 assert_eq!(m.role, "assistant");
159 }
160
161 #[test]
162 fn extracted_fact_deserializes_with_defaults() {
163 let json = r#"{"text": "User likes pizza"}"#;
164 let fact: ExtractedFact = serde_json::from_str(json).unwrap();
165 assert_eq!(fact.confidence, 1.0);
166 assert!(fact.entities.is_empty());
167 }
168}