Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
12f08e6
Fix performance and code quality issues across multiple files
MikeAlhayek Apr 24, 2026
ad5d8bf
Fix resource management and caching, consolidate completion clients
MikeAlhayek Apr 24, 2026
6ebee8b
Fix Moq setup/callback/verify calls to include CancellationToken
MikeAlhayek Apr 24, 2026
66511ec
add CancellationToken
MikeAlhayek Apr 24, 2026
2078107
rename provider marker to client marker
MikeAlhayek Apr 24, 2026
5756224
fix: address critical security and data integrity issues
MikeAlhayek Apr 24, 2026
f2e841d
feat: add HTTP resilience handlers for A2A, Copilot, and MCP
MikeAlhayek Apr 24, 2026
6142a9e
feat: add options validation, cache eviction, test tooling, and misc …
MikeAlhayek Apr 24, 2026
042bf1d
refactor: remove dead code, add glossary, improve docs, add granular …
MikeAlhayek Apr 24, 2026
92275dd
refactor: eliminate YesSql store CRUD duplication via DocumentCatalog…
MikeAlhayek Apr 24, 2026
c53e92b
refactor: extract shared auth header builder from MCP and A2A
MikeAlhayek Apr 24, 2026
89605df
test: add tests for DefaultConnectionAuthHeaderBuilder
MikeAlhayek Apr 24, 2026
4b18751
refactor: standardize A2A and MCP auth enum usage
MikeAlhayek Apr 24, 2026
f5a063c
cleanup seralizers
MikeAlhayek Apr 24, 2026
459b613
fix some issues
MikeAlhayek Apr 24, 2026
084ef00
cleanup
MikeAlhayek Apr 24, 2026
c8399e3
fix sample app
MikeAlhayek Apr 24, 2026
dcbd63f
register missing search services
MikeAlhayek Apr 24, 2026
90d1e44
add index rebuild and reindex functions
MikeAlhayek Apr 24, 2026
28baa5b
update the client sample project
MikeAlhayek Apr 24, 2026
48ba7e1
add full document to context when needed
MikeAlhayek Apr 24, 2026
a060996
fix build
MikeAlhayek Apr 24, 2026
976f5b0
fix mvc
MikeAlhayek Apr 24, 2026
c9bb219
fix warning
MikeAlhayek Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,18 @@ Keep the docs focused on `CrestApps.Core`. If you need to mention the Orchard Co
- Add a blank line before and after `if` blocks, `switch` statements, and loops unless the block is immediately preceded by `{`
- Do not add a blank line between an `if`/`else`/`switch`/loop condition and its opening `{`
- Use `var` consistently with repository style
- Do not use `global using` files; add explicit `using` directives at the top of each file instead.
- Prefer top-of-file `using` directives over fully qualified type names in code.
- Only use expression-bodied members when the entire member fits on a single short line; use a full block body for anything longer or split across lines
- Avoid `DateTime.UtcNow`; prefer injected `TimeProvider`.
- Keep public docs and comments honest to the current code.
- Always document new interfaces, their methods and arguments along with documenting every property on domain models using `<summary>` block.
- Always treat warnings are errors in the solutions and ensure every warning is addressed.
- Always learn from my prompts, preference and styles and update the `copilot-instructions.md` file with any new preferences that I share in the future.
- Prefer SOLID and DRY refactors that consolidate duplicated provider, transport, or store logic into shared abstractions before adding new one-off implementations.
- Favor additive shared infrastructure first, then migrate consumers in behavior-safe steps when a full replacement is too risky for a single change.
- When working in framework code meant for external adoption, optimize for consistency and long-term maintainability across providers and hosts, not just local fixes.
- For optional provider integrations in sample hosts, do not eagerly read validated options in UI setup paths when an unconfigured provider should simply appear unavailable rather than crash the page.

## Runtime notes

Expand Down
17 changes: 6 additions & 11 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<ModelContextProtocolVersion>1.2.0</ModelContextProtocolVersion>
</PropertyGroup>

<ItemGroup>
<!-- Miscellaneous Packages -->
<PackageVersion Include="A2A" Version="0.3.4-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.4-preview" />
<PackageVersion Include="Anthropic" Version="12.16.0" />
<PackageVersion Include="Anthropic" Version="12.17.0" />
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="8.19.18" />
<PackageVersion Include="Fluid.Core" Version="2.31.0" />
<PackageVersion Include="FluentFTP" Version="54.1.1" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.2.2" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.3.0" />
<PackageVersion Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageVersion Include="Markdig" Version="1.1.2" />
<PackageVersion Include="ModelContextProtocol" Version="$(ModelContextProtocolVersion)" />
Expand All @@ -31,13 +30,13 @@
<PackageVersion Include="YesSql.Provider.Sqlite" Version="5.4.7" />
<PackageVersion Include="ZString" Version="2.6.0" />
</ItemGroup>

