
Il software di gestione dei contratti basato sull'IA automatizza l'intero ciclo di vita del contratto — dalla redazione alla revisione, negoziazione, approvazione, finalizzazione e monitoraggio degli obblighi post-firma — combinando la comprensione del linguaggio IA con l'elaborazione deterministica dei documenti all'interno di un'applicazione .NET. La differenza rispetto all'automazione contrattuale a singola attività è l'ambito del ciclo di vita: uno strumento di revisione contrattuale segnala clausole rischiose; un sistema di gestione contrattuale fa avanzare un accordo attraverso ogni fase, mantiene una traccia di audit tra le versioni e traccia gli obblighi dopo la firma.
Spire.Agent.Office è un SDK agente IA per documenti che fornisce sia il livello di comprensione del linguaggio sia il livello di elaborazione dei documenti. Questo articolo mostra come costruire un sistema di gestione del ciclo di vita contrattuale in C# che concatena cinque fasi in un'unica pipeline, con isolamento degli errori per fase, smistamento del formato per PDF e Word e un corretto monitoraggio dello stato — schemi che gli esempi a singola attività non affrontano.
Questo articolo presenta un'implementazione di riferimento di una pipeline AI-assisted per il ciclo di vita contrattuale in .NET. Un sistema CLM di produzione necessiterebbe inoltre di persistenza del flusso di lavoro, controllo di identità e accesso, integrazione con firma elettronica, archiviazione dei documenti e conservazione delle versioni, notifiche e infrastruttura di audit.
1. Il ciclo di vita del contratto: cinque fasi in cui l'IA agisce
Un ciclo di vita contrattuale non è una singola operazione su un documento. È una sequenza di fasi, ciascuna con input, output e modalità di fallimento distinte. Comprendere dove l'IA aggiunge valore in ogni fase — e dove il codice deterministico deve tenere la barra — è la base di un sistema che regge in produzione.
| Fase | Cosa succede | Ruolo dell'IA | Ruolo del codice deterministico |
|---|---|---|---|
| Redazione | Genera un contratto da un modello più dati strutturati | Interpreta la richiesta, seleziona e compila i campi del modello | Carica il modello, preserva la formattazione, salva come .docx o .pdf
|
| Revisione | Legge il contratto, segnala clausole rischiose, estrae i termini chiave | Analisi semantica del linguaggio delle clausole, valutazione del rischio | Scrive un report di revisione strutturato, applica la checklist di revisione |
| Negoziazione | Confronta versioni, traccia modifiche, unisce modifiche | Riassume le differenze, segnala modifiche sostanziali vs. di formattazione | Attiva/disattiva revisioni, confronta documenti, accetta/rifiuta revisioni |
| Approvazione | Indirizza agli stakeholder, raccoglie le approvazioni | Suggerisce approvatori in base al tipo e al valore del contratto | Applica le regole di instradamento, registra la traccia di audit, produce una copia finalizzata |
| Post-firma | Traccia obblighi, scadenze, rinnovi | Estrae obblighi e date chiave dal contratto finalizzato | Memorizza metadati strutturati, attiva promemoria, genera report |
Il ciclo di vita non è lineare nella pratica — la negoziazione ritorna alla revisione, le modifiche riavviano la redazione — ma l'architettura della pipeline gestisce questo tramite l'instradamento delle fasi anziché una sequenza fissa.
Cosa rende questo diverso dalla sola revisione contrattuale
L'automazione della revisione contrattuale — l'argomento di Revisione contrattuale IA in C# — copre una fase: leggere un accordo e segnalare problemi. Un sistema di gestione contrattuale deve:
- Concatenare le fasi con i dati che fluiscono da una all'altra (l'output della redazione diventa l'input della revisione).
-
Gestire formati documentali multipli — i modelli arrivano come
.docx, le controparti possono inviare.pdf, e le fasi con smistamento del formato gestiscono entrambi senza errori. - Mantenere lo stato tra le fasi — lo stato di revisione di un contratto, il conteggio delle versioni di negoziazione e la catena di approvazione devono persistere tra le esecuzioni della pipeline.
- Isolare i guasti per contratto — un file corrotto in un batch di 200 non deve bloccare l'intera pipeline.
Questi requisiti plasmano l'architettura di seguito.
2. Architettura del sistema per l'automazione del ciclo di vita del contratto
Un sistema di gestione del ciclo di vita contrattuale ha tre livelli. Ogni livello ha una responsabilità specifica, e i confini tra essi sono dove i guasti di produzione vengono catturati o sfuggono.

