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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public interface IAIClientFactory
/// </returns>
ValueTask<IChatClient> CreateChatClientAsync(AIDeployment deployment);

/// <summary>
/// Asynchronously creates an <see cref="IChatClient"/> from the given deployment and applies optional pipeline configuration before building the final client.
/// </summary>
/// <param name="deployment">The AI deployment containing client, connection, and model information.</param>
/// <param name="configurePipeline">An optional delegate that configures the chat-client builder pipeline before the client is built.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="IChatClient"/>.
/// </returns>
ValueTask<IChatClient> CreateChatClientAsync(AIDeployment deployment, Action<ChatClientBuilder> configurePipeline);

/// <summary>
/// Asynchronously creates an <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> from the given deployment.
/// </summary>
Expand All @@ -28,6 +38,16 @@ public interface IAIClientFactory
/// </returns>
ValueTask<IEmbeddingGenerator<string, Embedding<float>>> CreateEmbeddingGeneratorAsync(AIDeployment deployment);

/// <summary>
/// Asynchronously creates an <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> from the given deployment and applies optional pipeline configuration before building the final generator.
/// </summary>
/// <param name="deployment">The AI deployment containing client, connection, and model information.</param>
/// <param name="configurePipeline">An optional delegate that configures the embedding-generator builder pipeline before the generator is built.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>.
/// </returns>
ValueTask<IEmbeddingGenerator<string, Embedding<float>>> CreateEmbeddingGeneratorAsync(AIDeployment deployment, Action<EmbeddingGeneratorBuilder<string, Embedding<float>>> configurePipeline);

#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
/// <summary>
/// Asynchronously creates an <see cref="IImageGenerator"/> from the given deployment.
Expand All @@ -37,6 +57,16 @@ public interface IAIClientFactory
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="IImageGenerator"/>.
/// </returns>
ValueTask<IImageGenerator> CreateImageGeneratorAsync(AIDeployment deployment);

/// <summary>
/// Asynchronously creates an <see cref="IImageGenerator"/> from the given deployment and applies optional pipeline configuration before building the final generator.
/// </summary>
/// <param name="deployment">The AI deployment containing client, connection, and model information.</param>
/// <param name="configurePipeline">An optional delegate that configures the image-generator builder pipeline before the generator is built.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="IImageGenerator"/>.
/// </returns>
ValueTask<IImageGenerator> CreateImageGeneratorAsync(AIDeployment deployment, Action<ImageGeneratorBuilder> configurePipeline);
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Expand All @@ -48,6 +78,16 @@ public interface IAIClientFactory
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="ISpeechToTextClient"/>.
/// </returns>
ValueTask<ISpeechToTextClient> CreateSpeechToTextClientAsync(AIDeployment deployment);

/// <summary>
/// Asynchronously creates an <see cref="ISpeechToTextClient"/> from the given deployment and applies optional pipeline configuration before building the final client.
/// </summary>
/// <param name="deployment">The AI deployment containing client, connection, and model information.</param>
/// <param name="configurePipeline">An optional delegate that configures the speech-to-text builder pipeline before the client is built.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="ISpeechToTextClient"/>.
/// </returns>
ValueTask<ISpeechToTextClient> CreateSpeechToTextClientAsync(AIDeployment deployment, Action<SpeechToTextClientBuilder> configurePipeline);
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Expand All @@ -59,5 +99,15 @@ public interface IAIClientFactory
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="ITextToSpeechClient"/>.
/// </returns>
ValueTask<ITextToSpeechClient> CreateTextToSpeechClientAsync(AIDeployment deployment);

