1use crate::cli::build_llm_provider_for_entity_extraction;
13use crate::config::SubcogConfig;
14use crate::mcp::tool_types::{
15 EntitiesArgs, EntityMergeArgs, ExtractEntitiesArgs, GraphArgs, GraphQueryArgs,
16 GraphVisualizeArgs, RelationshipInferArgs, RelationshipsArgs, parse_entity_type,
17 parse_relationship_type,
18};
19use crate::mcp::tools::{ToolContent, ToolResult};
20use crate::models::Domain;
21use crate::models::graph::{Entity, EntityId, EntityQuery, Relationship, RelationshipQuery};
22use crate::services::ServiceContainer;
23use crate::{Error, Result};
24use serde_json::Value;
25use std::collections::HashMap;
26
27pub fn execute_entities(arguments: Value) -> Result<ToolResult> {
45 let args: EntitiesArgs = serde_json::from_value(arguments)
46 .map_err(|e| Error::InvalidInput(format!("Invalid entities arguments: {e}")))?;
47
48 match args.action.as_str() {
49 "create" => execute_entity_create(&args),
50 "get" => execute_entity_get(&args),
51 "list" => execute_entity_list(&args),
52 "delete" => execute_entity_delete(&args),
53 "extract" => execute_entity_extract(&args),
54 "merge" => execute_entity_merge_action(&args),
55 _ => Err(Error::InvalidInput(format!(
56 "Unknown entity action: {}. Valid actions: create, get, list, delete, extract, merge",
57 args.action
58 ))),
59 }
60}
61
62fn execute_entity_create(args: &EntitiesArgs) -> Result<ToolResult> {
63 let name = args.name.as_ref().ok_or_else(|| {
64 Error::InvalidInput("Entity name is required for create action".to_string())
65 })?;
66
67 let entity_type_str = args.entity_type.as_deref().unwrap_or("Concept");
68 let entity_type = parse_entity_type(entity_type_str).ok_or_else(|| {
69 Error::InvalidInput(format!(
70 "Invalid entity type: {entity_type_str}. Valid types: Person, Organization, Technology, Concept, File"
71 ))
72 })?;
73
74 let container = ServiceContainer::from_current_dir_or_user()?;
75 let graph = container.graph()?;
76
77 let mut entity = Entity::new(entity_type, name, Domain::new());
78 if let Some(aliases) = &args.aliases {
79 entity.aliases.clone_from(aliases);
80 }
81
82 graph.store_entity(&entity)?;
83
84 let text = format!(
85 "**Entity Created**\n\n\
86 - **ID**: `{}`\n\
87 - **Name**: {}\n\
88 - **Type**: {:?}\n\
89 - **Aliases**: {}\n",
90 entity.id,
91 entity.name,
92 entity.entity_type,
93 if entity.aliases.is_empty() {
94 "none".to_string()
95 } else {
96 entity.aliases.join(", ")
97 }
98 );
99
100 Ok(ToolResult {
101 content: vec![ToolContent::Text { text }],
102 is_error: false,
103 })
104}
105
106fn execute_entity_get(args: &EntitiesArgs) -> Result<ToolResult> {
107 let entity_id = args
108 .entity_id
109 .as_ref()
110 .ok_or_else(|| Error::InvalidInput("Entity ID is required for get action".to_string()))?;
111
112 let container = ServiceContainer::from_current_dir_or_user()?;
113 let graph = container.graph()?;
114
115 let id = EntityId::new(entity_id);
116 match graph.get_entity(&id)? {
117 Some(entity) => {
118 let relationships = graph.get_outgoing_relationships(&id).unwrap_or_default();
119
120 let text = format!(
121 "**Entity: {}**\n\n\
122 - **ID**: `{}`\n\
123 - **Type**: {:?}\n\
124 - **Confidence**: {:.2}\n\
125 - **Aliases**: {}\n\
126 - **Outgoing Relationships**: {}\n",
127 entity.name,
128 entity.id,
129 entity.entity_type,
130 entity.confidence,
131 if entity.aliases.is_empty() {
132 "none".to_string()
133 } else {
134 entity.aliases.join(", ")
135 },
136 relationships.len()
137 );
138
139 Ok(ToolResult {
140 content: vec![ToolContent::Text { text }],
141 is_error: false,
142 })
143 },
144 None => Ok(ToolResult {
145 content: vec![ToolContent::Text {
146 text: format!("Entity not found: {entity_id}"),
147 }],
148 is_error: true,
149 }),
150 }
151}
152
153fn execute_entity_list(args: &EntitiesArgs) -> Result<ToolResult> {
154 let container = ServiceContainer::from_current_dir_or_user()?;
155 let graph = container.graph()?;
156
157 let limit = args.limit.unwrap_or(20);
158 let mut query = EntityQuery::new().with_limit(limit);
159
160 if let Some(type_str) = &args.entity_type
161 && let Some(entity_type) = parse_entity_type(type_str)
162 {
163 query = query.with_type(entity_type);
164 }
165
166 let entities = graph.query_entities(&query)?;
167
168 if entities.is_empty() {
169 return Ok(ToolResult {
170 content: vec![ToolContent::Text {
171 text: "No entities found.".to_string(),
172 }],
173 is_error: false,
174 });
175 }
176
177 let mut text = format!("**Found {} entities**\n\n", entities.len());
178 for entity in &entities {
179 text.push_str(&format!(
180 "- **{}** ({:?}) `{}`\n",
181 entity.name, entity.entity_type, entity.id
182 ));
183 }
184
185 Ok(ToolResult {
186 content: vec![ToolContent::Text { text }],
187 is_error: false,
188 })
189}
190
191fn execute_entity_delete(args: &EntitiesArgs) -> Result<ToolResult> {
192 let entity_id = args.entity_id.as_ref().ok_or_else(|| {
193 Error::InvalidInput("Entity ID is required for delete action".to_string())
194 })?;
195
196 let container = ServiceContainer::from_current_dir_or_user()?;
197 let graph = container.graph()?;
198
199 let id = EntityId::new(entity_id);
200 let deleted = graph.delete_entity(&id)?;
201
202 let text = if deleted {
203 format!("Entity `{entity_id}` deleted successfully (including relationships and mentions).")
204 } else {
205 format!("Entity `{entity_id}` not found.")
206 };
207
208 Ok(ToolResult {
209 content: vec![ToolContent::Text { text }],
210 is_error: !deleted,
211 })
212}
213
214fn execute_entity_extract(args: &EntitiesArgs) -> Result<ToolResult> {
218 let content = args.content.as_ref().ok_or_else(|| {
219 Error::InvalidInput("'content' is required for extract action".to_string())
220 })?;
221
222 if content.trim().is_empty() {
223 return Err(Error::InvalidInput(
224 "Content is required for entity extraction".to_string(),
225 ));
226 }
227
228 let container = ServiceContainer::from_current_dir_or_user()?;
229
230 let config = SubcogConfig::load_default();
232 tracing::info!(
233 llm_features = config.features.llm_features,
234 provider = ?config.llm.provider,
235 "execute_entity_extract: loaded config"
236 );
237 let extractor = if let Some(llm) = build_llm_provider_for_entity_extraction(&config) {
238 tracing::info!("execute_entity_extract: using LLM-powered extraction");
239 container.entity_extractor_with_llm(llm)
240 } else {
241 tracing::info!("execute_entity_extract: LLM provider not available, using fallback");
242 container.entity_extractor()
243 };
244
245 let min_confidence = args.min_confidence.unwrap_or(0.5);
246 let extractor = extractor.with_min_confidence(min_confidence);
247
248 let result = extractor.extract(content)?;
249
250 let mut text = format!(
251 "**Entity Extraction Results**{}{}\n\n",
252 if result.used_fallback {
253 " (fallback mode)"
254 } else {
255 ""
256 },
257 if let Some(ref memory_id) = args.memory_id {
258 format!("\nSource memory: `{memory_id}`")
259 } else {
260 String::new()
261 }
262 );
263
264 if result.entities.is_empty() && result.relationships.is_empty() {
265 text.push_str("No entities or relationships extracted.\n");
266 } else {
267 if !result.entities.is_empty() {
268 text.push_str(&format!("**Entities ({}):**\n", result.entities.len()));
269 for entity in &result.entities {
270 text.push_str(&format!(
271 "- **{}** ({}) - confidence: {:.2}\n",
272 entity.name, entity.entity_type, entity.confidence
273 ));
274 }
275 text.push('\n');
276 }
277
278 if !result.relationships.is_empty() {
279 text.push_str(&format!(
280 "**Relationships ({}):**\n",
281 result.relationships.len()
282 ));
283 for rel in &result.relationships {
284 text.push_str(&format!(
285 "- {} --[{}]--> {} (confidence: {:.2})\n",
286 rel.from, rel.relationship_type, rel.to, rel.confidence
287 ));
288 }
289 }
290 }
291
292 if args.store && !result.entities.is_empty() {
294 let graph = container.graph()?;
295 let graph_entities = extractor.to_graph_entities(&result);
296
297 let mut entity_map: HashMap<String, Entity> = HashMap::new();
299 for entity in &graph_entities {
300 let actual_id = graph.store_entity_deduped(entity)?;
301 let mut stored_entity = entity.clone();
303 stored_entity.id = actual_id;
304 entity_map.insert(entity.name.clone(), stored_entity);
305 }
306
307 let graph_rels = extractor.to_graph_relationships(&result, &entity_map);
309
310 for rel in &graph_rels {
311 graph.store_relationship(rel)?;
312 }
313
314 text.push_str(&format!(
315 "\nā Stored {} entities and {} relationships in graph (with deduplication).\n",
316 graph_entities.len(),
317 graph_rels.len()
318 ));
319 }
320
321 if !result.warnings.is_empty() {
322 text.push_str("\n**Warnings:**\n");
323 for warning in &result.warnings {
324 text.push_str(&format!("- {warning}\n"));
325 }
326 }
327
328 Ok(ToolResult {
329 content: vec![ToolContent::Text { text }],
330 is_error: false,
331 })
332}
333
334fn execute_entity_merge_action(args: &EntitiesArgs) -> Result<ToolResult> {
338 let merge_action = args.merge_action.as_deref().unwrap_or("find_duplicates");
339
340 match merge_action {
341 "find_duplicates" => execute_entity_find_duplicates(args),
342 "merge" => execute_entity_merge_impl(args),
343 _ => Err(Error::InvalidInput(format!(
344 "Unknown merge sub-action: '{merge_action}'. Valid sub-actions: find_duplicates, merge"
345 ))),
346 }
347}
348
349fn execute_entity_find_duplicates(args: &EntitiesArgs) -> Result<ToolResult> {
351 let entity_id = args.entity_id.as_ref().ok_or_else(|| {
352 Error::InvalidInput("'entity_id' is required for find_duplicates".to_string())
353 })?;
354
355 let container = ServiceContainer::from_current_dir_or_user()?;
356 let graph = container.graph()?;
357
358 let id = EntityId::new(entity_id);
359 let entity = graph
360 .get_entity(&id)?
361 .ok_or_else(|| Error::OperationFailed {
362 operation: "find_duplicates".to_string(),
363 cause: format!("Entity not found: {entity_id}"),
364 })?;
365
366 let threshold = args.threshold.unwrap_or(0.7);
367 let duplicates = graph.find_duplicates(&entity, threshold)?;
368
369 if duplicates.is_empty() {
370 return Ok(ToolResult {
371 content: vec![ToolContent::Text {
372 text: format!(
373 "No potential duplicates found for '{}' (threshold: {:.0}%)",
374 entity.name,
375 threshold * 100.0
376 ),
377 }],
378 is_error: false,
379 });
380 }
381
382 let mut text = format!(
383 "**Potential duplicates for '{}'** (threshold: {:.0}%)\n\n",
384 entity.name,
385 threshold * 100.0
386 );
387 for dup in &duplicates {
388 text.push_str(&format!(
389 "- **{}** ({:?}) `{}`\n",
390 dup.name, dup.entity_type, dup.id
391 ));
392 }
393 text.push_str(&format!(
394 "\nTo merge, use: action=merge, merge_action=merge, entity_ids=[\"{}\", {}], canonical_name=\"...\"",
395 entity_id,
396 duplicates
397 .iter()
398 .map(|e| format!("\"{}\"", e.id))
399 .collect::<Vec<_>>()
400 .join(", ")
401 ));
402
403 Ok(ToolResult {
404 content: vec![ToolContent::Text { text }],
405 is_error: false,
406 })
407}
408
409fn execute_entity_merge_impl(args: &EntitiesArgs) -> Result<ToolResult> {
411 let entity_ids = args.entity_ids.as_ref().ok_or_else(|| {
412 Error::InvalidInput("'entity_ids' is required for merge (minimum 2)".to_string())
413 })?;
414
415 if entity_ids.len() < 2 {
416 return Err(Error::InvalidInput(
417 "At least 2 entity IDs are required for merge".to_string(),
418 ));
419 }
420
421 let canonical_name = args
422 .canonical_name
423 .as_ref()
424 .ok_or_else(|| Error::InvalidInput("'canonical_name' is required for merge".to_string()))?;
425
426 let container = ServiceContainer::from_current_dir_or_user()?;
427 let graph = container.graph()?;
428
429 let ids: Vec<EntityId> = entity_ids.iter().map(EntityId::new).collect();
430 let merged = graph.merge_entities(&ids, canonical_name)?;
431
432 let text = format!(
433 "**Entities Merged Successfully**\n\n\
434 - **Canonical Entity**: {} `{}`\n\
435 - **Merged IDs**: {}\n\n\
436 All relationships and mentions have been transferred to the canonical entity.",
437 merged.name,
438 merged.id,
439 entity_ids.join(", ")
440 );
441
442 Ok(ToolResult {
443 content: vec![ToolContent::Text { text }],
444 is_error: false,
445 })
446}
447
448pub fn execute_relationships(arguments: Value) -> Result<ToolResult> {
458 let args: RelationshipsArgs = serde_json::from_value(arguments)
459 .map_err(|e| Error::InvalidInput(format!("Invalid relationships arguments: {e}")))?;
460
461 match args.action.as_str() {
462 "create" => execute_relationship_create(&args),
463 "get" | "list" => execute_relationship_list(&args),
464 "delete" => execute_relationship_delete(&args),
465 "infer" => execute_relationship_infer_action(&args),
466 _ => Err(Error::InvalidInput(format!(
467 "Unknown relationship action: {}. Valid actions: create, get, list, delete, infer",
468 args.action
469 ))),
470 }
471}
472
473fn execute_relationship_create(args: &RelationshipsArgs) -> Result<ToolResult> {
474 let from_id = args.from_entity.as_ref().ok_or_else(|| {
475 Error::InvalidInput("from_entity is required for create action".to_string())
476 })?;
477 let to_id = args.to_entity.as_ref().ok_or_else(|| {
478 Error::InvalidInput("to_entity is required for create action".to_string())
479 })?;
480 let rel_type_str = args.relationship_type.as_deref().ok_or_else(|| {
481 Error::InvalidInput("relationship_type is required for create action".to_string())
482 })?;
483
484 let rel_type = parse_relationship_type(rel_type_str).ok_or_else(|| {
485 Error::InvalidInput(format!(
486 "Invalid relationship type: {rel_type_str}. Valid types: WorksAt, Created, Uses, Implements, PartOf, RelatesTo, MentionedIn, Supersedes, ConflictsWith"
487 ))
488 })?;
489
490 let container = ServiceContainer::from_current_dir_or_user()?;
491 let graph = container.graph()?;
492
493 let from = EntityId::new(from_id);
494 let to = EntityId::new(to_id);
495
496 let relationship = graph.relate(&from, &to, rel_type)?;
497
498 let text = format!(
499 "**Relationship Created**\n\n\
500 - **From**: `{}`\n\
501 - **To**: `{}`\n\
502 - **Type**: {:?}\n",
503 relationship.from_entity, relationship.to_entity, relationship.relationship_type
504 );
505
506 Ok(ToolResult {
507 content: vec![ToolContent::Text { text }],
508 is_error: false,
509 })
510}
511
512fn execute_relationship_list(args: &RelationshipsArgs) -> Result<ToolResult> {
513 let container = ServiceContainer::from_current_dir_or_user()?;
514 let graph = container.graph()?;
515
516 let limit = args.limit.unwrap_or(20);
517 let direction = args.direction.as_deref().unwrap_or("both");
518
519 let relationships = if let Some(entity_id) = &args.entity_id {
520 let id = EntityId::new(entity_id);
521 match direction {
522 "outgoing" => graph.get_outgoing_relationships(&id)?,
523 "incoming" => graph.get_incoming_relationships(&id)?,
524 _ => {
525 let mut rels = graph.get_outgoing_relationships(&id)?;
526 rels.extend(graph.get_incoming_relationships(&id)?);
527 rels
528 },
529 }
530 } else {
531 let query = RelationshipQuery::new().with_limit(limit);
533 graph.query_relationships(&query)?
534 };
535
536 if relationships.is_empty() {
537 return Ok(ToolResult {
538 content: vec![ToolContent::Text {
539 text: "No relationships found.".to_string(),
540 }],
541 is_error: false,
542 });
543 }
544
545 let display_count = relationships.len().min(limit);
546 let mut text = format!("**Found {} relationships**\n\n", relationships.len());
547 for rel in relationships.iter().take(display_count) {
548 text.push_str(&format!(
549 "- `{}` --[{:?}]--> `{}`\n",
550 rel.from_entity, rel.relationship_type, rel.to_entity
551 ));
552 }
553
554 Ok(ToolResult {
555 content: vec![ToolContent::Text { text }],
556 is_error: false,
557 })
558}
559
560fn execute_relationship_delete(args: &RelationshipsArgs) -> Result<ToolResult> {
561 let from_id = args.from_entity.as_ref().ok_or_else(|| {
562 Error::InvalidInput("from_entity is required for delete action".to_string())
563 })?;
564 let to_id = args.to_entity.as_ref().ok_or_else(|| {
565 Error::InvalidInput("to_entity is required for delete action".to_string())
566 })?;
567
568 let container = ServiceContainer::from_current_dir_or_user()?;
569 let graph = container.graph()?;
570
571 let from = EntityId::new(from_id);
572 let to = EntityId::new(to_id);
573
574 let mut query = RelationshipQuery::new().from(from).to(to);
575 if let Some(rel_type_str) = &args.relationship_type
576 && let Some(rel_type) = parse_relationship_type(rel_type_str)
577 {
578 query = query.with_type(rel_type);
579 }
580
581 let deleted = graph.delete_relationships(&query)?;
582
583 let text = format!("Deleted {deleted} relationship(s).");
584
585 Ok(ToolResult {
586 content: vec![ToolContent::Text { text }],
587 is_error: false,
588 })
589}
590
591fn execute_relationship_infer_action(args: &RelationshipsArgs) -> Result<ToolResult> {
595 let container = ServiceContainer::from_current_dir_or_user()?;
596 let graph = container.graph()?;
597
598 let entities = if let Some(entity_ids) = &args.entity_ids {
600 let mut entities = Vec::new();
601 for id_str in entity_ids {
602 let id = EntityId::new(id_str);
603 if let Some(entity) = graph.get_entity(&id)? {
604 entities.push(entity);
605 }
606 }
607 entities
608 } else {
609 let limit = args.limit.unwrap_or(50);
611 let query = EntityQuery::new().with_limit(limit);
612 graph.query_entities(&query)?
613 };
614
615 if entities.is_empty() {
616 return Ok(ToolResult {
617 content: vec![ToolContent::Text {
618 text: "No entities found to analyze.".to_string(),
619 }],
620 is_error: false,
621 });
622 }
623
624 let config = SubcogConfig::load_default();
626 let extractor = if let Some(llm) = build_llm_provider_for_entity_extraction(&config) {
627 container.entity_extractor_with_llm(llm)
628 } else {
629 container.entity_extractor()
630 };
631
632 let min_confidence = args.min_confidence.unwrap_or(0.6);
633 let extractor = extractor.with_min_confidence(min_confidence);
634
635 let result = extractor.infer_relationships(&entities)?;
636
637 let mut text = format!(
638 "**Relationship Inference Results**{}\n\n",
639 if result.used_fallback {
640 " (fallback mode)"
641 } else {
642 ""
643 }
644 );
645
646 if result.relationships.is_empty() {
647 text.push_str("No relationships inferred.\n");
648 } else {
649 text.push_str(&format!(
650 "**Inferred Relationships ({}):**\n",
651 result.relationships.len()
652 ));
653 for rel in &result.relationships {
654 text.push_str(&format!(
655 "- {} --[{}]--> {} (confidence: {:.2})",
656 rel.from, rel.relationship_type, rel.to, rel.confidence
657 ));
658 if let Some(reasoning) = &rel.reasoning {
659 text.push_str(&format!("\n Reasoning: {reasoning}"));
660 }
661 text.push('\n');
662 }
663 }
664
665 if args.store && !result.relationships.is_empty() {
667 let entity_map: HashMap<String, &Entity> =
669 entities.iter().map(|e| (e.name.clone(), e)).collect();
670
671 let mut stored = 0;
672 for inferred in &result.relationships {
673 if let (Some(from), Some(to)) =
674 (entity_map.get(&inferred.from), entity_map.get(&inferred.to))
675 && let Some(rel_type) = parse_relationship_type(&inferred.relationship_type)
676 {
677 let mut rel = Relationship::new(from.id.clone(), to.id.clone(), rel_type);
678 rel.confidence = inferred.confidence;
679 graph.store_relationship(&rel)?;
680 stored += 1;
681 }
682 }
683
684 text.push_str(&format!("\nā Stored {stored} relationships in graph.\n"));
685 }
686
687 if !result.warnings.is_empty() {
688 text.push_str("\n**Warnings:**\n");
689 for warning in &result.warnings {
690 text.push_str(&format!("- {warning}\n"));
691 }
692 }
693
694 Ok(ToolResult {
695 content: vec![ToolContent::Text { text }],
696 is_error: false,
697 })
698}
699
700pub fn execute_graph(arguments: Value) -> Result<ToolResult> {
712 let args: GraphArgs = serde_json::from_value(arguments)
713 .map_err(|e| Error::InvalidInput(format!("Invalid graph arguments: {e}")))?;
714
715 match args.operation.as_str() {
716 "neighbors" => execute_graph_neighbors(&args),
717 "path" => execute_graph_path(&args),
718 "stats" => execute_graph_stats(),
719 "visualize" => execute_graph_visualize_action(&args),
720 _ => Err(Error::InvalidInput(format!(
721 "Unknown graph operation: {}. Valid operations: neighbors, path, stats, visualize",
722 args.operation
723 ))),
724 }
725}
726
727fn execute_graph_neighbors(args: &GraphArgs) -> Result<ToolResult> {
729 let entity_id = args.entity_id.as_ref().ok_or_else(|| {
730 Error::InvalidInput("'entity_id' is required for neighbors operation".to_string())
731 })?;
732
733 let container = ServiceContainer::from_current_dir_or_user()?;
734 let graph = container.graph()?;
735
736 let id = EntityId::new(entity_id);
737 let depth = u32::try_from(args.depth.unwrap_or(2).min(5)).unwrap_or(2);
738
739 let neighbors = graph.get_neighbors(&id, depth)?;
740
741 if neighbors.is_empty() {
742 return Ok(ToolResult {
743 content: vec![ToolContent::Text {
744 text: format!("No neighbors found for entity `{entity_id}` within depth {depth}."),
745 }],
746 is_error: false,
747 });
748 }
749
750 let mut text = format!(
751 "**Neighbors of `{entity_id}` (depth {depth})**\n\nFound {} entities:\n\n",
752 neighbors.len()
753 );
754 for entity in &neighbors {
755 text.push_str(&format!(
756 "- **{}** ({:?}) `{}`\n",
757 entity.name, entity.entity_type, entity.id
758 ));
759 }
760
761 Ok(ToolResult {
762 content: vec![ToolContent::Text { text }],
763 is_error: false,
764 })
765}
766
767fn execute_graph_path(args: &GraphArgs) -> Result<ToolResult> {
769 let from_id = args.from_entity.as_ref().ok_or_else(|| {
770 Error::InvalidInput("'from_entity' is required for path operation".to_string())
771 })?;
772 let to_id = args.to_entity.as_ref().ok_or_else(|| {
773 Error::InvalidInput("'to_entity' is required for path operation".to_string())
774 })?;
775
776 let container = ServiceContainer::from_current_dir_or_user()?;
777 let graph = container.graph()?;
778
779 let from = EntityId::new(from_id);
780 let to = EntityId::new(to_id);
781 let max_depth = u32::try_from(args.depth.unwrap_or(5).min(5)).unwrap_or(5);
782
783 match graph.find_path(&from, &to, max_depth)? {
784 Some(result) => {
785 let mut text = format!(
786 "**Path from `{from_id}` to `{to_id}`**\n\n\
787 Path length: {} entities, {} relationships\n\n",
788 result.entities.len(),
789 result.relationships.len()
790 );
791
792 text.push_str("**Entities in path:**\n");
793 for entity in &result.entities {
794 text.push_str(&format!("- {} ({:?})\n", entity.name, entity.entity_type));
795 }
796
797 if !result.relationships.is_empty() {
798 text.push_str("\n**Relationships:**\n");
799 for rel in &result.relationships {
800 text.push_str(&format!(
801 "- `{}` --[{:?}]--> `{}`\n",
802 rel.from_entity, rel.relationship_type, rel.to_entity
803 ));
804 }
805 }
806
807 Ok(ToolResult {
808 content: vec![ToolContent::Text { text }],
809 is_error: false,
810 })
811 },
812 None => Ok(ToolResult {
813 content: vec![ToolContent::Text {
814 text: format!(
815 "No path found from `{from_id}` to `{to_id}` within depth {max_depth}."
816 ),
817 }],
818 is_error: false,
819 }),
820 }
821}
822
823fn execute_graph_stats() -> Result<ToolResult> {
825 let container = ServiceContainer::from_current_dir_or_user()?;
826 let graph = container.graph()?;
827
828 let stats = graph.get_stats()?;
829
830 let text = format!(
831 "**Knowledge Graph Statistics**\n\n\
832 - **Entities**: {}\n\
833 - **Relationships**: {}\n\
834 - **Mentions**: {}\n\n\
835 **Entity Types:**\n{}\n\
836 **Relationship Types:**\n{}",
837 stats.entity_count,
838 stats.relationship_count,
839 stats.mention_count,
840 format_type_counts(&stats.entities_by_type),
841 format_type_counts(&stats.relationships_by_type),
842 );
843
844 Ok(ToolResult {
845 content: vec![ToolContent::Text { text }],
846 is_error: false,
847 })
848}
849
850fn execute_graph_visualize_action(args: &GraphArgs) -> Result<ToolResult> {
852 let container = ServiceContainer::from_current_dir_or_user()?;
853 let graph = container.graph()?;
854
855 let format = args.format.as_deref().unwrap_or("mermaid");
856 let limit = args.limit.unwrap_or(50);
857 let depth = u32::try_from(args.depth.unwrap_or(2).min(4)).unwrap_or(2);
858
859 let (entities, relationships) = if let Some(entity_id) = &args.entity_id {
861 let id = EntityId::new(entity_id);
863 let result = graph.traverse(&id, depth, None, None)?;
864 (result.entities, result.relationships)
865 } else {
866 let query = EntityQuery::new().with_limit(limit);
868 let entities = graph.query_entities(&query)?;
869
870 let entity_ids: std::collections::HashSet<_> = entities.iter().map(|e| &e.id).collect();
872 let all_rels =
873 graph.query_relationships(&RelationshipQuery::new().with_limit(limit * 2))?;
874 let relationships: Vec<_> = all_rels
875 .into_iter()
876 .filter(|r| entity_ids.contains(&r.from_entity) && entity_ids.contains(&r.to_entity))
877 .collect();
878
879 (entities, relationships)
880 };
881
882 let entities: Vec<_> = if let Some(type_filter) = &args.entity_types {
884 let allowed_types: std::collections::HashSet<_> = type_filter
885 .iter()
886 .filter_map(|s| parse_entity_type(s))
887 .collect();
888 entities
889 .into_iter()
890 .filter(|e| allowed_types.is_empty() || allowed_types.contains(&e.entity_type))
891 .collect()
892 } else {
893 entities
894 };
895
896 let relationships: Vec<_> = if let Some(type_filter) = &args.relationship_types {
897 let allowed_types: std::collections::HashSet<_> = type_filter
898 .iter()
899 .filter_map(|s| parse_relationship_type(s))
900 .collect();
901 relationships
902 .into_iter()
903 .filter(|r| allowed_types.is_empty() || allowed_types.contains(&r.relationship_type))
904 .collect()
905 } else {
906 relationships
907 };
908
909 if entities.is_empty() {
910 return Ok(ToolResult {
911 content: vec![ToolContent::Text {
912 text: "No entities to visualize.".to_string(),
913 }],
914 is_error: false,
915 });
916 }
917
918 let visualization = match format {
919 "mermaid" => generate_mermaid(&entities, &relationships),
920 "dot" => generate_dot(&entities, &relationships),
921 "ascii" => generate_ascii(&entities, &relationships),
922 _ => {
923 return Err(Error::InvalidInput(format!(
924 "Unknown visualization format: {format}. Valid formats: mermaid, dot, ascii"
925 )));
926 },
927 };
928
929 let text = format!(
930 "**Graph Visualization ({} entities, {} relationships)**\n\n```{}\n{}\n```",
931 entities.len(),
932 relationships.len(),
933 if format == "dot" { "dot" } else { format },
934 visualization
935 );
936
937 Ok(ToolResult {
938 content: vec![ToolContent::Text { text }],
939 is_error: false,
940 })
941}
942
943pub fn execute_graph_query(arguments: Value) -> Result<ToolResult> {
953 let args: GraphQueryArgs = serde_json::from_value(arguments)
954 .map_err(|e| Error::InvalidInput(format!("Invalid graph query arguments: {e}")))?;
955
956 match args.operation.as_str() {
957 "neighbors" => execute_query_neighbors(&args),
958 "path" => execute_query_path(&args),
959 "stats" => execute_query_stats(),
960 _ => Err(Error::InvalidInput(format!(
961 "Unknown graph query operation: {}. Valid operations: neighbors, path, stats",
962 args.operation
963 ))),
964 }
965}
966
967fn execute_query_neighbors(args: &GraphQueryArgs) -> Result<ToolResult> {
968 let entity_id = args.entity_id.as_ref().ok_or_else(|| {
969 Error::InvalidInput("entity_id is required for neighbors operation".to_string())
970 })?;
971
972 let container = ServiceContainer::from_current_dir_or_user()?;
973 let graph = container.graph()?;
974
975 let id = EntityId::new(entity_id);
976 let depth = u32::try_from(args.depth.unwrap_or(2).min(5)).unwrap_or(2);
977
978 let neighbors = graph.get_neighbors(&id, depth)?;
979
980 if neighbors.is_empty() {
981 return Ok(ToolResult {
982 content: vec![ToolContent::Text {
983 text: format!("No neighbors found for entity `{entity_id}` within depth {depth}."),
984 }],
985 is_error: false,
986 });
987 }
988
989 let mut text = format!(
990 "**Neighbors of `{entity_id}` (depth {depth})**\n\nFound {} entities:\n\n",
991 neighbors.len()
992 );
993 for entity in &neighbors {
994 text.push_str(&format!(
995 "- **{}** ({:?}) `{}`\n",
996 entity.name, entity.entity_type, entity.id
997 ));
998 }
999
1000 Ok(ToolResult {
1001 content: vec![ToolContent::Text { text }],
1002 is_error: false,
1003 })
1004}
1005
1006fn execute_query_path(args: &GraphQueryArgs) -> Result<ToolResult> {
1007 let from_id = args.from_entity.as_ref().ok_or_else(|| {
1008 Error::InvalidInput("from_entity is required for path operation".to_string())
1009 })?;
1010 let to_id = args.to_entity.as_ref().ok_or_else(|| {
1011 Error::InvalidInput("to_entity is required for path operation".to_string())
1012 })?;
1013
1014 let container = ServiceContainer::from_current_dir_or_user()?;
1015 let graph = container.graph()?;
1016
1017 let from = EntityId::new(from_id);
1018 let to = EntityId::new(to_id);
1019 let max_depth = u32::try_from(args.depth.unwrap_or(5).min(5)).unwrap_or(5);
1020
1021 match graph.find_path(&from, &to, max_depth)? {
1022 Some(result) => {
1023 let mut text = format!(
1024 "**Path from `{from_id}` to `{to_id}`**\n\n\
1025 Path length: {} entities, {} relationships\n\n",
1026 result.entities.len(),
1027 result.relationships.len()
1028 );
1029
1030 text.push_str("**Entities in path:**\n");
1031 for entity in &result.entities {
1032 text.push_str(&format!("- {} ({:?})\n", entity.name, entity.entity_type));
1033 }
1034
1035 if !result.relationships.is_empty() {
1036 text.push_str("\n**Relationships:**\n");
1037 for rel in &result.relationships {
1038 text.push_str(&format!(
1039 "- `{}` --[{:?}]--> `{}`\n",
1040 rel.from_entity, rel.relationship_type, rel.to_entity
1041 ));
1042 }
1043 }
1044
1045 Ok(ToolResult {
1046 content: vec![ToolContent::Text { text }],
1047 is_error: false,
1048 })
1049 },
1050 None => Ok(ToolResult {
1051 content: vec![ToolContent::Text {
1052 text: format!(
1053 "No path found from `{from_id}` to `{to_id}` within depth {max_depth}."
1054 ),
1055 }],
1056 is_error: false,
1057 }),
1058 }
1059}
1060
1061fn execute_query_stats() -> Result<ToolResult> {
1062 let container = ServiceContainer::from_current_dir_or_user()?;
1063 let graph = container.graph()?;
1064
1065 let stats = graph.get_stats()?;
1066
1067 let text = format!(
1068 "**Knowledge Graph Statistics**\n\n\
1069 - **Entities**: {}\n\
1070 - **Relationships**: {}\n\
1071 - **Mentions**: {}\n\n\
1072 **Entity Types:**\n{}\n\
1073 **Relationship Types:**\n{}",
1074 stats.entity_count,
1075 stats.relationship_count,
1076 stats.mention_count,
1077 format_type_counts(&stats.entities_by_type),
1078 format_type_counts(&stats.relationships_by_type),
1079 );
1080
1081 Ok(ToolResult {
1082 content: vec![ToolContent::Text { text }],
1083 is_error: false,
1084 })
1085}
1086
1087fn format_type_counts<K: std::fmt::Debug>(counts: &HashMap<K, usize>) -> String {
1088 if counts.is_empty() {
1089 return " (none)\n".to_string();
1090 }
1091 let mut result = String::new();
1092 for (type_name, count) in counts {
1093 result.push_str(&format!(" - {type_name:?}: {count}\n"));
1094 }
1095 result
1096}
1097
1098pub fn execute_extract_entities(arguments: Value) -> Result<ToolResult> {
1108 let args: ExtractEntitiesArgs = serde_json::from_value(arguments)
1109 .map_err(|e| Error::InvalidInput(format!("Invalid extract entities arguments: {e}")))?;
1110
1111 if args.content.trim().is_empty() {
1112 return Err(Error::InvalidInput(
1113 "Content is required for entity extraction".to_string(),
1114 ));
1115 }
1116
1117 let container = ServiceContainer::from_current_dir_or_user()?;
1118
1119 let config = SubcogConfig::load_default();
1121 let extractor = if let Some(llm) = build_llm_provider_for_entity_extraction(&config) {
1122 container.entity_extractor_with_llm(llm)
1123 } else {
1124 container.entity_extractor()
1125 };
1126
1127 let min_confidence = args.min_confidence.unwrap_or(0.5);
1128 let extractor = extractor.with_min_confidence(min_confidence);
1129
1130 let result = extractor.extract(&args.content)?;
1131
1132 let mut text = format!(
1133 "**Entity Extraction Results**{}{}\n\n",
1134 if result.used_fallback {
1135 " (fallback mode)"
1136 } else {
1137 ""
1138 },
1139 if let Some(ref memory_id) = args.memory_id {
1140 format!("\nSource memory: `{memory_id}`")
1141 } else {
1142 String::new()
1143 }
1144 );
1145
1146 if result.entities.is_empty() && result.relationships.is_empty() {
1147 text.push_str("No entities or relationships extracted.\n");
1148 } else {
1149 if !result.entities.is_empty() {
1150 text.push_str(&format!("**Entities ({}):**\n", result.entities.len()));
1151 for entity in &result.entities {
1152 text.push_str(&format!(
1153 "- **{}** ({}) - confidence: {:.2}\n",
1154 entity.name, entity.entity_type, entity.confidence
1155 ));
1156 }
1157 text.push('\n');
1158 }
1159
1160 if !result.relationships.is_empty() {
1161 text.push_str(&format!(
1162 "**Relationships ({}):**\n",
1163 result.relationships.len()
1164 ));
1165 for rel in &result.relationships {
1166 text.push_str(&format!(
1167 "- {} --[{}]--> {} (confidence: {:.2})\n",
1168 rel.from, rel.relationship_type, rel.to, rel.confidence
1169 ));
1170 }
1171 }
1172 }
1173
1174 if args.store && !result.entities.is_empty() {
1176 let graph = container.graph()?;
1177 let graph_entities = extractor.to_graph_entities(&result);
1178
1179 let mut entity_map: HashMap<String, Entity> = HashMap::new();
1181 for entity in &graph_entities {
1182 let actual_id = graph.store_entity_deduped(entity)?;
1183 let mut stored_entity = entity.clone();
1185 stored_entity.id = actual_id;
1186 entity_map.insert(entity.name.clone(), stored_entity);
1187 }
1188
1189 let graph_rels = extractor.to_graph_relationships(&result, &entity_map);
1191
1192 for rel in &graph_rels {
1193 graph.store_relationship(rel)?;
1194 }
1195
1196 text.push_str(&format!(
1197 "\nā Stored {} entities and {} relationships in graph (with deduplication).\n",
1198 graph_entities.len(),
1199 graph_rels.len()
1200 ));
1201 }
1202
1203 if !result.warnings.is_empty() {
1204 text.push_str("\n**Warnings:**\n");
1205 for warning in &result.warnings {
1206 text.push_str(&format!("- {warning}\n"));
1207 }
1208 }
1209
1210 Ok(ToolResult {
1211 content: vec![ToolContent::Text { text }],
1212 is_error: false,
1213 })
1214}
1215
1216pub fn execute_entity_merge(arguments: Value) -> Result<ToolResult> {
1226 let args: EntityMergeArgs = serde_json::from_value(arguments)
1227 .map_err(|e| Error::InvalidInput(format!("Invalid entity merge arguments: {e}")))?;
1228
1229 match args.action.as_str() {
1230 "find_duplicates" => execute_find_duplicates(&args),
1231 "merge" => execute_merge(&args),
1232 _ => Err(Error::InvalidInput(format!(
1233 "Unknown merge action: {}. Valid actions: find_duplicates, merge",
1234 args.action
1235 ))),
1236 }
1237}
1238
1239fn execute_find_duplicates(args: &EntityMergeArgs) -> Result<ToolResult> {
1240 let entity_id = args.entity_id.as_ref().ok_or_else(|| {
1241 Error::InvalidInput("entity_id is required for find_duplicates action".to_string())
1242 })?;
1243
1244 let container = ServiceContainer::from_current_dir_or_user()?;
1245 let graph = container.graph()?;
1246
1247 let id = EntityId::new(entity_id);
1248 let entity = graph
1249 .get_entity(&id)?
1250 .ok_or_else(|| Error::OperationFailed {
1251 operation: "find_duplicates".to_string(),
1252 cause: format!("Entity not found: {entity_id}"),
1253 })?;
1254
1255 let threshold = args.threshold.unwrap_or(0.7);
1256 let duplicates = graph.find_duplicates(&entity, threshold)?;
1257
1258 if duplicates.is_empty() {
1259 return Ok(ToolResult {
1260 content: vec![ToolContent::Text {
1261 text: format!(
1262 "No potential duplicates found for '{}' (threshold: {:.0}%)",
1263 entity.name,
1264 threshold * 100.0
1265 ),
1266 }],
1267 is_error: false,
1268 });
1269 }
1270
1271 let mut text = format!(
1272 "**Potential duplicates for '{}'** (threshold: {:.0}%)\n\n",
1273 entity.name,
1274 threshold * 100.0
1275 );
1276 for dup in &duplicates {
1277 text.push_str(&format!(
1278 "- **{}** ({:?}) `{}`\n",
1279 dup.name, dup.entity_type, dup.id
1280 ));
1281 }
1282 text.push_str(&format!(
1283 "\nTo merge, use: `merge` action with entity_ids: [\"{}\", {}]",
1284 entity_id,
1285 duplicates
1286 .iter()
1287 .map(|e| format!("\"{}\"", e.id))
1288 .collect::<Vec<_>>()
1289 .join(", ")
1290 ));
1291
1292 Ok(ToolResult {
1293 content: vec![ToolContent::Text { text }],
1294 is_error: false,
1295 })
1296}
1297
1298fn execute_merge(args: &EntityMergeArgs) -> Result<ToolResult> {
1299 let entity_ids = args.entity_ids.as_ref().ok_or_else(|| {
1300 Error::InvalidInput("entity_ids is required for merge action (minimum 2)".to_string())
1301 })?;
1302
1303 if entity_ids.len() < 2 {
1304 return Err(Error::InvalidInput(
1305 "At least 2 entity IDs are required for merge".to_string(),
1306 ));
1307 }
1308
1309 let canonical_name = args.canonical_name.as_ref().ok_or_else(|| {
1310 Error::InvalidInput("canonical_name is required for merge action".to_string())
1311 })?;
1312
1313 let container = ServiceContainer::from_current_dir_or_user()?;
1314 let graph = container.graph()?;
1315
1316 let ids: Vec<EntityId> = entity_ids.iter().map(EntityId::new).collect();
1317 let merged = graph.merge_entities(&ids, canonical_name)?;
1318
1319 let text = format!(
1320 "**Entities Merged Successfully**\n\n\
1321 - **Canonical Entity**: {} `{}`\n\
1322 - **Merged IDs**: {}\n\n\
1323 All relationships and mentions have been transferred to the canonical entity.",
1324 merged.name,
1325 merged.id,
1326 entity_ids.join(", ")
1327 );
1328
1329 Ok(ToolResult {
1330 content: vec![ToolContent::Text { text }],
1331 is_error: false,
1332 })
1333}
1334
1335pub fn execute_relationship_infer(arguments: Value) -> Result<ToolResult> {
1345 let args: RelationshipInferArgs = serde_json::from_value(arguments)
1346 .map_err(|e| Error::InvalidInput(format!("Invalid relationship infer arguments: {e}")))?;
1347
1348 let container = ServiceContainer::from_current_dir_or_user()?;
1349 let graph = container.graph()?;
1350
1351 let entities = if let Some(entity_ids) = &args.entity_ids {
1353 let mut entities = Vec::new();
1354 for id_str in entity_ids {
1355 let id = EntityId::new(id_str);
1356 if let Some(entity) = graph.get_entity(&id)? {
1357 entities.push(entity);
1358 }
1359 }
1360 entities
1361 } else {
1362 let limit = args.limit.unwrap_or(50);
1364 let query = EntityQuery::new().with_limit(limit);
1365 graph.query_entities(&query)?
1366 };
1367
1368 if entities.is_empty() {
1369 return Ok(ToolResult {
1370 content: vec![ToolContent::Text {
1371 text: "No entities found to analyze.".to_string(),
1372 }],
1373 is_error: false,
1374 });
1375 }
1376
1377 let config = SubcogConfig::load_default();
1379 let extractor = if let Some(llm) = build_llm_provider_for_entity_extraction(&config) {
1380 container.entity_extractor_with_llm(llm)
1381 } else {
1382 container.entity_extractor()
1383 };
1384
1385 let min_confidence = args.min_confidence.unwrap_or(0.6);
1386 let extractor = extractor.with_min_confidence(min_confidence);
1387
1388 let result = extractor.infer_relationships(&entities)?;
1389
1390 let mut text = format!(
1391 "**Relationship Inference Results**{}\n\n",
1392 if result.used_fallback {
1393 " (fallback mode)"
1394 } else {
1395 ""
1396 }
1397 );
1398
1399 if result.relationships.is_empty() {
1400 text.push_str("No relationships inferred.\n");
1401 } else {
1402 text.push_str(&format!(
1403 "**Inferred Relationships ({}):**\n",
1404 result.relationships.len()
1405 ));
1406 for rel in &result.relationships {
1407 text.push_str(&format!(
1408 "- {} --[{}]--> {} (confidence: {:.2})",
1409 rel.from, rel.relationship_type, rel.to, rel.confidence
1410 ));
1411 if let Some(reasoning) = &rel.reasoning {
1412 text.push_str(&format!("\n Reasoning: {reasoning}"));
1413 }
1414 text.push('\n');
1415 }
1416 }
1417
1418 if args.store && !result.relationships.is_empty() {
1420 let entity_map: HashMap<String, &Entity> =
1422 entities.iter().map(|e| (e.name.clone(), e)).collect();
1423
1424 let mut stored = 0;
1425 for inferred in &result.relationships {
1426 if let (Some(from), Some(to)) =
1427 (entity_map.get(&inferred.from), entity_map.get(&inferred.to))
1428 && let Some(rel_type) = parse_relationship_type(&inferred.relationship_type)
1429 {
1430 let mut rel = Relationship::new(from.id.clone(), to.id.clone(), rel_type);
1431 rel.confidence = inferred.confidence;
1432 graph.store_relationship(&rel)?;
1433 stored += 1;
1434 }
1435 }
1436
1437 text.push_str(&format!("\nā Stored {stored} relationships in graph.\n"));
1438 }
1439
1440 if !result.warnings.is_empty() {
1441 text.push_str("\n**Warnings:**\n");
1442 for warning in &result.warnings {
1443 text.push_str(&format!("- {warning}\n"));
1444 }
1445 }
1446
1447 Ok(ToolResult {
1448 content: vec![ToolContent::Text { text }],
1449 is_error: false,
1450 })
1451}
1452
1453pub fn execute_graph_visualize(arguments: Value) -> Result<ToolResult> {
1463 let args: GraphVisualizeArgs = serde_json::from_value(arguments)
1464 .map_err(|e| Error::InvalidInput(format!("Invalid graph visualize arguments: {e}")))?;
1465
1466 let container = ServiceContainer::from_current_dir_or_user()?;
1467 let graph = container.graph()?;
1468
1469 let format = args.format.as_deref().unwrap_or("mermaid");
1470 let limit = args.limit.unwrap_or(50);
1471 let depth = u32::try_from(args.depth.unwrap_or(2).min(4)).unwrap_or(2);
1472
1473 let (entities, relationships) = if let Some(entity_id) = &args.entity_id {
1475 let id = EntityId::new(entity_id);
1477 let result = graph.traverse(&id, depth, None, None)?;
1478 (result.entities, result.relationships)
1479 } else {
1480 let query = EntityQuery::new().with_limit(limit);
1482 let entities = graph.query_entities(&query)?;
1483
1484 let entity_ids: std::collections::HashSet<_> = entities.iter().map(|e| &e.id).collect();
1486 let all_rels =
1487 graph.query_relationships(&RelationshipQuery::new().with_limit(limit * 2))?;
1488 let relationships: Vec<_> = all_rels
1489 .into_iter()
1490 .filter(|r| entity_ids.contains(&r.from_entity) && entity_ids.contains(&r.to_entity))
1491 .collect();
1492
1493 (entities, relationships)
1494 };
1495
1496 let entities: Vec<_> = if let Some(type_filter) = &args.entity_types {
1498 let allowed_types: std::collections::HashSet<_> = type_filter
1499 .iter()
1500 .filter_map(|s| parse_entity_type(s))
1501 .collect();
1502 entities
1503 .into_iter()
1504 .filter(|e| allowed_types.is_empty() || allowed_types.contains(&e.entity_type))
1505 .collect()
1506 } else {
1507 entities
1508 };
1509
1510 let relationships: Vec<_> = if let Some(type_filter) = &args.relationship_types {
1511 let allowed_types: std::collections::HashSet<_> = type_filter
1512 .iter()
1513 .filter_map(|s| parse_relationship_type(s))
1514 .collect();
1515 relationships
1516 .into_iter()
1517 .filter(|r| allowed_types.is_empty() || allowed_types.contains(&r.relationship_type))
1518 .collect()
1519 } else {
1520 relationships
1521 };
1522
1523 if entities.is_empty() {
1524 return Ok(ToolResult {
1525 content: vec![ToolContent::Text {
1526 text: "No entities to visualize.".to_string(),
1527 }],
1528 is_error: false,
1529 });
1530 }
1531
1532 let visualization = match format {
1533 "mermaid" => generate_mermaid(&entities, &relationships),
1534 "dot" => generate_dot(&entities, &relationships),
1535 "ascii" => generate_ascii(&entities, &relationships),
1536 _ => {
1537 return Err(Error::InvalidInput(format!(
1538 "Unknown visualization format: {format}. Valid formats: mermaid, dot, ascii"
1539 )));
1540 },
1541 };
1542
1543 let text = format!(
1544 "**Graph Visualization ({} entities, {} relationships)**\n\n```{}\n{}\n```",
1545 entities.len(),
1546 relationships.len(),
1547 if format == "dot" { "dot" } else { format },
1548 visualization
1549 );
1550
1551 Ok(ToolResult {
1552 content: vec![ToolContent::Text { text }],
1553 is_error: false,
1554 })
1555}
1556
1557fn generate_mermaid(entities: &[Entity], relationships: &[Relationship]) -> String {
1558 let mut output = String::from("graph LR\n");
1559
1560 for entity in entities {
1562 let shape = match entity.entity_type {
1563 crate::models::graph::EntityType::Person => ("((", "))"),
1564 crate::models::graph::EntityType::Organization => ("[", "]"),
1565 crate::models::graph::EntityType::Technology => ("{", "}"),
1566 crate::models::graph::EntityType::Concept => ("([", "])"),
1567 crate::models::graph::EntityType::File => ("[[", "]]"),
1568 };
1569 let id = sanitize_mermaid_id(entity.id.as_ref());
1570 let name = sanitize_mermaid_label(&entity.name);
1571 output.push_str(&format!(" {id}{}{name}{}\n", shape.0, shape.1));
1572 }
1573
1574 for rel in relationships {
1576 let from = sanitize_mermaid_id(rel.from_entity.as_ref());
1577 let to = sanitize_mermaid_id(rel.to_entity.as_ref());
1578 let label = format!("{:?}", rel.relationship_type);
1579 output.push_str(&format!(" {from} -->|{label}| {to}\n"));
1580 }
1581
1582 output
1583}
1584
1585fn generate_dot(entities: &[Entity], relationships: &[Relationship]) -> String {
1586 let mut output =
1587 String::from("digraph G {\n rankdir=LR;\n node [fontname=\"Arial\"];\n\n");
1588
1589 for entity in entities {
1591 let shape = match entity.entity_type {
1592 crate::models::graph::EntityType::Person => "ellipse",
1593 crate::models::graph::EntityType::Organization => "box",
1594 crate::models::graph::EntityType::Technology => "diamond",
1595 crate::models::graph::EntityType::Concept => "oval",
1596 crate::models::graph::EntityType::File => "note",
1597 };
1598 let id = sanitize_dot_id(entity.id.as_ref());
1599 let name = entity.name.replace('"', "\\\"");
1600 output.push_str(&format!(" {id} [label=\"{name}\" shape={shape}];\n"));
1601 }
1602
1603 output.push('\n');
1604
1605 for rel in relationships {
1607 let from = sanitize_dot_id(rel.from_entity.as_ref());
1608 let to = sanitize_dot_id(rel.to_entity.as_ref());
1609 let label = format!("{:?}", rel.relationship_type);
1610 output.push_str(&format!(" {from} -> {to} [label=\"{label}\"];\n"));
1611 }
1612
1613 output.push_str("}\n");
1614 output
1615}
1616
1617fn generate_ascii(entities: &[Entity], relationships: &[Relationship]) -> String {
1618 let mut output = String::new();
1619
1620 output.push_str("ENTITIES:\n");
1621 output.push_str(&"-".repeat(40));
1622 output.push('\n');
1623 for entity in entities {
1624 let type_char = match entity.entity_type {
1625 crate::models::graph::EntityType::Person => "š¤",
1626 crate::models::graph::EntityType::Organization => "š¢",
1627 crate::models::graph::EntityType::Technology => "āļø",
1628 crate::models::graph::EntityType::Concept => "š”",
1629 crate::models::graph::EntityType::File => "š",
1630 };
1631 output.push_str(&format!("{} {} ({})\n", type_char, entity.name, entity.id));
1632 }
1633
1634 output.push_str("\nRELATIONSHIPS:\n");
1635 output.push_str(&"-".repeat(40));
1636 output.push('\n');
1637 for rel in relationships {
1638 output.push_str(&format!(
1639 "{} --[{:?}]--> {}\n",
1640 rel.from_entity, rel.relationship_type, rel.to_entity
1641 ));
1642 }
1643
1644 output
1645}
1646
1647fn sanitize_mermaid_id(id: &str) -> String {
1648 id.replace(['-', '.'], "_")
1649}
1650
1651fn sanitize_mermaid_label(label: &str) -> String {
1652 label.replace('"', "'").replace('[', "(").replace(']', ")")
1653}
1654
1655fn sanitize_dot_id(id: &str) -> String {
1656 let sanitized = id.replace(['-', '.'], "_");
1657 if sanitized.chars().next().is_none_or(char::is_numeric) {
1659 format!("n_{sanitized}")
1660 } else {
1661 sanitized
1662 }
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667 use super::*;
1668 use crate::models::graph::{EntityType, RelationshipType};
1669
1670 #[test]
1673 fn test_sanitize_mermaid_id() {
1674 assert_eq!(sanitize_mermaid_id("entity-123.test"), "entity_123_test");
1675 }
1676
1677 #[test]
1678 fn test_sanitize_mermaid_id_no_special_chars() {
1679 assert_eq!(sanitize_mermaid_id("simple_id"), "simple_id");
1680 }
1681
1682 #[test]
1683 fn test_sanitize_mermaid_id_multiple_special() {
1684 assert_eq!(sanitize_mermaid_id("a-b.c-d.e"), "a_b_c_d_e");
1685 }
1686
1687 #[test]
1688 fn test_sanitize_dot_id() {
1689 assert_eq!(sanitize_dot_id("entity-123"), "entity_123");
1690 assert_eq!(sanitize_dot_id("123-test"), "n_123_test");
1691 }
1692
1693 #[test]
1694 fn test_sanitize_dot_id_empty() {
1695 assert_eq!(sanitize_dot_id(""), "n_");
1696 }
1697
1698 #[test]
1699 fn test_sanitize_dot_id_numeric_start() {
1700 assert_eq!(sanitize_dot_id("42abc"), "n_42abc");
1701 }
1702
1703 #[test]
1706 fn test_parse_entity_type_person() {
1707 assert!(matches!(
1708 parse_entity_type("person"),
1709 Some(EntityType::Person)
1710 ));
1711 assert!(matches!(
1712 parse_entity_type("Person"),
1713 Some(EntityType::Person)
1714 ));
1715 assert!(matches!(
1716 parse_entity_type("PERSON"),
1717 Some(EntityType::Person)
1718 ));
1719 }
1720
1721 #[test]
1722 fn test_parse_entity_type_organization() {
1723 assert!(matches!(
1724 parse_entity_type("organization"),
1725 Some(EntityType::Organization)
1726 ));
1727 }
1728
1729 #[test]
1730 fn test_parse_entity_type_concept() {
1731 assert!(matches!(
1732 parse_entity_type("concept"),
1733 Some(EntityType::Concept)
1734 ));
1735 }
1736
1737 #[test]
1738 fn test_parse_entity_type_technology() {
1739 assert!(matches!(
1740 parse_entity_type("technology"),
1741 Some(EntityType::Technology)
1742 ));
1743 }
1744
1745 #[test]
1746 fn test_parse_entity_type_file() {
1747 assert!(matches!(parse_entity_type("file"), Some(EntityType::File)));
1748 }
1749
1750 #[test]
1751 fn test_parse_entity_type_unknown() {
1752 assert!(parse_entity_type("unknown_type").is_none());
1753 }
1754
1755 #[test]
1758 fn test_parse_relationship_type_works_at() {
1759 assert!(matches!(
1760 parse_relationship_type("works_at"),
1761 Some(RelationshipType::WorksAt)
1762 ));
1763 }
1764
1765 #[test]
1766 fn test_parse_relationship_type_uses() {
1767 assert!(matches!(
1768 parse_relationship_type("uses"),
1769 Some(RelationshipType::Uses)
1770 ));
1771 }
1772
1773 #[test]
1774 fn test_parse_relationship_type_relates_to() {
1775 assert!(matches!(
1776 parse_relationship_type("relates_to"),
1777 Some(RelationshipType::RelatesTo)
1778 ));
1779 }
1780
1781 #[test]
1782 fn test_parse_relationship_type_unknown() {
1783 assert!(parse_relationship_type("some_random_rel").is_none());
1784 }
1785
1786 #[test]
1789 fn test_entities_args_rejects_unknown_fields() {
1790 let json = r#"{"action": "list", "unknown_field": true}"#;
1791 let result: std::result::Result<EntitiesArgs, _> = serde_json::from_str(json);
1792 assert!(result.is_err());
1793 }
1794
1795 #[test]
1796 fn test_entities_args_accepts_valid_fields() {
1797 let json = r#"{"action": "create", "name": "Test", "entity_type": "Person"}"#;
1798 let result: std::result::Result<EntitiesArgs, _> = serde_json::from_str(json);
1799 assert!(result.is_ok());
1800 }
1801
1802 #[test]
1803 fn test_entities_args_list_action() {
1804 let json = r#"{"action": "list", "limit": 10}"#;
1805 let result: std::result::Result<EntitiesArgs, _> = serde_json::from_str(json);
1806 assert!(result.is_ok());
1807 let args = result.expect("should parse");
1808 assert_eq!(args.action, "list");
1809 assert_eq!(args.limit, Some(10));
1810 }
1811
1812 #[test]
1813 fn test_entities_args_with_aliases() {
1814 let json = r#"{"action": "create", "name": "Bob", "entity_type": "Person", "aliases": ["Robert", "Bobby"]}"#;
1815 let result: std::result::Result<EntitiesArgs, _> = serde_json::from_str(json);
1816 assert!(result.is_ok());
1817 let args = result.expect("should parse");
1818 assert_eq!(args.aliases.expect("should have aliases").len(), 2);
1819 }
1820
1821 #[test]
1824 fn test_relationships_args_list() {
1825 let json = r#"{"action": "list"}"#;
1826 let result: std::result::Result<RelationshipsArgs, _> = serde_json::from_str(json);
1827 assert!(result.is_ok());
1828 }
1829
1830 #[test]
1831 fn test_relationships_args_create() {
1832 let json = r#"{"action": "create", "from_entity": "e1", "to_entity": "e2", "relationship_type": "depends_on"}"#;
1833 let result: std::result::Result<RelationshipsArgs, _> = serde_json::from_str(json);
1834 assert!(result.is_ok());
1835 let args = result.expect("should parse");
1836 assert_eq!(args.from_entity.expect("from"), "e1");
1837 assert_eq!(args.to_entity.expect("to"), "e2");
1838 }
1839
1840 #[test]
1841 fn test_relationships_args_rejects_unknown() {
1842 let json = r#"{"action": "list", "extra": true}"#;
1843 let result: std::result::Result<RelationshipsArgs, _> = serde_json::from_str(json);
1844 assert!(result.is_err());
1845 }
1846
1847 #[test]
1850 fn test_graph_query_args_accepts_valid_fields() {
1851 let json = r#"{"operation": "stats"}"#;
1852 let result: std::result::Result<GraphQueryArgs, _> = serde_json::from_str(json);
1853 assert!(result.is_ok());
1854 }
1855
1856 #[test]
1857 fn test_graph_query_args_traverse() {
1858 let json = r#"{"operation": "traverse", "entity_id": "e123", "depth": 3}"#;
1859 let result: std::result::Result<GraphQueryArgs, _> = serde_json::from_str(json);
1860 assert!(result.is_ok());
1861 let args = result.expect("should parse");
1862 assert_eq!(args.operation, "traverse");
1863 assert_eq!(args.depth, Some(3));
1864 }
1865
1866 #[test]
1867 fn test_graph_query_args_path() {
1868 let json = r#"{"operation": "path", "from_entity": "start", "to_entity": "end"}"#;
1869 let result: std::result::Result<GraphQueryArgs, _> = serde_json::from_str(json);
1870 assert!(result.is_ok());
1871 let args = result.expect("should parse");
1872 assert_eq!(args.from_entity.expect("from"), "start");
1873 assert_eq!(args.to_entity.expect("to"), "end");
1874 }
1875
1876 #[test]
1879 fn test_extract_entities_args_accepts_valid_fields() {
1880 let json = r#"{"content": "Alice works at Acme", "store": true}"#;
1881 let result: std::result::Result<ExtractEntitiesArgs, _> = serde_json::from_str(json);
1882 assert!(result.is_ok());
1883 let args = result.expect("should parse");
1884 assert!(args.store);
1885 }
1886
1887 #[test]
1888 fn test_extract_entities_args_with_memory_id() {
1889 let json = r#"{"content": "Test content", "memory_id": "mem_123"}"#;
1890 let result: std::result::Result<ExtractEntitiesArgs, _> = serde_json::from_str(json);
1891 assert!(result.is_ok());
1892 let args = result.expect("should parse");
1893 assert_eq!(args.memory_id.expect("memory_id"), "mem_123");
1894 }
1895
1896 #[test]
1897 fn test_extract_entities_args_min_confidence() {
1898 let json = r#"{"content": "Test", "min_confidence": 0.8}"#;
1899 let result: std::result::Result<ExtractEntitiesArgs, _> = serde_json::from_str(json);
1900 assert!(result.is_ok());
1901 let args = result.expect("should parse");
1902 assert!((args.min_confidence.expect("confidence") - 0.8).abs() < f32::EPSILON);
1903 }
1904
1905 #[test]
1908 fn test_entity_merge_args_find_duplicates() {
1909 let json = r#"{"action": "find_duplicates", "entity_id": "e123"}"#;
1910 let result: std::result::Result<EntityMergeArgs, _> = serde_json::from_str(json);
1911 assert!(result.is_ok());
1912 }
1913
1914 #[test]
1915 fn test_entity_merge_args_merge() {
1916 let json =
1917 r#"{"action": "merge", "entity_ids": ["e1", "e2"], "canonical_name": "Merged Entity"}"#;
1918 let result: std::result::Result<EntityMergeArgs, _> = serde_json::from_str(json);
1919 assert!(result.is_ok());
1920 let args = result.expect("should parse");
1921 assert_eq!(args.entity_ids.expect("ids").len(), 2);
1922 }
1923
1924 #[test]
1925 fn test_entity_merge_args_threshold() {
1926 let json = r#"{"action": "find_duplicates", "threshold": 0.85}"#;
1927 let result: std::result::Result<EntityMergeArgs, _> = serde_json::from_str(json);
1928 assert!(result.is_ok());
1929 let args = result.expect("should parse");
1930 assert!((args.threshold.expect("threshold") - 0.85).abs() < f32::EPSILON);
1931 }
1932
1933 #[test]
1936 fn test_relationship_infer_args_basic() {
1937 let json = r#"{"store": false}"#;
1938 let result: std::result::Result<RelationshipInferArgs, _> = serde_json::from_str(json);
1939 assert!(result.is_ok());
1940 }
1941
1942 #[test]
1943 fn test_relationship_infer_args_with_entity_ids() {
1944 let json = r#"{"entity_ids": ["e1", "e2", "e3"], "store": true}"#;
1945 let result: std::result::Result<RelationshipInferArgs, _> = serde_json::from_str(json);
1946 assert!(result.is_ok());
1947 let args = result.expect("should parse");
1948 assert!(args.store);
1949 assert_eq!(args.entity_ids.expect("ids").len(), 3);
1950 }
1951
1952 #[test]
1953 fn test_relationship_infer_args_confidence() {
1954 let json = r#"{"min_confidence": 0.7, "limit": 25}"#;
1955 let result: std::result::Result<RelationshipInferArgs, _> = serde_json::from_str(json);
1956 assert!(result.is_ok());
1957 let args = result.expect("should parse");
1958 assert_eq!(args.limit, Some(25));
1959 }
1960
1961 #[test]
1964 fn test_graph_visualize_args_accepts_valid_fields() {
1965 let json = r#"{"format": "mermaid", "depth": 3}"#;
1966 let result: std::result::Result<GraphVisualizeArgs, _> = serde_json::from_str(json);
1967 assert!(result.is_ok());
1968 }
1969
1970 #[test]
1971 fn test_graph_visualize_args_dot_format() {
1972 let json = r#"{"format": "dot"}"#;
1973 let result: std::result::Result<GraphVisualizeArgs, _> = serde_json::from_str(json);
1974 assert!(result.is_ok());
1975 let args = result.expect("should parse");
1976 assert_eq!(args.format, Some("dot".to_string()));
1977 }
1978
1979 #[test]
1980 fn test_graph_visualize_args_with_entity() {
1981 let json = r#"{"entity_id": "e123", "depth": 2}"#;
1982 let result: std::result::Result<GraphVisualizeArgs, _> = serde_json::from_str(json);
1983 assert!(result.is_ok());
1984 let args = result.expect("should parse");
1985 assert_eq!(args.entity_id.expect("entity_id"), "e123");
1986 }
1987
1988 #[test]
1991 fn test_format_type_counts_empty() {
1992 let counts: HashMap<String, usize> = HashMap::new();
1993 assert_eq!(format_type_counts(&counts), " (none)\n");
1994 }
1995
1996 #[test]
1997 fn test_format_type_counts_single() {
1998 let mut counts: HashMap<String, usize> = HashMap::new();
1999 counts.insert("Person".to_string(), 5);
2000 assert_eq!(format_type_counts(&counts), " - \"Person\": 5\n");
2002 }
2003
2004 #[test]
2005 fn test_format_type_counts_multiple() {
2006 let mut counts: HashMap<String, usize> = HashMap::new();
2007 counts.insert("Person".to_string(), 3);
2008 counts.insert("Org".to_string(), 2);
2009 let result = format_type_counts(&counts);
2010 assert!(result.contains("\"Person\": 3"));
2012 assert!(result.contains("\"Org\": 2"));
2013 }
2014}