Refactor pipeline task handling and UI flows
This commit is contained in:
@@ -18,6 +18,8 @@ import {
|
||||
qualityReviewSchema,
|
||||
type AuditSpecialistFinding,
|
||||
type AuditSpecialistResult,
|
||||
type QualityReview,
|
||||
type QualityReviewRevisedCopy,
|
||||
} from "../lib/ai/schemas";
|
||||
import {
|
||||
validateCustomerFacingCopy,
|
||||
@@ -32,6 +34,7 @@ import {
|
||||
type ScreenshotOneRequest,
|
||||
} from "../lib/external-audit-services";
|
||||
import { type AuditUsedSkill } from "../lib/skills-registry";
|
||||
import { getAuditProgressForStep } from "../lib/audits/progress";
|
||||
import { internal } from "./_generated/api";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import {
|
||||
@@ -281,6 +284,44 @@ type GermanCopyOutput = {
|
||||
};
|
||||
};
|
||||
|
||||
function applyRevisedCopy(
|
||||
currentCopy: GermanCopyOutput,
|
||||
revisedCopy: QualityReviewRevisedCopy,
|
||||
): GermanCopyOutput {
|
||||
return {
|
||||
...currentCopy,
|
||||
publicSummary: revisedCopy.publicSummary,
|
||||
publicBody: revisedCopy.publicBody,
|
||||
emailSubject: revisedCopy.emailSubject,
|
||||
emailBody: revisedCopy.emailBody,
|
||||
phoneScript: {
|
||||
openingLine: revisedCopy.phoneScript.openingLine,
|
||||
callScript: revisedCopy.phoneScript.callScript,
|
||||
closeLine: revisedCopy.phoneScript.closeLine,
|
||||
},
|
||||
followUpDraft: {
|
||||
message: revisedCopy.followUpDraft.message,
|
||||
...(revisedCopy.followUpDraft.followInDays !== null
|
||||
? { followInDays: revisedCopy.followUpDraft.followInDays }
|
||||
: {}),
|
||||
...(revisedCopy.followUpDraft.goals !== null
|
||||
? { goals: revisedCopy.followUpDraft.goals }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function germanCopyGuardTelemetry(guardResult: GermanCopyGuardResult) {
|
||||
return {
|
||||
passed: guardResult.passed,
|
||||
issues: guardResult.issues.map((issue) => ({
|
||||
field: issue.field,
|
||||
rule: issue.rule,
|
||||
message: issue.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
type MultimodalContentPart =
|
||||
| {
|
||||
type: "text";
|
||||
@@ -554,7 +595,11 @@ function buildQualityReviewPrompt(
|
||||
`Öffentlicher Text: ${germanCopy.publicBody}`,
|
||||
`Email-Betreff: ${germanCopy.emailSubject}`,
|
||||
`Email-Text: ${germanCopy.emailBody}`,
|
||||
"Antworte als JSON mit isValid, issues, suggestions, notes.",
|
||||
"Wenn die Copy nur stilistische oder leichte fachliche Hinweise hat, nutze severity warning und rewriteRequired true.",
|
||||
"Wenn eine Korrektur sinnvoll ist, liefere revisedCopy vollständig mit publicSummary, publicBody, emailSubject, emailBody, phoneScript und followUpDraft.",
|
||||
"Wenn keine Korrektur nötig ist, setze rewriteRequired false und revisedCopy null.",
|
||||
"Nutze severity unsafe nur für harte Risiken wie falsche Sprache, erfundene Behauptungen, aggressive Tonalität oder Rohdaten-Leaks.",
|
||||
"Antworte als JSON mit isValid, severity, issues, suggestions, rewriteRequired, revisedCopy und notes.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -630,6 +675,27 @@ async function appendRunEvent(
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRootAuditProgress(
|
||||
ctx: ActionCtx,
|
||||
rootRunId: Id<"agentRuns"> | undefined,
|
||||
currentStep: string,
|
||||
) {
|
||||
if (!rootRunId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = getAuditProgressForStep(currentStep);
|
||||
await ctx.runMutation(internal.runs.updateProgressInternal, {
|
||||
id: rootRunId,
|
||||
status: "running",
|
||||
currentStep,
|
||||
progressStep: progress.step,
|
||||
progressTotal: progress.total,
|
||||
progressLabel: progress.label,
|
||||
progressPercent: progress.percent,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAuditSkillRegistry(
|
||||
ctx: ActionCtx,
|
||||
runId: Id<"agentRuns">,
|
||||
@@ -1204,6 +1270,7 @@ function getValidMediaType(mimeType: string) {
|
||||
export const processAuditGeneration = internalAction({
|
||||
args: {
|
||||
runId: v.id("agentRuns"),
|
||||
rootRunId: v.optional(v.id("agentRuns")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let started:
|
||||
@@ -1345,6 +1412,7 @@ export const processAuditGeneration = internalAction({
|
||||
MAX_PROMPT_BYTES,
|
||||
);
|
||||
currentStep = "classification";
|
||||
await updateRootAuditProgress(ctx, args.rootRunId, currentStep);
|
||||
await persistAuditStage({
|
||||
ctx,
|
||||
runId: args.runId,
|
||||
@@ -1545,6 +1613,7 @@ export const processAuditGeneration = internalAction({
|
||||
const verifierSystemPrompt =
|
||||
"Du bist EvidenceQA. Verifiziere Befunde streng gegen belegte Evidence-Refs.";
|
||||
currentStep = "evidenceVerifier";
|
||||
await updateRootAuditProgress(ctx, args.rootRunId, currentStep);
|
||||
|
||||
await persistAuditStage({
|
||||
ctx,
|
||||
@@ -1690,6 +1759,7 @@ export const processAuditGeneration = internalAction({
|
||||
}
|
||||
|
||||
currentStep = "multimodalAudit";
|
||||
await updateRootAuditProgress(ctx, args.rootRunId, currentStep);
|
||||
|
||||
const validScreenshotParts = screenshotParts.filter(
|
||||
(part): part is MultimodalFilePart => part !== null,
|
||||
@@ -1844,6 +1914,7 @@ export const processAuditGeneration = internalAction({
|
||||
}
|
||||
|
||||
currentStep = "germanCopy";
|
||||
await updateRootAuditProgress(ctx, args.rootRunId, currentStep);
|
||||
// Stage 3: german copy generation
|
||||
const germanSystemPrompt =
|
||||
"Du bist fachlicher Texter für lokale Unternehmen im B2B-Kontext.";
|
||||
@@ -1856,61 +1927,65 @@ export const processAuditGeneration = internalAction({
|
||||
const safeGermanPrompt = sanitizeAndCapString(germanPrompt, MAX_PROMPT_BYTES);
|
||||
|
||||
try {
|
||||
const publicSummaryResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: publicAuditTextSchema,
|
||||
prompt: safeGermanPrompt
|
||||
? `${safeGermanPrompt}\nAusgabe für publicSummary`
|
||||
: "Ausgabe für publicSummary",
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
|
||||
const germanBodyResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: publicAuditTextSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für publicBody`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
|
||||
const germanSubjectResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: emailSubjectSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für emailSubject`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
|
||||
const germanEmailResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: emailDraftSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für emailBody`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
|
||||
const germanCallScriptResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: callScriptSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für callScript`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
|
||||
const germanFollowUpResult = await generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: followUpDraftSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für followUpDraft`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
});
|
||||
const [
|
||||
publicSummaryResult,
|
||||
germanBodyResult,
|
||||
germanSubjectResult,
|
||||
germanEmailResult,
|
||||
germanCallScriptResult,
|
||||
germanFollowUpResult,
|
||||
] = await Promise.all([
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: publicAuditTextSchema,
|
||||
prompt: safeGermanPrompt
|
||||
? `${safeGermanPrompt}\nAusgabe für publicSummary`
|
||||
: "Ausgabe für publicSummary",
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: publicAuditTextSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für publicBody`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: emailSubjectSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für emailSubject`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: emailDraftSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für emailBody`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: callScriptSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für callScript`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
generateObject({
|
||||
model: provider(germanCopyProfile.modelId),
|
||||
system: germanSystemPrompt,
|
||||
schema: followUpDraftSchema,
|
||||
prompt: `${safeGermanPrompt ?? ""}\nAusgabe für followUpDraft`,
|
||||
temperature: germanCopyProfile.temperature,
|
||||
maxOutputTokens: germanCopyProfile.maxTokens,
|
||||
}),
|
||||
]);
|
||||
|
||||
const publicSummary = publicSummaryResult.object.publicText ?? "";
|
||||
const publicBody = germanBodyResult.object.publicText ?? "";
|
||||
@@ -2015,42 +2090,89 @@ export const processAuditGeneration = internalAction({
|
||||
},
|
||||
followUp: germanCopyOutput.followUpDraft.message,
|
||||
});
|
||||
const deterministicGuard = germanCopyGuardTelemetry(guardResult);
|
||||
|
||||
// Stage 4: final quality review
|
||||
const qualityPrompt = buildQualityReviewPrompt(
|
||||
let qualityPrompt = buildQualityReviewPrompt(
|
||||
verifiedFindingsText,
|
||||
germanCopyOutput,
|
||||
);
|
||||
const safeQualityPrompt = sanitizeAndCapString(qualityPrompt, MAX_PROMPT_BYTES);
|
||||
let safeQualityPrompt = sanitizeAndCapString(qualityPrompt, MAX_PROMPT_BYTES);
|
||||
const qualitySystemPrompt =
|
||||
"Du prüfst die erzeugten Inhalte als Qualitätssicherung.";
|
||||
|
||||
currentStep = "qualityReview";
|
||||
await updateRootAuditProgress(ctx, args.rootRunId, currentStep);
|
||||
try {
|
||||
const qualityResult = await generateObject({
|
||||
model: provider(qualityReviewProfile.modelId),
|
||||
system: qualitySystemPrompt,
|
||||
schema: qualityReviewSchema,
|
||||
prompt: safeQualityPrompt ?? "",
|
||||
temperature: qualityReviewProfile.temperature,
|
||||
maxOutputTokens: qualityReviewProfile.maxTokens,
|
||||
});
|
||||
let finalQualityReview: QualityReview | null = null;
|
||||
let qualityFinishReason: string | undefined;
|
||||
let rewriteApplied = false;
|
||||
let copyReviewAttempts = 0;
|
||||
const qualityReviewUsages: Array<OpenRouterUsage | undefined> = [];
|
||||
|
||||
qualityPassed = qualityResult.object.isValid && guardResult.passed;
|
||||
while (copyReviewAttempts < 2) {
|
||||
copyReviewAttempts += 1;
|
||||
const qualityResult = await generateObject({
|
||||
model: provider(qualityReviewProfile.modelId),
|
||||
system: qualitySystemPrompt,
|
||||
schema: qualityReviewSchema,
|
||||
prompt: safeQualityPrompt ?? "",
|
||||
temperature: qualityReviewProfile.temperature,
|
||||
maxOutputTokens: qualityReviewProfile.maxTokens,
|
||||
});
|
||||
|
||||
finalQualityReview = qualityResult.object;
|
||||
qualityFinishReason = qualityResult.finishReason;
|
||||
qualityReviewUsages.push(qualityResult.usage);
|
||||
|
||||
if (
|
||||
copyReviewAttempts === 1 &&
|
||||
qualityResult.object.rewriteRequired &&
|
||||
qualityResult.object.revisedCopy
|
||||
) {
|
||||
germanCopyOutput = applyRevisedCopy(
|
||||
germanCopyOutput,
|
||||
qualityResult.object.revisedCopy,
|
||||
);
|
||||
rewriteApplied = true;
|
||||
await appendRunEvent(ctx, {
|
||||
runId: args.runId,
|
||||
level: "warning",
|
||||
message: "Copy-Review hat korrigiert.",
|
||||
details: qualityResult.object.issues.slice(0, 4).map((issue) => ({
|
||||
label: "Hinweis",
|
||||
value: issue,
|
||||
})),
|
||||
});
|
||||
|
||||
qualityPrompt = buildQualityReviewPrompt(
|
||||
verifiedFindingsText,
|
||||
germanCopyOutput,
|
||||
);
|
||||
safeQualityPrompt = sanitizeAndCapString(
|
||||
qualityPrompt,
|
||||
MAX_PROMPT_BYTES,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!finalQualityReview) {
|
||||
throw new Error("Copy-Review konnte nicht ausgewertet werden.");
|
||||
}
|
||||
|
||||
qualityPassed =
|
||||
finalQualityReview.isValid && finalQualityReview.severity === "ok";
|
||||
|
||||
const qualityPayload = {
|
||||
isValid: qualityResult.object.isValid && guardResult.passed,
|
||||
issues: [
|
||||
...qualityResult.object.issues,
|
||||
...guardResult.issues.map(
|
||||
(issue) => `${issue.field}: ${issue.message}`,
|
||||
),
|
||||
],
|
||||
suggestions: qualityResult.object.suggestions,
|
||||
notes: qualityResult.object.notes ?? [],
|
||||
...finalQualityReview,
|
||||
rewriteApplied,
|
||||
reviewAttempts: copyReviewAttempts,
|
||||
deterministicGuard,
|
||||
finalDecision: qualityPassed ? "approved" : "stored_with_warnings",
|
||||
};
|
||||
const qualityErrorSummary =
|
||||
"Qualitätsprüfung hat Inhalte als ungenügend markiert.";
|
||||
|
||||
await persistAuditStage({
|
||||
ctx,
|
||||
@@ -2067,43 +2189,26 @@ export const processAuditGeneration = internalAction({
|
||||
MAX_RAW_RESPONSE_BYTES,
|
||||
),
|
||||
parsedJson: sanitizeAndCapParsedJson(qualityPayload),
|
||||
...withStageUsage(qualityResult.usage),
|
||||
status: qualityPassed ? "succeeded" : "failed",
|
||||
finishReason: qualityResult.finishReason,
|
||||
...(!qualityPassed ? { errorSummary: qualityErrorSummary } : {}),
|
||||
...withStageUsage(aggregateOpenRouterUsage(qualityReviewUsages)),
|
||||
status: "succeeded",
|
||||
...(qualityFinishReason ? { finishReason: qualityFinishReason } : {}),
|
||||
});
|
||||
await recordOpenRouterUsage(ctx, {
|
||||
runId: args.runId,
|
||||
leadId: started.lead._id,
|
||||
...(auditId ? { auditId } : {}),
|
||||
usage: qualityResult.usage,
|
||||
usage: aggregateOpenRouterUsage(qualityReviewUsages),
|
||||
});
|
||||
|
||||
if (!qualityPassed) {
|
||||
const message =
|
||||
"Qualitätsprüfung und German-Copy-Guard haben nicht bestanden.";
|
||||
if (!qualityPassed || !guardResult.passed) {
|
||||
await appendRunEvent(ctx, {
|
||||
runId: args.runId,
|
||||
level: "warning",
|
||||
message,
|
||||
});
|
||||
await ctx.runMutation(internal.auditGeneration.finishAuditGenerationRun, {
|
||||
runId: args.runId,
|
||||
status: "failed",
|
||||
currentStep: "qualityReview",
|
||||
errors: errors + 1,
|
||||
errorSummary: message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!qualityResult.object.isValid) {
|
||||
await appendRunEvent(ctx, {
|
||||
runId: args.runId,
|
||||
level: "warning",
|
||||
message:
|
||||
"Qualitätsprüfung hat Review-Hinweise gemeldet; German-Copy-Guard bestanden.",
|
||||
details: qualityResult.object.issues.slice(0, 4).map((issue) => ({
|
||||
message: "Copy-Review mit Hinweisen abgeschlossen.",
|
||||
details: [
|
||||
...finalQualityReview.issues,
|
||||
...guardResult.issues.map((issue) => issue.message),
|
||||
].slice(0, 4).map((issue) => ({
|
||||
label: "Hinweis",
|
||||
value: issue,
|
||||
})),
|
||||
@@ -2288,3 +2393,25 @@ export const processAuditGeneration = internalAction({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const processAuditGenerationForWorkflow = internalAction({
|
||||
args: {
|
||||
runId: v.id("agentRuns"),
|
||||
rootRunId: v.id("agentRuns"),
|
||||
},
|
||||
handler: async (ctx, args): Promise<Id<"agentRuns">> => {
|
||||
const result = await ctx.runAction(
|
||||
internal.auditGenerationAction.processAuditGeneration,
|
||||
{
|
||||
runId: args.runId,
|
||||
rootRunId: args.rootRunId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Audit-Generierung konnte nicht abgeschlossen werden.");
|
||||
}
|
||||
|
||||
return args.runId;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user