Skip to main content

subcog/mcp/tools/handlers/
graph.rs

1//! Graph tool execution handlers.
2//!
3//! Implements MCP tool handlers for knowledge graph operations:
4//! - Entity CRUD operations
5//! - Relationship management
6//! - Graph traversal and queries
7//! - LLM-powered entity extraction
8//! - Entity deduplication
9//! - Relationship inference
10//! - Graph visualization
11
12use 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
27// ============================================================================
28// Entity Operations
29// ============================================================================
30
31/// Executes the entities tool (CRUD operations on entities).
32///
33/// # Arguments
34///
35/// * `arguments` - JSON arguments containing action and entity parameters
36///
37/// # Returns
38///
39/// A tool result with the operation outcome.
40///
41/// # Errors
42///
43/// Returns an error if argument parsing or the operation fails.
44pub 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
214/// Handles the `extract` action for `subcog_entities`.
215///
216/// Extracts entities from text content using pattern-based extraction.
217fn 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    // Load config and build LLM provider if available
231    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    // Store entities if requested (with automatic deduplication)
293    if args.store && !result.entities.is_empty() {
294        let graph = container.graph()?;
295        let graph_entities = extractor.to_graph_entities(&result);
296
297        // Store entities with deduplication and build map with actual IDs
298        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            // Create entity with the actual ID for relationship mapping
302            let mut stored_entity = entity.clone();
303            stored_entity.id = actual_id;
304            entity_map.insert(entity.name.clone(), stored_entity);
305        }
306
307        // Store relationships using the actual entity IDs
308        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
334/// Handles the `merge` action for `subcog_entities`.
335///
336/// Supports sub-actions: `find_duplicates`, merge.
337fn 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
349/// Finds duplicate entities for the `merge` action.
350fn 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
409/// Merges entities for the `merge` action.
410fn 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
448// ============================================================================
449// Relationship Operations
450// ============================================================================
451
452/// Executes the relationships tool (CRUD operations on relationships).
453///
454/// # Errors
455///
456/// Returns an error if argument parsing or the operation fails.
457pub 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        // List all relationships with limit
532        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
591/// Handles the `infer` action for `subcog_relationships`.
592///
593/// Infers implicit relationships between entities using pattern-based extraction.
594fn execute_relationship_infer_action(args: &RelationshipsArgs) -> Result<ToolResult> {
595    let container = ServiceContainer::from_current_dir_or_user()?;
596    let graph = container.graph()?;
597
598    // Get entities to analyze
599    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        // Get recent entities
610        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    // Load config and build LLM provider if available
625    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    // Store relationships if requested
666    if args.store && !result.relationships.is_empty() {
667        // Build entity map for lookup
668        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
700// ============================================================================
701// Consolidated Graph Tool
702// ============================================================================
703
704/// Executes the consolidated `subcog_graph` tool.
705///
706/// Combines graph query (neighbors, path, stats) and visualization operations.
707///
708/// # Errors
709///
710/// Returns an error if argument parsing or the operation fails.
711pub 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
727/// Handles the `neighbors` operation for `subcog_graph`.
728fn 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
767/// Handles the `path` operation for `subcog_graph`.
768fn 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
823/// Handles the `stats` operation for `subcog_graph`.
824fn 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
850/// Handles the `visualize` operation for `subcog_graph`.
851fn 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    // Get entities and relationships to visualize
860    let (entities, relationships) = if let Some(entity_id) = &args.entity_id {
861        // Center on specific entity
862        let id = EntityId::new(entity_id);
863        let result = graph.traverse(&id, depth, None, None)?;
864        (result.entities, result.relationships)
865    } else {
866        // Get all entities up to limit
867        let query = EntityQuery::new().with_limit(limit);
868        let entities = graph.query_entities(&query)?;
869
870        // Get all relationships between these entities
871        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    // Apply type filters if specified
883    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
943// ============================================================================
944// Graph Query Operations (Legacy)
945// ============================================================================
946
947/// Executes the graph query tool (traversal operations).
948///
949/// # Errors
950///
951/// Returns an error if argument parsing or the operation fails.
952pub 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
1098// ============================================================================
1099// Entity Extraction
1100// ============================================================================
1101
1102/// Executes the extract entities tool (LLM-powered extraction).
1103///
1104/// # Errors
1105///
1106/// Returns an error if argument parsing or extraction fails.
1107pub 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    // Load config and build LLM provider if available
1120    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    // Store entities if requested (with automatic deduplication)
1175    if args.store && !result.entities.is_empty() {
1176        let graph = container.graph()?;
1177        let graph_entities = extractor.to_graph_entities(&result);
1178
1179        // Store entities with deduplication and build map with actual IDs
1180        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            // Create entity with the actual ID for relationship mapping
1184            let mut stored_entity = entity.clone();
1185            stored_entity.id = actual_id;
1186            entity_map.insert(entity.name.clone(), stored_entity);
1187        }
1188
1189        // Store relationships using the actual entity IDs
1190        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
1216// ============================================================================
1217// Entity Merge
1218// ============================================================================
1219
1220/// Executes the entity merge tool (deduplication).
1221///
1222/// # Errors
1223///
1224/// Returns an error if argument parsing or the operation fails.
1225pub 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
1335// ============================================================================
1336// Relationship Inference
1337// ============================================================================
1338
1339/// Executes the relationship inference tool.
1340///
1341/// # Errors
1342///
1343/// Returns an error if argument parsing or inference fails.
1344pub 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    // Get entities to analyze
1352    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        // Get recent entities
1363        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    // Load config and build LLM provider if available
1378    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    // Store relationships if requested
1419    if args.store && !result.relationships.is_empty() {
1420        // Build entity map for lookup
1421        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
1453// ============================================================================
1454// Graph Visualization
1455// ============================================================================
1456
1457/// Executes the graph visualization tool.
1458///
1459/// # Errors
1460///
1461/// Returns an error if argument parsing or visualization fails.
1462pub 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    // Get entities and relationships to visualize
1474    let (entities, relationships) = if let Some(entity_id) = &args.entity_id {
1475        // Center on specific entity
1476        let id = EntityId::new(entity_id);
1477        let result = graph.traverse(&id, depth, None, None)?;
1478        (result.entities, result.relationships)
1479    } else {
1480        // Get all entities up to limit
1481        let query = EntityQuery::new().with_limit(limit);
1482        let entities = graph.query_entities(&query)?;
1483
1484        // Get all relationships between these entities
1485        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    // Apply type filters if specified
1497    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    // Add entities as nodes
1561    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    // Add relationships as edges
1575    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    // Add entities as nodes
1590    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    // Add relationships as edges
1606    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    // Ensure it starts with a letter or underscore
1658    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    // ========== Sanitization Tests ==========
1671
1672    #[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    // ========== Entity Type Parsing Tests ==========
1704
1705    #[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    // ========== Relationship Type Parsing Tests ==========
1756
1757    #[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    // ========== EntitiesArgs Validation Tests ==========
1787
1788    #[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    // ========== RelationshipsArgs Validation Tests ==========
1822
1823    #[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    // ========== GraphQueryArgs Validation Tests ==========
1848
1849    #[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    // ========== ExtractEntitiesArgs Validation Tests ==========
1877
1878    #[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    // ========== EntityMergeArgs Validation Tests ==========
1906
1907    #[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    // ========== RelationshipInferArgs Validation Tests ==========
1934
1935    #[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    // ========== GraphVisualizeArgs Validation Tests ==========
1962
1963    #[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    // ========== Format Helper Tests ==========
1989
1990    #[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        // Uses Debug formatting, so strings get quotes
2001        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        // Order is not guaranteed, so check both are present (with Debug formatting)
2011        assert!(result.contains("\"Person\": 3"));
2012        assert!(result.contains("\"Org\": 2"));
2013    }
2014}