Livello 1: Interfaccia delle istruzioni
Il punto di ingresso è un'istruzione in linguaggio naturale che descrive il risultato desiderato — non i passaggi meccanici. "Redigi un accordo fornitore per Acme Corp usando il modello standard, rivedilo per termini di pagamento non standard e inoltra al Legale se il limite di responsabilità supera $500.000." Il livello delle istruzioni analizza questo in un piano di pipeline: quali fasi eseguire, in quale ordine e quali parametri necessita ciascuna fase.
Livello 2: Orchestratore delle fasi
L'orchestratore gestisce il flusso tra le fasi. Mantiene un oggetto condiviso ContractContext che trasporta i metadati del contratto, il documento corrente e i risultati delle fasi da una fase all'altra. Ogni fase riceve il contesto, esegue la sua operazione e restituisce un contesto aggiornato più un risultato di fase. L'orchestratore decide se procedere, riprovare o indirizzare a un gestore di eccezioni in base al risultato.
Livello 3: Elaborazione dei documenti
Ogni fase chiama Spire.Agent.Office per eseguire l'operazione effettiva sul documento. L'agente gestisce il ragionamento IA (comprensione dell'istruzione, estrazione delle informazioni) e il livello documentale gestisce le operazioni sui file (caricamento, modifica, salvataggio). Lo smistamento del formato viene applicato alle fasi in cui si prevedono più formati documentali: un input .pdf usa PdfDocument, mentre un input .docx usa Document.
Il contesto del contratto
L'oggetto di stato condiviso che fluisce attraverso la pipeline:
/// <summary>
/// Shared state that flows through every lifecycle stage.
/// Each stage reads from and writes to this context.
/// </summary>
public class ContractContext
{
// Identity
public string ContractId { get; set; } = string.Empty;
public string ContractType { get; set; } = string.Empty; // "NDA", "MSA", "Vendor", etc.
// Document state
public string CurrentFilePath { get; set; } = string.Empty;
public string WorkDir { get; set; } = string.Empty;
public int VersionNumber { get; set; } = 1;
// Stage results
public DraftResult? Draft { get; set; }
public ReviewResult? Review { get; set; }
public NegotiationResult? Negotiation { get; set; }
public ApprovalResult? Approval { get; set; }
public ObligationResult? Obligations { get; set; }
// Pipeline metadata
public string Status { get; set; } = PipelineStages.Pending;
public List<string> StageLog { get; set; } = new();
public string? ErrorMessage { get; set; }
}
/// <summary>
/// Stage status constants. Using named constants instead of raw strings
/// prevents the "Status never set" bug where results fall through all
/// reporting buckets and silently disappear from batch summaries.
/// </summary>
public static class PipelineStages
{
public const string Pending = "Pending";
public const string Drafted = "Drafted";
public const string Reviewed = "Reviewed";
public const string Negotiated = "Negotiated";
public const string Approved = "Approved";
public const string Executed = "Executed"; // In this sample: finalized post-approval PDF, not e-signature
public const string Monitored = "Monitored";
public const string Failed = "Failed";
public const string NeedsReview = "NeedsReview";
}
Il campo Status utilizza costanti con nome anziché stringhe grezze. Questo previene una modalità di errore comune in cui lo stato non viene mai impostato esplicitamente, causando la caduta dei documenti attraverso tutti i bucket di reporting e la loro scomparsa dai riepiloghi batch.
3. Fase 1: Redazione da modello e dati
La fase di redazione prende un modello (.docx con marcatori {{Placeholder}}) e una sorgente dati (foglio di calcolo Excel o input strutturato), quindi produce un contratto compilato. L'agente IA legge la struttura del modello e compila i segnaposto con i dati dalla sorgente — un'istruzione sostituisce il codice di mappatura dei campi richiesto da un SDK tradizionale.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
public class DraftResult
{
public string OutputPath { get; set; } = string.Empty;
public int ContractsGenerated { get; set; }
public List<string> PlaceholdersFilled { get; set; } = new();
}
public class DraftingStage
{
private readonly AIOptions _options;
public DraftingStage(AIOptions options) => _options = options;
public DraftResult Execute(ContractContext context, string templatePath, string dataSourcePath)
{
// Format dispatch: verify the template is .docx before loading
if (!templatePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException(
$"Drafting requires a .docx template. Received: {templatePath}");
string[] attachments = { dataSourcePath };
using (Document template = new Document())
{
template.LoadFromFile(templatePath);
string draftPath = Path.Combine(context.WorkDir, $"{context.ContractId}-v1-draft.docx");
string draftInstruction =
"Read the data source and fill every {{Placeholder}} field in this template " +
"with the corresponding data. Preserve the template's layout, styling, and " +
"clause numbering. Save the completed contract to: " +
$"{draftPath}. Contract type: {context.ContractType}.";
AIResult result = template.AI(_options).ExecuteInstruction(
template, draftInstruction, draftPath, attachments);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Drafting failed: {result?.ErrorMessage ?? "Unknown error"}");
// Collect the generated file: prefer the explicit output path,
// fall back to AIResult.OutputFiles (unreliable for some product types).
string? generated = File.Exists(draftPath)
? draftPath
: result.OutputFiles?.FirstOrDefault(p => File.Exists(p));
if (generated == null)
throw new InvalidOperationException(
"Drafting completed but no output file was found (neither at the requested " +
$"path '{draftPath}' nor in AIResult.OutputFiles).");
context.CurrentFilePath = generated;
context.VersionNumber = 1;
context.Status = PipelineStages.Drafted;
context.StageLog.Add($"Draft: generated contract at {generated}");
return new DraftResult
{
OutputPath = generated,
ContractsGenerated = 1,
PlaceholdersFilled = ExtractPlaceholderNames(templatePath)
};
}
}
private static List<string> ExtractPlaceholderNames(string templatePath)
{
// Quick scan of the template for {{...}} markers to report what was filled
var placeholders = new List<string>();
using (Document doc = new Document())
{
doc.LoadFromFile(templatePath);
string text = doc.GetText();
var matches = System.Text.RegularExpressions.Regex.Matches(
text, @"\{\{(\w+)\}\}");
foreach (System.Text.RegularExpressions.Match m in matches)
if (!placeholders.Contains(m.Groups[1].Value))
placeholders.Add(m.Groups[1].Value);
}
return placeholders;
}
}
Chiamate API principali
-
Document.LoadFromFile()— carica il modello.docxcon segnaposto -
template.AI(_options)— collega il processore di documenti IA -
ExecuteInstruction(doc, instruction, outputPath, attachments)— compila i segnaposto dalla sorgente dati; passa un percorso assoluto esplicito in modo che l'agente scriva il prodotto in una posizione nota - Raccolta del prodotto: controlla prima
File.Exists(outputPath);AIResult.OutputFilesnon è affidabile per alcuni tipi di prodotto e dovrebbe essere solo un fallback
Smistamento per formato
La fase convalida il formato di input prima dell'elaborazione. Un modello .pdf o un formato non supportato viene rifiutato esplicitamente con un messaggio chiaro, anziché fallire all'interno dell'agente con un'eccezione opaca. Questo schema previene una modalità di errore comune in cui lo smistamento del formato mancante causa eccezioni non gestite per input PDF in profondità nella pipeline di elaborazione.
Redigere un contratto è il caso semplice. Quando lo stesso modello deve essere compilato per dozzine di record — nuove assunzioni, fornitori, rinnovi — lo schema di istruzione scala a output batch senza modifiche; Generazione batch di contratti con Spire.Agent.Office copre sia la mail-merge sia la strada della sostituzione dei segnaposto, e confronta dove si adatta ciascuna.
Nota SDK: Quando un
outputPathesplicito viene passato aExecuteInstruction, l'SDK può anche scrivere una copiaoutput-<filename>nella stessa directory. Questo duplicato ha contenuto identico e può essere tranquillamente eliminato dopo l'elaborazione. Tutte le fasi che specificano percorsi di output sono interessate.
Il prodotto di questa fase è il documento compilato stesso — il modello di accordo fornitore con ogni segnaposto sostituito dal master workbook dei fornitori.

4. Fase 2: Revisione e analisi dei rischi
La fase di revisione legge il contratto redatto, identifica clausole rischiose o non standard e produce un report di revisione strutturato. A differenza della fase di redazione, l'input può essere .docx (bozza interna) o .pdf (carta della controparte), quindi la fase deve smistare al tipo di documento corretto.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Pdf;
public class ReviewResult
{
public string ReportPath { get; set; } = string.Empty;
public int ClausesAnalyzed { get; set; }
public int RiskFlags { get; set; }
public double RiskScore { get; set; } // ratio of flagged clauses to total (0.0 = none flagged, 1.0 = all flagged)
public List<string> FlaggedClauses { get; set; } = new();
}
public class ReviewStage
{
private readonly AIOptions _options;
// Risk threshold: contracts above this score require manual review.
// Using a named constant prevents the "threshold always passes" bug
// where a miscalculated score boundary lets everything auto-approve.
public const double AutoApproveThreshold = 0.3;
public const double ManualReviewThreshold = 0.6;
public ReviewStage(AIOptions options) => _options = options;
public ReviewResult Execute(ContractContext context)
{
string filePath = context.CurrentFilePath;
string reportPath = Path.Combine(context.WorkDir, $"{context.ContractId}-review.md");
string reviewInstruction =
"Review this contract and write a Markdown report with two sections:\n" +
"1. A table listing each major clause, its type, and a risk score (0-1).\n" +
"2. A bullet list of clauses that deviate from standard practice for a " +
$"standard {context.ContractType}. " +
"Flag any of: uncapped liability, automatic renewal without notice, " +
"broad indemnification, unilateral termination, or payment terms exceeding 60 days. " +
"Prefix each flagged clause bullet with 'FLAG:' so the parser can identify it. " +
$"Reference the source document as '{context.ContractId}', not as 'input.docx'.\n" +
$"Save the report to: {reportPath}";
AIResult result = DispatchByFormat(filePath, reviewInstruction, reportPath);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Review failed: {result?.ErrorMessage ?? "Unknown error"}");
// Parse the review report to extract structured data
string reportContent = File.ReadAllText(reportPath);
var review = ParseReviewReport(reportContent, reportPath);
// Route based on risk score (three-tier classification)
if (review.RiskScore >= ManualReviewThreshold)
context.Status = PipelineStages.NeedsReview;
else if (review.RiskScore >= AutoApproveThreshold)
{
// Middle zone: not clean enough to auto-approve, not risky enough to block
context.Status = PipelineStages.Reviewed;
context.StageLog.Add(
$"Review: risk score {review.RiskScore:F2} in warning zone " +
$"({AutoApproveThreshold}-{ManualReviewThreshold}); reviewed with warning");
}
else
context.Status = PipelineStages.Reviewed;
context.StageLog.Add(
$"Review: {review.ClausesAnalyzed} clauses, {review.RiskFlags} flags, " +
$"score {review.RiskScore:F2}, status={context.Status}");
return review;
}
/// <summary>
/// Dispatch to the correct document type based on file extension.
/// This prevents the "PDF throws exception" bug where a stage
/// only handles .docx and fails on counterparty PDFs.
/// </summary>
private AIResult DispatchByFormat(string filePath, string instruction, string outputPath)
{
string ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".docx" or ".doc" => ProcessWord(filePath, instruction, outputPath),
".pdf" => ProcessPdf(filePath, instruction, outputPath),
_ => throw new NotSupportedException(
$"Review stage does not support format: {ext}")
};
}
private AIResult ProcessWord(string filePath, string instruction, string outputPath)
{
using (Document doc = new Document())
{
doc.LoadFromFile(filePath);
return doc.AI(_options).ExecuteInstruction(
doc, instruction, outputPath, Array.Empty<string>());
}
}
private AIResult ProcessPdf(string filePath, string instruction, string outputPath)
{
using (PdfDocument pdf = new PdfDocument())
{
pdf.LoadFromFile(filePath);
return pdf.AI(_options).ExecuteInstruction(
pdf, instruction, outputPath, Array.Empty<string>());
}
}
private static ReviewResult ParseReviewReport(string markdown, string reportPath)
{
var result = new ReviewResult { ReportPath = reportPath };
// Count table rows as clauses analyzed
var tableLines = markdown.Split('\n')
.Where(l => l.StartsWith("|") && !l.StartsWith("|---") && !l.StartsWith("| --"))
.Skip(1); // skip header
result.ClausesAnalyzed = tableLines.Count();
// Count bullet points prefixed with "FLAG:" as risk flags
var flagLines = markdown.Split('\n')
.Select(l => l.TrimStart())
.Where(l => l.StartsWith("-") || l.StartsWith("*"))
.Where(l => l.Substring(1).TrimStart()
.StartsWith("FLAG:", StringComparison.OrdinalIgnoreCase));
result.FlaggedClauses = flagLines.Select(l => l.Trim()).ToList();
result.RiskFlags = result.FlaggedClauses.Count;
// Risk score: ratio of flagged clauses to total, clamped to [0, 1]
result.RiskScore = result.ClausesAnalyzed > 0
? Math.Min(1.0, (double)result.RiskFlags / result.ClausesAnalyzed)
: 0.0;
return result;
}
}
Il metodo DispatchByFormat è l'aggiunta critica. Nelle implementazioni precedenti della pipeline, una fase che gestiva solo .docx generava un'InvalidOperationException quando una controparte inviava un .pdf. L'espressione switch smista al tipo di documento corretto prima che l'agente venga eseguito, quindi entrambi i formati sono input di prima classe.
Le soglie di rischio (AutoApproveThreshold, ManualReviewThreshold) sono costanti con nome con semantica documentata. Il divario tra 0.3 e 0.6 crea una zona esplicita di "necessita revisione" — contratti che non sono né abbastanza puliti per l'approvazione automatica né abbastanza rischiosi da bloccare. Questo previene il bug della logica morta della soglia in cui ogni contratto supera lo stesso controllo.
I risultati della revisione vengono persistiti come report anziché restituiti in memoria, così l'audit a livello di clausola sopravvive indipendentemente dall'esecuzione che lo ha prodotto.

5. Fase 3: Negoziazione e controllo delle versioni
La negoziazione è dove i contratti cambiano mani. La controparte contrassegna il documento — clausole con modifiche, termini rettificati, condizioni aggiunte. Il sistema deve confrontare le versioni, tracciare le modifiche e aiutare il team a decidere cosa accettare.
Questa fase utilizza due capacità dal livello documentale Spire.Doc che vanno oltre l'ExecuteInstruction dell'agente IA:
- Traccia modifiche — abilita il monitoraggio delle revisioni in modo che ogni modifica sia visibile e attribuibile
- Confronto documenti — confronta due versioni e genera un documento di differenze
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Doc.Documents;
using System.Text.RegularExpressions;
public class NegotiationResult
{
public string ComparisonPath { get; set; } = string.Empty;
public int ChangesDetected { get; set; }
public int SubstantiveChanges { get; set; } // changes to clause text, not formatting
public int FormattingChanges { get; set; }
public string NegotiatedFilePath { get; set; } = string.Empty;
}
public class NegotiationStage
{
private readonly AIOptions _options;
public NegotiationStage(AIOptions options) => _options = options;
/// <summary>
/// Compare the current version with a counterparty's revised version.
/// Produces a diff document with all changes marked.
/// </summary>
public NegotiationResult CompareVersions(
ContractContext context, string counterpartyFilePath)
{
string comparisonPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-comparison.docx");
// Load both versions
using (Document ourVersion = new Document())
using (Document theirVersion = new Document())
{
ourVersion.LoadFromFile(context.CurrentFilePath);
theirVersion.LoadFromFile(counterpartyFilePath);
// Compare: marks every difference in ourVersion as a tracked change
ourVersion.Compare(theirVersion, "Contract Management System");
// Save the comparison document
ourVersion.SaveToFile(comparisonPath, FileFormat.Docx2013);
}
// Use the AI agent to analyze the comparison and categorize changes
string analysisPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-negotiation-analysis.md");
using (Document comparison = new Document())
{
comparison.LoadFromFile(comparisonPath);
string analyzeInstruction =
"Analyze this document comparison and write a Markdown report:\n" +
"1. On the first line, write 'Total changes: <N>' where N is the total count " +
"of categorized changes (substantive + formatting).\n" +
"2. List each change as a bullet line prefixed with 'SUBSTANTIVE:' or 'FORMATTING:' " +
"followed by the clause affected and the nature of the change.\n" +
"3. SUBSTANTIVE changes: clause text, numbers, dates. " +
"FORMATTING changes: style, spacing, font.\n" +
$"Save to: {analysisPath}";
AIResult result = comparison.AI(_options).ExecuteInstruction(
comparison, analyzeInstruction, analysisPath, Array.Empty<string>());
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Negotiation analysis failed: {result?.ErrorMessage}");
}
var negotiation = ParseNegotiationReport(analysisPath);
negotiation.ComparisonPath = comparisonPath;
negotiation.NegotiatedFilePath = counterpartyFilePath;
context.VersionNumber++;
context.CurrentFilePath = counterpartyFilePath;
context.Status = PipelineStages.Negotiated;
context.StageLog.Add(
$"Negotiation: {negotiation.ChangesDetected} changes " +
$"({negotiation.SubstantiveChanges} substantive, " +
$"{negotiation.FormattingChanges} formatting), v{context.VersionNumber}");
return negotiation;
}
/// <summary>
/// Enable track changes on a contract before sending it for negotiation.
/// This ensures every edit by the counterparty is visible and attributable.
/// </summary>
public string PrepareForRedlining(ContractContext context)
{
string redlineReadyPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-redline.docx");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
// Enable track changes so all edits are recorded as revisions
doc.TrackChanges = true;
doc.SaveToFile(redlineReadyPath, FileFormat.Docx2013);
}
context.StageLog.Add("Negotiation: track changes enabled for redlining");
return redlineReadyPath;
}
/// <summary>
/// Accept all tracked changes to produce a clean final version.
/// Optional post-review operation: not automatically invoked by the pipeline
/// because acceptance should occur only after human review of the changes.
/// </summary>
public string AcceptAllChanges(ContractContext context)
{
string cleanPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-final.docx");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
doc.AcceptChanges();
doc.SaveToFile(cleanPath, FileFormat.Docx2013);
}
context.CurrentFilePath = cleanPath;
context.StageLog.Add("Negotiation: all changes accepted, clean version produced");
return cleanPath;
}
private static NegotiationResult ParseNegotiationReport(string reportPath)
{
string content = File.ReadAllText(reportPath);
var result = new NegotiationResult();
// Parse total changes from the report (try multiple patterns for robustness)
var totalMatch = Regex.Match(content, @"Total changes:\s*(\d+)", RegexOptions.IgnoreCase);
if (!totalMatch.Success)
totalMatch = Regex.Match(content, @"(\d+)\s+(?:tracked\s+)?changes", RegexOptions.IgnoreCase);
if (totalMatch.Success)
result.ChangesDetected = int.Parse(totalMatch.Groups[1].Value);
// Count changes by prefix: SUBSTANTIVE: or FORMATTING:
result.SubstantiveChanges = CountByPrefix(content, "SUBSTANTIVE:");
result.FormattingChanges = CountByPrefix(content, "FORMATTING:");
return result;
}
private static int CountByPrefix(string content, string prefix)
{
return content.Split('\n')
.Select(l => l.TrimStart())
.Where(l => l.StartsWith("-") || l.StartsWith("*"))
.Count(l => l.Substring(1).TrimStart()
.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
}
}
Chiamate API principali
-
Document.Compare(otherDoc, authorName)— genera un documento di differenze con tutte le modifiche contrassegnate come revisioni monitorate (dalla demo di confronto Spire.Doc) -
Document.TrackChanges = true— abilita il monitoraggio delle revisioni in modo che ogni modifica sia visibile e attribuibile -
Document.AcceptChanges()— accetta tutte le modifiche monitorate per produrre una versione finale pulita -
doc.AI(_options).ExecuteInstruction(...)— analizza il confronto e categorizza le modifiche
La fase di negoziazione combina operazioni documentali deterministiche (confronta, traccia modifiche, accetta) con l'analisi IA (categorizzazione delle modifiche come sostanziali o di formattazione). Le operazioni deterministiche provengono dal livello Spire.Doc — le stesse API disponibili nella directory di riferimento del codice — mentre l'agente IA gestisce la categorizzazione semantica che altrimenti richiederebbe revisione manuale.
L'esempio confronta le versioni e tiene traccia del percorso del file corrente; un sistema di produzione normalmente persisterebbe ogni versione separatamente e registrerebbe esplicitamente quale revisione è stata accettata. AcceptAllChanges() è fornito come operazione di finalizzazione opzionale dopo la negoziazione, ma non viene automaticamente invocato dalla pipeline di esempio perché l'accettazione dovrebbe avvenire solo dopo la revisione umana.
Ciò che Document.Compare restituisce è un documento, non un riepilogo: le revisioni monitorate di seguito sono le modifiche che un revisore altrimenti assemblebbe a mano.

