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
9 changes: 9 additions & 0 deletions dotnet/typeagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This **sample code** is in early stage experimental developement with **frequent

Working towards .NET versions of the following **TypeAgent** packages:
* [KnowPro](../../ts/packages/knowPro/README.md)
* [Memory](../../ts/packages/memory/README.md)
* [AIClient](../../ts/packages/aiclient/README.md)
* [TypeAgent Common Libs](../../ts/packages/typeagent/README.md)

Expand All @@ -13,11 +14,19 @@ TypeAgent.NET also incorporates [TypeChat.NET](https://github.com/microsoft/type
KnowPro.NET will implement Structured RAG in C# for .NET platforms. This work is in curently in progress

It will improve on the Typescript implementation in the following ways:
* Storage and indexing using Storage providers
* Fully asynchronous
* Operators are/will be reworked for more efficient async operation
* Asynchronous storage providers with improved Sql schemas
* Larger index sizes

Libraries:
* [KnowPro](./src/knowpro): KnowPro Core
* [KnowProStorage](./src/knowproStorage): KnowPro Storage Providers
* [Conversation Memory](./src/conversationMemory): Implementations of memory types using KnowPro.NET
* [Vector](./src/vector)
* [AIClient](./src/aiclient)

## Trademarks

This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
Expand Down
1 change: 1 addition & 0 deletions dotnet/typeagent/examples/knowProConsole/Includes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
global using TypeAgent.AIClient;
global using TypeAgent.KnowPro;
global using TypeAgent.KnowPro.KnowledgeExtractor;
global using TypeAgent.KnowPro.Answer;
global using TypeAgent.KnowPro.Storage.Local;
global using TypeAgent.KnowPro.Storage.Sqlite;
global using TypeAgent.ConversationMemory;
Expand Down
42 changes: 41 additions & 1 deletion dotnet/typeagent/examples/knowProConsole/TestCommands.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using TypeAgent.KnowPro.Answer;
using TypeAgent.KnowPro.Lang;

namespace KnowProConsole;
Expand All @@ -25,6 +26,7 @@ public IList<Command> GetCommands()
SearchLangDef(),
KnowledgeDef(),
BuildIndexDef(),
AnswerDef()
];
}

Expand Down Expand Up @@ -351,7 +353,6 @@ await KnowProWriter.WriteConversationSearchResultsAsync(
}
}


private Command KnowledgeDef()
{
Command cmd = new("kpTestKnowledge")
Expand Down Expand Up @@ -382,6 +383,45 @@ private async Task KnowledgeAsync(ParseResult args, CancellationToken cancellati
}
}

private Command AnswerDef()
{
Command cmd = new("kpTestAnswer")
{
Args.Arg<string>("text")
};
cmd.TreatUnmatchedTokensAsErrors = false;
cmd.SetAction(this.AnswerAsync);
return cmd;
}

private async Task AnswerAsync(ParseResult args, CancellationToken cancellationToken)
{
IConversation conversation = EnsureConversation();

NamedArgs namedArgs = new NamedArgs(args);
AnswerContext context = new AnswerContext();

List<ConcreteEntity> entities = await conversation.SemanticRefs.SelectAsync<SemanticRef, ConcreteEntity>(
(sr) => sr.KnowledgeType == KnowledgeType.Entity ? sr.AsEntity() : null,
cancellationToken
);
entities = [.. entities.ToDistinct()];

List<Topic> topics = await conversation.SemanticRefs.SelectAsync<SemanticRef, Topic>(
(sr) => sr.KnowledgeType == KnowledgeType.Topic ? sr.AsTopic() : null,
cancellationToken
);
topics = [.. topics.ToDistinct()];

context.Entities = entities.Map((e) => new RelevantEntity { Entity = e });
context.Topics = topics.Map((t) => new RelevantTopic { Topic = t });

List<IMessage> messages = await conversation.Messages.GetAllAsync(cancellationToken);
context.Messages = messages.Map((m) => new RelevantMessage(m));
string prompt = context.ToPromptString();
ConsoleWriter.WriteLine(prompt);
}

private IConversation EnsureConversation()
{
return (_kpContext.Conversation is not null)
Expand Down
2 changes: 1 addition & 1 deletion dotnet/typeagent/src/common/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public static class DateTimeExtensions
{
public static string ToISOString(this DateTimeOffset dt)
{
return dt.ToString("o", System.Globalization.CultureInfo.InvariantCulture);
return dt.ToString("o");
}
}
48 changes: 48 additions & 0 deletions dotnet/typeagent/src/common/EnumerationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,52 @@ public static List<Scored<T>> GetTopK<T>(this IEnumerable<Scored<T>> items, int
topNList.Add(items);
return topNList.ByRankAndClear();
}

public static async ValueTask<List<T>> ToListAsync<T>(
this IAsyncEnumerable<T> source,
CancellationToken cancellationToken = default)
{
List<T> list = [];
await foreach (var item in source.WithCancellation(cancellationToken))
{
list.Add(item);
}
return list;
}

public static async ValueTask<List<TSelect>> SelectAsync<T, TSelect>(
this IAsyncEnumerable<T> source,
Func<T, TSelect?> selector,
CancellationToken cancellationToken = default)
{
ArgumentVerify.ThrowIfNull(selector, nameof(selector));

List<TSelect> list = [];
await foreach (var item in source.WithCancellation(cancellationToken))
{
TSelect? selected = selector(item);
if (selected is not null)
{
list.Add(selected);
}
}
return list;
}

public static async Task<List<T>> WhereAsync<T>(
this IAsyncEnumerable<T> source,
Func<T, bool> predicate,
CancellationToken cancellationToken = default)
{
ArgumentVerify.ThrowIfNull(predicate, nameof(predicate));
var list = new List<T>();
await foreach (var item in source.WithCancellation(cancellationToken))
{
if (predicate(item))
{
list.Add(item);
}
}
return list;
}
}
35 changes: 35 additions & 0 deletions dotnet/typeagent/src/common/IsoDateJsonConvertor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Globalization;

