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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Agent Framework C# Quick-Start
**Governed chat: Agentic Intent.** Every session gets a governed Instruction and every message a Preamble from a versioned policy. See [docs/governance/agentic-intent-adoption.md](docs/governance/agentic-intent-adoption.md) to replace the sample policy or add jurisdiction rules.

**Azure Bicep Infrastucture as Standalone Landing Zone**

[![Landing Zone IaC](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-iac.yml/badge.svg)](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-iac.yml)
Expand Down
180 changes: 143 additions & 37 deletions data/Admin/Drop Tables.sql
Original file line number Diff line number Diff line change
@@ -1,49 +1,155 @@
-- -- SELECT IS_SRVROLEMEMBER('sysadmin') AS IsSysAdmin, IS_SRVROLEMEMBER('dbcreator') AS IsDbCreator;
-- -- Go
/*
Safe dynamic database cleanup script
Purpose:
Remove all user objects in the current database without dropping the database itself.
This removes:
- foreign keys
- other table constraints
- triggers
- views
- stored procedures
- functions
- tables

Use AgentFramework
GO
IMPORTANT:
Run this against the target database only.
*/

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[__EFMigrationsHistory]') AND type in (N'U'))
DROP TABLE [dbo].[__EFMigrationsHistory]
GO
SET NOCOUNT ON;
SET XACT_ABORT ON;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[IntentEmbeddings]') AND type in (N'U'))
DROP TABLE [Chat].[IntentEmbeddings]
GO
DECLARE @sql NVARCHAR(MAX) = N'';
DECLARE @loopCount INT = 0;
DECLARE @maxLoops INT = 50;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[RequestIdempotency]') AND type in (N'U'))
DROP TABLE [Chat].[RequestIdempotency]
GO
------------------------------------------------------------
-- 1) Drop all foreign keys (repeat until none remain)
------------------------------------------------------------
WHILE EXISTS (SELECT 1 FROM sys.foreign_keys WHERE is_ms_shipped = 0)
BEGIN
SET @loopCount += 1;
IF @loopCount > @maxLoops
BEGIN
RAISERROR('Foreign key cleanup did not converge. Check for circular dependencies or blocked constraints.', 16, 1);
BREAK;
END;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatGovernance]') AND type in (N'U'))
DROP TABLE [Chat].[ChatGovernance]
GO
SET @sql = N'';

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatMessages]') AND type in (N'U'))
DROP TABLE [Chat].[ChatMessages]
GO
SELECT @sql +=
N'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(fk.schema_id)) + N'.' + QUOTENAME(OBJECT_NAME(fk.parent_object_id)) +
N' DROP CONSTRAINT ' + QUOTENAME(fk.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.foreign_keys AS fk
WHERE fk.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(fk.schema_id), OBJECT_NAME(fk.parent_object_id), fk.name;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatSessions]') AND type in (N'U'))
DROP TABLE [Chat].[ChatSessions]
GO
IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;
END;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[Actors]') AND type in (N'U'))
DROP TABLE [Chat].[Actors]
GO
------------------------------------------------------------
-- 2) Drop all other table constraints (PK, UNIQUE, CHECK, DEFAULT)
------------------------------------------------------------
SET @sql = N'';

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookMaterializations]') AND type in (N'U'))
DROP TABLE [Chat].[PlaybookMaterializations]
GO
SELECT @sql +=
N'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name) +
N' DROP CONSTRAINT ' + QUOTENAME(c.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.objects AS c
INNER JOIN sys.tables AS t
ON c.parent_object_id = t.object_id
WHERE c.type IN ('PK', 'UQ', 'C', 'D', 'F')
AND c.is_ms_shipped = 0
AND t.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(t.schema_id), t.name, c.name;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookExecutions]') AND type in (N'U'))
DROP TABLE [Chat].[PlaybookExecutions]
GO
IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookSteps]') AND type in (N'U'))
DROP TABLE [Chat].[PlaybookSteps]
GO
------------------------------------------------------------
-- 3) Drop triggers
------------------------------------------------------------
SET @sql = N'';

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[Playbooks]') AND type in (N'U'))
DROP TABLE [Chat].[Playbooks]
GO
SELECT @sql +=
N'DROP TRIGGER ' + QUOTENAME(SCHEMA_NAME(o.schema_id)) + N'.' + QUOTENAME(o.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.objects AS o
WHERE o.type = 'TR'
AND o.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(o.schema_id), o.name;

IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

------------------------------------------------------------
-- 4) Drop views
------------------------------------------------------------
SET @sql = N'';