/// <summary>
/// Asynchronously creates an <see cref="ITextToSpeechClient"/> from the given deployment and applies optional pipeline configuration before building the final client.
/// </summary>
/// <param name="deployment">The AI deployment containing client, connection, and model information.</param>
/// <param name="configurePipeline">An optional delegate that configures the text-to-speech builder pipeline before the client is built.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> representing the asynchronous operation, with the created <see cref="ITextToSpeechClient"/>.
/// </returns>
ValueTask<ITextToSpeechClient> CreateTextToSpeechClientAsync(AIDeployment deployment, Action<TextToSpeechClientBuilder> configurePipeline);
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" />
Expand Down
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- distinguishes uploaded vision images from searchable documents in the shared document-availability prompt so multimodal chat sessions analyze supported attached images directly instead of defaulting to document-tool or metadata-only responses
- caps the total uploaded vision-image bytes loaded into a single multimodal request through `ChatDocumentsOptions.MaxVisionInputBytesPerRequest`, removes the extra `MemoryStream` copy when attaching those images, and documents how to resolve a vision-capable chat client for direct image-description requests
- adds the standalone `CrestApps.Core.AI.Resilience` package with opt-in Microsoft.Extensions.AI builder resilience extensions for chat, embeddings, image generation, speech-to-text, and text-to-speech clients, including `UseDefaultResilience()` for provider `429 Too Many Requests` retries and `UseResilience(...)` for custom Polly/Microsoft resilience pipelines; the docs now include a dedicated AI Resilience page, the default retry schedule uses exponential backoff with jitter (about 1-2, 2-4, 4-8, 8-16, and 16-32 seconds across five retries), framework-owned completion clients and utility-deployment chat flows apply the default retry policy automatically, host-created clients remain opt-in, builder examples require `Build(serviceProvider)` instead of `Build(null)`, and Azure OpenAI exposes shared SDK retry settings through `CrestApps:AI:AzureClient` with matching five-retry exponential defaults
- adds `IAIClientFactory` overloads that accept builder-configuration delegates for chat, embeddings, image generation, speech-to-text, and text-to-speech clients, so callers can apply middleware such as `UseDefaultResilience()` while the factory owns the final `Build(serviceProvider)` step
- adds `CrestApps.Core.PostgreSQL` and `CrestApps.Core.AI.PostgreSQL` packages providing a lightweight PostgreSQL + pgvector vector search backend as an alternative to Elasticsearch and Azure AI Search, registers the same keyed services (`ISearchIndexManager`, `ISearchDocumentManager`, `IDataSourceContentManager`, `IDataSourceDocumentReader`, `IODataFilterTranslator`) under the `"PostgreSQL"` provider name, supports `AddAIDocuments()`, `AddAIDataSources()`, and `AddAIMemory()` builder extensions, and integrates into both MVC and Blazor sample hosts
- fixes hosted document and data-source indexing flows so background workers create a scoped service provider before resolving scoped indexing services, preventing upload-triggered failures and similar nightly alignment lifetime issues
- standardizes Azure AI Search configuration on top-level `AuthenticationType`, `ApiKey`, `IdentityClientId`, and `IndexPrefix` settings under `CrestApps:AzureAISearch`, and refreshes the sample host / docs examples to list the full supported option set in one place
Expand Down
28 changes: 22 additions & 6 deletions src/CrestApps.Core.Docs/docs/core/ai-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,34 @@ The AI services layer also registers the shared prompt-security services used by

### `IAIClientFactory`

The lowest-level service. Creates typed AI clients from a provider connection entry.
The lowest-level service. Creates typed AI clients from a resolved deployment and can optionally configure the final Microsoft.Extensions.AI builder pipeline before the factory builds the client.

```csharp
public interface IAIClientFactory
{
IChatClient CreateChatClient(AIProviderConnectionEntry connection, string deploymentName);
IEmbeddingGenerator<string, Embedding<float>> CreateEmbeddingGenerator(
AIProviderConnectionEntry connection, string deploymentName);
// Also: CreateImageGenerator, CreateSpeechToTextClient, CreateTextToSpeechClient
ValueTask<IChatClient> CreateChatClientAsync(AIDeployment deployment);
ValueTask<IChatClient> CreateChatClientAsync(
AIDeployment deployment,
Action<ChatClientBuilder> configurePipeline);

ValueTask<IEmbeddingGenerator<string, Embedding<float>>> CreateEmbeddingGeneratorAsync(
AIDeployment deployment);
ValueTask<IEmbeddingGenerator<string, Embedding<float>>> CreateEmbeddingGeneratorAsync(
AIDeployment deployment,
Action<EmbeddingGeneratorBuilder<string, Embedding<float>>> configurePipeline);

// Also: CreateImageGeneratorAsync, CreateSpeechToTextClientAsync, CreateTextToSpeechClientAsync
}
```

