1use crate::wasm_types::{
11 GraphQueryInput, GraphQueryOutput, GraphStoreInput, GraphStoreOutput,
12 MemoryRecallInput, MemoryRecallOutput, MemoryRememberInput, MemoryRememberOutput,
13 Triple, ObjectValue,
14};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone)]
19pub struct CortexClientConfig {
20 pub base_url: String,
22 pub auth_token: Option<String>,
24 pub timeout_ms: u64,
26}
27
28impl Default for CortexClientConfig {
29 fn default() -> Self {
30 Self {
31 base_url: "http://127.0.0.1:8080".to_string(),
32 auth_token: None,
33 timeout_ms: 5000,
34 }
35 }
36}
37
38#[derive(Serialize, Deserialize, Debug, Clone)]
40struct CortexTriple {
41 subject: String,
42 predicate: String,
43 object: serde_json::Value,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 hash: Option<String>,
46}
47
48#[derive(Serialize, Debug)]
50struct PatternQueryRequest {
51 #[serde(skip_serializing_if = "Option::is_none")]
52 subject: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 predicate: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 object: Option<serde_json::Value>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 limit: Option<u32>,
59}
60
61#[derive(Deserialize, Debug)]
63struct PatternQueryResponse {
64 matches: Vec<CortexTriple>,
65 #[serde(default)]
66 total: u64,
67}
68
69#[derive(Serialize, Debug)]
71struct CreateTripleRequest {
72 subject: String,
73 predicate: String,
74 object: serde_json::Value,
75}
76
77#[derive(Deserialize, Debug)]
79struct CreateTripleResponse {
80 hash: String,
81}
82
83#[derive(Serialize, Debug)]
85struct MemoryRecallRequest {
86 query: String,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 entry_type: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 limit: Option<u32>,
91}
92
93#[derive(Serialize, Debug)]
95struct MemoryRememberRequest {
96 data: String,
97 entry_type: String,
98 tags: Vec<String>,
99 importance: f32,
100}
101
102#[derive(Deserialize, Debug)]
104struct MemoryEntryResponse {
105 id: String,
106 data: String,
107 entry_type: String,
108 #[serde(default)]
109 tags: Vec<String>,
110 #[serde(default)]
111 importance: f32,
112 #[serde(default)]
113 created_at: String,
114}
115
116#[derive(Deserialize, Debug)]
118struct MemoryRecallResponse {
119 results: Vec<MemoryEntryResponse>,
120}
121
122#[derive(Deserialize, Debug)]
124struct MemoryRememberResponse {
125 id: String,
126}
127
128pub struct CortexInternalClient {
130 config: CortexClientConfig,
131 http: reqwest::Client,
132}
133
134impl CortexInternalClient {
135 pub fn new(config: CortexClientConfig) -> Self {
137 let http = reqwest::Client::builder()
138 .timeout(std::time::Duration::from_millis(config.timeout_ms))
139 .build()
140 .unwrap_or_default();
141 Self { config, http }
142 }
143
144 pub fn default_client() -> Self {
146 Self::new(CortexClientConfig::default())
147 }
148
149 fn url(&self, path: &str) -> String {
150 format!("{}{}", self.config.base_url, path)
151 }
152
153 fn apply_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
154 match &self.config.auth_token {
155 Some(token) => req.header("Authorization", token.as_str()),
156 None => req,
157 }
158 }
159
160 fn object_to_json(obj: &ObjectValue) -> serde_json::Value {
161 match obj {
162 ObjectValue::Node(s) => serde_json::json!({"type": "node", "value": s}),
163 ObjectValue::Literal(s) => serde_json::json!(s),
164 ObjectValue::Number(n) => serde_json::json!(n),
165 ObjectValue::Boolean(b) => serde_json::json!(b),
166 }
167 }
168
169 fn json_to_object(val: &serde_json::Value) -> ObjectValue {
170 if let Some(obj) = val.as_object() {
171 if obj.get("type").and_then(|t| t.as_str()) == Some("node") {
172 if let Some(v) = obj.get("value").and_then(|v| v.as_str()) {
173 return ObjectValue::Node(v.to_string());
174 }
175 }
176 }
177 if let Some(s) = val.as_str() {
178 return ObjectValue::Literal(s.to_string());
179 }
180 if let Some(n) = val.as_f64() {
181 return ObjectValue::Number(n);
182 }
183 if let Some(b) = val.as_bool() {
184 return ObjectValue::Boolean(b);
185 }
186 ObjectValue::Literal(val.to_string())
187 }
188
189 fn cortex_to_triple(ct: &CortexTriple) -> Triple {
190 Triple {
191 subject: ct.subject.clone(),
192 predicate: ct.predicate.clone(),
193 object: Self::json_to_object(&ct.object),
194 }
195 }
196
197 pub async fn graph_query(&self, input: GraphQueryInput) -> Result<GraphQueryOutput, String> {
199 let (subject, predicate) = if let Some(ref pattern) = input.pattern {
200 (pattern.subject.clone().or(input.subject), pattern.predicate.clone().or(input.predicate))
201 } else {
202 (input.subject, input.predicate)
203 };
204
205 let body = PatternQueryRequest {
206 subject,
207 predicate,
208 object: input.pattern.as_ref()
209 .and_then(|p| p.object.as_ref())
210 .map(Self::object_to_json),
211 limit: input.limit,
212 };
213
214 let req = self.apply_auth(
215 self.http.post(self.url("/api/v1/query")).json(&body),
216 );
217
218 let resp = req.send().await.map_err(|e| format!("Cortex query failed: {}", e))?;
219
220 if !resp.status().is_success() {
221 return Err(format!("Cortex query returned {}", resp.status()));
222 }
223
224 let result: PatternQueryResponse = resp.json().await
225 .map_err(|e| format!("Failed to parse Cortex response: {}", e))?;
226
227 Ok(GraphQueryOutput {
228 triples: result.matches.iter().map(Self::cortex_to_triple).collect(),
229 total: result.total,
230 })
231 }
232
233 pub async fn graph_store(&self, input: GraphStoreInput) -> Result<GraphStoreOutput, String> {
235 let body = CreateTripleRequest {
236 subject: input.subject,
237 predicate: input.predicate,
238 object: Self::object_to_json(&input.object),
239 };
240
241 let req = self.apply_auth(
242 self.http.post(self.url("/api/v1/triples")).json(&body),
243 );
244
245 let resp = req.send().await.map_err(|e| format!("Cortex store failed: {}", e))?;
246
247 if !resp.status().is_success() {
248 return Err(format!("Cortex store returned {}", resp.status()));
249 }
250
251 let result: CreateTripleResponse = resp.json().await
252 .map_err(|e| format!("Failed to parse Cortex response: {}", e))?;
253
254 Ok(GraphStoreOutput {
255 triple_id: result.hash,
256 })
257 }
258
259 pub async fn memory_recall(&self, input: MemoryRecallInput) -> Result<MemoryRecallOutput, String> {
261 let body = MemoryRecallRequest {
262 query: input.query,
263 entry_type: input.entry_type,
264 limit: input.limit,
265 };
266
267 let req = self.apply_auth(
268 self.http.post(self.url("/api/v1/memory/recall")).json(&body),
269 );
270
271 let resp = req.send().await.map_err(|e| format!("Titans recall failed: {}", e))?;
272
273 if !resp.status().is_success() {
274 return Err(format!("Titans recall returned {}", resp.status()));
275 }
276
277 let result: MemoryRecallResponse = resp.json().await
278 .map_err(|e| format!("Failed to parse Titans response: {}", e))?;
279
280 Ok(MemoryRecallOutput {
281 results: result.results.iter().map(|r| {
282 crate::wasm_types::MemoryResult {
283 id: r.id.clone(),
284 data: r.data.clone(),
285 entry_type: r.entry_type.clone(),
286 tags: r.tags.clone(),
287 importance: r.importance,
288 created_at: r.created_at.clone(),
289 }
290 }).collect(),
291 })
292 }
293
294 pub async fn memory_remember(&self, input: MemoryRememberInput) -> Result<MemoryRememberOutput, String> {
296 let body = MemoryRememberRequest {
297 data: input.data,
298 entry_type: input.entry_type,
299 tags: input.tags,
300 importance: input.importance,
301 };
302
303 let req = self.apply_auth(
304 self.http.post(self.url("/api/v1/memory/remember")).json(&body),
305 );
306
307 let resp = req.send().await.map_err(|e| format!("Titans remember failed: {}", e))?;
308
309 if !resp.status().is_success() {
310 return Err(format!("Titans remember returned {}", resp.status()));
311 }
312
313 let result: MemoryRememberResponse = resp.json().await
314 .map_err(|e| format!("Failed to parse Titans response: {}", e))?;
315
316 Ok(MemoryRememberOutput { id: result.id })
317 }
318
319 pub async fn health_check(&self) -> bool {
321 match self.apply_auth(self.http.get(self.url("/api/v1/health"))).send().await {
322 Ok(resp) => resp.status().is_success(),
323 Err(_) => false,
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_default_config() {
334 let config = CortexClientConfig::default();
335 assert_eq!(config.base_url, "http://127.0.0.1:8080");
336 assert!(config.auth_token.is_none());
337 assert_eq!(config.timeout_ms, 5000);
338 }
339
340 #[test]
341 fn test_object_value_conversion() {
342 let json = CortexInternalClient::object_to_json(&ObjectValue::Literal("hello".into()));
343 assert_eq!(json, serde_json::json!("hello"));
344
345 let obj = CortexInternalClient::json_to_object(&serde_json::json!("hello"));
346 assert_eq!(obj, ObjectValue::Literal("hello".into()));
347
348 let json = CortexInternalClient::object_to_json(&ObjectValue::Number(42.0));
349 assert_eq!(json, serde_json::json!(42.0));
350
351 let json = CortexInternalClient::object_to_json(&ObjectValue::Boolean(true));
352 assert_eq!(json, serde_json::json!(true));
353
354 let json = CortexInternalClient::object_to_json(&ObjectValue::Node("ns:foo".into()));
355 assert_eq!(json, serde_json::json!({"type": "node", "value": "ns:foo"}));
356 }
357}