SELECT @sql +=
N'DROP VIEW ' + QUOTENAME(SCHEMA_NAME(v.schema_id)) + N'.' + QUOTENAME(v.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.views AS v
WHERE v.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(v.schema_id), v.name;

IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

------------------------------------------------------------
-- 5) Drop stored procedures
------------------------------------------------------------
SET @sql = N'';

SELECT @sql +=
N'DROP PROCEDURE ' + QUOTENAME(SCHEMA_NAME(p.schema_id)) + N'.' + QUOTENAME(p.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.procedures AS p
WHERE p.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(p.schema_id), p.name;

IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

------------------------------------------------------------
-- 6) Drop functions
------------------------------------------------------------
SET @sql = N'';

SELECT @sql +=
N'DROP FUNCTION ' + QUOTENAME(SCHEMA_NAME(f.schema_id)) + N'.' + QUOTENAME(f.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.objects AS f
WHERE f.type IN ('FN', 'IF', 'TF', 'FS', 'FT')
AND f.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(f.schema_id), f.name;

IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

------------------------------------------------------------
-- 7) Drop tables
------------------------------------------------------------
SET @sql = N'';

SELECT @sql +=
N'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name) + N';' + CHAR(13) + CHAR(10)
FROM sys.tables AS t
WHERE t.is_ms_shipped = 0
ORDER BY SCHEMA_NAME(t.schema_id), t.name;

IF LEN(@sql) > 0
BEGIN
EXEC sys.sp_executesql @sql;
END;

PRINT 'Cleanup complete: all user-defined objects were dropped, database left intact.';
28 changes: 28 additions & 0 deletions docs/governance/agentic-intent-adoption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Agentic Intent: Instruction and Preamble

Agentic Intent is the first governance **policy**, applied beside the four pillars (Observability, Repeatability, Auditability, Defensibility). Principles are `Boundary`, `Value` and `Conduct`, with precedence Boundary > Value > Conduct > persona > user request.

## How the template applies it

| Concept | Cadence | Where |
|---|---|---|
| Instruction | Once per chat session | `ChatMessageIntentRouter` opens `ChatGovernanceScope.Begin(tenantId, instruction)` with the persona plus governed directives |
| Preamble | Before every user message | `GovernedChatClient` (via `UseGovernedIntent`) in `ConfigureServices.cs`; never rendered as a chat bubble |
| Policy identity | Every governed inference | `ChatGovernanceGate.Enforce` adds `policy://agentic-intent/{id}/{version}/{hash}` to `Defensibility.PoliciesApplied`; playbook steps record the same reference |

A change to the policy text or version changes the hash and is reported as drift.

## Configuration

```json
"GovernedIntent": { "IntentRequired": true, "PreambleMode": "Compact" }
```

- `IntentRequired`: fail the call when no policy resolves.
- `PreambleMode`: `Compact` (principle ids) or `Full` (statements).

## Replacing the sample policy

`Infrastructure.AgentFramework/Governance/SampleIntentPolicy.cs` is a small, tenant-agnostic sample. Replace the `IIntentPolicyProvider` registration in `ConfigureServices.cs` with your own provider (for example one that resolves a versioned policy per tenant and jurisdiction from storage). Do not hard-code legal text in the template; supply it as versioned policy.

Prompting is guidance, not enforcement. Register the governed client as the default `IChatClient` so every call goes through it.
2 changes: 1 addition & 1 deletion src/Core.Application/Core.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Goodtocode.Agents.Playbook" Version="2.1.24" />
<PackageReference Include="Goodtocode.Agents.Playbook" Version="2.1.26" />
<PackageReference Include="Goodtocode.Mediator" Version="1.1.26" />
<PackageReference Include="Goodtocode.Validation" Version="1.1.38" />
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.25.0" />
Expand Down
13 changes: 10 additions & 3 deletions src/Core.Application/Governance/ChatGovernanceGate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@

