
O software de gestão de contratos com IA automatiza todo o ciclo de vida do contrato — desde a elaboração até a revisão, negociação, aprovação, finalização e acompanhamento das obrigações pós-assinatura — combinando a compreensão de linguagem natural da IA com o processamento determinístico de documentos dentro de uma aplicação .NET. A distinção em relação à automação de contratos de tarefa única está no escopo do ciclo de vida: uma ferramenta de revisão de contratos sinaliza cláusulas de risco; um sistema de gestão de contratos conduz um acordo por todas as etapas, mantém uma trilha de auditoria entre versões e acompanha as obrigações após a assinatura.
O Spire.Agent.Office é um SDK de agente de IA para documentos que fornece tanto a camada de compreensão de linguagem quanto a de processamento de documentos. Este artigo mostra como construir um sistema de gestão do ciclo de vida de contratos em C# que encadeia cinco etapas em um único pipeline, com isolamento de erros por etapa, despacho de formato para PDF e Word e rastreamento adequado de status — padrões que exemplos de tarefa única não abordam.
Este artigo apresenta uma implementação de referência de um pipeline de ciclo de vida de contratos assistido por IA em .NET. Um sistema CLM de produção precisaria adicionalmente de persistência de fluxo de trabalho, controle de identidade e acesso, integração com assinatura eletrônica, armazenamento de documentos e retenção de versões, notificações e infraestrutura de auditoria.
1. O Ciclo de Vida do Contrato: Cinco Etapas em que a IA Atua
Um ciclo de vida de contrato não é uma única operação sobre um documento. É uma sequência de etapas, cada uma com entradas, saídas e modos de falha distintos. Entender onde a IA agrega valor em cada etapa — e onde o código determinístico deve manter a linha — é a base de um sistema que se sustenta em produção.
| Etapa | O que acontece | Papel da IA | Papel do código determinístico |
|---|---|---|---|
| Elaboração | Gerar um contrato a partir de um modelo mais dados estruturados | Interpretar a solicitação, selecionar e preencher campos do modelo | Carregar o modelo, preservar a formatação, salvar como .docx ou .pdf
|
| Revisão | Ler o contrato, sinalizar cláusulas de risco, extrair termos-chave | Análise semântica da linguagem das cláusulas, pontuação de risco | Escrever relatório de revisão estruturado, aplicar checklist de revisão |
| Negociação | Comparar versões, rastrear marcações, mesclar alterações | Resumir diferenças, sinalizar alterações substanciais vs. de formatação | Ativar/desativar controle de alterações, comparar documentos, aceitar/rejeitar revisões |
| Aprovação | Encaminhar para as partes interessadas, coletar aprovações | Sugerir aprovadores com base no tipo e valor do contrato | Aplicar regras de encaminhamento, registrar trilha de auditoria, produzir cópia finalizada |
| Pós-assinatura | Acompanhar obrigações, prazos, renovações | Extrair obrigações e datas-chave do contrato finalizado | Armazenar metadados estruturados, disparar lembretes, gerar relatórios |
Na prática, o ciclo de vida não é linear — a negociação retorna à revisão, as emendas reiniciam a elaboração —, mas a arquitetura de pipeline lida com isso por meio de roteamento de etapas em vez de uma sequência fixa.
O Que Diferencia Isto da Revisão de Contratos Isolada
A automação de revisão de contratos — tema de Revisão de Contratos com IA em C# — cobre uma etapa: ler um acordo e sinalizar problemas. Um sistema de gestão de contratos deve:
- Encadear etapas com dados fluindo de uma para a próxima (a saída da elaboração torna-se a entrada da revisão).
-
Lidar com múltiplos formatos de documento — modelos chegam como
.docx, contrapartes podem enviar.pdf, e etapas com despacho de formato lidam com ambos sem lançar exceções. - Manter o estado entre as etapas — o status de revisão de um contrato, a contagem de versões de negociação e a cadeia de aprovação devem persistir entre execuções do pipeline.
- Isolar falhas por contrato — um arquivo corrompido em um lote de 200 não deve interromper todo o pipeline.
Esses requisitos moldam a arquitetura a seguir.
2. Arquitetura do Sistema para Automação do Ciclo de Vida de Contratos
Um sistema de gestão do ciclo de vida de contratos tem três camadas. Cada camada tem uma responsabilidade específica, e as fronteiras entre elas são onde as falhas de produção são capturadas ou escapam.

