From 25a95852ddd0b6e25b52b020febe796a2d037b64 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 1 Aug 2026 06:01:16 +0300 Subject: [PATCH 1/3] Add versioned docs, bump development version to 1.1.0 - Configure Docusaurus versioned docs: freeze the released docs as the 1.0.x version so main-branch edits only affect the Latest version. - Rework deploy_docs.yml to match the CrestApps.OrchardCore workflow: prepare/build/deploy jobs that skip prerelease and patch tags and auto snapshot X.Y.x docs on vX.Y.0 tag pushes (idempotent when pre-committed). - Document the versioning and deployment process in the docs README. - Bump VersionPrefix to 1.1.0 so nightly builds produce the next version. - Add 1.1.0 release notes and link them from the changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deploy_docs.yml | 65 +- Directory.Build.props | 6 +- src/CrestApps.Core.Docs/README.md | 26 + .../docs/changelog/index.md | 1 + .../docs/changelog/v1.1.0.md | 24 + src/CrestApps.Core.Docs/sidebars.js | 1 + .../version-1.0.x/a2a/client.md | 553 +++++++++ .../versioned_docs/version-1.0.x/a2a/host.md | 322 +++++ .../versioned_docs/version-1.0.x/a2a/index.md | 105 ++ .../version-1.0.x/changelog/index.md | 14 + .../version-1.0.x/changelog/v1.0.0.md | 120 ++ .../version-1.0.x/core/agents.md | 317 +++++ .../version-1.0.x/core/ai-core.md | 477 +++++++ .../version-1.0.x/core/ai-documents.md | 166 +++ .../version-1.0.x/core/ai-memory.md | 118 ++ .../version-1.0.x/core/ai-profiles.md | 214 ++++ .../version-1.0.x/core/ai-resilience.md | 240 ++++ .../version-1.0.x/core/ai-templates.md | 449 +++++++ .../version-1.0.x/core/architecture.md | 114 ++ .../versioned_docs/version-1.0.x/core/chat.md | 867 +++++++++++++ .../version-1.0.x/core/context-builders.md | 381 ++++++ .../version-1.0.x/core/core-services.md | 171 +++ .../version-1.0.x/core/data-storage.md | 1106 +++++++++++++++++ .../version-1.0.x/core/document-processing.md | 138 ++ .../version-1.0.x/core/extensible-entity.md | 134 ++ .../core/getting-started-aspnet.md | 384 ++++++ .../version-1.0.x/core/index.md | 93 ++ .../version-1.0.x/core/interfaces.md | 127 ++ .../version-1.0.x/core/mvc-example.md | 365 ++++++ .../version-1.0.x/core/prompt-security.md | 600 +++++++++ .../version-1.0.x/core/response-handlers.md | 419 +++++++ .../version-1.0.x/core/signalr.md | 268 ++++ .../version-1.0.x/core/tool-instances.md | 436 +++++++ .../version-1.0.x/core/tools.md | 445 +++++++ .../version-1.0.x/core/use-cases.md | 122 ++ .../version-1.0.x/data-sources/azure-ai.md | 322 +++++ .../data-sources/custom-sources.md | 148 +++ .../data-sources/elasticsearch.md | 305 +++++ .../version-1.0.x/data-sources/index.md | 514 ++++++++ .../version-1.0.x/data-sources/postgresql.md | 363 ++++++ .../version-1.0.x/getting-started.md | 233 ++++ .../versioned_docs/version-1.0.x/glossary.md | 78 ++ .../versioned_docs/version-1.0.x/intro.md | 48 + .../version-1.0.x/mcp/client.md | 432 +++++++ .../versioned_docs/version-1.0.x/mcp/index.md | 78 ++ .../version-1.0.x/mcp/resource-types.md | 122 ++ .../version-1.0.x/mcp/server.md | 408 ++++++ .../version-1.0.x/orchestration/claude.md | 115 ++ .../version-1.0.x/orchestration/copilot.md | 453 +++++++ .../orchestration/default-orchestrator.md | 41 + .../version-1.0.x/orchestration/index.md | 414 ++++++ .../version-1.0.x/providers/architecture.md | 350 ++++++ .../providers/azure-ai-inference.md | 197 +++ .../version-1.0.x/providers/azure-openai.md | 255 ++++ .../version-1.0.x/providers/index.md | 341 +++++ .../version-1.0.x/providers/ollama.md | 180 +++ .../version-1.0.x/providers/openai.md | 167 +++ .../version-1.0.x-sidebars.json | 101 ++ src/CrestApps.Core.Docs/versions.json | 3 + 59 files changed, 15042 insertions(+), 14 deletions(-) create mode 100644 src/CrestApps.Core.Docs/docs/changelog/v1.1.0.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/client.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/host.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/v1.0.0.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/agents.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-core.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-documents.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-memory.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-profiles.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-resilience.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-templates.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/architecture.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/chat.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/context-builders.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/core-services.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/data-storage.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/document-processing.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/extensible-entity.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/getting-started-aspnet.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/interfaces.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/mvc-example.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/prompt-security.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/response-handlers.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/signalr.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/tool-instances.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/tools.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/use-cases.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/data-sources/azure-ai.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/data-sources/custom-sources.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/data-sources/elasticsearch.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/data-sources/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/data-sources/postgresql.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/getting-started.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/glossary.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/intro.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/mcp/client.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/mcp/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/mcp/resource-types.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/mcp/server.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/orchestration/claude.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/orchestration/copilot.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/orchestration/default-orchestrator.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/orchestration/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/architecture.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/azure-ai-inference.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/azure-openai.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/index.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/ollama.md create mode 100644 src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/providers/openai.md create mode 100644 src/CrestApps.Core.Docs/versioned_sidebars/version-1.0.x-sidebars.json create mode 100644 src/CrestApps.Core.Docs/versions.json diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index a3247c4a..e382132e 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -5,7 +5,7 @@ on: branches: - main tags: - - 'v[0-9]+.[0-9]+.0' + - 'v*.*.*' workflow_dispatch: permissions: @@ -18,7 +18,49 @@ concurrency: cancel-in-progress: false jobs: + prepare: + runs-on: ubuntu-latest + outputs: + should_deploy: ${{ steps.evaluate.outputs.should_deploy }} + doc_version: ${{ steps.evaluate.outputs.doc_version }} + skip_reason: ${{ steps.evaluate.outputs.skip_reason }} + steps: + - name: Evaluate deployment target + id: evaluate + shell: bash + run: | + should_deploy=false + doc_version= + skip_reason= + + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" || "${GITHUB_REF}" == "refs/heads/main" ]]; then + should_deploy=true + elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + if [[ "${GITHUB_REF_NAME}" == *-* ]]; then + skip_reason="Skipping documentation deployment for prerelease tag ${GITHUB_REF_NAME}." + elif [[ "${GITHUB_REF_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.0$ ]]; then + should_deploy=true + doc_version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.x" + else + skip_reason="Skipping documentation deployment for patch tag ${GITHUB_REF_NAME}." + fi + else + skip_reason="Skipping documentation deployment for ref ${GITHUB_REF}." + fi + + { + echo "should_deploy=${should_deploy}" + echo "doc_version=${doc_version}" + echo "skip_reason=${skip_reason}" + } >> "$GITHUB_OUTPUT" + + - name: Report skipped deployment + if: steps.evaluate.outputs.should_deploy != 'true' + run: echo "${{ steps.evaluate.outputs.skip_reason }}" + build: + needs: prepare + if: needs.prepare.outputs.should_deploy == 'true' runs-on: ubuntu-latest defaults: run: @@ -40,16 +82,16 @@ jobs: - name: Install dependencies run: npm ci - - name: Create versioned docs on tag push - if: startsWith(github.ref, 'refs/tags/v') + - name: Create versioned docs on qualifying tag push + if: needs.prepare.outputs.doc_version != '' run: | - FULL_VERSION="${GITHUB_REF#refs/tags/v}" - # Extract major.minor for the doc version (e.g., v3.1.0 -> 3.1.x) - MAJOR="$(echo "$FULL_VERSION" | cut -d. -f1)" - MINOR="$(echo "$FULL_VERSION" | cut -d. -f2)" - DOC_VERSION="${MAJOR}.${MINOR}.x" - echo "Creating docs version ${DOC_VERSION}" - npx docusaurus docs:version "${DOC_VERSION}" + DOC_VERSION="${{ needs.prepare.outputs.doc_version }}" + if [[ -f versions.json ]] && grep -q "\"${DOC_VERSION}\"" versions.json; then + echo "Docs version ${DOC_VERSION} already exists; skipping snapshot creation." + else + echo "Creating docs version ${DOC_VERSION}" + npx docusaurus docs:version "${DOC_VERSION}" + fi - name: Build site run: npm run build @@ -65,11 +107,12 @@ jobs: path: src/CrestApps.Core.Docs/build deploy: + if: needs.prepare.outputs.should_deploy == 'true' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build + needs: [prepare, build] steps: - name: Deploy to GitHub Pages id: deployment diff --git a/Directory.Build.props b/Directory.Build.props index 42956722..7ee744fc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -45,7 +45,7 @@ - 1.0.0 + 1.1.0 $(NoWarn);NU5104 diff --git a/src/CrestApps.Core.Docs/README.md b/src/CrestApps.Core.Docs/README.md index 2d213b8c..c5fadaf7 100644 --- a/src/CrestApps.Core.Docs/README.md +++ b/src/CrestApps.Core.Docs/README.md @@ -19,3 +19,29 @@ npm run build ``` This site contains the framework-only documentation for `CrestApps.Core`. + +## Versioning + +The site keeps a version selector so older releases stay available while `main` +continues to evolve. The unversioned `docs/` folder is the **Latest** version and +tracks `main`. Each released version is frozen under `versioned_docs/` and +`versioned_sidebars/`, with the list of published versions in `versions.json`. + +Versions are created automatically on qualifying tag pushes (`vX.Y.0`) by the +`deploy_docs.yml` GitHub Actions workflow, which snapshots the current docs as +`X.Y.x` (for example, `v1.0.0` produces the `1.0.x` version). To cut a version +manually: + +```bash +npx docusaurus docs:version 1.0.x +``` + +Commit the generated `versioned_docs/`, `versioned_sidebars/`, and `versions.json` +so the frozen version persists across future deployments. + +## Deployment + +The site is deployed automatically to GitHub Pages via the `deploy_docs.yml` +workflow on every push to `main`, on `vX.Y.0` release tag pushes, and on manual +`workflow_dispatch` runs. Prerelease tags (for example `v1.0.0-rc.1`) and patch +tags (for example `v1.0.1`) are intentionally skipped. diff --git a/src/CrestApps.Core.Docs/docs/changelog/index.md b/src/CrestApps.Core.Docs/docs/changelog/index.md index 39fad1db..f9177fb0 100644 --- a/src/CrestApps.Core.Docs/docs/changelog/index.md +++ b/src/CrestApps.Core.Docs/docs/changelog/index.md @@ -11,4 +11,5 @@ This section tracks `CrestApps.Core` releases and notable repository-level chang | Version | Highlights | | --- | --- | +| [1.1.0](v1.1.0) | In-development release; adds versioned documentation with a version selector while `main` keeps updating the Latest docs | | [1.0.0](v1.0.0) | Initial standalone release plus merged configuration catalogs, automatic AI tool dependency expansion, clearer quick-start guidance, and deployment configuration diagnostics | diff --git a/src/CrestApps.Core.Docs/docs/changelog/v1.1.0.md b/src/CrestApps.Core.Docs/docs/changelog/v1.1.0.md new file mode 100644 index 00000000..dfe0beb8 --- /dev/null +++ b/src/CrestApps.Core.Docs/docs/changelog/v1.1.0.md @@ -0,0 +1,24 @@ +--- +sidebar_label: 1.1.0 Release Notes +sidebar_position: 3 +title: "Version 1.1.0 Release Notes" +description: Release notes for the upcoming CrestApps.Core 1.1.0 release. +--- + +# Version 1.1.0 Release Notes + +**Package version**: `1.1.0` + +:::info +`CrestApps.Core` 1.1.0 is the current in-development release. This page tracks the +changes that will ship after 1.0.0. Nightly and preview builds are published from +`main` under the `1.1.0` version prefix. +::: + +## Highlights + +- documentation now ships with a version selector: the released 1.0 docs are frozen under the `1.0.x` version while `main` continues to update the **Latest** version + +## Notes + +- like 1.0.0, this release still ships against .NET 10 preview ecosystem packages (notably `Microsoft.Extensions.DataIngestion`, `A2A.AspNetCore`, and `Lucene.Net.Analysis.Common`) that remain pre-release, so the `NU5104` warning stays suppressed until those upstream packages ship stable releases diff --git a/src/CrestApps.Core.Docs/sidebars.js b/src/CrestApps.Core.Docs/sidebars.js index b31a6fc7..4a59aea6 100644 --- a/src/CrestApps.Core.Docs/sidebars.js +++ b/src/CrestApps.Core.Docs/sidebars.js @@ -96,6 +96,7 @@ const sidebars = { label: 'Changelog', items: [ 'changelog/index', + 'changelog/v1.1.0', 'changelog/v1.0.0', ], }, diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/client.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/client.md new file mode 100644 index 00000000..429f635a --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/client.md @@ -0,0 +1,553 @@ +--- +sidebar_label: A2A Client +sidebar_position: 2 +title: A2A Client +description: Discover and invoke remote AI agents using the A2A protocol client — connection management, tool registry integration, authentication, and built-in discovery tools. +--- + +# A2A Client + +> Discover and invoke remote AI agents by registering A2A connections, fetching their Agent Cards, and exposing their skills as tools in the orchestrator. + +## Quick Start + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddOpenAI() + .AddA2AClient(a2a => a2a + .AddEntityCoreStores() + ) + ) + .AddEntityCoreSqliteDataStore("Data Source=app.db") +); +``` + +This registers everything needed to consume remote A2A agents: HTTP infrastructure, agent card caching, tool registry integration, authentication services, and three built-in discovery tools. + +## Problem & Solution + +Your AI application needs to delegate tasks to agents running in other applications — a translation service, a code-review agent, a document-summarization agent. Each remote agent has its own AI model, tools, and reasoning. You need a way to: + +- **Discover** what remote agents can do (without hardcoding their capabilities) +- **Invoke** remote agents as if they were local tools +- **Authenticate** with each remote host (API keys, OAuth2, certificates) +- **Cache** agent metadata for performance + +The A2A client solves all of this. It fetches Agent Cards from remote hosts, converts each advertised skill into a tool registry entry, and proxies tool calls to the remote agent transparently. + +## Services Registered by `AddCoreAIA2AClient()` + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `HttpClient` | via `IHttpClientFactory` | — | HTTP communication with remote hosts | +| `IMemoryCache` | — | Singleton | Caching infrastructure for agent cards and OAuth2 tokens | +| `IHttpContextAccessor` | `HttpContextAccessor` | Singleton | Access to the current HTTP context for scoped service resolution | +| `IAICompletionContextBuilderHandler` | `A2AAICompletionContextBuilderHandler` | Scoped | Copies A2A connection IDs from the AI profile into the completion context | +| `IToolRegistryProvider` | `A2AToolRegistryProvider` | Scoped | Discovers remote agent skills and exposes them as tool entries | +| `IA2AAgentCardCacheService` | `DefaultA2AAgentCardCacheService` | Singleton | Fetches and caches Agent Cards from remote hosts (15-minute TTL) | +| `IA2AConnectionAuthService` | `DefaultA2AConnectionAuthService` | Scoped | Builds HTTP authentication headers for each connection | + +### Built-in Tools + +Three system tools are registered automatically: + +| Tool Name | Class | Purpose | +|-----------|-------|---------| +| `listAvailableAgents` | `ListAvailableAgentsFunction` | Lists all available agents — both local AI profiles and remote A2A agents | +| `findAgentForTask` | `FindAgentForTaskFunction` | Finds the best agents for a given task using keyword matching | +| `findToolsForTask` | `FindToolsForTaskFunction` | Discovers tools (including remote agent skills) relevant to a task | + +## How It All Fits Together + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ Your Application │ +│ │ +│ 1. AI Profile has A2AConnectionIds = ["conn-abc", "conn-xyz"] │ +│ │ │ +│ 2. A2AAICompletionContextBuilderHandler copies connection IDs │ +│ into AICompletionContext.A2AConnectionIds │ +│ │ │ +│ 3. A2AToolRegistryProvider.GetToolsAsync() runs: │ +│ ┌───────────────────▼──────────────────┐ │ +│ │ For each connection ID: │ │ +│ │ a. Load A2AConnection from store │ │ +│ │ b. Fetch Agent Card (cached 15 min) │ │ +│ │ c. For each skill on the card: │ │ +│ │ → Create ToolRegistryEntry │ │ +│ │ Id: "a2a:{connId}:{skillName}" │ │ +│ │ Source: A2AAgent │ │ +│ │ Factory: → A2AAgentProxyTool │ │ +│ └──────────────────────────────────────┘ │ +│ │ │ +│ 4. AI model sees remote skills as invokable tools │ +│ │ │ +│ 5. Model calls a tool → A2AAgentProxyTool executes: │ +│ a. Load connection + auth metadata │ +│ b. Configure HttpClient with auth headers │ +│ c. Send AgentMessage to remote endpoint │ +│ d. Extract text from response │ +│ e. Return text to the AI model │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Connection Management + +### The `A2AConnection` Model + +Every remote A2A host is represented by an `A2AConnection`: + +```csharp +public sealed class A2AConnection : CatalogItem, IDisplayTextAwareModel +{ + // Human-readable name for the connection (e.g., "Legal Review Agent") + public string DisplayText { get; set; } + + // The remote A2A host's base URL (e.g., "https://agents.example.com/a2a") + public string Endpoint { get; set; } + + // When the connection was created + public DateTime CreatedUtc { get; set; } + + // Who created the connection + public string Author { get; set; } + + // Owner user ID + public string OwnerId { get; set; } +} +``` + +`A2AConnection` extends `CatalogItem`, which means it supports the `Properties` dictionary for extensible metadata. Authentication details are stored as an `A2AConnectionMetadata` object in this dictionary. + +### Registering A2A Connection Stores + +The framework defines the `A2AConnection` model and ships built-in store implementations for both YesSql and Entity Framework Core. Register stores directly on the A2A client builder: + +**Entity Framework Core (via builder):** + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddA2AClient(a2a => a2a + .AddEntityCoreStores() + ) + ) +); +``` + +**YesSql (via builder):** + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddA2AClient(a2a => a2a + .AddYesSqlStores() + ) + ) +); +``` + +Both register an `ICatalog` implementation. If your host uses a different storage technology, implement `ICatalog` yourself: + +```csharp +public sealed class MyA2AConnectionStore : ICatalog +{ + private readonly IDbConnection _db; + + public MyA2AConnectionStore(IDbConnection db) + { + _db = db; + } + + public Task FindByIdAsync(string id) + { + // Load from your database + } + + public Task> GetAllAsync() + { + // Return all configured connections + } + + // ... other CRUD methods +} + +// Register in DI +builder.Services.AddScoped, MyA2AConnectionStore>(); +``` + +::: + +### Assigning Connections to AI Profiles + +Connections are linked to AI profiles via `AIProfileA2AMetadata`: + +```csharp +public sealed class AIProfileA2AMetadata +{ + // The IDs of A2A connections available to this profile + public string[] ConnectionIds { get; set; } +} +``` + +When the orchestrator builds the completion context, `A2AAICompletionContextBuilderHandler` reads these IDs from the profile and sets them on `AICompletionContext.A2AConnectionIds`. This tells `A2AToolRegistryProvider` which remote hosts to query for tools. + +```csharp +// How the handler works internally: +internal sealed class A2AAICompletionContextBuilderHandler : IAICompletionContextBuilderHandler +{ + public Task BuildingAsync(AICompletionContextBuildingContext context) + { + if (context.Resource is AIProfile profile && + profile.TryGet(out var a2aMetadata)) + { + context.Context.A2AConnectionIds = a2aMetadata.ConnectionIds; + } + + return Task.CompletedTask; + } +} +``` + +## Agent Card Discovery & Caching + +### What Is an Agent Card? + +An Agent Card is a JSON document published by an A2A host at a well-known URL (typically `/.well-known/agent.json`). It describes: + +- The agent's name and description +- A list of **skills** (capabilities the agent can perform) +- Each skill's ID, name, description, and tags +- The endpoint URL for sending messages + +### `IA2AAgentCardCacheService` + +The framework caches Agent Cards in memory to avoid fetching them on every request: + +```csharp +public interface IA2AAgentCardCacheService +{ + /// Fetches the Agent Card for a connection, using a cached value if available. + Task GetAgentCardAsync( + string connectionId, + A2AConnection connection, + CancellationToken cancellationToken = default); + + /// Removes the cached Agent Card for a connection. + void Invalidate(string connectionId); +} +``` + +The default implementation (`DefaultA2AAgentCardCacheService`) caches cards for **15 minutes** using `IMemoryCache`. It: + +1. Checks the cache using key `A2AAgentCard:{connectionId}` +2. On cache miss, creates an `HttpClient` and configures it with authentication headers +3. Uses `A2ACardResolver` to fetch the Agent Card from the remote host +4. Caches the result and returns it + +To customize caching behavior (e.g., use distributed cache, change TTL), register your own implementation: + +```csharp +builder.Services.AddSingleton(); +``` + +## Tool Registry Integration + +### How Remote Agents Become Tools + +`A2AToolRegistryProvider` implements `IToolRegistryProvider` and is called by the orchestrator when building the tool set for a completion request. + +For each connection ID in `AICompletionContext.A2AConnectionIds`: + +1. **Load** the `A2AConnection` from the catalog +2. **Fetch** the Agent Card (cached) +3. **Iterate** skills on the Agent Card +4. **Create** a `ToolRegistryEntry` for each skill: + +```csharp +new ToolRegistryEntry +{ + Id = $"a2a:{connectionId}:{skillName}", // Unique tool ID + Name = skillName, // Sanitized skill name + Description = skill.Description, // Shown to the AI model + Source = ToolRegistryEntrySource.A2AAgent, // Identifies this as an A2A tool + SourceId = connectionId, // Links back to the connection + CreateAsync = _ => new A2AAgentProxyTool(...) // Factory for the proxy tool +} +``` + +The tool name is sanitized to contain only letters, digits, and underscores — ensuring compatibility with AI model function-calling requirements. + +## Agent Proxy Execution + +When the AI model decides to invoke a remote agent skill, `A2AAgentProxyTool` handles the execution. + +### Input Schema + +```json +{ + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The message or task to send to the remote agent for processing." + }, + "contextId": { + "type": "string", + "description": "An optional context identifier to maintain conversation continuity with the remote agent." + } + }, + "required": ["message"] +} +``` + +### Execution Flow + +```text +1. Validate input — "message" is required +2. Create HttpClient via IHttpClientFactory +3. Load connection from ICatalog +4. Read A2AConnectionMetadata from connection properties +5. Configure HttpClient with authentication headers +6. Create A2AClient pointing at the remote endpoint +7. Build AgentMessage: + - Role: User + - MessageId: new GUID + - ContextId: provided or new GUID + - Parts: [TextPart with the message] + - Metadata: { "agentName": skill name } +8. Send message via client.SendMessageAsync() +9. Extract text from response: + - AgentMessage → join TextParts + - AgentTask → check Artifacts, then Status.Message +10. Return text to the AI model +``` + +### Response Handling + +The proxy tool handles two types of A2A responses: + +- **`AgentMessage`** — Direct response with text parts (synchronous completion) +- **`AgentTask`** — Task-based response where text may be in artifacts or status messages (async workflows) + +If no text can be extracted, the tool returns: `"The remote agent did not produce a text response."` + +If communication fails, the error is logged and a user-friendly message is returned (the exception is not propagated to the AI model). + +## Built-in Discovery Tools + +The A2A client registers three system tools that help the AI model discover available agents and tools at runtime. + +### `ListAvailableAgentsFunction` + +Lists **all** available agents — both local AI Agent profiles and remote agents from A2A connections. + +- **Tool name**: `listAvailableAgents` +- **Parameters**: None +- **Returns**: JSON array of agents with `name`, `id`, `description`, `source` ("local" or "remote"), and optionally `host` and `tags` + +```text +AI Model: "What agents are available?" +→ Calls listAvailableAgents +→ Returns: +[ + { "name": "Code Reviewer", "id": "code-reviewer", "source": "local" }, + { "name": "Legal Analyst", "id": "legal-analyst", "source": "remote", "host": "Legal Team" } +] +``` + +### `FindAgentForTaskFunction` + +Finds the most relevant agents for a given task using keyword and semantic matching. + +- **Tool name**: `findAgentForTask` +- **Parameters**: + - `taskDescription` (string, required) — What the task is about + - `maxResults` (integer, optional) — Maximum agents to return (default: 5) +- **Returns**: JSON array of agents ranked by relevance score + +The function tokenizes the task description and scores each agent based on keyword overlap between the query and the agent's name + description + tags. Both forward and reverse matching are used to balance precision and recall. + +### `FindToolsForTaskFunction` + +Discovers tools (including remote agent skills) relevant to a given task. + +- **Tool name**: `findToolsForTask` +- **Parameters**: + - `taskDescription` (string, required) — What the task is about + - `maxResults` (integer, optional) — Maximum tools to return (default: 10) +- **Returns**: JSON array of tools with `name`, `description`, and `source` + +This function delegates to `IToolRegistry.SearchAsync()` to use the same scoring logic as the orchestrator's tool-scoping system. It automatically includes all A2A connections when building the search context. + +## Authentication + +### `IA2AConnectionAuthService` + +The authentication service builds HTTP headers for each connection based on its configured authentication type: + +```csharp +public interface IA2AConnectionAuthService +{ + /// Builds authentication headers from connection metadata. + Task> BuildHeadersAsync( + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default); + + /// Configures an HttpClient with authentication headers. + Task ConfigureHttpClientAsync( + HttpClient httpClient, + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default); +} +``` + +### Supported Authentication Types + +Authentication metadata is stored in `A2AConnectionMetadata`: + +| Type | Enum Value | How It Works | +|------|-----------|--------------| +| **Anonymous** | `Anonymous` | No authentication headers added | +| **API Key** | `ApiKey` | Sends the key in a configurable header (default: `Authorization`) with optional prefix (e.g., `Bearer`) | +| **Basic** | `Basic` | Base64-encodes `username:password` and sends as `Authorization: Basic {encoded}` | +| **OAuth2 Client Credentials** | `OAuth2ClientCredentials` | Exchanges `client_id` + `client_secret` for a bearer token at the token endpoint | +| **OAuth2 Private Key JWT** | `OAuth2PrivateKeyJwt` | Creates a signed JWT assertion using an RSA private key and exchanges it for a token | +| **OAuth2 Mutual TLS** | `OAuth2Mtls` | Uses a client certificate for mutual TLS authentication when requesting a token | +| **Custom Headers** | `CustomHeaders` | Sends arbitrary key-value pairs as HTTP headers | + +### `A2AConnectionMetadata` Properties + +```csharp +public sealed class A2AConnectionMetadata +{ + // Which authentication type to use + public ClientAuthenticationType AuthenticationType { get; set; } + + // API Key authentication + public string ApiKeyHeaderName { get; set; } // Default: "Authorization" + public string ApiKeyPrefix { get; set; } // e.g., "Bearer" + public string ApiKey { get; set; } // The key (encrypted via DataProtection) + + // Basic authentication + public string BasicUsername { get; set; } + public string BasicPassword { get; set; } // Encrypted via DataProtection + + // OAuth 2.0 Client Credentials + public string OAuth2TokenEndpoint { get; set; } + public string OAuth2ClientId { get; set; } + public string OAuth2ClientSecret { get; set; } // Encrypted via DataProtection + public string OAuth2Scopes { get; set; } + + // OAuth 2.0 Private Key JWT + public string OAuth2PrivateKey { get; set; } // PEM-encoded RSA private key (encrypted) + public string OAuth2KeyId { get; set; } + + // OAuth 2.0 Mutual TLS (mTLS) + public string OAuth2ClientCertificate { get; set; } // Base64 PKCS#12 (encrypted) + public string OAuth2ClientCertificatePassword { get; set; } // Encrypted + + // Custom headers + public Dictionary AdditionalHeaders { get; set; } +} +``` + +### Credential Protection + +All sensitive fields (API keys, passwords, secrets, private keys, certificates) are encrypted using ASP.NET Core Data Protection with the purpose string `"A2AClientConnection"`. The `DefaultA2AConnectionAuthService` automatically decrypts values before use. + +OAuth2 tokens are cached in `IMemoryCache` with a TTL based on the token's `expires_in` minus a 60-second buffer. + +### Custom Authentication + +To implement a custom authentication scheme, register your own `IA2AConnectionAuthService`: + +```csharp +public sealed class MyA2AAuthService : IA2AConnectionAuthService +{ + public Task> BuildHeadersAsync( + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Your custom logic — e.g., fetch tokens from a vault + headers["Authorization"] = "Bearer " + GetTokenFromVault(); + + return Task.FromResult(headers); + } + + public async Task ConfigureHttpClientAsync( + HttpClient httpClient, + A2AConnectionMetadata metadata, + CancellationToken cancellationToken = default) + { + var headers = await BuildHeadersAsync(metadata, cancellationToken); + + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + } + } +} + +// Register (replaces the default) +builder.Services.AddScoped(); +``` + +## Configuration Examples + +### Minimal Setup (anonymous remote agent) + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIA2AClient(); + +// Register your connection store +builder.Services.AddScoped, MyA2AConnectionStore>(); +``` + +### With API Key Authentication + +```csharp +// When creating a connection, store the metadata: +var connection = new A2AConnection +{ + DisplayText = "Partner Translation Service", + Endpoint = "https://translate.partner.com/a2a", +}; + +var metadata = new A2AConnectionMetadata +{ + AuthenticationType = ClientAuthenticationType.ApiKey, + ApiKeyHeaderName = "Authorization", + ApiKeyPrefix = "Bearer", + ApiKey = protector.Protect("sk-partner-key-12345"), +}; + +connection.Put(metadata); + +await connectionStore.CreateAsync(connection); +``` + +### With OAuth2 Client Credentials + +```csharp +var metadata = new A2AConnectionMetadata +{ + AuthenticationType = ClientAuthenticationType.OAuth2ClientCredentials, + OAuth2TokenEndpoint = "https://auth.partner.com/oauth2/token", + OAuth2ClientId = "my-app-client-id", + OAuth2ClientSecret = protector.Protect("my-client-secret"), + OAuth2Scopes = "a2a.invoke", +}; +``` + +- Admin UI for creating and managing A2A connections +- Built-in `ICatalog` backed by YesSql +- Authentication configuration forms for all supported types +- Connection assignment to AI profiles via the profile editor +- Agent card preview and cache invalidation diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/host.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/host.md new file mode 100644 index 00000000..768fa254 --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/host.md @@ -0,0 +1,322 @@ +--- +sidebar_label: A2A Host +sidebar_position: 3 +title: A2A Host +description: Expose your AI agents to remote clients using the A2A protocol host — configuration, authentication modes, agent card generation, and skill exposure. +--- + +# A2A Host + +> Expose your AI agents to remote A2A clients so they can discover and invoke your agents over HTTP. + +## Quick Start + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-api-key"; +}); +``` + +## Problem & Solution + +You have AI agents (profiles) running in your application and you want other applications to be able to discover and invoke them. The A2A host configuration: + +- **Publishes** your agents via Agent Cards at a well-known endpoint +- **Authenticates** incoming requests using OpenID Connect, API keys, or no auth +- **Authorizes** access with optional permission checks +- **Controls** whether agents appear as individual agent cards or as skills of a single combined card + +## Host Configuration + +### `A2AHostOptions` + +All host behavior is controlled through `A2AHostOptions`: + +```csharp +public sealed class A2AHostOptions +{ + /// The authentication type for incoming A2A requests. + /// Default: OpenId + public A2AHostAuthenticationType AuthenticationType { get; set; } + = A2AHostAuthenticationType.OpenId; + + /// The API key required when AuthenticationType is ApiKey. + public string ApiKey { get; set; } + + /// Whether to require the AccessA2AHost permission. + /// Only applies to OpenId authentication. Default: true + public bool RequireAccessPermission { get; set; } = true; + + /// Whether to expose all agents as skills of a single combined agent card. + /// When false (default), each agent gets its own agent card. + public bool ExposeAgentsAsSkill { get; set; } = false; +} +``` + +### Configuration via `IServiceCollection` + +```csharp +// In Program.cs or Startup.cs +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.OpenId; + options.RequireAccessPermission = true; +}); +``` + +### Configuration via `appsettings.json` + +```json +{ + "A2AHost": { + "AuthenticationType": "ApiKey", + "ApiKey": "your-secret-api-key", + "RequireAccessPermission": true, + "ExposeAgentsAsSkill": false + } +} +``` + +```csharp +builder.Services.Configure( + builder.Configuration.GetSection("A2AHost")); +``` + +## Authentication Modes + +The host supports three authentication types via `A2AHostAuthenticationType`: + +### OpenID Connect (`OpenId`) — Default + +The most secure option for production. Incoming requests are authenticated using the `"Api"` OpenID Connect scheme. Tokens are validated against your OpenID provider. + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.OpenId; + options.RequireAccessPermission = true; // Require AccessA2AHost permission +}); +``` + +When `RequireAccessPermission` is `true`, the authenticated user must also have the `AccessA2AHost` permission. When `false`, any valid authenticated user can access the host. + +:::tip +::: + +### API Key (`ApiKey`) + +A simple shared-secret authentication. The client must send the API key in the `Authorization` header: + +```text +Authorization: Bearer your-secret-api-key +``` + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-api-key"; +}); +``` + +:::warning +Store the API key in a secure location (environment variable, Azure Key Vault, etc.). Never hardcode it in source code. The `RequireAccessPermission` option does **not** apply to API key authentication. +::: + +### None — Development Only + +Disables all authentication. Any request is accepted. + +```csharp +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.None; +}); +``` + +:::danger +**Never use `None` in production.** This option exists solely for local development and testing. +::: + +### Authentication Comparison + +| Feature | OpenId | ApiKey | None | +|---------|--------|--------|------| +| **Security level** | High | Medium | ❌ None | +| **Token validation** | ✅ JWT/OIDC | ❌ Shared secret | ❌ | +| **User identity** | ✅ Full claims | ❌ Anonymous | ❌ Anonymous | +| **Permission checks** | ✅ Optional | ❌ | ❌ | +| **Best for** | Production | Internal/partner APIs | Local dev | + +## Agent Card Generation + +### How Profiles Become Agent Cards + +When a remote client fetches the Agent Card from your host, the host implementation reads your AI profiles and converts them into the A2A Agent Card format: + +```text +┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ +│ AI Profile │────────►│ Agent Card │────────►│ /.well-known/ │ +│ (your app) │ │ Generator │ │ agent.json │ +│ │ │ │ │ │ +│ Name │ │ Name │ │ Published to remote │ +│ Description │ │ Description │ │ A2A clients │ +│ Type: Agent │ │ Skills [] │ │ │ +└──────────────┘ └──────────────┘ └──────────────────────┘ +``` + +Each AI profile of type `Agent` becomes either: +- An **independent Agent Card** (default behavior), or +- A **skill on a combined Agent Card** (when `ExposeAgentsAsSkill` is `true`) + +This includes **code-defined system agents** contributed through `IAIProfileProvider`. For example, the built-in Tabular Data Agent is hidden from the MVC and Blazor agent pickers because it is always available and system-managed, but it is still published through the A2A host because the host reads the merged `IAIProfileManager.GetAsync(AIProfileType.Agent)` result. + +### Agent Card Structure (A2A Protocol) + +A published Agent Card follows the A2A specification: + +```json +{ + "name": "My AI Assistant", + "description": "An AI assistant that can help with various tasks.", + "url": "https://myapp.example.com/a2a", + "skills": [ + { + "id": "translate-text", + "name": "Text Translator", + "description": "Translates text between languages.", + "tags": ["translation", "language"] + }, + { + "id": "summarize-document", + "name": "Document Summarizer", + "description": "Summarizes long documents into key points.", + "tags": ["summarization", "documents"] + } + ] +} +``` + +## Skill Exposure + +### Individual Agent Cards (Default) + +When `ExposeAgentsAsSkill` is `false` (the default), each agent profile is exposed as its own independent Agent Card. Remote clients see separate agents and can invoke them individually. + +```text +Profile: "Code Reviewer" → Agent Card: { name: "Code Reviewer", skills: [...] } +Profile: "Translator" → Agent Card: { name: "Translator", skills: [...] } +``` + +This is the recommended approach when your agents are independent and serve different purposes. + +### Combined Agent Card + +When `ExposeAgentsAsSkill` is `true`, a single Agent Card is published with each agent profile listed as a skill: + +```text +Combined Agent Card: { + name: "My Application", + skills: [ + { id: "code-reviewer", name: "Code Reviewer", ... }, + { id: "translator", name: "Translator", ... } + ] +} +``` + +This approach is useful when: +- You want remote clients to see a **single entry point** to your application +- The client's AI model should choose which skill to invoke based on the task +- You want to simplify discovery for clients that don't need to manage multiple connections + +Because system agents are part of the same merged agent list, they also appear here automatically. That means a hidden agent can remain unavailable for manual UI selection while still being discoverable to remote A2A clients. + +```csharp +builder.Services.Configure(options => +{ + options.ExposeAgentsAsSkill = true; +}); +``` + +## Endpoint Setup + +The A2A protocol defines two key endpoints that your host must serve: + +### Agent Card Endpoint + +Remote clients discover your agents by fetching the Agent Card: + +```text +GET /.well-known/agent.json +``` + +This returns the Agent Card JSON (or multiple cards, depending on your `ExposeAgentsAsSkill` setting). + +### Message Endpoint + +Remote clients send tasks to your agents via: + +```text +POST /a2a +Content-Type: application/json + +{ + "message": { + "role": "user", + "messageId": "msg-123", + "contextId": "ctx-456", + "parts": [{ "type": "text", "text": "Translate this to French: Hello world" }] + } +} +``` + +The host routes the message to the appropriate AI profile, processes it, and returns a response. + +### Implementation Pattern + +The actual endpoint implementation depends on your application framework. Here is a conceptual pattern: + +```csharp +// Agent Card endpoint +app.MapGet("/.well-known/agent.json", async ( + IOptions options, + IAIProfileManager profileManager) => +{ + // 1. Load agent profiles + // 2. Convert to Agent Card format + // 3. Return JSON +}); + +// Message endpoint +app.MapPost("/a2a", async ( + HttpContext context, + IOptions options, + IAICompletionService completionService) => +{ + // 1. Authenticate the request based on A2AHostOptions + // 2. Parse the incoming AgentMessage + // 3. Route to the appropriate AI profile + // 4. Process and return the response +}); +``` + +:::info +::: + +## Security Best Practices + +1. **Always use OpenID or API Key authentication in production** — never deploy with `AuthenticationType = None` +2. **Rotate API keys regularly** — treat them as secrets with a defined rotation policy +3. **Use `RequireAccessPermission = true`** with OpenID — this ensures only authorized users/applications can invoke your agents +4. **Restrict agent exposure** — only expose agent profiles that are intended for remote consumption +5. **Monitor agent invocations** — log and audit incoming A2A requests +6. **Use HTTPS** — the A2A protocol sends messages over HTTP; always use TLS in production + +- Full endpoint implementation (Agent Card + message endpoints) +- Admin UI for configuring `A2AHostOptions` +- Automatic conversion of AI profiles to Agent Cards +- Built-in authentication middleware for all three modes +- Permission management for the `AccessA2AHost` permission +- Support for both individual and combined Agent Card modes diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/index.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/index.md new file mode 100644 index 00000000..8563e83c --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/a2a/index.md @@ -0,0 +1,105 @@ +--- +sidebar_label: Overview +sidebar_position: 1 +title: Agent-to-Agent Protocol (A2A) +description: Connect to remote AI agents and expose your own agents using the A2A protocol for cross-application agent collaboration. +--- + +# Agent-to-Agent Protocol (A2A) + +> Discover, invoke, and expose AI agents across application boundaries using the [Agent-to-Agent (A2A) protocol](https://google.github.io/A2A/). + +## What Is A2A? + +The Agent-to-Agent (A2A) protocol, developed by Google, is an open standard that enables AI agents running in **different applications** to discover each other, negotiate capabilities, and delegate tasks — all over HTTP. Unlike tool-calling protocols that expose individual functions, A2A operates at the **agent level**: a remote agent is a self-contained entity with its own reasoning, tools, and context. + +Key concepts: + +| Concept | Description | +|---------|-------------| +| **Agent Card** | A JSON document published by a host that describes the agent's name, description, skills, and endpoint URL. Clients fetch this to discover what a host offers. | +| **Skill** | A named capability advertised on an Agent Card (e.g., "translate-text", "summarize-document"). Each skill becomes an invokable tool on the client side. | +| **Host** | An application that **exposes** one or more AI agents to remote clients. | +| **Client** | An application that **discovers and invokes** remote agents hosted elsewhere. | +| **Message** | The unit of communication — a client sends an `AgentMessage` to the host and receives a response containing text, artifacts, or task status. | + +## When to Use A2A vs MCP + +Both protocols connect AI systems across boundaries, but they solve different problems: + +| Criteria | A2A | MCP | +|----------|-----|-----| +| **Abstraction level** | Agent-level (send a task, get a result) | Tool-level (call a function, get a return value) | +| **Best for** | Delegating complex, multi-step work to a remote AI agent | Exposing individual functions, data sources, or resources | +| **Remote agent has its own AI model?** | ✅ Yes — the remote agent reasons independently | ❌ No — tools are stateless functions | +| **Conversation context** | Maintained via `contextId` across messages | Stateless per tool call | +| **Discovery** | Agent Cards with skills | Tool lists with JSON schemas | +| **Use when** | "Ask the legal team's agent to review this contract" | "Call the weather API to get today's forecast" | + +**Rule of thumb**: If the remote system needs to **think** (use an AI model, maintain context, choose its own tools), use A2A. If it just needs to **do** (execute a function and return data), use MCP. + +You can use both in the same application — A2A for agent delegation and MCP for tool access. + +## Architecture + +```text +┌─────────────────────────────────┐ ┌──────────────────────────────────┐ +│ A2A CLIENT │ │ A2A HOST │ +│ │ │ │ +│ ┌───────────┐ │ HTTP │ ┌──────────────┐ │ +│ │ AI Model │ │ ◄──────► │ │ AI Profiles │ │ +│ └─────┬─────┘ │ │ └──────┬───────┘ │ +│ │ tool call │ │ │ │ +│ ┌─────▼──────────────────┐ │ │ ┌───────────────────▼────────┐ │ +│ │ A2AToolRegistryProvider│ │ │ │ Agent Card Generator │ │ +│ │ (discovers skills as │ │ │ │ (profiles → agent cards) │ │ +│ │ tool entries) │ │ │ └───────────────────┬────────┘ │ +│ └─────┬──────────────────┘ │ │ │ │ +│ │ │ │ ┌──────────────────▼─────────┐ │ +│ ┌─────▼──────────────────┐ │ fetch │ │ /.well-known/agent.json │ │ +│ │ A2AAgentProxyTool ├─────┼──────────┼──► (Agent Card endpoint) │ │ +│ │ (proxies messages │ │ │ └───────────────────────────┘ │ +│ │ to remote agent) │ │ send │ │ +│ │ ├─────┼──────────┼──► /a2a (message endpoint) │ +│ └────────────────────────┘ │ │ │ +│ │ │ Authentication: │ +│ Authentication: │ │ • OpenID Connect │ +│ • API Key, Basic, OAuth2, │ │ • API Key │ +│ mTLS, Custom Headers │ │ • None (dev only) │ +└─────────────────────────────────┘ └──────────────────────────────────┘ +``` + +## Quick Start + +### As a Client (invoke remote agents) + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration() + .AddCoreAIA2AClient(); +``` + +→ See the [A2A Client](./client) page for connection setup, authentication, and tool registry details. + +### As a Host (expose your agents) + +```csharp +// Host configuration is done via A2AHostOptions +builder.Services.Configure(options => +{ + options.AuthenticationType = A2AHostAuthenticationType.ApiKey; + options.ApiKey = "your-secret-key"; +}); +``` + +→ See the [A2A Host](./host) page for authentication modes, agent card generation, and endpoint configuration. + +## Sub-Pages + +| Page | Description | +|------|-------------| +| [A2A Client](./client) | Discover and invoke remote A2A agents — connection management, tool registry, authentication, built-in discovery tools | +| [A2A Host](./host) | Expose your AI agents to remote clients — host configuration, authentication modes, agent card generation | + +The framework-level A2A support documented here is protocol infrastructure. For the full admin UI experience: diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/index.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/index.md new file mode 100644 index 00000000..39fad1db --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/index.md @@ -0,0 +1,14 @@ +--- +sidebar_label: Overview +sidebar_position: 0 +title: Changelog +description: Release history and migration notes for CrestApps.Core. +--- + +# Changelog + +This section tracks `CrestApps.Core` releases and notable repository-level changes. + +| Version | Highlights | +| --- | --- | +| [1.0.0](v1.0.0) | Initial standalone release plus merged configuration catalogs, automatic AI tool dependency expansion, clearer quick-start guidance, and deployment configuration diagnostics | diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/v1.0.0.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/v1.0.0.md new file mode 100644 index 00000000..08011b47 --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/changelog/v1.0.0.md @@ -0,0 +1,120 @@ +--- +sidebar_label: 1.0.0 Release Notes +sidebar_position: 2 +title: "Version 1.0.0 Release Notes" +description: Initial standalone release notes for the CrestApps.Core repository. +--- + +# Version 1.0.0 Release Notes + +**Package version**: `1.0.0` + +`CrestApps.Core` 1.0.0 establishes the standalone framework repository for the reusable CrestApps libraries. + +## Highlights + +- ships the shared abstractions, infrastructure, AI runtime, provider integrations, and protocol packages under the `CrestApps.Core` name +- assigns stable first-party anonymous visitor IDs to AI chat sessions, uses those visitor IDs for unique-visitor analytics, makes remote-address capture configurable with privacy-first hashed defaults plus optional plain-text or encrypted-at-rest storage, protects anonymous chat session starts with shared ASP.NET Core plus hub-level rate limiting, and documents how hosts can tune thresholds or replace the default endpoint policy with their own `Microsoft.AspNetCore.RateLimiting` policy +- lets AI Profiles and AI Profile-source templates override the site-wide anti-spam throttle limits through `PromptSecurityProfileSettings` (max messages per window, message window, max anonymous sessions per window, and anonymous session window), with each unset value falling back to the `PromptSecurityOptions` site default so profiles can raise or lower quotas per use case; both the message and anonymous session-start limiters honor the overrides, while high-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only concerns +- defers AI Profile initial-prompt persistence until the first real user prompt arrives and stops the sample chat widgets from auto-creating empty sessions on initial page load +- includes a reference MVC host and an Aspire host for local composition and testing +- includes a dedicated `CrestApps.Core.Tests` project for framework validation +- publishes a framework-focused documentation site at [core.crestapps.com](https://core.crestapps.com) +- makes the generic AI deployment and connection catalog interfaces provider-backed database stores when YesSql or EntityCore is registered, while keeping `IAIDeploymentStore` and `IAIProviderConnectionStore` as the merged runtime views across configuration and database sources +- merges appsettings-backed and UI-managed AI provider connections and deployments through runtime catalogs, so MVC selectors and AI resolution stay current without rebuilding options or restarting the app +- keeps `IAIProviderConnectionStore` and `IAIDeploymentStore` as the merged runtime views, makes the shared generic AI catalog interfaces provider-backed database stores once YesSql or EntityCore is registered, initializes configured YesSql collections up front so `AI`, `AIDocs`, and `AIMemory` each get their own document tables, adds generic `Add*DocumentCatalog()` helpers for custom catalog registration, keeps deterministic name conflict handling so UI-managed records override conflicting appsettings entries, and standardizes settings so connections and deployments are configured separately +- documents the default AI configuration sections (`CrestApps:AI:Connections` and `CrestApps:AI:Deployments`), tightens the quick-start path around Chat Interactions, and refreshes the docs navigation and landing page for faster onboarding +- keeps the MVC sample focused on the recommended standalone AI configuration layout by default, using `CrestApps:AI:Connections` and `CrestApps:AI:Deployments` instead of provider-grouped sample settings +- adds Debug-level diagnostics in `ConfigurationAIDeploymentCatalog` so hosts can trace which configuration sections were evaluated and how standalone deployments were parsed +- treats `CrestApps:AI:Deployments` as shared deployment metadata for all providers, not only providers with contained-connection support, so hosts can keep credentials in `Connections` while still declaring deployment names and types in appsettings +- evaluates every configured AI connection and deployment section when importing appsettings records, including provider-grouped connection sections and deployment entries that reference shared `ConnectionName` values +- adds shared `JsonNode` support extensions for common string, boolean, and raw-value extraction so AI configuration parsing and Elasticsearch document readers reuse one implementation instead of duplicating private helpers +- replaces removed obsolete connection-level deployment-name helpers with non-obsolete legacy lookup extensions for `AIProviderConnectionEntry`, keeping backward-compatible fallback resolution without depending on deleted APIs +- renames `CrestApps.Core.AI.AISearch` to `CrestApps.Core.AI.Azure.AISearch`, groups the docs navigation around orchestrators, surfaces the Claude docs page, renames AI Providers to AI Clients, and updates the OpenAI docs to call out common OpenAI-compatible endpoints plus the dedicated Claude path +- renames the old `AddCoreAIProfile()` provider-registration helper to the completion-client-based `AddCoreAICompletionClient(..., configure)` overload, and updates `AIOptions` metadata from `ProfileSources` / `AIProfileProviderEntry` to `CompletionClients` / `AICompletionClientEntry` +- aligns the built-in Entity Framework Core stores with the same `IStoreCommitter` unit-of-work pattern as YesSql and refreshes the storage/getting-started docs to explain MVC, Minimal API, SignalR, and background commit boundaries consistently +- adds hierarchical document retrieval mode support so document RAG can rank on chunks and then inject full matched document text when hosts or profiles opt into that behavior +- moves data-source source-to-knowledge-base synchronization into shared framework services so `AIDataSource` mappings react automatically to `ISearchDocumentManager` upserts and deletes, and adds nightly background reconciliation to repair drift without host-specific observer code +- replaces the data-source observer hook with shared `ISearchDocumentHandler`-based notifications, adds Trace-level logging across queueing and background processing for async data-source synchronization, and documents the queue/handler override points plus provider `AddAIDataSources()` registrations +- generalizes `AIDataSource` sources beyond `SearchIndexProfile` by adding first-class source-type handling, built-in external source connectors for Elasticsearch, Azure AI Search, and PostgreSQL, protected per-source credentials, and a public `IAIDataSourceChangeNotifier` contract so custom or external systems can push add/update/delete events into the knowledge-base sync pipeline +- removes Azure OpenAI connection-level logging flags in favor of shared `CrestApps:AI:AzureClient` settings, keeps Azure completion resolution deployment-driven, and refreshes the AI client docs around `ClientName`-based configuration +- clarifies deployment-store registration by introducing `IAIDeploymentStore` for persisted deployments, moves Chat Interactions ahead of AI Profiles / AI Chat in the MVC sample onboarding flow, and adds dedicated AI Profile documentation that explains how profiles power reusable chat, agents, orchestration, retrieval, and session processing +- registers a shared `DefaultAIProfileManager` in the framework so YesSql, EntityCore, and custom profile catalogs can reuse one `NamedCatalogManager`-based implementation instead of sample-host-specific manager classes +- centralizes reusable MCP runtime registration in `AddCoreAIMcpServices()`, moves the shared MCP metadata, capability-resolution, tool-registry, SSE settings-handler, and invoke-function services into `CrestApps.Core.AI.Mcp`, and splits optional StdIO transport registration so hosts can enable it only where needed +- standardizes A2A and MCP connection authentication on the shared `ClientAuthenticationType` enum, removes the protocol-specific duplicate enums, and adds an `AzureOpenAIClientMarker` so Azure OpenAI can participate in the same provider-marker conventions as the other AI clients without changing current runtime behavior +- 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 +- keeps the MVC and Blazor sample hosts writing runtime uploads and other mutable files into each project's own `App_Data` folder while switching their `.NET 10` watch exclusions to the documented `**/App_Data/**` glob so Visual Studio Aspire runs do not restart when chat document uploads create files under `App_Data/Documents` +- 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 +- adds declarative `data-*` auto-initialization for the shared AI Chat, AI Chat widget, Chat Interaction, Chat Interaction settings, and document drop-zone scripts so MVC and Blazor hosts can render configuration directly in markup without separate bootstrap calls, now uses universal `coreai`-prefixed chat/widget data attributes and script globals as a breaking rename, and shows a brief green success check on assistant-message copy buttons for clearer clipboard confirmation +- 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 +- moves the duplicated MVC and Blazor citation-reference collector into shared `CrestApps.Core.AI.Chat` services as `CitationReferenceCollector`, registers it from `AddCoreAIChatInteractions()`, and lets hosts reuse the same citation-merging logic without copying sample code +- detects when uploaded chat-interaction or chat-session documents are being used for whole-document tasks such as summarization, review, rewrite, translation, or complete extraction work, and injects the full document text instead of relying only on chunk-level RAG +- 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 +- makes the shared `@crestapps/ai-chat-ui` message-action icons compatible with both Font Awesome CSS/webfont hosts and SVG+JS hosts that load `fontawesome-free/js/all.js`, so dynamically rendered playback and action buttons now appear correctly in Orchard-style integrations +- makes the shared `@crestapps/ai-chat-ui` chat styles resolve colors through Bootstrap CSS variables with fallbacks, so admin and frontend chat widgets inherit Bootstrap 5 light/dark theme values when available +- 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 +- registers `IAIProfileStore` in the shared AI services layer with a null fallback, and replaces it with provider-backed EntityCore or YesSql stores when AI services data stores are enabled so downstream services can always resolve the profile store +- keeps the MVC and Blazor sample-host AI profile, template, and chat-edit screens usable when Claude is not configured by treating failed Claude options validation as "provider unavailable" instead of crashing the page, and removes the legacy memory-settings compatibility shim so profile/template memory state now flows only through `MemoryMetadata` +- keeps the MVC and Blazor sample-host index profile editors aligned with deployment-name-based indexing by posting embedding deployment names instead of catalog IDs and by accepting either selector during embedding profile validation +- fixes sample-host content-root resolution when MVC or Blazor are launched through the Aspire AppHost so `App_Data\appsettings.json` and related local sample assets still load from the web-project directory instead of an Aspire output folder fallback +- updates the shared A2A and MCP sample clients so one client app can target either the MVC or Blazor sample host through a built-in server selector, and wires the Aspire AppHost to advertise both endpoints to those samples +- moves AI chat extracted-data snapshot persistence into shared framework/store infrastructure by introducing `IAIChatSessionExtractedDataStore`, registering a default recorder automatically when YesSql or EntityCore chat session stores are enabled, and rewiring the sample extracted-data reports to consume the shared store instead of host-specific recorder services +- moves AI chat usage analytics and session analytics into shared framework abstractions/services, registers the default runtime analytics services from the framework, and lets YesSql or EntityCore provide the persisted `IAICompletionUsageStore` and `IAIChatSessionEventStore` implementations so the MVC and Blazor reports no longer depend on sample-only analytics services +- moves AI chat inactivity closing into a shared `AIChatSessionCloseBackgroundService` registered from `AddCoreAIChatSessionProcessing()`, so all hosts using the standard chat-session pipeline automatically evaluate inactive sessions and retry post-close work at startup and every 5 minutes instead of depending on sample-host-only background workers +- persists per-attempt post-session task failure history in `PostSessionResults`, records invalid structured task payloads explicitly, honors task-scoped post-session tool names during tool resolution, and makes `ProcessedAtUtc` nullable so pending tasks no longer serialize a default `0001-01-01` timestamp +- raises the shared AI chat post-close retry limit to 5 attempts, recalculates completion from the actual task results so stale terminal flags from older retry policies can recover, and splits the default inactivity-close worker into reusable `AIChatSessionCloseCycleService` and `AIChatSessionCloseRunner` services so non-`BackgroundService` hosts can reuse the same lifecycle logic through `RunOnceAsync`, `StartAsync`, and `StopAsync` +- makes the shared AI chat post-close retry cap configurable through `AIChatSessionProcessingOptions.MaxPostCloseAttempts` and the MVC admin site settings UI, and updates the shared processor to honor the live `IOptionsMonitor<>` value instead of a hard-coded constant +- treats valid post-session JSON with an empty `tasks` array as an explicit structured-result failure, persists that clearer error in `PostSessionResults`, and strengthens the shared post-session prompts so every configured task must still return a result even when no tool call is needed +- stops serializing redundant top-level post-session error fields on `PostSessionResult`, keeps attempt-specific failures in `AttemptHistory`, retries tool-enabled runs through structured recovery when the model returns invalid task entries such as blank names or blank values, and falls back to a no-tools structured retry when the tool path never actually invoked a tool +- refreshes site-settings-backed options through the standard `IOptionsMonitor<>` pipeline by documenting the minimal `IOptionsChangeTokenSource<>` pattern for custom hosts, and moves uploaded AI document vector indexing into a shared `DefaultAIDocumentIndexingService` so MVC and Blazor no longer carry duplicate sample-only indexer implementations +- writes and reads `ExtensibleEntity.Properties` only through the nested `Properties` JSON object instead of flattening typed metadata onto the document root +- introduces `AIDeploymentPurpose` as the primary deployment terminology, keeps the legacy type surface for backward compatibility, adds `Vision` plus `DefaultVisionDeploymentName`, updates the MVC and Blazor deployment/settings UX to say purpose, and allows vision-capable chat interactions and chat sessions to upload supported image files as multimodal inputs +- 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 +- expands Elasticsearch AI data source authentication beyond Basic by adding Elastic Cloud ID support plus `ApiKey`, `Base64ApiKey`, and `KeyIdAndKey` modes with protected per-source secrets in the MVC and Blazor editors and the shared Elasticsearch client factory +- 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 +- aligns named catalog manager creation overloads so `INamedCatalogManager` also exposes unnamed `NewAsync(...)` creation, while source-aware managers keep source-required creation paths and no longer advertise name-only manager registrations for source-bound AI templates and deployments +- 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 +- adds an explicit Elasticsearch data-source environment selector (`SelfManaged` vs `CloudHosted`) so the MVC and Blazor editors show either `Url` or `CloudId` as appropriate and validation now requires the matching field for the chosen environment +- makes `AIDataSource` source-aware through the shared `Source` property, updates the AI data-source stores to expose `ISourceCatalog`, and removes the public `SourceType` model property in favor of `Source` while still reading legacy persisted `SourceType` payloads +- replaces per-turn raw image byte injection with an analyze-once-at-upload strategy: `IImageAnalysisService` calls a vision model to extract caption, OCR text, and detected entities when images are uploaded, stores the results as `AIDocumentChunk` records searchable via `read_document` and `search_documents`, adds `inspect_image` as an on-demand tool for pixel-level inspection when the text analysis is insufficient, removes `BuildVisionUserContentsAsync` from `DocumentOrchestrationHandler` so image bytes are never attached to every user message, and updates the document-availability prompt to guide the model toward text-based tools first +- adds a defense-in-depth prompt security layer for AI Profile chat experiences with normalized regex-rule evaluation, weighted risk scoring, profile-level overrides, output filtering, audit logging, and documentation for remaining regex-based limitations +- adds AI tool dependency registration through the fluent `AIToolBuilder`, automatically expands selected tool sets to include registered dependencies during profile/system tool resolution, ignores missing dependencies safely, and adds focused unit coverage for recursive, shared, and circular dependency graphs +- replaces the always-on `read_tabular_data` system tool with an always-available, system **Tabular Data Agent** that loads uploaded non-embeddable files (such as CSV and Excel) lazily into an in-memory SQLite database and exposes `list_tabular_data`, `query_tabular_data`, `execute_tabular_command`, and `export_tabular_data` SQL tools, so the model analyzes, manipulates, and creates downloadable CSV versions of large tabular files through scoped SQL while only minimal results enter the prompt and the original uploaded file is always preserved; the agent's system prompt is sourced from the embedded `tabular-data-agent` AI template and its SQL tools are hidden from the user-facing tool picker +- tightens the MVC, Blazor, and shared chat-settings selection flows so only selectable tools and user-selectable agents can be chosen or persisted from the UI, keeping hidden/system tools and framework-managed system agents such as the Tabular Data Agent out of manual pickers while still exposing those system agents through the A2A host +- caches in-memory tabular databases per active chat scope instead of rebuilding them for every prompt: workspaces are keyed by chat interaction/session/profile document scope, reused while the user remains active, expired after a configurable sliding idle timeout (five minutes by default), cleaned by a hosted background service, and invalidated immediately when tabular documents or related chat interactions/sessions are removed; parsed tabular document artifacts are persisted through `ITabularDocumentArtifactStore` so another app instance can hydrate from shared document storage, and `ITabularWorkspaceInvalidationPublisher` provides the distributed backplane extension point for cross-instance cache clears; tabular files are identified through `ExtractorExtension.IsTabular` and `ChatDocumentsOptions.TabularFileExtensions` instead of a hardcoded extension list +- improves tabular upload handling by storing raw tabular content chunks without embeddings, preserving sparse XLSX cell positions, using compact survey header codes such as `Q3_C28` as SQL column names while retaining the full source header, exposing typed `object[]` query rows, and adding document-availability guidance that routes row counts, summaries, and calculations to the Tabular Data Agent +- ensures generated tabular-export files are always downloadable: `AICompletionReference.IsGenerated` flags tool-produced deliverables, the export tool sets it when it creates the CSV `AIDocument`, and the chat UI always renders generated references as a download even when the primary model omits the `[doc:n]` marker after delegating to the Tabular Data Agent +- adds tool-capable agents via `AgentMetadata.AllowToolInvocation`, letting designated agents run their own tools through the orchestrator under an `AIInvocationContext.AgentInvocationDepth` recursion-depth guard that prevents agent-to-agent recursion, plus code-defined profiles through the new `IAIProfileProvider` (with `AgentMetadata.IsSystem` for system agents) that are merged with stored profiles and can provide always-available agents exposed via A2A and hidden from the user-facing agent selection list +- adds a general file-generation chat capability: the always-available `generate_file` content-generation tool turns model-generated content into a downloadable `AIDocument` (PDF, Word, Markdown, HTML, text, CSV, or spreadsheet) and surfaces it through the same `[doc:N]` download path used for charts and tabular exports, backed by a pluggable `IGeneratedFileWriter`/`IGeneratedFileWriterResolver` abstraction (`AddGeneratedFileWriter(extensions)`) with core writers for `.csv` and plain-text formats plus OpenXml (`.xlsx`, `.docx`) and PDF (`.pdf`) writers in their respective modules +- preserves the original tabular upload format on export so `export_tabular_data` downloads an `.xlsx` workbook when the source was `.xlsx` and a `.csv` when the source was `.csv`, adds an optional `format` argument to request a different output format, and falls back to `.csv` when no writer is registered for the original extension +- exports tabular files from the current in-memory data instead of the original upload: `export_tabular_data` now treats `sql` as optional and, when omitted, dumps the entire current table (including every `execute_tabular_command` mutation) using the original source column headers, and each successful `execute_tabular_command` snapshots the mutated table back through `ITabularDocumentArtifactStore` so the edits survive workspace eviction, a process restart, or another app instance while the originally uploaded file stays untouched +- persists generated tabular-export and `generate_file` downloads to the shared `IDocumentFileStore` under a collision-free random storage name so they stay re-downloadable across in-memory workspace eviction and process restarts, keeps the `[doc:n]` reference saved with the assistant message so reopening a session re-renders the same download link, and removes a conversation's documents, stored files, tabular artifacts, and chunks automatically when its chat session or chat interaction is deleted through the new `IConversationDocumentCleanupService` (wired into the chat session managers and a `ChatInteractionDocumentCleanupHandler`) +- makes the PDF `generate_file` writer work out of the box on non-Windows hosts by falling back to a sans-serif font discovered in the standard system font directories when no custom `IFontResolver` is registered, and sanitizes user-provided conversation identifiers before they are written to cleanup log entries +- speeds up in-memory tabular edits by persisting `execute_tabular_command` snapshots through a coalesced background operation on `TabularWorkspace` instead of blocking each command on a full-table snapshot, serialization, and write, so a burst of edits no longer slows the model's tool-call loop while the mutated state is still saved through `ITabularDocumentArtifactStore` +- removes AI-generated downloadable files when a chat interaction's history is cleared: clearing history now collects the generated `[doc:n]` references stored on the cleared messages and deletes those files (and their stored content, chunks, and tabular artifacts) through the new `IConversationDocumentCleanupService.CleanupGeneratedDocumentsAsync` and a decoupled `IChatInteractionHistoryHandler`, so generated exports no longer linger in the document file store after the messages that produced them are gone while uploaded source documents are left intact +- steers the model to deliver updated tabular files correctly: the Tabular Data Agent prompt now requires a single set-based `UPDATE` for bulk cell changes (instead of slow per-cell commands) and mandates `export_tabular_data` for downloads, and the `generate_file` tool description warns that its `content` becomes the entire file verbatim and must not be used to re-save uploaded spreadsheet/tabular data +- makes `export_tabular_data` produce a single downloadable file by removing the duplicate parsed-artifact write, and excludes generated documents (flagged with `DefaultGeneratedDocumentService.GeneratedPropertyName`) from the in-memory tabular workspace so an exported file is never re-ingested as a duplicate source table or blocks a later full-table export +- strengthens the `export_tabular_data` tool description and result message so the model always returns the `[doc:N]` download marker verbatim instead of writing the file name in brackets, matching the `generate_file` download experience +- lets `execute_tabular_command` apply multiple SQL statements in a single call: the tool now accepts one or more semicolon-separated data/schema statements, validates each one independently against the tabular SQL guard (respecting string literals, quoted identifiers, and comments so semicolons inside them never split a statement), and runs the whole batch in one transaction that rolls back together on failure, so the model makes every requested change in one tool call instead of many slow per-cell round-trips that previously hit the tool iteration limit on large files +- stores AI-generated downloads under a dedicated `generated` subfolder inside each chat session/chat interaction document path, lets `FileSystemFileStore` open download streams with delete sharing so `ClearHistory` can remove generated files even after a user downloaded them, caches identical `export_tabular_data` calls within the same prompt, and rejects status-only `generate_file` calls after an existing export so the model no longer creates an extra bogus file that only says the download is ready +- adds a dedicated hidden `fill_empty_tabular_cells` tool for the Tabular Data Agent so “replace every empty cell with X” requests run as one set-based update instead of the model composing hundreds of per-column statements, and broadens `generate_file` tabular misuse detection so conversational/question text like “Would you like me to generate…” cannot be written into `.xlsx` downloads +- keeps hidden tools private to their owning profiles and agents across the shared MCP server handlers, so agent-only helpers such as the Tabular Data Agent SQL tools are no longer listed or callable as direct MCP tools +- moves tabular workspace storage from in-memory SQLite to a file-based SQLite database stored alongside uploaded documents in a `data` folder, so workspace state persists across process restarts without artifact-store round-trips and reduces peak memory usage under high traffic; removes the singleton workspace cache, invalidation publisher interfaces, and cleanup background service in favor of creating a disposable workspace per tool call that opens and closes its own connection, with the document cleanup service and document event handler deleting the database file directly on session or document removal +- adds parameterized AI tool instances so developers can author a tool blueprint once in code via the `IAIToolInstanceSource` interface (registered under a unique name with `AddAIToolInstanceSource()`) and let users create multiple configured `AIToolInstance` entries of it, each supplying its own settings (endpoint, authentication, headers, …), a unique name, and a natural-language description up front instead of relying on the AI model to provide them; the model still decides when to invoke each instance, `ToolInstanceRegistryProvider` (a pluggable `IToolRegistryProvider`) surfaces every referenced instance as a distinctly named `AITool` so multiple instances built from the same source appear as separate functions to every client (OpenAI, Azure OpenAI, …), projects can register their own `IToolRegistryProvider` to add logic such as permission checks, ships a built-in `http-api-request` source that calls arbitrary HTTP APIs with data-protected credentials, persists instances through the `AIToolInstance` catalog on both YesSql and EntityCore, and includes full management UI plus AI profile attachment in the MVC and Blazor sample hosts +- refines the parameterized AI tool instances so profiles and chat interactions reference instances by their stable unique **name** (via the renamed, feature-agnostic `AIToolInstanceMetadata` and `AICompletionContext.ToolInstanceNames`) resolved through `INamedCatalog.FindByNameAsync` instead of by generated id; the name is immutable after creation and `AIToolInstance.GetFunctionName()` appends a short deterministic hash when sanitizing would be lossy so distinct names can never collapse to the same function; decouples the default registry so the opt-out lives on `AddToolInstances(..., useDefaultRegistry: false)` and exposes `AddDefaultAIToolInstanceRegistryProvider()`, letting hosts opt out and register their own `IToolRegistryProvider` without ever calling `RemoveAll()`; makes `ToolInstanceRegistryProvider` public with a `ShouldIncludeInstanceAsync` hook for simple permission-gated subclasses; and adds OAuth 2.0 support to the built-in `http-api-request` source that acquires, data-protects, caches, and refreshes access/refresh tokens on the instance itself so it authenticates once and reuses the token across requests and restarts +- promotes the parameterized AI tool instances into a first-class opt-in feature registered on the AI suite builder with `AddToolInstances(toolInstances => toolInstances.AddSource(...))` (with an `AddHttpApiRequestSource()` convenience for the built-in source), instead of being wired into the core AI services automatically; persistence is registered on the tool-instances builder via `AddYesSqlStores()`/`AddEntityCoreStores()` rather than on the AI suite; drops the redundant per-instance `DisplayText` in favor of the unique `Name`, localizes the source `DisplayName`/`Description`/`Category`, simplifies `IAIToolInstanceSource.CreateTool(AIToolInstance instance)` to take the instance directly, generalizes the completion-context handler to honor tool instances on any resource so both AI profiles and chat interactions can select them, renames the built-in HTTP source's basic/OAuth credentials to `Username`/`Password` and adds the OAuth 2.0 resource-owner password grant, hardens model-provided paths so they cannot redirect a request off the configured host, and updates the MVC and Blazor sample hosts to let users attach instances to both AI profiles and chat interactions +- namespaces every tool-instance function name with the `AIToolInstanceExtensions.FunctionNamePrefix` (`tool_instance_`) prefix so a user-chosen instance name can never collide with a tool registered in code via `AddCoreAITool` (which the model sees under its bare registered name); both kinds of tool now coexist safely in the single function namespace exposed to OpenAI, Azure OpenAI, and every other client +- adds a source dropdown to the AI tool instance create form in the MVC and Blazor sample hosts that reveals only the selected source's fields (the source is fixed and shown read-only on edit), and relocates the "AI Tool Instances" admin menu item next to "AI Profiles" in both samples +- requires the key, title, and content field mappings when creating or editing an AI data source in the MVC and Blazor sample hosts, and makes every built-in source reader fall back to the document key instead of the serialized source document when no title is mapped, so chat citations never render a full JSON document as a reference title +- makes the Copilot CLI acquisition work behind corporate proxies and artifact mirrors, and downloads it only once per machine: `CrestApps.Core.AI.Copilot` now resolves the effective npm registry from `NPM_CONFIG_REGISTRY` or `npm config get registry` before the `GitHub.Copilot.SDK` targets download the CLI tarball (the SDK hardcodes `https://registry.npmjs.org`, and MSBuild's `DownloadFile` task cannot read npm configuration), and redirects the SDK's per-project, per-configuration cache to a shared cache under the NuGet global packages folder so a multi-project solution, a fresh worktree, or a CI agent no longer re-downloads the same large tarball for every project; both behaviors are opt-out through `CopilotResolveNpmRegistry` and `CopilotUseSharedCliCache`, the cache location is configurable through `CopilotCliCacheDir` (point it at a pre-seeded directory to build offline), and an explicitly set `CopilotNpmRegistryUrl`, `CopilotCliBinaryPath`, or `CopilotSkipCliDownload` always takes precedence diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/agents.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/agents.md new file mode 100644 index 00000000..439b1048 --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/agents.md @@ -0,0 +1,317 @@ +--- +sidebar_label: AI Agents +sidebar_position: 15 +title: AI Agents +description: Delegate tasks to specialized sub-agents that the primary AI model can invoke as tools during orchestration. +--- + +# AI Agents + +> Purpose-built AI profiles that the primary model can invoke as tools — each with its own system prompt, deployment, and capabilities. + +## Quick Start + +Agents are available automatically when orchestration is enabled: + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOrchestration(); // registers AgentToolRegistryProvider +``` + +Create an agent profile, then link it to a chat profile: + +```csharp +// 1. Create an agent profile +var agent = new AIProfile +{ + Type = AIProfileType.Agent, + Name = "code-reviewer", + DisplayText = "Code Reviewer", + Description = "Reviews code for bugs, security issues, and best practices.", + ChatDeploymentName = "gpt-4o-deployment", +}; +agent.Put(new AgentMetadata { Availability = AgentAvailability.OnDemand }); + +await profileManager.CreateAsync(agent); + +// 2. Link it to a chat profile +chatProfile.Put(new AgentInvocationMetadata { Names = ["code-reviewer"] }); +await profileManager.UpdateAsync(chatProfile); +``` + +The primary model can now call the `code-reviewer` agent as a tool during orchestration. + +## Problem & Solution + +A single AI profile often needs to handle diverse tasks — code review, translation, data analysis, summarization. Cramming all instructions into one system prompt leads to: + +- **Conflicting instructions** — a translator prompt fights with a code review prompt +- **Model confusion** — the model struggles with broad, unfocused responsibilities +- **No isolation** — all tasks share the same deployment, token limits, and context + +Agents solve this by allowing the primary model to **delegate** to specialized sub-agents: + +| Concern | Without Agents | With Agents | +|---------|---------------|-------------| +| System prompt | One monolithic prompt for all tasks | Each agent has a focused prompt | +| Model selection | Single deployment for everything | Each agent can use a different deployment | +| Token budget | Shared across all capabilities | Each agent runs its own completion | +| Scope | Everything in one context | Isolated per-task context | + +## How Agents Work + +``` +User message + │ + ▼ +┌──────────────────┐ +│ Primary Model │ ← Chat profile with tools + agents +│ (Orchestrator) │ +└────────┬─────────┘ + │ calls agent tool + ▼ +┌──────────────────┐ +│ AgentProxyTool │ ← Receives { "prompt": "Review this code..." } +└────────┬─────────┘ + │ builds agent context + ▼ +┌──────────────────┐ +│ Agent Model │ ← Agent profile (own system prompt, deployment) +│ (tools disabled)│ +└────────┬─────────┘ + │ returns response + ▼ +┌──────────────────┐ +│ Primary Model │ ← Incorporates agent's response and continues +│ (continues) │ +└──────────────────┘ +``` + +The primary model sees each agent as a regular tool with a `prompt` parameter. It decides when and how to invoke agents based on the user's request and the agent descriptions injected into the system message. + +## Agent Availability + +The `AgentAvailability` enum controls when an agent is included in orchestration: + +| Mode | Behavior | Use Case | +|------|----------|----------| +| `OnDemand` | Included only when explicitly listed in `AgentInvocationMetadata` on the chat profile | Specialized agents (code review, translation) assigned per profile | +| `AlwaysAvailable` | Automatically included in every orchestration request | Core agents needed globally (safety checker, logging agent) | + +```csharp +// On-demand: only available when a chat profile explicitly requests it +agent.Put(new AgentMetadata { Availability = AgentAvailability.OnDemand }); + +// Always available: included in every request automatically +agent.Put(new AgentMetadata { Availability = AgentAvailability.AlwaysAvailable }); +``` + +**Token considerations:** `AlwaysAvailable` agents increase token usage on every request because their descriptions are always present in the system message and their tool definitions are always registered. Use `OnDemand` to minimize cost. + +`AlwaysAvailable` agents are **hidden from the user-facing agent selection list** (they are included automatically, so there is nothing to select), yet they remain discoverable and invocable — including through the A2A host — like any other agent. + +### Tool-capable agents (controlled recursion) + +By default a sub-agent runs as a single isolated completion with **tools disabled** (see [Recursion Prevention](#recursion-prevention)). Set `AllowToolInvocation` to let an agent run its own tools: + +```csharp +agent.Put(new AgentMetadata +{ + Availability = AgentAvailability.AlwaysAvailable, + AllowToolInvocation = true, +}); +``` + +When a tool-capable agent is invoked, `AgentProxyTool` runs it through the orchestrator so its configured tools are available. A recursion-depth guard (`AIInvocationContext.AgentInvocationDepth`) suppresses nested agents, so an agent can never invoke another agent — bounding recursion to a single level. This is how the system [Tabular Data Agent](./ai-documents.md#tabular-files) runs its SQL tools. + +### Code-defined profiles and system agents + +Implement `IAIProfileProvider` to contribute code-defined profiles that are not persisted in the profile store. Provided profiles are merged into `IAIProfileManager.GetAsync(type)` for the requested profile type, and stored profiles with the same name take precedence. For system agents, return profiles when `type == AIProfileType.Agent`; they automatically flow to every consumer — the tool registry, `AgentProxyTool`, and the A2A host — while remaining read-only and hidden from the user-facing selection list. Mark them `AlwaysAvailable` (and optionally `AllowToolInvocation`) and set `IsSystem = true`: + +```csharp +internal sealed class MyAgentProvider : IAIProfileProvider +{ + public ValueTask> GetProfilesAsync( + AIProfileType type, + CancellationToken cancellationToken = default) + { + if (type != AIProfileType.Agent) + { + return ValueTask.FromResult>([]); + } + + var agent = new AIProfile { Type = AIProfileType.Agent, Name = "my-agent", Description = "…" }; + agent.Put(new AgentMetadata { Availability = AgentAvailability.AlwaysAvailable, IsSystem = true }); + + return ValueTask.FromResult>([agent]); + } +} +``` + +Register it with `services.TryAddEnumerable(ServiceDescriptor.Scoped())`. + +### Pattern: hidden system agents that still participate in A2A + +The built-in **Tabular Data Agent** is the reference pattern for a framework-managed system agent: + +- it is defined in code through `IAIProfileProvider` +- it is marked `AlwaysAvailable` +- it sets `IsSystem = true` +- it enables `AllowToolInvocation` so it can use its own hidden SQL tools +- it stays out of the AI Profile and Chat Interaction pickers because system agents are not user-selectable +- it is still returned by `IAIProfileManager.GetAsync(AIProfileType.Agent)`, so the A2A host exposes it like any other agent + +Use the same pattern for additional code-defined system agents when you want a capability to be automatically present for orchestration and remotely invocable over A2A without making it a manual UI choice. + +## Creating Agent Profiles + +Agent profiles are standard `AIProfile` objects with `Type = AIProfileType.Agent`. They require a `Name` and `Description` at minimum — the description is what the primary model sees when deciding whether to invoke the agent. + +```csharp +var translatorAgent = new AIProfile +{ + Type = AIProfileType.Agent, + Name = "translator", + DisplayText = "Translator", + Description = "Translates text between languages. Provide the target language and text to translate.", + ChatDeploymentName = "gpt-4o-mini-deployment", +}; +translatorAgent.Put(new AgentMetadata +{ + Availability = AgentAvailability.OnDemand, +}); + +await profileManager.CreateAsync(translatorAgent); +``` + +### Required Fields + +| Field | Purpose | +|-------|---------| +| `Type` | Must be `AIProfileType.Agent` | +| `Name` | Unique identifier used as the tool name (becomes `agent:{name}` in the registry) | +| `Description` | Shown to the primary model — drives its decision to invoke this agent | +| `ChatDeploymentName` | The AI deployment used for the agent's completion | + +### Optional Configuration + +- **System message** — Configure via templates or the profile's system message property +- **AgentMetadata** — Set availability mode (`OnDemand` or `AlwaysAvailable`) + +Agents with an empty `Name` or `Description` are silently skipped during registration. + +## Linking Agents to Chat Profiles + +On-demand agents must be explicitly linked to a chat profile via `AgentInvocationMetadata`: + +```csharp +// Make specific agents available to this chat profile +chatProfile.Put(new AgentInvocationMetadata +{ + Names = ["code-reviewer", "translator", "summarizer"], +}); + +await profileManager.UpdateAsync(chatProfile); +``` + +The `Names` array maps to agent profile names. At orchestration time, the `AgentToolRegistryProvider` reads these names from `AICompletionContext.AgentNames` and includes only matching agents. + +`AlwaysAvailable` agents do **not** need to be listed here — they are included automatically regardless of `AgentInvocationMetadata`. + +## Agent Execution Flow + +When the primary model invokes an agent tool, the following sequence occurs inside `AgentProxyTool`: + +1. **Parse input** — Extract the `prompt` string from the tool call arguments +2. **Resolve agent profile** — Look up the agent by name via `IAIProfileManager.GetAsync(AIProfileType.Agent)` +3. **Build agent context** — Call `IAICompletionContextBuilder.BuildAsync(agentProfile)` to construct the agent's own completion context (system message, settings, etc.) +4. **Disable tools** — Set `context.DisableTools = true` on the agent's context (see [Recursion Prevention](#recursion-prevention)) +5. **Resolve deployment** — Find the chat deployment via `IAIDeploymentManager.ResolveOrDefaultAsync()` +6. **Send prompt** — Create a single `ChatMessage` with `ChatRole.User` containing the prompt +7. **Execute completion** — Call `IAICompletionService.CompleteAsync()` with the agent's deployment, messages, and context +8. **Return response** — Extract the assistant's response text and return it to the primary model + +```csharp +// Simplified flow inside AgentProxyTool.InvokeCoreAsync: +var context = await contextBuilder.BuildAsync(agentProfile); +context.DisableTools = true; + +var deployment = await deploymentManager.ResolveOrDefaultAsync( + AIDeploymentType.Chat, deploymentName: context.ChatDeploymentName); + +var messages = new List +{ + new(ChatRole.User, task), +}; + +var response = await completionService.CompleteAsync( + deployment, messages, context, cancellationToken); +``` + +If the agent profile is not found or an error occurs, `AgentProxyTool` returns a descriptive error message to the primary model rather than throwing — allowing the orchestration to continue gracefully. + +## Recursion Prevention + +Without safeguards, an agent could invoke other agents (or itself), creating an infinite loop. The framework prevents this by **disabling tools on the agent's completion context**: + +```csharp +context.DisableTools = true; +``` + +> **Exception:** agents with `AgentMetadata.AllowToolInvocation = true` run *with* their tools enabled (through the orchestrator). For those, recursion is bounded instead by the `AIInvocationContext.AgentInvocationDepth` guard, which suppresses agents-as-tools once execution is already inside a sub-agent — so an agent can use its own tools but can never call another agent. + +This means: + +- Agents **cannot** call tools, including other agents +- Agents run a single, isolated completion with their own system prompt and the provided prompt +- The agent's response is pure text — no tool calls, no further delegation + +This is a deliberate design choice that keeps agent execution predictable and bounded. If you need multi-level delegation, compose it at the chat profile level by having multiple agents available to the primary model, which can invoke them sequentially. + +## System Message Enrichment + +The `AgentOrchestrationContextBuilderHandler` automatically enriches the primary model's system message with descriptions of all available agents. This gives the model awareness of which agents exist and what they can do, enabling informed routing decisions. + +The handler: + +1. Reads all agent profiles via `IAIProfileManager` +2. Filters to agents matching the availability criteria +3. Renders agent descriptions using the `AITemplateIds.AgentAvailability` template +4. Appends the rendered text to the orchestration context's `SystemMessageBuilder` + +This follows the industry-standard pattern used by orchestration frameworks where agent descriptions are included in the system prompt so the model can decide which capabilities to invoke. + +## Using or replacing `IAIProfileManager` + +`AddCoreAIServices()` registers the shared `DefaultAIProfileManager` for hosts that also register an `AIProfile` catalog through YesSql, EntityCore, or another custom catalog implementation. + +Hosts can still replace `IAIProfileManager`, but the default manager already covers the core agent/runtime behavior. The agent subsystem relies on these operations: + +```csharp +public interface IAIProfileManager +{ + ValueTask> GetAsync(AIProfileType type, CancellationToken cancellationToken = default); + ValueTask CreateAsync(AIProfile profile, CancellationToken cancellationToken = default); + ValueTask UpdateAsync(AIProfile profile, JsonNode data = null, CancellationToken cancellationToken = default); +} +``` + +The `GetAsync(AIProfileType.Agent)` call is the primary query used by: + +- **`AgentToolRegistryProvider`** — to discover agents and build tool entries +- **`AgentProxyTool`** — to resolve the target agent at invocation time +- **`AgentOrchestrationContextBuilderHandler`** — to enrich the system message with agent descriptions + +Any replacement implementation must return agent profiles with their `Properties` intact (including `AgentMetadata`) for availability filtering to work correctly. + +## Services Registered + +`AddCoreAIOrchestration()` registers the following agent-related services: + +| Service | Implementation | Purpose | +|---------|---------------|---------| +| `IToolRegistryProvider` | `AgentToolRegistryProvider` | Exposes agents as tool entries | +| `IOrchestrationContextBuilderHandler` | `AgentOrchestrationContextBuilderHandler` | Enriches system message with agent descriptions | + +Both are registered as **scoped** services via `TryAddEnumerable`, ensuring they participate alongside other tool providers and context handlers. diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-core.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-core.md new file mode 100644 index 00000000..1fa31482 --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-core.md @@ -0,0 +1,477 @@ +--- +sidebar_label: AI Core +sidebar_position: 3 +title: AI Core +description: Core AI services including completion clients, client factory, context building, and the deployment resolution chain. +--- + +# AI Core + +> Provider-agnostic AI completion services, client factory, and context-building pipeline. + +## Quick Start + +```csharp +builder.Services + .AddCoreAIServices() + .AddCoreAIOpenAI(); // or any other provider +``` + +This gives you access to `IAIClientFactory`, `IAICompletionService`, and `IAICompletionContextBuilder`. + +## Problem & Solution + +AI applications need to work with multiple LLM providers (OpenAI, Azure, Ollama, etc.) without coupling business logic to a specific SDK. The AI Core layer provides a **provider-agnostic abstraction** where you program against interfaces and swap providers through configuration. + +## Core Concepts + +### AI Profile + +An **AI Profile** is the reusable runtime definition that ties deployments, prompts, orchestration, tools, retrieval, memory, and session behavior together. It is the main contract used by higher-level features such as AI Chat and agents. + +Use Chat Interactions when you want fast ad hoc testing. Use an AI Profile when you want a named, reusable experience that multiple sessions, users, or orchestrators can share. + +See [AI Profiles](./ai-profiles.md) for the full conceptual model and guidance. + +### Deployment + +A **deployment** maps a logical name to a specific model on a specific provider connection. For example, deployment `"gpt-4o"` might map to the `gpt-4o` model on your OpenAI connection. Deployments now advertise one or more **purposes** through `AIDeploymentPurpose` (`Chat`, `Utility`, `Embedding`, `Image`, `SpeechToText`, `TextToSpeech`, `Vision`) so the runtime can resolve the best deployment for each task while preserving the older legacy type API for backward compatibility. + +The orchestrator resolves deployments at runtime using a fallback chain: + +1. Profile-level deployment override +2. Connection-level default deployment +3. Global default deployment + +### AI Connection + +An **AI connection** stores credentials and endpoint information for a specific AI client (API key, endpoint URL, and `ClientName`). + +## Services Registered by `AddCoreAIServices()` + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `IAIClientFactory` | `DefaultAIClientFactory` | Scoped | Creates typed AI clients | +| `IAICompletionService` | `DefaultAICompletionService` | Scoped | Deployment-aware completion | +| `IAICompletionContextBuilder` | `DefaultAICompletionContextBuilder` | Scoped | Builds context with handler pipeline | +| `IAIDeploymentStore` | `DefaultAIDeploymentStore` | Scoped | Multi-source deployment store (merges DB + config entries) | +| `IAIProviderConnectionStore` | `DefaultAIProviderConnectionStore` | Scoped | Multi-source connection store (merges DB + config entries) | +| `INamedSourceCatalogSource` | `ConfigurationAIDeploymentSource` | Scoped | Reads deployments from `appsettings.json` (Order 100) | +| `INamedSourceCatalogSource` | `ConfigurationAIProviderConnectionSource` | Scoped | Reads connections from `appsettings.json` (Order 100) | +| `ITemplateService` | *(from AddCoreAITemplating)* | Scoped | Template rendering | + +It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically. `IAIDeploymentStore` and `IAIProviderConnectionStore` are the merged runtime views across all registered binding sources. The generic catalog interfaces for those two models are intentionally left unbound by `AddCoreAIServices()` alone so the persistence packages can map `INamedSourceCatalog`, `INamedCatalog`, `ISourceCatalog`, and `ICatalog` to the concrete database-backed catalogs. + +Optional format-specific packages stay opt-in. For example, Markdown-aware normalization lives in `CrestApps.Core.AI.Markdown`, so hosts that want Markdig-backed RAG normalization should register `AddCoreAIMarkdown()` explicitly instead of expecting `AddCoreAIServices()` to pull it in automatically. + +The AI services layer also registers the shared prompt-security services used by AI Profile chat experiences, including normalization, weighted regex-rule evaluation, output filtering, and audit logging. See [Prompt Security](./prompt-security.md) for the security model and configuration guidance. + +## Key Interfaces + +### `IAIClientFactory` + +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 +{ + ValueTask CreateChatClientAsync(AIDeployment deployment); + ValueTask CreateChatClientAsync( + AIDeployment deployment, + Action configurePipeline); + + ValueTask>> CreateEmbeddingGeneratorAsync( + AIDeployment deployment); + ValueTask>> CreateEmbeddingGeneratorAsync( + AIDeployment deployment, + Action>> 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` + +Mid-level service that resolves a deployment and sends a completion request. + +```csharp +public interface IAICompletionService +{ + Task CompleteAsync( + AIDeployment deployment, + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + AIDeployment deployment, + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); +} +``` + +**When to use:** When you have a deployment reference and want completion without the full orchestration loop. + +### `IAICompletionContextBuilder` + +Builds an `AICompletionContext` by running a handler pipeline that enriches the context before and after construction. + +```csharp +public interface IAICompletionContextBuilder +{ + ValueTask BuildAsync( + AICompletionContextBuildingContext context, + CancellationToken cancellationToken = default); +} +``` + +The builder invokes all registered `IAICompletionContextBuilderHandler` instances in sequence. See [Context Builders](./context-builders.md) for details. + +### `IAICompletionClient` + +Implement this interface to add a new AI provider. Each provider registers its own completion client. + +```csharp +public interface IAICompletionClient +{ + string ClientName { get; } + + Task CompleteAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); +} +``` + +**When to implement:** When integrating an AI provider not already supported. See [Providers](../providers/index.md). + +## Configuration + +### `AIOptions` + +Central options class for registering completion clients, deployment providers, connection sources, and template sources. By default, connections are loaded from `CrestApps:AI:Connections` and deployments are loaded from `CrestApps:AI:Deployments`. + +```csharp +services.Configure(options => +{ + options.AddCompletionClient("MySource", configure => { /* ... */ }); + options.AddDeploymentProvider("MyProvider", configure => { /* ... */ }); + options.AddConnectionSource("MySource", configure => { /* ... */ }); +}); +``` + +### `DefaultAIDeploymentSettings` + +Global default deployment settings, typically loaded from configuration: + +```json +{ + "CrestApps": { + "AI": { + "DefaultChatDeploymentName": "gpt-4o", + "DefaultUtilityDeploymentName": "gpt-4o-mini", + "DefaultEmbeddingDeploymentName": "text-embedding-3-large", + "DefaultVisionDeploymentName": "gpt-4o", + "DefaultConnectionName": "my-openai" + } + } +} +``` + +Use `DefaultVisionDeploymentName` when chat and document flows need a default deployment that can accept image inputs. + +## Streaming Example + +Use `CompleteStreamingAsync` to stream tokens as they are generated: + +```csharp +public sealed class StreamingService(IAICompletionService completionService) +{ + public async IAsyncEnumerable StreamAsync( + AIDeployment deployment, + string question, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, question), + }; + + await foreach (var update in completionService.CompleteStreamingAsync( + deployment, messages, cancellationToken: cancellationToken)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + yield return update.Text; + } + } + } +} +``` + +### Using Streaming in an API Controller + +```csharp +[ApiController] +[Route("api/[controller]")] +public sealed class ChatApiController : ControllerBase +{ + private readonly IAICompletionService _completionService; + + public ChatApiController(IAICompletionService completionService) + { + _completionService = completionService; + } + + [HttpPost("stream")] + public async Task StreamResponse( + [FromBody] ChatRequest request, + CancellationToken cancellationToken) + { + Response.ContentType = "text/event-stream"; + + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, request.Message), + }; + + await foreach (var update in _completionService.CompleteStreamingAsync( + request.Deployment, messages, cancellationToken: cancellationToken)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + await Response.WriteAsync($"data: {update.Text}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + } + } +} +``` + +## Error Handling + +### Common Exceptions + +| Exception | When | How to Handle | +|-----------|------|--------------| +| `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` 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 + +```csharp +public sealed class ResilientCompletionService +{ + private readonly IAICompletionService _completionService; + private readonly ILogger _logger; + + public ResilientCompletionService( + IAICompletionService completionService, + ILogger logger) + { + _completionService = completionService; + _logger = logger; + } + + public async Task SafeCompleteAsync( + AIDeployment deployment, + IList messages, + CancellationToken cancellationToken = default) + { + try + { + var response = await _completionService.CompleteAsync( + deployment, messages, cancellationToken: cancellationToken); + + return response.Text; + } + catch (OperationCanceledException) + { + throw; // Always re-throw cancellation + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "AI configuration error — check deployment settings."); + throw; // Configuration errors should not be silently swallowed + } + catch (Exception ex) + { + _logger.LogError(ex, "AI completion failed for deployment '{Deployment}'.", + deployment.Name); + return null; // Or return a fallback message + } + } +} +``` + +When you finish a `ChatClientBuilder` pipeline, always call `Build(serviceProvider)` with the active service provider instead of `Build(null)`. Several framework chat middlewares resolve services from DI at execution time, especially tool-related components. + +:::warning +Never swallow `OperationCanceledException` — always re-throw it. Catching and ignoring it breaks the cancellation token contract and can cause resource leaks. +::: + +## Implementing a Custom AI Provider + +To integrate an AI provider that is not already supported (e.g., Anthropic, Mistral, Cohere), implement `IAICompletionClient`: + +```csharp +public interface IAICompletionClient +{ + string ClientName { get; } + + Task CompleteAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); + + IAsyncEnumerable CompleteStreamingAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default); +} +``` + +### Example: Custom Provider Implementation + +```csharp +public sealed class MyProviderCompletionClient : IAICompletionClient +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public MyProviderCompletionClient( + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public string ClientName => "MyProvider"; + + public async Task CompleteAsync( + IEnumerable messages, + AICompletionContext context, + CancellationToken cancellationToken = default) + { + var client = _httpClientFactory.CreateClient("MyProvider"); + + // Convert messages to your provider's API format + var request = new + { + model = context.Deployment.ModelName, + messages = messages.Select(m => new + { + role = m.Role.Value, + content = m.Text, + }), + max_tokens = context.Options?.MaxOutputTokens ?? 1024, + temperature = context.Options?.Temperature ?? 0.7f, + }; + + var response = await client.PostAsJsonAsync("/v1/chat/completions", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(cancellationToken); + + return new ChatResponse(new ChatMessage(ChatRole.Assistant, result.Content)); + } + + public async IAsyncEnumerable CompleteStreamingAsync( + IEnumerable messages, + AICompletionContext context, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Similar to CompleteAsync but reads Server-Sent Events (SSE) + // and yields ChatResponseUpdate for each token + var client = _httpClientFactory.CreateClient("MyProvider"); + + // Build request with stream: true + var request = new + { + model = context.Deployment.ModelName, + messages = messages.Select(m => new { role = m.Role.Value, content = m.Text }), + stream = true, + }; + + using var response = await client.PostAsJsonAsync("/v1/chat/completions", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var reader = new StreamReader(stream); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(cancellationToken); + if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) + { + continue; + } + + var data = line["data: ".Length..]; + if (data == "[DONE]") + { + break; + } + + var chunk = JsonSerializer.Deserialize(data); + if (!string.IsNullOrEmpty(chunk?.Delta?.Content)) + { + yield return new ChatResponseUpdate + { + Text = chunk.Delta.Content, + }; + } + } + } +} +``` + +### Registering the Provider + +```csharp +services.AddScoped(); +``` + +The `IAIClientFactory` uses the `Name` property to route requests to the correct provider. When a deployment's provider connection references `"MyProvider"`, the factory creates a client using your implementation. + +## Example + +```csharp +// Inject the high-level service +public class MyService(IAICompletionService completionService) +{ + public async Task AskAsync(string question, AIDeployment deployment) + { + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, question), + }; + + var response = await completionService.CompleteAsync(deployment, messages); + return response.Text; + } +} +``` diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-documents.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-documents.md new file mode 100644 index 00000000..ca52684b --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-documents.md @@ -0,0 +1,166 @@ +--- +sidebar_label: AI Documents +sidebar_position: 14 +title: AI Documents +description: Add document uploads, search, citations, image understanding, and tabular file workflows to AI conversations. +--- + +# AI Documents + +> Let users upload files and ask questions about them in chat, with citations, downloads, and built-in support for text, images, and tabular data. + +## Quick Start + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddMarkdown() + .AddChatInteractions() + .AddDocumentProcessing(documentProcessing => documentProcessing + .AddEntityCoreStores() + .AddOpenXml() + .AddPdf() + .AddReferenceDownloads() + ) + .AddOpenAI() + ) + .AddEntityCoreSqliteDataStore("Data Source=app.db") +); + +app.AddChatApiEndpoints() + .AddDownloadAIDocumentEndpoint(); +``` + +## What It Gives You + +With AI Documents enabled, your users can: + +- Upload knowledge files for chat, profiles, or templates +- Ask questions about uploaded content and get cited answers +- Search document content semantically instead of by exact keyword only +- Work with spreadsheets and CSV files through a tabular workflow +- Download generated files such as exports and AI-authored documents +- Include supported images in chat flows + +## Supported Experiences + +### Text and knowledge files + +For text-heavy files such as Markdown, text, Word, PDF, HTML, JSON, or XML, CrestApps.Core extracts the useful content and makes it available during conversation. This is the main path for summaries, Q&A, reviews, rewrites, extraction, and similar knowledge tasks. + +### Tabular files + +CSV and Excel uploads are handled as structured data instead of plain text. That means the AI can filter, update, reshape, and export rows without asking the model to copy large tables into the prompt. + +This is the recommended path for tasks such as: + +- filling blank cells +- filtering rows +- adding calculated columns +- exporting an updated spreadsheet for download + +Under the hood, tabular workflows are handled by the built-in **Tabular Data Agent**. It is a code-defined, always-available **system agent** that stays hidden from the AI Profile and Chat Interaction agent pickers, yet still participates in orchestration and is exposed through the A2A host for remote clients. + +### Images + +When your deployment supports vision, users can upload supported image files alongside standard documents. This enables image-aware chat scenarios such as describing screenshots, extracting visible text, or answering questions about diagrams and photos. + +## Download Links and Citations + +Uploaded documents and generated deliverables can both appear as downloadable references in chat. + +Use these registrations together when you want document references to render as clickable downloads: + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddDocumentProcessing(documentProcessing => documentProcessing + .AddEntityCoreStores() + .AddOpenXml() + .AddPdf() + .AddReferenceDownloads() + ) + ) +); + +app.AddChatApiEndpoints() + .AddDownloadAIDocumentEndpoint(); +``` + +Generated downloads are kept separate from user-uploaded source documents, which helps hosts clean up conversation artifacts without touching knowledge uploads. + +## File Types + +Out of the box, the document features support common text, document, image, and tabular formats. Add the packages you need: + +- `AddOpenXml()` for Office formats such as Word, PowerPoint, and Excel +- `AddPdf()` for PDF reading +- `AddMarkdown()` for Markdown-aware normalization and chunking + +Use the document upload options to control which extensions your app accepts. + +## Common Setup Choices + +### Entity Framework Core stores + +```csharp +.AddDocumentProcessing(documentProcessing => documentProcessing + .AddEntityCoreStores() + .AddOpenXml() + .AddPdf() + .AddReferenceDownloads() +) +``` + +### YesSql stores + +```csharp +.AddDocumentProcessing(documentProcessing => documentProcessing + .AddYesSqlStores() + .AddOpenXml() + .AddPdf() + .AddReferenceDownloads() +) +``` + +Pick the store stack that matches the rest of your app. + +## Upload Configuration + +Use `ChatDocumentsOptions` to decide which file types users can attach. + +```csharp +services.Configure(options => +{ + options.Add(".rtf", embeddable: true); + options.Add(".tsv", embeddable: false); +}); +``` + +Use the configured option values in both your UI and server-side validation so the visible upload guidance matches what the app actually supports. + +## Storage + +Uploaded files are stored through `IDocumentFileStore`. The default setup uses local storage, but you can replace it when you want a different backend such as cloud blob storage. + +```csharp +builder.Services.AddSingleton(); +``` + +## Extending the Experience + +If you need another file format, register a custom reader for that extension and keep the rest of the document pipeline the same. + +```csharp +builder.Services.AddCoreAIIngestionDocumentReader(".custom", ".myformat"); +``` + +## When to Use AI Documents + +Choose AI Documents when your app needs any of the following: + +- chat over uploaded knowledge files +- searchable document context with citations +- spreadsheet and CSV workflows in chat +- downloadable generated files tied to a conversation +- multimodal chat that includes image uploads diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-memory.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-memory.md new file mode 100644 index 00000000..35b3eb3d --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-memory.md @@ -0,0 +1,118 @@ +--- +sidebar_label: AI Memory +sidebar_position: 13 +title: AI Memory +description: Long-term memory services for user-scoped facts, semantic retrieval, and memory-aware orchestration. +--- + +# AI Memory + +`CrestApps.Core` includes reusable memory services for applications that want an AI assistant to remember durable user facts across sessions. + +## What the framework provides + +`AddCoreAIMemory()` adds the shared runtime behavior for: + +- memory tool registration +- safety validation for memory writes +- semantic memory search orchestration +- preemptive memory retrieval during orchestration +- shared indexing and search helpers + +```csharp +builder.Services.AddCrestAppsCore(crestApps => crestApps + .AddAISuite(ai => ai + .AddAIMemory(memory => memory + .AddEntityCoreStores() + ) + .AddOpenAI() + ) + .AddEntityCoreSqliteDataStore("Data Source=app.db") +); +``` + +## What your host must provide + +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 +- 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` + +Register stores directly on the AI memory builder: + +**Entity Framework Core (via builder):** + +```csharp +.AddAIMemory(memory => memory + .AddEntityCoreStores() +) +``` + +**YesSql (via builder):** + +```csharp +.AddAIMemory(memory => memory + .AddYesSqlStores() +) +``` + +Both register the `IAIMemoryStore` implementation. See [Data Storage](data-storage.md) for the full per-feature store reference. + +## Core concepts + +### Memory entries + +A memory entry is a durable user-scoped fact: + +| Field | Purpose | +| --- | --- | +| `UserId` | Identifies the owner of the memory | +| `Name` | Stable key such as `preferred-language` | +| `Description` | Semantic summary used to improve retrieval quality | +| `Content` | The value to retain for later recall | +| `CreatedUtc` / `UpdatedUtc` | Lifecycle timestamps | + +### Safety validation + +Before a memory is stored, `IAIMemorySafetyService` can reject obviously sensitive data such as credentials, connection strings, SSNs, or payment card numbers. The framework ships with the validation pipeline; hosts decide how they surface validation failures. + +### User scoping + +Memory tools operate on the current authenticated user. The framework resolves identity from orchestration scope or the current HTTP context so retrieval stays user-specific. + +## Key contracts + +| Contract | Purpose | +| --- | --- | +| `IAIMemoryStore` | CRUD and query access for persisted memory entries | +| `IAIMemorySearchService` | Shared semantic retrieval over memory entries | +| `IMemoryVectorSearchService` | Provider-specific vector search adapter | +| `IAIMemorySafetyService` | Validation for writes before they are stored | +| `IPreemptiveRagHandler` | Injects relevant memory context before the model responds | + +## Built-in tools + +When memory is enabled, the orchestration layer can expose these system tools: + +| Tool | Purpose | +| --- | --- | +| `save_user_memory` | Create or update a durable memory | +| `search_user_memories` | Find relevant memories by semantic similarity | +| `list_user_memories` | Enumerate saved memories for the current user | +| `remove_user_memory` | Delete a saved memory by name | + +These tools are intended for long-lived facts such as preferences, recurring projects, or roles, not for transient one-off chat state. + +## Typical flow + +1. Register `AddCoreAIMemory()` with the rest of the AI runtime. +2. Provide the store, vector search, and option bindings for your host. +3. Enable memory-aware orchestration for the profiles or chat surfaces that should use it. +4. Let the orchestrator decide when to store, search, or inject memory context. + +## Related guidance + +- Pair memory with **[Orchestration Overview](../orchestration/index.md)** when you want automatic recall +- Pair memory with **[Data Sources](../data-sources/index.md)** when you also need document or index-backed RAG diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-profiles.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-profiles.md new file mode 100644 index 00000000..61e03b9b --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-profiles.md @@ -0,0 +1,214 @@ +--- +sidebar_label: AI Profiles +sidebar_position: 4 +title: AI Profiles +description: Understand AI Profiles as the reusable runtime contract that powers chat, agents, orchestration, memory, and retrieval across CrestApps.Core. +--- + +# AI Profiles + +> The reusable contract that tells CrestApps.Core **how an AI experience should behave**, not just which model to call. + +An **AI Profile** is the main composition unit for higher-level AI features in `CrestApps.Core`. It groups the instructions, deployments, orchestrator choice, tools, knowledge, and session-processing rules that define a reusable AI experience. + +If a deployment answers **"which model should run?"**, an AI Profile answers **"how should this experience behave from start to finish?"** + +## Why AI Profiles matter + +Profiles are used across many parts of the framework because they let you define AI behavior once and reuse it consistently: + +- **AI Chat** uses a profile as the session contract for reusable conversations +- **Agents** use profiles to describe specialized behavior and routing intent +- **Orchestration** reads the profile to decide how prompts, tools, and downstream steps should run +- **Knowledge-aware chat** uses profile-attached documents and data sources for retrieval +- **Memory and analytics** use profile settings to control long-lived personalization and post-session processing +- **Templates** can prefill or stamp profile behavior so teams do not repeat the same configuration manually + +## AI Profile vs. other AI building blocks + +| Concept | Purpose | Best way to think about it | +| --- | --- | --- | +| **AI Connection** | Stores provider credentials and endpoint details | "How do I talk to a provider?" | +| **AI Deployment** | Maps a logical deployment name to a concrete model on a provider/connection | "Which model should be used?" | +| **Chat Interactions** | Playground-style or ad hoc conversations with directly chosen parameters | "Let me test this setup quickly." | +| **AI Profile** | Reusable runtime behavior for chat, agents, orchestration, knowledge, and processing | "How should this AI experience behave?" | +| **AI Chat** | Session-driven chat experience built around a selected profile | "Run ongoing conversations from this reusable profile." | + +## When to use Chat Interactions vs. AI Profiles + +Start with **Chat Interactions** when you want the fastest validation path for a new provider connection and deployment. + +Move to **AI Profiles** when you want any of the following: + +- a reusable system prompt or welcome experience +- a stable deployment choice for repeated sessions +- orchestration and tool usage +- knowledge retrieval from documents or data sources +- memory, analytics, extraction, or post-session behavior +- agent-style routing or specialized assistant identities + +## What an AI Profile contains + +The exact fields depend on enabled features, but a profile can act as the home for: + +### 1. Identity and purpose + +- technical name +- display title +- profile type +- description, especially for agent profiles + +This gives the runtime and UI a stable identity for the experience. + +### 2. Deployment selection + +A profile can point to: + +- a **chat deployment** for primary conversational responses +- a **utility deployment** for supporting tasks such as planning, extraction, or summarization + +That lets the same profile use different models for different responsibilities. + +### 3. Prompt and conversation behavior + +Profiles can define: + +- system instructions +- welcome message +- initial assistant prompt +- prompt subject +- prompt templates +- completion settings such as temperature, top-p, penalties, token limits, and past-message depth + +This is where you shape tone, constraints, and conversation style. + +### 4. Orchestration and tool usage + +Profiles can select: + +- an orchestrator +- local tools +- agent references +- remote A2A connections +- remote MCP connections + +This is why profiles are broader than plain chat presets. They can define how the AI experience coordinates work, not just how it talks. + +If a selected tool has registered dependencies, CrestApps.Core automatically includes those dependent tools at runtime. That lets profiles keep only the top-level tool selection while helper tools remain hidden or system-managed. + +The MVC and Blazor editors only surface **selectable** tools and **user-selectable** agents here. Hidden tools, system tools, always-available agents, and system agents such as the Tabular Data Agent stay out of the picker and continue to be managed by the framework. + +### 5. Knowledge and retrieval + +Profiles can be linked to: + +- uploaded profile documents +- session document behavior +- index-backed data sources +- retrieval tuning such as strictness, top-N, scope, and filters + +This makes the profile the reusable knowledge boundary for RAG-oriented experiences. + +### 6. Session and outcome processing + +Profiles can enable: + +- extracted data definitions +- session metrics +- AI resolution detection +- conversion goals +- post-session processing tasks + +That turns a profile into more than a prompt container. It becomes the contract for what should happen during and after a session. + +### 7. Memory and personalization + +Profiles can opt into user memory so experiences can carry durable context forward between sessions instead of starting from zero every time. + +That toggle is stored directly as `MemoryMetadata`, so profile and template consumers read and write one shared metadata shape instead of carrying legacy memory-setting aliases forward. + +### 8. Anti-spam throttling + +Profiles can override the site-level anti-spam throttle limits through `PromptSecurityProfileSettings`. + +That lets you keep a strong global baseline while raising or lowering throttle quotas for individual profiles, for cases such as: + +- tighter per-minute message limits for public, unauthenticated widgets +- higher limits for carefully managed internal or authenticated workflows +- adjusting anonymous session-start quotas for a specific use case + +Only anti-spam throttling is per-profile. High-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only and are configured through `PromptSecurityOptions`. + +See [Prompt Security](./prompt-security.md) for the full option set, scoring model, and limitations. + +## Profile types + +`AIProfile.Type` lets one model support different runtime roles. + +Common examples: + +- **Chat** for reusable conversational assistants +- **Agent** for specialized routed behavior that an orchestrator can call when appropriate +- **TemplatePrompt** when the profile is oriented around prompt generation or reusable prompt-driven tasks + +The important idea is that the profile type changes how the framework interprets and uses the same underlying profile record. + +## Typical lifecycle + +1. Create a provider connection. +2. Create one or more deployments. +3. Use **Chat Interactions** to verify the model behaves correctly. +4. Create an AI Profile once you want a reusable behavior contract. +5. Attach tools, documents, data sources, memory, or post-session rules as needed. +6. Use the profile from AI Chat, agents, orchestrators, or other runtime features. + +## Practical examples + +### Example 1: Reusable support assistant + +Use an AI Profile when you want: + +- a fixed support tone +- a shared knowledge base +- extracted contact or issue fields +- post-session resolution analysis + +This profile can then power every support chat session consistently. + +### Example 2: Specialized agent + +Use an AI Profile when you want: + +- a description that explains what the agent is good at +- a specific deployment and tool set +- orchestration-based routing into that agent + +The profile becomes the unit the orchestrator can reason about and invoke. + +### Example 3: Knowledge-aware internal assistant + +Use an AI Profile when you want: + +- indexed data sources +- attached profile documents +- stricter retrieval settings +- user memory for returning employees + +That profile can then serve as a reusable internal assistant instead of rebuilding the configuration per session. + +## Design guidance + +- Use **deployments** to separate model selection from behavior. +- Use **profiles** to capture reusable behavior and lifecycle rules. +- Use **Chat Interactions** for fast testing and experimentation. +- Use **AI Chat** when you want repeatable session-based experiences built on top of a profile. +- Use **Prompt Security overrides** when a specific profile needs tighter or looser enforcement than the site default. + +## Related docs + +- [AI Core](./ai-core.md) +- [Chat Interactions](./chat.md) +- [AI Templates](./ai-templates.md) +- [AI Agents](./agents.md) +- [Prompt Security](./prompt-security.md) +- [MVC Example](./mvc-example.md) diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-resilience.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-resilience.md new file mode 100644 index 00000000..b8cdede2 --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-resilience.md @@ -0,0 +1,240 @@ +--- +sidebar_position: 4 +title: AI Resilience +description: Builder-based resilience middleware for Microsoft.Extensions.AI chat, embeddings, image, speech-to-text, and text-to-speech clients. +--- + +# AI Resilience + +> Add reusable retry middleware to Microsoft.Extensions.AI clients without forcing a global policy on every host-created client. + +`CrestApps.Core.AI.Resilience` is a standalone package that adds builder-based resilience extensions for: + +- `IChatClient` +- `IEmbeddingGenerator` +- `IImageGenerator` +- `ISpeechToTextClient` +- `ITextToSpeechClient` + +Framework-owned completion and utility chat paths in `CrestApps.Core` already use the default retry policy internally. This package is for host-created clients and for applications that want to opt into the same pattern explicitly. + +## Package + +```xml + +``` + +The package depends on: + +- `Microsoft.Extensions.AI` +- `Microsoft.Extensions.Resilience` + +## Builder Extensions + +Every supported client follows one of these patterns: + +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)`. + +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 + +`UseDefaultResilience()` is intentionally narrow: it retries provider rate-limit failures such as HTTP `429 Too Many Requests`. + +Default settings: + +| Setting | Default | +|---|---| +| `MaxRateLimitRetries` | `5` | +| `RateLimitRetryDelay` | `1 second` | +| `BackoffType` | `Exponential` | +| `UseJitter` | `true` | +| `MaxRetryDelay` | `32 seconds` | + +That produces an approximate retry schedule like this: + +| Attempt | Delay | +|---|---| +| Initial | immediately | +| Retry 1 | ~1-2 seconds | +| Retry 2 | ~2-4 seconds | +| Retry 3 | ~4-8 seconds | +| Retry 4 | ~8-16 seconds | +| Retry 5 | ~16-32 seconds | + +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() + .UseDefaultResilience() + .Build(serviceProvider); +``` + +## Customizing the Default Settings + +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() + .UseDefaultResilience(options => + { + options.MaxRateLimitRetries = 3; + options.RateLimitRetryDelay = TimeSpan.FromSeconds(2); + options.BackoffType = DelayBackoffType.Exponential; + options.UseJitter = true; + options.MaxRetryDelay = TimeSpan.FromSeconds(20); + }) + .Build(serviceProvider); +``` + +If you prefer the old fixed schedule, configure it explicitly: + +```csharp +var resilientClient = chatClient + .AsBuilder() + .UseDefaultResilience(options => + { + options.MaxRateLimitRetries = 4; + options.RateLimitRetryDelay = TimeSpan.FromSeconds(5); + options.BackoffType = DelayBackoffType.Constant; + options.UseJitter = false; + options.MaxRetryDelay = TimeSpan.FromSeconds(5); + }) + .Build(serviceProvider); +``` + +## Fully Custom Pipelines + +Use `UseResilience(...)` when you want full control over the Polly pipeline: + +```csharp +var resilientClient = chatClient + .AsBuilder() + .UseResilience(pipeline => pipeline.AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = 2, + Delay = TimeSpan.FromSeconds(1), + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + ShouldHandle = args => ValueTask.FromResult( + args.Outcome.Exception is HttpRequestException ex && + ex.StatusCode == HttpStatusCode.TooManyRequests), + })) + .Build(serviceProvider); +``` + +You can also supply a prebuilt `ResiliencePipeline`. + +## Other Client Types + +The same extension methods are available on the other Microsoft.Extensions.AI builders: + +### Embeddings + +```csharp +var resilientGenerator = await aiClientFactory.CreateEmbeddingGeneratorAsync( + deployment, + builder => builder.UseDefaultResilience()); +``` + +```csharp +var resilientGenerator = embeddingGenerator + .AsBuilder() + .UseDefaultResilience() + .Build(serviceProvider); +``` + +### Image Generation + +```csharp +var resilientGenerator = await aiClientFactory.CreateImageGeneratorAsync( + deployment, + builder => builder.UseDefaultResilience()); +``` + +```csharp +var resilientGenerator = imageGenerator + .AsBuilder() + .UseDefaultResilience() + .Build(serviceProvider); +``` + +### Speech to Text + +```csharp +var resilientClient = await aiClientFactory.CreateSpeechToTextClientAsync( + deployment, + builder => builder.UseDefaultResilience()); +``` + +```csharp +var resilientClient = speechToTextClient + .AsBuilder() + .UseDefaultResilience() + .Build(serviceProvider); +``` + +### Text to Speech + +```csharp +var resilientClient = await aiClientFactory.CreateTextToSpeechClientAsync( + deployment, + builder => builder.UseDefaultResilience()); +``` + +```csharp +var resilientClient = textToSpeechClient + .AsBuilder() + .UseDefaultResilience() + .Build(serviceProvider); +``` + +## Streaming Notes + +- `ITextToSpeechClient` streaming retries are supported when the failure happens before the first streamed update is yielded. +- `ISpeechToTextClient` non-streaming retries work for both seekable and non-seekable streams. +- `ISpeechToTextClient` streaming retries require a seekable input stream so the audio can be replayed safely across retry attempts. + +## When to Use It + +Use `UseDefaultResilience()` when: + +- you want a safe default for provider throttling +- you want framework-style retries on your own clients +- you do not need a custom Polly pipeline yet + +Use `UseResilience(...)` when: + +- you need custom retry predicates +- you want to add additional strategies yourself +- you want one shared prebuilt pipeline across multiple clients diff --git a/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-templates.md b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-templates.md new file mode 100644 index 00000000..1e8e734a --- /dev/null +++ b/src/CrestApps.Core.Docs/versioned_docs/version-1.0.x/core/ai-templates.md @@ -0,0 +1,449 @@ +--- +sidebar_label: AI Templates +sidebar_position: 7 +title: AI Templates +description: Liquid-based prompt template engine for managing, rendering, and composing AI system prompts. +--- + +# AI Templates + +> A Liquid-based template engine for managing, rendering, and composing AI system prompts from multiple sources. + +## Quick Start + +```csharp +builder.Services.AddCoreAITemplating(); +``` + +:::info +You rarely need to call this directly — `AddCoreAIServices()` chains it automatically. +::: + +## Problem & Solution + +Hard-coding system prompts in C# makes them difficult to maintain, localize, and customize. The template system: + +- Stores prompts as **markdown files** with front-matter metadata +- Renders them with **Liquid** syntax for dynamic content +- Discovers templates from **multiple sources** (embedded resources, file system, code) +- Supports **merging** multiple templates into a single prompt + +## Services Registered by `AddCoreAITemplating()` + +`AddCoreAITemplating()` builds on the lower-level `AddTemplating()` registration and also adds the built-in AI template source metadata for `SystemPrompt` and `Profile` templates. + +| Service | Implementation | Lifetime | Purpose | +|---------|---------------|----------|---------| +| `ITemplateParser` | `DefaultMarkdownTemplateParser` | Singleton | Parses markdown front-matter templates | +| `ITemplateEngine` | `FluidTemplateEngine` | Singleton | Renders Liquid templates | +| `ITemplateService` | `DefaultTemplateService` | Scoped | Unified template discovery and rendering | +| `OptionsTemplateProvider` | — | Singleton | Templates registered via code | +| `FileSystemTemplateProvider` | — | Singleton | Generic templates discovered directly from `Templates/` | +| `PromptsFileSystemTemplateProvider` | — | Singleton | System prompt templates discovered from `Templates/Prompts/` | + +## Key Interfaces + +### `ITemplateService` + +The main service for working with templates. + +```csharp +public interface ITemplateService +{ + Task> ListAsync(); + Task