public Goodtocode.Agents.Governance.Application.GovernedEvaluationResult Enforce(
IRlsContext userContext,
string prompt)
string prompt,
IntentPolicy? policy = null)
{
ArgumentNullException.ThrowIfNull(userContext);
ArgumentException.ThrowIfNullOrWhiteSpace(prompt);

var correlationId = Guid.NewGuid();
var policiesApplied = new List<GovernanceReference> { GovernanceReference.Parse("policy://chat/governance-v1") };
if (policy is not null)
{
policiesApplied.Add(GovernanceReference.Parse($"policy://agentic-intent/{policy.PolicyId}/{policy.Version}/{policy.ComputeLockHash()}"));
}

return enforcer.Enforce(new GovernanceEvaluationRequest
{
Governance = new EvaluationGovernanceRecord
Expand Down Expand Up @@ -45,7 +52,7 @@
},
Defensibility = new DefensibilityRecord
{
PoliciesApplied = [GovernanceReference.Parse("policy://chat/governance-v1")],
PoliciesApplied = policiesApplied,
JustificationRefs =
[
GovernanceReference.Parse("justification://chat/governance-v1")
Expand All @@ -54,7 +61,7 @@
ConfidenceScore = null
}
},
ExistingSystemInstruction = "You are a helpful AI assistant.",
Instruction = AgentInstruction.Create("You are a helpful AI assistant.", InstructionScope.Session),
RepeatabilityPromptContent = prompt,
RepeatabilityInputs = new Dictionary<string, object?>
{
Expand All @@ -63,7 +70,7 @@
});
}