Camada 1: Interface de Instrução
O ponto de entrada é uma instrução em linguagem natural que descreve o resultado desejado — não os passos mecânicos. "Elabore um contrato de fornecedor para a Acme Corp usando o modelo padrão, revise-o quanto a condições de pagamento não padrão e encaminhe ao Jurídico se o limite de responsabilidade exceder $500.000." A camada de instrução analisa isso e produz um plano de pipeline: quais etapas executar, em que ordem e quais parâmetros cada etapa precisa.
Camada 2: Orquestrador de Etapas
O orquestrador gerencia o fluxo entre as etapas. Ele mantém um objeto ContractContext compartilhado que carrega os metadados do contrato, o documento atual e os resultados das etapas de uma etapa para a seguinte. Cada etapa recebe o contexto, realiza sua operação e retorna um contexto atualizado mais um resultado da etapa. O orquestrador decide se prossegue, tenta novamente ou encaminha para um tratador de exceções com base no resultado.
Camada 3: Processamento de Documentos
Cada etapa chama o Spire.Agent.Office para realizar a operação real sobre o documento. O agente cuida do raciocínio de IA (entender a instrução, extrair informações) e a camada de documento cuida das operações de arquivo (carregar, modificar, salvar). O despacho de formato é aplicado nas etapas em que múltiplos formatos de documento são esperados: uma entrada .pdf usa PdfDocument, enquanto uma entrada .docx usa Document.
O Contexto do Contrato
O objeto de estado compartilhado que flui pelo 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";
}
O campo Status usa constantes nomeadas em vez de strings brutas. Isso evita um modo de falha comum em que o status nunca é definido explicitamente, fazendo com que os documentos caiam por todos os grupos de relatório e desapareçam dos resumos de lote.
3. Etapa 1: Elaboração a partir de Modelo e Dados
A etapa de elaboração recebe um modelo (.docx com marcadores {{Placeholder}}) e uma fonte de dados (planilha do Excel ou entrada estruturada), e então produz um contrato preenchido. O agente de IA lê a estrutura do modelo e preenche os marcadores com dados da fonte — uma única instrução substitui o código de mapeamento de campos exigido por um SDK tradicional.
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;
}
}
Chamadas Principais da API
-
Document.LoadFromFile()— carrega o modelo.docxcom marcadores -
template.AI(_options)— anexa o processador de documentos com IA -
ExecuteInstruction(doc, instruction, outputPath, attachments)— preenche os marcadores a partir da fonte de dados; passe um caminho absoluto explícito para que o agente grave o produto em um local conhecido - Coleta do produto: verifique primeiro
File.Exists(outputPath);AIResult.OutputFilesnão é confiável para alguns tipos de produto e deve ser usado apenas como fallback
Despacho de Formato
A etapa valida o formato de entrada antes do processamento. Um modelo .pdf ou um formato não suportado é rejeitado explicitamente com uma mensagem clara, em vez de falhar dentro do agente com uma exceção opaca. Esse padrão evita um modo de falha comum em que a ausência de despacho de formato faz com que entradas PDF lancem exceções não tratadas em camadas profundas do pipeline de processamento.
Elaborar um contrato é o caso simples. Quando o mesmo modelo precisa ser preenchido para dezenas de registros — novos funcionários, fornecedores, renovações — o padrão de instrução escala para saída em lote sem alterações; Geração de Contratos em Lote com Spire.Agent.Office aborda tanto a rota de mala direta quanto a de substituição de marcadores, e compara onde cada uma se encaixa.
Nota do SDK: Quando um
outputPathexplícito é passado paraExecuteInstruction, o SDK também pode gravar uma cópiaoutput-<filename>no mesmo diretório. Essa duplicata tem conteúdo idêntico e pode ser removida com segurança após o processamento. Todas as etapas que especificam caminhos de saída são afetadas.
O produto desta etapa é o próprio documento preenchido — o modelo de contrato de fornecedor com cada marcador substituído a partir da planilha mestre de fornecedores.

