do-memory-mcp 0.1.31

Model Context Protocol (MCP) server for AI agents
Documentation
use super::*;

impl EpisodeRelationshipTools {
    #[instrument(skip(self, input), fields(episode_id = %input.episode_id))]
    pub async fn get_dependency_graph(
        &self,
        input: DependencyGraphInput,
    ) -> Result<DependencyGraphOutput> {
        info!(
            "Building dependency graph for episode: {}",
            input.episode_id
        );

        let episode_id =
            Uuid::parse_str(&input.episode_id).map_err(|e| anyhow!("Invalid episode_id: {}", e))?;
        let depth = input.depth.map(|d| d.clamp(1, 5)).unwrap_or(2);
        let format = input.format.as_deref().unwrap_or("json");

        let graph = self
            .memory
            .build_relationship_graph(episode_id, depth)
            .await?;

        let nodes: Vec<RelationshipNode> = graph
            .nodes
            .values()
            .map(|ep| RelationshipNode {
                id: ep.episode_id.to_string(),
                task_description: ep.task_description.clone(),
                task_type: format!("{:?}", ep.task_type),
                is_complete: ep.is_complete(),
            })
            .collect();

        let edges: Vec<RelationshipEdge> = graph.edges.iter().map(relationship_to_edge).collect();
        let dot = if format == "dot" {
            Some(graph.to_dot())
        } else {
            None
        };

        let node_count = nodes.len();
        let edge_count = edges.len();

        info!(
            "Built dependency graph with {} nodes and {} edges",
            node_count, edge_count
        );

        Ok(DependencyGraphOutput {
            success: true,
            root: input.episode_id,
            node_count,
            edge_count,
            nodes,
            edges,
            dot,
            message: format!(
                "Graph contains {} nodes and {} edges",
                node_count, edge_count
            ),
        })
    }

    #[instrument(skip(self, input), fields(from = %input.from_episode_id, to = %input.to_episode_id))]
    pub async fn validate_no_cycles(
        &self,
        input: ValidateNoCyclesInput,
    ) -> Result<ValidateNoCyclesOutput> {
        info!(
            "Validating no cycles for relationship from {} to {}",
            input.from_episode_id, input.to_episode_id
        );

        let from_id = Uuid::parse_str(&input.from_episode_id)
            .map_err(|e| anyhow!("Invalid from_episode_id: {}", e))?;
        let to_id = Uuid::parse_str(&input.to_episode_id)
            .map_err(|e| anyhow!("Invalid to_episode_id: {}", e))?;
        let rel_type = RelationshipType::parse(&input.relationship_type)
            .map_err(|e| anyhow!("Invalid relationship_type: {}", e))?;

        if !rel_type.requires_acyclic() {
            return Ok(ValidateNoCyclesOutput {
                success: true,
                would_create_cycle: false,
                is_valid: true,
                cycle_path: None,
                message: format!(
                    "Relationship type '{}' does not require acyclic validation",
                    input.relationship_type
                ),
            });
        }

        let mut adjacency_list: HashMap<Uuid, Vec<EpisodeRelationship>> = HashMap::new();
        let all_rels = self.get_all_relationships().await?;

        for rel in all_rels {
            adjacency_list
                .entry(rel.from_episode_id)
                .or_default()
                .push(rel);
        }

        let would_create_cycle = do_memory_core::episode::graph_algorithms::has_path_dfs(
            &adjacency_list,
            to_id,
            from_id,
        )?;

        let cycle_path = if would_create_cycle {
            match do_memory_core::episode::graph_algorithms::find_path_dfs(
                &adjacency_list,
                to_id,
                from_id,
            ) {
                Ok(path) => Some(path.iter().map(|u| u.to_string()).collect()),
                Err(_) => None,
            }
        } else {
            None
        };

        Ok(ValidateNoCyclesOutput {
            success: true,
            would_create_cycle,
            is_valid: !would_create_cycle,
            cycle_path,
            message: if would_create_cycle {
                "Adding this relationship would create a cycle".to_string()
            } else {
                "No cycle would be created".to_string()
            },
        })
    }

    #[instrument(skip(self, input), fields(episode_count = input.episode_ids.len()))]
    pub async fn get_topological_order(
        &self,
        input: GetTopologicalOrderInput,
    ) -> Result<GetTopologicalOrderOutput> {
        info!(
            "Getting topological order for {} episodes",
            input.episode_ids.len()
        );

        if input.episode_ids.is_empty() {
            return Ok(GetTopologicalOrderOutput {
                success: true,
                order: vec![],
                count: 0,
                has_cycles: false,
                message: "No episodes provided".to_string(),
            });
        }

        let mut episode_ids = Vec::new();
        for id_str in &input.episode_ids {
            let id = Uuid::parse_str(id_str)
                .map_err(|e| anyhow!("Invalid episode_id '{}': {}", id_str, e))?;
            episode_ids.push(id);
        }

        let mut adjacency_list: HashMap<Uuid, Vec<EpisodeRelationship>> = HashMap::new();

        for episode_id in &episode_ids {
            if let Ok(rels) = self
                .memory
                .get_episode_relationships(*episode_id, Direction::Outgoing)
                .await
            {
                let filtered_rels: Vec<_> = rels
                    .into_iter()
                    .filter(|r| episode_ids.contains(&r.to_episode_id))
                    .collect();

                if !filtered_rels.is_empty() {
                    adjacency_list.insert(*episode_id, filtered_rels);
                }
            }
        }

        let has_cycles = do_memory_core::episode::graph_algorithms::has_cycle(&adjacency_list)?;
        if has_cycles {
            return Ok(GetTopologicalOrderOutput {
                success: true,
                order: vec![],
                count: 0,
                has_cycles: true,
                message: "Cannot compute topological order: graph contains cycles".to_string(),
            });
        }

        let sorted_ids =
            do_memory_core::episode::graph_algorithms::topological_sort(&adjacency_list)?;

        let mut order = Vec::new();
        for (position, id) in sorted_ids.iter().enumerate() {
            if let Ok(episode) = self.memory.get_episode(*id).await {
                order.push(TopologicalEpisode {
                    episode_id: id.to_string(),
                    task_description: episode.task_description.clone(),
                    position: position + 1,
                });
            }
        }

        for id in &episode_ids {
            if !sorted_ids.contains(id) {
                if let Ok(episode) = self.memory.get_episode(*id).await {
                    order.push(TopologicalEpisode {
                        episode_id: id.to_string(),
                        task_description: episode.task_description.clone(),
                        position: order.len() + 1,
                    });
                }
            }
        }

        let count = order.len();
        info!("Computed topological order for {} episodes", count);

        Ok(GetTopologicalOrderOutput {
            success: true,
            order,
            count,
            has_cycles: false,
            message: format!("Episodes in topological order ({} total)", count),
        })
    }

    async fn get_all_relationships(&self) -> Result<Vec<EpisodeRelationship>> {
        Ok(Vec::new())
    }
}

pub(super) fn relationship_to_edge(rel: &EpisodeRelationship) -> RelationshipEdge {
    RelationshipEdge {
        id: rel.id.to_string(),
        from: rel.from_episode_id.to_string(),
        to: rel.to_episode_id.to_string(),
        relationship_type: rel.relationship_type.as_str().to_string(),
        reason: rel.metadata.reason.clone(),
        priority: rel.metadata.priority,
        created_by: rel.metadata.created_by.clone(),
        created_at: rel.created_at.to_rfc3339(),
    }
}