contrag_core/entity.rs
1use candid::CandidType;
2use serde::Serialize;
3pub use crate::types::{EntityRelationship, RelationshipType};
4
5/// Trait that marks a struct as a RAG entity
6///
7/// Implement this trait for your canister data structures to enable
8/// automatic context building and relationship mapping.
9///
10/// # Example
11///
12/// ```rust
13/// use contrag_core::prelude::*;
14/// use candid::{CandidType, Deserialize};
15/// use serde::Serialize;
16///
17/// #[derive(CandidType, Deserialize, Serialize, Clone)]
18/// pub struct User {
19/// pub id: String,
20/// pub name: String,
21/// pub email: String,
22/// }
23///
24/// impl RagEntity for User {
25/// fn entity_type() -> &'static str {
26/// "User"
27/// }
28///
29/// fn entity_id(&self) -> String {
30/// self.id.clone()
31/// }
32///
33/// fn to_context_map(&self) -> Vec<(String, String)> {
34/// vec![
35/// ("id".to_string(), self.id.clone()),
36/// ("name".to_string(), self.name.clone()),
37/// ("email".to_string(), self.email.clone()),
38/// ]
39/// }
40///
41/// fn relationships(&self) -> Vec<EntityRelationship> {
42/// vec![]
43/// }
44/// }
45/// ```
46pub trait RagEntity: CandidType + Serialize + Clone {
47 /// Returns the entity type identifier
48 fn entity_type() -> &'static str
49 where
50 Self: Sized;
51
52 /// Returns the unique identifier for this entity instance
53 fn entity_id(&self) -> String;
54
55 /// Converts the entity to a flat key-value representation
56 ///
57 /// Use dot notation for nested fields: "profile.age"
58 fn to_context_map(&self) -> Vec<(String, String)>;
59
60 /// Returns relationships to other entities
61 fn relationships(&self) -> Vec<EntityRelationship>;
62
63 /// Converts the entity to a human-readable text representation
64 ///
65 /// Override this for custom formatting. Default implementation
66 /// joins the context map entries.
67 fn to_text(&self) -> String {
68 let mut lines = vec![
69 format!("Entity: {}", Self::entity_type()),
70 format!("ID: {}", self.entity_id()),
71 String::from("---"),
72 ];
73
74 for (key, value) in self.to_context_map() {
75 lines.push(format!("{}: {}", key, value));
76 }
77
78 lines.join("\n")
79 }
80
81 /// Returns a summary of the entity (first N characters)
82 fn to_summary(&self, max_length: usize) -> String {
83 let text = self.to_text();
84 if text.len() <= max_length {
85 text
86 } else {
87 format!("{}...", &text[..max_length])
88 }
89 }
90}
91
92/// Helper function to flatten nested JSON-like structures using serde_json
93///
94/// This is useful when you have complex nested structs and want to
95/// automatically flatten them for context building.
96///
97/// # Example
98///
99/// ```rust
100/// use serde_json::json;
101/// use contrag_core::entity::flatten_json_to_context;
102///
103/// let data = json!({
104/// "user": {
105/// "name": "Alice",
106/// "profile": {
107/// "age": 30,
108/// "location": "NYC"
109/// }
110/// }
111/// });
112///
113/// let flattened = flatten_json_to_context(&data, "");
114/// // Results in:
115/// // [
116/// // ("user.name", "Alice"),
117/// // ("user.profile.age", "30"),
118/// // ("user.profile.location", "NYC")
119/// // ]
120/// ```
121pub fn flatten_json_to_context(
122 value: &serde_json::Value,
123 prefix: &str,
124) -> Vec<(String, String)> {
125 let mut result = vec![];
126
127 match value {
128 serde_json::Value::Object(map) => {
129 for (key, val) in map {
130 let new_prefix = if prefix.is_empty() {
131 key.clone()
132 } else {
133 format!("{}.{}", prefix, key)
134 };
135 result.extend(flatten_json_to_context(val, &new_prefix));
136 }
137 }
138 serde_json::Value::Array(arr) => {
139 let items: Vec<String> = arr
140 .iter()
141 .map(|v| match v {
142 serde_json::Value::String(s) => s.clone(),
143 _ => v.to_string(),
144 })
145 .collect();
146 result.push((prefix.to_string(), items.join(", ")));
147 }
148 serde_json::Value::Null => {
149 result.push((prefix.to_string(), String::new()));
150 }
151 _ => {
152 result.push((prefix.to_string(), value.to_string().trim_matches('"').to_string()));
153 }
154 }
155
156 result
157}
158
159/// Helper macro to implement RagEntity with automatic flattening
160///
161/// This macro uses serde_json to automatically flatten your struct,
162/// which is useful for rapid prototyping or complex nested structures.
163#[macro_export]
164macro_rules! impl_rag_entity_auto {
165 ($struct_name:ident, $entity_type:expr, $id_field:ident) => {
166 impl RagEntity for $struct_name {
167 fn entity_type() -> &'static str {
168 $entity_type
169 }
170
171 fn entity_id(&self) -> String {
172 self.$id_field.clone()
173 }
174
175 fn to_context_map(&self) -> Vec<(String, String)> {
176 let json = serde_json::to_value(self).unwrap();
177 $crate::entity::flatten_json_to_context(&json, "")
178 }
179
180 fn relationships(&self) -> Vec<EntityRelationship> {
181 vec![]
182 }
183 }
184 };
185}