<ItemGroup>
<!-- Microsoft Packages -->
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="Azure.Search.Documents" Version="11.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
<PackageVersion Include="Microsoft.CognitiveServices.Speech" Version="1.49.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
Expand All @@ -49,39 +48,35 @@
<PackageVersion Include="Microsoft.Extensions.DataIngestion.Markdig" Version="10.4.0-preview.1.26160.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
<PackageVersion Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<!-- Testing Packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.52.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.59.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="xunit.analyzers" Version="1.27.0" />
<PackageVersion Include="xunit.runner.inproc" Version="3.2.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>

<ItemGroup>
<!-- Benchmark Packages -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup>
<!-- Aspire Packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.2.3" />
<PackageVersion Include="Aspire.Hosting.Elasticsearch" Version="13.2.0" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="13.2.3" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.Ollama" Version="13.1.1" />
</ItemGroup>

<ItemGroup>
<!-- Transitive Packages -->
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ public interface IAIChatSessionHandler : ICatalogEntryHandler<AIChatSession>
/// profile, session, messages, and an <see cref="IServiceProvider"/> scoped
/// to the current request.
/// </param>
Task MessageCompletedAsync(ChatMessageCompletedContext context);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Task MessageCompletedAsync(ChatMessageCompletedContext context, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,70 +12,77 @@ public interface IAIChatSessionManager
/// Asynchronously retrieves an existing AI chat session by its session ID.
/// </summary>
/// <param name="id">The unique identifier of the chat session.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is the <see cref="AIChatSession"/> if found,
/// or <c>null</c> if no session with the specified ID exists.
/// </returns>
Task<AIChatSession> FindByIdAsync(string id);
Task<AIChatSession> FindByIdAsync(string id, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously retrieves an existing AI chat session by its session ID after applying ownership check.
/// </summary>
/// <param name="sessionId">The unique identifier of the chat session. Must not be null or empty.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is the <see cref="AIChatSession"/> if found,
/// or <c>null</c> if no session with the specified session ID exists.
/// </returns>
Task<AIChatSession> FindAsync(string id);
Task<AIChatSession> FindAsync(string id, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously retrieves a list of top AI chat sessions based on the provided pagination parameters and query context.
/// </summary>
/// <param name="page">The page number to retrieve (1-based index). Must be greater than 0.</param>
/// <param name="pageSize">The number of sessions to retrieve per page. Must be greater than 0.</param>
/// <param name="context">The context used to filter and order the chat sessions. Must not be null.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is a list of <see cref="AIChatSessionResult"/> objects,
/// which represent the top sessions based on the query context and pagination parameters.
/// </returns>
Task<AIChatSessionResult> PageAsync(int page, int pageSize, AIChatSessionQueryContext context = null);
Task<AIChatSessionResult> PageAsync(int page, int pageSize, AIChatSessionQueryContext context = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates a new AI chat session for the specified AI chat profile.
/// </summary>
/// <param name="profile">The AI chat profile for which the new session will be created. Must not be null.</param>
/// <param name="context">The request context</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is a new <see cref="AIChatSession"/>
/// associated with the provided profile.
/// </returns>
Task<AIChatSession> NewAsync(AIProfile profile, NewAIChatSessionContext context);
Task<AIChatSession> NewAsync(AIProfile profile, NewAIChatSessionContext context, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously saves or updates the specified AI chat session.
/// </summary>
/// <param name="chatSession">The AI chat session to save or update. Must not be null.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. This method does not return any value.
/// </returns>
Task SaveAsync(AIChatSession chatSession);
Task SaveAsync(AIChatSession chatSession, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously deletes the specified AI chat session.
/// </summary>
/// <param name="sessionId">The unique identifier of the chat session to delete. Must not be null or empty.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is <c>true</c> if the session was successfully deleted,
/// or <c>false</c> if the session was not found or could not be deleted.
/// </returns>
Task<bool> DeleteAsync(string sessionId);
Task<bool> DeleteAsync(string sessionId, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously deletes all AI chat sessions for the specified profile and current user.
/// </summary>
/// <param name="profileId">The profile identifier to filter sessions. Must not be null or empty.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is the number of sessions that were deleted.
/// </returns>
Task<int> DeleteAllAsync(string profileId);
Task<int> DeleteAllAsync(string profileId, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ public interface IChatInteractionSettingsHandler
/// </summary>
/// <param name="interaction">The <see cref="ChatInteraction"/> being updated.</param>
/// <param name="settings">The raw settings payload from the client.</param>
Task UpdatingAsync(ChatInteraction interaction, JsonElement settings);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Task UpdatingAsync(ChatInteraction interaction, JsonElement settings, CancellationToken cancellationToken = default);

/// <summary>
/// Called after the <see cref="ChatInteraction"/> has been persisted.
/// </summary>
/// <param name="interaction">The updated <see cref="ChatInteraction"/>.</param>
/// <param name="settings">The raw settings payload from the client.</param>
Task UpdatedAsync(ChatInteraction interaction, JsonElement settings);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Task UpdatedAsync(ChatInteraction interaction, JsonElement settings, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ public interface IAICompletionContextBuilder
/// </summary>
/// <param name="resource">The resource object (e.g., <see cref="AIProfile"/> or <c>ChatInteraction</c>) used to seed and configure the completion context. Must not be <see langword="null"/>.</param>
/// <param name="configure">An optional delegate to override or fine-tune the context after handlers have run <c>BuildingAsync</c> but before <c>BuiltAsync</c>.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A task that completes with the fully built <see cref="AICompletionContext"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="resource"/> is <see langword="null"/>.</exception>
/// <seealso cref="IAICompletionContextBuilderHandler"/>
/// <seealso cref="AICompletionContextBuildingContext"/>
/// <seealso cref="AICompletionContextBuiltContext"/>
ValueTask<AICompletionContext> BuildAsync(object resource, Action<AICompletionContext> configure = null);
ValueTask<AICompletionContext> BuildAsync(object resource, Action<AICompletionContext> configure = null, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ public interface IAICompletionHandler
/// Handles a received message asynchronously.
/// </summary>
/// <param name="context">The context containing details of the received message.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ReceivedMessageAsync(ReceivedMessageContext context);
Task ReceivedMessageAsync(ReceivedMessageContext context, CancellationToken cancellationToken = default);

/// <summary>
/// Handles a received update asynchronously.
/// </summary>
/// <param name="context">The context containing details of the received update.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ReceivedUpdateAsync(ReceivedUpdateContext context);
Task ReceivedUpdateAsync(ReceivedUpdateContext context, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public interface IAICompletionServiceHandler
/// <param name="context">
/// The <see cref="CompletionServiceConfigureContext"/> that provides access to request-specific options and settings.
/// </param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ConfigureAsync(CompletionServiceConfigureContext context);
Task ConfigureAsync(CompletionServiceConfigureContext context, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ public interface IAIDeploymentManager : INamedSourceCatalogManager<AIDeployment>
/// Asynchronously retrieves a list of model deployments for the specified client.
/// </summary>
/// <param name="clientName">The name of the client. Must not be null or empty.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A ValueTask that represents the asynchronous operation. The result is an <see cref="IEnumerable{AIDeployment}"/>
/// containing the model deployments for the specified client.
/// </returns>
ValueTask<IEnumerable<AIDeployment>> GetAllAsync(string clientName);
ValueTask<IEnumerable<AIDeployment>> GetAllAsync(string clientName, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously retrieves all deployments supporting the specified type.
/// </summary>
/// <param name="type">The deployment type to filter by.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A ValueTask that represents the asynchronous operation. The result is an <see cref="IEnumerable{AIDeployment}"/>
/// containing all deployments matching the specified type.
/// </returns>
ValueTask<IEnumerable<AIDeployment>> GetByTypeAsync(AIDeploymentType type);
ValueTask<IEnumerable<AIDeployment>> GetByTypeAsync(AIDeploymentType type, CancellationToken cancellationToken = default);

/// <summary>
/// Resolves the default deployment of a given type for a specific client.
Expand All @@ -37,7 +39,8 @@ public interface IAIDeploymentManager : INamedSourceCatalogManager<AIDeployment>
/// </summary>
/// <param name="clientName">The name of the client to resolve the default deployment for.</param>
/// <param name="type">The deployment type to filter by.</param>
ValueTask<AIDeployment> GetDefaultAsync(string clientName, AIDeploymentType type);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask<AIDeployment> GetDefaultAsync(string clientName, AIDeploymentType type, CancellationToken cancellationToken = default);

/// <summary>
/// Resolves a deployment using the full fallback chain:
Expand All @@ -49,13 +52,15 @@ public interface IAIDeploymentManager : INamedSourceCatalogManager<AIDeployment>
/// <param name="type">The deployment type to resolve.</param>
/// <param name="deploymentName">The optional deployment name to look up directly.</param>
/// <param name="clientName">The optional client name to scope the resolution.</param>
ValueTask<AIDeployment> ResolveOrDefaultAsync(AIDeploymentType type, string deploymentName = null, string clientName = null);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask<AIDeployment> ResolveOrDefaultAsync(AIDeploymentType type, string deploymentName = null, string clientName = null, CancellationToken cancellationToken = default);

/// <summary>
/// Gets all deployments of a given type, optionally filtered by client.
/// Results are suitable for dropdown population, grouped by connection.
/// </summary>
/// <param name="type">The deployment type to filter by.</param>
/// <param name="clientName">The optional client name to further filter results.</param>
ValueTask<IEnumerable<AIDeployment>> GetAllByTypeAsync(AIDeploymentType type, string clientName = null);
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask<IEnumerable<AIDeployment>> GetAllByTypeAsync(AIDeploymentType type, string clientName = null, CancellationToken cancellationToken = default);
}

This file was deleted.

Loading
Loading