Skip to Content
Examples.NETGovernance Session Builder

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 exporter
  • Veriproof.Sdk.Annotations — typed governance Activity extensions

Prerequisites

dotnet add package Veriproof.Sdk.Core dotnet add package Veriproof.Sdk.Annotations dotnet add package Microsoft.Extensions.AI

Configure 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

FieldValue
Session intenttransaction
Risk levelDerived from credit score
Governance decisionAPPROVE / DENY with prompt and options
Step typesllm_call, llm_call, tool_call
Data sensitivityRestricted
Regulatory scopeECOA

Next steps

Last updated on