4. Etapa 2: Revisão e Análise de Risco
A etapa de revisão lê o contrato elaborado, identifica cláusulas de risco ou não padronizadas e produz um relatório de revisão estruturado. Diferentemente da etapa de elaboração, a entrada pode ser .docx (minuta interna) ou .pdf (documento da contraparte), então a etapa deve despachar para o tipo de documento correto.
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;
}
}
O método DispatchByFormat é a adição crítica. Em implementações anteriores de pipeline, uma etapa que só lidava com .docx lançava uma InvalidOperationException quando a contraparte enviava um .pdf. A expressão switch despacha para o tipo de documento correto antes que o agente seja executado, de modo que ambos os formatos são entradas de primeira classe.
Os limites de risco (AutoApproveThreshold, ManualReviewThreshold) são constantes nomeadas com semântica documentada. A diferença entre 0,3 e 0,6 cria uma zona explícita de "precisa de revisão" — contratos que não são limpos o suficiente para aprovação automática nem arriscados o suficiente para bloquear. Isso evita o bug de lógica morta de limite em que todo contrato passa na mesma verificação.
Os achados da revisão são persistidos como relatório em vez de retornados em memória, de modo que a auditoria em nível de cláusula sobrevive independentemente da execução que a produziu.

5. Etapa 3: Negociação e Controle de Versão
A negociação é onde os contratos mudam de mãos. A contraparte marca o documento — riscando cláusulas, ajustando termos, adicionando condições. O sistema deve comparar versões, rastrear alterações e ajudar a equipe a decidir o que aceitar.
Esta etapa usa dois recursos da camada de documento Spire.Doc que vão além do ExecuteInstruction do agente de IA:
- Controle de Alterações — ativar o rastreamento de revisões para que toda edição seja visível e atribuível
- Comparação de Documentos — comparar duas versões e gerar um documento de diferenças
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));
}
}
Chamadas Principais da API
-
Document.Compare(otherDoc, authorName)— gera um documento de diferenças com todas as alterações marcadas como revisões rastreadas (da demonstração de comparação do Spire.Doc) -
Document.TrackChanges = true— ativa o rastreamento de revisões para que toda edição seja visível e atribuível -
Document.AcceptChanges()— aceita todas as alterações rastreadas para produzir uma versão final limpa -
doc.AI(_options).ExecuteInstruction(...)— analisa a comparação e categoriza as alterações
A etapa de negociação combina operações determinísticas sobre documentos (comparar, rastrear alterações, aceitar) com a análise de IA (categorizar alterações como substanciais ou de formatação). As operações determinísticas vêm da camada Spire.Doc — as mesmas APIs disponíveis no diretório de referência de código — enquanto o agente de IA cuida da categorização semântica que de outra forma exigiria revisão manual.
O exemplo compara versões e rastreia o caminho do arquivo atual; um sistema de produção normalmente persistiria cada versão separadamente e registraria explicitamente qual revisão foi aceita. AcceptAllChanges() é fornecida como uma operação opcional de finalização após a negociação, mas não é invocada automaticamente pelo pipeline de exemplo, porque a aceitação deve ocorrer somente após a revisão humana.
O que Document.Compare retorna é um documento, não um resumo: as revisões rastreadas abaixo são as marcações que um revisor montaria manualmente.

