- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
/**
|
|
* Runs one audit against an IT Glue record.
|
|
*
|
|
* Phase 4 supported only Application (flexible_asset) audits, all-time
|
|
* scoped. Phase 4.1 generalizes:
|
|
* - assetType: 'flexible_asset' | 'configuration'
|
|
* - ticketScopeAnalysisId: when set, ticket evidence is the single source
|
|
* analysis only ("ticket-first" mode).
|
|
*
|
|
* Flow:
|
|
* 1. buildAssetAuditContext() — pulls everything; redacted.
|
|
* 2. callLLMStage() — single shot using the deep-analysis tier model for
|
|
* the requested provider (Sonnet for Anthropic, V4 Pro for OpenRouter).
|
|
* 3. insertAssetAudit() — persists the row, including ticket linkage.
|
|
*/
|
|
|
|
import { callLLMStage } from '@/lib/services/llm/call';
|
|
import {
|
|
type Provider,
|
|
stageModelsFor,
|
|
} from '@/lib/services/llm/models';
|
|
import { AssetAuditResponse } from '@/lib/types/analyzer';
|
|
import {
|
|
buildAssetAuditContext,
|
|
type AuditAssetType,
|
|
} from './data-builder';
|
|
import { getSystemPrompt, buildAssetAuditUserPayload } from './prompt';
|
|
import {
|
|
insertAssetAudit,
|
|
insertFailedAssetAudit,
|
|
} from './persistence';
|
|
|
|
const STAGE_MAX_TOKENS = 8_000;
|
|
|
|
export interface RunAssetAuditInput {
|
|
assetType: AuditAssetType;
|
|
assetId: number | string;
|
|
generatedByUserId: string | null;
|
|
provider?: Provider;
|
|
/** Phase 4.1: when set, ticket evidence narrows to just this analysis. */
|
|
ticketScopeAnalysisId?: string;
|
|
}
|
|
|
|
export interface RunAssetAuditResult {
|
|
auditId: string;
|
|
status: 'complete' | 'failed';
|
|
ticketCount: number;
|
|
errorMessage?: string;
|
|
}
|
|
|
|
export async function runAssetAudit(
|
|
input: RunAssetAuditInput
|
|
): Promise<RunAssetAuditResult> {
|
|
const provider: Provider = input.provider ?? 'anthropic';
|
|
const model = stageModelsFor(provider).deep_analysis;
|
|
|
|
const ctx = await buildAssetAuditContext({
|
|
assetType: input.assetType,
|
|
assetId: input.assetId,
|
|
ticketScopeAnalysisId: input.ticketScopeAnalysisId,
|
|
});
|
|
const ticketCount = ctx.ticket_evidence.length;
|
|
|
|
const assetSnapshot = {
|
|
id: ctx.asset.id,
|
|
name: ctx.asset.name,
|
|
organization_id: ctx.asset.organization_id,
|
|
organization_name: ctx.asset.organization_name,
|
|
asset_type: ctx.asset_type,
|
|
type_id: ctx.type_id,
|
|
type_name: ctx.type_name,
|
|
fields: ctx.asset.traits,
|
|
};
|
|
|
|
const triggeredByTicketNumber = ctx.ticket_scope?.ticket_number ?? null;
|
|
const triggeredByAnalysisId = ctx.ticket_scope?.analysis_id ?? null;
|
|
|
|
const { payload } = buildAssetAuditUserPayload(ctx);
|
|
const systemPrompt = getSystemPrompt(ctx);
|
|
|
|
try {
|
|
const result = await callLLMStage({
|
|
model,
|
|
system: systemPrompt,
|
|
user: payload,
|
|
schema: AssetAuditResponse,
|
|
maxTokens: STAGE_MAX_TOKENS,
|
|
});
|
|
|
|
const inserted = await insertAssetAudit({
|
|
asset_type: ctx.asset_type,
|
|
asset_id: ctx.asset.id,
|
|
asset_type_id: ctx.type_id,
|
|
organization_id: ctx.asset.organization_id,
|
|
generated_by_user_id: input.generatedByUserId,
|
|
provider,
|
|
model_used: model,
|
|
asset_snapshot: assetSnapshot,
|
|
ticket_count: ticketCount,
|
|
response: result.data,
|
|
estimated_cost_usd: result.estimated_cost_usd,
|
|
total_input_tokens: result.usage.input_tokens,
|
|
total_output_tokens: result.usage.output_tokens,
|
|
triggered_by_ticket_number: triggeredByTicketNumber,
|
|
triggered_by_analysis_id: triggeredByAnalysisId,
|
|
});
|
|
|
|
return {
|
|
auditId: inserted.id,
|
|
status: 'complete',
|
|
ticketCount,
|
|
};
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
const failed = await insertFailedAssetAudit({
|
|
asset_type: ctx.asset_type,
|
|
asset_id: ctx.asset.id,
|
|
asset_type_id: ctx.type_id,
|
|
organization_id: ctx.asset.organization_id,
|
|
generated_by_user_id: input.generatedByUserId,
|
|
provider,
|
|
model_used: model,
|
|
asset_snapshot: assetSnapshot,
|
|
ticket_count: ticketCount,
|
|
error_message: message,
|
|
triggered_by_ticket_number: triggeredByTicketNumber,
|
|
triggered_by_analysis_id: triggeredByAnalysisId,
|
|
});
|
|
return {
|
|
auditId: failed.id,
|
|
status: 'failed',
|
|
ticketCount,
|
|
errorMessage: message,
|
|
};
|
|
}
|
|
}
|