diff --git a/README.md b/README.md index 8a422dc6e..1e683aff8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Both versions exist purely as research projects used to explore new ideas and ga # What’s next -An important aspect of KM² is how we are building the next memory prototype. In parallel, our team is developing [Amplifier](https://github.com/microsoft/amplifier/tree/next), a platform for metacognitive AI engineering. We use Amplifier to build Amplifier itself — and in the same way, we are using Amplifier to build the next generation of Kernel Memory. +An important aspect of KM² is how we are building the next memory prototype. In parallel, our team is developing [Amplifier](https://github.com/microsoft/amplifier), a platform for metacognitive AI engineering. We use Amplifier to build Amplifier itself — and in a similar way, we are using AI and Amplifier concepts to build the next generation of Kernel Memory. KM² will focus on the following areas, which will be documented in more detail when ready: - quality of content generated @@ -76,4 +76,4 @@ gh api repos/:owner/:repo/contributors --paginate --jq ' | Valkozaur | vicperdana | walexee | aportillo83 | carlodek | KSemenenko | | [Valkozaur](https://github.com/Valkozaur) | [vicperdana](https://github.com/vicperdana) | [walexee](https://github.com/walexee) | [aportillo83](https://github.com/aportillo83) | [carlodek](https://github.com/carlodek) | [KSemenenko](https://github.com/KSemenenko) | | roldengarm | snakex64 | -| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) | \ No newline at end of file +| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) | diff --git a/src/Core/Storage/Models/ContentDtoWithNode.cs b/src/Core/Storage/Models/ContentDtoWithNode.cs new file mode 100644 index 000000000..7cc168bfe --- /dev/null +++ b/src/Core/Storage/Models/ContentDtoWithNode.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +namespace KernelMemory.Core.Storage.Models; + +/// +/// Content DTO with node information included. +/// Used by CLI commands to show which node the content came from. +/// +public class ContentDtoWithNode +{ + public string Id { get; set; } = string.Empty; + public string Node { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string MimeType { get; set; } = string.Empty; + public long ByteSize { get; set; } + public DateTimeOffset ContentCreatedAt { get; set; } + public DateTimeOffset RecordCreatedAt { get; set; } + public DateTimeOffset RecordUpdatedAt { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Tags { get; set; } = []; + public Dictionary Metadata { get; set; } = new(); + + /// + /// Creates a ContentDtoWithNode from a ContentDto and node ID. + /// + /// The content DTO to wrap. + /// The node ID to include. + /// A new ContentDtoWithNode instance. + public static ContentDtoWithNode FromContentDto(ContentDto content, string nodeId) + { + return new ContentDtoWithNode + { + Id = content.Id, + Node = nodeId, + Content = content.Content, + MimeType = content.MimeType, + ByteSize = content.ByteSize, + ContentCreatedAt = content.ContentCreatedAt, + RecordCreatedAt = content.RecordCreatedAt, + RecordUpdatedAt = content.RecordUpdatedAt, + Title = content.Title, + Description = content.Description, + Tags = content.Tags, + Metadata = content.Metadata + }; + } +} diff --git a/src/Main/CLI/Commands/GetCommand.cs b/src/Main/CLI/Commands/GetCommand.cs index b8bd656b1..0bfc1095d 100644 --- a/src/Main/CLI/Commands/GetCommand.cs +++ b/src/Main/CLI/Commands/GetCommand.cs @@ -70,24 +70,8 @@ public override async Task ExecuteAsync( } // Wrap result with node information - var response = new - { - id = result.Id, - node = node.Id, - content = result.Content, - mimeType = result.MimeType, - byteSize = result.ByteSize, - contentCreatedAt = result.ContentCreatedAt, - recordCreatedAt = result.RecordCreatedAt, - recordUpdatedAt = result.RecordUpdatedAt, - title = result.Title, - description = result.Description, - tags = result.Tags, - metadata = result.Metadata - }; - - // If --full flag is set, ensure verbose mode for human formatter - // For JSON/YAML, all fields are always included + var response = Core.Storage.Models.ContentDtoWithNode.FromContentDto(result, node.Id); + formatter.Format(response); return Constants.ExitCodeSuccess; diff --git a/src/Main/CLI/Commands/ListCommand.cs b/src/Main/CLI/Commands/ListCommand.cs index 2a1a4e98f..403b3649a 100644 --- a/src/Main/CLI/Commands/ListCommand.cs +++ b/src/Main/CLI/Commands/ListCommand.cs @@ -75,21 +75,8 @@ public override async Task ExecuteAsync( var items = await service.ListAsync(settings.Skip, settings.Take, CancellationToken.None).ConfigureAwait(false); // Wrap items with node information - var itemsWithNode = items.Select(item => new - { - id = item.Id, - node = node.Id, - content = item.Content, - mimeType = item.MimeType, - byteSize = item.ByteSize, - contentCreatedAt = item.ContentCreatedAt, - recordCreatedAt = item.RecordCreatedAt, - recordUpdatedAt = item.RecordUpdatedAt, - title = item.Title, - description = item.Description, - tags = item.Tags, - metadata = item.Metadata - }); + var itemsWithNode = items.Select(item => + Core.Storage.Models.ContentDtoWithNode.FromContentDto(item, node.Id)); // Format list with pagination info formatter.FormatList(itemsWithNode, totalCount, settings.Skip, settings.Take); diff --git a/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs b/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs index 5e92198d2..bbff39234 100644 --- a/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs +++ b/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs @@ -41,6 +41,9 @@ public void Format(object data) switch (data) { + case Core.Storage.Models.ContentDtoWithNode contentWithNode: + this.FormatContentWithNode(contentWithNode); + break; case ContentDto content: this.FormatContent(content); break; @@ -82,7 +85,11 @@ public void FormatList(IEnumerable items, long totalCount, int skip, int t var itemsList = items.ToList(); - if (typeof(T) == typeof(ContentDto)) + if (typeof(T) == typeof(Core.Storage.Models.ContentDtoWithNode)) + { + this.FormatContentWithNodeList(itemsList.Cast(), totalCount, skip, take); + } + else if (typeof(T) == typeof(ContentDto)) { this.FormatContentList(itemsList.Cast(), totalCount, skip, take); } @@ -278,4 +285,121 @@ private void FormatGenericList(IEnumerable items, long totalCount, int ski AnsiConsole.WriteLine(item?.ToString() ?? string.Empty); } } + + private void FormatContentWithNode(Core.Storage.Models.ContentDtoWithNode content) + { + var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase); + var isVerbose = this.Verbosity.Equals("verbose", StringComparison.OrdinalIgnoreCase); + + if (isQuiet) + { + // Quiet mode: just the ID + AnsiConsole.WriteLine(content.Id); + return; + } + + var table = new Table(); + table.Border(TableBorder.Rounded); + table.AddColumn("Property"); + table.AddColumn("Value"); + + table.AddRow("[yellow]Node[/]", Markup.Escape(content.Node)); + table.AddRow("[yellow]ID[/]", Markup.Escape(content.Id)); + + // Truncate content unless verbose + var displayContent = content.Content; + if (!isVerbose && displayContent.Length > Constants.MaxContentDisplayLength) + { + displayContent = string.Concat(displayContent.AsSpan(0, Constants.MaxContentDisplayLength), "..."); + } + table.AddRow("[yellow]Content[/]", Markup.Escape(displayContent)); + + if (!string.IsNullOrEmpty(content.Title)) + { + table.AddRow("[yellow]Title[/]", Markup.Escape(content.Title)); + } + + if (!string.IsNullOrEmpty(content.Description)) + { + table.AddRow("[yellow]Description[/]", Markup.Escape(content.Description)); + } + + if (content.Tags.Length > 0) + { + table.AddRow("[yellow]Tags[/]", Markup.Escape(string.Join(", ", content.Tags))); + } + + if (isVerbose) + { + table.AddRow("[yellow]MimeType[/]", Markup.Escape(content.MimeType)); + table.AddRow("[yellow]Size[/]", $"{content.ByteSize} bytes"); + table.AddRow("[yellow]ContentCreatedAt[/]", content.ContentCreatedAt.ToString("O")); + table.AddRow("[yellow]RecordCreatedAt[/]", content.RecordCreatedAt.ToString("O")); + table.AddRow("[yellow]RecordUpdatedAt[/]", content.RecordUpdatedAt.ToString("O")); + + if (content.Metadata.Count > 0) + { + var metadataStr = string.Join(", ", content.Metadata.Select(kvp => $"{kvp.Key}={kvp.Value}")); + table.AddRow("[yellow]Metadata[/]", Markup.Escape(metadataStr)); + } + } + + AnsiConsole.Write(table); + } + + private void FormatContentWithNodeList(IEnumerable contents, long totalCount, int skip, int take) + { + var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase); + var contentsList = contents.ToList(); + + // Check if list is empty + if (contentsList.Count == 0) + { + if (this._useColors) + { + AnsiConsole.MarkupLine("[dim]No content found[/]"); + } + else + { + AnsiConsole.WriteLine("No content found"); + } + return; + } + + if (isQuiet) + { + // Quiet mode: just IDs + foreach (var content in contentsList) + { + AnsiConsole.WriteLine(content.Id); + } + return; + } + + // Show pagination info + AnsiConsole.MarkupLine($"[cyan]Showing {contentsList.Count} of {totalCount} items (skip: {skip})[/]"); + AnsiConsole.WriteLine(); + + // Create table + var table = new Table(); + table.Border(TableBorder.Rounded); + table.AddColumn("[yellow]Node[/]"); + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Content Preview[/]"); + + foreach (var content in contentsList) + { + var preview = content.Content.Length > 50 + ? string.Concat(content.Content.AsSpan(0, 50), "...") + : content.Content; + + table.AddRow( + Markup.Escape(content.Node), + Markup.Escape(content.Id), + Markup.Escape(preview) + ); + } + + AnsiConsole.Write(table); + } }