6. Fase 4: Approvazione e finalizzazione
La fase di approvazione indirizza il contratto agli stakeholder giusti in base al tipo e al valore del contratto, registra le loro approvazioni e produce una copia PDF finalizzata dopo che tutte le approvazioni richieste sono registrate. La logica di instradamento è deterministica — usa regole aziendali, non IA — ma l'agente assiste suggerendo approvatori e generando il riepilogo di approvazione.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
public class ApprovalResult
{
public List<ApprovalRecord> Approvals { get; set; } = new();
public bool AllApproved { get; set; }
public string ExecutedFilePath { get; set; } = string.Empty; // Finalized PDF path, not a signed contract
public DateTime? ExecutionDate { get; set; } // Finalization timestamp, not a signing date
}
public class ApprovalRecord
{
public string Approver { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public bool Approved { get; set; }
public DateTime Timestamp { get; set; }
public string? Comments { get; set; }
}
public class ApprovalStage
{
private readonly AIOptions _options;
public ApprovalStage(AIOptions options) => _options = options;
/// <summary>
/// Route the contract for approval based on type and value.
/// Routing rules are deterministic business logic, not AI.
/// </summary>
public ApprovalResult Execute(
ContractContext context, double contractValue,
Dictionary<string, bool>? approvalDecisions = null)
{
var routingPlan = DetermineApprovers(context.ContractType, contractValue);
var result = new ApprovalResult();
// Generate an approval summary for each approver
string summaryPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-approval-summary.md");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
string summaryInstruction =
"Write a one-page approval summary for this contract: parties, value, " +
"key terms, risk flags from review, and changes from negotiation. " +
$"Save to: {summaryPath}";
AIResult aiResult = doc.AI(_options).ExecuteInstruction(
doc, summaryInstruction, summaryPath, Array.Empty<string>());
if (aiResult == null || !aiResult.Success)
throw new InvalidOperationException(
$"Approval summary generation failed: {aiResult?.ErrorMessage}");
}
// In a real system, this would integrate with an approval workflow API.
// approvalDecisions maps approver name → approved/rejected. A missing key
// means no decision was recorded — which is not the same as a rejection.
int approvedCount = 0, rejectedCount = 0, pendingCount = 0;
foreach (var approver in routingPlan)
{
bool decision = false;
bool hasDecision = approvalDecisions != null
&& approvalDecisions.TryGetValue(approver.Name, out decision);
bool approved = hasDecision && decision;
// Three states, all distinguishable in the audit record
string comment = !hasDecision ? "Pending approval"
: approved ? "Approved by approver"
: "Rejected by approver";
if (!hasDecision) pendingCount++;
else if (approved) approvedCount++;
else rejectedCount++;
result.Approvals.Add(new ApprovalRecord
{
Approver = approver.Name,
Role = approver.Role,
Approved = approved,
Timestamp = DateTime.UtcNow,
Comments = comment
});
}
result.AllApproved = result.Approvals.All(a => a.Approved);
if (result.AllApproved)
{
// Produce the finalized copy as PDF
string executedPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-executed.pdf");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
doc.SaveToFile(executedPath, FileFormat.PDF);
result.ExecutedFilePath = executedPath;
result.ExecutionDate = DateTime.UtcNow;
}
context.CurrentFilePath = result.ExecutedFilePath;
context.Status = PipelineStages.Executed;
}
else
{
context.Status = PipelineStages.NeedsReview;
}
context.StageLog.Add(
$"Approval: {result.Approvals.Count} approvers, " +
$"approved={approvedCount}, rejected={rejectedCount}, " +
$"pending={pendingCount}, all approved={result.AllApproved}");
return result;
}
/// <summary>
/// Deterministic routing rules. These are business logic, not AI.
/// </summary>
private static List<(string Name, string Role)> DetermineApprovers(
string contractType, double value)
{
var approvers = new List<(string, string)>();
// All contracts need Legal sign-off (example business rule)
approvers.Add(("Legal Team", "Legal Counsel"));
// Contracts over $100K need VP approval (example business rule)
if (value > 100_000)
approvers.Add(("VP Operations", "VP"));
// Contracts over $500K need CFO approval (example business rule)
if (value > 500_000)
approvers.Add(("CFO", "CFO"));
// Vendor contracts need Procurement sign-off
if (contractType.Equals("Vendor", StringComparison.OrdinalIgnoreCase))
approvers.Add(("Procurement", "Procurement Manager"));
return approvers;
}
}
Le regole di instradamento in DetermineApprovers sono intenzionalmente deterministiche. L'IA suggerisce e riassume; non decide chi firma un contratto. La catena di approvazione è una regola aziendale che deve essere verificabile e coerente — esattamente il tipo di decisione che appartiene al codice, non a un modello linguistico.
Nota: Lo stato
Executedin questo esempio indica il PDF finale prodotto dopo l'approvazione — non un contratto firmato. La firma elettronica effettiva — buste di firma, verifica dell'identità del firmatario, incorporamento dei campi firma — dovrebbe essere gestita da un flusso di lavoro di firma elettronica dedicato integrato separatamente.
Una volta che la decisione di ogni approvatore è registrata, la fase scrive il contratto finalizzato come PDF — e quel PDF, non la bozza di origine, è ciò che consuma la Fase 5.

