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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
33 changes: 32 additions & 1 deletion .github/workflows/publish_npm_preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
name: Publish @crestapps/ai-chat-ui (preview)
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Check if should publish
id: check-publish
Expand Down Expand Up @@ -57,7 +59,36 @@ jobs:
Write-Output "Current SHA: ${{ github.sha }}"
Write-Output "New commits since last preview publish: $hasNewCommitSinceLastRelease"

$shouldPublish = $eventName -eq 'schedule' -and $hasNewCommitSinceLastRelease
if (-not $hasNewCommitSinceLastRelease)
{
"should-publish=false" >> $Env:GITHUB_OUTPUT
exit 0
}

$assetPaths = @(
'gulpfile.js',
'package.json',
'package-lock.json',
'src/Resources/CrestApps.AI.Resources/Assets.json',
'src/Resources/CrestApps.AI.Resources/package.json',
'src/Resources/CrestApps.AI.Resources/Assets',
'src/Resources/CrestApps.AI.Resources/scripts',
'src/Resources/CrestApps.AI.Resources/wwwroot'
)

$changedAssetFiles = @(git diff --name-only $previousRun.head_sha '${{ github.sha }}' -- $assetPaths |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) })

$hasAssetChangesSinceLastRelease = $changedAssetFiles.Count -gt 0
Write-Output "Asset changes since last preview publish: $hasAssetChangesSinceLastRelease"

if ($hasAssetChangesSinceLastRelease)
{
Write-Output 'Changed asset-related files:'
$changedAssetFiles | ForEach-Object { Write-Output " - $_" }
}

$shouldPublish = $eventName -eq 'schedule' -and $hasAssetChangesSinceLastRelease
"should-publish=$($shouldPublish ? 'true' : 'false')" >> $Env:GITHUB_OUTPUT

- uses: actions/setup-node@v6
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/validate_prompts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ jobs:
with:
dotnet-version: |
10.0.x
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install Python dependencies
run: |
python -m pip install --disable-pip-version-check pyyaml
- name: Build prompt validation tool
run: |
dotnet build -c Release src/Common/CrestApps.Core.Templates/CrestApps.Core.Templates.csproj /p:NuGetAudit=false
dotnet build -c Release src/Primitives/CrestApps.Core.Templates/CrestApps.Core.Templates.csproj /p:NuGetAudit=false
- name: Validate template file structure
shell: bash
run: |
Expand Down Expand Up @@ -207,4 +213,3 @@ jobs:
print("✓ All Parameters entries are valid")

sys.exit(exit_code)

3 changes: 3 additions & 0 deletions CrestApps.Core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
<Project Path="src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.A2AClient/CrestApps.Core.Mvc.Samples.A2AClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.McpClient/CrestApps.Core.Mvc.Samples.McpClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Blazor.Web/CrestApps.Core.Blazor.Web.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Web/CrestApps.Core.Mvc.Web.csproj" />
<Project Path="src/Startup/CrestApps.Core.Startup.Shared/CrestApps.Core.Startup.Shared.csproj" />
</Folder>
<Folder Name="/src/Stores/">
<Project Path="src/Stores/CrestApps.Core.Data.EntityCore/CrestApps.Core.Data.EntityCore.csproj" />
Expand All @@ -62,6 +64,7 @@
<Project Path="src/Utilities/CrestApps.Core.Support/CrestApps.Core.Support.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/CrestApps.Core.Tests.Samples/CrestApps.Core.Tests.Samples.csproj" />
<Project Path="tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj" />
</Folder>
</Solution>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<ItemGroup>
<!-- Testing Packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.52.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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public interface IAIClientProvider
/// <param name = "connection">The connection entry containing provider configuration.</param>
/// <param name = "deploymentName">The optional deployment name to use.</param>
/// <returns>A <see cref = "ValueTask{IImageGenerator}"/> representing the asynchronous operation.</returns>

