From c47f98c2c2474f87f0dd1af3a6275d3f370d10c1 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 4 Jun 2025 09:10:05 -0400 Subject: [PATCH 01/25] feat add tools for relations using ID --- .gitignore | 1 + grc20-core/src/mapping/relation/utils.rs | 2 +- mcp-server/src/main.rs | 198 ++++++++++++++++++++++- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 39a1aa6..1867157 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **/target **/ipfs-cache **/data +**/sample_data *.json .env **/.fastembed_cache \ No newline at end of file diff --git a/grc20-core/src/mapping/relation/utils.rs b/grc20-core/src/mapping/relation/utils.rs index 15082d5..fba8398 100644 --- a/grc20-core/src/mapping/relation/utils.rs +++ b/grc20-core/src/mapping/relation/utils.rs @@ -85,6 +85,6 @@ impl<'a> MatchOneRelation<'a> { .subquery(format!("ORDER BY {edge_var}.index")) .params("id", self.id) .params("space_id", self.space_id) - .with(vec![from_node_var, edge_var, to_node_var], next) + .with(vec![from_node_var, edge_var.to_string(), to_node_var], next) } } diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 7e55c45..f1302a3 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -2,9 +2,9 @@ use clap::{Args, Parser}; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::TryStreamExt; use grc20_core::{ - entity::{self, Entity, EntityRelationFilter, TypesFilter}, - mapping::{Query, QueryStream}, - neo4rs, system_ids, + entity::{self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter}, + mapping::{Query, QueryStream, RelationEdge, prop_filter}, + neo4rs, relation, system_ids, }; use grc20_sdk::models::BaseEntity; use rmcp::{ @@ -326,6 +326,198 @@ impl KnowledgeGraph { .expect("Failed to create JSON content"), ])) } + + #[tool(description = "Search Relation outbound from entity")] + async fn get_outbound_relations( + &self, + #[tool(param)] + #[schemars(description = "The id of the Relation type to find")] + relation_type_id: String, + #[tool(param)] + #[schemars(description = "The id of the from in the relation")] + entity_id: String, + ) -> Result { + let relations = relation::find_many::>(&self.neo4j) + .filter( + relation::RelationFilter::default() + .relation_type( + EntityFilter::default().id(prop_filter::value(relation_type_id.clone())), + ) + .from_(EntityFilter::default().id(prop_filter::value(entity_id.clone()))), + ) + .limit(100) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + })?; + + tracing::info!("Found result from entity '{}'", entity_id); + + Ok(CallToolResult::success( + relations + .into_iter() + .map(|result| { + Content::json(json!({ + "id": result.id, + "relation_type": result.relation_type, + "from": result.from.id, + "to": result.to.id, + })) + .expect("Failed to create JSON content") + }) + .collect(), + )) + } + + #[tool(description = "Search Relation inbound from entity")] + async fn get_inbound_relations( + &self, + #[tool(param)] + #[schemars(description = "The id of the Relation type to find")] + relation_type_id: String, + #[tool(param)] + #[schemars(description = "The id of the to in the relation")] + entity_id: String, + ) -> Result { + let relations = relation::find_many::>(&self.neo4j) + .filter( + relation::RelationFilter::default() + .relation_type( + EntityFilter::default().id(prop_filter::value(relation_type_id.clone())), + ) + .to_(EntityFilter::default().id(prop_filter::value(entity_id.clone()))), + ) + .limit(100) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + })?; + + tracing::info!("Found result from entity '{}'", entity_id); + + Ok(CallToolResult::success( + relations + .into_iter() + .map(|result| { + Content::json(json!({ + "id": result.id, + "relation_type": result.relation_type, + "from": result.from.id, + "to": result.to.id, + })) + .expect("Failed to create JSON content") + }) + .collect(), + )) + } + + #[tool(description = "Search Relations between 2 entities")] + async fn get_relations_between_entities( + &self, + #[tool(param)] + #[schemars(description = "The id of the first Entity to find relations")] + entity1_id: String, + #[tool(param)] + #[schemars(description = "The id of the second Entity to find relations")] + entity2_id: String, + ) -> Result { + let mut relations_first_direction = + relation::find_many::>(&self.neo4j) + .filter( + relation::RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))) + .to_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))), + ) + .limit(100) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + })?; + + let mut relations_second_direction = + relation::find_many::>(&self.neo4j) + .filter( + relation::RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))) + .to_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))), + ) + .limit(100) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + })?; + + tracing::info!( + "Found {} relations from the first to the second and {} from the second to the first", + relations_first_direction.len(), + relations_second_direction.len() + ); + + relations_first_direction.append(&mut relations_second_direction); + + Ok(CallToolResult::success( + relations_first_direction + .into_iter() + .map(|result| { + Content::json(json!({ + "id": result.id, + "relation_type": result.relation_type, + "from": result.from.id, + "to": result.to.id, + })) + .expect("Failed to create JSON content") + }) + .collect(), + )) + } } #[tool(tool_box)] From 8474154393d1adec041bc0f255ffbffd810f6b21 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 4 Jun 2025 12:24:07 -0400 Subject: [PATCH 02/25] fix can subquery multiple attributes --- grc20-core/src/mapping/query_utils/attributes_filter.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/grc20-core/src/mapping/query_utils/attributes_filter.rs b/grc20-core/src/mapping/query_utils/attributes_filter.rs index 7860342..3b1ff61 100644 --- a/grc20-core/src/mapping/query_utils/attributes_filter.rs +++ b/grc20-core/src/mapping/query_utils/attributes_filter.rs @@ -104,9 +104,10 @@ impl AttributeFilter { pub fn subquery(&self, node_var: &str) -> MatchQuery { let attr_rel_var = format!("r_{node_var}_{}", self.attribute); let attr_node_var = format!("{node_var}_{}", self.attribute); + let attr_id_var = format!("a_{node_var}_{}", self.attribute); MatchQuery::new( - format!("({node_var}) -[{attr_rel_var}:ATTRIBUTE]-> ({attr_node_var}:Attribute {{id: $attribute}})") + format!("({node_var}) -[{attr_rel_var}:ATTRIBUTE]-> ({attr_node_var}:Attribute {{id: ${attr_id_var}}})") ) .r#where(self.version.subquery(&attr_rel_var)) .where_opt( @@ -118,6 +119,6 @@ impl AttributeFilter { .where_opt( self.value_type.as_ref().map(|value_type| value_type.subquery(&attr_node_var, "value_type", None)) ) - .params("attribute", self.attribute.clone()) + .params(attr_id_var, self.attribute.clone()) } } From 476a177eb682a7d25eac70944a14a835bd608fc8 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 4 Jun 2025 12:24:41 -0400 Subject: [PATCH 03/25] feat setup embeddings on seed data --- sink/examples/seed_data.rs | 61 ++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/sink/examples/seed_data.rs b/sink/examples/seed_data.rs index 57d9439..05dedd0 100644 --- a/sink/examples/seed_data.rs +++ b/sink/examples/seed_data.rs @@ -72,13 +72,18 @@ async fn main() -> anyhow::Result<()> { .await .expect("Failed to connect to Neo4j"); + let embedding_model = TextEmbedding::try_new( + InitOptions::new(EMBEDDING_MODEL).with_show_download_progress(true), + )?; + // Reset and bootstrap the database reset_db(&neo4j).await?; - bootstrap(&neo4j).await?; + bootstrap(&neo4j, &embedding_model).await?; // Create some common types create_type( &neo4j, + &embedding_model, "Person", [], [ @@ -91,6 +96,7 @@ async fn main() -> anyhow::Result<()> { create_type( &neo4j, + &embedding_model, "Event", [], [ @@ -103,6 +109,7 @@ async fn main() -> anyhow::Result<()> { create_type( &neo4j, + &embedding_model, "City", [], [ @@ -115,6 +122,7 @@ async fn main() -> anyhow::Result<()> { create_property( &neo4j, + &embedding_model, "Event location", system_ids::RELATION_SCHEMA_TYPE, Some(CITY_TYPE), @@ -124,6 +132,7 @@ async fn main() -> anyhow::Result<()> { create_property( &neo4j, + &embedding_model, "Speakers", system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::PERSON_TYPE), @@ -133,6 +142,7 @@ async fn main() -> anyhow::Result<()> { create_property( &neo4j, + &embedding_model, "Side events", system_ids::RELATION_SCHEMA_TYPE, Some(EVENT_TYPE), @@ -143,6 +153,7 @@ async fn main() -> anyhow::Result<()> { // Create person entities create_entity( &neo4j, + &embedding_model, "Alice", None, [system_ids::PERSON_TYPE], @@ -160,6 +171,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Bob", None, [system_ids::PERSON_TYPE], @@ -171,6 +183,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Carol", None, [system_ids::PERSON_TYPE], @@ -182,6 +195,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Dave", None, [system_ids::PERSON_TYPE], @@ -193,6 +207,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Joe", None, [system_ids::PERSON_TYPE], @@ -204,6 +219,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Chris", None, [system_ids::PERSON_TYPE], @@ -216,6 +232,7 @@ async fn main() -> anyhow::Result<()> { // Create city entities create_entity( &neo4j, + &embedding_model, "San Francisco", Some("City in California"), [CITY_TYPE], @@ -227,6 +244,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "New York", Some("City in New York State"), [CITY_TYPE], @@ -240,6 +258,7 @@ async fn main() -> anyhow::Result<()> { // Create side event entities for RustConf 2023 create_entity( &neo4j, + &embedding_model, "Rust Async Workshop", Some("A hands-on workshop about async programming in Rust"), [EVENT_TYPE], @@ -254,6 +273,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "RustConf Hackathon", Some("A hackathon for RustConf 2023 attendees"), [EVENT_TYPE], @@ -268,6 +288,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "Rust Conference 2023", Some("A conference about Rust programming language"), [EVENT_TYPE], @@ -285,6 +306,7 @@ async fn main() -> anyhow::Result<()> { create_entity( &neo4j, + &embedding_model, "JavaScript Summit 2024", Some("A summit for JavaScript enthusiasts and professionals"), [EVENT_TYPE], @@ -301,11 +323,7 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { - let embedding_model = TextEmbedding::try_new( - InitOptions::new(EMBEDDING_MODEL).with_show_download_progress(true), - )?; - +pub async fn bootstrap(neo4j: &neo4rs::Graph, embedding_model: &TextEmbedding) -> anyhow::Result<()> { let triples = vec![ // Value types Triple::new(system_ids::CHECKBOX, system_ids::NAME_ATTRIBUTE, "Checkbox"), @@ -401,6 +419,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { // Create properties create_property( neo4j, + &embedding_model, "Properties", system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::ATTRIBUTE), @@ -410,6 +429,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Types", system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::SCHEMA_TYPE), @@ -419,6 +439,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Value Type", system_ids::RELATION_SCHEMA_TYPE, None::<&str>, @@ -428,6 +449,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Relation type attribute", system_ids::RELATION_SCHEMA_TYPE, None::<&str>, @@ -437,6 +459,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Relation index", system_ids::TEXT, None::<&str>, @@ -446,6 +469,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Relation value type", system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::SCHEMA_TYPE), @@ -455,6 +479,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Name", system_ids::TEXT, None::<&str>, @@ -464,6 +489,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_property( neo4j, + &embedding_model, "Description", system_ids::TEXT, None::<&str>, @@ -474,6 +500,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { // Create types create_type( neo4j, + &embedding_model, "Type", [system_ids::SCHEMA_TYPE], [ @@ -488,6 +515,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_type( neo4j, + &embedding_model, "Relation schema type", [system_ids::RELATION_SCHEMA_TYPE], [system_ids::RELATION_VALUE_RELATIONSHIP_TYPE], @@ -497,6 +525,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_type( neo4j, + &embedding_model, "Attribute", [system_ids::SCHEMA_TYPE], [ @@ -510,6 +539,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { create_type( neo4j, + &embedding_model, "Relation instance type", [system_ids::RELATION_TYPE], [ @@ -525,6 +555,7 @@ pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { pub async fn create_entity( neo4j: &neo4rs::Graph, + embedding_model: &TextEmbedding, name: impl Into, description: Option<&str>, types: impl IntoIterator, @@ -538,10 +569,11 @@ pub async fn create_entity( // Set: Entity.name triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) - .triples(vec![Triple::new( + .triples(vec![Triple::with_embedding( &entity_id, system_ids::NAME_ATTRIBUTE, - name, + name.clone(), + embedding_model.embed(vec!(name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), )]) .send() .await?; @@ -596,6 +628,7 @@ pub async fn create_entity( /// Creates a type with the given name, types, and properties. pub async fn create_type( neo4j: &neo4rs::Graph, + embedding_model: &TextEmbedding, name: impl Into, types: impl IntoIterator, properties: impl IntoIterator, @@ -612,10 +645,11 @@ pub async fn create_type( // Set: Type.name triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_TYPE, DEFAULT_VERSION) - .triples(vec![Triple::new( + .triples(vec![Triple::with_embedding( &type_id, system_ids::NAME_ATTRIBUTE, - name, + name.clone(), + embedding_model.embed(vec!(name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), )]) .send() .await?; @@ -650,6 +684,7 @@ pub async fn create_type( /// Note: if that is the case, then `value_type` should be the system_ids::RELATION_SCHEMA_TYPE type). pub async fn create_property( neo4j: &neo4rs::Graph, + embedding_model: &TextEmbedding, name: impl Into, value_type: impl Into, relation_value_type: Option>, @@ -658,13 +693,15 @@ pub async fn create_property( let block = BlockMetadata::default(); let property_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); + let string_name = name.into(); // Set: Property.name triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) - .triples(vec![Triple::new( + .triples(vec![Triple::with_embedding( &property_id, system_ids::NAME_ATTRIBUTE, - name.into(), + string_name.clone(), + embedding_model.embed(vec!(string_name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), )]) .send() .await?; From 0031b29d827783e8b5facedc3b2b0df8385f3b43 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Thu, 5 Jun 2025 11:50:13 -0400 Subject: [PATCH 04/25] feat attributes and relation on get and name lookup --- mcp-server/src/main.rs | 207 ++++++++++++++++++++----------------- sink/examples/seed_data.rs | 34 +++++- 2 files changed, 144 insertions(+), 97 deletions(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index f1302a3..b801e36 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -1,9 +1,9 @@ use clap::{Args, Parser}; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; -use futures::TryStreamExt; +use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter}, - mapping::{Query, QueryStream, RelationEdge, prop_filter}, + mapping::{Attributes, Query, QueryStream, RelationEdge, prop_filter}, neo4rs, relation, system_ids, }; use grc20_sdk::models::BaseEntity; @@ -132,7 +132,7 @@ impl KnowledgeGraph { entity::EntityFilter::default() .relations(TypesFilter::default().r#type(system_ids::SCHEMA_TYPE)), ) - .limit(8) + .limit(10) .send() .await .map_err(|e| { @@ -193,7 +193,7 @@ impl KnowledgeGraph { .to_id(system_ids::RELATION_SCHEMA_TYPE), ), ) - .limit(8) + .limit(10) .send() .await .map_err(|e| { @@ -251,7 +251,7 @@ impl KnowledgeGraph { entity::EntityFilter::default() .relations(TypesFilter::default().r#type(system_ids::ATTRIBUTE)), ) - .limit(8) + .limit(10) .send() .await .map_err(|e| { @@ -287,24 +287,16 @@ impl KnowledgeGraph { )) } - // #[tool(description = "Search Properties")] - // async fn get_entities( - // &self, - // #[tool(param)] - // #[schemars(description = "The query string to search for properties")] - // query: String, - // ) - - #[tool(description = "Get entity by ID")] - async fn get_entity( + #[tool(description = "Get entity by ID with it's attributes and relations")] + async fn get_entity_info( &self, #[tool(param)] #[schemars( - description = "Return an entity by its ID along with its attributes (name, description, etc.) and types" + description = "Return an entity by its ID along with its attributes (name, description, etc.), relations and types" )] id: String, ) -> Result { - let entity = entity::find_one::>(&self.neo4j, &id) + let entity = entity::find_one::>(&self.neo4j, &id) .send() .await .map_err(|e| { @@ -314,38 +306,12 @@ impl KnowledgeGraph { McpError::internal_error("entity_not_found", Some(json!({ "id": id }))) })?; - tracing::info!("Found entity with ID '{}'", id); - - Ok(CallToolResult::success(vec![ - Content::json(json!({ - "id": entity.id(), - "name": entity.attributes.name, - "description": entity.attributes.description, - "types": entity.types, - })) - .expect("Failed to create JSON content"), - ])) - } - - #[tool(description = "Search Relation outbound from entity")] - async fn get_outbound_relations( - &self, - #[tool(param)] - #[schemars(description = "The id of the Relation type to find")] - relation_type_id: String, - #[tool(param)] - #[schemars(description = "The id of the from in the relation")] - entity_id: String, - ) -> Result { - let relations = relation::find_many::>(&self.neo4j) + let mut out_relations = relation::find_many::>(&self.neo4j) .filter( relation::RelationFilter::default() - .relation_type( - EntityFilter::default().id(prop_filter::value(relation_type_id.clone())), - ) - .from_(EntityFilter::default().id(prop_filter::value(entity_id.clone()))), + .from_(EntityFilter::default().id(prop_filter::value(id.clone()))), ) - .limit(100) + .limit(10) .send() .await .map_err(|e| { @@ -363,43 +329,12 @@ impl KnowledgeGraph { ) })?; - tracing::info!("Found result from entity '{}'", entity_id); - - Ok(CallToolResult::success( - relations - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.id, - "relation_type": result.relation_type, - "from": result.from.id, - "to": result.to.id, - })) - .expect("Failed to create JSON content") - }) - .collect(), - )) - } - - #[tool(description = "Search Relation inbound from entity")] - async fn get_inbound_relations( - &self, - #[tool(param)] - #[schemars(description = "The id of the Relation type to find")] - relation_type_id: String, - #[tool(param)] - #[schemars(description = "The id of the to in the relation")] - entity_id: String, - ) -> Result { - let relations = relation::find_many::>(&self.neo4j) + let mut in_relations = relation::find_many::>(&self.neo4j) .filter( relation::RelationFilter::default() - .relation_type( - EntityFilter::default().id(prop_filter::value(relation_type_id.clone())), - ) - .to_(EntityFilter::default().id(prop_filter::value(entity_id.clone()))), + .to_(EntityFilter::default().id(prop_filter::value(id.clone()))), ) - .limit(100) + .limit(10) .send() .await .map_err(|e| { @@ -417,22 +352,46 @@ impl KnowledgeGraph { ) })?; - tracing::info!("Found result from entity '{}'", entity_id); + tracing::info!("Found entity with ID '{}'", id); - Ok(CallToolResult::success( - relations + out_relations.append(&mut in_relations); + + let relations_vec: Vec<_> = join_all(out_relations .into_iter() - .map(|result| { + .map(|result| async move { Content::json(json!({ - "id": result.id, - "relation_type": result.relation_type, - "from": result.from.id, - "to": result.to.id, + "relation_id": result.id, + "relation_type": self.get_name_of_id(result.relation_type).await.expect("No relation type"), + "from_id": result.from.id, + "from_name": self.get_name_of_id(result.from.id.clone()).await.expect(&result.from.id), + "to_id": result.to.id, + "to_name": self.get_name_of_id(result.to.id.clone()).await.expect(&result.to.id), })) .expect("Failed to create JSON content") - }) - .collect(), + })).await.to_vec(); + + let attributes_vec: Vec<_> = join_all(entity.attributes.0.clone().into_iter().map( + |(key, attr)| async { + Content::json(json!({ + "attribute_name": self.get_name_of_id(key).await.expect("No attribute name"), + "attribute_value": String::try_from(attr).expect("No attributes"), + })) + .expect("Failed to create JSON content") + }, )) + .await + .to_vec(); + + Ok(CallToolResult::success(vec![ + Content::json(json!({ + "id": entity.id(), + "name": entity.attributes.get::(system_ids::NAME_ATTRIBUTE).expect("Impossible to get Name"), + "description": entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).expect("Impossible to get Description"), + "types": entity.types, + "all_attributes": attributes_vec, + "all_relations": relations_vec, + })).expect("Failed to create JSON content"), + ])) } #[tool(description = "Search Relations between 2 entities")] @@ -452,7 +411,7 @@ impl KnowledgeGraph { .from_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))) .to_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))), ) - .limit(100) + .limit(10) .send() .await .map_err(|e| { @@ -477,7 +436,7 @@ impl KnowledgeGraph { .from_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))) .to_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))), ) - .limit(100) + .limit(10) .send() .await .map_err(|e| { @@ -518,6 +477,70 @@ impl KnowledgeGraph { .collect(), )) } + + #[tool(description = "Get Entity by Attribute")] + async fn get_entity_by_attribute( + &self, + #[tool(param)] + #[schemars(description = "The value of the attribute of an Entity")] + attribute_value: String, + ) -> Result { + let embedding = self + .embedding_model + .embed(vec![&attribute_value], None) + .expect("Failed to get embedding") + .pop() + .expect("Embedding is empty") + .into_iter() + .map(|v| v as f64) + .collect::>(); + + let entities = entity::search::>(&self.neo4j, embedding) + .filter(entity::EntityFilter::default()) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error("get_entity", Some(json!({ "error": e.to_string() }))) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + })?; + + tracing::info!("Found {} entities with given attributes", entities.len()); + + Ok(CallToolResult::success( + entities + .into_iter() + .map(|result| { + Content::json(json!({ + "id": result.entity.id(), + "name": result.entity.attributes.name, + "description": result.entity.attributes.description, + })) + .expect("Failed to create JSON content") + }) + .collect(), + )) + } + + async fn get_name_of_id(&self, id: String) -> Result { + let entity = entity::find_one::>(&self.neo4j, &id) + .send() + .await + .map_err(|e| { + McpError::internal_error("get_entity_name", Some(json!({ "error": e.to_string() }))) + })? + .ok_or_else(|| { + McpError::internal_error("entity_name_not_found", Some(json!({ "id": id }))) + })?; + Ok(entity.attributes.name.unwrap()) + } } #[tool(tool_box)] diff --git a/sink/examples/seed_data.rs b/sink/examples/seed_data.rs index 05dedd0..48b25f9 100644 --- a/sink/examples/seed_data.rs +++ b/sink/examples/seed_data.rs @@ -73,7 +73,7 @@ async fn main() -> anyhow::Result<()> { .expect("Failed to connect to Neo4j"); let embedding_model = TextEmbedding::try_new( - InitOptions::new(EMBEDDING_MODEL).with_show_download_progress(true), + InitOptions::new(EMBEDDING_MODEL).with_show_download_progress(true), )?; // Reset and bootstrap the database @@ -323,7 +323,10 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -pub async fn bootstrap(neo4j: &neo4rs::Graph, embedding_model: &TextEmbedding) -> anyhow::Result<()> { +pub async fn bootstrap( + neo4j: &neo4rs::Graph, + embedding_model: &TextEmbedding, +) -> anyhow::Result<()> { let triples = vec![ // Value types Triple::new(system_ids::CHECKBOX, system_ids::NAME_ATTRIBUTE, "Checkbox"), @@ -573,7 +576,14 @@ pub async fn create_entity( &entity_id, system_ids::NAME_ATTRIBUTE, name.clone(), - embedding_model.embed(vec!(name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), + embedding_model + .embed(vec![name], Some(1)) + .unwrap_or(vec![Vec::::new()]) + .get(0) + .unwrap_or(&Vec::::new()) + .iter() + .map(|&x| x as f64) + .collect(), )]) .send() .await?; @@ -649,7 +659,14 @@ pub async fn create_type( &type_id, system_ids::NAME_ATTRIBUTE, name.clone(), - embedding_model.embed(vec!(name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), + embedding_model + .embed(vec![name], Some(1)) + .unwrap_or(vec![Vec::::new()]) + .get(0) + .unwrap_or(&Vec::::new()) + .iter() + .map(|&x| x as f64) + .collect(), )]) .send() .await?; @@ -701,7 +718,14 @@ pub async fn create_property( &property_id, system_ids::NAME_ATTRIBUTE, string_name.clone(), - embedding_model.embed(vec!(string_name), Some(1)).unwrap_or(vec!(Vec::::new())).get(0).unwrap_or(&Vec::::new()).iter().map(|&x| x as f64).collect(), + embedding_model + .embed(vec![string_name], Some(1)) + .unwrap_or(vec![Vec::::new()]) + .get(0) + .unwrap_or(&Vec::::new()) + .iter() + .map(|&x| x as f64) + .collect(), )]) .send() .await?; From 1689abb45a75b6829be6997a0351d5d48d723e89 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Fri, 6 Jun 2025 15:31:54 -0400 Subject: [PATCH 05/25] feat add distant relations and fix search relationType --- .../src/mapping/entity/find_relation.rs | 245 ++++++++++++++++++ grc20-core/src/mapping/entity/mod.rs | 6 + mcp-server/src/main.rs | 100 ++----- 3 files changed, 279 insertions(+), 72 deletions(-) create mode 100644 grc20-core/src/mapping/entity/find_relation.rs diff --git a/grc20-core/src/mapping/entity/find_relation.rs b/grc20-core/src/mapping/entity/find_relation.rs new file mode 100644 index 0000000..9960ae9 --- /dev/null +++ b/grc20-core/src/mapping/entity/find_relation.rs @@ -0,0 +1,245 @@ +use std::collections::HashMap; + +use neo4rs::{BoltType, Relation}; +use serde::{Deserialize, Serialize}; + +use crate::{ + entity::EntityFilter, + error::DatabaseError, + mapping::{ + order_by::FieldOrderBy, + query_utils::{ + query_builder::{MatchQuery, QueryBuilder, Subquery}, + VersionFilter, + }, + AttributeFilter, FromAttributes, PropFilter, Query, + }, +}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConnectedRelationship { + pub identity: i64, + pub start: i64, + pub end: i64, + pub rel_type: String, + pub properties: HashMap, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Properties { + min_version: String, + max_version: Option, + #[serde(rename = "7pXCVQDV9C7ozrXkpVg8RJ")] + _updated_at: String, + index: String, + #[serde(rename = "82nP7aFmHJLbaPFszj2nbx")] + _created: String, + #[serde(rename = "5Ms1pYq8v8G1RXC3wWb9ix")] + _updated: String, + relation_type: String, + id: String, + #[serde(rename = "59HTYnd2e4gBx2aA98JfNx")] + _created_at_block: String, + space_id: String, +} + +pub struct FindRelationQuery { + neo4j: neo4rs::Graph, + id1: String, + id2: String, + filter: EntityFilter, + order_by: Option, + limit: usize, + skip: Option, + space_id: Option>, + version: VersionFilter, + _phantom: std::marker::PhantomData, +} + +impl FindRelationQuery { + pub(super) fn new(neo4j: &neo4rs::Graph, id1: String, id2: String) -> Self { + Self { + neo4j: neo4j.clone(), + id1, + id2, + filter: EntityFilter::default(), + order_by: None, + limit: 100, + skip: None, + space_id: None, + version: VersionFilter::default(), + _phantom: std::marker::PhantomData, + } + } + + pub fn id(mut self, id: PropFilter) -> Self { + self.filter.id = Some(id); + self + } + + pub fn attribute(mut self, attribute: AttributeFilter) -> Self { + self.filter.attributes.push(attribute); + self + } + + pub fn attribute_mut(&mut self, attribute: AttributeFilter) { + self.filter.attributes.push(attribute); + } + + pub fn attributes(mut self, attributes: impl IntoIterator) -> Self { + self.filter.attributes.extend(attributes); + self + } + + pub fn attributes_mut(&mut self, attributes: impl IntoIterator) { + self.filter.attributes.extend(attributes); + } + + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + pub fn skip(mut self, skip: usize) -> Self { + self.skip = Some(skip); + self + } + + /// Overwrite the current filter with a new one + pub fn with_filter(mut self, filter: EntityFilter) -> Self { + self.filter = filter; + self + } + + pub fn order_by(mut self, order_by: FieldOrderBy) -> Self { + self.order_by = Some(order_by); + self + } + + pub fn order_by_mut(&mut self, order_by: FieldOrderBy) { + self.order_by = Some(order_by); + } + + pub fn space_id(mut self, space_id: impl Into>) -> Self { + self.space_id = Some(space_id.into()); + self + } + + pub fn version(mut self, space_version: String) -> Self { + self.version.version_mut(space_version); + self + } + + pub fn version_opt(mut self, space_version: Option) -> Self { + self.version.version_opt(space_version); + self + } + + fn subquery(&self) -> QueryBuilder { + QueryBuilder::default() + .subquery(MatchQuery::new( + "e = (e1:Entity {id: $id1}) -[:RELATION*1..3]-(e2:Entity {id: $id2})", + )) + .limit(self.limit) + .params("id1", self.id1.clone()) + .params("id2", self.id2.clone()) + } +} + +impl Query>> for FindRelationQuery { + async fn send(self) -> Result>, DatabaseError> { + let query = self.subquery().r#return("relationships(e)"); + + if cfg!(debug_assertions) || cfg!(test) { + println!( + "entity::FindRelationQuery:::\n{}\nparams:{:?}", + query.compile(), + [self.id1, self.id2] + ); + }; + + let mut result = self.neo4j.execute(query.build()).await?; + let mut all_relationship_paths = Vec::new(); + + // Process each row + while let Some(row) = result.next().await? { + // Get the relationships collection from the row - note the key name + let relationships: Vec = row.get("relationships(e)")?; + + let mut path_relationships = Vec::new(); + + // Convert each Neo4j Relation to our custom struct + for rel in relationships { + let relationship = ConnectedRelationship { + identity: rel.id(), + start: rel.start_node_id(), + end: rel.end_node_id(), + rel_type: rel.typ().to_string(), + properties: convert_properties(&rel), + }; + path_relationships.push(relationship); + } + + all_relationship_paths.push(path_relationships); + } + + Ok(all_relationship_paths) + /*Row { + attributes: BoltMap { + value: {BoltString { value: "relationships(e)" }: List(BoltList { value: [ + Relation(BoltRelation { id: BoltInteger { value: 74 }, start_node_id: BoltInteger { value: 4 }, end_node_id: BoltInteger { value: 1836 }, typ: BoltString { value: "RELATION" }, properties: BoltMap { + value: { + BoltString { value: "index" }: String(BoltString { value: "0" }), + BoltString { value: "relation_type" }: String(BoltString { value: "Jfmby78N4BCseZinBmdVov" }), + BoltString { value: "id" }: String(BoltString { value: "AqysdC1YZt2iBkCL2qgyg5" }), + BoltString { value: "7pXCVQDV9C7ozrXkpVg8RJ" }: String(BoltString { value: "0" }), + BoltString { value: "82nP7aFmHJLbaPFszj2nbx" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), + BoltString { value: "min_version" }: String(BoltString { value: "0" }), BoltString { value: "5Ms1pYq8v8G1RXC3wWb9ix" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), + BoltString { value: "59HTYnd2e4gBx2aA98JfNx" }: String(BoltString { value: "0" }), BoltString { value: "space_id" }: String(BoltString { value: "25omwWh6HYgeRQKCaSpVpa" }) + } } }), + Relation(BoltRelation { id: BoltInteger { value: 77 }, start_node_id: BoltInteger { value: 7 }, end_node_id: BoltInteger { value: 1836 }, typ: BoltString { value: "RELATION" }, properties: BoltMap { value: {BoltString { value: "id" }: String(BoltString { value: "QmqmtmDj4jAY5v3dArk2av" }), BoltString { value: "82nP7aFmHJLbaPFszj2nbx" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), BoltString { value: "7pXCVQDV9C7ozrXkpVg8RJ" }: String(BoltString { value: "0" }), BoltString { value: "59HTYnd2e4gBx2aA98JfNx" }: String(BoltString { value: "0" }), BoltString { value: "5Ms1pYq8v8G1RXC3wWb9ix" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), BoltString { value: "min_version" }: String(BoltString { value: "0" }), BoltString { value: "index" }: String(BoltString { value: "0" }), BoltString { value: "relation_type" }: String(BoltString { value: "Jfmby78N4BCseZinBmdVov" }), BoltString { value: "space_id" }: String(BoltString { value: "25omwWh6HYgeRQKCaSpVpa" })} } }) + ] })} } }*/ + } +} + +fn convert_properties(rel: &Relation) -> HashMap { + let mut properties = HashMap::new(); + + for key in rel.keys() { + if let Ok(value) = rel.get::(key) { + match value { + BoltType::String(s) => { + properties.insert(key.to_string(), serde_json::Value::String(s.value)); + } + BoltType::Integer(i) => { + properties.insert( + key.to_string(), + serde_json::Value::Number(serde_json::Number::from(i.value)), + ); + } + BoltType::Float(f) => { + if let Some(num) = serde_json::Number::from_f64(f.value) { + properties.insert(key.to_string(), serde_json::Value::Number(num)); + } + } + BoltType::Boolean(b) => { + properties.insert(key.to_string(), serde_json::Value::Bool(b.value)); + } + BoltType::DateTime(_) => { + properties.insert( + key.to_string(), + serde_json::Value::String("1970-01-01T00:00:00Z".to_string()), + ); + } + BoltType::List(_list) => { + properties.insert(key.to_string(), serde_json::Value::Array(vec![])); + } + _ => { + properties.insert(key.to_string(), serde_json::Value::Null); + } + } + } + } + + properties +} diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 5f8192b..0ae607b 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -2,6 +2,7 @@ pub mod delete_many; pub mod delete_one; pub mod find_many; pub mod find_one; +pub mod find_relation; pub mod insert_many; pub mod insert_one; pub mod models; @@ -11,6 +12,7 @@ pub mod utils; pub use delete_one::DeleteOneQuery; pub use find_many::FindManyQuery; pub use find_one::FindOneQuery; +pub use find_relation::FindRelationQuery; pub use insert_one::InsertOneQuery; pub use models::{Entity, EntityNode, EntityNodeRef, SystemProperties}; pub use semantic_search::SemanticSearchQuery; @@ -128,6 +130,10 @@ pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery SemanticSearchQuery::new(neo4j, vector) } +pub fn find_relation(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindRelationQuery { + FindRelationQuery::new(neo4j, id1, id2) +} + pub fn insert_one( neo4j: &neo4rs::Graph, block: &BlockMetadata, diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index b801e36..c455e6d 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -186,13 +186,9 @@ impl KnowledgeGraph { .collect::>(); let results = entity::search::>(&self.neo4j, embedding) - .filter( - entity::EntityFilter::default().relations( - EntityRelationFilter::default() - .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) - .to_id(system_ids::RELATION_SCHEMA_TYPE), - ), - ) + .filter(entity::EntityFilter::default().relations( + EntityRelationFilter::default().relation_type(system_ids::RELATION_SCHEMA_TYPE), + )) .limit(10) .send() .await @@ -385,8 +381,8 @@ impl KnowledgeGraph { Ok(CallToolResult::success(vec![ Content::json(json!({ "id": entity.id(), - "name": entity.attributes.get::(system_ids::NAME_ATTRIBUTE).expect("Impossible to get Name"), - "description": entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).expect("Impossible to get Description"), + "name": entity.attributes.get::(system_ids::NAME_ATTRIBUTE).unwrap_or("No name".to_string()), + "description": entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).unwrap_or("No description".to_string()), "types": entity.types, "all_attributes": attributes_vec, "all_relations": relations_vec, @@ -394,7 +390,7 @@ impl KnowledgeGraph { ])) } - #[tool(description = "Search Relations between 2 entities")] + #[tool(description = "Search for distant or close Relations between 2 entities")] async fn get_relations_between_entities( &self, #[tool(param)] @@ -404,73 +400,33 @@ impl KnowledgeGraph { #[schemars(description = "The id of the second Entity to find relations")] entity2_id: String, ) -> Result { - let mut relations_first_direction = - relation::find_many::>(&self.neo4j) - .filter( - relation::RelationFilter::default() - .from_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))) - .to_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id", - Some(json!({ "error": e.to_string() })), - ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id_not_found", - Some(json!({ "error": e.to_string() })), - ) - })?; - - let mut relations_second_direction = - relation::find_many::>(&self.neo4j) - .filter( - relation::RelationFilter::default() - .from_(EntityFilter::default().id(prop_filter::value(entity2_id.clone()))) - .to_(EntityFilter::default().id(prop_filter::value(entity1_id.clone()))), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id", - Some(json!({ "error": e.to_string() })), - ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id_not_found", - Some(json!({ "error": e.to_string() })), - ) - })?; - - tracing::info!( - "Found {} relations from the first to the second and {} from the second to the first", - relations_first_direction.len(), - relations_second_direction.len() - ); - - relations_first_direction.append(&mut relations_second_direction); + let relations = entity::find_relation::( + &self.neo4j, + entity1_id.clone(), + entity2_id.clone(), + ) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_ids", + Some(json!({ "error": e.to_string() })), + ) + })? + .into_iter() + .collect::>>(); + + tracing::info!("Found {} relations", relations.len()); Ok(CallToolResult::success( - relations_first_direction + relations .into_iter() .map(|result| { Content::json(json!({ - "id": result.id, - "relation_type": result.relation_type, - "from": result.from.id, - "to": result.to.id, + "relations": result.into_iter().map(|rel| + rel.properties + ).collect::>(), })) .expect("Failed to create JSON content") }) From 30edac2bea436dd495757f1a03b8a69b8cee0ef4 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Fri, 6 Jun 2025 18:03:57 -0400 Subject: [PATCH 06/25] feat relationships can now be indirect --- grc20-core/src/mapping/entity/find_path.rs | 162 ++++++++++++ .../src/mapping/entity/find_relation.rs | 245 ------------------ grc20-core/src/mapping/entity/mod.rs | 9 +- mcp-server/src/main.rs | 53 ++-- 4 files changed, 192 insertions(+), 277 deletions(-) create mode 100644 grc20-core/src/mapping/entity/find_path.rs delete mode 100644 grc20-core/src/mapping/entity/find_relation.rs diff --git a/grc20-core/src/mapping/entity/find_path.rs b/grc20-core/src/mapping/entity/find_path.rs new file mode 100644 index 0000000..a63b2be --- /dev/null +++ b/grc20-core/src/mapping/entity/find_path.rs @@ -0,0 +1,162 @@ +use neo4rs::Path; + +use crate::{ + entity::EntityFilter, + error::DatabaseError, + mapping::{ + order_by::FieldOrderBy, + query_utils::{ + query_builder::{MatchQuery, QueryBuilder, Subquery}, + VersionFilter, + }, + AttributeFilter, FromAttributes, PropFilter, Query, + }, +}; + +pub struct Relation { + pub nodes_ids: Vec, + pub relations_ids: Vec, +} + +pub struct FindPathQuery { + neo4j: neo4rs::Graph, + id1: String, + id2: String, + filter: EntityFilter, + order_by: Option, + limit: usize, + skip: Option, + space_id: Option>, + version: VersionFilter, + _phantom: std::marker::PhantomData, +} + +impl FindPathQuery { + pub(super) fn new(neo4j: &neo4rs::Graph, id1: String, id2: String) -> Self { + Self { + neo4j: neo4j.clone(), + id1, + id2, + filter: EntityFilter::default(), + order_by: None, + limit: 100, + skip: None, + space_id: None, + version: VersionFilter::default(), + _phantom: std::marker::PhantomData, + } + } + + pub fn id(mut self, id: PropFilter) -> Self { + self.filter.id = Some(id); + self + } + + pub fn attribute(mut self, attribute: AttributeFilter) -> Self { + self.filter.attributes.push(attribute); + self + } + + pub fn attribute_mut(&mut self, attribute: AttributeFilter) { + self.filter.attributes.push(attribute); + } + + pub fn attributes(mut self, attributes: impl IntoIterator) -> Self { + self.filter.attributes.extend(attributes); + self + } + + pub fn attributes_mut(&mut self, attributes: impl IntoIterator) { + self.filter.attributes.extend(attributes); + } + + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + pub fn skip(mut self, skip: usize) -> Self { + self.skip = Some(skip); + self + } + + /// Overwrite the current filter with a new one + pub fn with_filter(mut self, filter: EntityFilter) -> Self { + self.filter = filter; + self + } + + pub fn order_by(mut self, order_by: FieldOrderBy) -> Self { + self.order_by = Some(order_by); + self + } + + pub fn order_by_mut(&mut self, order_by: FieldOrderBy) { + self.order_by = Some(order_by); + } + + pub fn space_id(mut self, space_id: impl Into>) -> Self { + self.space_id = Some(space_id.into()); + self + } + + pub fn version(mut self, space_version: String) -> Self { + self.version.version_mut(space_version); + self + } + + pub fn version_opt(mut self, space_version: Option) -> Self { + self.version.version_opt(space_version); + self + } + + fn subquery(&self) -> QueryBuilder { + QueryBuilder::default() + .subquery(MatchQuery::new( + "p = allShortestPaths((e1:Entity {id: $id1}) -[:RELATION*1..5]-(e2:Entity {id: $id2}))", + )) + .limit(self.limit) + .params("id1", self.id1.clone()) + .params("id2", self.id2.clone()) + } +} + +impl Query> for FindPathQuery { + async fn send(self) -> Result, DatabaseError> { + let query = self.subquery().r#return("p"); + + if cfg!(debug_assertions) || cfg!(test) { + println!( + "entity::FindPathQuery:::\n{}\nparams:{:?}", + query.compile(), + [self.id1, self.id2] + ); + } + + let mut result = self.neo4j.execute(query.build()).await?; + let mut all_relationship_data = Vec::new(); + + // Process each row + while let Some(row) = result.next().await? { + let path: Path = row.get("p")?; + tracing::info!("This is the info for Path: {:?}", path); + + let relationship_data: Relation = Relation { + nodes_ids: (path + .nodes() + .iter() + .filter_map(|rel| rel.get("id").ok()) + .collect()), + relations_ids: (path + .rels() + .iter() + .filter_map(|rel| rel.get("relation_type").ok()) + .collect()), + }; + + all_relationship_data.push(relationship_data); + } + + Ok(all_relationship_data) + } +} diff --git a/grc20-core/src/mapping/entity/find_relation.rs b/grc20-core/src/mapping/entity/find_relation.rs deleted file mode 100644 index 9960ae9..0000000 --- a/grc20-core/src/mapping/entity/find_relation.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::collections::HashMap; - -use neo4rs::{BoltType, Relation}; -use serde::{Deserialize, Serialize}; - -use crate::{ - entity::EntityFilter, - error::DatabaseError, - mapping::{ - order_by::FieldOrderBy, - query_utils::{ - query_builder::{MatchQuery, QueryBuilder, Subquery}, - VersionFilter, - }, - AttributeFilter, FromAttributes, PropFilter, Query, - }, -}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct ConnectedRelationship { - pub identity: i64, - pub start: i64, - pub end: i64, - pub rel_type: String, - pub properties: HashMap, -} - -#[derive(Debug, Deserialize, Serialize)] -struct Properties { - min_version: String, - max_version: Option, - #[serde(rename = "7pXCVQDV9C7ozrXkpVg8RJ")] - _updated_at: String, - index: String, - #[serde(rename = "82nP7aFmHJLbaPFszj2nbx")] - _created: String, - #[serde(rename = "5Ms1pYq8v8G1RXC3wWb9ix")] - _updated: String, - relation_type: String, - id: String, - #[serde(rename = "59HTYnd2e4gBx2aA98JfNx")] - _created_at_block: String, - space_id: String, -} - -pub struct FindRelationQuery { - neo4j: neo4rs::Graph, - id1: String, - id2: String, - filter: EntityFilter, - order_by: Option, - limit: usize, - skip: Option, - space_id: Option>, - version: VersionFilter, - _phantom: std::marker::PhantomData, -} - -impl FindRelationQuery { - pub(super) fn new(neo4j: &neo4rs::Graph, id1: String, id2: String) -> Self { - Self { - neo4j: neo4j.clone(), - id1, - id2, - filter: EntityFilter::default(), - order_by: None, - limit: 100, - skip: None, - space_id: None, - version: VersionFilter::default(), - _phantom: std::marker::PhantomData, - } - } - - pub fn id(mut self, id: PropFilter) -> Self { - self.filter.id = Some(id); - self - } - - pub fn attribute(mut self, attribute: AttributeFilter) -> Self { - self.filter.attributes.push(attribute); - self - } - - pub fn attribute_mut(&mut self, attribute: AttributeFilter) { - self.filter.attributes.push(attribute); - } - - pub fn attributes(mut self, attributes: impl IntoIterator) -> Self { - self.filter.attributes.extend(attributes); - self - } - - pub fn attributes_mut(&mut self, attributes: impl IntoIterator) { - self.filter.attributes.extend(attributes); - } - - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - pub fn skip(mut self, skip: usize) -> Self { - self.skip = Some(skip); - self - } - - /// Overwrite the current filter with a new one - pub fn with_filter(mut self, filter: EntityFilter) -> Self { - self.filter = filter; - self - } - - pub fn order_by(mut self, order_by: FieldOrderBy) -> Self { - self.order_by = Some(order_by); - self - } - - pub fn order_by_mut(&mut self, order_by: FieldOrderBy) { - self.order_by = Some(order_by); - } - - pub fn space_id(mut self, space_id: impl Into>) -> Self { - self.space_id = Some(space_id.into()); - self - } - - pub fn version(mut self, space_version: String) -> Self { - self.version.version_mut(space_version); - self - } - - pub fn version_opt(mut self, space_version: Option) -> Self { - self.version.version_opt(space_version); - self - } - - fn subquery(&self) -> QueryBuilder { - QueryBuilder::default() - .subquery(MatchQuery::new( - "e = (e1:Entity {id: $id1}) -[:RELATION*1..3]-(e2:Entity {id: $id2})", - )) - .limit(self.limit) - .params("id1", self.id1.clone()) - .params("id2", self.id2.clone()) - } -} - -impl Query>> for FindRelationQuery { - async fn send(self) -> Result>, DatabaseError> { - let query = self.subquery().r#return("relationships(e)"); - - if cfg!(debug_assertions) || cfg!(test) { - println!( - "entity::FindRelationQuery:::\n{}\nparams:{:?}", - query.compile(), - [self.id1, self.id2] - ); - }; - - let mut result = self.neo4j.execute(query.build()).await?; - let mut all_relationship_paths = Vec::new(); - - // Process each row - while let Some(row) = result.next().await? { - // Get the relationships collection from the row - note the key name - let relationships: Vec = row.get("relationships(e)")?; - - let mut path_relationships = Vec::new(); - - // Convert each Neo4j Relation to our custom struct - for rel in relationships { - let relationship = ConnectedRelationship { - identity: rel.id(), - start: rel.start_node_id(), - end: rel.end_node_id(), - rel_type: rel.typ().to_string(), - properties: convert_properties(&rel), - }; - path_relationships.push(relationship); - } - - all_relationship_paths.push(path_relationships); - } - - Ok(all_relationship_paths) - /*Row { - attributes: BoltMap { - value: {BoltString { value: "relationships(e)" }: List(BoltList { value: [ - Relation(BoltRelation { id: BoltInteger { value: 74 }, start_node_id: BoltInteger { value: 4 }, end_node_id: BoltInteger { value: 1836 }, typ: BoltString { value: "RELATION" }, properties: BoltMap { - value: { - BoltString { value: "index" }: String(BoltString { value: "0" }), - BoltString { value: "relation_type" }: String(BoltString { value: "Jfmby78N4BCseZinBmdVov" }), - BoltString { value: "id" }: String(BoltString { value: "AqysdC1YZt2iBkCL2qgyg5" }), - BoltString { value: "7pXCVQDV9C7ozrXkpVg8RJ" }: String(BoltString { value: "0" }), - BoltString { value: "82nP7aFmHJLbaPFszj2nbx" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), - BoltString { value: "min_version" }: String(BoltString { value: "0" }), BoltString { value: "5Ms1pYq8v8G1RXC3wWb9ix" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), - BoltString { value: "59HTYnd2e4gBx2aA98JfNx" }: String(BoltString { value: "0" }), BoltString { value: "space_id" }: String(BoltString { value: "25omwWh6HYgeRQKCaSpVpa" }) - } } }), - Relation(BoltRelation { id: BoltInteger { value: 77 }, start_node_id: BoltInteger { value: 7 }, end_node_id: BoltInteger { value: 1836 }, typ: BoltString { value: "RELATION" }, properties: BoltMap { value: {BoltString { value: "id" }: String(BoltString { value: "QmqmtmDj4jAY5v3dArk2av" }), BoltString { value: "82nP7aFmHJLbaPFszj2nbx" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), BoltString { value: "7pXCVQDV9C7ozrXkpVg8RJ" }: String(BoltString { value: "0" }), BoltString { value: "59HTYnd2e4gBx2aA98JfNx" }: String(BoltString { value: "0" }), BoltString { value: "5Ms1pYq8v8G1RXC3wWb9ix" }: DateTime(BoltDateTime { seconds: BoltInteger { value: 0 }, nanoseconds: BoltInteger { value: 0 }, tz_offset_seconds: BoltInteger { value: 0 } }), BoltString { value: "min_version" }: String(BoltString { value: "0" }), BoltString { value: "index" }: String(BoltString { value: "0" }), BoltString { value: "relation_type" }: String(BoltString { value: "Jfmby78N4BCseZinBmdVov" }), BoltString { value: "space_id" }: String(BoltString { value: "25omwWh6HYgeRQKCaSpVpa" })} } }) - ] })} } }*/ - } -} - -fn convert_properties(rel: &Relation) -> HashMap { - let mut properties = HashMap::new(); - - for key in rel.keys() { - if let Ok(value) = rel.get::(key) { - match value { - BoltType::String(s) => { - properties.insert(key.to_string(), serde_json::Value::String(s.value)); - } - BoltType::Integer(i) => { - properties.insert( - key.to_string(), - serde_json::Value::Number(serde_json::Number::from(i.value)), - ); - } - BoltType::Float(f) => { - if let Some(num) = serde_json::Number::from_f64(f.value) { - properties.insert(key.to_string(), serde_json::Value::Number(num)); - } - } - BoltType::Boolean(b) => { - properties.insert(key.to_string(), serde_json::Value::Bool(b.value)); - } - BoltType::DateTime(_) => { - properties.insert( - key.to_string(), - serde_json::Value::String("1970-01-01T00:00:00Z".to_string()), - ); - } - BoltType::List(_list) => { - properties.insert(key.to_string(), serde_json::Value::Array(vec![])); - } - _ => { - properties.insert(key.to_string(), serde_json::Value::Null); - } - } - } - } - - properties -} diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 0ae607b..5b16c4f 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -2,7 +2,7 @@ pub mod delete_many; pub mod delete_one; pub mod find_many; pub mod find_one; -pub mod find_relation; +pub mod find_path; pub mod insert_many; pub mod insert_one; pub mod models; @@ -12,7 +12,7 @@ pub mod utils; pub use delete_one::DeleteOneQuery; pub use find_many::FindManyQuery; pub use find_one::FindOneQuery; -pub use find_relation::FindRelationQuery; +pub use find_path::FindPathQuery; pub use insert_one::InsertOneQuery; pub use models::{Entity, EntityNode, EntityNodeRef, SystemProperties}; pub use semantic_search::SemanticSearchQuery; @@ -130,8 +130,9 @@ pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery SemanticSearchQuery::new(neo4j, vector) } -pub fn find_relation(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindRelationQuery { - FindRelationQuery::new(neo4j, id1, id2) +// TODO: add docs for use via GraphQL +pub fn find_path(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindPathQuery { + FindPathQuery::new(neo4j, id1, id2) } pub fn insert_one( diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index c455e6d..4c75270 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -357,11 +357,11 @@ impl KnowledgeGraph { .map(|result| async move { Content::json(json!({ "relation_id": result.id, - "relation_type": self.get_name_of_id(result.relation_type).await.expect("No relation type"), + "relation_type": self.get_name_of_id(result.relation_type).await.unwrap_or("No relation type".to_string()), "from_id": result.from.id, - "from_name": self.get_name_of_id(result.from.id.clone()).await.expect(&result.from.id), + "from_name": self.get_name_of_id(result.from.id).await.unwrap_or("No name".to_string()), "to_id": result.to.id, - "to_name": self.get_name_of_id(result.to.id.clone()).await.expect(&result.to.id), + "to_name": self.get_name_of_id(result.to.id).await.unwrap_or("No name".to_string()), })) .expect("Failed to create JSON content") })).await.to_vec(); @@ -369,8 +369,8 @@ impl KnowledgeGraph { let attributes_vec: Vec<_> = join_all(entity.attributes.0.clone().into_iter().map( |(key, attr)| async { Content::json(json!({ - "attribute_name": self.get_name_of_id(key).await.expect("No attribute name"), - "attribute_value": String::try_from(attr).expect("No attributes"), + "attribute_name": self.get_name_of_id(key).await.unwrap_or("No attribute name".to_string()), + "attribute_value": String::try_from(attr).unwrap_or("No attributes".to_string()), })) .expect("Failed to create JSON content") }, @@ -400,37 +400,34 @@ impl KnowledgeGraph { #[schemars(description = "The id of the second Entity to find relations")] entity2_id: String, ) -> Result { - let relations = entity::find_relation::( - &self.neo4j, - entity1_id.clone(), - entity2_id.clone(), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_ids", - Some(json!({ "error": e.to_string() })), - ) - })? - .into_iter() - .collect::>>(); + let relations = + entity::find_path::(&self.neo4j, entity1_id.clone(), entity2_id.clone()) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_ids", + Some(json!({ "error": e.to_string() })), + ) + })? + .into_iter() + .collect::>(); tracing::info!("Found {} relations", relations.len()); Ok(CallToolResult::success( - relations + join_all(relations .into_iter() - .map(|result| { + .map(|result| async { Content::json(json!({ - "relations": result.into_iter().map(|rel| - rel.properties - ).collect::>(), + "nodes": join_all(result.nodes_ids.into_iter().map(|node_id| async {self.get_name_of_id(node_id).await.unwrap_or("No attribute name".to_string())})).await.to_vec(), + "relations": join_all(result.relations_ids.into_iter().map(|node_id| async {self.get_name_of_id(node_id).await.unwrap_or("No attribute name".to_string())})).await.to_vec(), })) .expect("Failed to create JSON content") - }) - .collect(), + })) + .await + .to_vec(), )) } From 580cfe9a8a52c7e63f6741e5d987b18d69813a8e Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Mon, 9 Jun 2025 11:34:36 -0400 Subject: [PATCH 07/25] feat restrict to paths with no primitives --- grc20-core/src/mapping/entity/find_path.rs | 17 ++++----- grc20-core/src/mapping/entity/mod.rs | 2 +- mcp-server/src/main.rs | 42 +++++++++++----------- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/grc20-core/src/mapping/entity/find_path.rs b/grc20-core/src/mapping/entity/find_path.rs index a63b2be..9339afa 100644 --- a/grc20-core/src/mapping/entity/find_path.rs +++ b/grc20-core/src/mapping/entity/find_path.rs @@ -1,7 +1,7 @@ use neo4rs::Path; use crate::{ - entity::EntityFilter, + entity::{EntityFilter, EntityNode}, error::DatabaseError, mapping::{ order_by::FieldOrderBy, @@ -9,8 +9,9 @@ use crate::{ query_builder::{MatchQuery, QueryBuilder, Subquery}, VersionFilter, }, - AttributeFilter, FromAttributes, PropFilter, Query, + AttributeFilter, PropFilter, Query, }, + system_ids::SCHEMA_TYPE, }; pub struct Relation { @@ -18,7 +19,7 @@ pub struct Relation { pub relations_ids: Vec, } -pub struct FindPathQuery { +pub struct FindPathQuery { neo4j: neo4rs::Graph, id1: String, id2: String, @@ -28,10 +29,10 @@ pub struct FindPathQuery { skip: Option, space_id: Option>, version: VersionFilter, - _phantom: std::marker::PhantomData, + _phantom: std::marker::PhantomData, } -impl FindPathQuery { +impl FindPathQuery { pub(super) fn new(neo4j: &neo4rs::Graph, id1: String, id2: String) -> Self { Self { neo4j: neo4j.clone(), @@ -113,15 +114,15 @@ impl FindPathQuery { fn subquery(&self) -> QueryBuilder { QueryBuilder::default() .subquery(MatchQuery::new( - "p = allShortestPaths((e1:Entity {id: $id1}) -[:RELATION*1..5]-(e2:Entity {id: $id2}))", - )) + "p = allShortestPaths((e1:Entity {id: $id1}) -[:RELATION*1..10]-(e2:Entity {id: $id2}))", + ).r#where(format!("NONE(n IN nodes(p) WHERE EXISTS((n)-[:RELATION]-(:Entity {{id: \"{SCHEMA_TYPE}\"}})))")))//makes sure to not use primitive types .limit(self.limit) .params("id1", self.id1.clone()) .params("id2", self.id2.clone()) } } -impl Query> for FindPathQuery { +impl Query> for FindPathQuery { async fn send(self) -> Result, DatabaseError> { let query = self.subquery().r#return("p"); diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 5b16c4f..3e67560 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -131,7 +131,7 @@ pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery } // TODO: add docs for use via GraphQL -pub fn find_path(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindPathQuery { +pub fn find_path(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindPathQuery { FindPathQuery::new(neo4j, id1, id2) } diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 4c75270..5e4bcd3 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -302,7 +302,7 @@ impl KnowledgeGraph { McpError::internal_error("entity_not_found", Some(json!({ "id": id }))) })?; - let mut out_relations = relation::find_many::>(&self.neo4j) + let out_relations = relation::find_many::>(&self.neo4j) .filter( relation::RelationFilter::default() .from_(EntityFilter::default().id(prop_filter::value(id.clone()))), @@ -325,7 +325,7 @@ impl KnowledgeGraph { ) })?; - let mut in_relations = relation::find_many::>(&self.neo4j) + let in_relations = relation::find_many::>(&self.neo4j) .filter( relation::RelationFilter::default() .to_(EntityFilter::default().id(prop_filter::value(id.clone()))), @@ -350,9 +350,8 @@ impl KnowledgeGraph { tracing::info!("Found entity with ID '{}'", id); - out_relations.append(&mut in_relations); - - let relations_vec: Vec<_> = join_all(out_relations + let clean_up_relations = |relations: Vec>| async { + join_all(relations .into_iter() .map(|result| async move { Content::json(json!({ @@ -364,7 +363,10 @@ impl KnowledgeGraph { "to_name": self.get_name_of_id(result.to.id).await.unwrap_or("No name".to_string()), })) .expect("Failed to create JSON content") - })).await.to_vec(); + })).await.to_vec() + }; + let inbound_relations = clean_up_relations(in_relations).await; + let outbound_relations = clean_up_relations(out_relations).await; let attributes_vec: Vec<_> = join_all(entity.attributes.0.clone().into_iter().map( |(key, attr)| async { @@ -385,7 +387,8 @@ impl KnowledgeGraph { "description": entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).unwrap_or("No description".to_string()), "types": entity.types, "all_attributes": attributes_vec, - "all_relations": relations_vec, + "inbound_relations": inbound_relations, + "outbound_relations": outbound_relations, })).expect("Failed to create JSON content"), ])) } @@ -400,19 +403,18 @@ impl KnowledgeGraph { #[schemars(description = "The id of the second Entity to find relations")] entity2_id: String, ) -> Result { - let relations = - entity::find_path::(&self.neo4j, entity1_id.clone(), entity2_id.clone()) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_ids", - Some(json!({ "error": e.to_string() })), - ) - })? - .into_iter() - .collect::>(); + let relations = entity::find_path(&self.neo4j, entity1_id.clone(), entity2_id.clone()) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_ids", + Some(json!({ "error": e.to_string() })), + ) + })? + .into_iter() + .collect::>(); tracing::info!("Found {} relations", relations.len()); From aaf9fe28f07854bc179b93874ac167d1d88cf54a Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Tue, 10 Jun 2025 10:24:41 -0400 Subject: [PATCH 08/25] fix remove phantom data --- grc20-core/src/mapping/entity/find_path.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/grc20-core/src/mapping/entity/find_path.rs b/grc20-core/src/mapping/entity/find_path.rs index 9339afa..6531906 100644 --- a/grc20-core/src/mapping/entity/find_path.rs +++ b/grc20-core/src/mapping/entity/find_path.rs @@ -1,7 +1,7 @@ use neo4rs::Path; use crate::{ - entity::{EntityFilter, EntityNode}, + entity::{EntityFilter}, error::DatabaseError, mapping::{ order_by::FieldOrderBy, @@ -29,7 +29,6 @@ pub struct FindPathQuery { skip: Option, space_id: Option>, version: VersionFilter, - _phantom: std::marker::PhantomData, } impl FindPathQuery { @@ -44,7 +43,6 @@ impl FindPathQuery { skip: None, space_id: None, version: VersionFilter::default(), - _phantom: std::marker::PhantomData, } } From e161048fac3e189a864aafbf252c2de69c63cbf7 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Tue, 10 Jun 2025 10:25:44 -0400 Subject: [PATCH 09/25] fix format --- grc20-core/src/mapping/entity/find_path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grc20-core/src/mapping/entity/find_path.rs b/grc20-core/src/mapping/entity/find_path.rs index 6531906..90e9cdd 100644 --- a/grc20-core/src/mapping/entity/find_path.rs +++ b/grc20-core/src/mapping/entity/find_path.rs @@ -1,7 +1,7 @@ use neo4rs::Path; use crate::{ - entity::{EntityFilter}, + entity::EntityFilter, error::DatabaseError, mapping::{ order_by::FieldOrderBy, From 4262216f62a8dfe78196ce7ce4a273e94ad187f0 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Tue, 10 Jun 2025 13:36:23 -0400 Subject: [PATCH 10/25] fix update descriptions for mcp tools --- .../resources/get_entity_info_description.md | 98 +++++++++++++++++++ .../resources/get_properties_description.md | 0 ..._relations_between_entities_description.md | 34 +++++++ mcp-server/resources/instructions.md | 78 +++++++++++++-- .../resources/search_entity_description.md | 50 ++++++++++ .../search_properties_description.md | 51 ++++++++++ .../resources/search_type_description.md | 60 ++++++++++++ 7 files changed, 365 insertions(+), 6 deletions(-) create mode 100644 mcp-server/resources/get_entity_info_description.md delete mode 100644 mcp-server/resources/get_properties_description.md create mode 100644 mcp-server/resources/get_relations_between_entities_description.md create mode 100644 mcp-server/resources/search_properties_description.md diff --git a/mcp-server/resources/get_entity_info_description.md b/mcp-server/resources/get_entity_info_description.md new file mode 100644 index 0000000..beb5416 --- /dev/null +++ b/mcp-server/resources/get_entity_info_description.md @@ -0,0 +1,98 @@ +This request allows you to get the detailled information about an Entity with it's ID. You will get the name, description, other attributes, inbound relations and outbound relations of the Entity. + +The id for San Francisco is: 3qayfdjYyPv1dAYf8gPL5r + +ToolCall> get_entity_info("3qayfdjYyPv1dAYf8gPL5r") +ToolResult> +``` +{ + "all_attributes": [ + { + "text": "{\"attribute_name\":\"Description\",\"attribute_value\":\"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.\"}", + "type": "text" + }, + { + "text": "{\"attribute_name\":\"Name\",\"attribute_value\":\"San Francisco\"}", + "type": "text" + } + ], + "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "id": "3qayfdjYyPv1dAYf8gPL5r", + "inbound_relations": [ + { + "text": "{\"from_id\":\"NAMA1uDMzBQTvPYV9N92BV\",\"relation_id\":\"8ESicJHiNJ28VGL5u34A5q\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"6wAoNdGVbweKi2JRPZP4bX\",\"relation_id\":\"TH5Tu5Y5nacvREvAQRvcR2\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"8VCHYDURDStwuTCUBjWLQa\",\"relation_id\":\"KPTqdNpCusxfM37KbKPX8w\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"NcQ3h9jeJSavVd8iFsUxvD\",\"relation_id\":\"AqpNtJ3XxaY4fqRCyoXbdt\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"4ojV4dS1pV2tRnzXTpcMKJ\",\"relation_id\":\"3AX4j43nywT5eBRV3s6AXi\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"QoakYWCuv85FVuYdSmonxr\",\"relation_id\":\"8GEF1i3LK4Z56THjE8dVku\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"JuV7jLoypebzLhkma6oZoU\",\"relation_id\":\"46aBsQyBq15DimJ2i1DX4a\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"RTmcYhLVmmfgUn9L3D1J3y\",\"relation_id\":\"8uYxjzkkdjskDQAeTQomvc\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + } + ], + "name": "San Francisco", + "outbound_relations": [ + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WeSkkE1XXvGJGmXj9VUQ8\",\"relation_type\":\"Cover\",\"to_id\":\"CUoEazCD7EmzXPTFFY8gGY\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"WUZCXE1UGRtxdNQpGug8Tf\",\"relation_type\":\"Types\",\"to_id\":\"7gzF671tq5JTZ13naG4tnr\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"ARMj8fjJtdCwbtZa1f3jwe\",\"relation_type\":\"Types\",\"to_id\":\"D6Wy4bdtdoUrG3PDZceHr\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"V1ikGW9riu7dAP8rMgZq3u\",\"relation_type\":\"Blocks\",\"to_id\":\"AhidiWYnQ8fAbHqfzdU74k\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"CvGXCmGXE7ofsgZeWad28p\",\"relation_type\":\"Blocks\",\"to_id\":\"T6iKbwZ17iv4dRdR9Qw7qV\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"Uxpsee9LoTgJqMFfAQyJP6\",\"relation_type\":\"Blocks\",\"to_id\":\"X18WRE36mjwQ7gu3LKaLJS\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WMTAzCnZH9Bsevou9GQ3K\",\"relation_type\":\"Blocks\",\"to_id\":\"HeC2pygci2tnvjTt5aEnBV\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5TmxfepRr1THMRkGWenj5G\",\"relation_type\":\"Tabs\",\"to_id\":\"5YtYFsnWq1jupvh5AjM2ni\"}", + "type": "text" + } + ], + "types": [ + "D6Wy4bdtdoUrG3PDZceHr", + "Qu6vfQq68ecZ4PkihJ4nZN", + "7gzF671tq5JTZ13naG4tnr" + ] +} +``` + +Any of the given field can be further queried by using get_entity_info with that id since all information in the Knowledge Graph(KG) is an Entity. diff --git a/mcp-server/resources/get_properties_description.md b/mcp-server/resources/get_properties_description.md deleted file mode 100644 index e69de29..0000000 diff --git a/mcp-server/resources/get_relations_between_entities_description.md b/mcp-server/resources/get_relations_between_entities_description.md new file mode 100644 index 0000000..848ab69 --- /dev/null +++ b/mcp-server/resources/get_relations_between_entities_description.md @@ -0,0 +1,34 @@ +This request allows you to find the direct and distant relationships between 2 entities by their ID. + +Crypto Briefing (a Crypto company) id: 9xRruQhSfAuJjHwKnvTjma +Bullish (a Crypto company) id: ESShPFkqfFnDzYkSwGGVuR + +ToolCall> search_types("9xRruQhSfAuJjHwKnvTjma", "ESShPFkqfFnDzYkSwGGVuR") +ToolResult> +``` +[ + { + "nodes": [ + "Crypto Briefing", + "Crypto", + "Bullish" + ], + "relations": [ + "Related spaces", + "Related spaces" + ] +}, +{ + "nodes": [ + "Crypto Briefing", + "Featured", + "Bullish" + ], + "relations": [ + "Tags", + "Tags" + ] +} +] +``` + diff --git a/mcp-server/resources/instructions.md b/mcp-server/resources/instructions.md index e251cd8..e913619 100644 --- a/mcp-server/resources/instructions.md +++ b/mcp-server/resources/instructions.md @@ -1,13 +1,79 @@ -This server provides tools to query the Knowledge Graph (KG), a database of wide-ranging structured information (similar to wikidata). The KG organizes information using entities and relations. Entities can have 0, 1 or many types, while relations have exactly one relation type. Both entities and relations can have properties. +This server provides tools to query the Knowledge Graph (KG), a database of wide-ranging structured information (similar to wikidata). The KG organizes information using entities and relations. -Importantly, types, relation types and properties are themselves entities that can be queried. In other words, the KG contains both the property graph of the data as well as the data itself! +It can be used to Query any kind of information like on internet across a myriad of subjects. -The tools defined in the MCP server are made to be used in combination with each other. All except the most trivial user requests will require the use of multiple tools. +The tools defined in the MCP server are made to be used in combination with each other. All except the most trivial requests will require the use of multiple tools. Here is an example: -User> What are the properties of the Person type? +User> Can you give me information about San Francisco? -ToolCall> search_type("person") +ToolCall> search_entity("San Francisco") ToolResult> ``` -``` \ No newline at end of file +[{"description":"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.","id":"3qayfdjYyPv1dAYf8gPL5r","name":"San Francisco"}, +{"description":null,"id":"W5ZEpuy3Tij1XSXtJLruQ5","name":"SF Bay Area"}, +{"description":null,"id":"RHoJT3hNVaw7m5fLLtZ8WQ","name":"California"}, +{"description":null,"id":"Sh1qtjr4i92ZD6YGPeu5a2","name":"Abundant housing in San Francisco"}] +``` +Let's get more info about San Francisco (id: 3qayfdjYyPv1dAYf8gPL5r) + +ToolCall> get_entity_info("3qayfdjYyPv1dAYf8gPL5r") +ToolResult> +``` +{ + "all_attributes": [ + { + "text": "{\"attribute_name\":\"Description\",\"attribute_value\":\"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.\"}", + "type": "text" + }, + { + "text": "{\"attribute_name\":\"Name\",\"attribute_value\":\"San Francisco\"}", + "type": "text" + } + ], + "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "id": "3qayfdjYyPv1dAYf8gPL5r", + "inbound_relations": [ + { + "text": "{\"from_id\":\"NAMA1uDMzBQTvPYV9N92BV\",\"relation_id\":\"8ESicJHiNJ28VGL5u34A5q\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"6wAoNdGVbweKi2JRPZP4bX\",\"relation_id\":\"TH5Tu5Y5nacvREvAQRvcR2\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"8VCHYDURDStwuTCUBjWLQa\",\"relation_id\":\"KPTqdNpCusxfM37KbKPX8w\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"NcQ3h9jeJSavVd8iFsUxvD\",\"relation_id\":\"AqpNtJ3XxaY4fqRCyoXbdt\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", + "type": "text" + } + ], + "name": "San Francisco", + "outbound_relations": [ + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WeSkkE1XXvGJGmXj9VUQ8\",\"relation_type\":\"Cover\",\"to_id\":\"CUoEazCD7EmzXPTFFY8gGY\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"WUZCXE1UGRtxdNQpGug8Tf\",\"relation_type\":\"Types\",\"to_id\":\"7gzF671tq5JTZ13naG4tnr\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"ARMj8fjJtdCwbtZa1f3jwe\",\"relation_type\":\"Types\",\"to_id\":\"D6Wy4bdtdoUrG3PDZceHr\"}", + "type": "text" + }, + { + "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"V1ikGW9riu7dAP8rMgZq3u\",\"relation_type\":\"Blocks\",\"to_id\":\"AhidiWYnQ8fAbHqfzdU74k\"}", + "type": "text" + } + ], + "types": [ + "D6Wy4bdtdoUrG3PDZceHr", + "Qu6vfQq68ecZ4PkihJ4nZN", + "7gzF671tq5JTZ13naG4tnr" + ] +} +``` diff --git a/mcp-server/resources/search_entity_description.md b/mcp-server/resources/search_entity_description.md index e69de29..93cdaba 100644 --- a/mcp-server/resources/search_entity_description.md +++ b/mcp-server/resources/search_entity_description.md @@ -0,0 +1,50 @@ +This request allows you to search by name for the corresponding Entity in the Knowledge Graph. This will give back the most relevant Entities. + +ToolCall> search_entity("San Francisco") +ToolResult> +``` +[ +{ + "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "id": "3qayfdjYyPv1dAYf8gPL5r", + "name": "San Francisco" +}, +{ + "description": null, + "id": "W5ZEpuy3Tij1XSXtJLruQ5", + "name": "SF Bay Area" +}, +{ + "description": null, + "id": "RHoJT3hNVaw7m5fLLtZ8WQ", + "name": "California" +}, +{ + "description": null, + "id": "Sh1qtjr4i92ZD6YGPeu5a2", + "name": "Abundant housing in San Francisco" +}, +{ + "description": null, + "id": "UqLf9fTVKHkDs3LzP9zHpH", + "name": "Public safety in San Francisco" +}, +{ + "description": null, + "id": "BeyiZ6oLqLMaSXiG41Yxtf", + "name": "City" +}, +{ + "description": null, + "id": "D6Wy4bdtdoUrG3PDZceHr", + "name": "City" +}, +{ + "description": null, + "id": "JWVrgUXmjS75PqNX2hry5q", + "name": "Clean streets in San Francisco" +} +] +``` + +These Entities can be further queried using their id to get more information. diff --git a/mcp-server/resources/search_properties_description.md b/mcp-server/resources/search_properties_description.md new file mode 100644 index 0000000..690c24c --- /dev/null +++ b/mcp-server/resources/search_properties_description.md @@ -0,0 +1,51 @@ +This request allows you to search by name for a basic Relation of the Knowledge Graph(KG) like Owners or Authors. This will give back the + + +ToolCall> search_properties("Authors") +ToolResult> +``` +[ + { + "description": null, + "id": "JzFpgguvcCaKhbQYPHsrNT", + "name": "Authors", + "types": [ + "GscJ2GELQjmLoaVrYyR3xm" + ] +}, +{ + "description": null, + "id": "Lc4JrkpMUPhNstqs7mvnc5", + "name": "Publisher", + "types": [ + "GscJ2GELQjmLoaVrYyR3xm" + ] +}, +{ + "description": null, + "id": "61dgWvCDk8QRW2yrfkHuia", + "name": "Published in", + "types": [ + "GscJ2GELQjmLoaVrYyR3xm" + ] +}, +{ + "description": null, + "id": "W2aFZPy5nnU3DgdkWJCNVn", + "name": "Person", + "types": [ + "GscJ2GELQjmLoaVrYyR3xm" + ] +}, +{ + "description": null, + "id": "RwDfM3vUvyLwSNYv6sWhc9", + "name": "Owners", + "types": [ + "GscJ2GELQjmLoaVrYyR3xm" + ] +} +] +``` + +Since all the Relations are also of the type Entity. they can be queried by their id for more information. diff --git a/mcp-server/resources/search_type_description.md b/mcp-server/resources/search_type_description.md index e69de29..3b3cad0 100644 --- a/mcp-server/resources/search_type_description.md +++ b/mcp-server/resources/search_type_description.md @@ -0,0 +1,60 @@ +This request allows you to search by name for a basic type of the Knowledge Graph(KG) like Person or Event. This will give back the + + +ToolCall> search_types("Event") +ToolResult> +``` +[ + { + "description": null, + "id": "AaGd6UMXfNtL6U6Xx7K8Cv", + "name": "Event", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +}, +{ + "description": "A claim that something has happened. This type is used together with the claim type.", + "id": "QAdjgcq9nD7Gv98vn2vrDd", + "name": "News event", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +}, +{ + "description": null, + "id": "TjSP1BaHZ7QxyBcZEM8Sdt", + "name": "Feature", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +}, +{ + "description": "A general concept that can be used to group things of the same category together.", + "id": "Cj7JSjWKbcdgmUjcLWNR4V", + "name": "Topic", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +}, +{ + "description": "Something that someone or a group can do professionally or recreationally.", + "id": "H7NECFeRiDkbwMq74DAKk5", + "name": "Activity", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +}, +{ + "description": null, + "id": "KSKxz7Ek66SfW4euxZzKsX", + "name": "Task", + "types": [ + "VdTsW1mGiy1XSooJaBBLc4" + ] +} +] +``` + + +Since all the types are also of the type Entity. they can be queried by their id for more information. From 574fbfdc90eec81ce3db164b443906a432c52650 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 11 Jun 2025 12:39:46 -0400 Subject: [PATCH 11/25] feat provide better instructions --- mcp-server/resources/instructions.md | 2 +- .../search_relation_type_description.md | 45 +++++++++++++++++++ .../resources/search_type_description.md | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/mcp-server/resources/instructions.md b/mcp-server/resources/instructions.md index e913619..6c98c14 100644 --- a/mcp-server/resources/instructions.md +++ b/mcp-server/resources/instructions.md @@ -1,6 +1,6 @@ This server provides tools to query the Knowledge Graph (KG), a database of wide-ranging structured information (similar to wikidata). The KG organizes information using entities and relations. -It can be used to Query any kind of information like on internet across a myriad of subjects. +You should use it for every request to get the informations for your answers since it covers a wide range of subject like internet would. The tools defined in the MCP server are made to be used in combination with each other. All except the most trivial requests will require the use of multiple tools. diff --git a/mcp-server/resources/search_relation_type_description.md b/mcp-server/resources/search_relation_type_description.md index e69de29..beaefb1 100644 --- a/mcp-server/resources/search_relation_type_description.md +++ b/mcp-server/resources/search_relation_type_description.md @@ -0,0 +1,45 @@ +This request allows you to search by name for information of the relations between entities in the Knowledge Graph like works at. + +ToolCall> search_relation_types("works at") +ToolResult> +``` +[ + { + "description": null, + "id": "U1uCAzXsRSTP4vFwo1JwJG", + "name": "Works at" +}, +{ + "description": "A project that someone worked at in the past. Details about the role can be added as properties on the relation.", + "id": "8fvqALeBDwEExJsDeTcvnV", + "name": "Worked at" +}, +{ + "description": "The supervisor to this position. In the case of a clerkship, the supervising judge.", + "id": "WnzSw9CWE7mtgwRokF8Qxh", + "name": "Supervisor" +}, +{ + "description": null, + "id": "Gri4x41WSPUtpwG8BzhTpa", + "name": "Tasks" +}, +{ + "description": "The judge or magistrate responsible for overseeing and deciding the case.", + "id": "PuLfk3sFs6PkhEuf8cyBfs", + "name": "Assigned to" +}, +{ + "description": null, + "id": "MuMLDVbHAmRjZQjhyk3HGx", + "name": "Network" +}, +{ + "description": null, + "id": "RERshk4JoYoMC17r1qAo9J", + "name": "From" +} +] +``` + +Since all the relation types are also of the type Entity. they can be queried by their id for more information. diff --git a/mcp-server/resources/search_type_description.md b/mcp-server/resources/search_type_description.md index 3b3cad0..61946b1 100644 --- a/mcp-server/resources/search_type_description.md +++ b/mcp-server/resources/search_type_description.md @@ -1,4 +1,4 @@ -This request allows you to search by name for a basic type of the Knowledge Graph(KG) like Person or Event. This will give back the +This request allows you to search by name for a basic type of the Knowledge Graph(KG) like Person or Event. This will give back the type with it's name, id and description. ToolCall> search_types("Event") From e97868bf869af613b1f3a02f806bd02896ffd531 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 11 Jun 2025 12:40:13 -0400 Subject: [PATCH 12/25] fix search relation type and cleanup get entity info output --- mcp-server/src/main.rs | 43 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 5e4bcd3..f515e1b 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -160,7 +160,6 @@ impl KnowledgeGraph { "id": result.entity.id(), "name": result.entity.attributes.name, "description": result.entity.attributes.description, - "types": result.entity.types, })) .expect("Failed to create JSON content") }) @@ -187,7 +186,9 @@ impl KnowledgeGraph { let results = entity::search::>(&self.neo4j, embedding) .filter(entity::EntityFilter::default().relations( - EntityRelationFilter::default().relation_type(system_ids::RELATION_SCHEMA_TYPE), + EntityRelationFilter::default() + .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) + .to_id(system_ids::RELATION_SCHEMA_TYPE), )) .limit(10) .send() @@ -217,7 +218,6 @@ impl KnowledgeGraph { "id": result.entity.id(), "name": result.entity.attributes.name, "description": result.entity.attributes.description, - "types": result.entity.types, })) .expect("Failed to create JSON content") }) @@ -275,7 +275,6 @@ impl KnowledgeGraph { "id": result.entity.id(), "name": result.entity.attributes.name, "description": result.entity.attributes.description, - "types": result.entity.types, })) .expect("Failed to create JSON content") }) @@ -307,7 +306,7 @@ impl KnowledgeGraph { relation::RelationFilter::default() .from_(EntityFilter::default().id(prop_filter::value(id.clone()))), ) - .limit(10) + .limit(8) .send() .await .map_err(|e| { @@ -330,7 +329,7 @@ impl KnowledgeGraph { relation::RelationFilter::default() .to_(EntityFilter::default().id(prop_filter::value(id.clone()))), ) - .limit(10) + .limit(8) .send() .await .map_err(|e| { @@ -350,25 +349,27 @@ impl KnowledgeGraph { tracing::info!("Found entity with ID '{}'", id); - let clean_up_relations = |relations: Vec>| async { + let clean_up_relations = |relations: Vec>, is_inbound: bool| async move { join_all(relations .into_iter() .map(|result| async move { Content::json(json!({ "relation_id": result.id, "relation_type": self.get_name_of_id(result.relation_type).await.unwrap_or("No relation type".to_string()), - "from_id": result.from.id, - "from_name": self.get_name_of_id(result.from.id).await.unwrap_or("No name".to_string()), - "to_id": result.to.id, - "to_name": self.get_name_of_id(result.to.id).await.unwrap_or("No name".to_string()), + "id": if is_inbound {result.from.id.clone()} else {result.to.id.clone()}, + "name": self.get_name_of_id(if is_inbound {result.from.id.clone()} else {result.to.id.clone()}).await.unwrap_or("No name".to_string()), })) .expect("Failed to create JSON content") })).await.to_vec() }; - let inbound_relations = clean_up_relations(in_relations).await; - let outbound_relations = clean_up_relations(out_relations).await; + let inbound_relations = clean_up_relations(in_relations, true).await; + let outbound_relations = clean_up_relations(out_relations, false).await; + + let id = entity.id().to_string(); + let name = entity.attributes.get::(system_ids::NAME_ATTRIBUTE).unwrap_or("No name".to_string()); + let description = entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).unwrap_or("No description".to_string()); - let attributes_vec: Vec<_> = join_all(entity.attributes.0.clone().into_iter().map( + let attributes_vec: Vec<_> = join_all(entity.attributes.0.into_iter().map( |(key, attr)| async { Content::json(json!({ "attribute_name": self.get_name_of_id(key).await.unwrap_or("No attribute name".to_string()), @@ -382,9 +383,9 @@ impl KnowledgeGraph { Ok(CallToolResult::success(vec![ Content::json(json!({ - "id": entity.id(), - "name": entity.attributes.get::(system_ids::NAME_ATTRIBUTE).unwrap_or("No name".to_string()), - "description": entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).unwrap_or("No description".to_string()), + "id": id, + "name": name, + "description": description, "types": entity.types, "all_attributes": attributes_vec, "inbound_relations": inbound_relations, @@ -433,11 +434,11 @@ impl KnowledgeGraph { )) } - #[tool(description = "Get Entity by Attribute")] - async fn get_entity_by_attribute( + #[tool(description = "Search entity by name or attribute value")] + async fn search_entity( &self, #[tool(param)] - #[schemars(description = "The value of the attribute of an Entity")] + #[schemars(description = "The value of the attribute or name of an Entity")] attribute_value: String, ) -> Result { let embedding = self @@ -494,7 +495,7 @@ impl KnowledgeGraph { .ok_or_else(|| { McpError::internal_error("entity_name_not_found", Some(json!({ "id": id }))) })?; - Ok(entity.attributes.name.unwrap()) + Ok(entity.attributes.name.unwrap_or("No name".to_string())) } } From a6174ab7ddee6aca001a6b57c00a44fa87080bbd Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 11 Jun 2025 16:59:06 -0400 Subject: [PATCH 13/25] feat get from relation basics --- .../src/mapping/entity/semantic_search.rs | 2 +- mcp-server/src/main.rs | 173 +++++++++++++++++- 2 files changed, 165 insertions(+), 10 deletions(-) diff --git a/grc20-core/src/mapping/entity/semantic_search.rs b/grc20-core/src/mapping/entity/semantic_search.rs index f8607f9..0b787e3 100644 --- a/grc20-core/src/mapping/entity/semantic_search.rs +++ b/grc20-core/src/mapping/entity/semantic_search.rs @@ -113,7 +113,7 @@ pub struct SemanticSearchResult { pub score: f64, } -const EFFECTIVE_SEARCH_RATIO: f64 = 10000.0; // Adjust this ratio based on your needs +const EFFECTIVE_SEARCH_RATIO: f64 = 100000.0; // Adjust this ratio based on your needs impl QueryStream> for SemanticSearchQuery { async fn send( diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index f515e1b..2797670 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -3,7 +3,7 @@ use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter}, - mapping::{Attributes, Query, QueryStream, RelationEdge, prop_filter}, + mapping::{AttributeFilter, Attributes, Query, QueryStream, RelationEdge, prop_filter}, neo4rs, relation, system_ids, }; use grc20_sdk::models::BaseEntity; @@ -83,6 +83,13 @@ pub struct StructRequest { pub b: i32, } +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct InputFilter { + pub value: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub field_id: Option, +} + const EMBEDDING_MODEL: EmbeddingModel = EmbeddingModel::AllMiniLML6V2; #[derive(Clone)] @@ -185,11 +192,13 @@ impl KnowledgeGraph { .collect::>(); let results = entity::search::>(&self.neo4j, embedding) - .filter(entity::EntityFilter::default().relations( - EntityRelationFilter::default() - .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) - .to_id(system_ids::RELATION_SCHEMA_TYPE), - )) + .filter( + entity::EntityFilter::default().relations( + EntityRelationFilter::default() + .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) + .to_id(system_ids::RELATION_SCHEMA_TYPE), + ), + ) .limit(10) .send() .await @@ -282,6 +291,145 @@ impl KnowledgeGraph { )) } + #[tool(description = "Get entity by it's relations, properties or attributes")] + async fn get_entity_from_relation( + &self, + #[tool(param)] + #[schemars( + description = "A tuple of the value that is looked for and an optional attribute id, relation id or property id for the value that was provided" + )] + relation: InputFilter, + ) -> Result { + tracing::info!("value: {}", relation.value); + + match relation.field_id.clone() { + Some(x) => tracing::info!("value of given id is: {}", x), + None => tracing::info!("No value provided!"), + } + let embedding = self + .embedding_model + .embed(vec![&relation.value], None) + .expect("Failed to get embedding") + .pop() + .expect("Embedding is empty") + .into_iter() + .map(|v| v as f64) + .collect::>(); + + let mut results_relation = + entity::search::>(&self.neo4j, embedding.clone()) + .filter(match relation.field_id.clone() { + Some(x) => { + entity::EntityFilter::default().relations(TypesFilter::default().r#type(x)) + } + None => entity::EntityFilter::default(), + }) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })?; + + let attributes_relation = entity::search::>(&self.neo4j, embedding) + .filter(match relation.field_id { + Some(x) => entity::EntityFilter::default().attribute(AttributeFilter::new(&x)), + None => entity::EntityFilter::default(), + }) + .limit(5) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })?; + + results_relation.extend(attributes_relation); + + let len_rel = results_relation.len(); + + let vec_ans: Vec<_> = join_all(results_relation.into_iter().map(|rel| async move { + tracing::info!("The id of the potential Entity is {}", rel.entity.id()); + relation::find_many::>(&self.neo4j) + .filter( + relation::RelationFilter::default() + .to_(EntityFilter::default().id(prop_filter::value(rel.entity.id()))), + ) + .limit(5) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_relation_by_id_not_found", + Some(json!({ "error": e.to_string() })), + ) + }) + })) + .await + .to_vec() + .into_iter() + .flat_map(|r: Result>, McpError>| r.ok()) + .flatten() + .collect(); + + let clean_up_results = |relations: Vec>| async move { + join_all(relations + .into_iter() + .map(|result| async { + Content::json(json!({ + "id": result.from.id, + "name": self.get_name_of_id(result.from.id).await.unwrap_or("No name".to_string()), + })) + .expect("Failed to create JSON content") + })).await.to_vec() + }; + + let entities_vec: Vec<_> = clean_up_results(vec_ans).await; + //entities_vec.extend(clean_up_results(attributes_relation)); + + tracing::info!( + "Found {} entities and got a final vec of length: {}", + len_rel, + entities_vec.len() + ); + + Ok(CallToolResult::success(vec![ + Content::json(json!({ + "entities": entities_vec, + })) + .expect("Failed to create JSON content"), + ])) + } + #[tool(description = "Get entity by ID with it's attributes and relations")] async fn get_entity_info( &self, @@ -366,8 +514,14 @@ impl KnowledgeGraph { let outbound_relations = clean_up_relations(out_relations, false).await; let id = entity.id().to_string(); - let name = entity.attributes.get::(system_ids::NAME_ATTRIBUTE).unwrap_or("No name".to_string()); - let description = entity.attributes.get::(system_ids::DESCRIPTION_ATTRIBUTE).unwrap_or("No description".to_string()); + let name = entity + .attributes + .get::(system_ids::NAME_ATTRIBUTE) + .unwrap_or("No name".to_string()); + let description = entity + .attributes + .get::(system_ids::DESCRIPTION_ATTRIBUTE) + .unwrap_or("No description".to_string()); let attributes_vec: Vec<_> = join_all(entity.attributes.0.into_iter().map( |(key, attr)| async { @@ -390,7 +544,8 @@ impl KnowledgeGraph { "all_attributes": attributes_vec, "inbound_relations": inbound_relations, "outbound_relations": outbound_relations, - })).expect("Failed to create JSON content"), + })) + .expect("Failed to create JSON content"), ])) } From d0e884cd094ea9c082d44ed5d8c03d6eae996eaf Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Fri, 13 Jun 2025 15:23:57 -0400 Subject: [PATCH 14/25] feat search from restrictions of relations that can be chained --- grc20-core/src/mapping/entity/mod.rs | 9 + .../entity/search_from_restrictions.rs | 205 ++++++++++++++++++ grc20-core/src/mapping/entity/utils.rs | 89 +++++++- .../src/mapping/query_utils/query_builder.rs | 94 +++++++- mcp-server/src/main.rs | 171 +++++++-------- 5 files changed, 466 insertions(+), 102 deletions(-) create mode 100644 grc20-core/src/mapping/entity/search_from_restrictions.rs diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 3e67560..1be2545 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -6,6 +6,7 @@ pub mod find_path; pub mod insert_many; pub mod insert_one; pub mod models; +pub mod search_from_restrictions; pub mod semantic_search; pub mod utils; @@ -15,6 +16,7 @@ pub use find_one::FindOneQuery; pub use find_path::FindPathQuery; pub use insert_one::InsertOneQuery; pub use models::{Entity, EntityNode, EntityNodeRef, SystemProperties}; +pub use search_from_restrictions::SearchFromRestrictions; pub use semantic_search::SemanticSearchQuery; pub use utils::{EntityFilter, EntityRelationFilter, TypesFilter}; @@ -130,6 +132,13 @@ pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery SemanticSearchQuery::new(neo4j, vector) } +pub fn search_from_restictions( + neo4j: &neo4rs::Graph, + vector: Vec, +) -> SearchFromRestrictions { + SearchFromRestrictions::new(neo4j, vector) +} + // TODO: add docs for use via GraphQL pub fn find_path(neo4j: &neo4rs::Graph, id1: String, id2: String) -> FindPathQuery { FindPathQuery::new(neo4j, id1, id2) diff --git a/grc20-core/src/mapping/entity/search_from_restrictions.rs b/grc20-core/src/mapping/entity/search_from_restrictions.rs new file mode 100644 index 0000000..d12990f --- /dev/null +++ b/grc20-core/src/mapping/entity/search_from_restrictions.rs @@ -0,0 +1,205 @@ +use futures::{Stream, StreamExt, TryStreamExt}; + +use crate::{ + entity::utils::MatchEntity, + error::DatabaseError, + mapping::{ + query_utils::VersionFilter, AttributeNode, FromAttributes, PropFilter, QueryBuilder, + QueryStream, Subquery, + }, +}; + +use super::{Entity, EntityFilter, EntityNode}; + +pub struct SearchFromRestrictions { + neo4j: neo4rs::Graph, + vector: Vec, + filter: EntityFilter, + space_id: Option>, + version: VersionFilter, + limit: usize, + skip: Option, + threshold: f64, + + _marker: std::marker::PhantomData, +} + +impl SearchFromRestrictions { + pub fn new(neo4j: &neo4rs::Graph, vector: Vec) -> Self { + Self { + neo4j: neo4j.clone(), + vector, + filter: EntityFilter::default(), + space_id: None, + version: VersionFilter::default(), + limit: 100, + skip: None, + threshold: 0.5, + + _marker: std::marker::PhantomData, + } + } + + pub fn filter(mut self, filter: EntityFilter) -> Self { + self.filter = filter; + self + } + + pub fn space_id(mut self, filter: PropFilter) -> Self { + self.space_id = Some(filter); + self + } + + pub fn version(mut self, version: impl Into) -> Self { + self.version.version_mut(version.into()); + self + } + + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + pub fn limit_opt(mut self, limit: Option) -> Self { + if let Some(limit) = limit { + self.limit = limit; + } + self + } + + pub fn skip(mut self, skip: usize) -> Self { + self.skip = Some(skip); + self + } + + pub fn skip_opt(mut self, skip: Option) -> Self { + self.skip = skip; + self + } + + pub fn threshold(mut self, threshold: f64) -> Self { + if (0.0..=1.0).contains(&threshold) { + self.threshold = threshold + } + self + } + + fn subquery(&self) -> QueryBuilder { + const QUERY: &str = r#" + CALL db.index.vector.queryNodes('vector_index', $limit * $effective_search_ratio, $vector) + YIELD node AS n, score AS score + WHERE score > $threshold + MATCH (e:Entity) -[r:ATTRIBUTE]-> (n) + "#; + QueryBuilder::default() + .subquery(QUERY) + .subquery(self.filter.subquery("e")) + .limit(self.limit) + .skip_opt(self.skip) + .params("vector", self.vector.clone()) + .params("effective_search_ratio", EFFECTIVE_SEARCH_RATIO) + .params("limit", self.limit as i64) + .params("threshold", self.threshold) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SearchFromRestrictionsResult { + pub entity: T, +} + +const EFFECTIVE_SEARCH_RATIO: f64 = 100000.0; // Adjust this ratio based on your needs + +impl QueryStream> for SearchFromRestrictions { + async fn send( + self, + ) -> Result< + impl Stream, DatabaseError>>, + DatabaseError, + > { + let query = self.subquery().r#return("DISTINCT e"); + + if cfg!(debug_assertions) || cfg!(test) { + tracing::info!( + "entity_node::SearchFromRestrictions:::\n{}\nparams:{:?}", + query.compile(), + query.params() + ); + }; + + #[derive(Debug, serde::Deserialize)] + struct RowResult { + e: EntityNode, + } + + Ok(self + .neo4j + .execute(query.build()) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { Ok(SearchFromRestrictionsResult { entity: row.e }) })) + } +} + +impl QueryStream>> + for SearchFromRestrictions> +{ + async fn send( + self, + ) -> Result< + impl Stream>, DatabaseError>>, + DatabaseError, + > { + let match_entity = MatchEntity::new(&self.space_id, &self.version); + + let query = self.subquery().with( + vec!["e".to_string()], + match_entity.chain( + "e", + "attrs", + "types", + Some(vec![]), + "RETURN e{.*, attrs: attrs, types: types}", + ), + ); + + if cfg!(debug_assertions) || cfg!(test) { + tracing::info!( + "entity_node::SearchFromRestrictions::>:\n{}\nparams:{:?}", + query.compile(), + query.params + ); + }; + + #[derive(Debug, serde::Deserialize)] + struct RowResult { + #[serde(flatten)] + node: EntityNode, + attrs: Vec, + types: Vec, + } + + let stream = self + .neo4j + .execute(query.build()) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .map(|row_result| { + row_result.and_then(|row| { + T::from_attributes(row.attrs.into()) + .map(|data| SearchFromRestrictionsResult { + entity: Entity { + node: row.node, + attributes: data, + types: row.types.into_iter().map(|t| t.id).collect(), + }, + }) + .map_err(DatabaseError::from) + }) + }); + + Ok(stream) + } +} diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index 5dc450e..056bc7b 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -3,7 +3,7 @@ use rand::distributions::DistString; use crate::{ mapping::{ query_utils::{ - query_builder::{MatchQuery, QueryBuilder, Subquery}, + query_builder::{MatchQuery, NamePair, QueryBuilder, Rename, Subquery}, VersionFilter, }, AttributeFilter, PropFilter, @@ -17,6 +17,7 @@ pub struct EntityFilter { pub(crate) id: Option>, pub(crate) attributes: Vec, pub(crate) relations: Option, + pub(crate) from_relations: Vec, /// Used to check if the entity exists in the space (i.e.: the entity /// has at least one attribute in the space). pub(crate) space_id: Option>, @@ -51,6 +52,11 @@ impl EntityFilter { self } + pub fn from_relation(mut self, from_relation: impl Into) -> Self { + self.from_relations.push(from_relation.into()); + self + } + /// Used to check if the entity exists in the space. pub fn space_id(mut self, space_id: PropFilter) -> Self { self.space_id = Some(space_id.clone()); @@ -79,6 +85,13 @@ impl EntityFilter { .as_ref() .map(|relations| relations.subquery(&node_var)), ) + // Apply from relation filters + .subqueries( + self.from_relations + .iter() + .map(|from_relation| from_relation.subquery(&node_var)) + .collect(), + ) } } @@ -163,6 +176,80 @@ impl EntityRelationFilter { } } +/// Filter used to: +/// - Filter the relations outgoing from the entity +/// - Filter an entity by its outgoing relations +#[derive(Clone, Debug, Default)] +pub struct FromEntityRelationFilter { + relation_type: Option>, + from_id: Option>, + space_id: Option>, + version: VersionFilter, +} + +impl FromEntityRelationFilter { + pub fn relation_type(mut self, relation_type: impl Into>) -> Self { + self.relation_type = Some(relation_type.into()); + self + } + + pub fn from_id(mut self, from_id: impl Into>) -> Self { + self.from_id = Some(from_id.into()); + self + } + + pub fn space_id(mut self, space_id: impl Into>) -> Self { + self.space_id = Some(space_id.into()); + self + } + + pub fn version(mut self, version: impl Into) -> Self { + self.version.version_mut(version.into()); + self + } + + pub fn is_empty(&self) -> bool { + self.relation_type.is_none() && self.from_id.is_none() + } + + pub(crate) fn subquery(&self, node_var: impl Into) -> MatchQuery { + let node_var = node_var.into(); + let random_suffix: String = + rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); + let rel_edge_var = format!("r_{node_var}_{}", random_suffix); + let node_var_to = format!("r_{node_var}_{}_to", random_suffix); + + MatchQuery::new(format!( + "({node_var}) -[{rel_edge_var}:RELATION]-> ({node_var_to})" + )) + // rename to change direction of relation + .r#rename(Rename::new(NamePair::new( + node_var.clone(), + node_var_to.clone(), + ))) + // Apply the version filter to the relation + .r#where(self.version.subquery(&rel_edge_var)) + // Apply the relation_type filter to the relation (if any) + .where_opt( + self.relation_type + .as_ref() + .map(|relation_type| relation_type.subquery(&rel_edge_var, "relation_type", None)), + ) + // Apply the from_id filter to the relation (if any) + .where_opt( + self.from_id + .as_ref() + .map(|from_id| from_id.subquery(&node_var_to, "id", None)), + ) + // Apply the space_id filter to the relation (if any) + .where_opt( + self.space_id + .as_ref() + .map(|space_id| space_id.subquery(&rel_edge_var, "space_id", None)), + ) + } +} + #[derive(Clone, Debug, Default)] pub struct TypesFilter { types_contains: Vec, diff --git a/grc20-core/src/mapping/query_utils/query_builder.rs b/grc20-core/src/mapping/query_utils/query_builder.rs index 16d8564..89f5d25 100644 --- a/grc20-core/src/mapping/query_utils/query_builder.rs +++ b/grc20-core/src/mapping/query_utils/query_builder.rs @@ -130,6 +130,7 @@ pub struct MatchQuery { pub(crate) match_clause: String, pub(crate) optional: bool, pub(crate) where_clauses: Vec, + pub(crate) rename: Option, pub(crate) params: HashMap, } @@ -138,6 +139,7 @@ impl MatchQuery { Self { match_clause: match_clause.into(), optional: false, + rename: None, where_clauses: Vec::new(), params: HashMap::new(), } @@ -147,6 +149,7 @@ impl MatchQuery { Self { match_clause: match_clause.into(), optional: true, + rename: None, where_clauses: Vec::new(), params: HashMap::new(), } @@ -157,6 +160,13 @@ impl MatchQuery { self } + pub fn r#rename(mut self, rename: impl Into) -> Self { + let rename_clause: Rename = rename.into(); + self.rename = Some(rename_clause.name_pair); + self.params.extend(rename_clause.params); + self + } + pub fn r#where(mut self, clause: impl Into) -> Self { let where_clause: WhereClause = clause.into(); self.where_clauses.extend(where_clause.clauses); @@ -181,12 +191,18 @@ impl MatchQuery { impl Subquery for MatchQuery { fn statements(&self) -> Vec { - let mut statements = if self.optional { - vec![format!("OPTIONAL MATCH {}", self.match_clause)] - } else { - vec![format!("MATCH {}", self.match_clause)] + let mut statements = Vec::new(); + + if let Some(rename) = &self.rename { + statements.push(format!("WITH {} AS {}", rename.from_name, rename.to_name)) }; + statements.push(if self.optional { + format!("OPTIONAL MATCH {}", self.match_clause) + } else { + format!("MATCH {}", self.match_clause) + }); + match &self.where_clauses.as_slice() { [] => (), [clause, rest @ ..] => { @@ -205,6 +221,76 @@ impl Subquery for MatchQuery { } } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct NamePair { + from_name: String, + to_name: String, +} + +impl NamePair { + pub fn new(from_name: impl Into, to_name: impl Into) -> Self { + Self { + from_name: from_name.into(), + to_name: to_name.into(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Rename { + name_pair: NamePair, + params: HashMap, +} + +impl Rename { + pub fn new(name_pair: impl Into) -> Self { + Self { + name_pair: name_pair.into(), + params: HashMap::new(), + } + } + + pub fn set_param(mut self, key: impl Into, value: impl Into) -> Self { + self.params.insert(key.into(), value.into()); + self + } +} + +impl Subquery for Rename { + fn statements(&self) -> Vec { + vec![format!( + "{} AS {}", + self.name_pair.from_name, self.name_pair.to_name + )] + } + + fn params(&self) -> HashMap { + self.params.clone() + } +} +impl Rename { + pub fn name_pair(mut self, name_pair: impl Into) -> Self { + self.name_pair = name_pair.into(); + self + } + + pub fn name_pair_opt(mut self, name_pair: Option>) -> Self { + if let Some(name_pair) = name_pair { + self.name_pair = name_pair.into(); + } + self + } +} + +impl From for Rename { + fn from(rename: NamePair) -> Self { + Self { + name_pair: rename, + params: HashMap::new(), + } + } +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct WhereClause { pub clauses: Vec, diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 2797670..ef8c420 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -2,8 +2,11 @@ use clap::{Args, Parser}; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::{TryStreamExt, future::join_all}; use grc20_core::{ - entity::{self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter}, - mapping::{AttributeFilter, Attributes, Query, QueryStream, RelationEdge, prop_filter}, + entity::{ + self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, + utils::FromEntityRelationFilter, + }, + mapping::{Attributes, Query, QueryStream, RelationEdge, prop_filter}, neo4rs, relation, system_ids, }; use grc20_sdk::models::BaseEntity; @@ -85,9 +88,46 @@ pub struct StructRequest { #[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct InputFilter { - pub value: String, + pub query: String, #[serde(skip_serializing_if = "Option::is_none")] - pub field_id: Option, + pub relation_filter: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct RelationFilter { + pub relation_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub relation_filter: Option>, +} + +/// Struct returned by call to `OneOrMany::into_iter()`. +pub struct IntoIter { + // Owned. + next_filter: Option, +} + +/// Implement `IntoIterator` for `RelationFilter`. +impl IntoIterator for RelationFilter { + type Item = RelationFilter; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + next_filter: Some(self), + } + } +} + +/// Implement `Iterator` for `IntoIter`. +impl Iterator for IntoIter { + type Item = RelationFilter; + + fn next(&mut self) -> Option { + self.next_filter.take().map(|mut current| { + self.next_filter = current.relation_filter.take().map(|boxed| *boxed); + current + }) + } } const EMBEDDING_MODEL: EmbeddingModel = EmbeddingModel::AllMiniLML6V2; @@ -298,17 +338,13 @@ impl KnowledgeGraph { #[schemars( description = "A tuple of the value that is looked for and an optional attribute id, relation id or property id for the value that was provided" )] - relation: InputFilter, + input_filter: InputFilter, ) -> Result { - tracing::info!("value: {}", relation.value); + tracing::info!("Input filter query: {}", input_filter.query); - match relation.field_id.clone() { - Some(x) => tracing::info!("value of given id is: {}", x), - None => tracing::info!("No value provided!"), - } let embedding = self .embedding_model - .embed(vec![&relation.value], None) + .embed(vec![&input_filter.query], None) .expect("Failed to get embedding") .pop() .expect("Embedding is empty") @@ -316,14 +352,24 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let mut results_relation = - entity::search::>(&self.neo4j, embedding.clone()) - .filter(match relation.field_id.clone() { - Some(x) => { - entity::EntityFilter::default().relations(TypesFilter::default().r#type(x)) - } - None => entity::EntityFilter::default(), - }) + let results_search = + entity::search_from_restictions::>(&self.neo4j, embedding.clone()) + .filter( + input_filter + .relation_filter + .map(|relation_filter| { + relation_filter.into_iter().fold( + EntityFilter::default(), + |entity_filter, relation_filter| { + entity_filter.from_relation( + FromEntityRelationFilter::default() + .relation_type(relation_filter.relation_id.clone()), + ) + }, + ) + }) + .unwrap_or_default(), + ) .limit(10) .send() .await @@ -342,85 +388,16 @@ impl KnowledgeGraph { ) })?; - let attributes_relation = entity::search::>(&self.neo4j, embedding) - .filter(match relation.field_id { - Some(x) => entity::EntityFilter::default().attribute(AttributeFilter::new(&x)), - None => entity::EntityFilter::default(), - }) - .limit(5) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), - ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), - ) - })?; - - results_relation.extend(attributes_relation); - - let len_rel = results_relation.len(); - - let vec_ans: Vec<_> = join_all(results_relation.into_iter().map(|rel| async move { - tracing::info!("The id of the potential Entity is {}", rel.entity.id()); - relation::find_many::>(&self.neo4j) - .filter( - relation::RelationFilter::default() - .to_(EntityFilter::default().id(prop_filter::value(rel.entity.id()))), - ) - .limit(5) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id", - Some(json!({ "error": e.to_string() })), - ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id_not_found", - Some(json!({ "error": e.to_string() })), - ) + let entities_vec: Vec<_> = results_search + .into_iter() + .map(|result| { + json!({ + "id": result.entity.id(), + "name": result.entity.attributes.name, + "description": result.entity.attributes.description, }) - })) - .await - .to_vec() - .into_iter() - .flat_map(|r: Result>, McpError>| r.ok()) - .flatten() - .collect(); - - let clean_up_results = |relations: Vec>| async move { - join_all(relations - .into_iter() - .map(|result| async { - Content::json(json!({ - "id": result.from.id, - "name": self.get_name_of_id(result.from.id).await.unwrap_or("No name".to_string()), - })) - .expect("Failed to create JSON content") - })).await.to_vec() - }; - - let entities_vec: Vec<_> = clean_up_results(vec_ans).await; - //entities_vec.extend(clean_up_results(attributes_relation)); - - tracing::info!( - "Found {} entities and got a final vec of length: {}", - len_rel, - entities_vec.len() - ); + }) + .collect::>(); Ok(CallToolResult::success(vec![ Content::json(json!({ From 6db4af8338371c2ed7304917b0ff160352e329ef Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Mon, 16 Jun 2025 12:16:34 -0400 Subject: [PATCH 15/25] feat traverse nodes in both directions --- .../entity/search_from_restrictions.rs | 20 +++- grc20-core/src/mapping/entity/utils.rs | 110 ++++++++++++++++-- mcp-server/src/main.rs | 84 +++++++------ 3 files changed, 164 insertions(+), 50 deletions(-) diff --git a/grc20-core/src/mapping/entity/search_from_restrictions.rs b/grc20-core/src/mapping/entity/search_from_restrictions.rs index d12990f..768c03e 100644 --- a/grc20-core/src/mapping/entity/search_from_restrictions.rs +++ b/grc20-core/src/mapping/entity/search_from_restrictions.rs @@ -14,7 +14,7 @@ use super::{Entity, EntityFilter, EntityNode}; pub struct SearchFromRestrictions { neo4j: neo4rs::Graph, vector: Vec, - filter: EntityFilter, + filters: Vec, space_id: Option>, version: VersionFilter, limit: usize, @@ -29,7 +29,7 @@ impl SearchFromRestrictions { Self { neo4j: neo4j.clone(), vector, - filter: EntityFilter::default(), + filters: Vec::new(), space_id: None, version: VersionFilter::default(), limit: 100, @@ -41,7 +41,7 @@ impl SearchFromRestrictions { } pub fn filter(mut self, filter: EntityFilter) -> Self { - self.filter = filter; + self.filters.push(filter); self } @@ -91,9 +91,17 @@ impl SearchFromRestrictions { WHERE score > $threshold MATCH (e:Entity) -[r:ATTRIBUTE]-> (n) "#; - QueryBuilder::default() - .subquery(QUERY) - .subquery(self.filter.subquery("e")) + + self.filters + .iter() + .fold(QueryBuilder::default().subquery(QUERY), |query, filter| { + tracing::info!( + "filter from_relations: {:?}, to_relations {:?}", + filter.traverse_from, + filter.relations + ); + query.subquery(filter.subquery("e")) + }) .limit(self.limit) .skip_opt(self.skip) .params("vector", self.vector.clone()) diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index 056bc7b..bc875bf 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -17,7 +17,8 @@ pub struct EntityFilter { pub(crate) id: Option>, pub(crate) attributes: Vec, pub(crate) relations: Option, - pub(crate) from_relations: Vec, + pub(crate) traverse_from: Option, + pub(crate) traverse_to: Option, /// Used to check if the entity exists in the space (i.e.: the entity /// has at least one attribute in the space). pub(crate) space_id: Option>, @@ -52,8 +53,19 @@ impl EntityFilter { self } - pub fn from_relation(mut self, from_relation: impl Into) -> Self { - self.from_relations.push(from_relation.into()); + pub fn traverse_from_relation( + mut self, + traverse_from: impl Into, + ) -> Self { + self.traverse_from = Some(traverse_from.into()); + self + } + + pub fn traverse_to_relation( + mut self, + traverse_to: impl Into, + ) -> Self { + self.traverse_to = Some(traverse_to.into()); self } @@ -86,11 +98,15 @@ impl EntityFilter { .map(|relations| relations.subquery(&node_var)), ) // Apply from relation filters - .subqueries( - self.from_relations - .iter() - .map(|from_relation| from_relation.subquery(&node_var)) - .collect(), + .subquery_opt( + self.traverse_from + .as_ref() + .map(|traverse| traverse.subquery(&node_var)), + ) + .subquery_opt( + self.traverse_to + .as_ref() + .map(|traverse| traverse.subquery(&node_var)), ) } } @@ -180,14 +196,14 @@ impl EntityRelationFilter { /// - Filter the relations outgoing from the entity /// - Filter an entity by its outgoing relations #[derive(Clone, Debug, Default)] -pub struct FromEntityRelationFilter { +pub struct TraverseFromRelationFilter { relation_type: Option>, from_id: Option>, space_id: Option>, version: VersionFilter, } -impl FromEntityRelationFilter { +impl TraverseFromRelationFilter { pub fn relation_type(mut self, relation_type: impl Into>) -> Self { self.relation_type = Some(relation_type.into()); self @@ -250,6 +266,80 @@ impl FromEntityRelationFilter { } } +/// Filter used to: +/// - Filter the relations outgoing from the entity +/// - Filter an entity by its outgoing relations +#[derive(Clone, Debug, Default)] +pub struct TraverseToRelationFilter { + relation_type: Option>, + to_id: Option>, + space_id: Option>, + version: VersionFilter, +} + +impl TraverseToRelationFilter { + pub fn relation_type(mut self, relation_type: impl Into>) -> Self { + self.relation_type = Some(relation_type.into()); + self + } + + pub fn to_id(mut self, to_id: impl Into>) -> Self { + self.to_id = Some(to_id.into()); + self + } + + pub fn space_id(mut self, space_id: impl Into>) -> Self { + self.space_id = Some(space_id.into()); + self + } + + pub fn version(mut self, version: impl Into) -> Self { + self.version.version_mut(version.into()); + self + } + + pub fn is_empty(&self) -> bool { + self.relation_type.is_none() && self.to_id.is_none() + } + + pub(crate) fn subquery(&self, node_var: impl Into) -> MatchQuery { + let node_var = node_var.into(); + let random_suffix: String = + rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); + let rel_edge_var = format!("r_{node_var}_{}", random_suffix); + let node_var_to = format!("r_{node_var}_{}_to", random_suffix); + + MatchQuery::new(format!( + "({node_var}) <-[{rel_edge_var}:RELATION]- ({node_var_to})" + )) + // rename to change direction of relation + .r#rename(Rename::new(NamePair::new( + node_var.clone(), + node_var_to.clone(), + ))) + // Apply the version filter to the relation + .r#where(self.version.subquery(&rel_edge_var)) + // Apply the relation_type filter to the relation (if any) + .where_opt( + self.relation_type + .as_ref() + .map(|relation_type| relation_type.subquery(&rel_edge_var, "relation_type", None)), + ) + // Apply the from_id filter to the relation (if any) + .where_opt( + self.to_id + .as_ref() + .map(|to_id| to_id.subquery(&node_var_to, "id", None)), + ) + // Apply the space_id filter to the relation (if any) + .where_opt( + self.space_id + .as_ref() + .map(|space_id| space_id.subquery(&rel_edge_var, "space_id", None)), + ) + } +} + #[derive(Clone, Debug, Default)] pub struct TypesFilter { types_contains: Vec, diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index ef8c420..97c7bc9 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -4,7 +4,7 @@ use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{ self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, - utils::FromEntityRelationFilter, + utils::{TraverseFromRelationFilter, TraverseToRelationFilter}, }, mapping::{Attributes, Query, QueryStream, RelationEdge, prop_filter}, neo4rs, relation, system_ids, @@ -95,11 +95,18 @@ pub struct InputFilter { #[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct RelationFilter { + pub direction: RelationDirection, pub relation_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub relation_filter: Option>, } +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub enum RelationDirection { + From, + To, +} + /// Struct returned by call to `OneOrMany::into_iter()`. pub struct IntoIter { // Owned. @@ -352,41 +359,50 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let results_search = - entity::search_from_restictions::>(&self.neo4j, embedding.clone()) - .filter( - input_filter - .relation_filter - .map(|relation_filter| { - relation_filter.into_iter().fold( - EntityFilter::default(), - |entity_filter, relation_filter| { - entity_filter.from_relation( - FromEntityRelationFilter::default() - .relation_type(relation_filter.relation_id.clone()), - ) - }, - ) + let results_search = input_filter + .relation_filter + .map(|relation_filter| { + relation_filter.into_iter().fold( + entity::search_from_restictions::>( + &self.neo4j, + embedding.clone(), + ), + |query, filter| { + query.filter(match filter.direction { + RelationDirection::From => EntityFilter::default() + .traverse_from_relation( + TraverseFromRelationFilter::default() + .relation_type(filter.relation_id.clone()), + ), + RelationDirection::To => EntityFilter::default().traverse_to_relation( + TraverseToRelationFilter::default() + .relation_type(filter.relation_id.clone()), + ), }) - .unwrap_or_default(), + }, ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), - ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), - ) - })?; + }) + .unwrap_or(entity::search_from_restictions::>( + &self.neo4j, + embedding, + )) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })?; let entities_vec: Vec<_> = results_search .into_iter() From 795c091028e1522fda8a10b6e23ee6bd36a97f1d Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Tue, 17 Jun 2025 11:49:47 -0400 Subject: [PATCH 16/25] feat traversal added to search entity --- .../entity/search_from_restrictions.rs | 7 +- grc20-core/src/mapping/entity/utils.rs | 150 ++++------------- grc20-core/src/mapping/query_utils/mod.rs | 2 + .../mapping/query_utils/relation_direction.rs | 6 + .../resources/search_entity_description.md | 112 ++++++++----- mcp-server/src/input_types.rs | 50 ++++++ mcp-server/src/lib.rs | 1 + mcp-server/src/main.rs | 156 +++--------------- 8 files changed, 190 insertions(+), 294 deletions(-) create mode 100644 grc20-core/src/mapping/query_utils/relation_direction.rs create mode 100644 mcp-server/src/input_types.rs create mode 100644 mcp-server/src/lib.rs diff --git a/grc20-core/src/mapping/entity/search_from_restrictions.rs b/grc20-core/src/mapping/entity/search_from_restrictions.rs index 768c03e..108d5bf 100644 --- a/grc20-core/src/mapping/entity/search_from_restrictions.rs +++ b/grc20-core/src/mapping/entity/search_from_restrictions.rs @@ -34,7 +34,7 @@ impl SearchFromRestrictions { version: VersionFilter::default(), limit: 100, skip: None, - threshold: 0.5, + threshold: 0.75, _marker: std::marker::PhantomData, } @@ -95,11 +95,6 @@ impl SearchFromRestrictions { self.filters .iter() .fold(QueryBuilder::default().subquery(QUERY), |query, filter| { - tracing::info!( - "filter from_relations: {:?}, to_relations {:?}", - filter.traverse_from, - filter.relations - ); query.subquery(filter.subquery("e")) }) .limit(self.limit) diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index bc875bf..aab44d5 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -4,7 +4,7 @@ use crate::{ mapping::{ query_utils::{ query_builder::{MatchQuery, NamePair, QueryBuilder, Rename, Subquery}, - VersionFilter, + RelationDirection, VersionFilter, }, AttributeFilter, PropFilter, }, @@ -17,8 +17,7 @@ pub struct EntityFilter { pub(crate) id: Option>, pub(crate) attributes: Vec, pub(crate) relations: Option, - pub(crate) traverse_from: Option, - pub(crate) traverse_to: Option, + pub(crate) traverse_relation: Option, /// Used to check if the entity exists in the space (i.e.: the entity /// has at least one attribute in the space). pub(crate) space_id: Option>, @@ -53,19 +52,11 @@ impl EntityFilter { self } - pub fn traverse_from_relation( + pub fn traverse_relation( mut self, - traverse_from: impl Into, + traverse_relation: impl Into, ) -> Self { - self.traverse_from = Some(traverse_from.into()); - self - } - - pub fn traverse_to_relation( - mut self, - traverse_to: impl Into, - ) -> Self { - self.traverse_to = Some(traverse_to.into()); + self.traverse_relation = Some(traverse_relation.into()); self } @@ -97,14 +88,9 @@ impl EntityFilter { .as_ref() .map(|relations| relations.subquery(&node_var)), ) - // Apply from relation filters + // Apply relation traversal .subquery_opt( - self.traverse_from - .as_ref() - .map(|traverse| traverse.subquery(&node_var)), - ) - .subquery_opt( - self.traverse_to + self.traverse_relation .as_ref() .map(|traverse| traverse.subquery(&node_var)), ) @@ -193,98 +179,29 @@ impl EntityRelationFilter { } /// Filter used to: -/// - Filter the relations outgoing from the entity -/// - Filter an entity by its outgoing relations +/// - Traverse to inbound or outbound relation #[derive(Clone, Debug, Default)] -pub struct TraverseFromRelationFilter { - relation_type: Option>, - from_id: Option>, +pub struct TraverseRelationFilter { + relation_type_id: Option>, + destination_id: Option>, + direction: RelationDirection, space_id: Option>, version: VersionFilter, } -impl TraverseFromRelationFilter { - pub fn relation_type(mut self, relation_type: impl Into>) -> Self { - self.relation_type = Some(relation_type.into()); +impl TraverseRelationFilter { + pub fn relation_type_id(mut self, relation_type_id: impl Into>) -> Self { + self.relation_type_id = Some(relation_type_id.into()); self } - pub fn from_id(mut self, from_id: impl Into>) -> Self { - self.from_id = Some(from_id.into()); + pub fn destination_id(mut self, destination_id: impl Into>) -> Self { + self.destination_id = Some(destination_id.into()); self } - pub fn space_id(mut self, space_id: impl Into>) -> Self { - self.space_id = Some(space_id.into()); - self - } - - pub fn version(mut self, version: impl Into) -> Self { - self.version.version_mut(version.into()); - self - } - - pub fn is_empty(&self) -> bool { - self.relation_type.is_none() && self.from_id.is_none() - } - - pub(crate) fn subquery(&self, node_var: impl Into) -> MatchQuery { - let node_var = node_var.into(); - let random_suffix: String = - rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); - let rel_edge_var = format!("r_{node_var}_{}", random_suffix); - let node_var_to = format!("r_{node_var}_{}_to", random_suffix); - - MatchQuery::new(format!( - "({node_var}) -[{rel_edge_var}:RELATION]-> ({node_var_to})" - )) - // rename to change direction of relation - .r#rename(Rename::new(NamePair::new( - node_var.clone(), - node_var_to.clone(), - ))) - // Apply the version filter to the relation - .r#where(self.version.subquery(&rel_edge_var)) - // Apply the relation_type filter to the relation (if any) - .where_opt( - self.relation_type - .as_ref() - .map(|relation_type| relation_type.subquery(&rel_edge_var, "relation_type", None)), - ) - // Apply the from_id filter to the relation (if any) - .where_opt( - self.from_id - .as_ref() - .map(|from_id| from_id.subquery(&node_var_to, "id", None)), - ) - // Apply the space_id filter to the relation (if any) - .where_opt( - self.space_id - .as_ref() - .map(|space_id| space_id.subquery(&rel_edge_var, "space_id", None)), - ) - } -} - -/// Filter used to: -/// - Filter the relations outgoing from the entity -/// - Filter an entity by its outgoing relations -#[derive(Clone, Debug, Default)] -pub struct TraverseToRelationFilter { - relation_type: Option>, - to_id: Option>, - space_id: Option>, - version: VersionFilter, -} - -impl TraverseToRelationFilter { - pub fn relation_type(mut self, relation_type: impl Into>) -> Self { - self.relation_type = Some(relation_type.into()); - self - } - - pub fn to_id(mut self, to_id: impl Into>) -> Self { - self.to_id = Some(to_id.into()); + pub fn direction(mut self, direction: RelationDirection) -> Self { + self.direction = direction; self } @@ -299,37 +216,42 @@ impl TraverseToRelationFilter { } pub fn is_empty(&self) -> bool { - self.relation_type.is_none() && self.to_id.is_none() + self.relation_type_id.is_none() && self.destination_id.is_none() } pub(crate) fn subquery(&self, node_var: impl Into) -> MatchQuery { - let node_var = node_var.into(); + let node_var_curr = node_var.into(); let random_suffix: String = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); - let rel_edge_var = format!("r_{node_var}_{}", random_suffix); - let node_var_to = format!("r_{node_var}_{}_to", random_suffix); + let rel_edge_var = format!("r_{node_var_curr}_{}", random_suffix); + let node_var_dest = format!("r_{node_var_curr}_{}_to", random_suffix); - MatchQuery::new(format!( - "({node_var}) <-[{rel_edge_var}:RELATION]- ({node_var_to})" - )) + MatchQuery::new(match self.direction { + RelationDirection::From => { + format!("({node_var_curr}) -[{rel_edge_var}:RELATION]-> ({node_var_dest})") + } + RelationDirection::To => { + format!("({node_var_dest}) -[{rel_edge_var}:RELATION]-> ({node_var_curr})") + } + }) // rename to change direction of relation .r#rename(Rename::new(NamePair::new( - node_var.clone(), - node_var_to.clone(), + node_var_curr.clone(), + node_var_dest.clone(), ))) // Apply the version filter to the relation .r#where(self.version.subquery(&rel_edge_var)) // Apply the relation_type filter to the relation (if any) .where_opt( - self.relation_type + self.relation_type_id .as_ref() .map(|relation_type| relation_type.subquery(&rel_edge_var, "relation_type", None)), ) // Apply the from_id filter to the relation (if any) .where_opt( - self.to_id + self.destination_id .as_ref() - .map(|to_id| to_id.subquery(&node_var_to, "id", None)), + .map(|dest_id| dest_id.subquery(&node_var_curr, "id", None)), ) // Apply the space_id filter to the relation (if any) .where_opt( diff --git a/grc20-core/src/mapping/query_utils/mod.rs b/grc20-core/src/mapping/query_utils/mod.rs index af743b5..af27e5a 100644 --- a/grc20-core/src/mapping/query_utils/mod.rs +++ b/grc20-core/src/mapping/query_utils/mod.rs @@ -6,12 +6,14 @@ pub mod order_by; pub mod prop_filter; pub mod query_builder; pub mod query_part; +pub mod relation_direction; pub mod version_filter; pub use attributes_filter::AttributeFilter; pub use order_by::{FieldOrderBy, OrderDirection}; pub use prop_filter::PropFilter; pub use query_part::QueryPart; +pub use relation_direction::RelationDirection; pub use version_filter::VersionFilter; pub trait Query: Sized { diff --git a/grc20-core/src/mapping/query_utils/relation_direction.rs b/grc20-core/src/mapping/query_utils/relation_direction.rs new file mode 100644 index 0000000..d1cc35b --- /dev/null +++ b/grc20-core/src/mapping/query_utils/relation_direction.rs @@ -0,0 +1,6 @@ +#[derive(Clone, Debug, Default)] +pub enum RelationDirection { + From, + #[default] + To, +} diff --git a/mcp-server/resources/search_entity_description.md b/mcp-server/resources/search_entity_description.md index 93cdaba..b9f70f4 100644 --- a/mcp-server/resources/search_entity_description.md +++ b/mcp-server/resources/search_entity_description.md @@ -1,50 +1,74 @@ -This request allows you to search by name for the corresponding Entity in the Knowledge Graph. This will give back the most relevant Entities. +This request allows you to get the Entities from a name/description search and traversal from that query if needed. -ToolCall> search_entity("San Francisco") -ToolResult> + +Example Query: Can you give me information about San Francisco? + +ToolCall> +``` +search_entity({ +"query": "San Francisco" +}) +``` +Tool Result> ``` -[ -{ - "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", - "id": "3qayfdjYyPv1dAYf8gPL5r", - "name": "San Francisco" -}, -{ - "description": null, - "id": "W5ZEpuy3Tij1XSXtJLruQ5", - "name": "SF Bay Area" -}, -{ - "description": null, - "id": "RHoJT3hNVaw7m5fLLtZ8WQ", - "name": "California" -}, -{ - "description": null, - "id": "Sh1qtjr4i92ZD6YGPeu5a2", - "name": "Abundant housing in San Francisco" -}, -{ - "description": null, - "id": "UqLf9fTVKHkDs3LzP9zHpH", - "name": "Public safety in San Francisco" -}, -{ - "description": null, - "id": "BeyiZ6oLqLMaSXiG41Yxtf", - "name": "City" -}, -{ - "description": null, - "id": "D6Wy4bdtdoUrG3PDZceHr", - "name": "City" -}, { - "description": null, - "id": "JWVrgUXmjS75PqNX2hry5q", - "name": "Clean streets in San Francisco" + "entities": [ + { + "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "id": "3qayfdjYyPv1dAYf8gPL5r", + "name": "San Francisco" + }, + { + "description": null, + "id": "W5ZEpuy3Tij1XSXtJLruQ5", + "name": "SF Bay Area" + }, + { + "description": null, + "id": "RHoJT3hNVaw7m5fLLtZ8WQ", + "name": "California" + }, + { + "description": null, + "id": "Sh1qtjr4i92ZD6YGPeu5a2", + "name": "Abundant housing in San Francisco" + } + ] } -] ``` -These Entities can be further queried using their id to get more information. +Another Query: Give me the employees that work at The Graph? + +Work_at id: U1uCAzXsRSTP4vFwo1JwJG +ToolCall> +``` +search_entity({ +"query": "The Graph", +"traversal_filter": { + "relation_type_id": "U1uCAzXsRSTP4vFwo1JwJG", + "direction": "From" +} +}) +``` +ToolResult> +``` +{ + "entities": [ + { + "description": "Founder & CEO of Geo. Cofounder of The Graph, Edge & Node, House of Web3. Building a vibrant decentralized future.", + "id": "9HsfMWYHr9suYdMrtssqiX", + "name": "Yaniv Tal" + }, + { + "description": "Developer Relations Engineer", + "id": "22MGz47c9WHtRiHuSEPkcG", + "name": "Kevin Jones" + }, + { + "description": "Description will go here", + "id": "JYTfEcdmdjiNzBg469gE83", + "name": "Pedro Diogo" + } + ] +} +``` diff --git a/mcp-server/src/input_types.rs b/mcp-server/src/input_types.rs new file mode 100644 index 0000000..4fa59af --- /dev/null +++ b/mcp-server/src/input_types.rs @@ -0,0 +1,50 @@ +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct SearchTraversalInputFilter { + pub query: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub traversal_filter: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct TraversalFilter { + pub direction: RelationDirection, + pub relation_type_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub traversal_filter: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, Clone)] +pub enum RelationDirection { + From, + To, +} + +/// Struct returned by call to `OneOrMany::into_iter()`. +pub struct IntoIter { + // Owned. + next_filter: Option, +} + +/// Implement `IntoIterator` for `TraversalFilter`. +impl IntoIterator for TraversalFilter { + type Item = TraversalFilter; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + next_filter: Some(self), + } + } +} + +/// Implement `Iterator` for `IntoIter`. +impl Iterator for IntoIter { + type Item = TraversalFilter; + + fn next(&mut self) -> Option { + self.next_filter.take().map(|mut current| { + self.next_filter = current.traversal_filter.take().map(|boxed| *boxed); + current + }) + } +} diff --git a/mcp-server/src/lib.rs b/mcp-server/src/lib.rs new file mode 100644 index 0000000..f728dbe --- /dev/null +++ b/mcp-server/src/lib.rs @@ -0,0 +1 @@ +pub mod input_types; diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 97c7bc9..c24b9ec 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -4,12 +4,15 @@ use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{ self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, - utils::{TraverseFromRelationFilter, TraverseToRelationFilter}, + utils::TraverseRelationFilter, + }, + mapping::{ + Attributes, Query, QueryStream, RelationEdge, prop_filter, query_utils::RelationDirection, }, - mapping::{Attributes, Query, QueryStream, RelationEdge, prop_filter}, neo4rs, relation, system_ids, }; use grc20_sdk::models::BaseEntity; +use mcp_server::input_types::{self, SearchTraversalInputFilter}; use rmcp::{ Error as McpError, RoleServer, ServerHandler, model::*, @@ -80,63 +83,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct StructRequest { - pub a: i32, - pub b: i32, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub struct InputFilter { - pub query: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub relation_filter: Option, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub struct RelationFilter { - pub direction: RelationDirection, - pub relation_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub relation_filter: Option>, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub enum RelationDirection { - From, - To, -} - -/// Struct returned by call to `OneOrMany::into_iter()`. -pub struct IntoIter { - // Owned. - next_filter: Option, -} - -/// Implement `IntoIterator` for `RelationFilter`. -impl IntoIterator for RelationFilter { - type Item = RelationFilter; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - IntoIter { - next_filter: Some(self), - } - } -} - -/// Implement `Iterator` for `IntoIter`. -impl Iterator for IntoIter { - type Item = RelationFilter; - - fn next(&mut self) -> Option { - self.next_filter.take().map(|mut current| { - self.next_filter = current.relation_filter.take().map(|boxed| *boxed); - current - }) - } -} - const EMBEDDING_MODEL: EmbeddingModel = EmbeddingModel::AllMiniLML6V2; #[derive(Clone)] @@ -338,20 +284,20 @@ impl KnowledgeGraph { )) } - #[tool(description = "Get entity by it's relations, properties or attributes")] - async fn get_entity_from_relation( + #[tool( + description = "Get entity from a query over the name and traversals based on relation types" + )] + async fn search_entity( &self, #[tool(param)] - #[schemars( - description = "A tuple of the value that is looked for and an optional attribute id, relation id or property id for the value that was provided" - )] - input_filter: InputFilter, + #[schemars(description = "A filter of the relation(s) to traverse from the query")] + search_traversal_filter: SearchTraversalInputFilter, ) -> Result { - tracing::info!("Input filter query: {}", input_filter.query); + tracing::info!("SearchTraversalFilter query: {}", search_traversal_filter.query); let embedding = self .embedding_model - .embed(vec![&input_filter.query], None) + .embed(vec![&search_traversal_filter.query], None) .expect("Failed to get embedding") .pop() .expect("Embedding is empty") @@ -359,8 +305,8 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let results_search = input_filter - .relation_filter + let results_search = search_traversal_filter + .traversal_filter .map(|relation_filter| { relation_filter.into_iter().fold( entity::search_from_restictions::>( @@ -368,17 +314,18 @@ impl KnowledgeGraph { embedding.clone(), ), |query, filter| { - query.filter(match filter.direction { - RelationDirection::From => EntityFilter::default() - .traverse_from_relation( - TraverseFromRelationFilter::default() - .relation_type(filter.relation_id.clone()), - ), - RelationDirection::To => EntityFilter::default().traverse_to_relation( - TraverseToRelationFilter::default() - .relation_type(filter.relation_id.clone()), + query.filter( + EntityFilter::default().traverse_relation( + TraverseRelationFilter::default() + .relation_type_id(filter.relation_type_id) + .direction(match filter.direction { + input_types::RelationDirection::From => { + RelationDirection::From + } + input_types::RelationDirection::To => RelationDirection::To, + }), ), - }) + ) }, ) }) @@ -582,57 +529,6 @@ impl KnowledgeGraph { )) } - #[tool(description = "Search entity by name or attribute value")] - async fn search_entity( - &self, - #[tool(param)] - #[schemars(description = "The value of the attribute or name of an Entity")] - attribute_value: String, - ) -> Result { - let embedding = self - .embedding_model - .embed(vec![&attribute_value], None) - .expect("Failed to get embedding") - .pop() - .expect("Embedding is empty") - .into_iter() - .map(|v| v as f64) - .collect::>(); - - let entities = entity::search::>(&self.neo4j, embedding) - .filter(entity::EntityFilter::default()) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error("get_entity", Some(json!({ "error": e.to_string() }))) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "get_relation_by_id_not_found", - Some(json!({ "error": e.to_string() })), - ) - })?; - - tracing::info!("Found {} entities with given attributes", entities.len()); - - Ok(CallToolResult::success( - entities - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.entity.id(), - "name": result.entity.attributes.name, - "description": result.entity.attributes.description, - })) - .expect("Failed to create JSON content") - }) - .collect(), - )) - } - async fn get_name_of_id(&self, id: String) -> Result { let entity = entity::find_one::>(&self.neo4j, &id) .send() From 7759a290c075ac64a96ad05cfed254ec407e9006 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Tue, 17 Jun 2025 11:50:34 -0400 Subject: [PATCH 17/25] fix fmt --- mcp-server/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index c24b9ec..9e0bb50 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -293,7 +293,10 @@ impl KnowledgeGraph { #[schemars(description = "A filter of the relation(s) to traverse from the query")] search_traversal_filter: SearchTraversalInputFilter, ) -> Result { - tracing::info!("SearchTraversalFilter query: {}", search_traversal_filter.query); + tracing::info!( + "SearchTraversalFilter query: {}", + search_traversal_filter.query + ); let embedding = self .embedding_model From 74e88c81d4ffee80a903af282cb2c103700f569a Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 25 Jun 2025 15:49:38 -0400 Subject: [PATCH 18/25] feat search by query with relation names and search space --- mcp-server/src/main.rs | 333 ++++++++++++++++++++++++++++++++++------- 1 file changed, 277 insertions(+), 56 deletions(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 9e0bb50..8d53f53 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -3,13 +3,14 @@ use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{ - self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, - utils::TraverseRelationFilter, + self, utils::TraverseRelationFilter, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter }, mapping::{ - Attributes, Query, QueryStream, RelationEdge, prop_filter, query_utils::RelationDirection, + prop_filter, query_utils::RelationDirection, triple, Attributes, Query, QueryStream, RelationEdge }, - neo4rs, relation, system_ids, + neo4rs, + relation::{self}, + system_ids, }; use grc20_sdk::models::BaseEntity; use mcp_server::input_types::{self, SearchTraversalInputFilter}; @@ -227,6 +228,66 @@ impl KnowledgeGraph { )) } + #[tool(description = "Search Space")] + async fn search_space( + &self, + #[tool(param)] + #[schemars(description = "The query string to search for space")] + query: String, + ) -> Result { + let embedding = self + .embedding_model + .embed(vec![&query], None) + .expect("Failed to get embedding") + .pop() + .expect("Embedding is empty") + .into_iter() + .map(|v| v as f64) + .collect::>(); + + let results = entity::search::>(&self.neo4j, embedding) + .filter( + entity::EntityFilter::default().relations( + EntityRelationFilter::default() + .relation_type(system_ids::TYPES_ATTRIBUTE) + .to_id(system_ids::SPACE_TYPE), + ), + ) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_space", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_space", + Some(json!({ "error": e.to_string() })), + ) + })?; + + tracing::info!("Found {} results for query '{}'", results.len(), query); + + Ok(CallToolResult::success( + results + .into_iter() + .map(|result| { + Content::json(json!({ + "id": result.entity.id(), + "name": result.entity.attributes.name, + "description": result.entity.attributes.description, + })) + .expect("Failed to create JSON content") + }) + .collect(), + )) + } + #[tool(description = "Search Properties")] async fn search_properties( &self, @@ -308,34 +369,157 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let results_search = search_traversal_filter + let traversal_filters: Vec<_> = search_traversal_filter .traversal_filter - .map(|relation_filter| { - relation_filter.into_iter().fold( - entity::search_from_restictions::>( - &self.neo4j, - embedding.clone(), - ), - |query, filter| { - query.filter( - EntityFilter::default().traverse_relation( - TraverseRelationFilter::default() - .relation_type_id(filter.relation_type_id) - .direction(match filter.direction { - input_types::RelationDirection::From => { - RelationDirection::From - } - input_types::RelationDirection::To => RelationDirection::To, - }), - ), - ) - }, + .map(|relation_filter| relation_filter.into_iter().collect()) + .unwrap_or_default(); + + let results_search = traversal_filters + .into_iter() + .fold( + entity::search_from_restictions::>( + &self.neo4j, + embedding.clone(), + ), + |query, filter| { + query.filter( + EntityFilter::default().traverse_relation( + TraverseRelationFilter::default() + .relation_type_id(filter.relation_type_id) + .direction(match filter.direction { + input_types::RelationDirection::From => RelationDirection::From, + input_types::RelationDirection::To => RelationDirection::To, + }), + ), + ) + }, + ) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_properties", + Some(json!({ "error": e.to_string() })), ) + })?; + + let entities_vec: Vec<_> = results_search + .into_iter() + .map(|result| { + json!({ + "id": result.entity.id(), + "name": result.entity.attributes.name, + "description": result.entity.attributes.description, + }) }) - .unwrap_or(entity::search_from_restictions::>( - &self.neo4j, - embedding, - )) + .collect::>(); + + Ok(CallToolResult::success(vec![ + Content::json(json!({ + "entities": entities_vec, + })) + .expect("Failed to create JSON content"), + ])) + } + + #[tool( + description = "Get entity from a query over the name and traversals based on relation types names" + )] + async fn name_search_entity( + &self, + #[tool(param)] + #[schemars(description = "A filter of the relation(s) to traverse from the query")] + search_traversal_filter: SearchTraversalInputFilter, + ) -> Result { + tracing::info!("SearchTraversalFilter query: {:?}", search_traversal_filter); + + let embedding = self + .embedding_model + .embed(vec![&search_traversal_filter.query], None) + .expect("Failed to get embedding") + .pop() + .expect("Embedding is empty") + .into_iter() + .map(|v| v as f64) + .collect::>(); + + let traversal_filters: Vec> = + match search_traversal_filter.traversal_filter { + Some(traversal_filter) => { + join_all(traversal_filter.into_iter().map(|filter| async move { + let rel_embedding = self + .embedding_model + .embed(vec![&filter.relation_type_id], None) + .expect("Failed to get embedding") + .pop() + .expect("Embedding is empty") + .into_iter() + .map(|v| v as f64) + .collect::>(); + + let rel_results = entity::search::(&self.neo4j, rel_embedding) + .filter( + entity::EntityFilter::default().relations( + EntityRelationFilter::default() + .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) + .to_id(system_ids::RELATION_SCHEMA_TYPE), + ), + ) + .limit(10) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_relation_types", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_relation_types", + Some(json!({ "error": e.to_string() })), + ) + })?; + let relation_ids: Vec = rel_results + .into_iter() + .map(|sem_search| sem_search.entity.id) + .collect(); + Ok(TraverseRelationFilter::default() + .direction(match filter.direction { + input_types::RelationDirection::From => RelationDirection::From, + input_types::RelationDirection::To => RelationDirection::To, + }) + .relation_type_id(prop_filter::value_in(relation_ids))) + })) + .await + .to_vec() + } + None => Vec::new(), + }; + + let results_search = traversal_filters + .into_iter() + .fold( + entity::search_from_restictions::>( + &self.neo4j, + embedding.clone(), + ), + |query, result_ids: Result<_, McpError>| match result_ids { + Ok(ids) => query.filter(EntityFilter::default().traverse_relation(ids)), + Err(_) => query, + }, + ) .limit(10) .send() .await @@ -382,14 +566,19 @@ impl KnowledgeGraph { )] id: String, ) -> Result { - let entity = entity::find_one::>(&self.neo4j, &id) + let entity_attributes = triple::find_many(&self.neo4j).entity_id(prop_filter::value(&id)) .send() .await .map_err(|e| { - McpError::internal_error("get_entity", Some(json!({ "error": e.to_string() }))) + McpError::internal_error("get_entity_info", Some(json!({ "error": e.to_string() }))) })? - .ok_or_else(|| { - McpError::internal_error("entity_not_found", Some(json!({ "id": id }))) + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "get_entity_info", + Some(json!({ "error": e.to_string() })), + ) })?; let out_relations = relation::find_many::>(&self.neo4j) @@ -444,35 +633,23 @@ impl KnowledgeGraph { join_all(relations .into_iter() .map(|result| async move { - Content::json(json!({ + json!({ "relation_id": result.id, "relation_type": self.get_name_of_id(result.relation_type).await.unwrap_or("No relation type".to_string()), "id": if is_inbound {result.from.id.clone()} else {result.to.id.clone()}, "name": self.get_name_of_id(if is_inbound {result.from.id.clone()} else {result.to.id.clone()}).await.unwrap_or("No name".to_string()), - })) - .expect("Failed to create JSON content") + }) })).await.to_vec() }; let inbound_relations = clean_up_relations(in_relations, true).await; let outbound_relations = clean_up_relations(out_relations, false).await; - let id = entity.id().to_string(); - let name = entity - .attributes - .get::(system_ids::NAME_ATTRIBUTE) - .unwrap_or("No name".to_string()); - let description = entity - .attributes - .get::(system_ids::DESCRIPTION_ATTRIBUTE) - .unwrap_or("No description".to_string()); - - let attributes_vec: Vec<_> = join_all(entity.attributes.0.into_iter().map( - |(key, attr)| async { - Content::json(json!({ - "attribute_name": self.get_name_of_id(key).await.unwrap_or("No attribute name".to_string()), - "attribute_value": String::try_from(attr).unwrap_or("No attributes".to_string()), - })) - .expect("Failed to create JSON content") + let attributes_vec: Vec<_> = join_all(entity_attributes.into_iter().map( + |attr| async { + json!({ + "attribute_name": self.get_name_of_id(attr.attribute).await.unwrap_or("No attribute name".to_string()), + "attribute_value": String::try_from(attr.value).unwrap_or("No attributes".to_string()), + }) }, )) .await @@ -481,9 +658,6 @@ impl KnowledgeGraph { Ok(CallToolResult::success(vec![ Content::json(json!({ "id": id, - "name": name, - "description": description, - "types": entity.types, "all_attributes": attributes_vec, "inbound_relations": inbound_relations, "outbound_relations": outbound_relations, @@ -573,6 +747,53 @@ impl ServerHandler for KnowledgeGraph { } Ok(self.get_info()) } + + //TODO: make prompt examples to use on data + async fn list_prompts( + &self, + _request: Option, + _: RequestContext, + ) -> Result { + Ok(ListPromptsResult { + next_cursor: None, + prompts: vec![Prompt::new( + "example_prompt", + Some("This is an example prompt that takes one required argument, message"), + Some(vec![PromptArgument { + name: "message".to_string(), + description: Some("A message to put in the prompt".to_string()), + required: Some(true), + }]), + )], + }) + } + + async fn get_prompt( + &self, + GetPromptRequestParam { name, arguments }: GetPromptRequestParam, + _: RequestContext, + ) -> Result { + match name.as_str() { + "example_prompt" => { + let message = arguments + .and_then(|json| json.get("message")?.as_str().map(|s| s.to_string())) + .ok_or_else(|| { + McpError::invalid_params("No message provided to example_prompt", None) + })?; + + let prompt = + format!("This is an example prompt with your message here: '{message}'"); + Ok(GetPromptResult { + description: None, + messages: vec![PromptMessage { + role: PromptMessageRole::User, + content: PromptMessageContent::text(prompt), + }], + }) + } + _ => Err(McpError::invalid_params("prompt not found", None)), + } + } } #[derive(Debug, Parser)] From 919d3b4e6785d2083b5e4faa7187a8d493ea70e0 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 25 Jun 2025 15:50:10 -0400 Subject: [PATCH 19/25] feat new data for pluralism tests using spaces --- sink/examples/seed_data.rs | 375 ++++++++++++++++++++++++++++++++++--- 1 file changed, 351 insertions(+), 24 deletions(-) diff --git a/sink/examples/seed_data.rs b/sink/examples/seed_data.rs index 48b25f9..d73858d 100644 --- a/sink/examples/seed_data.rs +++ b/sink/examples/seed_data.rs @@ -1,11 +1,9 @@ +use chrono::Local; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use grc20_core::{ - block::BlockMetadata, - entity::EntityNodeRef, - ids, - mapping::{triple, Query, RelationEdge, Triple}, - neo4rs, relation, system_ids, + block::BlockMetadata, entity::EntityNodeRef, ids, indexer_ids, mapping::{triple, Query, RelationEdge, Triple, Value}, neo4rs, relation, system_ids }; +use grc20_sdk::models::space; const EMBEDDING_MODEL: EmbeddingModel = EmbeddingModel::AllMiniLML6V2; @@ -32,17 +30,19 @@ const RUST_ASYNC_WORKSHOP_SIDEEVENT: &str = "QPZnckrRUebWjdwQZTR7Ka"; const RUST_HACKATHON_SIDEEVENT: &str = "ReJ5RRMqTer9qfr87Yjexp"; const JOE_ID: &str = "MpR7wuVWyXV988F5NWZ21r"; const CHRIS_ID: &str = "ScHYh4PpRpyuvY2Ab4Znf5"; -const _: &str = "Mu7ddiBnwZH1LvpDTpKcvq"; -const _: &str = "DVurPdLUZi7Ajfv9BC3ADm"; -const _: &str = "MPxRvh35rnDeRJNEJLU1YF"; -const _: &str = "JjoWPp8LiCKVZiWtE5iZaJ"; -const _: &str = "8bCuTuWqL3dxALLff1Awdb"; -const _: &str = "9Bj46RXQzHQq25WNPY4Lw"; -const _: &str = "RkTkM28NSx3WZuW33vZUjx"; -const _: &str = "Lc9L7StPfXMFGWw45utaTY"; -const _: &str = "G49gECRJmW6BwqHaENF5nS"; -const _: &str = "GfugZRvoWmQhkjMcFJHg49"; -const _: &str = "5bwj7yNukCHoJnW8ksgZY"; +const POLYMTL_ID: &str = "Mu7ddiBnwZH1LvpDTpKcvq"; +const MAUD_COHEN_ID: &str = "DVurPdLUZi7Ajfv9BC3ADm"; +const CIVIL_ENGINEERING_ID: &str = "YEZVCYJTudKVreLEWuxFXV"; +const SOFTWARE_ENGINEERING_ID: &str = "MPxRvh35rnDeRJNEJLU1YF"; +const COMPUTER_ENGINEERING_ID: &str = "JjoWPp8LiCKVZiWtE5iZaJ"; +const MECANICAL_ENGINEERING_ID: &str = "8bCuTuWqL3dxALLff1Awdb"; +const OLIVIER_GENDREAU_ID: &str = "9Bj46RXQzHQq25WNPY4Lw"; +const FR_SPACE_ID: &str = "RkTkM28NSx3WZuW33vZUjx"; +const FR_QC_SPACE_ID: &str = "Lc9L7StPfXMFGWw45utaTY"; +const DIRECTOR_PROP: &str = "G49gECRJmW6BwqHaENF5nS"; +const PROGRAM_TYPE: &str = "GfugZRvoWmQhkjMcFJHg49"; +const SCHOOL_TYPE: &str = "M89C7wwdJVaCW9rAVQpJbY"; +const PROGRAM_PROP: &str = "5bwj7yNukCHoJnW8ksgZY"; const _: &str = "GKXfCXBAJ2oAufgETPcFK7"; const _: &str = "X6q73SFySo5u2BuQrYUxR5"; const _: &str = "S2etHTe7W92QbXz32QWimW"; @@ -80,6 +80,49 @@ async fn main() -> anyhow::Result<()> { reset_db(&neo4j).await?; bootstrap(&neo4j, &embedding_model).await?; + let dt = Local::now(); + + let block = BlockMetadata { cursor: "random_cursor".to_string(), block_number: 0, timestamp:dt.to_utc(), request_id: "request_id".to_string() }; + + create_type( + &neo4j, + &embedding_model, + "Space", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(system_ids::SPACE_TYPE), + None, + ) + .await?; + + space::builder(FR_SPACE_ID, "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c") + .build() + .insert(&neo4j, &block, indexer_ids::INDEXER_SPACE_ID, "0") + .send() + .await?; + + space::builder(FR_QC_SPACE_ID, "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c") + .build() + .insert(&neo4j, &block, indexer_ids::INDEXER_SPACE_ID, "0") + .send() + .await?; + + space::builder(system_ids::ROOT_SPACE_ID, "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c") + .build() + .insert(&neo4j, &block, indexer_ids::INDEXER_SPACE_ID, "0") + .send() + .await?; + + insert_relation(&neo4j, FR_QC_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; + insert_relation(&neo4j, FR_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; + insert_relation(&neo4j, system_ids::ROOT_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; + insert_attribute_with_embedding(&neo4j, &embedding_model, FR_QC_SPACE_ID, system_ids::NAME_ATTRIBUTE, "Quebec", FR_QC_SPACE_ID).await?; + insert_attribute_with_embedding(&neo4j, &embedding_model, FR_QC_SPACE_ID, system_ids::DESCRIPTION_ATTRIBUTE, "The space for Quebec related content", FR_QC_SPACE_ID).await?; + insert_attribute_with_embedding(&neo4j, &embedding_model, FR_SPACE_ID, system_ids::NAME_ATTRIBUTE, "Francophonie", FR_SPACE_ID).await?; + // Create some common types create_type( &neo4j, @@ -91,6 +134,7 @@ async fn main() -> anyhow::Result<()> { system_ids::DESCRIPTION_ATTRIBUTE, ], Some(system_ids::PERSON_TYPE), + None, ) .await?; @@ -104,6 +148,7 @@ async fn main() -> anyhow::Result<()> { system_ids::DESCRIPTION_ATTRIBUTE, ], Some(EVENT_TYPE), + None, ) .await?; @@ -117,6 +162,35 @@ async fn main() -> anyhow::Result<()> { system_ids::DESCRIPTION_ATTRIBUTE, ], Some(CITY_TYPE), + None, + ) + .await?; + + create_type( + &neo4j, + &embedding_model, + "Program", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(PROGRAM_TYPE), + None, + ) + .await?; + + create_type( + &neo4j, + &embedding_model, + "School", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(SCHOOL_TYPE), + None, ) .await?; @@ -127,6 +201,7 @@ async fn main() -> anyhow::Result<()> { system_ids::RELATION_SCHEMA_TYPE, Some(CITY_TYPE), Some(EVENT_LOCATION_PROP), + None, ) .await?; @@ -137,6 +212,7 @@ async fn main() -> anyhow::Result<()> { system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::PERSON_TYPE), Some(SPEAKERS_PROP), + None, ) .await?; @@ -147,9 +223,33 @@ async fn main() -> anyhow::Result<()> { system_ids::RELATION_SCHEMA_TYPE, Some(EVENT_TYPE), Some(SIDE_EVENTS), + None, + ) + .await?; + + create_property( + &neo4j, + &embedding_model, + "Director", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::PERSON_TYPE), + Some(DIRECTOR_PROP), + None, + ) + .await?; + + create_property( + &neo4j, + &embedding_model, + "Program", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::PERSON_TYPE), + Some(PROGRAM_PROP), + None, ) .await?; + // Create person entities create_entity( &neo4j, @@ -166,6 +266,7 @@ async fn main() -> anyhow::Result<()> { ], [], Some(ALICE_ID), + None, ) .await?; @@ -178,6 +279,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(BOB_ID), + None, ) .await?; @@ -190,6 +292,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(CAROL_ID), + None, ) .await?; @@ -202,6 +305,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(DAVE_ID), + None, ) .await?; @@ -214,6 +318,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(JOE_ID), + None, ) .await?; @@ -226,9 +331,127 @@ async fn main() -> anyhow::Result<()> { [], [], Some(CHRIS_ID), + None, + ) + .await?; + + create_entity( + &neo4j, + &embedding_model, + "Maud Cohen", + None, + [system_ids::PERSON_TYPE], + [], + [], + Some(MAUD_COHEN_ID), + None, + ) + .await?; + + create_entity( + &neo4j, + &embedding_model, + "Olivier Gendreau", + None, + [system_ids::PERSON_TYPE], + [], + [], + Some(OLIVIER_GENDREAU_ID), + None, + ) + .await?; + + //Create programs entities + create_entity( + &neo4j, + &embedding_model, + "Software Engineering", + None, + [PROGRAM_TYPE], + [], + [(DIRECTOR_PROP, OLIVIER_GENDREAU_ID)], + Some(SOFTWARE_ENGINEERING_ID), + None, ) .await?; + insert_attribute_with_embedding( + &neo4j, + &embedding_model, + SOFTWARE_ENGINEERING_ID, + system_ids::NAME_ATTRIBUTE, + "Génie logiciel", + FR_SPACE_ID, + ).await?; + + create_entity( + &neo4j, + &embedding_model, + "Computer Engineering", + None, + [PROGRAM_TYPE], + [], + [], + Some(COMPUTER_ENGINEERING_ID), + None, + ) + .await?; + + insert_attribute_with_embedding( + &neo4j, + &embedding_model, + SOFTWARE_ENGINEERING_ID, + system_ids::NAME_ATTRIBUTE, + "Génie informatique", + FR_SPACE_ID, + ).await?; + + create_entity( + &neo4j, + &embedding_model, + "Civil Engineering", + None, + [PROGRAM_TYPE], + [], + [], + Some(CIVIL_ENGINEERING_ID), + None, + ) + .await?; + + create_entity( + &neo4j, + &embedding_model, + "Mecanical Engineering", + None, + [PROGRAM_TYPE], + [], + [], + Some(MECANICAL_ENGINEERING_ID), + None, + ) + .await?; + + create_entity( + &neo4j, + &embedding_model, + "Polytechnique Montreal", + None, + [SCHOOL_TYPE], + [], + [ + (DIRECTOR_PROP, MAUD_COHEN_ID), + (PROGRAM_PROP, CIVIL_ENGINEERING_ID), + (PROGRAM_PROP, SOFTWARE_ENGINEERING_ID), + (PROGRAM_PROP, COMPUTER_ENGINEERING_ID), + (PROGRAM_PROP, MECANICAL_ENGINEERING_ID), + ], + Some(POLYMTL_ID), + None, + ) + .await?; + + // Create city entities create_entity( &neo4j, @@ -239,6 +462,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(SAN_FRANCISCO_ID), + None, ) .await?; @@ -251,6 +475,7 @@ async fn main() -> anyhow::Result<()> { [], [], Some(NEW_YORK_ID), + None, ) .await?; @@ -268,6 +493,7 @@ async fn main() -> anyhow::Result<()> { (SPEAKERS_PROP, JOE_ID), ], Some(RUST_ASYNC_WORKSHOP_SIDEEVENT), + None, ) .await?; @@ -283,6 +509,7 @@ async fn main() -> anyhow::Result<()> { (SPEAKERS_PROP, CHRIS_ID), ], Some(RUST_HACKATHON_SIDEEVENT), + None, ) .await?; @@ -301,6 +528,7 @@ async fn main() -> anyhow::Result<()> { (SIDE_EVENTS, RUST_HACKATHON_SIDEEVENT), // RustConf Hackathon ], Some(RUSTCONF_2023), + None, ) .await?; @@ -317,6 +545,7 @@ async fn main() -> anyhow::Result<()> { (EVENT_LOCATION_PROP, NEW_YORK_ID), // New York ], Some(JSCONF_2024), + None, ) .await?; @@ -427,6 +656,7 @@ pub async fn bootstrap( system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::ATTRIBUTE), Some(system_ids::PROPERTIES), + None, ) .await?; @@ -437,6 +667,7 @@ pub async fn bootstrap( system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::SCHEMA_TYPE), Some(system_ids::TYPES_ATTRIBUTE), + None, ) .await?; @@ -447,6 +678,7 @@ pub async fn bootstrap( system_ids::RELATION_SCHEMA_TYPE, None::<&str>, Some(system_ids::VALUE_TYPE_ATTRIBUTE), + None, ) .await?; @@ -457,6 +689,7 @@ pub async fn bootstrap( system_ids::RELATION_SCHEMA_TYPE, None::<&str>, Some(system_ids::RELATION_TYPE_ATTRIBUTE), + None, ) .await?; @@ -467,6 +700,7 @@ pub async fn bootstrap( system_ids::TEXT, None::<&str>, Some(system_ids::RELATION_INDEX), + None, ) .await?; @@ -477,6 +711,7 @@ pub async fn bootstrap( system_ids::RELATION_SCHEMA_TYPE, Some(system_ids::SCHEMA_TYPE), Some(system_ids::RELATION_TYPE_ATTRIBUTE), + None, ) .await?; @@ -487,6 +722,7 @@ pub async fn bootstrap( system_ids::TEXT, None::<&str>, Some(system_ids::NAME_ATTRIBUTE), + None, ) .await?; @@ -497,6 +733,7 @@ pub async fn bootstrap( system_ids::TEXT, None::<&str>, Some(system_ids::DESCRIPTION_ATTRIBUTE), + None, ) .await?; @@ -513,6 +750,7 @@ pub async fn bootstrap( system_ids::DESCRIPTION_ATTRIBUTE, ], Some(system_ids::SCHEMA_TYPE), + None, ) .await?; @@ -523,6 +761,7 @@ pub async fn bootstrap( [system_ids::RELATION_SCHEMA_TYPE], [system_ids::RELATION_VALUE_RELATIONSHIP_TYPE], Some(system_ids::RELATION_SCHEMA_TYPE), + None, ) .await?; @@ -537,6 +776,7 @@ pub async fn bootstrap( system_ids::DESCRIPTION_ATTRIBUTE, ], Some(system_ids::ATTRIBUTE), + None, ) .await?; @@ -550,6 +790,7 @@ pub async fn bootstrap( system_ids::RELATION_INDEX, ], Some(system_ids::RELATION_TYPE), + None, ) .await?; @@ -565,13 +806,17 @@ pub async fn create_entity( properties: impl IntoIterator, relations: impl IntoIterator, id: Option<&str>, + space_id: Option, ) -> anyhow::Result { let block = BlockMetadata::default(); let entity_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); let name = name.into(); + + let space_id = space_id.as_deref().unwrap_or(system_ids::ROOT_SPACE_ID); + // Set: Entity.name - triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples(vec![Triple::with_embedding( &entity_id, system_ids::NAME_ATTRIBUTE, @@ -590,7 +835,7 @@ pub async fn create_entity( // Set: Entity.description if let Some(description) = description { - triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples(vec![Triple::new( &entity_id, system_ids::DESCRIPTION_ATTRIBUTE, @@ -604,7 +849,7 @@ pub async fn create_entity( set_types(neo4j, &entity_id, types).await?; // Set: Entity.* - triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples( properties .into_iter() @@ -617,7 +862,7 @@ pub async fn create_entity( relation::insert_many::>( neo4j, &block, - system_ids::ROOT_SPACE_ID, + space_id, DEFAULT_VERSION, ) .relations(relations.into_iter().map(|(relation_type, target_id)| { @@ -635,6 +880,81 @@ pub async fn create_entity( Ok(entity_id) } +pub async fn insert_attribute( + neo4j: &neo4rs::Graph, + entity_id: impl Into, + attribute_id: impl Into, + attribute_value: impl Into, + space_id: impl Into, +) -> anyhow::Result { + + let block = BlockMetadata::default(); + let attribute_id = attribute_id.into(); + let attribute_value = attribute_value.into(); + let space_id = space_id.into(); + let entity_id = entity_id.into(); + + triple::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, Triple::new(entity_id.clone(), attribute_id, attribute_value)) + .send() + .await?; + Ok(entity_id) +} + +pub async fn insert_relation( + neo4j: &neo4rs::Graph, + entity_from_id: impl Into, + relation_id: impl Into, + entity_to_id: impl Into, + space_id: impl Into, +) -> anyhow::Result { + + let block = BlockMetadata::default(); + let entity_from_id = entity_from_id.into(); + let relation_id = relation_id.into(); + let space_id = space_id.into(); + let entity_to_id = entity_to_id.into(); + + relation::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, RelationEdge::new("id".to_string(), entity_from_id, entity_to_id, relation_id.clone(), Value::text(relation_id.clone()))) + .send() + .await?; + Ok(relation_id) +} + +pub async fn insert_attribute_with_embedding( + neo4j: &neo4rs::Graph, + embedding_model: &TextEmbedding, + entity_id: impl Into, + attribute_id: impl Into, + attribute_value: impl Into, + space_id: impl Into, +) -> anyhow::Result { + + let block = BlockMetadata::default(); + let attribute_id = attribute_id.into(); + let attribute_value = attribute_value.into(); + let space_id = space_id.into(); + let entity_id = entity_id.into(); + + triple::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, + Triple::with_embedding( + &entity_id, + attribute_id, + attribute_value.clone(), + embedding_model + .embed(vec![attribute_value], Some(1)) + .unwrap_or(vec![Vec::::new()]) + .get(0) + .unwrap_or(&Vec::::new()) + .iter() + .map(|&x| x as f64) + .collect() + )) + .send() + .await?; + Ok(entity_id) +} + + /// Creates a type with the given name, types, and properties. pub async fn create_type( neo4j: &neo4rs::Graph, @@ -643,6 +963,7 @@ pub async fn create_type( types: impl IntoIterator, properties: impl IntoIterator, id: Option<&str>, + space_id: Option, ) -> anyhow::Result { let block = BlockMetadata::default(); let type_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); @@ -653,8 +974,11 @@ pub async fn create_type( types_vec.push(system_ids::SCHEMA_TYPE); } + let space_id = space_id.as_deref().unwrap_or(system_ids::ROOT_SPACE_ID); + + // Set: Type.name - triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_TYPE, DEFAULT_VERSION) + triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples(vec![Triple::with_embedding( &type_id, system_ids::NAME_ATTRIBUTE, @@ -678,7 +1002,7 @@ pub async fn create_type( relation::insert_many::>( neo4j, &block, - system_ids::ROOT_SPACE_ID, + space_id, DEFAULT_VERSION, ) .relations(properties.into_iter().map(|property_id| { @@ -706,14 +1030,17 @@ pub async fn create_property( value_type: impl Into, relation_value_type: Option>, id: Option>, + space_id: Option, ) -> anyhow::Result { let block = BlockMetadata::default(); let property_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); let string_name = name.into(); + let space_id = space_id.as_deref().unwrap_or(system_ids::ROOT_SPACE_ID); + // Set: Property.name - triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples(vec![Triple::with_embedding( &property_id, system_ids::NAME_ATTRIBUTE, @@ -734,7 +1061,7 @@ pub async fn create_property( relation::insert_one::>( neo4j, &block, - system_ids::ROOT_SPACE_ID, + space_id, DEFAULT_VERSION, RelationEdge::new( ids::create_geo_id(), From d9d92a30c3e30099653d233495367c25697a3559 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 25 Jun 2025 15:51:36 -0400 Subject: [PATCH 20/25] fix formatting and imports --- mcp-server/src/main.rs | 23 ++--- sink/examples/seed_data.rs | 206 ++++++++++++++++++++++++------------- 2 files changed, 143 insertions(+), 86 deletions(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 8d53f53..4f50a20 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -3,10 +3,11 @@ use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::{TryStreamExt, future::join_all}; use grc20_core::{ entity::{ - self, utils::TraverseRelationFilter, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter + self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, + utils::TraverseRelationFilter, }, mapping::{ - prop_filter, query_utils::RelationDirection, triple, Attributes, Query, QueryStream, RelationEdge + Query, QueryStream, RelationEdge, prop_filter, query_utils::RelationDirection, triple, }, neo4rs, relation::{self}, @@ -257,18 +258,12 @@ impl KnowledgeGraph { .send() .await .map_err(|e| { - McpError::internal_error( - "search_space", - Some(json!({ "error": e.to_string() })), - ) + McpError::internal_error("search_space", Some(json!({ "error": e.to_string() }))) })? .try_collect::>() .await .map_err(|e| { - McpError::internal_error( - "search_space", - Some(json!({ "error": e.to_string() })), - ) + McpError::internal_error("search_space", Some(json!({ "error": e.to_string() }))) })?; tracing::info!("Found {} results for query '{}'", results.len(), query); @@ -566,7 +561,8 @@ impl KnowledgeGraph { )] id: String, ) -> Result { - let entity_attributes = triple::find_many(&self.neo4j).entity_id(prop_filter::value(&id)) + let entity_attributes = triple::find_many(&self.neo4j) + .entity_id(prop_filter::value(&id)) .send() .await .map_err(|e| { @@ -575,10 +571,7 @@ impl KnowledgeGraph { .try_collect::>() .await .map_err(|e| { - McpError::internal_error( - "get_entity_info", - Some(json!({ "error": e.to_string() })), - ) + McpError::internal_error("get_entity_info", Some(json!({ "error": e.to_string() }))) })?; let out_relations = relation::find_many::>(&self.neo4j) diff --git a/sink/examples/seed_data.rs b/sink/examples/seed_data.rs index d73858d..d5640d3 100644 --- a/sink/examples/seed_data.rs +++ b/sink/examples/seed_data.rs @@ -1,7 +1,11 @@ use chrono::Local; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use grc20_core::{ - block::BlockMetadata, entity::EntityNodeRef, ids, indexer_ids, mapping::{triple, Query, RelationEdge, Triple, Value}, neo4rs, relation, system_ids + block::BlockMetadata, + entity::EntityNodeRef, + ids, indexer_ids, + mapping::{triple, Query, RelationEdge, Triple, Value}, + neo4rs, relation, system_ids, }; use grc20_sdk::models::space; @@ -82,7 +86,12 @@ async fn main() -> anyhow::Result<()> { let dt = Local::now(); - let block = BlockMetadata { cursor: "random_cursor".to_string(), block_number: 0, timestamp:dt.to_utc(), request_id: "request_id".to_string() }; + let block = BlockMetadata { + cursor: "random_cursor".to_string(), + block_number: 0, + timestamp: dt.to_utc(), + request_id: "request_id".to_string(), + }; create_type( &neo4j, @@ -110,18 +119,66 @@ async fn main() -> anyhow::Result<()> { .send() .await?; - space::builder(system_ids::ROOT_SPACE_ID, "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c") - .build() - .insert(&neo4j, &block, indexer_ids::INDEXER_SPACE_ID, "0") - .send() - .await?; + space::builder( + system_ids::ROOT_SPACE_ID, + "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + ) + .build() + .insert(&neo4j, &block, indexer_ids::INDEXER_SPACE_ID, "0") + .send() + .await?; - insert_relation(&neo4j, FR_QC_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; - insert_relation(&neo4j, FR_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; - insert_relation(&neo4j, system_ids::ROOT_SPACE_ID, system_ids::TYPES_ATTRIBUTE, system_ids::SPACE_TYPE, indexer_ids::INDEXER_SPACE_ID).await?; - insert_attribute_with_embedding(&neo4j, &embedding_model, FR_QC_SPACE_ID, system_ids::NAME_ATTRIBUTE, "Quebec", FR_QC_SPACE_ID).await?; - insert_attribute_with_embedding(&neo4j, &embedding_model, FR_QC_SPACE_ID, system_ids::DESCRIPTION_ATTRIBUTE, "The space for Quebec related content", FR_QC_SPACE_ID).await?; - insert_attribute_with_embedding(&neo4j, &embedding_model, FR_SPACE_ID, system_ids::NAME_ATTRIBUTE, "Francophonie", FR_SPACE_ID).await?; + insert_relation( + &neo4j, + FR_QC_SPACE_ID, + system_ids::TYPES_ATTRIBUTE, + system_ids::SPACE_TYPE, + indexer_ids::INDEXER_SPACE_ID, + ) + .await?; + insert_relation( + &neo4j, + FR_SPACE_ID, + system_ids::TYPES_ATTRIBUTE, + system_ids::SPACE_TYPE, + indexer_ids::INDEXER_SPACE_ID, + ) + .await?; + insert_relation( + &neo4j, + system_ids::ROOT_SPACE_ID, + system_ids::TYPES_ATTRIBUTE, + system_ids::SPACE_TYPE, + indexer_ids::INDEXER_SPACE_ID, + ) + .await?; + insert_attribute_with_embedding( + &neo4j, + &embedding_model, + FR_QC_SPACE_ID, + system_ids::NAME_ATTRIBUTE, + "Quebec", + FR_QC_SPACE_ID, + ) + .await?; + insert_attribute_with_embedding( + &neo4j, + &embedding_model, + FR_QC_SPACE_ID, + system_ids::DESCRIPTION_ATTRIBUTE, + "The space for Quebec related content", + FR_QC_SPACE_ID, + ) + .await?; + insert_attribute_with_embedding( + &neo4j, + &embedding_model, + FR_SPACE_ID, + system_ids::NAME_ATTRIBUTE, + "Francophonie", + FR_SPACE_ID, + ) + .await?; // Create some common types create_type( @@ -249,7 +306,6 @@ async fn main() -> anyhow::Result<()> { ) .await?; - // Create person entities create_entity( &neo4j, @@ -382,7 +438,8 @@ async fn main() -> anyhow::Result<()> { system_ids::NAME_ATTRIBUTE, "Génie logiciel", FR_SPACE_ID, - ).await?; + ) + .await?; create_entity( &neo4j, @@ -404,7 +461,8 @@ async fn main() -> anyhow::Result<()> { system_ids::NAME_ATTRIBUTE, "Génie informatique", FR_SPACE_ID, - ).await?; + ) + .await?; create_entity( &neo4j, @@ -451,7 +509,6 @@ async fn main() -> anyhow::Result<()> { ) .await?; - // Create city entities create_entity( &neo4j, @@ -812,7 +869,6 @@ pub async fn create_entity( let entity_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); let name = name.into(); - let space_id = space_id.as_deref().unwrap_or(system_ids::ROOT_SPACE_ID); // Set: Entity.name @@ -859,23 +915,18 @@ pub async fn create_entity( .await?; // Set: Entity > RELATIONS > Relation[] - relation::insert_many::>( - neo4j, - &block, - space_id, - DEFAULT_VERSION, - ) - .relations(relations.into_iter().map(|(relation_type, target_id)| { - RelationEdge::new( - ids::create_geo_id(), - &entity_id, - target_id, - relation_type, - "0", - ) - })) - .send() - .await?; + relation::insert_many::>(neo4j, &block, space_id, DEFAULT_VERSION) + .relations(relations.into_iter().map(|(relation_type, target_id)| { + RelationEdge::new( + ids::create_geo_id(), + &entity_id, + target_id, + relation_type, + "0", + ) + })) + .send() + .await?; Ok(entity_id) } @@ -887,16 +938,21 @@ pub async fn insert_attribute( attribute_value: impl Into, space_id: impl Into, ) -> anyhow::Result { - let block = BlockMetadata::default(); let attribute_id = attribute_id.into(); let attribute_value = attribute_value.into(); let space_id = space_id.into(); let entity_id = entity_id.into(); - - triple::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, Triple::new(entity_id.clone(), attribute_id, attribute_value)) - .send() - .await?; + + triple::insert_one( + neo4j, + &block, + space_id, + DEFAULT_VERSION, + Triple::new(entity_id.clone(), attribute_id, attribute_value), + ) + .send() + .await?; Ok(entity_id) } @@ -907,16 +963,27 @@ pub async fn insert_relation( entity_to_id: impl Into, space_id: impl Into, ) -> anyhow::Result { - let block = BlockMetadata::default(); let entity_from_id = entity_from_id.into(); let relation_id = relation_id.into(); let space_id = space_id.into(); let entity_to_id = entity_to_id.into(); - - relation::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, RelationEdge::new("id".to_string(), entity_from_id, entity_to_id, relation_id.clone(), Value::text(relation_id.clone()))) - .send() - .await?; + + relation::insert_one( + neo4j, + &block, + space_id, + DEFAULT_VERSION, + RelationEdge::new( + "id".to_string(), + entity_from_id, + entity_to_id, + relation_id.clone(), + Value::text(relation_id.clone()), + ), + ) + .send() + .await?; Ok(relation_id) } @@ -928,14 +995,17 @@ pub async fn insert_attribute_with_embedding( attribute_value: impl Into, space_id: impl Into, ) -> anyhow::Result { - let block = BlockMetadata::default(); let attribute_id = attribute_id.into(); let attribute_value = attribute_value.into(); let space_id = space_id.into(); let entity_id = entity_id.into(); - - triple::insert_one(neo4j, &block, space_id, DEFAULT_VERSION, + + triple::insert_one( + neo4j, + &block, + space_id, + DEFAULT_VERSION, Triple::with_embedding( &entity_id, attribute_id, @@ -947,14 +1017,14 @@ pub async fn insert_attribute_with_embedding( .unwrap_or(&Vec::::new()) .iter() .map(|&x| x as f64) - .collect() - )) - .send() - .await?; + .collect(), + ), + ) + .send() + .await?; Ok(entity_id) } - /// Creates a type with the given name, types, and properties. pub async fn create_type( neo4j: &neo4rs::Graph, @@ -976,7 +1046,6 @@ pub async fn create_type( let space_id = space_id.as_deref().unwrap_or(system_ids::ROOT_SPACE_ID); - // Set: Type.name triple::insert_many(neo4j, &block, space_id, DEFAULT_VERSION) .triples(vec![Triple::with_embedding( @@ -999,23 +1068,18 @@ pub async fn create_type( set_types(neo4j, &type_id, types_vec).await?; // Set: Type > PROPERTIES > Property[] - relation::insert_many::>( - neo4j, - &block, - space_id, - DEFAULT_VERSION, - ) - .relations(properties.into_iter().map(|property_id| { - RelationEdge::new( - ids::create_geo_id(), - &type_id, - system_ids::PROPERTIES, - property_id, - "0", - ) - })) - .send() - .await?; + relation::insert_many::>(neo4j, &block, space_id, DEFAULT_VERSION) + .relations(properties.into_iter().map(|property_id| { + RelationEdge::new( + ids::create_geo_id(), + &type_id, + system_ids::PROPERTIES, + property_id, + "0", + ) + })) + .send() + .await?; Ok(type_id) } From 7d72340e7b5755a12f7e1fd422a7ef22a2f19f8f Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 2 Jul 2025 11:40:11 -0400 Subject: [PATCH 21/25] feat triples are use for searches --- mcp-server/src/main.rs | 412 +++++++++++++++++++++++++---------------- 1 file changed, 257 insertions(+), 155 deletions(-) diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 4f50a20..104d823 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -1,16 +1,17 @@ use clap::{Args, Parser}; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; -use futures::{TryStreamExt, future::join_all}; +use futures::{TryStreamExt, future::join_all, pin_mut}; use grc20_core::{ entity::{ - self, Entity, EntityFilter, EntityNode, EntityRelationFilter, TypesFilter, - utils::TraverseRelationFilter, + self, Entity, EntityFilter, EntityNode, EntityRelationFilter, utils::TraverseRelationFilter, }, mapping::{ - Query, QueryStream, RelationEdge, prop_filter, query_utils::RelationDirection, triple, + Query, QueryStream, RelationEdge, Triple, prop_filter, + query_utils::RelationDirection, + triple::{self, SemanticSearchResult}, }, neo4rs, - relation::{self}, + relation::{self, RelationFilter}, system_ids, }; use grc20_sdk::models::BaseEntity; @@ -22,8 +23,8 @@ use rmcp::{ tool, transport::sse_server::{SseServer, SseServerConfig}, }; -use serde_json::json; -use std::sync::Arc; +use serde_json::{Value, json}; +use std::{collections::HashSet, sync::Arc}; use tracing_subscriber::{ layer::SubscriberExt, util::SubscriberInitExt, @@ -112,13 +113,11 @@ impl KnowledgeGraph { RawResource::new(uri, name.to_string()).no_annotation() } - #[tool(description = "Search Types")] - async fn search_types( + async fn search( &self, - #[tool(param)] - #[schemars(description = "The query string to search for types")] query: String, - ) -> Result { + limit: Option, + ) -> Result, McpError> { let embedding = self .embedding_model .embed(vec![&query], None) @@ -129,12 +128,10 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let results = entity::search::>(&self.neo4j, embedding) - .filter( - entity::EntityFilter::default() - .relations(TypesFilter::default().r#type(system_ids::SCHEMA_TYPE)), - ) - .limit(10) + let limit = limit.unwrap_or(10); + + let semantic_search_triples = triple::search(&self.neo4j, embedding) + .limit(limit) .send() .await .map_err(|e| { @@ -151,21 +148,110 @@ impl KnowledgeGraph { Some(json!({ "error": e.to_string() })), ) })?; + Ok(semantic_search_triples) + } + + async fn get_ids_from_search( + &self, + search_triples: Vec, + create_relation_filter: impl Fn(SemanticSearchResult) -> RelationFilter, + ) -> Result, McpError> { + let mut seen_ids: HashSet = HashSet::new(); + let mut result_ids: Vec = Vec::new(); + + for semantic_search_triple in search_triples { + let filtered_for_types = relation::find_many::>(&self.neo4j) + .filter(create_relation_filter(semantic_search_triple)) + .send() + .await; + + //We only need to get the first relation since they would share the same entity id + if let Ok(stream) = filtered_for_types { + pin_mut!(stream); + if let Some(edge) = stream.try_next().await.ok().flatten() { + let id = edge.from.id; + if seen_ids.insert(id.clone()) { + result_ids.push(id); + } + } + } + } + Ok(result_ids) + } + + async fn format_triples_detailled( + &self, + triples: Result, ErrorData>, + ) -> Vec { + if let Ok(triples) = triples { + join_all(triples.into_iter().map(|triple| async move {json!({ + "entity_id": triple.entity, + "attribute_name": self.get_name_of_id(triple.attribute).await.unwrap_or("No attribute name".to_string()), + "attribute_value": String::try_from(triple.value).unwrap_or("No value".to_string()) + })})).await.to_vec() + } else { + Vec::new() + } + } + + #[tool(description = "Search Types")] + async fn search_types( + &self, + #[tool(param)] + #[schemars(description = "The query string to search for types")] + query: String, + ) -> Result { + let semantic_search_triples = self.search(query, Some(10)).await.unwrap_or_default(); - tracing::info!("Found {} results for query '{}'", results.len(), query); + let create_relation_filter = |search_result: SemanticSearchResult| { + RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(search_result.triple.entity))) + .relation_type( + EntityFilter::default().id(prop_filter::value(system_ids::TYPES_ATTRIBUTE)), + ) + .to_(EntityFilter::default().id(prop_filter::value(system_ids::SCHEMA_TYPE))) + }; + + let result_types = self + .get_ids_from_search(semantic_search_triples, &create_relation_filter) + .await + .unwrap_or_default(); + + let entities: Vec, McpError>> = + join_all(result_types.into_iter().map(|id| async { + triple::find_many(&self.neo4j) + .entity_id(prop_filter::value(id)) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + }) + })) + .await + .to_vec(); Ok(CallToolResult::success( - results - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.entity.id(), - "name": result.entity.attributes.name, - "description": result.entity.attributes.description, - })) - .expect("Failed to create JSON content") - }) - .collect(), + join_all( + entities + .into_iter() + .map(|result: Result, _>| async { + Content::json(self.format_triples_detailled(result).await) + .expect("Failed to create JSON content") + }), + ) + .await + .to_vec(), )) } @@ -176,56 +262,61 @@ impl KnowledgeGraph { #[schemars(description = "The query string to search for relation types")] query: String, ) -> Result { - let embedding = self - .embedding_model - .embed(vec![&query], None) - .expect("Failed to get embedding") - .pop() - .expect("Embedding is empty") - .into_iter() - .map(|v| v as f64) - .collect::>(); - - let results = entity::search::>(&self.neo4j, embedding) - .filter( - entity::EntityFilter::default().relations( - EntityRelationFilter::default() - .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) - .to_id(system_ids::RELATION_SCHEMA_TYPE), - ), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "search_relation_types", - Some(json!({ "error": e.to_string() })), + let semantic_search_triples = self.search(query, Some(10)).await.unwrap_or_default(); + + let create_relation_filter = |search_result: SemanticSearchResult| { + RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(search_result.triple.entity))) + .relation_type( + EntityFilter::default() + .id(prop_filter::value(system_ids::RELATION_SCHEMA_TYPE)), ) - })? - .try_collect::>() - .await - .map_err(|e| { - McpError::internal_error( - "search_relation_types", - Some(json!({ "error": e.to_string() })), + .to_( + EntityFilter::default() + .id(prop_filter::value(system_ids::VALUE_TYPE_ATTRIBUTE)), ) - })?; + }; - tracing::info!("Found {} results for query '{}'", results.len(), query); + let result_types = self + .get_ids_from_search(semantic_search_triples, &create_relation_filter) + .await + .unwrap_or_default(); + + let entities: Vec, McpError>> = + join_all(result_types.into_iter().map(|id| async { + triple::find_many(&self.neo4j) + .entity_id(prop_filter::value(id)) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + }) + })) + .await + .to_vec(); Ok(CallToolResult::success( - results - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.entity.id(), - "name": result.entity.attributes.name, - "description": result.entity.attributes.description, - })) - .expect("Failed to create JSON content") - }) - .collect(), + join_all( + entities + .into_iter() + .map(|result: Result, _>| async { + Content::json(self.format_triples_detailled(result).await) + .expect("Failed to create JSON content") + }), + ) + .await + .to_vec(), )) } @@ -236,50 +327,57 @@ impl KnowledgeGraph { #[schemars(description = "The query string to search for space")] query: String, ) -> Result { - let embedding = self - .embedding_model - .embed(vec![&query], None) - .expect("Failed to get embedding") - .pop() - .expect("Embedding is empty") - .into_iter() - .map(|v| v as f64) - .collect::>(); + let semantic_search_triples = self.search(query, Some(10)).await.unwrap_or_default(); - let results = entity::search::>(&self.neo4j, embedding) - .filter( - entity::EntityFilter::default().relations( - EntityRelationFilter::default() - .relation_type(system_ids::TYPES_ATTRIBUTE) - .to_id(system_ids::SPACE_TYPE), - ), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error("search_space", Some(json!({ "error": e.to_string() }))) - })? - .try_collect::>() + let create_relation_filter = |search_result: SemanticSearchResult| { + RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(search_result.triple.entity))) + .relation_type( + EntityFilter::default().id(prop_filter::value(system_ids::TYPES_ATTRIBUTE)), + ) + .to_(EntityFilter::default().id(prop_filter::value(system_ids::SPACE_TYPE))) + }; + + let result_types = self + .get_ids_from_search(semantic_search_triples, &create_relation_filter) .await - .map_err(|e| { - McpError::internal_error("search_space", Some(json!({ "error": e.to_string() }))) - })?; + .unwrap_or_default(); - tracing::info!("Found {} results for query '{}'", results.len(), query); + let entities: Vec, McpError>> = + join_all(result_types.into_iter().map(|id| async { + triple::find_many(&self.neo4j) + .entity_id(prop_filter::value(id)) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + }) + })) + .await + .to_vec(); Ok(CallToolResult::success( - results - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.entity.id(), - "name": result.entity.attributes.name, - "description": result.entity.attributes.description, - })) - .expect("Failed to create JSON content") - }) - .collect(), + join_all( + entities + .into_iter() + .map(|result: Result, _>| async { + Content::json(self.format_triples_detailled(result).await) + .expect("Failed to create JSON content") + }), + ) + .await + .to_vec(), )) } @@ -290,53 +388,57 @@ impl KnowledgeGraph { #[schemars(description = "The query string to search for properties")] query: String, ) -> Result { - let embedding = self - .embedding_model - .embed(vec![&query], None) - .expect("Failed to get embedding") - .pop() - .expect("Embedding is empty") - .into_iter() - .map(|v| v as f64) - .collect::>(); + let semantic_search_triples = self.search(query, Some(10)).await.unwrap_or_default(); - let results = entity::search::>(&self.neo4j, embedding) - .filter( - entity::EntityFilter::default() - .relations(TypesFilter::default().r#type(system_ids::ATTRIBUTE)), - ) - .limit(10) - .send() - .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), + let create_relation_filter = |search_result: SemanticSearchResult| { + RelationFilter::default() + .from_(EntityFilter::default().id(prop_filter::value(search_result.triple.entity))) + .relation_type( + EntityFilter::default().id(prop_filter::value(system_ids::TYPES_ATTRIBUTE)), ) - })? - .try_collect::>() + .to_(EntityFilter::default().id(prop_filter::value(system_ids::ATTRIBUTE))) + }; + + let result_types = self + .get_ids_from_search(semantic_search_triples, &create_relation_filter) .await - .map_err(|e| { - McpError::internal_error( - "search_properties", - Some(json!({ "error": e.to_string() })), - ) - })?; + .unwrap_or_default(); - tracing::info!("Found {} results for query '{}'", results.len(), query); + let entities: Vec, McpError>> = + join_all(result_types.into_iter().map(|id| async { + triple::find_many(&self.neo4j) + .entity_id(prop_filter::value(id)) + .send() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + })? + .try_collect::>() + .await + .map_err(|e| { + McpError::internal_error( + "search_types_failed", + Some(json!({ "error": e.to_string() })), + ) + }) + })) + .await + .to_vec(); Ok(CallToolResult::success( - results - .into_iter() - .map(|result| { - Content::json(json!({ - "id": result.entity.id(), - "name": result.entity.attributes.name, - "description": result.entity.attributes.description, - })) - .expect("Failed to create JSON content") - }) - .collect(), + join_all( + entities + .into_iter() + .map(|result: Result, _>| async { + Content::json(self.format_triples_detailled(result).await) + .expect("Failed to create JSON content") + }), + ) + .await + .to_vec(), )) } From a5f4ffdf13b1d7bd15eb0dadd52d64a61305da00 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 2 Jul 2025 12:48:09 -0400 Subject: [PATCH 22/25] fix adapt descriptions --- .../resources/get_entity_info_description.md | 111 +++++++----- ..._relations_between_entities_description.md | 44 ++--- mcp-server/resources/instructions.md | 171 ++++++++++++++---- .../name_search_entity_description.md | 81 +++++++++ .../resources/search_entity_description.md | 30 +++ .../search_properties_description.md | 50 ++--- .../search_relation_type_description.md | 72 ++++---- .../resources/search_space_description.md | 40 ++++ .../resources/search_type_description.md | 71 +++----- mcp-server/src/main.rs | 4 +- 10 files changed, 455 insertions(+), 219 deletions(-) create mode 100644 mcp-server/resources/name_search_entity_description.md create mode 100644 mcp-server/resources/search_space_description.md diff --git a/mcp-server/resources/get_entity_info_description.md b/mcp-server/resources/get_entity_info_description.md index beb5416..b7ed047 100644 --- a/mcp-server/resources/get_entity_info_description.md +++ b/mcp-server/resources/get_entity_info_description.md @@ -8,89 +8,114 @@ ToolResult> { "all_attributes": [ { - "text": "{\"attribute_name\":\"Description\",\"attribute_value\":\"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.\"}", - "type": "text" + "attribute_name": "Description", + "attribute_value": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District." }, { - "text": "{\"attribute_name\":\"Name\",\"attribute_value\":\"San Francisco\"}", - "type": "text" + "attribute_name": "Name", + "attribute_value": "San Francisco" } ], - "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", "id": "3qayfdjYyPv1dAYf8gPL5r", "inbound_relations": [ { - "text": "{\"from_id\":\"NAMA1uDMzBQTvPYV9N92BV\",\"relation_id\":\"8ESicJHiNJ28VGL5u34A5q\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "NAMA1uDMzBQTvPYV9N92BV", + "name": "SF Mayor Lurie launching police task force to counter crime in core downtown areas", + "relation_id": "8ESicJHiNJ28VGL5u34A5q", + "relation_type": "Related spaces" }, { - "text": "{\"from_id\":\"6wAoNdGVbweKi2JRPZP4bX\",\"relation_id\":\"TH5Tu5Y5nacvREvAQRvcR2\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "6wAoNdGVbweKi2JRPZP4bX", + "name": "San Francisco Independent Film Festival", + "relation_id": "TH5Tu5Y5nacvREvAQRvcR2", + "relation_type": "Related spaces" }, { - "text": "{\"from_id\":\"8VCHYDURDStwuTCUBjWLQa\",\"relation_id\":\"KPTqdNpCusxfM37KbKPX8w\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "8VCHYDURDStwuTCUBjWLQa", + "name": "Product Engineer at Geo", + "relation_id": "KPTqdNpCusxfM37KbKPX8w", + "relation_type": "Related spaces" }, { - "text": "{\"from_id\":\"NcQ3h9jeJSavVd8iFsUxvD\",\"relation_id\":\"AqpNtJ3XxaY4fqRCyoXbdt\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "NcQ3h9jeJSavVd8iFsUxvD", + "name": "Senior Civil Engineer @ Golden Gate Bridge, Highway & Transportation District", + "relation_id": "AqpNtJ3XxaY4fqRCyoXbdt", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"4ojV4dS1pV2tRnzXTpcMKJ\",\"relation_id\":\"3AX4j43nywT5eBRV3s6AXi\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "4ojV4dS1pV2tRnzXTpcMKJ", + "name": "Senior Plan Check Engineer (FT - Hybrid) @ CSG Consultants, Inc.", + "relation_id": "3AX4j43nywT5eBRV3s6AXi", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"QoakYWCuv85FVuYdSmonxr\",\"relation_id\":\"8GEF1i3LK4Z56THjE8dVku\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "QoakYWCuv85FVuYdSmonxr", + "name": "Senior Civil Engineer - Land Development (FT - Hybrid) @ CSG Consultants, Inc.", + "relation_id": "8GEF1i3LK4Z56THjE8dVku", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"JuV7jLoypebzLhkma6oZoU\",\"relation_id\":\"46aBsQyBq15DimJ2i1DX4a\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "JuV7jLoypebzLhkma6oZoU", + "name": "Lead Django Backend Engineer @ Textme Inc", + "relation_id": "46aBsQyBq15DimJ2i1DX4a", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"RTmcYhLVmmfgUn9L3D1J3y\",\"relation_id\":\"8uYxjzkkdjskDQAeTQomvc\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "RTmcYhLVmmfgUn9L3D1J3y", + "name": "Chief Engineer @ Wyndham Hotels & Resorts", + "relation_id": "8uYxjzkkdjskDQAeTQomvc", + "relation_type": "Cities" } ], - "name": "San Francisco", "outbound_relations": [ { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WeSkkE1XXvGJGmXj9VUQ8\",\"relation_type\":\"Cover\",\"to_id\":\"CUoEazCD7EmzXPTFFY8gGY\"}", - "type": "text" + "id": "CUoEazCD7EmzXPTFFY8gGY", + "name": "No name", + "relation_id": "5WeSkkE1XXvGJGmXj9VUQ8", + "relation_type": "Cover" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"WUZCXE1UGRtxdNQpGug8Tf\",\"relation_type\":\"Types\",\"to_id\":\"7gzF671tq5JTZ13naG4tnr\"}", - "type": "text" + "id": "7gzF671tq5JTZ13naG4tnr", + "name": "Space", + "relation_id": "WUZCXE1UGRtxdNQpGug8Tf", + "relation_type": "Types" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"ARMj8fjJtdCwbtZa1f3jwe\",\"relation_type\":\"Types\",\"to_id\":\"D6Wy4bdtdoUrG3PDZceHr\"}", - "type": "text" + "id": "D6Wy4bdtdoUrG3PDZceHr", + "name": "City", + "relation_id": "ARMj8fjJtdCwbtZa1f3jwe", + "relation_type": "Types" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"V1ikGW9riu7dAP8rMgZq3u\",\"relation_type\":\"Blocks\",\"to_id\":\"AhidiWYnQ8fAbHqfzdU74k\"}", - "type": "text" + "id": "AhidiWYnQ8fAbHqfzdU74k", + "name": "Upcoming events", + "relation_id": "V1ikGW9riu7dAP8rMgZq3u", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"CvGXCmGXE7ofsgZeWad28p\",\"relation_type\":\"Blocks\",\"to_id\":\"T6iKbwZ17iv4dRdR9Qw7qV\"}", - "type": "text" + "id": "T6iKbwZ17iv4dRdR9Qw7qV", + "name": "Trending restaurants", + "relation_id": "CvGXCmGXE7ofsgZeWad28p", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"Uxpsee9LoTgJqMFfAQyJP6\",\"relation_type\":\"Blocks\",\"to_id\":\"X18WRE36mjwQ7gu3LKaLJS\"}", - "type": "text" + "id": "X18WRE36mjwQ7gu3LKaLJS", + "name": "Neighborhoods", + "relation_id": "Uxpsee9LoTgJqMFfAQyJP6", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WMTAzCnZH9Bsevou9GQ3K\",\"relation_type\":\"Blocks\",\"to_id\":\"HeC2pygci2tnvjTt5aEnBV\"}", - "type": "text" + "id": "HeC2pygci2tnvjTt5aEnBV", + "name": "Top goals", + "relation_id": "5WMTAzCnZH9Bsevou9GQ3K", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5TmxfepRr1THMRkGWenj5G\",\"relation_type\":\"Tabs\",\"to_id\":\"5YtYFsnWq1jupvh5AjM2ni\"}", - "type": "text" + "id": "5YtYFsnWq1jupvh5AjM2ni", + "name": "Culture", + "relation_id": "5TmxfepRr1THMRkGWenj5G", + "relation_type": "Tabs" } - ], - "types": [ - "D6Wy4bdtdoUrG3PDZceHr", - "Qu6vfQq68ecZ4PkihJ4nZN", - "7gzF671tq5JTZ13naG4tnr" ] } ``` diff --git a/mcp-server/resources/get_relations_between_entities_description.md b/mcp-server/resources/get_relations_between_entities_description.md index 848ab69..70b0160 100644 --- a/mcp-server/resources/get_relations_between_entities_description.md +++ b/mcp-server/resources/get_relations_between_entities_description.md @@ -7,28 +7,28 @@ ToolCall> search_types("9xRruQhSfAuJjHwKnvTjma", "ESShPFkqfFnDzYkSwGGVuR") ToolResult> ``` [ - { - "nodes": [ - "Crypto Briefing", - "Crypto", - "Bullish" - ], - "relations": [ - "Related spaces", - "Related spaces" - ] -}, -{ - "nodes": [ - "Crypto Briefing", - "Featured", - "Bullish" - ], - "relations": [ - "Tags", - "Tags" - ] -} + { + "nodes": [ + "Crypto Briefing", + "Crypto", + "Bullish" + ], + "relations": [ + "Related spaces", + "Related spaces" + ] + }, + { + "nodes": [ + "Crypto Briefing", + "Featured", + "Bullish" + ], + "relations": [ + "Tags", + "Tags" + ] + } ] ``` diff --git a/mcp-server/resources/instructions.md b/mcp-server/resources/instructions.md index 6c98c14..9aeec45 100644 --- a/mcp-server/resources/instructions.md +++ b/mcp-server/resources/instructions.md @@ -7,13 +7,63 @@ The tools defined in the MCP server are made to be used in combination with each Here is an example: User> Can you give me information about San Francisco? -ToolCall> search_entity("San Francisco") +ToolCall> search_entity({"query": "San Francisco"}) ToolResult> ``` -[{"description":"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.","id":"3qayfdjYyPv1dAYf8gPL5r","name":"San Francisco"}, -{"description":null,"id":"W5ZEpuy3Tij1XSXtJLruQ5","name":"SF Bay Area"}, -{"description":null,"id":"RHoJT3hNVaw7m5fLLtZ8WQ","name":"California"}, -{"description":null,"id":"Sh1qtjr4i92ZD6YGPeu5a2","name":"Abundant housing in San Francisco"}] +{ + "entities": [ + { + "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "id": "3qayfdjYyPv1dAYf8gPL5r", + "name": "San Francisco" + }, + { + "description": null, + "id": "W5ZEpuy3Tij1XSXtJLruQ5", + "name": "SF Bay Area" + }, + { + "description": null, + "id": "RHoJT3hNVaw7m5fLLtZ8WQ", + "name": "California" + }, + { + "description": null, + "id": "Sh1qtjr4i92ZD6YGPeu5a2", + "name": "Abundant housing in San Francisco" + }, + { + "description": null, + "id": "UqLf9fTVKHkDs3LzP9zHpH", + "name": "Public safety in San Francisco" + }, + { + "description": null, + "id": "BeyiZ6oLqLMaSXiG41Yxtf", + "name": "City" + }, + { + "description": null, + "id": "D6Wy4bdtdoUrG3PDZceHr", + "name": "City" + }, + { + "description": null, + "id": "JWVrgUXmjS75PqNX2hry5q", + "name": "Clean streets in San Francisco" + }, + { + "description": null, + "id": "DcA2c7ooFTgEdtaRcaj7Z1", + "name": "Revitalizing downtown San Francisco" + }, + { + "description": null, + "id": "KWBLj9czHBBmYUT98rnxVM", + "name": "Location" + } + ] +} ``` Let's get more info about San Francisco (id: 3qayfdjYyPv1dAYf8gPL5r) @@ -23,57 +73,114 @@ ToolResult> { "all_attributes": [ { - "text": "{\"attribute_name\":\"Description\",\"attribute_value\":\"A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.\"}", - "type": "text" + "attribute_name": "Description", + "attribute_value": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District." }, { - "text": "{\"attribute_name\":\"Name\",\"attribute_value\":\"San Francisco\"}", - "type": "text" + "attribute_name": "Name", + "attribute_value": "San Francisco" } ], - "description": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", "id": "3qayfdjYyPv1dAYf8gPL5r", "inbound_relations": [ { - "text": "{\"from_id\":\"NAMA1uDMzBQTvPYV9N92BV\",\"relation_id\":\"8ESicJHiNJ28VGL5u34A5q\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "NAMA1uDMzBQTvPYV9N92BV", + "name": "SF Mayor Lurie launching police task force to counter crime in core downtown areas", + "relation_id": "8ESicJHiNJ28VGL5u34A5q", + "relation_type": "Related spaces" + }, + { + "id": "6wAoNdGVbweKi2JRPZP4bX", + "name": "San Francisco Independent Film Festival", + "relation_id": "TH5Tu5Y5nacvREvAQRvcR2", + "relation_type": "Related spaces" + }, + { + "id": "8VCHYDURDStwuTCUBjWLQa", + "name": "Product Engineer at Geo", + "relation_id": "KPTqdNpCusxfM37KbKPX8w", + "relation_type": "Related spaces" + }, + { + "id": "NcQ3h9jeJSavVd8iFsUxvD", + "name": "Senior Civil Engineer @ Golden Gate Bridge, Highway & Transportation District", + "relation_id": "AqpNtJ3XxaY4fqRCyoXbdt", + "relation_type": "Cities" + }, + { + "id": "4ojV4dS1pV2tRnzXTpcMKJ", + "name": "Senior Plan Check Engineer (FT - Hybrid) @ CSG Consultants, Inc.", + "relation_id": "3AX4j43nywT5eBRV3s6AXi", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"6wAoNdGVbweKi2JRPZP4bX\",\"relation_id\":\"TH5Tu5Y5nacvREvAQRvcR2\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "QoakYWCuv85FVuYdSmonxr", + "name": "Senior Civil Engineer - Land Development (FT - Hybrid) @ CSG Consultants, Inc.", + "relation_id": "8GEF1i3LK4Z56THjE8dVku", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"8VCHYDURDStwuTCUBjWLQa\",\"relation_id\":\"KPTqdNpCusxfM37KbKPX8w\",\"relation_type\":\"Related spaces\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "JuV7jLoypebzLhkma6oZoU", + "name": "Lead Django Backend Engineer @ Textme Inc", + "relation_id": "46aBsQyBq15DimJ2i1DX4a", + "relation_type": "Cities" }, { - "text": "{\"from_id\":\"NcQ3h9jeJSavVd8iFsUxvD\",\"relation_id\":\"AqpNtJ3XxaY4fqRCyoXbdt\",\"relation_type\":\"Cities\",\"to_id\":\"3qayfdjYyPv1dAYf8gPL5r\"}", - "type": "text" + "id": "RTmcYhLVmmfgUn9L3D1J3y", + "name": "Chief Engineer @ Wyndham Hotels & Resorts", + "relation_id": "8uYxjzkkdjskDQAeTQomvc", + "relation_type": "Cities" } ], - "name": "San Francisco", "outbound_relations": [ { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"5WeSkkE1XXvGJGmXj9VUQ8\",\"relation_type\":\"Cover\",\"to_id\":\"CUoEazCD7EmzXPTFFY8gGY\"}", - "type": "text" + "id": "CUoEazCD7EmzXPTFFY8gGY", + "name": "No name", + "relation_id": "5WeSkkE1XXvGJGmXj9VUQ8", + "relation_type": "Cover" + }, + { + "id": "7gzF671tq5JTZ13naG4tnr", + "name": "Space", + "relation_id": "WUZCXE1UGRtxdNQpGug8Tf", + "relation_type": "Types" + }, + { + "id": "D6Wy4bdtdoUrG3PDZceHr", + "name": "City", + "relation_id": "ARMj8fjJtdCwbtZa1f3jwe", + "relation_type": "Types" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"WUZCXE1UGRtxdNQpGug8Tf\",\"relation_type\":\"Types\",\"to_id\":\"7gzF671tq5JTZ13naG4tnr\"}", - "type": "text" + "id": "AhidiWYnQ8fAbHqfzdU74k", + "name": "Upcoming events", + "relation_id": "V1ikGW9riu7dAP8rMgZq3u", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"ARMj8fjJtdCwbtZa1f3jwe\",\"relation_type\":\"Types\",\"to_id\":\"D6Wy4bdtdoUrG3PDZceHr\"}", - "type": "text" + "id": "T6iKbwZ17iv4dRdR9Qw7qV", + "name": "Trending restaurants", + "relation_id": "CvGXCmGXE7ofsgZeWad28p", + "relation_type": "Blocks" }, { - "text": "{\"from_id\":\"3qayfdjYyPv1dAYf8gPL5r\",\"relation_id\":\"V1ikGW9riu7dAP8rMgZq3u\",\"relation_type\":\"Blocks\",\"to_id\":\"AhidiWYnQ8fAbHqfzdU74k\"}", - "type": "text" + "id": "X18WRE36mjwQ7gu3LKaLJS", + "name": "Neighborhoods", + "relation_id": "Uxpsee9LoTgJqMFfAQyJP6", + "relation_type": "Blocks" + }, + { + "id": "HeC2pygci2tnvjTt5aEnBV", + "name": "Top goals", + "relation_id": "5WMTAzCnZH9Bsevou9GQ3K", + "relation_type": "Blocks" + }, + { + "id": "5YtYFsnWq1jupvh5AjM2ni", + "name": "Culture", + "relation_id": "5TmxfepRr1THMRkGWenj5G", + "relation_type": "Tabs" } - ], - "types": [ - "D6Wy4bdtdoUrG3PDZceHr", - "Qu6vfQq68ecZ4PkihJ4nZN", - "7gzF671tq5JTZ13naG4tnr" ] } ``` diff --git a/mcp-server/resources/name_search_entity_description.md b/mcp-server/resources/name_search_entity_description.md new file mode 100644 index 0000000..8377684 --- /dev/null +++ b/mcp-server/resources/name_search_entity_description.md @@ -0,0 +1,81 @@ +This request allows you to get Entities from a name/description search and traversal from that query by using relation name. + +Example Query: Find employees that works at The Graph. + +ToolCall> +``` +name_search_entity( + { + "query": "The Graph", + "traversal_filter": { + "relation_type_id": "Works at", + "direction": "From" + } + } +) +``` + +ToolResult> +``` +{ + "entities": [ + { + "description": "Founder & CEO of Geo. Cofounder of The Graph, Edge & Node, House of Web3. Building a vibrant decentralized future.", + "id": "9HsfMWYHr9suYdMrtssqiX", + "name": "Yaniv Tal" + }, + { + "description": "Developer Relations Engineer", + "id": "22MGz47c9WHtRiHuSEPkcG", + "name": "Kevin Jones" + }, + { + "description": "Description will go here", + "id": "JYTfEcdmdjiNzBg469gE83", + "name": "Pedro Diogo" + } + ] +} +``` + +Example Query: Find all the articles written by employees that works at The Graph. + +ToolCall> +``` +name_search_entity( + { + "query": "The Graph", + "traversal_filter": { + "relation_type_id": "Works at", + "direction": "From", + "traversal_filter": { + "relation_type_id": "Author", + "direction": "From" + } + } + } +) +``` + +ToolResult> +``` +{ + "entities": [ + { + "description": "A fresh look at what web3 is and what the missing pieces have been for making it a reality.", + "id": "XYo6aR3VqFQSEcf6AeTikW", + "name": "Knowledge graphs are web3" + }, + { + "description": "A new standard is here for structuring knowledge. GRC-20 will reshape how we make applications composable and redefine web3.", + "id": "5FkVvS4mTz6Ge7wHkAUMRk", + "name": "Introducing GRC-20: A knowledge graph standard for web3" + }, + { + "description": "How do you know what is true? Who do you trust? Everybody has a point of view, but no one is an authority. As humanity we need a way to aggregate our knowledge into something we can trust. We need a system.", + "id": "5WHP8BuoCdSiqtfy87SYWG", + "name": "Governing public knowledge" + } + ] +} +``` diff --git a/mcp-server/resources/search_entity_description.md b/mcp-server/resources/search_entity_description.md index b9f70f4..6ba6e33 100644 --- a/mcp-server/resources/search_entity_description.md +++ b/mcp-server/resources/search_entity_description.md @@ -32,6 +32,36 @@ Tool Result> "description": null, "id": "Sh1qtjr4i92ZD6YGPeu5a2", "name": "Abundant housing in San Francisco" + }, + { + "description": null, + "id": "UqLf9fTVKHkDs3LzP9zHpH", + "name": "Public safety in San Francisco" + }, + { + "description": null, + "id": "BeyiZ6oLqLMaSXiG41Yxtf", + "name": "City" + }, + { + "description": null, + "id": "D6Wy4bdtdoUrG3PDZceHr", + "name": "City" + }, + { + "description": null, + "id": "JWVrgUXmjS75PqNX2hry5q", + "name": "Clean streets in San Francisco" + }, + { + "description": null, + "id": "DcA2c7ooFTgEdtaRcaj7Z1", + "name": "Revitalizing downtown San Francisco" + }, + { + "description": null, + "id": "KWBLj9czHBBmYUT98rnxVM", + "name": "Location" } ] } diff --git a/mcp-server/resources/search_properties_description.md b/mcp-server/resources/search_properties_description.md index 690c24c..8f5a804 100644 --- a/mcp-server/resources/search_properties_description.md +++ b/mcp-server/resources/search_properties_description.md @@ -5,46 +5,20 @@ ToolCall> search_properties("Authors") ToolResult> ``` [ + [ { - "description": null, - "id": "JzFpgguvcCaKhbQYPHsrNT", - "name": "Authors", - "types": [ - "GscJ2GELQjmLoaVrYyR3xm" - ] -}, -{ - "description": null, - "id": "Lc4JrkpMUPhNstqs7mvnc5", - "name": "Publisher", - "types": [ - "GscJ2GELQjmLoaVrYyR3xm" - ] -}, -{ - "description": null, - "id": "61dgWvCDk8QRW2yrfkHuia", - "name": "Published in", - "types": [ - "GscJ2GELQjmLoaVrYyR3xm" - ] -}, -{ - "description": null, - "id": "W2aFZPy5nnU3DgdkWJCNVn", - "name": "Person", - "types": [ - "GscJ2GELQjmLoaVrYyR3xm" - ] -}, -{ - "description": null, - "id": "RwDfM3vUvyLwSNYv6sWhc9", - "name": "Owners", - "types": [ - "GscJ2GELQjmLoaVrYyR3xm" + "attribute_name": "Name", + "attribute_value": "Authors", + "entity_id": "JzFpgguvcCaKhbQYPHsrNT" + } + ], + [ + { + "attribute_name": "Name", + "attribute_value": "Owners", + "entity_id": "RwDfM3vUvyLwSNYv6sWhc9" + } ] -} ] ``` diff --git a/mcp-server/resources/search_relation_type_description.md b/mcp-server/resources/search_relation_type_description.md index beaefb1..55d4091 100644 --- a/mcp-server/resources/search_relation_type_description.md +++ b/mcp-server/resources/search_relation_type_description.md @@ -4,41 +4,45 @@ ToolCall> search_relation_types("works at") ToolResult> ``` [ + [ { - "description": null, - "id": "U1uCAzXsRSTP4vFwo1JwJG", - "name": "Works at" -}, -{ - "description": "A project that someone worked at in the past. Details about the role can be added as properties on the relation.", - "id": "8fvqALeBDwEExJsDeTcvnV", - "name": "Worked at" -}, -{ - "description": "The supervisor to this position. In the case of a clerkship, the supervising judge.", - "id": "WnzSw9CWE7mtgwRokF8Qxh", - "name": "Supervisor" -}, -{ - "description": null, - "id": "Gri4x41WSPUtpwG8BzhTpa", - "name": "Tasks" -}, -{ - "description": "The judge or magistrate responsible for overseeing and deciding the case.", - "id": "PuLfk3sFs6PkhEuf8cyBfs", - "name": "Assigned to" -}, -{ - "description": null, - "id": "MuMLDVbHAmRjZQjhyk3HGx", - "name": "Network" -}, -{ - "description": null, - "id": "RERshk4JoYoMC17r1qAo9J", - "name": "From" -} + "attribute_name": "Name", + "attribute_value": "Works at", + "entity_id": "U1uCAzXsRSTP4vFwo1JwJG" + }, + { + "attribute_name": "Is type property", + "attribute_value": "0", + "entity_id": "U1uCAzXsRSTP4vFwo1JwJG" + }, + { + "attribute_name": "Name", + "attribute_value": "Works at", + "entity_id": "U1uCAzXsRSTP4vFwo1JwJG" + } + ], + [ + { + "attribute_name": "Name", + "attribute_value": "Worked at", + "entity_id": "8fvqALeBDwEExJsDeTcvnV" + }, + { + "attribute_name": "Is type property", + "attribute_value": "0", + "entity_id": "8fvqALeBDwEExJsDeTcvnV" + }, + { + "attribute_name": "Name", + "attribute_value": "Worked at", + "entity_id": "8fvqALeBDwEExJsDeTcvnV" + }, + { + "attribute_name": "Description", + "attribute_value": "A project that someone worked at in the past. Details about the role can be added as properties on the relation.", + "entity_id": "8fvqALeBDwEExJsDeTcvnV" + } + ] ] ``` diff --git a/mcp-server/resources/search_space_description.md b/mcp-server/resources/search_space_description.md new file mode 100644 index 0000000..3224922 --- /dev/null +++ b/mcp-server/resources/search_space_description.md @@ -0,0 +1,40 @@ +This request allows you to find a Space from it's name or description. The spaces are where the attributes and relations are and may be useful to specify when querying entities and relations. + +ToolCall> +``` +search_space("San Francisco") +``` + +ToolResult> +``` +[ + [ + { + "attribute_name": "Description", + "attribute_value": "A vibrant city known for its iconic Golden Gate Bridge, steep rolling hills, historic cable cars, and a rich cultural tapestry including diverse neighborhoods like the Castro and the Mission District.", + "entity_id": "3qayfdjYyPv1dAYf8gPL5r" + }, + { + "attribute_name": "Name", + "attribute_value": "San Francisco", + "entity_id": "3qayfdjYyPv1dAYf8gPL5r" + } + ], + [ + { + "attribute_name": "Name", + "attribute_value": "SF Bay Area", + "entity_id": "W5ZEpuy3Tij1XSXtJLruQ5" + } + ], + [ + { + "attribute_name": "Name", + "attribute_value": "California", + "entity_id": "RHoJT3hNVaw7m5fLLtZ8WQ" + } + ] +] +``` + +Eventually, space will be used to narrow research or help format result diff --git a/mcp-server/resources/search_type_description.md b/mcp-server/resources/search_type_description.md index 61946b1..7ec21a1 100644 --- a/mcp-server/resources/search_type_description.md +++ b/mcp-server/resources/search_type_description.md @@ -1,58 +1,33 @@ This request allows you to search by name for a basic type of the Knowledge Graph(KG) like Person or Event. This will give back the type with it's name, id and description. - -ToolCall> search_types("Event") +ToolCall> search_types("University") ToolResult> ``` [ + [ { - "description": null, - "id": "AaGd6UMXfNtL6U6Xx7K8Cv", - "name": "Event", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" - ] -}, -{ - "description": "A claim that something has happened. This type is used together with the claim type.", - "id": "QAdjgcq9nD7Gv98vn2vrDd", - "name": "News event", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" - ] -}, -{ - "description": null, - "id": "TjSP1BaHZ7QxyBcZEM8Sdt", - "name": "Feature", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" - ] -}, -{ - "description": "A general concept that can be used to group things of the same category together.", - "id": "Cj7JSjWKbcdgmUjcLWNR4V", - "name": "Topic", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" - ] -}, -{ - "description": "Something that someone or a group can do professionally or recreationally.", - "id": "H7NECFeRiDkbwMq74DAKk5", - "name": "Activity", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" - ] -}, -{ - "description": null, - "id": "KSKxz7Ek66SfW4euxZzKsX", - "name": "Task", - "types": [ - "VdTsW1mGiy1XSooJaBBLc4" + "attribute_name": "Description", + "attribute_value": "An institution of higher education offering undergraduate and graduate degrees, research opportunities, and specialized academic programs.", + "entity_id": "L8iozarUyS8bkcUiS6kPqV" + }, + { + "attribute_name": "Name", + "attribute_value": "University", + "entity_id": "L8iozarUyS8bkcUiS6kPqV" + } + ], + [ + { + "attribute_name": "Description", + "attribute_value": "An educational institution where students acquire knowledge, skills, and credentials through structured learning programs.", + "entity_id": "M89C7wwdJVaCW9rAVQpJbY" + }, + { + "attribute_name": "Name", + "attribute_value": "School", + "entity_id": "M89C7wwdJVaCW9rAVQpJbY" + } ] -} ] ``` diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 104d823..8b970a5 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -269,11 +269,11 @@ impl KnowledgeGraph { .from_(EntityFilter::default().id(prop_filter::value(search_result.triple.entity))) .relation_type( EntityFilter::default() - .id(prop_filter::value(system_ids::RELATION_SCHEMA_TYPE)), + .id(prop_filter::value(system_ids::VALUE_TYPE_ATTRIBUTE)), ) .to_( EntityFilter::default() - .id(prop_filter::value(system_ids::VALUE_TYPE_ATTRIBUTE)), + .id(prop_filter::value(system_ids::RELATION_SCHEMA_TYPE)), ) }; From a2a6add369515ae323bdd2d84343f617dca95a85 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 2 Jul 2025 14:00:14 -0400 Subject: [PATCH 23/25] fix update format for clippy update --- api/build.rs | 4 ++-- api/src/query_mapping.rs | 8 +++----- grc20-core/src/ids/base58.rs | 2 +- grc20-core/src/ids/id.rs | 6 +++--- grc20-core/src/mapping/entity/utils.rs | 6 +++--- grc20-core/src/mapping/mod.rs | 2 +- grc20-core/src/mapping/query_utils/prop_filter.rs | 2 +- grc20-core/src/mapping/query_utils/query_builder.rs | 8 ++++---- grc20-core/src/mapping/query_utils/query_part.rs | 4 ++-- grc20-core/src/mapping/query_utils/version_filter.rs | 2 +- grc20-core/src/mapping/value.rs | 5 +---- grc20-macros/src/entity.rs | 6 +++--- grc20-macros/src/relation.rs | 4 ++-- grc20-sdk/src/models/edit.rs | 4 ++-- grc20-sdk/src/models/proposal.rs | 7 ++----- grc20-sdk/src/models/space/parent_spaces_query.rs | 2 +- grc20-sdk/src/models/space/subspaces_query.rs | 2 +- grc20-sdk/src/models/vote.rs | 5 ++--- sink/build.rs | 4 ++-- sink/src/events/handler.rs | 10 ++++------ sink/src/main.rs | 4 ++-- substreams-utils/src/sink.rs | 2 +- substreams-utils/src/substreams_stream.rs | 4 ++-- 23 files changed, 46 insertions(+), 57 deletions(-) diff --git a/api/build.rs b/api/build.rs index da489a8..191dcc3 100644 --- a/api/build.rs +++ b/api/build.rs @@ -17,8 +17,8 @@ fn main() { .map(|desc| desc.trim().to_string()) .unwrap_or_else(|| "unknown".to_string()); - println!("cargo::rustc-env=GIT_COMMIT={}", git_hash); - println!("cargo::rustc-env=GIT_TAG={}", git_tag); + println!("cargo::rustc-env=GIT_COMMIT={git_hash}"); + println!("cargo::rustc-env=GIT_TAG={git_tag}"); // Always rerun if any git changes occur println!("cargo::rerun-if-changed=.git/HEAD"); diff --git a/api/src/query_mapping.rs b/api/src/query_mapping.rs index 43a991b..2b94d25 100644 --- a/api/src/query_mapping.rs +++ b/api/src/query_mapping.rs @@ -28,7 +28,7 @@ impl QueryMapper { self.node_counter += 1; self.match_statements - .push(format!("MATCH ({} {{id: \"{id}\"}})", node_var)); + .push(format!("MATCH ({node_var} {{id: \"{id}\"}})")); self.return_statement_vars.insert(node_var.clone()); selection @@ -50,10 +50,8 @@ impl QueryMapper { self.relation_counter += 1; self.node_counter += 1; - self.match_statements.push(format!( - "MATCH ({}) -[{}]-> ({})", - node_var, relation_var, to_var - )); + self.match_statements + .push(format!("MATCH ({node_var}) -[{relation_var}]-> ({to_var})")); self.return_statement_vars.insert(self.relation_var()); selection diff --git a/grc20-core/src/ids/base58.rs b/grc20-core/src/ids/base58.rs index 6bb21b9..35a68ad 100644 --- a/grc20-core/src/ids/base58.rs +++ b/grc20-core/src/ids/base58.rs @@ -34,7 +34,7 @@ pub fn decode_base58_to_uuid(encoded: &str) -> Result { } } - let hex_str = format!("{:032x}", decoded); + let hex_str = format!("{decoded:032x}"); Ok(format!( "{}-{}-{}-{}-{}", &hex_str[0..8], diff --git a/grc20-core/src/ids/id.rs b/grc20-core/src/ids/id.rs index f1864f5..edf0413 100644 --- a/grc20-core/src/ids/id.rs +++ b/grc20-core/src/ids/id.rs @@ -43,11 +43,11 @@ pub fn create_merged_version_id(merged_version_ids: Vec<&str>) -> String { } pub fn create_version_id(space_id: &str, proposal_id: &str) -> String { - create_id_from_unique_string(format!("{}:{}", space_id, proposal_id)) + create_id_from_unique_string(format!("{space_id}:{proposal_id}")) } pub fn create_version_id_from_block(space_id: &str, block: u64) -> String { - create_id_from_unique_string(format!("{}:{}", space_id, block)) + create_id_from_unique_string(format!("{space_id}:{block}")) } /** @@ -56,7 +56,7 @@ pub fn create_version_id_from_block(space_id: &str, block: u64) -> String { * the new one that they're creating. */ pub fn create_space_id(network: &str, address: &str) -> String { - create_id_from_unique_string(format!("{}:{}", network, address)) + create_id_from_unique_string(format!("{network}:{address}")) } pub fn create_id_from_unique_string(text: impl Into) -> String { diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index aab44d5..5b1b0ba 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -149,7 +149,7 @@ impl EntityRelationFilter { let node_var = node_var.into(); let random_suffix: String = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); - let rel_edge_var = format!("r_{node_var}_{}", random_suffix); + let rel_edge_var = format!("r_{node_var}_{random_suffix}"); let to_node_var = format!("r_{node_var}_to"); MatchQuery::new(format!( @@ -223,8 +223,8 @@ impl TraverseRelationFilter { let node_var_curr = node_var.into(); let random_suffix: String = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 4); - let rel_edge_var = format!("r_{node_var_curr}_{}", random_suffix); - let node_var_dest = format!("r_{node_var_curr}_{}_to", random_suffix); + let rel_edge_var = format!("r_{node_var_curr}_{random_suffix}"); + let node_var_dest = format!("r_{node_var_curr}_{random_suffix}_to"); MatchQuery::new(match self.direction { RelationDirection::From => { diff --git a/grc20-core/src/mapping/mod.rs b/grc20-core/src/mapping/mod.rs index aa7d43d..9733cc0 100644 --- a/grc20-core/src/mapping/mod.rs +++ b/grc20-core/src/mapping/mod.rs @@ -29,7 +29,7 @@ pub use value::{Options, Value, ValueType}; use crate::{error::DatabaseError, indexer_ids}; pub fn new_version_index(block_number: u64, idx: usize) -> String { - format!("{:016}:{:04}", block_number, idx) + format!("{block_number:016}:{idx:04}") } pub async fn get_version_index( diff --git a/grc20-core/src/mapping/query_utils/prop_filter.rs b/grc20-core/src/mapping/query_utils/prop_filter.rs index 9eb816f..44d14f4 100644 --- a/grc20-core/src/mapping/query_utils/prop_filter.rs +++ b/grc20-core/src/mapping/query_utils/prop_filter.rs @@ -197,7 +197,7 @@ impl> PropFilter { let expr = expr .map(|e| e.to_string()) - .unwrap_or(format!("{}.`{}`", node_var, key)); + .unwrap_or(format!("{node_var}.`{key}`")); if let Some(value) = &self.value { let param_key = format!("{node_var}_{key}_value"); diff --git a/grc20-core/src/mapping/query_utils/query_builder.rs b/grc20-core/src/mapping/query_utils/query_builder.rs index 89f5d25..1e24f8e 100644 --- a/grc20-core/src/mapping/query_utils/query_builder.rs +++ b/grc20-core/src/mapping/query_utils/query_builder.rs @@ -206,9 +206,9 @@ impl Subquery for MatchQuery { match &self.where_clauses.as_slice() { [] => (), [clause, rest @ ..] => { - statements.push(format!("WHERE {}", clause)); + statements.push(format!("WHERE {clause}")); for rest_clause in rest { - statements.push(format!("AND {}", rest_clause)); + statements.push(format!("AND {rest_clause}")); } } } @@ -333,9 +333,9 @@ impl Subquery for WhereClause { match &self.clauses.as_slice() { [] => vec![], [clause, rest @ ..] => { - let mut statements = vec![format!("WHERE {}", clause)]; + let mut statements = vec![format!("WHERE {clause}")]; for rest_clause in rest { - statements.push(format!("AND {}", rest_clause)); + statements.push(format!("AND {rest_clause}")); } statements } diff --git a/grc20-core/src/mapping/query_utils/query_part.rs b/grc20-core/src/mapping/query_utils/query_part.rs index 0c8b7c0..1ffeaa9 100644 --- a/grc20-core/src/mapping/query_utils/query_part.rs +++ b/grc20-core/src/mapping/query_utils/query_part.rs @@ -217,11 +217,11 @@ impl QueryPart { } if let Some(skip) = self.skip { - query.push_str(&format!("SKIP {}\n", skip)); + query.push_str(&format!("SKIP {skip}\n")); } if let Some(limit) = self.limit { - query.push_str(&format!("LIMIT {}\n", limit)); + query.push_str(&format!("LIMIT {limit}\n")); } if let Some((clause, other)) = &self.with_clauses { diff --git a/grc20-core/src/mapping/query_utils/version_filter.rs b/grc20-core/src/mapping/query_utils/version_filter.rs index 615c066..16cc119 100644 --- a/grc20-core/src/mapping/query_utils/version_filter.rs +++ b/grc20-core/src/mapping/query_utils/version_filter.rs @@ -25,7 +25,7 @@ impl VersionFilter { pub fn subquery(&self, var: &str) -> WhereClause { if let Some(version) = &self.version { - let param_key = format!("{}_version", var); + let param_key = format!("{var}_version"); WhereClause::new(format!("{var}.min_version <= ${param_key} AND ({var}.max_version IS NULL OR {var}.max_version > ${param_key})")) .set_param(param_key, version.clone()) diff --git a/grc20-core/src/mapping/value.rs b/grc20-core/src/mapping/value.rs index 78a10c9..1d279da 100644 --- a/grc20-core/src/mapping/value.rs +++ b/grc20-core/src/mapping/value.rs @@ -253,10 +253,7 @@ impl TryFrom for DateTime { fn try_from(value: Value) -> Result { Ok(DateTime::parse_from_rfc3339(&value.value) .map_err(|e| { - TriplesConversionError::InvalidValue(format!( - "Failed to parse DateTime value: {}", - e - )) + TriplesConversionError::InvalidValue(format!("Failed to parse DateTime value: {e}")) })? .with_timezone(&Utc)) } diff --git a/grc20-macros/src/entity.rs b/grc20-macros/src/entity.rs index 2bddf3e..1cfba9c 100644 --- a/grc20-macros/src/entity.rs +++ b/grc20-macros/src/entity.rs @@ -120,7 +120,7 @@ pub(crate) fn generate_from_attributes_impl(opts: &EntityOpts) -> TokenStream2 { pub(crate) fn generate_builder_impl(opts: &EntityOpts) -> TokenStream2 { let struct_name = &opts.ident; - let builder_name = Ident::new(&format!("{}Builder", struct_name), Span::call_site()); + let builder_name = Ident::new(&format!("{struct_name}Builder"), Span::call_site()); let fields = opts.data.as_ref().take_struct().expect("Expected struct"); let schema_type = opts.schema_type.as_ref().map(|s| quote!(#s)); @@ -136,7 +136,7 @@ pub(crate) fn generate_builder_impl(opts: &EntityOpts) -> TokenStream2 { // Generate setter methods for each field let setter_methods = fields.iter().map(|field| { let field_name = field.ident.as_ref().expect("Expected named field"); - let mut_name = Ident::new(&format!("{}_mut", field_name), Span::call_site()); + let mut_name = Ident::new(&format!("{field_name}_mut"), Span::call_site()); let field_type = &field.ty; // For Option types, we don't need impl Into @@ -346,7 +346,7 @@ pub(crate) fn generate_query_impls(opts: &EntityOpts) -> TokenStream2 { .iter() .zip(field_types.iter()) .map(|(field_name, field_type)| { - let doc_comment = format!("Filter by {}", field_name); + let doc_comment = format!("Filter by {field_name}"); let filter_type = if let syn::Type::Path(type_path) = field_type { if type_path .path diff --git a/grc20-macros/src/relation.rs b/grc20-macros/src/relation.rs index 313b700..dc65d94 100644 --- a/grc20-macros/src/relation.rs +++ b/grc20-macros/src/relation.rs @@ -120,7 +120,7 @@ pub(crate) fn generate_from_attributes_impl(opts: &RelationOpts) -> TokenStream2 pub(crate) fn generate_builder_impl(opts: &RelationOpts) -> TokenStream2 { let struct_name = &opts.ident; - let builder_name = Ident::new(&format!("{}Builder", struct_name), Span::call_site()); + let builder_name = Ident::new(&format!("{struct_name}Builder"), Span::call_site()); let fields = opts.data.as_ref().take_struct().expect("Expected struct"); let relation_type = opts.relation_type.as_ref().map(|s| quote!(#s)); @@ -136,7 +136,7 @@ pub(crate) fn generate_builder_impl(opts: &RelationOpts) -> TokenStream2 { // Generate setter methods for each field let setter_methods = fields.iter().map(|field| { let field_name = field.ident.as_ref().expect("Expected named field"); - let mut_name = Ident::new(&format!("{}_mut", field_name), Span::call_site()); + let mut_name = Ident::new(&format!("{field_name}_mut"), Span::call_site()); let field_type = &field.ty; // For Option types, we don't need impl Into diff --git a/grc20-sdk/src/models/edit.rs b/grc20-sdk/src/models/edit.rs index 62fe140..4e2181a 100644 --- a/grc20-sdk/src/models/edit.rs +++ b/grc20-sdk/src/models/edit.rs @@ -41,7 +41,7 @@ pub struct Edits; impl Edits { pub fn gen_id(space_id: &str, edit_id: &str) -> String { - ids::create_id_from_unique_string(format!("{}:{}", space_id, edit_id)) + ids::create_id_from_unique_string(format!("{space_id}:{edit_id}")) } pub fn new( @@ -70,7 +70,7 @@ pub struct ProposedEdit; impl ProposedEdit { pub fn gen_id(proposal_id: &str, edit_id: &str) -> String { - ids::create_id_from_unique_string(format!("{}:{}", proposal_id, edit_id)) + ids::create_id_from_unique_string(format!("{proposal_id}:{edit_id}")) } pub fn new( diff --git a/grc20-sdk/src/models/proposal.rs b/grc20-sdk/src/models/proposal.rs index 07770c8..2e2678e 100644 --- a/grc20-sdk/src/models/proposal.rs +++ b/grc20-sdk/src/models/proposal.rs @@ -139,7 +139,7 @@ impl TryFrom for ProposalType { pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), pb::ipfs::ActionType::ImportSpace => Ok(Self::ImportSpace), pb::ipfs::ActionType::ArchiveSpace => Ok(Self::ArchiveSpace), - _ => Err(format!("Invalid action type: {:?}", action_type)), + _ => Err(format!("Invalid action type: {action_type:?}")), } } } @@ -422,10 +422,7 @@ pub struct ProposedAccount; impl ProposedAccount { pub fn gen_id(proposal_id: &str, account_id: &str) -> String { - ids::create_id_from_unique_string(format!( - "PROPOSED_ACCOUNT:{}:{}", - proposal_id, account_id - )) + ids::create_id_from_unique_string(format!("PROPOSED_ACCOUNT:{proposal_id}:{account_id}")) } pub fn new(proposal_id: &str, account_id: &str) -> Relation { diff --git a/grc20-sdk/src/models/space/parent_spaces_query.rs b/grc20-sdk/src/models/space/parent_spaces_query.rs index 4218703..c720b8f 100644 --- a/grc20-sdk/src/models/space/parent_spaces_query.rs +++ b/grc20-sdk/src/models/space/parent_spaces_query.rs @@ -61,7 +61,7 @@ impl ParentSpacesQuery { indexer_ids::INDEXER_SPACE_ID, )) .subquery("WHERE size(s) = size(COLLECT { WITH s UNWIND s AS _ RETURN DISTINCT _ })") - .subquery_opt(self.max_depth.map(|depth| format!("AND size(s) <= {}", depth))) + .subquery_opt(self.max_depth.map(|depth| format!("AND size(s) <= {depth}"))) .subquery("WITH {space_id: LAST([start] + s).id, depth: SIZE(s)} AS parent_spaces") .limit(self.limit) .skip_opt(self.skip) diff --git a/grc20-sdk/src/models/space/subspaces_query.rs b/grc20-sdk/src/models/space/subspaces_query.rs index d4a4eec..8cfabd3 100644 --- a/grc20-sdk/src/models/space/subspaces_query.rs +++ b/grc20-sdk/src/models/space/subspaces_query.rs @@ -61,7 +61,7 @@ impl SubspacesQuery { indexer_ids::INDEXER_SPACE_ID, )) .subquery("WHERE size(s) = size(COLLECT { WITH s UNWIND s AS _ RETURN DISTINCT _ })") - .subquery_opt(self.max_depth.map(|depth| format!("AND size(s) <= {}", depth))) + .subquery_opt(self.max_depth.map(|depth| format!("AND size(s) <= {depth}"))) .subquery("WITH {space_id: LAST([start] + s).id, depth: SIZE(s)} AS subspaces") .limit(self.limit) .skip_opt(self.skip) diff --git a/grc20-sdk/src/models/vote.rs b/grc20-sdk/src/models/vote.rs index d1b3ee9..8a5f388 100644 --- a/grc20-sdk/src/models/vote.rs +++ b/grc20-sdk/src/models/vote.rs @@ -51,7 +51,7 @@ impl TryFrom for VoteType { match vote { 2 => Ok(Self::Accept), 3 => Ok(Self::Reject), - _ => Err(format!("Invalid vote type: {}", vote)), + _ => Err(format!("Invalid vote type: {vote}")), } } } @@ -73,8 +73,7 @@ impl TryFrom for VoteType { (mapping::ValueType::Text, "ACCEPT") => Ok(Self::Accept), (mapping::ValueType::Text, "REJECT") => Ok(Self::Reject), (value_type, _) => Err(TriplesConversionError::InvalidValue(format!( - "Invalid vote type value_type: {:?}", - value_type + "Invalid vote type value_type: {value_type:?}" ))), } } diff --git a/sink/build.rs b/sink/build.rs index da489a8..191dcc3 100644 --- a/sink/build.rs +++ b/sink/build.rs @@ -17,8 +17,8 @@ fn main() { .map(|desc| desc.trim().to_string()) .unwrap_or_else(|| "unknown".to_string()); - println!("cargo::rustc-env=GIT_COMMIT={}", git_hash); - println!("cargo::rustc-env=GIT_TAG={}", git_tag); + println!("cargo::rustc-env=GIT_COMMIT={git_hash}"); + println!("cargo::rustc-env=GIT_TAG={git_tag}"); // Always rerun if any git changes occur println!("cargo::rerun-if-changed=.git/HEAD"); diff --git a/sink/src/events/handler.rs b/sink/src/events/handler.rs index 7cfd3d4..49edabd 100644 --- a/sink/src/events/handler.rs +++ b/sink/src/events/handler.rs @@ -96,15 +96,13 @@ impl EventHandler { InitOptions::new(EMBEDDING_MODEL).with_show_download_progress(true), ) .map_err(|e| { - tracing::error!("Error initializing embedding model: {:?}", e); - HandlerError::Other(format!("Error initializing embedding model: {:?}", e).into()) + tracing::error!("Error initializing embedding model: {e:?}"); + HandlerError::Other(format!("Error initializing embedding model: {e:?}").into()) })?, embedding_model_dim: TextEmbedding::get_model_info(&EMBEDDING_MODEL) .map_err(|e| { - tracing::error!("Error getting embedding model info: {:?}", e); - HandlerError::Other( - format!("Error getting embedding model info: {:?}", e).into(), - ) + tracing::error!("Error getting embedding model info: {e:?}"); + HandlerError::Other(format!("Error getting embedding model info: {e:?}").into()) })? .dim, versioning: false, diff --git a/sink/src/main.rs b/sink/src/main.rs index 7913559..ccd8828 100644 --- a/sink/src/main.rs +++ b/sink/src/main.rs @@ -81,10 +81,10 @@ async fn main() -> Result<(), Error> { MODULE_NAME, start_block .parse() - .unwrap_or_else(|_| panic!("Invalid start block: {}! Must be integer", start_block)), + .unwrap_or_else(|_| panic!("Invalid start block: {start_block}! Must be integer")), end_block .parse() - .unwrap_or_else(|_| panic!("Invalid end block: {}! Must be integer", end_block)), + .unwrap_or_else(|_| panic!("Invalid end block: {end_block}! Must be integer")), Some(64), ) .await?; diff --git a/substreams-utils/src/sink.rs b/substreams-utils/src/sink.rs index 8dd6d23..793a9a2 100644 --- a/substreams-utils/src/sink.rs +++ b/substreams-utils/src/sink.rs @@ -128,7 +128,7 @@ pub trait Sink: Send + Sync { Some(Err(err)) => { println!(); println!("Stream terminated with error"); - println!("{:?}", err); + println!("{err:?}"); exit(1); } } diff --git a/substreams-utils/src/substreams_stream.rs b/substreams-utils/src/substreams_stream.rs index 35178d5..f041743 100644 --- a/substreams-utils/src/substreams_stream.rs +++ b/substreams-utils/src/substreams_stream.rs @@ -119,7 +119,7 @@ fn stream_blocks( return Err(anyhow::Error::new(status.clone()))?; } - println!("Received tonic error {:#}", status); + println!("Received tonic error {status:#}"); encountered_error = true; break; }, @@ -136,7 +136,7 @@ fn stream_blocks( // case where we actually _want_ to back off in case we keep // having connection errors. - println!("Unable to connect to endpoint: {:#}", e); + println!("Unable to connect to endpoint: {e:#}"); } } From 54db2b5527657a1a2b8a17b18c887a22d4ea1e60 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Wed, 2 Jul 2025 16:26:45 -0400 Subject: [PATCH 24/25] fix code review comments --- grc20-core/src/mapping/entity/mod.rs | 8 +++--- ...trictions.rs => search_with_traversals.rs} | 28 +++++++++---------- .../src/mapping/entity/semantic_search.rs | 5 +--- grc20-core/src/mapping/entity/utils.rs | 12 ++++---- grc20-core/src/mapping/mod.rs | 2 ++ .../src/mapping/query_utils/query_builder.rs | 16 ++--------- grc20-core/src/mapping/triple.rs | 9 +++--- .../search_properties_description.md | 2 +- .../search_relation_type_description.md | 5 ---- mcp-server/src/main.rs | 28 ++++++++----------- 10 files changed, 46 insertions(+), 69 deletions(-) rename grc20-core/src/mapping/entity/{search_from_restrictions.rs => search_with_traversals.rs} (83%) diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 1be2545..93b8efb 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -6,7 +6,7 @@ pub mod find_path; pub mod insert_many; pub mod insert_one; pub mod models; -pub mod search_from_restrictions; +pub mod search_with_traversals; pub mod semantic_search; pub mod utils; @@ -16,7 +16,7 @@ pub use find_one::FindOneQuery; pub use find_path::FindPathQuery; pub use insert_one::InsertOneQuery; pub use models::{Entity, EntityNode, EntityNodeRef, SystemProperties}; -pub use search_from_restrictions::SearchFromRestrictions; +pub use search_with_traversals::SearchWithTraversals; pub use semantic_search::SemanticSearchQuery; pub use utils::{EntityFilter, EntityRelationFilter, TypesFilter}; @@ -135,8 +135,8 @@ pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery pub fn search_from_restictions( neo4j: &neo4rs::Graph, vector: Vec, -) -> SearchFromRestrictions { - SearchFromRestrictions::new(neo4j, vector) +) -> SearchWithTraversals { + SearchWithTraversals::new(neo4j, vector) } // TODO: add docs for use via GraphQL diff --git a/grc20-core/src/mapping/entity/search_from_restrictions.rs b/grc20-core/src/mapping/entity/search_with_traversals.rs similarity index 83% rename from grc20-core/src/mapping/entity/search_from_restrictions.rs rename to grc20-core/src/mapping/entity/search_with_traversals.rs index 108d5bf..ade6741 100644 --- a/grc20-core/src/mapping/entity/search_from_restrictions.rs +++ b/grc20-core/src/mapping/entity/search_with_traversals.rs @@ -5,13 +5,13 @@ use crate::{ error::DatabaseError, mapping::{ query_utils::VersionFilter, AttributeNode, FromAttributes, PropFilter, QueryBuilder, - QueryStream, Subquery, + QueryStream, Subquery, EFFECTIVE_SEARCH_RATIO, }, }; use super::{Entity, EntityFilter, EntityNode}; -pub struct SearchFromRestrictions { +pub struct SearchWithTraversals { neo4j: neo4rs::Graph, vector: Vec, filters: Vec, @@ -24,7 +24,7 @@ pub struct SearchFromRestrictions { _marker: std::marker::PhantomData, } -impl SearchFromRestrictions { +impl SearchWithTraversals { pub fn new(neo4j: &neo4rs::Graph, vector: Vec) -> Self { Self { neo4j: neo4j.clone(), @@ -107,24 +107,22 @@ impl SearchFromRestrictions { } #[derive(Clone, Debug, PartialEq)] -pub struct SearchFromRestrictionsResult { +pub struct SearchWithTraversalsResult { pub entity: T, } -const EFFECTIVE_SEARCH_RATIO: f64 = 100000.0; // Adjust this ratio based on your needs - -impl QueryStream> for SearchFromRestrictions { +impl QueryStream> for SearchWithTraversals { async fn send( self, ) -> Result< - impl Stream, DatabaseError>>, + impl Stream, DatabaseError>>, DatabaseError, > { let query = self.subquery().r#return("DISTINCT e"); if cfg!(debug_assertions) || cfg!(test) { tracing::info!( - "entity_node::SearchFromRestrictions:::\n{}\nparams:{:?}", + "entity_node::SearchWithTraversals:::\n{}\nparams:{:?}", query.compile(), query.params() ); @@ -141,17 +139,17 @@ impl QueryStream> for SearchFromRestric .await? .into_stream_as::() .map_err(DatabaseError::from) - .and_then(|row| async move { Ok(SearchFromRestrictionsResult { entity: row.e }) })) + .and_then(|row| async move { Ok(SearchWithTraversalsResult { entity: row.e }) })) } } -impl QueryStream>> - for SearchFromRestrictions> +impl QueryStream>> + for SearchWithTraversals> { async fn send( self, ) -> Result< - impl Stream>, DatabaseError>>, + impl Stream>, DatabaseError>>, DatabaseError, > { let match_entity = MatchEntity::new(&self.space_id, &self.version); @@ -169,7 +167,7 @@ impl QueryStream>> if cfg!(debug_assertions) || cfg!(test) { tracing::info!( - "entity_node::SearchFromRestrictions::>:\n{}\nparams:{:?}", + "entity_node::SearchWithTraversals::>:\n{}\nparams:{:?}", query.compile(), query.params ); @@ -192,7 +190,7 @@ impl QueryStream>> .map(|row_result| { row_result.and_then(|row| { T::from_attributes(row.attrs.into()) - .map(|data| SearchFromRestrictionsResult { + .map(|data| SearchWithTraversalsResult { entity: Entity { node: row.node, attributes: data, diff --git a/grc20-core/src/mapping/entity/semantic_search.rs b/grc20-core/src/mapping/entity/semantic_search.rs index 0b787e3..22c181a 100644 --- a/grc20-core/src/mapping/entity/semantic_search.rs +++ b/grc20-core/src/mapping/entity/semantic_search.rs @@ -5,7 +5,7 @@ use crate::{ error::DatabaseError, mapping::{ query_utils::VersionFilter, AttributeNode, FromAttributes, PropFilter, QueryBuilder, - QueryStream, Subquery, + QueryStream, Subquery, EFFECTIVE_SEARCH_RATIO, }, }; @@ -112,9 +112,6 @@ pub struct SemanticSearchResult { pub entity: T, pub score: f64, } - -const EFFECTIVE_SEARCH_RATIO: f64 = 100000.0; // Adjust this ratio based on your needs - impl QueryStream> for SemanticSearchQuery { async fn send( self, diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index 5b1b0ba..e093112 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -17,7 +17,8 @@ pub struct EntityFilter { pub(crate) id: Option>, pub(crate) attributes: Vec, pub(crate) relations: Option, - pub(crate) traverse_relation: Option, + /// traverse relation now in entity directly but eventually for modularity will be standalone to be chained + pub(crate) traverse_relation: Option, /// Used to check if the entity exists in the space (i.e.: the entity /// has at least one attribute in the space). pub(crate) space_id: Option>, @@ -52,10 +53,7 @@ impl EntityFilter { self } - pub fn traverse_relation( - mut self, - traverse_relation: impl Into, - ) -> Self { + pub fn traverse_relation(mut self, traverse_relation: impl Into) -> Self { self.traverse_relation = Some(traverse_relation.into()); self } @@ -181,7 +179,7 @@ impl EntityRelationFilter { /// Filter used to: /// - Traverse to inbound or outbound relation #[derive(Clone, Debug, Default)] -pub struct TraverseRelationFilter { +pub struct TraverseRelation { relation_type_id: Option>, destination_id: Option>, direction: RelationDirection, @@ -189,7 +187,7 @@ pub struct TraverseRelationFilter { version: VersionFilter, } -impl TraverseRelationFilter { +impl TraverseRelation { pub fn relation_type_id(mut self, relation_type_id: impl Into>) -> Self { self.relation_type_id = Some(relation_type_id.into()); self diff --git a/grc20-core/src/mapping/mod.rs b/grc20-core/src/mapping/mod.rs index 9733cc0..dc9f1fd 100644 --- a/grc20-core/src/mapping/mod.rs +++ b/grc20-core/src/mapping/mod.rs @@ -28,6 +28,8 @@ pub use value::{Options, Value, ValueType}; use crate::{error::DatabaseError, indexer_ids}; +pub const EFFECTIVE_SEARCH_RATIO: f64 = 100000.0; + pub fn new_version_index(block_number: u64, idx: usize) -> String { format!("{block_number:016}:{idx:04}") } diff --git a/grc20-core/src/mapping/query_utils/query_builder.rs b/grc20-core/src/mapping/query_utils/query_builder.rs index 1e24f8e..e26bb4d 100644 --- a/grc20-core/src/mapping/query_utils/query_builder.rs +++ b/grc20-core/src/mapping/query_utils/query_builder.rs @@ -163,7 +163,6 @@ impl MatchQuery { pub fn r#rename(mut self, rename: impl Into) -> Self { let rename_clause: Rename = rename.into(); self.rename = Some(rename_clause.name_pair); - self.params.extend(rename_clause.params); self } @@ -239,21 +238,14 @@ impl NamePair { #[derive(Clone, Debug, Default, PartialEq)] pub struct Rename { name_pair: NamePair, - params: HashMap, } impl Rename { pub fn new(name_pair: impl Into) -> Self { Self { name_pair: name_pair.into(), - params: HashMap::new(), } } - - pub fn set_param(mut self, key: impl Into, value: impl Into) -> Self { - self.params.insert(key.into(), value.into()); - self - } } impl Subquery for Rename { @@ -265,9 +257,10 @@ impl Subquery for Rename { } fn params(&self) -> HashMap { - self.params.clone() + HashMap::new() } } + impl Rename { pub fn name_pair(mut self, name_pair: impl Into) -> Self { self.name_pair = name_pair.into(); @@ -284,10 +277,7 @@ impl Rename { impl From for Rename { fn from(rename: NamePair) -> Self { - Self { - name_pair: rename, - params: HashMap::new(), - } + Self { name_pair: rename } } } diff --git a/grc20-core/src/mapping/triple.rs b/grc20-core/src/mapping/triple.rs index 61d504b..c917c9b 100644 --- a/grc20-core/src/mapping/triple.rs +++ b/grc20-core/src/mapping/triple.rs @@ -5,8 +5,11 @@ use neo4rs::{BoltMap, BoltType}; use serde::Deserialize; use crate::{ - block::BlockMetadata, error::DatabaseError, ids, indexer_ids, - mapping::query_utils::query_builder::Subquery, pb, + block::BlockMetadata, + error::DatabaseError, + ids, indexer_ids, + mapping::{query_utils::query_builder::Subquery, EFFECTIVE_SEARCH_RATIO}, + pb, }; use super::{ @@ -678,8 +681,6 @@ pub struct SemanticSearchResult { pub space_version: String, } -const EFFECTIVE_SEARCH_RATIO: f64 = 10000.0; // Adjust this ratio based on your needs - impl QueryStream for SemanticSearchQuery { async fn send( self, diff --git a/mcp-server/resources/search_properties_description.md b/mcp-server/resources/search_properties_description.md index 8f5a804..7b1c41f 100644 --- a/mcp-server/resources/search_properties_description.md +++ b/mcp-server/resources/search_properties_description.md @@ -1,4 +1,4 @@ -This request allows you to search by name for a basic Relation of the Knowledge Graph(KG) like Owners or Authors. This will give back the +This request allows you to search by name for the ATTRIBUTES (properties) that can be used to describe an Entity. ToolCall> search_properties("Authors") diff --git a/mcp-server/resources/search_relation_type_description.md b/mcp-server/resources/search_relation_type_description.md index 55d4091..02134b4 100644 --- a/mcp-server/resources/search_relation_type_description.md +++ b/mcp-server/resources/search_relation_type_description.md @@ -14,11 +14,6 @@ ToolResult> "attribute_name": "Is type property", "attribute_value": "0", "entity_id": "U1uCAzXsRSTP4vFwo1JwJG" - }, - { - "attribute_name": "Name", - "attribute_value": "Works at", - "entity_id": "U1uCAzXsRSTP4vFwo1JwJG" } ], [ diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index 243f777..3147abe 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -3,7 +3,7 @@ use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::{TryStreamExt, future::join_all, pin_mut}; use grc20_core::{ entity::{ - self, Entity, EntityFilter, EntityNode, EntityRelationFilter, utils::TraverseRelationFilter, + self, Entity, EntityFilter, EntityNode, EntityRelationFilter, utils::TraverseRelation, }, mapping::{ Query, QueryStream, RelationEdge, Triple, prop_filter, @@ -194,7 +194,7 @@ impl KnowledgeGraph { } } - #[tool(description = "Search Types")] + #[tool(description = include_str!("../resources/search_type_description.md"))] async fn search_types( &self, #[tool(param)] @@ -255,7 +255,7 @@ impl KnowledgeGraph { )) } - #[tool(description = "Search Relation Types")] + #[tool(description = include_str!("../resources/search_relation_type_description.md"))] async fn search_relation_types( &self, #[tool(param)] @@ -320,7 +320,7 @@ impl KnowledgeGraph { )) } - #[tool(description = "Search Space")] + #[tool(description = include_str!("../resources/search_space_description.md"))] async fn search_space( &self, #[tool(param)] @@ -381,7 +381,7 @@ impl KnowledgeGraph { )) } - #[tool(description = "Search Properties")] + #[tool(description = include_str!("../resources/search_properties_description.md"))] async fn search_properties( &self, #[tool(param)] @@ -442,9 +442,7 @@ impl KnowledgeGraph { )) } - #[tool( - description = "Get entity from a query over the name and traversals based on relation types" - )] + #[tool(description = include_str!("../resources/search_entity_description.md"))] async fn search_entity( &self, #[tool(param)] @@ -481,7 +479,7 @@ impl KnowledgeGraph { |query, filter| { query.filter( EntityFilter::default().traverse_relation( - TraverseRelationFilter::default() + TraverseRelation::default() .relation_type_id(filter.relation_type_id) .direction(match filter.direction { input_types::RelationDirection::From => RelationDirection::From, @@ -528,9 +526,7 @@ impl KnowledgeGraph { ])) } - #[tool( - description = "Get entity from a query over the name and traversals based on relation types names" - )] + #[tool(description = include_str!("../resources/name_search_entity_description.md"))] async fn name_search_entity( &self, #[tool(param)] @@ -549,7 +545,7 @@ impl KnowledgeGraph { .map(|v| v as f64) .collect::>(); - let traversal_filters: Vec> = + let traversal_filters: Vec> = match search_traversal_filter.traversal_filter { Some(traversal_filter) => { join_all(traversal_filter.into_iter().map(|filter| async move { @@ -592,7 +588,7 @@ impl KnowledgeGraph { .into_iter() .map(|sem_search| sem_search.entity.id) .collect(); - Ok(TraverseRelationFilter::default() + Ok(TraverseRelation::default() .direction(match filter.direction { input_types::RelationDirection::From => RelationDirection::From, input_types::RelationDirection::To => RelationDirection::To, @@ -654,7 +650,7 @@ impl KnowledgeGraph { ])) } - #[tool(description = "Get entity by ID with it's attributes and relations")] + #[tool(description = include_str!("../resources/get_entity_info_description.md"))] async fn get_entity_info( &self, #[tool(param)] @@ -761,7 +757,7 @@ impl KnowledgeGraph { ])) } - #[tool(description = "Search for distant or close Relations between 2 entities")] + #[tool(description = include_str!("../resources/get_relations_between_entities_description.md"))] async fn get_relations_between_entities( &self, #[tool(param)] From c2c2e851866711b50431c5c13d37c12ea5145eb6 Mon Sep 17 00:00:00 2001 From: tourtourigny Date: Thu, 3 Jul 2025 09:45:38 -0400 Subject: [PATCH 25/25] fix typos --- grc20-core/src/mapping/entity/utils.rs | 2 +- grc20-core/src/mapping/query_utils/query_builder.rs | 2 +- mcp-server/resources/get_entity_info_description.md | 2 +- mcp-server/resources/search_type_description.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index e093112..faadd08 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -233,7 +233,7 @@ impl TraverseRelation { } }) // rename to change direction of relation - .r#rename(Rename::new(NamePair::new( + .rename(Rename::new(NamePair::new( node_var_curr.clone(), node_var_dest.clone(), ))) diff --git a/grc20-core/src/mapping/query_utils/query_builder.rs b/grc20-core/src/mapping/query_utils/query_builder.rs index e26bb4d..a5ce4d4 100644 --- a/grc20-core/src/mapping/query_utils/query_builder.rs +++ b/grc20-core/src/mapping/query_utils/query_builder.rs @@ -160,7 +160,7 @@ impl MatchQuery { self } - pub fn r#rename(mut self, rename: impl Into) -> Self { + pub fn rename(mut self, rename: impl Into) -> Self { let rename_clause: Rename = rename.into(); self.rename = Some(rename_clause.name_pair); self diff --git a/mcp-server/resources/get_entity_info_description.md b/mcp-server/resources/get_entity_info_description.md index b7ed047..97bf061 100644 --- a/mcp-server/resources/get_entity_info_description.md +++ b/mcp-server/resources/get_entity_info_description.md @@ -1,4 +1,4 @@ -This request allows you to get the detailled information about an Entity with it's ID. You will get the name, description, other attributes, inbound relations and outbound relations of the Entity. +This request allows you to get the detailed information about an Entity with it's ID. You will get the name, description, other attributes, inbound relations and outbound relations of the Entity. The id for San Francisco is: 3qayfdjYyPv1dAYf8gPL5r diff --git a/mcp-server/resources/search_type_description.md b/mcp-server/resources/search_type_description.md index 7ec21a1..478367f 100644 --- a/mcp-server/resources/search_type_description.md +++ b/mcp-server/resources/search_type_description.md @@ -1,6 +1,6 @@ This request allows you to search by name for a basic type of the Knowledge Graph(KG) like Person or Event. This will give back the type with it's name, id and description. -ToolCall> search_types("University") +ToolCall> search_type("University") ToolResult> ``` [