6. Etapa 4: Aprovação e Finalização
A etapa de aprovação encaminha o contrato para as partes interessadas corretas com base no tipo e no valor do contrato, registra suas aprovações e produz uma cópia finalizada em PDF após todas as aprovações exigidas serem registradas. A lógica de encaminhamento é determinística — usa regras de negócio, não IA —, mas o agente ajuda sugerindo aprovadores e gerando o resumo de aprovação.
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;
}
}
As regras de encaminhamento em DetermineApprovers são intencionalmente determinísticas. A IA sugere e resume; ela não decide quem assina um contrato. A cadeia de aprovação é uma regra de negócio que precisa ser auditável e consistente — exatamente o tipo de decisão que pertence ao código, não a um modelo de linguagem.
Nota: O status
Executednesta amostra significa o PDF final produzido após a aprovação — não um contrato assinado. A assinatura eletrônica real — envelopes de assinatura, verificação da identidade do signatário, incorporação de campos de assinatura — deve ser tratada por um fluxo de trabalho dedicado de assinatura eletrônica integrado separadamente.
Assim que a decisão de cada aprovador estiver registrada, a etapa grava o contrato finalizado como PDF — e é esse PDF, não a minuta de origem, que a Etapa 5 consome.

7. Etapa 5: Monitoramento Pós-Assinatura
Em um fluxo de trabalho de produção, o monitoramento pós-assinatura começa após o contrato ter sido assinado eletronicamente ou de outra forma formalmente. Nesta implementação de referência, o PDF finalizado produzido pela Etapa 4 é usado como entrada do monitoramento. O monitoramento pós-assinatura extrai obrigações, datas-chave e termos de renovação do contrato e então os armazena como metadados estruturados que sistemas posteriores usam para lembretes e relatórios.
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;
}
}
Os metadados das obrigações são armazenados como JSON, não como documento. Isso os torna consultáveis por sistemas posteriores — um painel de renovações pode verificar todos os contratos em busca de autoRenewal == true e renewalDate < DateTime.Now.AddDays(90) sem abrir um único documento. Obrigações com valores dueDate estruturados podem ser rastreadas automaticamente; aquelas em que o prazo não pôde ser convertido em data absoluta mantêm o texto original em dueDateRaw para revisão manual. O agente de IA extrai a informação; a camada determinística a armazena em um formato que sistemas de negócio podem consumir.
O padrão de extração é o mesmo usado para faturas em Automatize o Processamento de Faturas com um Agente de IA em .NET: extrair campos estruturados de um documento recebido, validá-los contra regras determinísticas e encaminhar os valores incertos para revisão. Os contratos diferem principalmente no que acontece depois — as obrigações tornam-se registros de longa duração que sobrevivem à transação, enquanto os campos de fatura são consumidos uma única vez.
Os dados extraídos são armazenados como JSON em vez de prosa, de modo que cada obrigação e data-chave carrega o texto original do contrato ao lado do valor que o analisador derivou dele.

