1use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::types::{KgExportResponse, KgPathResponse, KgQueryResponse};
7use crate::DakeraClient;
8
9#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CrossAgentNetworkRequest {
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub agent_ids: Option<Vec<String>>,
122 pub min_similarity: f32,
124 pub max_nodes_per_agent: usize,
126 pub min_importance: f32,
128 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#[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#[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 pub created_at: u64,
163}
164
165#[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#[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#[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 #[serde(default)]
193 pub node_count: usize,
194}
195
196impl DakeraClient {
201 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 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 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 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 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 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 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 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}
331
332#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
345 fn test_knowledge_graph_request_minimal_omits_optional() {
346 let req = KnowledgeGraphRequest {
347 agent_id: "agent-1".to_string(),
348 memory_id: None,
349 depth: None,
350 min_similarity: None,
351 };
352 let json = serde_json::to_string(&req).unwrap();
353 assert!(json.contains("\"agent_id\":\"agent-1\""));
354 assert!(!json.contains("memory_id"));
355 assert!(!json.contains("depth"));
356 assert!(!json.contains("min_similarity"));
357 }
358
359 #[test]
360 fn test_knowledge_graph_request_with_all_fields() {
361 let req = KnowledgeGraphRequest {
362 agent_id: "agent-1".to_string(),
363 memory_id: Some("mem-abc".to_string()),
364 depth: Some(3),
365 min_similarity: Some(0.7),
366 };
367 let json = serde_json::to_string(&req).unwrap();
368 assert!(json.contains("\"memory_id\":\"mem-abc\""));
369 assert!(json.contains("\"depth\":3"));
370 assert!(json.contains("\"min_similarity\":0.7"));
371 }
372
373 #[test]
378 fn test_knowledge_node_deserializes_minimal() {
379 let json = r#"{
380 "id": "n1",
381 "content": "user likes coffee"
382 }"#;
383 let node: KnowledgeNode = serde_json::from_str(json).unwrap();
384 assert_eq!(node.id, "n1");
385 assert!(node.memory_type.is_none());
386 assert!(node.importance.is_none());
387 assert_eq!(node.metadata, serde_json::Value::Null);
388 }
389
390 #[test]
391 fn test_knowledge_node_deserializes_with_optional_fields() {
392 let json = r#"{
393 "id": "n2",
394 "content": "works at Dakera",
395 "memory_type": "semantic",
396 "importance": 0.9
397 }"#;
398 let node: KnowledgeNode = serde_json::from_str(json).unwrap();
399 assert_eq!(node.memory_type.as_deref(), Some("semantic"));
400 assert!((node.importance.unwrap() - 0.9).abs() < 1e-6);
401 }
402
403 #[test]
408 fn test_knowledge_edge_without_relationship_omits_field() {
409 let edge = KnowledgeEdge {
410 source: "n1".to_string(),
411 target: "n2".to_string(),
412 similarity: 0.85,
413 relationship: None,
414 };
415 let json = serde_json::to_string(&edge).unwrap();
416 assert!(json.contains("\"similarity\":0.85"));
417 assert!(!json.contains("relationship"));
418 }
419
420 #[test]
421 fn test_knowledge_edge_with_relationship() {
422 let edge = KnowledgeEdge {
423 source: "n1".to_string(),
424 target: "n2".to_string(),
425 similarity: 0.92,
426 relationship: Some("colleague".to_string()),
427 };
428 let json = serde_json::to_string(&edge).unwrap();
429 assert!(json.contains("\"relationship\":\"colleague\""));
430 }
431
432 #[test]
437 fn test_knowledge_graph_response_deserializes_empty() {
438 let json = r#"{"nodes": [], "edges": []}"#;
439 let resp: KnowledgeGraphResponse = serde_json::from_str(json).unwrap();
440 assert!(resp.nodes.is_empty());
441 assert!(resp.edges.is_empty());
442 assert!(resp.clusters.is_none());
443 }
444
445 #[test]
450 fn test_full_knowledge_graph_request_all_optional_omitted() {
451 let req = FullKnowledgeGraphRequest {
452 agent_id: "a".to_string(),
453 max_nodes: None,
454 min_similarity: None,
455 cluster_threshold: None,
456 max_edges_per_node: None,
457 };
458 let json = serde_json::to_string(&req).unwrap();
459 assert!(!json.contains("max_nodes"));
460 assert!(!json.contains("min_similarity"));
461 assert!(!json.contains("cluster_threshold"));
462 }
463
464 #[test]
469 fn test_summarize_request_dry_run_default_false() {
470 let req = SummarizeRequest {
471 agent_id: "a".to_string(),
472 memory_ids: None,
473 target_type: None,
474 dry_run: false,
475 };
476 let json = serde_json::to_string(&req).unwrap();
477 assert!(json.contains("\"dry_run\":false"));
478 assert!(!json.contains("memory_ids"));
479 assert!(!json.contains("target_type"));
480 }
481
482 #[test]
483 fn test_summarize_request_with_memory_ids() {
484 let req = SummarizeRequest {
485 agent_id: "a".to_string(),
486 memory_ids: Some(vec!["m1".to_string(), "m2".to_string()]),
487 target_type: Some("semantic".to_string()),
488 dry_run: true,
489 };
490 let json = serde_json::to_string(&req).unwrap();
491 assert!(json.contains("\"dry_run\":true"));
492 assert!(json.contains("\"m1\""));
493 assert!(json.contains("\"target_type\":\"semantic\""));
494 }
495
496 #[test]
501 fn test_summarize_response_deserializes() {
502 let json = r#"{"summary": "user is a developer", "source_count": 5}"#;
503 let resp: SummarizeResponse = serde_json::from_str(json).unwrap();
504 assert_eq!(resp.summary, "user is a developer");
505 assert_eq!(resp.source_count, 5);
506 assert!(resp.new_memory_id.is_none());
507 }
508
509 #[test]
514 fn test_deduplicate_request_dry_run_default_false() {
515 let req = DeduplicateRequest {
516 agent_id: "a".to_string(),
517 threshold: None,
518 memory_type: None,
519 dry_run: false,
520 };
521 let json = serde_json::to_string(&req).unwrap();
522 assert!(json.contains("\"dry_run\":false"));
523 assert!(!json.contains("threshold"));
524 assert!(!json.contains("memory_type"));
525 }
526
527 #[test]
528 fn test_deduplicate_request_with_threshold() {
529 let req = DeduplicateRequest {
530 agent_id: "a".to_string(),
531 threshold: Some(0.92),
532 memory_type: Some("episodic".to_string()),
533 dry_run: true,
534 };
535 let json = serde_json::to_string(&req).unwrap();
536 assert!(json.contains("\"threshold\":0.92"));
537 assert!(json.contains("\"memory_type\":\"episodic\""));
538 }
539
540 #[test]
545 fn test_deduplicate_response_deserializes() {
546 let json =
547 r#"{"duplicates_found": 3, "removed_count": 2, "groups": [["a","b"],["c","d","e"]]}"#;
548 let resp: DeduplicateResponse = serde_json::from_str(json).unwrap();
549 assert_eq!(resp.duplicates_found, 3);
550 assert_eq!(resp.removed_count, 2);
551 assert_eq!(resp.groups.len(), 2);
552 }
553
554 #[test]
559 fn test_cross_agent_network_request_default_values() {
560 let req = CrossAgentNetworkRequest::default();
561 assert!(req.agent_ids.is_none());
562 assert!((req.min_similarity - 0.3).abs() < 1e-6);
563 assert_eq!(req.max_nodes_per_agent, 50);
564 assert!((req.min_importance - 0.0).abs() < 1e-6);
565 assert_eq!(req.max_cross_edges, 200);
566 }
567
568 #[test]
573 fn test_cross_agent_network_response_node_count_defaults_zero() {
574 let json = r#"{
575 "agents": [],
576 "nodes": [],
577 "edges": [],
578 "stats": {
579 "total_agents": 0,
580 "total_nodes": 0,
581 "total_cross_edges": 0,
582 "density": 0.0
583 }
584 }"#;
585 let resp: CrossAgentNetworkResponse = serde_json::from_str(json).unwrap();
586 assert_eq!(resp.node_count, 0);
587 assert!(resp.agents.is_empty());
588 }
589}