Skip to main content

dakera_client/
knowledge.rs

1//! Knowledge graph operations for the Dakera client.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::types::{KgExportResponse, KgPathResponse, KgQueryResponse};
7use crate::DakeraClient;
8
9// ============================================================================
10// Knowledge Graph Types
11// ============================================================================
12
13/// Request to build a knowledge graph
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct KnowledgeGraphRequest {
16    pub agent_id: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub memory_id: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub depth: Option<u32>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub min_similarity: Option<f32>,
23}
24
25/// A node in the knowledge graph
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct KnowledgeNode {
28    pub id: String,
29    pub content: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub memory_type: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub importance: Option<f32>,
34    #[serde(default)]
35    pub metadata: serde_json::Value,
36}
37
38/// An edge in the knowledge graph
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct KnowledgeEdge {
41    pub source: String,
42    pub target: String,
43    pub similarity: f32,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub relationship: Option<String>,
46}
47
48/// Response from knowledge graph operations
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct KnowledgeGraphResponse {
51    pub nodes: Vec<KnowledgeNode>,
52    pub edges: Vec<KnowledgeEdge>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub clusters: Option<Vec<Vec<String>>>,
55}
56
57/// Request to build a full knowledge graph
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct FullKnowledgeGraphRequest {
60    pub agent_id: String,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub max_nodes: Option<u32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub min_similarity: Option<f32>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub cluster_threshold: Option<f32>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub max_edges_per_node: Option<u32>,
69}
70
71/// Request to summarize memories
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SummarizeRequest {
74    pub agent_id: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub memory_ids: Option<Vec<String>>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub target_type: Option<String>,
79    #[serde(default)]
80    pub dry_run: bool,
81}
82
83/// Response from summarization
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct SummarizeResponse {
86    pub summary: String,
87    pub source_count: usize,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub new_memory_id: Option<String>,
90}
91
92/// Request to deduplicate memories
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct DeduplicateRequest {
95    pub agent_id: String,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub threshold: Option<f32>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub memory_type: Option<String>,
100    #[serde(default)]
101    pub dry_run: bool,
102}
103
104/// Response from deduplication
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct DeduplicateResponse {
107    pub duplicates_found: usize,
108    pub removed_count: usize,
109    pub groups: Vec<Vec<String>>,
110}
111
112// ============================================================================
113// Cross-Agent Network Types (DASH-A)
114// ============================================================================
115
116/// Request to build a cross-agent knowledge network
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CrossAgentNetworkRequest {
119    /// Agent IDs to include; `None` means all agents.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub agent_ids: Option<Vec<String>>,
122    /// Minimum cosine similarity for cross-agent edges (default 0.3).
123    pub min_similarity: f32,
124    /// Maximum memory nodes returned per agent (default 50).
125    pub max_nodes_per_agent: usize,
126    /// Minimum importance score for included nodes (default 0.0).
127    pub min_importance: f32,
128    /// Maximum cross-agent edges in the response (default 200).
129    pub max_cross_edges: usize,
130}
131
132impl Default for CrossAgentNetworkRequest {
133    fn default() -> Self {
134        Self {
135            agent_ids: None,
136            min_similarity: 0.3,
137            max_nodes_per_agent: 50,
138            min_importance: 0.0,
139            max_cross_edges: 200,
140        }
141    }
142}
143
144/// Summary information about a single agent in the network
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AgentNetworkInfo {
147    pub agent_id: String,
148    pub memory_count: usize,
149    pub avg_importance: f32,
150}
151
152/// A memory node in the cross-agent network graph
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AgentNetworkNode {
155    pub id: String,
156    pub agent_id: String,
157    pub content: String,
158    pub importance: f32,
159    pub tags: Vec<String>,
160    pub memory_type: String,
161    /// Unix milliseconds.
162    pub created_at: u64,
163}
164
165/// A cross-agent similarity edge between two memory nodes
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct AgentNetworkEdge {
168    pub source: String,
169    pub target: String,
170    pub source_agent: String,
171    pub target_agent: String,
172    pub similarity: f32,
173}
174
175/// Aggregate statistics for the cross-agent network
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct AgentNetworkStats {
178    pub total_agents: usize,
179    pub total_nodes: usize,
180    pub total_cross_edges: usize,
181    pub density: f32,
182}
183
184/// Response from the cross-agent network endpoint
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct CrossAgentNetworkResponse {
187    pub agents: Vec<AgentNetworkInfo>,
188    pub nodes: Vec<AgentNetworkNode>,
189    pub edges: Vec<AgentNetworkEdge>,
190    pub stats: AgentNetworkStats,
191    /// Total number of memory nodes in the network (added in server v0.6.2).
192    #[serde(default)]
193    pub node_count: usize,
194}
195
196// ============================================================================
197// Knowledge Graph Client Methods
198// ============================================================================
199
200impl DakeraClient {
201    /// Build a knowledge graph from a seed memory
202    pub async fn knowledge_graph(
203        &self,
204        request: KnowledgeGraphRequest,
205    ) -> Result<KnowledgeGraphResponse> {
206        let url = format!("{}/v1/knowledge/graph", self.base_url);
207        let response = self.client.post(&url).json(&request).send().await?;
208        self.handle_response(response).await
209    }
210
211    /// Build a full knowledge graph for an agent
212    pub async fn full_knowledge_graph(
213        &self,
214        request: FullKnowledgeGraphRequest,
215    ) -> Result<KnowledgeGraphResponse> {
216        let url = format!("{}/v1/knowledge/graph/full", self.base_url);
217        let response = self.client.post(&url).json(&request).send().await?;
218        self.handle_response(response).await
219    }
220
221    /// Summarize memories
222    pub async fn summarize(&self, request: SummarizeRequest) -> Result<SummarizeResponse> {
223        let url = format!("{}/v1/knowledge/summarize", self.base_url);
224        let response = self.client.post(&url).json(&request).send().await?;
225        self.handle_response(response).await
226    }
227
228    /// Deduplicate memories
229    pub async fn deduplicate(&self, request: DeduplicateRequest) -> Result<DeduplicateResponse> {
230        let url = format!("{}/v1/knowledge/deduplicate", self.base_url);
231        let response = self.client.post(&url).json(&request).send().await?;
232        self.handle_response(response).await
233    }
234
235    /// Build a cross-agent knowledge network (DASH-A).
236    ///
237    /// Calls `POST /v1/knowledge/network/cross-agent` (Admin scope) and returns
238    /// a graph of memory nodes and cross-agent similarity edges.
239    pub async fn cross_agent_network(
240        &self,
241        request: CrossAgentNetworkRequest,
242    ) -> Result<CrossAgentNetworkResponse> {
243        let url = format!("{}/v1/knowledge/network/cross-agent", self.base_url);
244        let response = self.client.post(&url).json(&request).send().await?;
245        self.handle_response(response).await
246    }
247
248    // =========================================================================
249    // KG-2: Graph Query & Export
250    // =========================================================================
251
252    /// Query the memory knowledge graph using a filter DSL (KG-2).
253    ///
254    /// Calls `GET /v1/knowledge/query`.
255    ///
256    /// # Arguments
257    /// - `agent_id` — agent whose graph to query.
258    /// - `root_id` — optional root memory ID for BFS traversal.
259    /// - `edge_type` — comma-separated edge types to filter (e.g. `"related_to,shares_entity"`).
260    /// - `min_weight` — minimum edge weight (0.0–1.0).
261    /// - `max_depth` — BFS depth when `root_id` is set (1–5, default 3).
262    /// - `limit` — maximum edges to return (default 100, max 1000).
263    pub async fn knowledge_query(
264        &self,
265        agent_id: &str,
266        root_id: Option<&str>,
267        edge_type: Option<&str>,
268        min_weight: Option<f32>,
269        max_depth: Option<u32>,
270        limit: Option<usize>,
271    ) -> Result<KgQueryResponse> {
272        let mut url = format!("{}/v1/knowledge/query?agent_id={}", self.base_url, agent_id);
273        if let Some(v) = root_id {
274            url.push_str(&format!("&root_id={}", v));
275        }
276        if let Some(v) = edge_type {
277            url.push_str(&format!("&edge_type={}", v));
278        }
279        if let Some(v) = min_weight {
280            url.push_str(&format!("&min_weight={}", v));
281        }
282        if let Some(v) = max_depth {
283            url.push_str(&format!("&max_depth={}", v));
284        }
285        if let Some(v) = limit {
286            url.push_str(&format!("&limit={}", v));
287        }
288        let response = self.client.get(&url).send().await?;
289        self.handle_response(response).await
290    }
291
292    /// Find the BFS shortest path between two memory IDs (KG-2).
293    ///
294    /// Calls `GET /v1/knowledge/path`.
295    ///
296    /// Returns an error if no path exists between the two memories.
297    pub async fn knowledge_path(
298        &self,
299        agent_id: &str,
300        from_id: &str,
301        to_id: &str,
302    ) -> Result<KgPathResponse> {
303        let url = format!(
304            "{}/v1/knowledge/path?agent_id={}&from={}&to={}",
305            self.base_url, agent_id, from_id, to_id
306        );
307        let response = self.client.get(&url).send().await?;
308        self.handle_response(response).await
309    }
310
311    /// Export the memory knowledge graph as JSON or GraphML (KG-2).
312    ///
313    /// Calls `GET /v1/knowledge/export`.
314    ///
315    /// For `format = "graphml"` the server returns `application/xml`. This
316    /// method deserializes JSON only — use a raw HTTP client for GraphML.
317    pub async fn knowledge_export(
318        &self,
319        agent_id: &str,
320        format: Option<&str>,
321    ) -> Result<KgExportResponse> {
322        let fmt = format.unwrap_or("json");
323        let url = format!(
324            "{}/v1/knowledge/export?agent_id={}&format={}",
325            self.base_url, agent_id, fmt
326        );
327        let response = self.client.get(&url).send().await?;
328        self.handle_response(response).await
329    }
330}