Governance Session Builder
This example instruments a three-step loan decisioning workflow using plain System.Diagnostics.Activity spans enriched with VeriProof governance attributes — no Semantic Kernel or AutoGen required.
Use this pattern when you control the LLM calls directly or when you need fine-grained per-step governance metadata.
Packages used
Veriproof.Sdk.Core— OTel exporterVeriproof.Sdk.Annotations— typed governanceActivityextensions
Prerequisites
dotnet add package Veriproof.Sdk.Core
dotnet add package Veriproof.Sdk.Annotations
dotnet add package Microsoft.Extensions.AIConfigure the exporter
// Program.cs
using OpenTelemetry.Trace;
using Veriproof.Sdk;
builder.Services.AddOpenTelemetry()
.WithTraces(traces => traces
.AddSource("LoanDecisioning")
.AddVeriproofTracing(options =>
{
options.ApiKey = Environment.GetEnvironmentVariable("VERIPROOF_API_KEY")!;
options.ApplicationId = Environment.GetEnvironmentVariable("VERIPROOF_APP_ID")!;
})
);
builder.Services.AddSingleton<LoanDecisionService>();Full example
// LoanDecisionService.cs
using System.Diagnostics;
using Microsoft.Extensions.AI;
using Veriproof.Sdk.Annotations;
using Veriproof.Sdk.Annotations.Models;
public sealed class LoanDecisionService(IChatClient chatClient)
{
private static readonly ActivitySource Tracer = new("LoanDecisioning");
public async Task<LoanDecisionResult> DecideAsync(
LoanApplication application,
CancellationToken ct = default)
{
// ── Session span ────────────────────────────────────────────────────────
using var session = Tracer.StartActivity("ai.session");
session?
.SetSessionIntent(SessionIntent.transaction)
.SetTransactionId($"loan:{application.Id}")
.SetDataSensitivity(DataSensitivity.Restricted)
.SetRegulatoryScope("ECOA")
.SetHumanReviewRequired(false);
// ── Step 1: Classify ─────────────────────────────────────────────────
string riskTier;
using (var step1 = Tracer.StartActivity("ai.step"))
{
step1?
.SetStepType(StepType.llm_call)
.SetStepName("classify_risk_tier")
.SetGroundingSource("credit-bureau-v2");
var classifyPrompt = new[]
{
new ChatMessage(ChatRole.System,
"Classify the loan applicant into LOW_RISK, MEDIUM_RISK, or HIGH_RISK " +
"based on credit score and debt-to-income ratio. Reply with only the tier."),
new ChatMessage(ChatRole.User,
$"Credit score: {application.CreditScore}. " +
$"DTI ratio: {application.DebtToIncomeRatio:P0}.")
};
var classifyResponse =
await chatClient.GetResponseAsync(classifyPrompt, ct: ct);
riskTier = classifyResponse.Message.Text?.Trim() ?? "HIGH_RISK";
step1?.SetStepOutcome(StepOutcome.success);
step1?.SetGroundingStatus(GroundingStatus.grounded);
}
// ── Step 2: Assess eligibility ────────────────────────────────────────
string eligibilitySummary;
using (var step2 = Tracer.StartActivity("ai.step"))
{
step2?
.SetStepType(StepType.llm_call)
.SetStepName("assess_eligibility")
.SetStepSequence(2);
var assessPrompt = new[]
{
new ChatMessage(ChatRole.System,
"You are a loan eligibility officer. " +
"Given the risk tier, recommend APPROVE, CONDITIONAL, or DENY " +
"and provide a brief one-sentence justification."),
new ChatMessage(ChatRole.User,
$"Risk tier: {riskTier}. " +
$"Requested amount: {application.RequestedAmount:C0}.")
};
var assessResponse =
await chatClient.GetResponseAsync(assessPrompt, ct: ct);
eligibilitySummary = assessResponse.Message.Text ?? "DENY";
step2?.SetStepOutcome(StepOutcome.success);
}
// ── Step 3: Final decision ────────────────────────────────────────────
bool approved;
using (var step3 = Tracer.StartActivity("ai.step"))
{
step3?
.SetStepType(StepType.tool_call)
.SetStepName("record_decision")
.SetStepSequence(3);
approved = eligibilitySummary.Contains("APPROVE",
StringComparison.OrdinalIgnoreCase);
step3?.SetStepOutcome(approved ? StepOutcome.success : StepOutcome.failure);
}
// ── Enrich session with outcome ───────────────────────────────────────
var riskLevel = riskTier == "HIGH_RISK" ? RiskLevel.High
: riskTier == "MEDIUM_RISK" ? RiskLevel.Medium
: RiskLevel.Low;
session?
.SetRiskLevel(riskLevel)
.SetSessionOutcome(approved ? SessionOutcome.success : SessionOutcome.failure)
.SetGovernanceDecision(
DecisionContext.WithOptions(
prompt: $"Loan application {application.Id}",
decision: approved ? "APPROVE" : "DENY",
decisionType: DecisionType.Approval,
options: new[] { "APPROVE", "CONDITIONAL", "DENY" }
)
);
if (riskLevel >= RiskLevel.Medium)
{
session?.AddRiskFactor(
RiskFactor.LowConfidence(
$"Risk tier {riskTier} — additional review recommended"));
}
return new LoanDecisionResult(
ApplicationId: application.Id,
Approved: approved,
RiskTier: riskTier,
Summary: eligibilitySummary
);
}
}
public record LoanApplication(
string Id,
int CreditScore,
double DebtToIncomeRatio,
decimal RequestedAmount
);
public record LoanDecisionResult(
string Id,
bool Approved,
string RiskTier,
string Summary
) {
public LoanDecisionResult(string ApplicationId, bool Approved, string RiskTier, string Summary)
: this(ApplicationId, Approved, RiskTier, Summary) {}
}Span hierarchy
ai.session (LoanDecisioning — loan:{Id})
│ veriproof.session.intent = "transaction"
│ veriproof.risk.level = "medium"
│ veriproof.governance.decision.type = "approval"
│
├── ai.step classify_risk_tier
│ veriproof.step.type = "llm_call"
│ veriproof.step.outcome = "success"
│
├── ai.step assess_eligibility
│ veriproof.step.type = "llm_call"
│ veriproof.step.outcome = "success"
│
└── ai.step record_decision
veriproof.step.type = "tool_call"
veriproof.step.outcome = "success"What you’ll see in VeriProof
| Field | Value |
|---|---|
| Session intent | transaction |
| Risk level | Derived from credit score |
| Governance decision | APPROVE / DENY with prompt and options |
| Step types | llm_call, llm_call, tool_call |
| Data sensitivity | Restricted |
| Regulatory scope | ECOA |
Next steps
- AutoGen multi-agent example — orchestrate multiple agents with
AutoGenOTelAdapter - Semantic Kernel example — automatic span capture with
VeriproofKernelFilter - Veriproof.Sdk.Annotations reference — full enum and extension method reference
Last updated on