Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .azure/modules/appi-applicationinsights.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ param name string

@description('The resource ID of the Log Analytics workspace to link to Application Insights. Must be a valid resourceId string.')
@minLength(1)
param workResourceId string
param logResourceId string

@description('Controls whether Application Insights accepts public telemetry ingestion.')
@allowed([
Expand All @@ -44,7 +44,7 @@ resource appiResource 'Microsoft.Insights/components@2020-02-02' = {
properties: {
Application_Type: 'web'
Flow_Type: 'Bluefield'
WorkspaceResourceId: workResourceId
WorkspaceResourceId: logResourceId
publicNetworkAccessForIngestion: publicNetworkAccessForIngestion
publicNetworkAccessForQuery: publicNetworkAccessForQuery
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ param stAccountName string
@minLength(3)
@maxLength(63)
@description('Name of the Azure Files share mounted into Ollama for persistent model data.')
param storageShareName string = 'ollama-models'
param storageShareName string = 'share-ollamamodels'

@description('Ollama model to pull and serve on startup.')
@allowed([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ param tags object = {}
@minValue(30)
param retentionInDays int = 30

resource workResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
resource logResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: name
location: location
tags: empty(tags) ? null : tags
Expand All @@ -34,4 +34,4 @@ resource workResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
}
}

output id string = workResource.id
output id string = logResource.id
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ param tags object = {}
@minValue(30)
param retentionInDays int = 30

// Sentinel = the plain workspace (work-) plus onboarding
module workspace 'law-loganalyticsworkspace.bicep' = {
name: 'sent-${name}'
// Sentinel = the plain Log Analytics workspace (log-loganalyticsworkspace) plus onboarding
module workspace 'log-loganalyticsworkspace.bicep' = {
name: 'log-sentinel-${name}'
params: {
name: name
location: location
Expand All @@ -34,7 +34,7 @@ module workspace 'law-loganalyticsworkspace.bicep' = {
}
}

resource workResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = {
resource logResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = {
name: name
dependsOn: [
workspace
Expand All @@ -43,7 +43,7 @@ resource workResource 'Microsoft.OperationalInsights/workspaces@2023-09-01' exis

resource onboarding 'Microsoft.SecurityInsights/onboardingStates@2021-03-01-preview' = {
name: 'default'
scope: workResource
scope: logResource
properties: {}
}

Expand Down
12 changes: 6 additions & 6 deletions .azure/templates/workload-digitalproduct.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ var sharedSubscriptionId = shared.?subscriptionId ?? subscription().subscription
var sharedResourceGroupName = shared.?resourceGroupName ?? resourceGroup().name
var vnetSubscriptionId = vnet.?subscriptionId ?? subscription().subscriptionId

module lawModule '../modules/law-loganalyticsworkspace.bicep' = if (isStandalone) {
name: 'digitalproduct-law'
module logModule '../modules/log-loganalyticsworkspace.bicep' = if (isStandalone) {
name: 'digitalproduct-log'
params: {
name: hosting.?workspaceName ?? '${hosting.appiName}-work'
name: hosting.workspaceName!
location: location
tags: tags
}
Expand All @@ -74,12 +74,12 @@ module appiModule '../modules/appi-applicationinsights.bicep' = if (isStandalone
name: hosting.appiName
location: location
tags: tags
workResourceId: lawModule!.outputs.id
logResourceId: logModule!.outputs.id
}
}

module planModule '../modules/plan-appserviceplan.bicep' = if (isStandalone) {
name: 'digitalproduct-plan'
module planModule '../modules/asp-appserviceplan.bicep' = if (isStandalone) {
name: 'digitalproduct-asp'
params: {
name: hosting.planName
location: location
Expand Down
2 changes: 1 addition & 1 deletion .azure/types/digitalproduct-types.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type hostingConfig = {

@minLength(4)
@maxLength(63)
@description('Standalone only. Log Analytics workspace name backing Application Insights.')
@description('Required in standalone mode. Log Analytics workspace name backing Application Insights.')
workspaceName: string?
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ using '../templates/workload-digitalproduct.bicep'

// Phase 1 (mode 1, standalone): plan, Log Analytics, App Insights, web, API and SQL all live in this RG.
// RG: {product}-{env}-{region}-{instance}-rg. Secrets: set SQL_ADMIN_USER and SQL_ADMIN_PASSWORD in the environment before running az deployment group.
// Deploy: az deployment group create -g agent-dev-wus2-001-rg -f .azure/templates/workload-digitalproduct.bicep -p .azure/variables/workload-digitalproduct-standalone-dev.bicepparam
// Deploy: az deployment group create -g agent-dev-wus2-100-rg -f .azure/templates/workload-digitalproduct.bicep -p .azure/variables/workload-digitalproduct-standalone-dev.bicepparam

var productIac = 'agent'
var environmentIac = 'dev'
var regionIac = 'wus2'
var instanceIac = '001'
var instanceIac = '100'
var prefix = '${productIac}-${environmentIac}-${regionIac}-${instanceIac}'

param mode = 'standalone'
Expand All @@ -17,9 +17,9 @@ param tags = { Environment: environmentIac, CostCenter: '0000' }

param hosting = {
appiName: '${prefix}-appi'
planName: '${prefix}-F1-plan'
planName: '${prefix}-F1-asp'
planSku: 'F1'
workspaceName: '${prefix}-work'
workspaceName: '${prefix}-log'
}

// Components: delete a line to skip that component.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/agent-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ env:
# ===== EDIT PER INSTALL: naming slots (names: {product}-{env}-{region}-{instance}-{type}) =====
PRODUCT: "agent"
REGION: "wus2"
INSTANCE: "001"
INSTANCE: "100"
# Same value as MODE in the IaC workflow. standalone: App Insights is in the product RG; any other mode: in the shared management RG.
MODE: "standalone"
# Shared management RG and App Insights from platform-shared-mgmt; product is normally 'mgmt'.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/agent-iac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ env:
# ===== EDIT PER INSTALL: naming slots (RG: {product}-{env}-{region}-{instance}-rg) =====
PRODUCT: "agent"
REGION: "wus2"
INSTANCE: "001"
INSTANCE: "100"
LOCATION: "westus2"
# standalone | shared | network | zerotrust
MODE: "standalone"
Expand Down
6 changes: 5 additions & 1 deletion Initialize-Local.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Run from any folder in PowerShell (not 'pwsh -File', so a splatted hashtable wor
3. Database: applies the EF migrations to the local SQL Server (LocalDB by default).
Local only: it does not touch GitHub, Azure Pipelines or Azure resources. CI/CD secrets and federation are set up separately.
Safe to re-run: existing registrations are reused, and the Web client secret is only regenerated with -RotateSecrets.
Leave -ApplicationInsightsConnectionString empty until the infrastructure is deployed; the apps start without it, with no Azure telemetry. Deployment names are only stored when you pass them, because the appsettings files already carry the template defaults (chat-fast, chat-balanced, embedding-fast).
Leave -ApplicationInsightsConnectionString empty until the infrastructure is deployed; the apps start without it, with no Azure telemetry. Deployment names are only stored when you pass them, because the appsettings files already carry the template defaults (chat-fast, tool-standard, tool-reasoning, embedding-standard).

.EXAMPLE
# Omit -AzureOpenAIApiKey to be prompted without echo:
Expand All @@ -31,6 +31,8 @@ param(
[Parameter(Mandatory)][string]$AzureOpenAIEndpoint,
[string]$AzureOpenAIApiKey,
[string]$ChatDeploymentName,
[string]$ToolDeploymentName,
[string]$ToolReasoningDeploymentName,
[string]$EmbeddingDeploymentName,
[string]$ApplicationInsightsConnectionString = "",

Expand Down Expand Up @@ -108,6 +110,8 @@ $openAi = @{
"AzureOpenAI:ApiKey" = $AzureOpenAIApiKey
}
if ($ChatDeploymentName) { $openAi["AzureOpenAI:ChatDeploymentName"] = $ChatDeploymentName }
if ($ToolDeploymentName) { $openAi["AzureOpenAI:ToolDeploymentName"] = $ToolDeploymentName }
if ($ToolReasoningDeploymentName) { $openAi["AzureOpenAI:ToolReasoningDeploymentName"] = $ToolReasoningDeploymentName }
if ($EmbeddingDeploymentName) { $openAi["AzureOpenAI:EmbeddingDeploymentName"] = $EmbeddingDeploymentName }
$appInsights = @{ "ApplicationInsights:ConnectionString" = $ApplicationInsightsConnectionString }

Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ dotnet user-secrets set "ApplicationInsights:ConnectionString" "<connection-stri
```
In Azure, the deployment sets the connection string for you.

**Azure OpenAI deployment names:** the committed appsettings use `chat-fast` (Development and local), `chat-balanced` (Production) and `embedding-fast`. These are placeholders for the names of your own Azure OpenAI or AI Foundry model deployments. Replace them in `src/Presentation.Api/appsettings.*.json` to match your deployments (deployment names are not secrets, so they need no GitHub secrets), or pass `-ChatDeploymentName` and `-EmbeddingDeploymentName` to `Initialize-Local.ps1` to override them in your local user-secrets only.
**Azure OpenAI deployment names:** all committed environments use `chat-fast`, `tool-standard`, `tool-reasoning`, and `embedding-standard`, matching the shared `platform-azure` Foundry deployments. To use different purpose-compatible deployments, update `src/Presentation.Api/appsettings.*.json`, or pass `-ChatDeploymentName`, `-ToolDeploymentName`, `-ToolReasoningDeploymentName`, and `-EmbeddingDeploymentName` to `Initialize-Local.ps1` for local user-secret overrides only. Deployment names are not secrets.

## Quick-Start: Re-Setup for Existing Entra (no Entra setup scripts)
```powershell
Expand Down Expand Up @@ -283,7 +283,7 @@ dotnet user-secrets set "AzureOpenAI:ApiKey" "<azure-openai-api-key>" --project
dotnet user-secrets set "AzureOpenAI:Endpoint" "https://<resource>.openai.azure.com/openai/v1" --project .\src\Tests.Integration\Tests.Integration.csproj
dotnet user-secrets set "AzureOpenAI:ApiKey" "<azure-openai-api-key>" --project .\src\Tests.Integration\Tests.Integration.csproj
```
The deployment names come from `AzureOpenAI:ChatDeploymentName` and `AzureOpenAI:EmbeddingDeploymentName` in `src/Presentation.Api/appsettings.*.json` (`chat-fast`, `chat-balanced`, `embedding-fast`). Replace them with your own deployment names, or override them with `dotnet user-secrets set` using the same keys.
Deployment names come from `AzureOpenAI:ChatDeploymentName`, `AzureOpenAI:ToolDeploymentName`, `AzureOpenAI:ToolReasoningDeploymentName`, and `AzureOpenAI:EmbeddingDeploymentName` in `src/Presentation.Api/appsettings.*.json`. Override these same keys with `dotnet user-secrets set` when using different purpose-compatible deployments.

Alternately you can set in Environment variables
```
Expand Down Expand Up @@ -441,7 +441,7 @@ doc's **UI Changes** and **API Changes** sections for details.
# Github Actions for Azure IaC and CI/CD
## GitHub Actions (.github folder)

The `.github/workflows` folder contains the GitHub Actions pipelines for CI/CD. Workflows follow the [github-actions](https://github.com/goodtocode/github-actions) conventions and are named `{product}-{capability}.yml`. The naming slots (`PRODUCT`, `REGION`, `INSTANCE`, `MODE`) at the top of each workflow produce the Azure names `{product}-{env}-{region}-{instance}-{type}`, for example `agent-dev-wus2-001-rg`. Below is a summary of the workflow files, their purposes, and triggers:
The `.github/workflows` folder contains the GitHub Actions pipelines for CI/CD. Workflows follow the [github-actions](https://github.com/goodtocode/github-actions) conventions and are named `{product}-{capability}.yml`. The naming slots (`PRODUCT`, `REGION`, `INSTANCE`, `MODE`) at the top of each workflow produce the Azure names `{product}-{env}-{region}-{instance}-{type}`, for example `agent-dev-wus2-100-rg`. Below is a summary of the workflow files, their purposes, and triggers:

### Triggers
All workflow YAML files in this repo are designed to:
Expand Down Expand Up @@ -567,4 +567,11 @@ This setup ensures your GitHub Actions workflows can securely deploy to Azure us
| 1.0.0 | 2026-Feb-02 | Initial Release |
| 1.1.0 | 2026-Jul-27 | AI-ize md/yml, AgentProvider |

This project is licensed with the [MIT license](https://mit-license.org/).
This project is licensed with the [MIT license](https://mit-license.org/).

### Shared Foundry deployment configuration

Use `AzureOpenAI:ChatDeploymentName=chat-fast` for text-only Phi chat, `AzureOpenAI:ToolDeploymentName=tool-standard` for Azure-direct Mistral-Large-3 function calls, and `AzureOpenAI:ToolReasoningDeploymentName=tool-reasoning` for MAI playbook inference. Azure-direct Cohere embed-v-4-0 intent embeddings use `AzureOpenAI:EmbeddingDeploymentName=embedding-standard` and `EmbeddingDimension=1536`.
These names match the `platform-azure` shared Foundry definitions; provision them and verify regional model access/quota before running live AI scenarios. MAI-Thinking-1 remains preview. Existing embedding vectors must be regenerated after switching models.
All three purpose deployments are required for the selected provider. Old user-secret/environment overrides may need migration; a chat-only override does not select a tool deployment.
See [semantic intent classification](docs/product/features/feature-semantic-intent-classification.md#purpose-specific-foundry-inference) for routing, governance, and migration details.
35 changes: 33 additions & 2 deletions docs/product/features/feature-semantic-intent-classification.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ The API configuration exposes the following section in every environment file:

The section is bound through `IOptions<IntentClassificationOptions>`. `EnableSemantic` must remain `false` until evaluation and rollout approval are complete.

Azure embedding settings are bound through `AzureOpenAIOptions`, including `EmbeddingDeploymentName` with the default deployment name `embedding-fast`.
Azure embedding settings are bound through `AzureOpenAIOptions`, including `EmbeddingDeploymentName` with the default deployment name `embedding-standard`.

## Shared Implementation

Expand Down Expand Up @@ -190,7 +190,7 @@ Both repositories require:
- Reqnroll scenarios covering the disabled switch and an enabled semantic match using deterministic embeddings and in-memory storage.
- API configuration parsing tests or equivalent validation confirming the switch is present and disabled by default.

Live Azure-backed seeding is a separate environment validation. It requires a configured `embedding-fast` deployment, SQL database, credentials, and an applied migration.
Live Azure-backed seeding is a separate environment validation. It requires a configured `embedding-standard` deployment, SQL database, credentials, and an applied migration.

## Rollout

Expand Down Expand Up @@ -221,3 +221,34 @@ Live Azure-backed seeding is a separate environment validation. It requires a co
- [ ] Production enablement is approved and reversible.
- [ ] Governance documentation contains durable principles only.
- [ ] Tactical implementation details remain in this product feature document.


## Purpose-specific Foundry inference

These applications consume the shared `platform-azure` Foundry deployments:

| Purpose | Configuration | Deployment | Model |
| --- | --- | --- | --- |
| Text-only chat | `AzureOpenAI:ChatDeploymentName` | `chat-fast` | Phi-4-mini-instruct |
| Interactive functions | `AzureOpenAI:ToolDeploymentName` | `tool-standard` | Mistral-Large-3 (Azure-direct) |
| Playbook reasoning | `AzureOpenAI:ToolReasoningDeploymentName` | `tool-reasoning` | MAI-Thinking-1 |
| Intent embeddings | `AzureOpenAI:EmbeddingDeploymentName` | `embedding-standard` | Cohere embed-v-4-0 (Azure-direct, 1536 dimensions) |

`embedding-standard` is shared by intent classification and retrieval consumers, using Cohere embed-v-4-0 at 1536 dimensions.
Callable function names must fit the OpenAI-compatible API's 64-character limit. Names within the limit
remain unchanged; longer tool-qualified names use a readable 47-character prefix and a deterministic
16-hex-character SHA-256 suffix to avoid truncation collisions and provider HTTP 422 errors.
MAI-Thinking-1 remains a preview model despite the deployment name having no preview suffix.
Verify model availability, access, and quota before provisioning the shared deployments; the names alone do not establish capabilities.

The keyed `InferencePurpose.Chat` agent advertises no application functions. Direct mode and reply-wording correction use only this agent. Correction receives the original reply, retains the governed policy context, and cannot repeat application operations.
The default agent is `InferencePurpose.Tool`: described public tool methods become real `AIFunction` instances with typed schemas and qualified names.
After classification misses, general requests use one automatic-function turn. Recognized chat-history requests require a function call; missing calls, provider failures, and cancellation propagate without a second open-agent attempt.
Agentic Essay and Taxonomy stages explicitly consume `InferencePurpose.ToolReasoning`; these stages do not expose chat administration functions.
Existing durable chat governance and hosted catalog/embedding initialization remain enabled.

AzureAIFoundry uses the corresponding `ChatCompletionModelId`, `ToolCompletionModelId`, and `ToolReasoningModelId` deployment settings. OpenAI, GitHubCopilotSDK, and Ollama also require explicit purpose models; unused providers remain unvalidated. Ollama defaults retain text-only phi4 and use OpenAI gpt-oss:20b for function/reasoning purposes, requiring a local model pull before use.

Embedding calls use the official OpenAI SDK and the normalized `/openai/v1/embeddings` endpoint, passing the deployment name and configured dimension. Root endpoints and existing `/openai/v1/` endpoints are both supported.
Before enabling semantic classification, regenerate persisted intent embeddings when changing the embedding model, even if the dimension is unchanged. Reconcile SQL vector storage when changing dimensions; do not compare embeddings from different model spaces.
No timeout increases, automatic model fallback, or Azure deployment are part of this change.
4 changes: 2 additions & 2 deletions src/Core.Application/Abstractions/ChatRoutingMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace Goodtocode.AgentFramework.Core.Application.Abstractions;
/// </summary>
public enum ChatRoutingMode
{
/// <summary>Uses deterministic routing, forced-tool inference, then an open agent turn.</summary>
/// <summary>Uses deterministic routing, then purpose-specific function inference without open-agent retries.</summary>
Routed = 0,

/// <summary>Bypasses deterministic and forced-tool routing for diagnostics or explicit direct-agent requests.</summary>
/// <summary>Uses text-only inference without classification or application functions.</summary>
Direct = 1
}
Loading
Loading