Use the overload when you want the factory to own the final `Build(serviceProvider)` step:

```csharp
var chatClient = await aiClientFactory.CreateChatClientAsync(
deployment,
builder => builder.UseDefaultResilience());
```

**When to use:** Only when you need direct, low-level access to a specific client type.

### `IAICompletionService`
Expand Down Expand Up @@ -259,7 +275,7 @@ public sealed class ChatApiController : ControllerBase
| `InvalidOperationException` | No deployment found, no provider connection configured | Check AI configuration — this is a setup error |
| `HttpRequestException` | Provider API unreachable (network error, DNS failure) | Check network connectivity; framework-owned completion and utility chat paths already use the default retry policy, and host-created AI clients can opt in separately through the resilience builders |
| `OperationCanceledException` | Request was cancelled (user navigated away, timeout) | Normal flow — let it propagate |
| Provider-specific rate limit errors | Too many requests to the AI provider | Framework-owned completion and utility chat paths already use the default retry policy; for host-created AI clients, use `CrestApps.Core.AI.Resilience` with `.AsBuilder().UseDefaultResilience()` or a custom `UseResilience(...)` pipeline; see [AI Resilience](./ai-resilience.md) |
| Provider-specific rate limit errors | Too many requests to the AI provider | Framework-owned completion and utility chat paths already use the default retry policy; for host-created AI clients, use `CrestApps.Core.AI.Resilience` through the `IAIClientFactory` overloads or through `.AsBuilder().UseDefaultResilience()` / `UseResilience(...)`; see [AI Resilience](./ai-resilience.md) |
| Provider-specific auth errors | Invalid API key or expired credentials | Check provider connection configuration |

### Handling Provider Failures
Expand Down
59 changes: 53 additions & 6 deletions src/CrestApps.Core.Docs/docs/core/ai-resilience.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ The package depends on:

## Builder Extensions

Every supported client follows the same pattern:
Every supported client follows one of these patterns:

1. Resolve or create the AI client
2. Convert it to the corresponding builder with `.AsBuilder()`
3. Apply either `UseDefaultResilience()` or `UseResilience(...)`
4. Finish with `Build(serviceProvider)`
1. Resolve the client through `IAIClientFactory` and configure the builder pipeline through the factory overload, or
2. Resolve or create the raw Microsoft.Extensions.AI client yourself, convert it to the corresponding builder with `.AsBuilder()`, apply `UseDefaultResilience()` or `UseResilience(...)`, and finish with `Build(serviceProvider)`.

Always pass the active `IServiceProvider` to `Build(serviceProvider)`. Do not use `Build()` or `Build(null)`, because downstream middleware may need DI to resolve services such as tools and related runtime components.
When you build manually, always pass the active `IServiceProvider` to `Build(serviceProvider)`. Do not use `Build()` or `Build(null)`, because downstream middleware may need DI to resolve services such as tools and related runtime components.

## Default Policy

Expand Down Expand Up @@ -69,6 +67,16 @@ The exact delay varies because jitter is enabled by default.

## Chat Example

If you are resolving the client through `IAIClientFactory`, use the overload and let the factory own the final build:

```csharp
var resilientClient = await aiClientFactory.CreateChatClientAsync(
deployment,
builder => builder.UseDefaultResilience());
```

If you already have a raw `IChatClient`, use the builder directly:

```csharp
var resilientClient = chatClient
.AsBuilder()
Expand All @@ -80,6 +88,21 @@ var resilientClient = chatClient

Use the options callback when you want to keep the built-in rate-limit handling but tune the retry shape:

```csharp
var resilientClient = await aiClientFactory.CreateChatClientAsync(
deployment,
builder => builder.UseDefaultResilience(options =>
{
options.MaxRateLimitRetries = 3;
options.RateLimitRetryDelay = TimeSpan.FromSeconds(2);
options.BackoffType = DelayBackoffType.Exponential;
options.UseJitter = true;
options.MaxRetryDelay = TimeSpan.FromSeconds(20);
}));
```

The equivalent direct-builder form is:

```csharp
var resilientClient = chatClient
.AsBuilder()
Expand Down Expand Up @@ -138,6 +161,12 @@ The same extension methods are available on the other Microsoft.Extensions.AI bu