8. Orquestrando o Pipeline Completo
O orquestrador encadeia as etapas, passando o ContractContext de uma para a seguinte. Suas duas responsabilidades críticas são o isolamento de erros por contrato (um contrato com falha não deve interromper o lote) e a conservação de status (todo contrato deve terminar em exatamente um grupo de relatório).
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 de Erros por Etapa
Cada etapa é envolvida em seu próprio bloco try-catch. Se a etapa de revisão falhar porque o agente de IA não consegue analisar uma cláusula particularmente complexa, o pipeline registra a falha e para — mas o ContractContext do próximo contrato do lote permanece intacto. Isso evita um modo de falha comum em que um único arquivo corrompido em um lote de 200 interrompe todo o pipeline sem resultados parciais.
Conservação de Status
O resumo do lote usa o método do resto para calcular a contagem de Flagged: Flagged = Total - Successful - Errored. Essa é uma propriedade estrutural — qualquer resultado que não seja explicitamente bem-sucedido ou com erro cai em "flagged" (encaminhamento conservador para revisão manual). Isso evita o bug em que resultados com valores de status inesperados desaparecem silenciosamente de todos os três grupos de relatório.
Executando o 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}");
}
As decisões de aprovação são fornecidas à chamada do lote em vez de inferidas pelo pipeline: um contrato sem decisão registrada mantém seus aprovadores pendentes, cai em Flagged e para antes da Etapa 5 — o resultado pretendido para um acordo que ninguém aprovou ainda.
Uma execução sobre alguns contratos é o caso simples. Coloque as mesmas etapas por trás de uma fila de documentos e as questões de orquestração mudam: limites de concorrência, isolamento de erros por documento, política de repetição e agregação em um relatório de lote. Processamento Inteligente de Documentos em .NET: Construindo Pipelines de IDP percorre esses pontos no nível do pipeline, e a seção de lote deste artigo é uma instância específica de contratos do mesmo padrão.
9. Onde a IA Termina e a Governança Começa
A arquitetura acima atribui responsabilidades específicas ao agente de IA e responsabilidades específicas ao código determinístico. A fronteira não é arbitrária — ela segue um princípio: a IA interpreta e sugere; o código determinístico decide e registra.
Avaliações de risco geradas por IA e obrigações extraídas devem ser validadas por revisores qualificados antes de serem usadas para decisões legais ou comerciais.
| Responsabilidade | Tratada por | Por quê |
|---|---|---|
| Entender uma instrução em linguagem natural | Agente de IA | É exatamente a finalidade dos modelos de linguagem |
| Extrair informações de texto não estruturado | Agente de IA | Requer compreensão semântica |
| Categorizar alterações como substanciais ou de formatação | Agente de IA | Requer julgamento sobre significado |
| Sugerir aprovadores com base no tipo de contrato | Regras de negócio | Auditabilidade e consistência |
| Determinar quem deve assinar | Regras de negócio | Auditabilidade e consistência |
| Registrar a cadeia de aprovação | Código determinístico | A trilha de auditoria legal deve ser à prova de adulteração |
| Definir limites de risco | Regras de negócio | A política de risco é uma decisão de governança |
| Armazenar metadados de obrigações | Código determinístico | Sistemas posteriores precisam de estrutura confiável |
| Disparar lembretes de renovação | Código determinístico | Deve disparar conforme o cronograma, não por inferência |
Os limites de risco na etapa de revisão (AutoApproveThreshold = 0.3, ManualReviewThreshold = 0.6) são definidos pelo negócio, não pela IA. A aplicação deriva uma proporção de revisão a partir do número de cláusulas sinalizadas; o negócio define os limites. Essa separação é o que torna o sistema defensável em uma auditoria — toda decisão automatizada remonta a uma regra definida por humanos, não a uma inferência de modelo.
Integração com Sistemas Existentes
Um sistema de ciclo de vida de contratos não existe isoladamente. Os metadados pós-assinatura (obrigações, datas-chave, termos de renovação) são projetados para serem consumidos por sistemas posteriores:
- ERP / Finanças: marcos de pagamento e conciliação de faturas
- CRM: alertas de renovação para gerentes de contas
- Compras: acompanhamento de desempenho de fornecedores em relação aos SLAs do contrato
- Jurídico: monitoramento de conformidade e preparação para auditoria
O formato de metadados JSON da Etapa 5 torna essa integração simples — sistemas posteriores consultam os dados estruturados sem precisar analisar documentos de contrato.
10. Perguntas Frequentes
Como isto é diferente da revisão de contratos com IA?
A revisão de contratos é uma etapa do ciclo de vida — ler um acordo e sinalizar cláusulas de risco. Um sistema de gestão de contratos cobre todo o ciclo de vida: elaboração, revisão, negociação, aprovação, finalização e monitoramento pós-assinatura. O artigo existente Revisão de Contratos com IA em C# aborda a etapa de revisão em profundidade; este artigo cobre o pipeline completo dentro do qual a revisão está inserida.
O pipeline consegue lidar com entradas PDF e Word?
Sim, nas etapas que implementam despacho de formato. As etapas de revisão e monitoramento incluem um switch na extensão do arquivo que encaminha .docx para Document e .pdf para PdfDocument. A etapa de elaboração exige um modelo .docx, e a etapa de negociação opera em documentos do Word (a comparação de documentos usa Spire.Doc.Document). Isso reflete um padrão do mundo real: o despacho de formato é aplicado onde PDFs de contrapartes são esperados, não uniformemente em todas as etapas.
O que acontece se um contrato falhar em um lote?
O processador de lote envolve cada contrato em seu próprio bloco try-catch. Uma falha em um contrato é registrada com a mensagem de erro e a etapa em que ocorreu, e o lote continua para o próximo contrato. O resumo do lote usa o método do resto para conservação de status: Flagged = Total - Successful - Errored, garantindo que todo resultado caia em exatamente um grupo de relatório.
O agente de IA decide quem aprova um contrato?
Não. As regras de encaminhamento de aprovação são lógica de negócio determinística — neste exemplo, contratos acima de $100 mil exigem aprovação do VP, acima de $500 mil exigem aprovação do CFO, e contratos com fornecedores precisam de aprovação de Compras. O agente de IA gera o resumo de aprovação que os aprovadores leem, mas o encaminhamento em si é código. Isso mantém a cadeia de aprovação auditável e consistente.
Como funciona a etapa de negociação?
A etapa de negociação usa dois recursos da camada de documento Spire.Doc: Document.Compare() para gerar um diff entre versões e Document.TrackChanges para ativar o rastreamento de revisões. O agente de IA então analisa o documento de comparação e categoriza cada alteração como substancial (texto da cláusula, números, datas) ou de formatação (estilo, espaçamento). Essa combinação — comparação determinística mais categorização por IA — é o que torna a etapa de negociação útil para equipes jurídicas.
O que é monitoramento pós-assinatura?
Após um contrato ser finalizado (assinado em produção), o sistema extrai obrigações (quem deve fazer o quê, até quando), datas-chave (renovação, rescisão, marcos) e termos de renovação (renovação automática, período de aviso prévio) do documento finalizado. Esses metadados são armazenados como JSON estruturado que sistemas posteriores — ERP, CRM, compras — podem consultar sem abrir o documento do contrato. Esta é a etapa que a maioria das ferramentas de revisão de contratos não aborda, e é onde mais valor é perdido em processos manuais: obrigações esquecidas, renovações perdidas, prazos vencidos sem aviso.
Quais dependências .NET são necessárias?
O pacote NuGet Spire.Agent.Office, que traz transitivamente Spire.Doc, Spire.Pdf, Spire.XLS e Spire.Presentation. A amostra tem como alvo o .NET 6 ou posterior; verifique os frameworks de destino suportados pela versão do pacote antes da implantação. Um SpireToken (chave de API) é necessário para que o agente de IA se comunique com o serviço de modelo de linguagem.
Pronto para Automatizar o Ciclo de Vida dos Seus Contratos?
Se sua aplicação elabora, revisa ou acompanha acordos, um agente de IA para documentos transforma uma única instrução em linguagem natural em um arquivo real e formatado, em vez de um pipeline de extração e reconstrução que você precisa manter. Siga o Guia de Introdução para integrar o SDK a um projeto .NET e executar sua primeira instrução, e então reutilize os cinco padrões de etapa acima para seus próprios tipos de contrato, regras de aprovação e acompanhamento de obrigações.
Leitura Adicional
- Geração de Contratos em Lote com Spire.Agent.Office -- a etapa de elaboração em profundidade: mala direta e substituição de marcadores em dezenas de modelos ao mesmo tempo
- Automatize o Processamento de Faturas com um Agente de IA em .NET -- o mesmo padrão de extrair-validar-revisar aplicado a faturas, onde cada documento é consumido uma única vez
- Processamento Inteligente de Documentos em .NET: Construindo Pipelines de IDP -- orquestração no nível do pipeline: concorrência, isolamento de erros por documento e relatórios em lote
- Visão geral do produto Spire.Agent.Office -- SDKs de agente de IA para todos os formatos de documento do Office