7. Fase 5: Monitoraggio post-firma
In un flusso di lavoro di produzione, il monitoraggio post-firma inizia dopo che il contratto è stato firmato elettronicamente o formalmente in altro modo. In questa implementazione di riferimento, il PDF finalizzato prodotto dalla Fase 4 viene utilizzato come input per il monitoraggio. Il monitoraggio post-firma estrae obblighi, date chiave e termini di rinnovo dal contratto, quindi li memorizza come metadati strutturati che i sistemi a valle usano per promemoria e reportistica.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Pdf;
public class ObligationResult
{
public List<Obligation> Obligations { get; set; } = new();
public List<KeyDate> KeyDates { get; set; } = new();
public bool AutoRenewal { get; set; }
public DateTime? RenewalDate { get; set; }
public string MetadataPath { get; set; } = string.Empty;
}
public class Obligation
{
public string Description { get; set; } = string.Empty;
public string Party { get; set; } = string.Empty; // who must fulfill
public string Frequency { get; set; } = string.Empty; // "monthly", "annual", "one-time"
public DateTime? DueDate { get; set; }
public string DueDateRaw { get; set; } = string.Empty; // original deadline text from the contract
}
public class KeyDate
{
public string Description { get; set; } = string.Empty;
public DateTime Date { get; set; }
public string Type { get; set; } = string.Empty; // "renewal", "termination", "milestone"
}
public class MonitoringStage
{
private readonly AIOptions _options;
public MonitoringStage(AIOptions options) => _options = options;
public ObligationResult Execute(ContractContext context)
{
string metadataPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-obligations.json");
string extractInstruction =
"Extract all post-signature obligations and key dates from this contract. " +
"Write a JSON file with:\n" +
"1. \"obligations\": array of {description, party, frequency, dueDate, dueDateRaw} " +
"where dueDate is an ISO-8601 date (YYYY-MM-DD). If the obligation has a relative " +
"deadline (e.g., 'Within 90 days of receipt'), convert it to an absolute date based " +
"on the contract effective date. Use null only for ongoing obligations with no " +
"fixed deadline. Always put the original deadline text in dueDateRaw.\n" +
"2. \"keyDates\": array of {description, date, type} where date is ISO-8601 " +
"(YYYY-MM-DD) and type is 'renewal', 'termination', or 'milestone'\n" +
"3. \"autoRenewal\": boolean\n" +
"4. \"renewalDate\": ISO-8601 date (YYYY-MM-DD) or null\n" +
$"Save to: {metadataPath}";
// Dispatch by format — executed contracts may be PDF or DOCX
AIResult result = DispatchExtraction(
context.CurrentFilePath, extractInstruction, metadataPath, _options);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Obligation extraction failed: {result?.ErrorMessage}");
var obligations = ParseObligationMetadata(metadataPath);
obligations.MetadataPath = metadataPath;
context.Obligations = obligations;
context.Status = PipelineStages.Monitored;
// Diagnostics: bucket every obligation by how its deadline came out, so that a
// field the agent writes but the parser fails to read cannot pass unnoticed.
int datesParsed = obligations.Obligations.Count(o => o.DueDate != null);
int datesUnparsed = obligations.Obligations.Count(
o => o.DueDate == null && !string.IsNullOrEmpty(o.DueDateRaw));
int datesOngoing = obligations.Obligations.Count(
o => o.DueDate == null && string.IsNullOrEmpty(o.DueDateRaw));
int datesNoSourceText = obligations.Obligations.Count(
o => o.DueDate != null && string.IsNullOrEmpty(o.DueDateRaw));
context.StageLog.Add(
$"Monitoring: {obligations.Obligations.Count} obligations, " +
$"{obligations.KeyDates.Count} key dates, " +
$"auto-renewal={obligations.AutoRenewal}, " +
$"dates parsed={datesParsed}, unparsed={datesUnparsed}, ongoing={datesOngoing}, " +
$"parsed-without-source-text={datesNoSourceText}");
return obligations;
}
private static AIResult DispatchExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
string ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".docx" or ".doc" => ProcessWordExtraction(filePath, instruction, outputPath, options),
".pdf" => ProcessPdfExtraction(filePath, instruction, outputPath, options),
_ => throw new NotSupportedException(
$"Obligation extraction does not support format: {ext}")
};
}
private static AIResult ProcessWordExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
using (Document doc = new Document())
{
doc.LoadFromFile(filePath);
return doc.AI(options).ExecuteInstruction(
doc, instruction, outputPath, Array.Empty<string>());
}
}
private static AIResult ProcessPdfExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
using (PdfDocument pdf = new PdfDocument())
{
pdf.LoadFromFile(filePath);
return pdf.AI(options).ExecuteInstruction(
pdf, instruction, outputPath, Array.Empty<string>());
}
}
private static ObligationResult ParseObligationMetadata(string jsonPath)
{
string json = File.ReadAllText(jsonPath);
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
var result = new ObligationResult();
if (TryGetPropertyIgnoreCase(root, "obligations", out var obs))
foreach (var ob in obs.EnumerateArray())
{
string dueDateRaw = TryGetPropertyIgnoreCase(ob, "dueDateRaw", out var dr)
&& dr.ValueKind == System.Text.Json.JsonValueKind.String
? dr.GetString() ?? "" : "";
result.Obligations.Add(new Obligation
{
Description = TryGetPropertyIgnoreCase(ob, "description", out var d) ? d.GetString() ?? "" : "",
Party = TryGetPropertyIgnoreCase(ob, "party", out var p) ? p.GetString() ?? "" : "",
Frequency = TryGetPropertyIgnoreCase(ob, "frequency", out var f) ? f.GetString() ?? "" : "",
DueDate = TryGetDate(TryGetPropertyIgnoreCase(ob, "dueDate", out var dd2), dd2),
DueDateRaw = dueDateRaw
});
}
if (TryGetPropertyIgnoreCase(root, "keyDates", out var kds))
foreach (var kd in kds.EnumerateArray())
result.KeyDates.Add(new KeyDate
{
Description = TryGetPropertyIgnoreCase(kd, "description", out var d) ? d.GetString() ?? "" : "",
Date = TryGetDate(TryGetPropertyIgnoreCase(kd, "date", out var dt), dt) ?? default,
Type = TryGetPropertyIgnoreCase(kd, "type", out var t) ? t.GetString() ?? "" : ""
});
result.AutoRenewal = TryGetBool(TryGetPropertyIgnoreCase(root, "autoRenewal", out var ar), ar);
result.RenewalDate = TryGetDate(TryGetPropertyIgnoreCase(root, "renewalDate", out var rd), rd);
return result;
}
/// <summary>
/// Case-insensitive property lookup. Prevents silent data loss when
/// the AI agent outputs PascalCase or snake_case instead of camelCase.
/// </summary>
private static bool TryGetPropertyIgnoreCase(
System.Text.Json.JsonElement element, string name,
out System.Text.Json.JsonElement value)
{
foreach (var prop in element.EnumerateObject())
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
value = prop.Value;
return true;
}
}
value = default;
return false;
}
/// <summary>
/// Safe date parsing: handles null, non-string values, and invalid formats
/// without throwing. Returns null on any parse failure.
/// </summary>
private static DateTime? TryGetDate(bool exists, System.Text.Json.JsonElement element)
{
if (!exists || element.ValueKind != System.Text.Json.JsonValueKind.String)
return null;
string? s = element.GetString();
return DateTime.TryParse(s, out var date) ? date : null;
}
/// <summary>
/// Safe boolean parsing: handles string "true"/"false" and actual booleans
/// without throwing on type mismatch.
/// </summary>
private static bool TryGetBool(bool exists, System.Text.Json.JsonElement element)
{
if (!exists) return false;
if (element.ValueKind == System.Text.Json.JsonValueKind.True) return true;
if (element.ValueKind == System.Text.Json.JsonValueKind.False) return false;
if (element.ValueKind == System.Text.Json.JsonValueKind.String)
return bool.TryParse(element.GetString(), out var b) && b;
return false;
}
}
I metadati degli obblighi sono memorizzati come JSON, non come documento. Questo li rende interrogabili dai sistemi a valle — una dashboard dei rinnovi può scansionare tutti i contratti per autoRenewal == true e renewalDate < DateTime.Now.AddDays(90) senza aprire un singolo documento. Gli obblighi con valori dueDate strutturati possono essere tracciati automaticamente; quelli in cui la scadenza non era convertibile in una data assoluta conservano il testo originale in dueDateRaw per la revisione manuale. L'agente IA estrae le informazioni; il livello deterministico le memorizza in un formato che i sistemi aziendali possono consumare.
Lo schema di estrazione è lo stesso utilizzato per le fatture in Automazione dell'elaborazione delle fatture con un agente IA in .NET: estrai campi strutturati da un documento in entrata, convalidali rispetto a regole deterministiche e indirizza i valori incerti per la revisione. I contratti differiscono principalmente in ciò che accade dopo — gli obblighi diventano record a lunga vita che sopravvivono alla transazione, mentre i campi delle fatture vengono consumati una volta.
I dati estratti sono memorizzati come JSON anziché come prosa, quindi ogni obbligo e data chiave porta la formulazione originale del contratto accanto al valore che il parser ha derivato da essa.

