feat: add AI-assisted ISAC authoring
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { requirePermission } from '$lib/server/auth/authorization';
|
||||
import { recordIsacAudit } from '$lib/server/audit';
|
||||
import {
|
||||
AiMechanicConfigurationError,
|
||||
AiMechanicGenerationError
|
||||
} from '$lib/server/ai/mechanic-authoring-service';
|
||||
import {
|
||||
AiMechanicRateLimitError,
|
||||
reserveAiMechanicGeneration
|
||||
} from '$lib/server/ai/mechanic-authoring-rate-limit';
|
||||
import {
|
||||
AiCalculationRequestError,
|
||||
parseAiCalculationAuthoringRequest
|
||||
} from '$lib/server/ai/calculation-authoring-schema';
|
||||
import { generateAiCalculationDraft } from '$lib/server/ai/calculation-authoring-service';
|
||||
import { AI_CALCULATION_PROMPT_VERSION } from '$lib/server/ai/calculation-authoring-prompt';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const MAX_REQUEST_BYTES = 350_000;
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request, url }) => {
|
||||
const operator = await requirePermission(
|
||||
await getSessionUser(cookies),
|
||||
'ai.calculation.generate'
|
||||
);
|
||||
const origin = request.headers.get('origin');
|
||||
if (origin && origin !== url.origin) {
|
||||
return json(
|
||||
{ message: 'Cross-origin calculation authoring requests are not allowed.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
const contentLength = Number(request.headers.get('content-length') ?? 0);
|
||||
if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BYTES) {
|
||||
return json({ message: 'The calculation authoring request is too large.' }, { status: 413 });
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
const body = await request.text();
|
||||
if (Buffer.byteLength(body, 'utf8') > MAX_REQUEST_BYTES) {
|
||||
return json({ message: 'The calculation authoring request is too large.' }, { status: 413 });
|
||||
}
|
||||
payload = JSON.parse(body);
|
||||
} catch {
|
||||
return json({ message: 'The calculation authoring request must be valid JSON.' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const authoringRequest = parseAiCalculationAuthoringRequest(payload);
|
||||
const release = reserveAiMechanicGeneration(operator.id);
|
||||
let response;
|
||||
try {
|
||||
response = await generateAiCalculationDraft(authoringRequest, operator.id);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'ai_assistance',
|
||||
action: 'calculation_ai.generated',
|
||||
entityId: authoringRequest.baseRevisionId
|
||||
? `calculation_revision:${authoringRequest.baseRevisionId}`
|
||||
: `calculation_revision_draft:${response.meta.requestId}`,
|
||||
after: {
|
||||
promptVersion: AI_CALCULATION_PROMPT_VERSION,
|
||||
baseRevisionId: authoringRequest.baseRevisionId,
|
||||
responseStatus: response.status,
|
||||
model: response.meta.model,
|
||||
requestId: response.meta.requestId,
|
||||
usage: {
|
||||
inputTokens: response.meta.inputTokens,
|
||||
outputTokens: response.meta.outputTokens,
|
||||
totalTokens: response.meta.totalTokens
|
||||
},
|
||||
clarificationCount: response.status === 'clarification_required'
|
||||
? response.questions.length
|
||||
: 0,
|
||||
capabilityGapCount: response.status === 'capability_gap'
|
||||
? response.gaps.length
|
||||
: 0,
|
||||
draftValidated: response.status === 'draft_ready'
|
||||
}
|
||||
});
|
||||
return json(response);
|
||||
} catch (caught) {
|
||||
if (caught instanceof AiCalculationRequestError) {
|
||||
return json({ message: caught.message }, { status: caught.status });
|
||||
}
|
||||
if (caught instanceof AiMechanicConfigurationError) {
|
||||
return json({ message: caught.message }, { status: 503 });
|
||||
}
|
||||
if (caught instanceof AiMechanicRateLimitError) {
|
||||
return json(
|
||||
{ message: caught.message },
|
||||
{
|
||||
status: 429,
|
||||
headers: { 'retry-after': String(caught.retryAfterSeconds) }
|
||||
}
|
||||
);
|
||||
}
|
||||
if (caught instanceof AiMechanicGenerationError) {
|
||||
return json({ message: caught.message }, { status: 422 });
|
||||
}
|
||||
console.error('Calculation authoring assistant failed.', caught);
|
||||
return json(
|
||||
{ message: 'The AI calculation authoring service is temporarily unavailable.' },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { requirePermission } from '$lib/server/auth/authorization';
|
||||
import { recordIsacAudit } from '$lib/server/audit';
|
||||
import {
|
||||
AiMechanicConfigurationError,
|
||||
AiMechanicGenerationError,
|
||||
generateAiMechanicDraft
|
||||
} from '$lib/server/ai/mechanic-authoring-service';
|
||||
import { AI_MECHANIC_PROMPT_VERSION } from '$lib/server/ai/mechanic-authoring-prompt';
|
||||
import {
|
||||
AiMechanicRequestError,
|
||||
parseAiMechanicAuthoringRequest
|
||||
} from '$lib/server/ai/mechanic-authoring-schema';
|
||||
import {
|
||||
AiMechanicRateLimitError,
|
||||
reserveAiMechanicGeneration
|
||||
} from '$lib/server/ai/mechanic-authoring-rate-limit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const MAX_REQUEST_BYTES = 100_000;
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request, url }) => {
|
||||
const operator = await requirePermission(
|
||||
await getSessionUser(cookies),
|
||||
'ai.mechanic.generate'
|
||||
);
|
||||
const origin = request.headers.get('origin');
|
||||
if (origin && origin !== url.origin) {
|
||||
return json(
|
||||
{ message: 'Cross-origin mechanic authoring requests are not allowed.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
const contentLength = Number(request.headers.get('content-length') ?? 0);
|
||||
if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BYTES) {
|
||||
return json({ message: 'The mechanic authoring request is too large.' }, { status: 413 });
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
const body = await request.text();
|
||||
if (Buffer.byteLength(body, 'utf8') > MAX_REQUEST_BYTES) {
|
||||
return json({ message: 'The mechanic authoring request is too large.' }, { status: 413 });
|
||||
}
|
||||
payload = JSON.parse(body);
|
||||
} catch {
|
||||
return json({ message: 'The mechanic authoring request must be valid JSON.' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const authoringRequest = parseAiMechanicAuthoringRequest(payload);
|
||||
const release = reserveAiMechanicGeneration(operator.id);
|
||||
let response;
|
||||
try {
|
||||
response = await generateAiMechanicDraft(authoringRequest, operator.id);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'ai_assistance',
|
||||
action: 'mechanic_ai.generated',
|
||||
entityId: `${authoringRequest.context.sourceKind}:${response.meta.requestId}`,
|
||||
after: {
|
||||
sourceKind: authoringRequest.context.sourceKind,
|
||||
promptVersion: AI_MECHANIC_PROMPT_VERSION,
|
||||
responseStatus: response.status,
|
||||
model: response.meta.model,
|
||||
requestId: response.meta.requestId,
|
||||
usage: {
|
||||
inputTokens: response.meta.inputTokens,
|
||||
outputTokens: response.meta.outputTokens,
|
||||
totalTokens: response.meta.totalTokens
|
||||
},
|
||||
variantKeys: authoringRequest.valueColumns.map((column) => column.variantKey),
|
||||
clarificationCount: response.status === 'clarification_required'
|
||||
? response.questions.length
|
||||
: 0,
|
||||
capabilityGapCount: response.status === 'capability_gap'
|
||||
? response.gaps.length
|
||||
: 0,
|
||||
draftValidated: response.status === 'draft_ready'
|
||||
}
|
||||
});
|
||||
return json(response);
|
||||
} catch (caught) {
|
||||
if (caught instanceof AiMechanicRequestError) {
|
||||
return json({ message: caught.message }, { status: caught.status });
|
||||
}
|
||||
if (caught instanceof AiMechanicConfigurationError) {
|
||||
return json({ message: caught.message }, { status: 503 });
|
||||
}
|
||||
if (caught instanceof AiMechanicRateLimitError) {
|
||||
return json(
|
||||
{ message: caught.message },
|
||||
{
|
||||
status: 429,
|
||||
headers: { 'retry-after': String(caught.retryAfterSeconds) }
|
||||
}
|
||||
);
|
||||
}
|
||||
if (caught instanceof AiMechanicGenerationError) {
|
||||
return json({ message: caught.message }, { status: 422 });
|
||||
}
|
||||
console.error('Mechanic authoring assistant failed.', caught);
|
||||
return json(
|
||||
{ message: 'The AI mechanic authoring service is temporarily unavailable.' },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user