Skip to main content

aingle_cortex/
client.rs

1// Copyright 2019-2026 Apilium Technologies OÜ. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR Commercial
3
4//! Internal Rust client for AIngle Cortex.
5//!
6//! Provides programmatic access to the Cortex semantic graph and Titans
7//! memory system, used by WASM host functions to bridge zome code with
8//! the knowledge layer.
9
10use crate::wasm_types::{
11    GraphQueryInput, GraphQueryOutput, GraphStoreInput, GraphStoreOutput,
12    MemoryRecallInput, MemoryRecallOutput, MemoryRememberInput, MemoryRememberOutput,
13    Triple, ObjectValue,
14};
15use serde::{Deserialize, Serialize};
16
17/// Configuration for the Cortex internal client.
18#[derive(Debug, Clone)]
19pub struct CortexClientConfig {
20    /// Base URL of the Cortex REST API (e.g., "http://127.0.0.1:8080").
21    pub base_url: String,
22    /// Optional authentication token.
23    pub auth_token: Option<String>,
24    /// Request timeout in milliseconds.
25    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/// Internal triple representation matching the Cortex REST API.
39#[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/// Pattern query request body.
49#[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/// Pattern query response from Cortex.
62#[derive(Deserialize, Debug)]
63struct PatternQueryResponse {
64    matches: Vec<CortexTriple>,
65    #[serde(default)]
66    total: u64,
67}
68
69/// Create triple request body.
70#[derive(Serialize, Debug)]
71struct CreateTripleRequest {
72    subject: String,
73    predicate: String,
74    object: serde_json::Value,
75}
76
77/// Create triple response from Cortex.
78#[derive(Deserialize, Debug)]
79struct CreateTripleResponse {
80    hash: String,
81}
82
83/// Memory recall request body for Titans API.
84#[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/// Memory remember request body for Titans API.
94#[derive(Serialize, Debug)]
95struct MemoryRememberRequest {
96    data: String,
97    entry_type: String,
98    tags: Vec<String>,
99    importance: f32,
100}
101
102/// Memory response from Titans API.
103#[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/// Memory recall response from Titans API.
117#[derive(Deserialize, Debug)]
118struct MemoryRecallResponse {
119    results: Vec<MemoryEntryResponse>,
120}
121
122/// Memory remember response from Titans API.
123#[derive(Deserialize, Debug)]
124struct MemoryRememberResponse {
125    id: String,
126}
127
128/// The internal Cortex client used by WASM host functions.
129pub struct CortexInternalClient {
130    config: CortexClientConfig,
131    http: reqwest::Client,
132}
133
134impl CortexInternalClient {
135    /// Create a new Cortex internal client.
136    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    /// Create a client with default configuration.
145    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    /// Query the semantic graph.
198    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    /// Store a triple in the semantic graph.
234    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    /// Recall memories from the Titans system.
260    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    /// Store a new memory in the Titans system.
295    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    /// Check if Cortex is healthy and reachable.
320    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}