### Embeddings

```csharp
var resilientGenerator = await aiClientFactory.CreateEmbeddingGeneratorAsync(
deployment,
builder => builder.UseDefaultResilience());
```

```csharp
var resilientGenerator = embeddingGenerator
.AsBuilder()
Expand All @@ -147,6 +176,12 @@ var resilientGenerator = embeddingGenerator

### Image Generation

```csharp
var resilientGenerator = await aiClientFactory.CreateImageGeneratorAsync(
deployment,
builder => builder.UseDefaultResilience());
```

```csharp
var resilientGenerator = imageGenerator
.AsBuilder()
Expand All @@ -156,6 +191,12 @@ var resilientGenerator = imageGenerator

### Speech to Text

```csharp
var resilientClient = await aiClientFactory.CreateSpeechToTextClientAsync(
deployment,
builder => builder.UseDefaultResilience());
```

```csharp
var resilientClient = speechToTextClient
.AsBuilder()
Expand All @@ -165,6 +206,12 @@ var resilientClient = speechToTextClient

### Text to Speech

```csharp
var resilientClient = await aiClientFactory.CreateTextToSpeechClientAsync(
deployment,
builder => builder.UseDefaultResilience());
```

```csharp
var resilientClient = textToSpeechClient
.AsBuilder()
Expand Down
16 changes: 14 additions & 2 deletions src/CrestApps.Core.Docs/docs/core/getting-started-aspnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,19 @@ services.AddSingleton<IConfigureOptions<GeneralAIOptions>, SiteSettingsConfigure

That keeps settings refresh host-agnostic and avoids custom accessor interfaces.

`AddCoreAIServices()` keeps host-created AI clients opt-in for retries. Framework-owned completion clients and utility-deployment chat paths already use the default retry policy internally. If you want the same builder extensions for your own resolved clients outside the framework defaults, reference the standalone `CrestApps.Core.AI.Resilience` package and wrap the client through the corresponding Microsoft.Extensions.AI builder pipeline:
`AddCoreAIServices()` keeps host-created AI clients opt-in for retries. Framework-owned completion clients and utility-deployment chat paths already use the default retry policy internally. If you want the same builder extensions for your own resolved clients outside the framework defaults, reference the standalone `CrestApps.Core.AI.Resilience` package and use the `IAIClientFactory` overload when you resolve clients through deployments:

```csharp
var resilientClient = await aiClientFactory.CreateChatClientAsync(
deployment,
builder => builder.UseDefaultResilience(options =>
{
options.MaxRateLimitRetries = 5;
options.RateLimitRetryDelay = TimeSpan.FromSeconds(1);
}));
```

If you already have a raw client instance, wrap it through the corresponding Microsoft.Extensions.AI builder pipeline:

```csharp
var resilientClient = chatClient
Expand All @@ -326,7 +338,7 @@ var resilientClient = chatClient
.Build(serviceProvider);
```

The same `UseDefaultResilience()` and `UseResilience(...)` extensions are also available on `IEmbeddingGenerator<TInput, TEmbedding>`, `IImageGenerator`, `ISpeechToTextClient`, and `ITextToSpeechClient` through their `.AsBuilder()` adapters. For streaming speech-to-text, retries require a seekable input stream so the audio can be replayed safely. See [AI Resilience](./ai-resilience.md) for the full builder surface, default retry schedule, and customization options.
The same `UseDefaultResilience()` and `UseResilience(...)` extensions are also available on `IEmbeddingGenerator<TInput, TEmbedding>`, `IImageGenerator`, `ISpeechToTextClient`, and `ITextToSpeechClient` through the matching `IAIClientFactory` overloads or their `.AsBuilder()` adapters. For streaming speech-to-text, retries require a seekable input stream so the audio can be replayed safely. See [AI Resilience](./ai-resilience.md) for the full builder surface, default retry schedule, and customization options.

For a custom policy, use the lower-level builder extension and configure the resilience pipeline yourself:

Expand Down
Loading
Loading