Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6d1aae2
feat(agentic): Phase 0 — message-based IChatClient<T> model abstracti…
franklinic Jun 8, 2026
78f75da
feat(agentic): Phase 0 — tool/function layer with auto JSON-schema (#…
franklinic Jun 8, 2026
db51ee6
refactor(agentic): replace ImageContent MIME string with ImageMediaTy…
franklinic Jun 8, 2026
994bb08
Merge branch 'master' into feature/agentic-phase0-model-abstractions
ooples Jun 8, 2026
15a8454
feat(agentic): Phase 0 — IChatClient connector base + OpenAI connecto…
franklinic Jun 8, 2026
634bc38
feat(agentic): Phase 0 — Anthropic + Azure OpenAI connectors (#1544)
franklinic Jun 8, 2026
28b2ab3
refactor(reasoning): rewire Reasoning + facade onto IChatClient<T> (#…
franklinic Jun 8, 2026
1a8635a
refactor(facade): AiModelResult reasoning takes injected IChatClient;…
franklinic Jun 8, 2026
0f56ba0
chore(agentic): delete legacy Agents/Tools/Chains/LanguageModels + in…
franklinic Jun 8, 2026
43aee0c
chore(agentic): drop dead global usings (Agents/LanguageModels/Tools)…
franklinic Jun 8, 2026
92e88ef
refactor(facade): excise agent-assistance + PromptChain features; src…
franklinic Jun 8, 2026
0da629f
test(agentic): remove legacy feature tests; rewire reasoning mock to …
franklinic Jun 9, 2026
e07b963
fix(reasoning): implement MCTS backpropagation; remove orphaned legac…
franklinic Jun 9, 2026
123ff61
feat(agentic): typed structured-output helper (schema-constrained -> …
franklinic Jun 9, 2026
6e45abb
feat(agentic): Microsoft.Extensions.AI adapter; remove all null-forgi…
franklinic Jun 9, 2026
bc4fa02
feat(agentic): source-generated tool schemas (compile-time, reflectio…
franklinic Jun 9, 2026
03a68eb
fix(agentic): address PR #1545 review — HTTP client/retry/response-va…
franklinic Jun 11, 2026
dbe4933
fix(agentic): address PR #1545 review — media-type default, tool erro…
franklinic Jun 11, 2026
2f4e9e5
fix(agentic): address PR #1545 review — generator defaults + reasonin…
franklinic Jun 11, 2026
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
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
<!-- Microsoft / .NET -->
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.6.0" />
<PackageVersion Include="Microsoft.ML" Version="3.0.1" />
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.26.0" />
<PackageVersion Include="Microsoft.Research.SEALNet" Version="4.1.1" />
Expand Down Expand Up @@ -199,4 +200,4 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Moq" Version="4.20.72" />
</ItemGroup>
</Project>
</Project>
28 changes: 28 additions & 0 deletions src/Agentic/Models/AiContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace AiDotNet.Agentic.Models;

/// <summary>
/// Base type for a single piece of content inside a <see cref="ChatMessage"/>.
/// </summary>
/// <remarks>
/// <para>
/// A chat message is not just a string — it is a list of content parts. This lets one message mix
/// modalities and structured items: plain text, images, a request from the model to call a tool
/// (<see cref="ToolCallContent"/>), or the result of running a tool (<see cref="ToolResultContent"/>).
/// Concrete subclasses are matched with pattern matching, e.g. <c>part is TextContent t</c>.
/// </para>
/// <para><b>For Beginners:</b> Older APIs treated a message as one block of text. Real conversations
/// are richer — a single turn might contain a sentence <em>and</em> an image, or the model might
/// reply with "please run the calculator tool" instead of text. Representing a message as a list of
/// typed parts lets us model all of that cleanly. Each part is one of the subclasses of this class.
/// </para>
/// </remarks>
public abstract class AiContent
{
/// <summary>
/// Initializes the base content part. Protected because <see cref="AiContent"/> is abstract and
/// only constructed through a concrete subclass.
/// </summary>
private protected AiContent()
{
}
}
62 changes: 62 additions & 0 deletions src/Agentic/Models/AiToolDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Newtonsoft.Json.Linq;

namespace AiDotNet.Agentic.Models;

/// <summary>
/// Describes a tool/function the model is allowed to call: its name, a description, and a JSON-schema
/// describing its parameters.
/// </summary>
/// <remarks>
/// <para>
/// This is the <em>declaration</em> sent to the model (distinct from the executable tool in the Tools
/// layer). The model uses the name and description to decide <em>whether</em> to call the tool, and the
/// parameter schema to decide <em>how</em> to fill in the arguments. The schema is a standard JSON Schema
/// object (the same shape OpenAI, Anthropic, and the local engine all consume).
/// </para>
/// <para><b>For Beginners:</b> Before a model can use a tool, you have to describe the tool to it — like
/// a menu entry: the tool's name, what it does, and what inputs it needs. This class is that menu entry.
/// The "what inputs it needs" part is written in JSON Schema (e.g., "an object with a string field
/// called <c>city</c>"). In later phases this schema is generated automatically from your C# method
/// signature, so you won't usually hand-write it.
/// </para>
/// </remarks>
public sealed class AiToolDefinition
{
/// <summary>
/// Initializes a new tool definition.
/// </summary>
/// <param name="name">The unique tool name the model will reference when calling it.</param>
/// <param name="description">A natural-language description of what the tool does.</param>
/// <param name="parametersSchema">
/// A JSON Schema object describing the tool's parameters. When <c>null</c>, an empty
/// parameter-less schema (<c>{"type":"object","properties":{}}</c>) is used.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="description"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is empty/whitespace.</exception>
public AiToolDefinition(string name, string description, JObject? parametersSchema = null)
{
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(description);
Name = name;
Description = description;
ParametersSchema = parametersSchema ?? CreateEmptySchema();
}

/// <summary>
/// Gets the unique name the model references when requesting this tool.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the natural-language description that helps the model decide when to call the tool.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the JSON Schema object describing the tool's parameters.
/// </summary>
public JObject ParametersSchema { get; }

private static JObject CreateEmptySchema() =>
new() { ["type"] = "object", ["properties"] = new JObject() };
}
86 changes: 86 additions & 0 deletions src/Agentic/Models/ChatClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace AiDotNet.Agentic.Models;

/// <summary>
/// Convenience helpers over <see cref="IChatClient{T}"/> for the common "prompt in, text out" case.
/// </summary>
/// <remarks>
/// <para>
/// The agentic model layer is message-based, but plenty of callers (and internal consumers such as the
/// reasoning strategies) just want to send a prompt and read back text. These extensions wrap a single
/// user message and return the assistant's concatenated text, so simple call sites stay simple while the
/// full message/tool/streaming API remains available when needed.
/// </para>
/// <para><b>For Beginners:</b> Instead of constructing a list of messages and reading a response object,
/// you can write <c>await client.GenerateTextAsync("Summarize this")</c> and get a plain string back.
/// </para>
/// </remarks>
public static class ChatClientExtensions
{
/// <summary>
/// Sends a single user prompt and returns the assistant's text reply.
/// </summary>
/// <typeparam name="T">The client's numeric type.</typeparam>
/// <param name="client">The chat client.</param>
/// <param name="prompt">The user prompt.</param>
/// <param name="options">Optional per-call settings.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>The assistant's reply text.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="prompt"/> is <c>null</c>.</exception>
public static async Task<string> GenerateTextAsync<T>(
this IChatClient<T> client,
string prompt,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
Guard.NotNull(client);
Guard.NotNull(prompt);

var response = await client
.GetResponseAsync(new[] { ChatMessage.User(prompt) }, options, cancellationToken)
.ConfigureAwait(false);
return response.Text;
}

/// <summary>
/// Sends a system instruction plus a user prompt and returns the assistant's text reply.
/// </summary>
/// <typeparam name="T">The client's numeric type.</typeparam>
/// <param name="client">The chat client.</param>
/// <param name="systemPrompt">The system instructions.</param>
/// <param name="userPrompt">The user prompt.</param>
/// <param name="options">Optional per-call settings.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>The assistant's reply text.</returns>
/// <exception cref="ArgumentNullException">Thrown when an argument is <c>null</c>.</exception>
public static async Task<string> GenerateTextAsync<T>(
this IChatClient<T> client,
string systemPrompt,
string userPrompt,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
Guard.NotNull(client);
Guard.NotNull(systemPrompt);
Guard.NotNull(userPrompt);

var messages = new[] { ChatMessage.System(systemPrompt), ChatMessage.User(userPrompt) };
var response = await client.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
return response.Text;
}

/// <summary>
/// Sends a single user prompt and returns the assistant's text reply. Alias of
/// <see cref="GenerateTextAsync{T}(IChatClient{T}, string, ChatOptions?, CancellationToken)"/> kept for
/// callers that read more naturally as "generate a response".
/// </summary>
/// <typeparam name="T">The client's numeric type.</typeparam>
/// <param name="client">The chat client.</param>
/// <param name="prompt">The user prompt.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>The assistant's reply text.</returns>
public static Task<string> GenerateResponseAsync<T>(
this IChatClient<T> client,
string prompt,
CancellationToken cancellationToken = default)
=> client.GenerateTextAsync(prompt, options: null, cancellationToken);
}
47 changes: 47 additions & 0 deletions src/Agentic/Models/ChatFinishReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace AiDotNet.Agentic.Models;

/// <summary>
/// Describes why a chat model stopped generating a response.
/// </summary>
/// <remarks>
/// <para>
/// Every completed (or streamed) response carries a finish reason so callers can react correctly:
/// for example, retrying with a larger token budget on <see cref="Length"/>, or executing the
/// requested tools on <see cref="ToolCalls"/>.
/// </para>
/// <para><b>For Beginners:</b> When the model stops "typing", it tells you why. Did it finish its
/// thought naturally (<see cref="Stop"/>)? Did it run out of room (<see cref="Length"/>)? Did it
/// pause to ask for a tool to be run (<see cref="ToolCalls"/>)? Was the output blocked by a safety
/// filter (<see cref="ContentFilter"/>)? This enum captures those outcomes so your code can decide
/// what to do next.
/// </para>
/// </remarks>
public enum ChatFinishReason
{
/// <summary>
/// The model finished naturally (it reached a stopping point or a configured stop sequence).
/// </summary>
Stop,

/// <summary>
/// Generation was cut off because the maximum output-token budget was reached. The response is
/// likely incomplete; consider increasing <c>MaxOutputTokens</c> and retrying.
/// </summary>
Length,

/// <summary>
/// The model paused to request one or more tool/function calls. The caller is expected to execute
/// the tools and feed the results back as <see cref="ChatRole.Tool"/> messages, then continue.
/// </summary>
ToolCalls,

/// <summary>
/// Output was withheld or truncated by a content-safety filter.
/// </summary>
ContentFilter,

/// <summary>
/// The provider returned a finish reason that does not map to any of the known values.
/// </summary>
Unknown
}
120 changes: 120 additions & 0 deletions src/Agentic/Models/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace AiDotNet.Agentic.Models;

/// <summary>
/// A single message in a chat conversation: a <see cref="ChatRole"/> plus one or more content parts.
/// </summary>
/// <remarks>
/// <para>
/// A chat request is an ordered list of <see cref="ChatMessage"/> values. Each message is authored by
/// a role (system/user/assistant/tool) and carries a list of <see cref="AiContent"/> parts so it can
/// mix text, images, tool-call requests, and tool results. Messages are immutable once constructed.
/// </para>
/// <para><b>For Beginners:</b> This is one line in the conversation transcript. The most common case is
/// a bit of text from the user or the assistant, so there are shortcuts for that:
/// <c>ChatMessage.User("Hello")</c>, <c>ChatMessage.System("You are helpful")</c>,
/// <c>ChatMessage.Assistant("Hi!")</c>. For tool calling there are richer parts, but the shortcuts
/// cover everyday use.
/// </para>
/// </remarks>
public sealed class ChatMessage
{
/// <summary>
/// Initializes a new message from a role and an explicit list of content parts.
/// </summary>
/// <param name="role">The author role.</param>
/// <param name="contents">The content parts. Must be non-null and contain no null elements.</param>
/// <param name="authorName">Optional author name (e.g., a specific tool or participant name).</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contents"/> or any element is <c>null</c>.</exception>
public ChatMessage(ChatRole role, IReadOnlyList<AiContent> contents, string? authorName = null)
{
Guard.NotNull(contents);
var copy = new List<AiContent>(contents.Count);
foreach (var part in contents)
{
Guard.NotNull(part);
copy.Add(part);
}

Role = role;
Contents = copy;
AuthorName = authorName;
}

/// <summary>
/// Initializes a new message from a role and a single piece of text.
/// </summary>
/// <param name="role">The author role.</param>
/// <param name="text">The message text.</param>
/// <param name="authorName">Optional author name.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="text"/> is <c>null</c>.</exception>
public ChatMessage(ChatRole role, string text, string? authorName = null)
: this(role, new AiContent[] { new TextContent(text) }, authorName)
{
}

/// <summary>
/// Gets the role that authored this message.
/// </summary>
public ChatRole Role { get; }

/// <summary>
/// Gets the ordered, immutable list of content parts that make up this message.
/// </summary>
public IReadOnlyList<AiContent> Contents { get; }

/// <summary>
/// Gets the optional author name associated with this message.
/// </summary>
public string? AuthorName { get; }

/// <summary>
/// Gets the concatenated text of all <see cref="TextContent"/> parts in this message.
/// Non-text parts (images, tool calls) are ignored.
/// </summary>
public string Text => string.Concat(Contents.OfType<TextContent>().Select(t => t.Text));

/// <summary>
/// Gets the tool-call requests contained in this message (typically present on assistant messages
/// whose finish reason was <see cref="ChatFinishReason.ToolCalls"/>).
/// </summary>
public IReadOnlyList<ToolCallContent> ToolCalls => Contents.OfType<ToolCallContent>().ToList();

/// <summary>
/// Creates a system message carrying high-level instructions.
/// </summary>
/// <param name="text">The system instructions.</param>
/// <returns>A new system-role message.</returns>
public static ChatMessage System(string text) => new(ChatRole.System, text);

/// <summary>
/// Creates a user message.
/// </summary>
/// <param name="text">The user's text.</param>
/// <returns>A new user-role message.</returns>
public static ChatMessage User(string text) => new(ChatRole.User, text);

/// <summary>
/// Creates an assistant message from text.
/// </summary>
/// <param name="text">The assistant's text.</param>
/// <returns>A new assistant-role message.</returns>
public static ChatMessage Assistant(string text) => new(ChatRole.Assistant, text);

/// <summary>
/// Creates an assistant message from explicit content parts (for example, one or more
/// <see cref="ToolCallContent"/> parts the model produced).
/// </summary>
/// <param name="contents">The content parts.</param>
/// <returns>A new assistant-role message.</returns>
public static ChatMessage Assistant(IReadOnlyList<AiContent> contents) => new(ChatRole.Assistant, contents);

/// <summary>
/// Creates a tool-result message answering a prior <see cref="ToolCallContent"/>.
/// </summary>
/// <param name="callId">The id of the tool call being answered.</param>
/// <param name="result">The tool's output.</param>
/// <param name="isError">Whether the tool invocation failed.</param>
/// <returns>A new tool-role message.</returns>
public static ChatMessage Tool(string callId, string result, bool isError = false) =>
new(ChatRole.Tool, new AiContent[] { new ToolResultContent(callId, result, isError) });
}
Loading
Loading