Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 94 additions & 19 deletions dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;

Expand Down Expand Up @@ -37,49 +37,98 @@ public class TextMemorySkill
/// </summary>
public const string KeyParam = "key";

/// <summary>
/// Name of the context variable used to specify the number of memories to recall
/// </summary>
public const string LimitParam = "limit";

private const string DefaultCollection = "generic";
private const string DefaultRelevance = "0.75";
private const string DefaultLimit = "1";

/// <summary>
/// Recall a fact from the long term memory
/// Key-based lookup for a specific memory
/// </summary>
/// <example>
/// SKContext[TextMemorySkill.KeyParam] = "countryInfo1"
/// {{memory.retrieve }}
/// </example>
/// <param name="context">Contains the 'collection' containing the memory to retrieve and the `key` associated with it.</param>
[SKFunction("Key-based lookup for a specific memory")]
[SKFunctionName("Retrieve")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the memory to retrieve",
DefaultValue = DefaultCollection)]
[SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the memory to retrieve")]
public async Task<string> RetrieveAsync(SKContext context)
{
var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection;
Verify.NotEmpty(collection, "Memory collection not defined");

var key = context.Variables.ContainsKey(KeyParam) ? context[KeyParam] : string.Empty;
Verify.NotEmpty(key, "Memory key not defined");

context.Log.LogTrace("Recalling memory with key '{0}' from collection '{1}'", key, collection);

var memory = await context.Memory.GetAsync(collection, key);

return memory?.Text ?? string.Empty;
}

/// <summary>
/// Semantic search and return up to N memories related to the input text
/// </summary>
/// <example>
/// SKContext["input"] = "what is the capital of France?"
/// {{memory.recall $input }} => "Paris"
/// </example>
/// <param name="ask">The information to retrieve</param>
/// <param name="context">Contains the 'collection' to search for information and 'relevance' score</param>
[SKFunction("Recall a fact from the long term memory")]
/// <param name="text">The input text to find related memories for</param>
/// <param name="context">Contains the 'collection' to search for the topic and 'relevance' score</param>
[SKFunction("Semantic search and return up to N memories related to the input text")]
[SKFunctionName("Recall")]
[SKFunctionInput(Description = "The information to retrieve")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection where to search for information", DefaultValue = DefaultCollection)]
[SKFunctionInput(Description = "The input text to find related memories for")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection to search", DefaultValue = DefaultCollection)]
[SKFunctionContextParameter(Name = RelevanceParam, Description = "The relevance score, from 0.0 to 1.0, where 1.0 means perfect match",
DefaultValue = DefaultRelevance)]
public async Task<string> RecallAsync(string ask, SKContext context)
[SKFunctionContextParameter(Name = LimitParam, Description = "The maximum number of relevant memories to recall", DefaultValue = DefaultLimit)]
public string Recall(string text, SKContext context)
{
var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection;
Verify.NotEmpty(collection, "Memory collection not defined");
Verify.NotEmpty(collection, "Memories collection not defined");

var relevance = context.Variables.ContainsKey(RelevanceParam) ? context[RelevanceParam] : DefaultRelevance;
if (string.IsNullOrWhiteSpace(relevance)) { relevance = DefaultRelevance; }

context.Log.LogTrace("Searching memory for '{0}', collection '{1}', relevance '{2}'", ask, collection, relevance);
var limit = context.Variables.ContainsKey(LimitParam) ? context[LimitParam] : DefaultLimit;
if (string.IsNullOrWhiteSpace(limit)) { relevance = DefaultLimit; }

context.Log.LogTrace("Searching memories in collection '{0}', relevance '{1}'", collection, relevance);

// TODO: support locales, e.g. "0.7" and "0,7" must both work
MemoryQueryResult? memory = await context.Memory
.SearchAsync(collection, ask, limit: 1, minRelevanceScore: float.Parse(relevance, CultureInfo.InvariantCulture))
.FirstOrDefaultAsync();
int limitInt = int.Parse(limit, CultureInfo.InvariantCulture);
var memories = context.Memory
.SearchAsync(collection, text, limitInt, minRelevanceScore: float.Parse(relevance, CultureInfo.InvariantCulture))
.ToEnumerable();

if (memory == null)
context.Log.LogTrace("Done looking for memories in collection '{0}')", collection);

string resultString;

if (limitInt == 1)
{
context.Log.LogWarning("Memory not found in collection: {0}", collection);
var memory = memories.FirstOrDefault();
resultString = (memory != null) ? memory.Text : string.Empty;
}
else
{
context.Log.LogTrace("Memory found (collection: {0})", collection);
resultString = JsonSerializer.Serialize(memories.Select(x => x.Text));
}

if (resultString.Length == 0)
{
context.Log.LogWarning("Memories not found in collection: {0}", collection);
}

return memory != null ? memory.Text : string.Empty;
return resultString;
}

/// <summary>
Expand All @@ -95,8 +144,8 @@ public async Task<string> RecallAsync(string ask, SKContext context)
[SKFunction("Save information to semantic memory")]
[SKFunctionName("Save")]
[SKFunctionInput(Description = "The information to save")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection where to save the information", DefaultValue = DefaultCollection)]
[SKFunctionContextParameter(Name = KeyParam, Description = "The key to save the information")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the information to save", DefaultValue = DefaultCollection)]
[SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the information to save")]
public async Task SaveAsync(string text, SKContext context)
{
var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection;
Expand All @@ -109,4 +158,30 @@ public async Task SaveAsync(string text, SKContext context)

await context.Memory.SaveInformationAsync(collection, text: text, id: key);
}

/// <summary>
/// Remove specific memory
/// </summary>
/// <example>
/// SKContext[TextMemorySkill.KeyParam] = "countryInfo1"
/// {{memory.remove }}
/// </example>
/// <param name="context">Contains the 'collection' containing the memory to remove.</param>
[SKFunction("Remove specific memory")]
[SKFunctionName("Remove")]
[SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the memory to remove",
DefaultValue = DefaultCollection)]
[SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the memory to remove")]
public async Task RemoveAsync(SKContext context)
{
var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection;
Verify.NotEmpty(collection, "Memory collection not defined");

var key = context.Variables.ContainsKey(KeyParam) ? context[KeyParam] : string.Empty;
Verify.NotEmpty(key, "Memory key not defined");

context.Log.LogTrace("Removing memory from collection '{0}'", collection);

await context.Memory.RemoveAsync(collection, key);
}
}
10 changes: 10 additions & 0 deletions dotnet/src/SemanticKernel/Memory/ISemanticTextMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ public Task SaveReferenceAsync(
/// <returns>Memory record, or null when nothing is found</returns>
public Task<MemoryQueryResult?> GetAsync(string collection, string key, CancellationToken cancel = default);

/// <summary>
/// Remove a memory by key.
/// For local memories the key is the "id" used when saving the record.
/// For external reference, the key is the "URI" used when saving the record.
/// </summary>
/// <param name="collection">Collection to search</param>
/// <param name="key">Unique memory record identifier</param>
/// <param name="cancel">Cancellation token</param>
public Task RemoveAsync(string collection, string key, CancellationToken cancel = default);

/// <summary>
/// Find some information in memory
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions dotnet/src/SemanticKernel/Memory/NullMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ public Task SaveReferenceAsync(
return Task.FromResult(null as MemoryQueryResult);
}

/// <inheritdoc/>
public Task RemoveAsync(
string collection,
string key,
CancellationToken cancel = default)
{
return Task.CompletedTask;
}

/// <inheritdoc/>
public IAsyncEnumerable<MemoryQueryResult> SearchAsync(
string collection,
Expand Down
9 changes: 9 additions & 0 deletions dotnet/src/SemanticKernel/Memory/SemanticTextMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ public async Task SaveReferenceAsync(
return MemoryQueryResult.FromMemoryRecord(result, 1);
}

/// <inheritdoc/>
public async Task RemoveAsync(
string collection,
string key,
CancellationToken cancel = default)
{
await this._storage.RemoveAsync(collection, key, cancel);
}

/// <inheritdoc/>
public async IAsyncEnumerable<MemoryQueryResult> SearchAsync(
string collection,
Expand Down
82 changes: 63 additions & 19 deletions samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static async Task RunAsync()
// ========= Store memories using semantic function =========

// Add Memory as a skill for other functions
var memorySkill = new TextMemorySkill();
kernel.ImportSkill(new TextMemorySkill());

// Build a semantic function that saves info to memory
Expand All @@ -47,24 +48,45 @@ public static async Task RunAsync()
context["info"] = "My family is from New York";
await memorySaver.InvokeAsync(context);

// ========= Test memory =========
// ========= Test memory remember =========
Console.WriteLine("========= Example: Recalling a Memory =========");

await AnswerAsync("where did I grow up?", kernel);
await AnswerAsync("where do I live?", kernel);
context[TextMemorySkill.KeyParam] = "info1";
var answer = await memorySkill.RetrieveAsync(context);
Console.WriteLine("Memory associated with 'info1': {0}", answer);
/*
Output:
"Memory associated with 'info1': My name is Andrea
*/

// ========= Test memory recall =========
Console.WriteLine("========= Example: Recalling an Idea =========");

context[TextMemorySkill.LimitParam] = "2";
string ask = "where did I grow up?";
answer = memorySkill.Recall(ask, context);
Console.WriteLine("Ask: {0}", ask);
Console.WriteLine("Answer:\n{0}", answer);

ask = "where do I live?";
answer = memorySkill.Recall(ask, context);
Console.WriteLine("Ask: {0}", ask);
Console.WriteLine("Answer:\n{0}", answer);

/*
Output:

Ask: where did I grow up?
Fact 1: My family is from New York (relevance: 0.8202760217073308)
Fact 2: I've been living in Seattle since 2005 (relevance: 0.7923238361094278)
Answer:
["My family is from New York","I\u0027ve been living in Seattle since 2005"]

Ask: where do I live?
Fact 1: I've been living in Seattle since 2005 (relevance: 0.8010884368220728)
Fact 2: My family is from New York (relevance: 0.785718105747128)
Answer:
["I\u0027ve been living in Seattle since 2005","My family is from New York"]
*/

// ========= Use memory in a semantic function =========
Console.WriteLine("========= Example: Using Recall in a Semantic Function =========");

// Build a semantic function that uses memory to find facts
const string RECALL_FUNCTION_DEFINITION = @"
Expand Down Expand Up @@ -95,18 +117,40 @@ Do I live in the same town where I grew up?

No, I do not live in the same town where I grew up since my family is from New York and I have been living in Seattle since 2005.
*/
}

private static async Task AnswerAsync(string ask, IKernel kernel)
{
Console.WriteLine($"Ask: {ask}");
var memories = kernel.Memory.SearchAsync(MemoryCollectionName, ask, limit: 2, minRelevanceScore: 0.6);
var i = 0;
await foreach (MemoryQueryResult memory in memories)
{
Console.WriteLine($" Fact {++i}: {memory.Text} (relevance: {memory.Relevance})");
}

Console.WriteLine();
// ========= Remove a memory =========
Console.WriteLine("========= Example: Forgetting a Memory =========");

context["fact1"] = "What is my name?";
context["fact2"] = "What do I do for a living?";
context["query"] = "Tell me a bit about myself";
context[TextMemorySkill.RelevanceParam] = ".75";

result = await aboutMeOracle.InvokeAsync(context);

Console.WriteLine(context["query"] + "\n");
Console.WriteLine(result);

/*
Approximate Output:
Tell me a bit about myself

My name is Andrea and my family is from New York. I work as a tourist operator.
*/

context[TextMemorySkill.KeyParam] = "info1";
await memorySkill.RemoveAsync(context);

result = await aboutMeOracle.InvokeAsync(context);

Console.WriteLine(context["query"] + "\n");
Console.WriteLine(result);

/*
Approximate Output:
Tell me a bit about myself

I'm from a family originally from New York and I work as a tourist operator. I've been living in Seattle since 2005.
*/
}
}
2 changes: 1 addition & 1 deletion samples/notebooks/dotnet/6-memory-and-embeddings.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
"This is done by using the `TextMemorySkill` which exposes the `recall` native function.\n",
"\n",
"`recall` takes an input ask and performs a similarity search on the contents that have\n",
"been embedded in the Memory Store and returns the most relevant memory. "
"been embedded in the Memory Store. By default, `recall` returns the most relevant memory."
]
},
{
Expand Down