-
Notifications
You must be signed in to change notification settings - Fork 0
Dev #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev #2
Changes from all commits
0aea321
b89bd66
86c77de
99b6b58
83d4834
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,20 +2,13 @@ | |||||
| using Microsoft.Azure.Functions.Worker; | ||||||
| using Microsoft.Azure.Functions.Worker.Extensions.Mcp; | ||||||
| using System.ComponentModel; | ||||||
| using System; // for Guid | ||||||
| using CentralMemoryMcp.Functions.Services; | ||||||
|
|
||||||
| namespace CentralMemoryMcp.Functions.Functions; | ||||||
|
|
||||||
| public class GraphFunctions | ||||||
| public class GraphFunctions(IKnowledgeGraphService graph, IRelationService relations) | ||||||
| { | ||||||
| private readonly IKnowledgeGraphService _graph; | ||||||
| private readonly IRelationService _relations; | ||||||
|
|
||||||
| public GraphFunctions(IKnowledgeGraphService graph, IRelationService relations) | ||||||
| { | ||||||
| _graph = graph; | ||||||
| _relations = relations; | ||||||
| } | ||||||
| private readonly IRelationService _relations = relations; | ||||||
|
|
||||||
| [Function(nameof(ReadGraph))] | ||||||
| public async Task<object> ReadGraph( | ||||||
|
|
@@ -24,7 +17,7 @@ public async Task<object> ReadGraph( | |||||
| [McpToolProperty("workspaceName", "The unique identifier of the workspace.", isRequired: true)] | ||||||
| string workspaceName) | ||||||
| { | ||||||
| var entities = await _graph.ReadGraphAsync(workspaceName); | ||||||
| var entities = await graph.ReadGraphAsync(workspaceName); | ||||||
| var relations = await _relations.GetRelationsForWorkspaceAsync(workspaceName); | ||||||
|
|
||||||
| return new | ||||||
|
|
@@ -55,7 +48,7 @@ public async Task<object> UpsertEntity( | |||||
| request.Observations ?? [], | ||||||
| request.Metadata); | ||||||
|
|
||||||
| model = await _graph.UpsertEntityAsync(model); // capture potentially reused Id | ||||||
| model = await graph.UpsertEntityAsync(model); // capture potentially reused Id | ||||||
| return new { success = true, id = model.Id, workspace = model.WorkspaceName, name = model.Name }; | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -80,12 +73,12 @@ public async Task<object> UpsertRelation( | |||||
|
|
||||||
| if (fromId == Guid.Empty && !string.IsNullOrWhiteSpace(request.From)) | ||||||
| { | ||||||
| var entity = await _graph.GetEntityAsync(request.WorkspaceName, request.From); | ||||||
| var entity = await graph.GetEntityAsync(request.WorkspaceName, request.From); | ||||||
| if (entity is not null) fromId = entity.Id; else return new { success = false, message = $"Source entity '{request.From}' not found." }; | ||||||
| } | ||||||
| if (toId == Guid.Empty && !string.IsNullOrWhiteSpace(request.To)) | ||||||
| { | ||||||
| var entity = await _graph.GetEntityAsync(request.WorkspaceName, request.To); | ||||||
| var entity = await graph.GetEntityAsync(request.WorkspaceName, request.To); | ||||||
| if (entity is not null) toId = entity.Id; else return new { success = false, message = $"Target entity '{request.To}' not found." }; | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -116,14 +109,14 @@ public async Task<object> GetEntityRelations( | |||||
| [McpToolProperty("entityName", "Legacy entity name (used if entityId not provided).", isRequired: false)] | ||||||
| string? entityName) | ||||||
| { | ||||||
| Guid resolvedId = Guid.Empty; | ||||||
| Guid resolvedId; | ||||||
|
||||||
| Guid resolvedId; | |
| Guid resolvedId = Guid.Empty; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| using Azure; | ||
| using Azure.Data.Tables; | ||
| using CentralMemoryMcp.Functions.Models; | ||
| using CentralMemoryMcp.Functions.Storage; | ||
|
|
||
| namespace CentralMemoryMcp.Functions; | ||
| namespace CentralMemoryMcp.Functions.Services; | ||
|
|
||
| public interface IRelationService | ||
| { | ||
|
|
@@ -19,7 +20,7 @@ | |
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| // Check for existing relation (same workspace, from, to, type) | ||
| string filter = $"PartitionKey eq '{model.WorkspaceName}' and FromEntityId eq '{model.FromEntityId.ToString("N")}' and ToEntityId eq '{model.ToEntityId.ToString("N")}' and RelationType eq '{EscapeFilterValue(model.RelationType)}'"; | ||
| var filter = $"PartitionKey eq '{model.WorkspaceName}' and FromEntityId eq '{model.FromEntityId:N}' and ToEntityId eq '{model.ToEntityId:N}' and RelationType eq '{EscapeFilterValue(model.RelationType)}'"; | ||
|
||
| await foreach(var e in table.QueryAsync<TableEntity>(filter: filter, maxPerPage:1, cancellationToken: ct)) | ||
| { | ||
| // Reuse its Id | ||
|
|
@@ -47,17 +48,142 @@ | |
| var table = await storage.GetRelationsTableAsync(ct); | ||
| try | ||
| { | ||
| var partitionKey = workspaceName; | ||
| var rowKey = relationId.ToString("N"); | ||
| var response = await table.GetEntityAsync<TableEntity>(partitionKey, rowKey, cancellationToken: ct); | ||
| var e = response.Value; | ||
| var response = await table.GetEntityAsync<TableEntity>(workspaceName, relationId.ToString("N"), cancellationToken: ct); | ||
| var model = new RelationModel( | ||
| response.Value.GetString("WorkspaceName")!, | ||
| Guid.Parse(response.Value.GetString("FromEntityId")!), | ||
| Guid.Parse(response.Value.GetString("ToEntityId")!), | ||
| response.Value.GetString("RelationType")!, | ||
| response.Value.GetString("Metadata")) | ||
| { | ||
| Id = relationId | ||
| }; | ||
| return model; | ||
| } | ||
| catch (RequestFailedException ex) when (ex.Status == 404) | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| public async Task<List<RelationModel>> GetRelationsFromEntityAsync(string workspaceName, Guid fromEntityId, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| var results = new List<RelationModel>(); | ||
| var fromIdStr = fromEntityId.ToString("N"); | ||
| await foreach (var e in table.QueryAsync<TableEntity>( | ||
| filter: $"PartitionKey eq '{workspaceName}' and FromEntityId eq '{fromIdStr}'", | ||
|
Comment on lines
+74
to
+75
|
||
| cancellationToken: ct)) | ||
| { | ||
| var relationId = Guid.TryParse(e.GetString("Id"), out var rid) ? rid : Guid.NewGuid(); | ||
| var model = new RelationModel( | ||
| e.GetString("WorkspaceName")!, | ||
| Guid.Parse(e.GetString("FromEntityId")!), | ||
| Guid.Parse(e.GetString("ToEntityId")!), | ||
| e.GetString("RelationType")!, | ||
| e.GetString("Metadata")); | ||
| model.Id = relationId; | ||
| results.Add(model); | ||
| } | ||
| return results; | ||
| } | ||
|
|
||
| public async Task<List<RelationModel>> GetRelationsForWorkspaceAsync(string workspaceName, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| var results = new List<RelationModel>(); | ||
| await foreach (var e in table.QueryAsync<TableEntity>( | ||
| filter: $"PartitionKey eq '{workspaceName}'", | ||
|
Comment on lines
+95
to
+96
|
||
| cancellationToken: ct)) | ||
| { | ||
| var relationId = Guid.TryParse(e.GetString("Id"), out var rid) ? rid : Guid.NewGuid(); | ||
| var model = new RelationModel( | ||
| e.GetString("WorkspaceName")!, | ||
| Guid.Parse(e.GetString("FromEntityId")!), | ||
| Guid.Parse(e.GetString("ToEntityId")!), | ||
| e.GetString("RelationType")!, | ||
| e.GetString("Metadata")); | ||
| model.Id = relationId; | ||
| results.Add(model); | ||
| } | ||
| return results; | ||
| } | ||
|
|
||
| public async Task DeleteRelationAsync(string workspaceName, Guid relationId, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| try | ||
| { | ||
| await table.DeleteEntityAsync(workspaceName, relationId.ToString("N"), cancellationToken: ct); | ||
| } | ||
| catch (RequestFailedException ex) when (ex.Status == 404) | ||
| { | ||
| // not found; ignore | ||
| } | ||
| } | ||
|
|
||
| private static string EscapeFilterValue(string value) => value.Replace("'", "''"); | ||
| } | ||
| using Azure; | ||
|
Check failure on line 127 in CentralMemoryMcp.Functions/Services/RelationService.cs
|
||
| using Azure.Data.Tables; | ||
|
Check failure on line 128 in CentralMemoryMcp.Functions/Services/RelationService.cs
|
||
| using CentralMemoryMcp.Functions.Models; | ||
|
Check failure on line 129 in CentralMemoryMcp.Functions/Services/RelationService.cs
|
||
| using CentralMemoryMcp.Functions.Storage; | ||
|
Check failure on line 130 in CentralMemoryMcp.Functions/Services/RelationService.cs
|
||
|
|
||
| namespace CentralMemoryMcp.Functions.Services; | ||
|
|
||
| public interface IRelationService | ||
| { | ||
| Task<RelationModel> UpsertRelationAsync(RelationModel model, CancellationToken ct = default); | ||
| Task<RelationModel?> GetRelationAsync(string workspaceName, Guid relationId, CancellationToken ct = default); | ||
| Task<List<RelationModel>> GetRelationsFromEntityAsync(string workspaceName, Guid fromEntityId, CancellationToken ct = default); | ||
| Task<List<RelationModel>> GetRelationsForWorkspaceAsync(string workspaceName, CancellationToken ct = default); | ||
| Task DeleteRelationAsync(string workspaceName, Guid relationId, CancellationToken ct = default); | ||
| } | ||
|
|
||
| public class RelationService(ITableStorageService storage) : IRelationService | ||
| { | ||
| public async Task<RelationModel> UpsertRelationAsync(RelationModel model, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| // Check for existing relation (same workspace, from, to, type) | ||
| var filter = $"PartitionKey eq '{model.WorkspaceName}' and FromEntityId eq '{model.FromEntityId:N}' and ToEntityId eq '{model.ToEntityId:N}' and RelationType eq '{EscapeFilterValue(model.RelationType)}'"; | ||
| await foreach(var e in table.QueryAsync<TableEntity>(filter: filter, maxPerPage:1, cancellationToken: ct)) | ||
| { | ||
| // Reuse its Id | ||
| if (e.TryGetValue("Id", out var idObj) && idObj is string idStr && Guid.TryParse(idStr, out var existingId)) | ||
| { | ||
| model.Id = existingId; | ||
| } | ||
| break; | ||
| } | ||
| var entity = new TableEntity(model.PartitionKey, model.RowKey) | ||
| { | ||
| {"Id", model.Id.ToString("N")}, | ||
| {"WorkspaceName", model.WorkspaceName}, | ||
| {"FromEntityId", model.FromEntityId.ToString("N")}, | ||
| {"ToEntityId", model.ToEntityId.ToString("N")}, | ||
| {"RelationType", model.RelationType}, | ||
| {"Metadata", model.Metadata ?? string.Empty} | ||
| }; | ||
| await table.UpsertEntityAsync(entity, TableUpdateMode.Replace, ct); | ||
| return model; | ||
| } | ||
|
|
||
| public async Task<RelationModel?> GetRelationAsync(string workspaceName, Guid relationId, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| try | ||
| { | ||
| var response = await table.GetEntityAsync<TableEntity>(workspaceName, relationId.ToString("N"), cancellationToken: ct); | ||
| var model = new RelationModel( | ||
| response.Value.GetString("WorkspaceName")!, | ||
| Guid.Parse(response.Value.GetString("FromEntityId")!), | ||
| Guid.Parse(response.Value.GetString("ToEntityId")!), | ||
| response.Value.GetString("RelationType")!, | ||
| response.Value.GetString("Metadata")) | ||
| { | ||
| Id = relationId | ||
| }; | ||
| return model; | ||
| } | ||
| catch (RequestFailedException ex) when (ex.Status == 404) | ||
|
|
@@ -112,11 +238,9 @@ | |
| public async Task DeleteRelationAsync(string workspaceName, Guid relationId, CancellationToken ct = default) | ||
| { | ||
| var table = await storage.GetRelationsTableAsync(ct); | ||
| var partitionKey = workspaceName; | ||
| var rowKey = relationId.ToString("N"); | ||
| try | ||
| { | ||
| await table.DeleteEntityAsync(partitionKey, rowKey, cancellationToken: ct); | ||
| await table.DeleteEntityAsync(workspaceName, relationId.ToString("N"), cancellationToken: ct); | ||
| } | ||
| catch (RequestFailedException ex) when (ex.Status == 404) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,10 +4,10 @@ | |||||
| "mcp": { | ||||||
| "instructions": "This server offers LLMs long-term memory storage capabilities about users, projects, and anything else needed. This should not entirely replace using public information tooling like Microsoft Docs or Context7.", | ||||||
| "serverName": "Central Memory MCP", | ||||||
| "serverVersion": "1.0.0", | ||||||
| "serverVersion": "0.5.1", | ||||||
|
||||||
| "serverVersion": "0.5.1", | |
| "serverVersion": "1.0.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
graphparameter is used directly in method calls (lines 20, 51, 76, 81, 119) but is not stored in a field. However, a field_relationsis created for therelationsparameter at line 11. For consistency, either both should be stored as fields, or neither should be (using parameters directly throughout). The current approach is inconsistent.