public ChatGovernanceEntity CreatePersistenceRecord(

Check warning on line 73 in src/Core.Application/Governance/ChatGovernanceGate.cs

View workflow job for this annotation

GitHub Actions / Web, API and SQL CI (10.x)

Member 'CreatePersistenceRecord' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
Guid ownerId,
Guid tenantId,
Guid chatSessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,25 @@ public class SharedPlaybookGovernanceActivityRecorder<TEvidence, TFinding, TMate
/// </summary>
public IReadOnlyList<EvaluationGovernanceRecord> Records => _records;

public Task OnCollectedAsync(PlaybookIdentity identity, TEvidence evidence, string? toolName, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "collect", toolName, summarySelector.SummarizeEvidence(evidence), evidence);
public Task OnCollectedAsync(PlaybookIdentity identity, TEvidence evidence, string? toolName, IReadOnlyList<GovernedTurnRecord> governedTurns, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "collect", toolName, summarySelector.SummarizeEvidence(evidence), evidence, governedTurns);

public Task OnEvaluatedAsync(PlaybookIdentity identity, TFinding finding, string? toolName, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "evaluate", toolName, summarySelector.SummarizeFinding(finding), finding);
public Task OnEvaluatedAsync(PlaybookIdentity identity, TFinding finding, string? toolName, IReadOnlyList<GovernedTurnRecord> governedTurns, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "evaluate", toolName, summarySelector.SummarizeFinding(finding), finding, governedTurns);

public Task OnRecordedAsync(PlaybookIdentity identity, TMaterialization materialization, string? toolName, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "record", toolName, summarySelector.SummarizeMaterialization(materialization), materialization);
public Task OnRecordedAsync(PlaybookIdentity identity, TMaterialization materialization, string? toolName, IReadOnlyList<GovernedTurnRecord> governedTurns, CancellationToken cancellationToken = default) =>
RecordStageAsync(identity, "record", toolName, summarySelector.SummarizeMaterialization(materialization), materialization, governedTurns);

private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? toolName, string promptContent, object? rawInput)
private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? toolName, string promptContent, object? rawInput, IReadOnlyList<GovernedTurnRecord> governedTurns)
{
var correlationId = Guid.NewGuid();
var intent = governedTurns.FirstOrDefault(x => x.PolicyId is not null);
var policies = new List<GovernanceReference> { GovernanceReference.Parse($"policy://playbook/{identity.PlaybookKey}-v1") };
if (intent is not null)
{
policies.Add(GovernanceReference.Parse($"policy://agentic-intent/{intent.PolicyId}/{intent.PolicyVersion}/{intent.PolicyHash}"));
}

var record = new EvaluationGovernanceRecord
{
PolicyProfileVersion = $"playbook-{identity.PlaybookKey}-{identity.Version}",
Expand All @@ -70,7 +77,7 @@ private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? t
},
Defensibility = new DefensibilityRecord
{
PoliciesApplied = [GovernanceReference.Parse($"policy://playbook/{identity.PlaybookKey}-v1")],
PoliciesApplied = policies,
JustificationRefs = [GovernanceReference.Parse($"justification://playbook/{identity.PlaybookKey}-{stage}")],
ReasoningSummary = $"Playbook '{identity.PlaybookKey}' '{stage}' stage requires an attributable, traceable, and repeatable governance envelope.",
ConfidenceScore = null
Expand Down
30 changes: 26 additions & 4 deletions src/Infrastructure.AgentFramework/ChatMessageIntentRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using Goodtocode.AgentFramework.Core.Application.Governance;
using Goodtocode.AgentFramework.Core.Domain.Governance;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Intents;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
using Goodtocode.Agents.Governance.Application;
using Goodtocode.Agents.Governance.Domain;
using Goodtocode.Mediator;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
Expand All @@ -26,8 +29,14 @@ public sealed class ChatMessageIntentRouter(
IWebSearchProvider webSearchProvider,
IIntentClassifier intentClassifier,
Tools.IAgentChatContextAccessor chatContextAccessor,
IIntentPolicyProvider intentPolicyProvider,
ChatGovernanceScope governanceScope,
IAgentInstructionsComposer instructionsComposer,
ILogger<ChatMessageIntentRouter> logger) : IChatMessageRouter, IIntentRouter
{
private readonly IIntentPolicyProvider _intentPolicyProvider = intentPolicyProvider;
private readonly ChatGovernanceScope _governanceScope = governanceScope;
private readonly IAgentInstructionsComposer _instructionsComposer = instructionsComposer;
private readonly AIAgent _agent = agent;
private readonly ISender _sender = sender;
private readonly IAgentFrameworkContext _context = context;
Expand Down Expand Up @@ -67,6 +76,11 @@ public async Task<string> ResolveReplyAsync(
_chatContextAccessor.SetCurrentChatSession(chatSessionId);
try
{
var governed = await GovernTurnAsync(chatSessionId, message, cancellationToken);
var instruction = AgentInstruction.Create(
$"{governed.PromptContext.SystemInstruction}\n\n{_instructionsComposer.Compose()}",
InstructionScope.Session);
using var governedScope = _governanceScope.Begin(_rlsContext.TenantId, instruction);
var chatHistory = await BuildChatHistoryAsync(chatSessionId, message, cancellationToken);

if (mode == ChatRoutingMode.Routed)
Expand Down Expand Up @@ -151,22 +165,30 @@ private async Task<string> GuardReplyAsync(
}
}

private async Task<List<ChatMessage>> BuildChatHistoryAsync(
private async Task<GovernedEvaluationResult> GovernTurnAsync(
Guid chatSessionId,
string userMessage,
CancellationToken cancellationToken)
{
var chatSession = await _sender.Send(new GetMyChatSessionQuery { Id = chatSessionId }, cancellationToken);
var governed = _governanceGate.Enforce(_rlsContext, userMessage);
var policy = await _intentPolicyProvider.ResolveAsync(new IntentPolicyRequest(_rlsContext.TenantId), cancellationToken);
var governed = _governanceGate.Enforce(_rlsContext, userMessage, policy);
_context.Set<ChatGovernanceEntity>().Add(_governanceGate.CreatePersistenceRecord(
_rlsContext.OwnerId,
_rlsContext.TenantId,
chatSessionId,
governed));
await _context.SaveChangesAsync(cancellationToken);
return governed;
}

private async Task<List<ChatMessage>> BuildChatHistoryAsync(
Guid chatSessionId,
string userMessage,
CancellationToken cancellationToken)
{
var chatSession = await _sender.Send(new GetMyChatSessionQuery { Id = chatSessionId }, cancellationToken);
var chatHistory = new List<ChatMessage>
{
new(ChatRole.System, governed.PromptContext.SystemInstruction),
new(ChatRole.System, ToolRoutingInstructions.NoFutureTensePreamble),
BuildSelectionContextMessage(chatSessionId)
};
Expand Down
Loading
Loading