namespace TypeAgent.Common;

/// <summary>
/// Forces DateTimeOffset serialization/deserialization to a stable ISO 8601 (round‑trip) string ("o").
/// Example: 2025-11-05T14:23:17.1234567+00:00
/// </summary>
public class IsoDateJsonConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string? s = reader.GetString();
if (!string.IsNullOrEmpty(s))
{
if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var value))
{
return value;
}
}
throw new JsonException($"Invalid DateTimeOffset value: '{s}'.");
}
throw new JsonException("Invalid DateTimeOffset value");
}

public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToISOString());
}
}
117 changes: 117 additions & 0 deletions dotnet/typeagent/src/common/OneOrMany.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace TypeAgent.Common;

public abstract class OneOrManyItem
{
[JsonIgnore]
public abstract bool IsSingle { get; }

public static OneOrManyItem<T>? Create<T>(T? value)
{
return value is not null ? new SingleItem<T>(value) : null;
}

public static OneOrManyItem<T>? Create<T>(IList<T>? value)
{
return value.IsNullOrEmpty()
? null
: value.Count == 1
? new SingleItem<T>(value[0])
: new ListItem<T>(value);
}
}

[JsonConverter(typeof(OneOrManyJsonConverterFactory))]
public abstract class OneOrManyItem<T> : OneOrManyItem
{
}

public class SingleItem<T> : OneOrManyItem<T>
{
public SingleItem() { }

public SingleItem(T value)
{
Value = value;
}

[JsonIgnore]
public override bool IsSingle => true;

public T Value { get; set; }

public static implicit operator T(SingleItem<T> item)
{
return item.Value;
}
}

public class ListItem<T> : OneOrManyItem<T>
{
public ListItem()
{

}

public ListItem(IList<T> value)
{
Value = value;
}

[JsonIgnore]
public override bool IsSingle => false;

public IList<T> Value { get; set; }
}

public class OneOrManyJsonConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) => true;

public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var elementType = typeToConvert.GetGenericArguments()[0];
var converterType = typeof(OneOrManyJsonConverter<>).MakeGenericType(elementType);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
}

public class OneOrManyJsonConverter<T> : JsonConverter<OneOrManyItem<T>>
{
public override OneOrManyItem<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

if (reader.TokenType == JsonTokenType.StartArray)
{
var list = JsonSerializer.Deserialize<List<T>>(ref reader, options);
return new ListItem<T>()
{
Value = list
};
}

T value = JsonSerializer.Deserialize<T>(ref reader, options);
return new SingleItem<T>
{
Value = value
};
}

public override void Write(Utf8JsonWriter writer, OneOrManyItem<T> value, JsonSerializerOptions options)
{
if (value is ListItem<T> list)
{
JsonSerializer.Serialize(writer, list.Value, options);
}
else if (value is SingleItem<T> item)
{
JsonSerializer.Serialize(writer, item.Value, options);
}
}
}
2 changes: 1 addition & 1 deletion dotnet/typeagent/src/common/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace TypeAgent.Common;

public static partial class StringExtensions
public static partial class StringExtensions
{
/// <summary>
/// Splits an enumerable of strings into chunks, each chunk containing up to maxChunkLength strings and
Expand Down
59 changes: 59 additions & 0 deletions dotnet/typeagent/src/knowpro/Answer/AnswerContexSchema.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace TypeAgent.KnowPro.Answer;

public class RelevantKnowledge
{
// Entity or entities who mentioned the knowledge
[JsonPropertyName("origin")]
public OneOrManyItem<string>? Origin { get; set; }

// Entity or entities who received or consumed this knowledge
[JsonPropertyName("audience")]
public OneOrManyItem<string>? Audience { get; set; }

// Time period during which this knowledge was gathered
[JsonPropertyName("timeRange")]
public DateRange? TimeRange { get; set; }
};

public class RelevantTopic : RelevantKnowledge
{
[JsonPropertyName("knowledge")]
public string? Topic { get; set; }
}

public class RelevantEntity : RelevantKnowledge
{
[JsonPropertyName("knowledge")]
public ConcreteEntity? Entity { get; set; }
}

public partial class RelevantMessage
{
[JsonPropertyName("from")]
public OneOrManyItem<string>? From { get; set; }

[JsonPropertyName("to")]
public OneOrManyItem<string>? To { get; set; }

[JsonPropertyName("timestamp")]
public string? Timestamp { get; set; }

[JsonPropertyName("messageText")]
public OneOrManyItem<string>? MessageText { get; set; }
}

public partial class AnswerContext
{
// Relevant entities
// Use the 'name' and 'type' properties of entities to PRECISELY identify those that answer the user question.
public IList<RelevantEntity>? Entities { get; set; }

// Relevant topics
public IList<RelevantTopic> Topics { get; set; }

// Relevant messages
public IList<RelevantMessage>? Messages { get; set; }
};
Loading