8. Orchestrazione dell'intera pipeline
L'orchestratore concatena le fasi, passando il ContractContext da una all'altra. Le sue due responsabilità critiche sono l'isolamento degli errori per contratto (un contratto fallito non deve bloccare il batch) e la conservazione dello stato (ogni contratto deve finire in esattamente un bucket di reporting).
using Spire.Agent.Office.AI;
public class PipelineResult
{
public string ContractId { get; set; } = string.Empty;
public string Status { get; set; } = PipelineStages.Pending;
public List<string> StageLog { get; set; } = new();
public string? ErrorMessage { get; set; }
}
public class BatchResult
{
public int Total { get; set; }
public int Successful { get; set; }
public int Flagged { get; set; } // needs review
public int Errored { get; set; }
public List<PipelineResult> Results { get; set; } = new();
}
public class ContractLifecyclePipeline
{
private readonly DraftingStage _drafting;
private readonly ReviewStage _review;
private readonly NegotiationStage _negotiation;
private readonly ApprovalStage _approval;
private readonly MonitoringStage _monitoring;
public ContractLifecyclePipeline(AIOptions options)
{
_drafting = new DraftingStage(options);
_review = new ReviewStage(options);
_negotiation = new NegotiationStage(options);
_approval = new ApprovalStage(options);
_monitoring = new MonitoringStage(options);
}
/// <summary>
/// Run a single contract through the full lifecycle.
/// Each stage is wrapped in its own try-catch so that a failure
/// in one stage produces a clear error without corrupting the
/// context for subsequent contracts in the batch.
/// </summary>
public PipelineResult RunSingle(
ContractContext context,
string templatePath,
string dataSourcePath,
double contractValue,
string? counterpartyFilePath = null,
Dictionary<string, bool>? approvalDecisions = null)
{
try
{
// Stage 1: Drafting
try
{
context.Draft = _drafting.Execute(context, templatePath, dataSourcePath);
}
catch (Exception ex)
{
return Fail(context, "Drafting", ex);
}
// Stage 2: Review
try
{
context.Review = _review.Execute(context);
}
catch (Exception ex)
{
return Fail(context, "Review", ex);
}
// If review flagged for manual review, stop here
if (context.Status == PipelineStages.NeedsReview)
return Flag(context, "Review flagged for manual review");
// Stage 3: Negotiation (only when a counterparty version exists)
if (counterpartyFilePath != null)
{
try
{
context.Negotiation = _negotiation.CompareVersions(context, counterpartyFilePath);
}
catch (Exception ex)
{
return Fail(context, "Negotiation", ex);
}
}
// Stage 4: Approval and Finalization
try
{
context.Approval = _approval.Execute(context, contractValue, approvalDecisions);
}
catch (Exception ex)
{
return Fail(context, "Approval", ex);
}
if (context.Status == PipelineStages.NeedsReview)
return Flag(context, "Approval incomplete");
// Stage 5: Post-signature monitoring
try
{
context.Obligations = _monitoring.Execute(context);
}
catch (Exception ex)
{
return Fail(context, "Monitoring", ex);
}
return new PipelineResult
{
ContractId = context.ContractId,
Status = context.Status,
StageLog = context.StageLog
};
}
catch (Exception ex)
{
return Fail(context, "Pipeline", ex);
}
}
/// <summary>
/// Process a batch of contracts with per-file error isolation.
/// One corrupt file does not halt the batch.
/// Approval decisions are keyed by contract id; a contract with no
/// entry keeps its approvers pending and stops at NeedsReview.
/// </summary>
public BatchResult RunBatch(
List<(ContractContext context, string template, string data, double value)> contracts,
Dictionary<string, Dictionary<string, bool>>? approvalDecisions = null)
{
var results = new List<PipelineResult>();
foreach (var (context, template, data, value) in contracts)
{
// Per-contract try-catch: one failure does not stop the batch
try
{
Dictionary<string, bool>? decisions = null;
if (approvalDecisions != null &&
approvalDecisions.TryGetValue(context.ContractId, out var d))
decisions = d;
results.Add(RunSingle(context, template, data, value, null, decisions));
}
catch (Exception ex)
{
results.Add(Fail(context, "Batch", ex));
}
}
// Status conservation: every result must land in exactly one bucket.
// Using the remainder method ensures that any result with an unexpected
// status lands in "Flagged" (conservative) rather than vanishing.
int successful = results.Count(r => r.Status == PipelineStages.Monitored
|| r.Status == PipelineStages.Executed);
int errored = results.Count(r => r.Status == PipelineStages.Failed);
return new BatchResult
{
Total = results.Count,
Successful = successful,
Flagged = results.Count - successful - errored, // remainder → conservative
Errored = errored,
Results = results
};
}
private static PipelineResult Fail(ContractContext context, string stage, Exception ex)
{
context.Status = PipelineStages.Failed;
context.ErrorMessage = $"{stage}: {ex.Message}";
context.StageLog.Add($"ERROR at {stage}: {ex.Message}");
return new PipelineResult
{
ContractId = context.ContractId,
Status = PipelineStages.Failed,
StageLog = context.StageLog,
ErrorMessage = context.ErrorMessage
};
}
private static PipelineResult Flag(ContractContext context, string reason)
{
context.StageLog.Add($"FLAGGED: {reason}");
return new PipelineResult
{
ContractId = context.ContractId,
Status = context.Status,
StageLog = context.StageLog
};
}
}
Isolamento degli errori per fase
Ogni fase è racchiusa nel proprio blocco try-catch. Se la fase di revisione fallisce perché l'agente IA non riesce a interpretare una clausola particolarmente complessa, la pipeline registra il fallimento e si ferma — ma il ContractContext per il contratto successivo nel batch rimane intatto. Questo previene una modalità di errore comune in cui un singolo file corrotto in un batch di 200 blocca l'intera pipeline senza risultati parziali.
Conservazione dello stato
Il riepilogo del batch utilizza il metodo del resto per calcolare il conteggio Flagged: Flagged = Total - Successful - Errored. Questa è una proprietà strutturale — qualsiasi risultato che non sia esplicitamente riuscito o in errore finisce in "flagged" (instradamento conservativo alla revisione manuale). Questo previene il bug in cui risultati con valori di stato imprevisti scompaiono silenziosamente da tutti e tre i bucket di reporting.
Esecuzione della pipeline
// Configure the agent
AIOptions options = new AIOptions
{
WorkDir = @"C:\contract-mgmt\work",
SpireToken = Environment.GetEnvironmentVariable("SPIRE_TOKEN")!
};
// Create the pipeline
var pipeline = new ContractLifecyclePipeline(options);
// Prepare a batch of contracts
var batch = new List<(ContractContext, string, string, double)>
{
new(new ContractContext
{
ContractId = "CTR-2026-001",
ContractType = "Vendor",
WorkDir = @"C:\contract-mgmt\work\CTR-2026-001"
},
@"C:\templates\vendor-agreement.docx",
@"C:\data\vendors-q1.xlsx",
250_000)
};
// Run the batch. Approval decisions are supplied per contract id —
// a contract with no recorded decision stays in NeedsReview and
// never reaches Stage 5.
BatchResult result = pipeline.RunBatch(batch, new Dictionary<string, Dictionary<string, bool>>
{
["CTR-2026-001"] = new Dictionary<string, bool>
{
["Legal Team"] = true,
["VP Operations"] = true,
["Procurement"] = true
}
});
Console.WriteLine($"Total: {result.Total}");
Console.WriteLine($"Successful: {result.Successful}");
Console.WriteLine($"Flagged: {result.Flagged}");
Console.WriteLine($"Errored: {result.Errored}");
foreach (var r in result.Results)
{
Console.WriteLine($" {r.ContractId}: {r.Status}");
foreach (var log in r.StageLog)
Console.WriteLine($" {log}");
}
Le decisioni di approvazione vengono fornite alla chiamata batch anziché essere dedotte dalla pipeline: un contratto senza decisione registrata mantiene i suoi approvatori in sospeso, finisce in Flagged e si ferma prima della Fase 5 — il risultato previsto per un accordo che nessuno ha ancora approvato.
Una singola esecuzione su una manciata di contratti è il caso semplice. Metti le stesse fasi dietro una coda di documenti e le domande di orchestrazione cambiano: limiti di concorrenza, isolamento degli errori per documento, politica di retry e aggregazione in un report batch. Elaborazione intelligente dei documenti in .NET: creazione di pipeline IDP approfondisce questi a livello di pipeline, e la sezione batch di questo articolo è un'istanza specifica per i contratti dello stesso schema.
9. Dove finisce l'IA e inizia la governance
L'architettura sopra assegna responsabilità specifiche all'agente IA e responsabilità specifiche al codice deterministico. Il confine non è arbitrario — segue un principio: l'IA interpreta e suggerisce; il codice deterministico decide e registra.
Le valutazioni del rischio generate dall'IA e gli obblighi estratti dovrebbero essere convalidati da revisori qualificati prima di essere utilizzati per decisioni legali o commerciali.
| Responsabilità | Gestito da | Perché |
|---|---|---|
| Comprensione di un'istruzione in linguaggio naturale | Agente IA | Lo scopo stesso dei modelli linguistici |
| Estrazione di informazioni da testo non strutturato | Agente IA | Richiede comprensione semantica |
| Classificazione delle modifiche come sostanziali o di formattazione | Agente IA | Richiede giudizio sul significato |
| Suggerimento degli approvatori in base al tipo di contratto | Regole aziendali | Verificabilità e coerenza |
| Determinazione di chi deve firmare | Regole aziendali | Verificabilità e coerenza |
| Registrazione della catena di approvazione | Codice deterministico | La traccia di audit legale deve essere a prova di manomissione |
| Impostazione delle soglie di rischio | Regole aziendali | La politica di rischio è una decisione di governance |
| Memorizzazione dei metadati degli obblighi | Codice deterministico | I sistemi a valle necessitano di una struttura affidabile |
| Attivazione dei promemoria di rinnovo | Codice deterministico | Deve scattare secondo pianificazione, non su inferenza |
Le soglie di rischio nella fase di revisione (AutoApproveThreshold = 0.3, ManualReviewThreshold = 0.6) sono impostate dal business, non dall'IA. L'applicazione deriva un rapporto di revisione dal numero di clausole segnalate; il business definisce le soglie. Questa separazione è ciò che rende il sistema difendibile in un audit — ogni decisione automatizzata risale a una regola definita dall'uomo, non a un'inferenza del modello.
Integrazione con i sistemi esistenti
Un sistema di ciclo di vita contrattuale non esiste in isolamento. I metadati post-firma (obblighi, date chiave, termini di rinnovo) sono progettati per essere consumati dai sistemi a valle:
- ERP / Finanza: milestone di pagamento e riconciliazione delle fatture
- CRM: avvisi di rinnovo per i responsabili account
- Approvvigionamento: monitoraggio delle prestazioni dei fornitori rispetto agli SLA contrattuali
- Legale: monitoraggio della conformità e preparazione degli audit
Il formato dei metadati JSON della Fase 5 rende questa integrazione semplice — i sistemi a valle interrogano i dati strutturati senza dover analizzare i documenti contrattuali.
10. FAQ
In cosa è diverso dalla revisione contrattuale con IA?
La revisione contrattuale è una fase del ciclo di vita — leggere un accordo e segnalare clausole rischiose. Un sistema di gestione contrattuale copre l'intero ciclo di vita: redazione, revisione, negoziazione, approvazione, finalizzazione e monitoraggio post-firma. L'articolo esistente Revisione contrattuale IA in C# copre in profondità la fase di revisione; questo articolo copre l'intera pipeline in cui la revisione si inserisce.
La pipeline può gestire input sia PDF che Word?
Sì, nelle fasi che implementano lo smistamento per formato. Le fasi di revisione e monitoraggio includono uno switch sull'estensione del file che indirizza .docx a Document e .pdf a PdfDocument. La fase di redazione richiede un modello .docx, e la fase di negoziazione opera su documenti Word (il confronto documenti usa Spire.Doc.Document). Questo riflette uno schema del mondo reale: lo smistamento del formato viene applicato dove sono previsti PDF delle controparti, non uniformemente su ogni fase.
Cosa succede se un contratto fallisce in un batch?
Il processore batch racchiude ogni contratto nel proprio blocco try-catch. Un fallimento in un contratto viene registrato con il messaggio di errore e la fase in cui si è verificato, e il batch continua con il contratto successivo. Il riepilogo del batch utilizza il metodo del resto per la conservazione dello stato: Flagged = Total - Successful - Errored, assicurando che ogni risultato finisca in esattamente un bucket di reporting.
L'agente IA decide chi approva un contratto?
No. Le regole di instradamento dell'approvazione sono logica aziendale deterministica — in questo esempio, i contratti superiori a $100K richiedono l'approvazione del VP, quelli superiori a $500K richiedono l'approvazione del CFO, e i contratti fornitore necessitano dell'approvazione dell'Approvvigionamento. L'agente IA genera il riepilogo di approvazione che gli approvatori leggono, ma l'instradamento stesso è codice. Questo mantiene la catena di approvazione verificabile e coerente.
Come funziona la fase di negoziazione?
La fase di negoziazione utilizza due capacità dal livello documentale Spire.Doc: Document.Compare() per generare un diff tra versioni, e Document.TrackChanges per abilitare il monitoraggio delle revisioni. L'agente IA analizza quindi il documento di confronto e categorizza ogni modifica come sostanziale (testo della clausola, numeri, date) o di formattazione (stile, spaziatura). Questa combinazione — confronto deterministico più categorizzazione IA — è ciò che rende utile la fase di negoziazione per i team legali.
Cos'è il monitoraggio post-firma?
Dopo che un contratto è finalizzato (firmato in produzione), il sistema estrae obblighi (chi deve fare cosa, entro quando), date chiave (rinnovo, cessazione, milestone) e termini di rinnovo (rinnovo automatico, periodo di preavviso) dal documento finalizzato. Questi metadati sono memorizzati come JSON strutturato che i sistemi a valle — ERP, CRM, approvvigionamento — possono interrogare senza aprire il documento contrattuale. Questa è la fase che la maggior parte degli strumenti di revisione contrattuale non affronta, ed è dove si perde il maggior valore nei processi manuali: obblighi dimenticati, rinnovi mancati, scadenze superate senza preavviso.
Quali dipendenze .NET sono necessarie?
Il pacchetto NuGet Spire.Agent.Office, che include transitivamente Spire.Doc, Spire.Pdf, Spire.XLS e Spire.Presentation. L'esempio è destinato a .NET 6 o versioni successive; controlla i framework di destinazione supportati dalla versione del pacchetto prima della distribuzione. È richiesto un SpireToken (chiave API) affinché l'agente IA comunichi con il servizio del modello linguistico.
Pronto ad automatizzare il ciclo di vita dei tuoi contratti?
Se la tua applicazione redige, rivede o traccia accordi, un agente IA documentale trasforma una singola istruzione in linguaggio naturale in un file reale e formattato invece di una pipeline di estrazione e ricostruzione che devi mantenere. Segui la Guida introduttiva per collegare l'SDK a un progetto .NET ed eseguire la tua prima istruzione, poi riutilizza i cinque schemi di fase sopra per i tuoi tipi di contratto, regole di approvazione e monitoraggio degli obblighi.
Ulteriori letture
- Generazione batch di contratti con Spire.Agent.Office -- la fase di redazione in dettaglio: mail merge e sostituzione di segnaposto su dozzine di modelli contemporaneamente
- Automazione dell'elaborazione delle fatture con un agente IA in .NET -- lo stesso schema estrai-convalida-esamina applicato alle fatture, dove ogni documento viene consumato una volta
- Elaborazione intelligente dei documenti in .NET: creazione di pipeline IDP -- orchestrazione a livello di pipeline: concorrenza, isolamento degli errori per documento e reportistica batch
- Panoramica del prodotto Spire.Agent.Office -- SDK di agenti IA per ogni formato di documento Office