#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.
ValueTask<IImageGenerator> GetImageGeneratorAsync(AIProviderConnectionEntry connection, string deploymentName = null);
#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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace CrestApps.Core.AI.Exceptions;

public class AIDeploymentConfigurationException : Exception
{
public AIDeploymentConfigurationException(string message)
: base(message)
{
}

public AIDeploymentConfigurationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace CrestApps.Core.AI.Exceptions;

public sealed class AIDeploymentNotFoundException : AIDeploymentConfigurationException
{
public AIDeploymentNotFoundException(string message)
: base(message)
{
}

public AIDeploymentNotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CrestApps.Core.AI.Memory;
using CrestApps.Core.AI.Orchestration;

namespace CrestApps.Core.AI.Models;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Memory;
namespace CrestApps.Core.AI.Orchestration;

/// <summary>
/// Defines a handler that processes preemptive RAG (Retrieval-Augmented Generation) for a specific
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ public interface ISearchDocumentHandler
Task DocumentsAddedOrUpdatedAsync(
IIndexProfileInfo profile,
IReadOnlyCollection<string> documentIds,
CancellationToken cancellationToken = default)
=> Task.CompletedTask;
CancellationToken cancellationToken = default);

/// <summary>
/// Called after <see cref="ISearchDocumentManager"/> successfully deletes documents from a source index.
Expand All @@ -30,6 +29,5 @@ Task DocumentsAddedOrUpdatedAsync(
Task DocumentsDeletedAsync(
IIndexProfileInfo profile,
IReadOnlyCollection<string> documentIds,
CancellationToken cancellationToken = default)
=> Task.CompletedTask;
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using CrestApps.Core.Infrastructure.Indexing.Models;
using CrestApps.Core.Models;

namespace CrestApps.Core.Infrastructure.Indexing;

public interface ISearchIndexProfileProvisioningService
{
Task<ValidationResultDetails> CreateAsync(SearchIndexProfile profile, CancellationToken cancellationToken = default);
}
4 changes: 4 additions & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- treats aborted and canceled request-stream failures in the Aspire AppHost as observed task exceptions so local development no longer floods the console with benign unobserved-task noise
- generates external `.map` source map files for all JS and CSS assets in the gulp build pipeline, copies them into `dist/` during npm package preparation, and includes them in the `@crestapps/ai-chat-ui` package exports
- adds per-message text-to-speech play/pause controls on assistant messages in the AI Chat and Chat Interaction UIs, keeps the action toolbar pinned to the bottom-right of each response without reserving a separate action row, automatically stops other message players before starting a new one, and hides manual playback controls during Conversation mode
- renders sample-host `[doc:n]` citations as superscript markers and shows the resolved document links below each cited assistant response in both the MVC and Blazor chat UIs
- adds `AddReferenceDownloads()` plus `AddDownloadAIDocumentEndpoint()` so attached-document citation links can be registered and downloaded explicitly in sample or custom hosts
- upgrades the MVC sample host to Font Awesome 7 and adds draggable, resizable AI Chat widget layout persistence with a reset-size control that hosts can disable through widget config
- adds Chat History page listing previous sessions per AI Profile sorted by creation date, with resume, delete, delete-all, and new-chat actions
- adds Test page for Utility and Agent AI Profiles providing a single-prompt/single-response streamed UI
- renames the "Chat" button to "New Chat" on the AI Profile list and adds "Chat History" and "Test" buttons for applicable profile types
- splits document ingestion, document-processing services, document endpoints, and document RAG into the dedicated `CrestApps.Core.AI.Documents` package, renames the format-specific helpers to `CrestApps.Core.AI.Documents.OpenXml` and `CrestApps.Core.AI.Documents.Pdf`, removes the data-ingestion dependency from `CrestApps.Core.AI`, persists uploaded files through `IDocumentFileStore` with GUID-based stored file names plus database-backed stored file metadata so hosts can redirect or clean up physical files reliably, and now registers a default filesystem-backed `IDocumentFileStore` from `AddCoreAIDocumentProcessing()` with `DocumentFileSystemFileStoreOptions` for base-path overrides
- simplifies template discovery by splitting generic `Templates/` loading from prompt-only `Templates/Prompts/`, keeps generic file discovery flat so provider-specific subfolders are not double-loaded, adds `Kind`-based template selection through `ITemplateService`, suppresses duplicate template IDs with first-match wins behavior, and removes Orchard-specific embedded-resource path handling from the standalone framework templating providers
- registers shared indexing services in the framework by default, including `ISearchIndexProfileManager`, `ISearchIndexProfileProvisioningService`, and a null fallback `ISearchIndexProfileStore`, so hosts only need `.AddIndexingServices(...).AddYesSqlStores()` or `.AddEntityCoreStores()` when they want persisted index profile records
2 changes: 1 addition & 1 deletion src/CrestApps.Core.Docs/docs/core/ai-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ builder.Services.AddCrestAppsCore(crestApps => crestApps
The framework does not assume a single persistence model. A host application is responsible for wiring the storage and search pieces that match its runtime:

- an `IAIMemoryStore` implementation for durable memory entries
- an `ISearchIndexProfileStore` implementation for index profile lookup (registered via `.AddIndexingServices(indexing => indexing.AddEntityCoreStores())` or `.AddYesSqlStores()`)
- a persistent `ISearchIndexProfileStore` implementation for index profile lookup when you want saved index profiles (registered via `.AddIndexingServices(indexing => indexing.AddEntityCoreStores())` or `.AddYesSqlStores()`). `AddCoreAIServices()` already supplies a null fallback store so hosts can start before a persistent store is added.
- one or more keyed `IMemoryVectorSearchService` implementations
- options such as `AIMemoryOptions`, `GeneralAIOptions`, and `ChatInteractionMemoryOptions`

Expand Down
40 changes: 35 additions & 5 deletions src/CrestApps.Core.Docs/docs/core/ai-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Hard-coding system prompts in C# makes them difficult to maintain, localize, and
| `ITemplateEngine` | `FluidTemplateEngine` | Singleton | Renders Liquid templates |
| `ITemplateService` | `DefaultTemplateService` | Scoped | Unified template discovery and rendering |
| `OptionsTemplateProvider` | — | Singleton | Templates registered via code |
| `FileSystemTemplateProvider` | — | Singleton | Templates discovered from disk |
| `FileSystemTemplateProvider` | — | Singleton | Generic templates discovered directly from `Templates/` |
| `PromptsFileSystemTemplateProvider` | — | Singleton | System prompt templates discovered from `Templates/Prompts/` |

## Key Interfaces

Expand Down Expand Up @@ -84,7 +85,29 @@ public interface ITemplateProvider

## Template File Format

Templates are markdown files with YAML front matter, stored in `Templates/Prompts/`:
Templates are markdown files with YAML front matter. You can either:

- store generic templates in `Templates/` and set `Kind` explicitly
- store system prompt templates in `Templates/Prompts/`, which defaults `Kind` to `SystemPrompt`

Example generic template:

```markdown
---
Title: Helpful Assistant
Kind: SystemPrompt
Description: A general-purpose helpful assistant prompt
Category: General
IsListable: true
---
You are a helpful assistant. Today's date is {{ "now" | date: "%Y-%m-%d" }}.

{% if user_name %}
You are assisting {{ user_name }}.
{% endif %}
```

The same content can live under `Templates/Prompts/` without repeating `Kind`:

```markdown
---
Expand All @@ -108,12 +131,13 @@ You are assisting {{ user_name }}.
| `Description` | string | Human-readable description |
| `Category` | string | Grouping category |
| `IsListable` | bool | Whether the template appears in listing APIs |
| `Kind` | string | Semantic template kind such as `SystemPrompt` or `Profile` |

## Registering Templates

### From Embedded Resources

Store `.md` files as embedded resources under `Templates/Prompts/` in your assembly:
Store `.md` files as embedded resources under `Templates/` in your assembly. Files under `Templates/Prompts/` default to `SystemPrompt`; files elsewhere should set `Kind` in front matter:

```csharp
builder.Services.AddTemplatesFromAssembly(typeof(MyClass).Assembly, source: "MyApp");
Expand All @@ -140,6 +164,13 @@ builder.Services.AddTemplating(options =>
});
```

With that discovery path:

- `/app/templates/Templates/*.md` is treated as the generic template root and should declare `Kind`
- `/app/templates/Templates/Prompts/*.md` is treated as prompt-only and defaults `Kind` to `SystemPrompt`

Generic `Templates/` discovery is intentionally not recursive. Subfolders can be reserved for other provider-specific conventions without being double-loaded.

## Configuration

### `TemplateOptions`
Expand Down Expand Up @@ -414,7 +445,6 @@ builder.Services.AddSingleton<ITemplateProvider, DatabaseAITemplateProvider>();
```

:::info
All registered `ITemplateProvider` instances are queried by `ITemplateService`. Templates from multiple providers are merged into a single collection. If two providers return templates with the same `Id`, the last-registered provider wins.
All registered `ITemplateProvider` instances are queried by `ITemplateService`. Templates from multiple providers are merged into a single collection. If two providers return templates with the same `Id`, the first discovered template wins and later duplicates are ignored.
:::


3 changes: 2 additions & 1 deletion src/CrestApps.Core.Docs/docs/core/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ The chat system provides all of this with a pluggable handler architecture.

In the MVC sample, Chat Interactions now reserve automatic spoken playback for **active conversation mode** only. Typed prompts and microphone dictation still produce normal streamed text responses, but they no longer auto-read the assistant reply unless the user explicitly started the live two-way conversation flow.

Both the MVC and Blazor sample hosts now render `[doc:n]` citations as superscript markers in assistant responses and show the resolved document references as clickable links directly below the cited message. When a citation points to an attached AI document, the reference link now downloads that file from the server after the host registers both `AddReferenceDownloads()` on the document-processing builder and `AddDownloadAIDocumentEndpoint()` on the endpoint route builder.

## Services Registered by `AddCoreAIChatInteractions()`

| Service | Implementation | Lifetime | Purpose |
Expand Down Expand Up @@ -800,4 +802,3 @@ window.openAIChatManager.initialize({
```

The play icon appears on completed assistant messages when a TTS deployment is available on the server. In Conversation mode, the per-message playback icon is hidden so the live voice exchange is not interrupted by manual playback controls.

3 changes: 2 additions & 1 deletion src/CrestApps.Core.Docs/docs/core/data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ Every CrestApps feature that needs persistent storage exposes `.AddYesSqlStores(
The **AI Services** builder method (`.AddYesSqlStores()` / `.AddEntityCoreStores()` on `CrestAppsAISuiteBuilder`) is a convenience that registers AI Profile Template and Chat Session stores together. Implementations that need finer-grained control (e.g., Orchard Core) can call the individual `IServiceCollection` extensions (`AddCoreAIProfileTemplateStoresYesSql()`, `AddCoreAIMcpServerStoresYesSql()`, etc.) directly.
:::

`AddCoreAIServices()` now registers the shared indexing runtime services (`ISearchIndexProfileManager`, `ISearchIndexProfileProvisioningService`, and a null fallback `ISearchIndexProfileStore`). Call `.AddIndexingServices(indexing => indexing.AddEntityCoreStores())` or `.AddYesSqlStores()` when you want persisted index profile records instead of the fallback.

**Entity Framework Core example** — register stores inline with each feature:

```csharp
Expand Down Expand Up @@ -1090,4 +1092,3 @@ builder.Services.AddScoped<ISourceCatalog<AIProfile>, CustomNamedSourceCatalog<A
builder.Services.AddScoped<INamedSourceCatalog<AIProfile>, CustomNamedSourceCatalog<AIProfile>>();
```


Loading
Loading