ai-chatbot/lib/agents/generation-project-state.ts

438 lines
16 KiB
TypeScript

import {
createEmptyAgentModeWorkspaceState,
normalizeAgentModeWorkspaceState,
} from "@/lib/agents/mode-workspace-state";
import {
createEmptyAgentWorkflowMaterialSelections,
normalizeAgentWorkflowMaterialSelections,
} from "@/lib/agents/workflow-material-config";
import type {
AgentGenerationProjectPersistedState,
AgentGenerationProjectReplacementResultState,
AgentGenerationProjectResultCollections,
AgentGenerationResultModeType,
AgentGenerationReplacementResultStatus,
AgentGenerationProjectWorkspaceState,
AgentWorkflowPersistedState,
} from "@/types/agent";
export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerationProjectWorkspaceState {
return {
input: "",
attachments: [],
selectedModelId: "auto",
imageParams: {
aspectRatio: "auto",
imageSize: "2K",
imageCount: 1,
},
};
}
export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersistedState {
return {
inputs: {
primarySku: "",
secondarySku: "",
brand: "",
store: "",
officialSecondCategory: "",
officialThirdCategory: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
},
materials: createEmptyAgentWorkflowMaterialSelections(),
appearanceRecognitionEnabled: false,
selections: {},
selectedBrandModel: null,
selectedAgentPresetId: null,
currentStage: "workflow",
agentModeState: createEmptyAgentModeWorkspaceState(),
};
}
export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerationProjectPersistedState {
return {
workflowState: createEmptyAgentWorkflowPersistedState(),
workspaceState: createEmptyAgentGenerationProjectWorkspaceState(),
resultImageUrls: [],
resultCollections: {},
deletedResultCollections: {},
replacementResultState: {},
};
}
function normalizeResultCollections(value: unknown): AgentGenerationProjectResultCollections {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const normalizeUrlList = (input: unknown) =>
Array.isArray(input)
? Array.from(
new Set(
input
.map((item) => String(item || "").trim())
.filter((item) => /^https?:\/\//i.test(item))
)
).slice(0, 128)
: [];
return {
scene_generation: normalizeUrlList(raw.scene_generation),
detail_generation: normalizeUrlList(raw.detail_generation),
detail_replacement: normalizeUrlList(raw.detail_replacement),
sku_replacement: normalizeUrlList(raw.sku_replacement),
};
}
const REPLACEMENT_RESULT_STATUSES = new Set<AgentGenerationReplacementResultStatus>([
"completed",
"passthrough",
"fallback",
]);
function normalizeOptionalHttpUrl(value: unknown) {
const raw = String(value || "").trim();
return /^https?:\/\//i.test(raw) ? raw : null;
}
function normalizeReplacementResultStatus(value: unknown): AgentGenerationReplacementResultStatus {
return REPLACEMENT_RESULT_STATUSES.has(value as AgentGenerationReplacementResultStatus)
? (value as AgentGenerationReplacementResultStatus)
: "completed";
}
export function normalizeAgentGenerationProjectReplacementResultState(
value: unknown
): AgentGenerationProjectReplacementResultState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const normalizeModeState = (input: unknown) => {
const records =
input && typeof input === "object" && !Array.isArray(input)
? (input as Record<string, unknown>)
: {};
const entries: Array<
readonly [
string,
NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>[string],
]
> = [];
Object.entries(records).forEach(([rawCardId, rawRecord]) => {
const cardId = String(rawCardId || "").trim();
if (!cardId || !rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord)) {
return;
}
const record = rawRecord as Record<string, unknown>;
const resultUrl = normalizeOptionalHttpUrl(record.url);
const text = String(record.text || "").trim();
const compareSourceUrl = normalizeOptionalHttpUrl(record.compareSourceUrl);
const referenceImageUrl = normalizeOptionalHttpUrl(record.referenceImageUrl);
const referencePreviewUrl = normalizeOptionalHttpUrl(record.referencePreviewUrl);
const label = String(record.label || "").trim() || null;
const updatedAt = Number(record.updatedAt);
if (!resultUrl && !text && !compareSourceUrl && !referenceImageUrl && !referencePreviewUrl) {
return;
}
entries.push([
cardId.slice(0, 160),
{
url: resultUrl,
text,
status: normalizeReplacementResultStatus(record.status),
updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? Math.round(updatedAt) : 0,
compareSourceUrl,
referenceImageUrl,
referencePreviewUrl,
label,
},
]);
});
return Object.fromEntries(entries.slice(0, 256));
};
return {
detail_replacement: normalizeModeState(raw.detail_replacement),
sku_replacement: normalizeModeState(raw.sku_replacement),
};
}
function getReplacementResultUrls(
value:
| NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>
| undefined
| null
) {
return Array.from(
new Set(
Object.values(value || {})
.map((record) => String(record?.url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
)
).slice(0, 128);
}
function reconcileReplacementResultCollections(params: {
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
const detailReplacementUrls = getReplacementResultUrls(
params.replacementResultState.detail_replacement
);
const skuReplacementUrls = getReplacementResultUrls(
params.replacementResultState.sku_replacement
);
if (detailReplacementUrls.length === 0 && skuReplacementUrls.length === 0) {
return params.resultCollections;
}
return {
...params.resultCollections,
detail_replacement:
detailReplacementUrls.length > 0
? detailReplacementUrls
: params.resultCollections.detail_replacement,
sku_replacement:
skuReplacementUrls.length > 0
? skuReplacementUrls
: params.resultCollections.sku_replacement,
} satisfies AgentGenerationProjectResultCollections;
}
function inferLegacyResultModeType(
workflowState: AgentWorkflowPersistedState,
resultUrls: string[]
): AgentGenerationResultModeType {
const detailReferenceCount = workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount = workflowState.agentModeState?.skuReplacement?.references?.length || 0;
const resultCount = resultUrls.length;
if (detailReferenceCount > 0 && skuReferenceCount === 0) {
return "detail_replacement";
}
if (skuReferenceCount > 0 && detailReferenceCount === 0) {
return "sku_replacement";
}
if (detailReferenceCount > 0 || skuReferenceCount > 0) {
const detailGap = Math.abs(resultCount - detailReferenceCount);
const skuGap = Math.abs(resultCount - skuReferenceCount);
return detailGap <= skuGap ? "detail_replacement" : "sku_replacement";
}
return "scene_generation";
}
function normalizeMigratedResultCollections(params: {
workflowState: AgentWorkflowPersistedState;
resultImageUrls: string[];
resultCollections: AgentGenerationProjectResultCollections;
}) {
const normalizedSceneUrls = params.resultCollections.scene_generation || [];
const normalizedDetailGenerationUrls = params.resultCollections.detail_generation || [];
const normalizedDetailUrls = params.resultCollections.detail_replacement || [];
const normalizedSkuUrls = params.resultCollections.sku_replacement || [];
const hasExplicitBuckets =
normalizedSceneUrls.length > 0 ||
normalizedDetailGenerationUrls.length > 0 ||
normalizedDetailUrls.length > 0 ||
normalizedSkuUrls.length > 0;
if (!hasExplicitBuckets && params.resultImageUrls.length > 0) {
const inferredModeType = inferLegacyResultModeType(
params.workflowState,
params.resultImageUrls
);
return {
scene_generation:
inferredModeType === "scene_generation" ? params.resultImageUrls : [],
detail_replacement:
inferredModeType === "detail_replacement" ? params.resultImageUrls : [],
sku_replacement:
inferredModeType === "sku_replacement" ? params.resultImageUrls : [],
} satisfies AgentGenerationProjectResultCollections;
}
const detailReferenceCount =
params.workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount =
params.workflowState.agentModeState?.skuReplacement?.references?.length || 0;
if (
normalizedSceneUrls.length > 0 &&
normalizedDetailUrls.length === 0 &&
normalizedSkuUrls.length === 0
) {
if (
detailReferenceCount > 0 &&
normalizedSceneUrls.length >= Math.max(4, Math.floor(detailReferenceCount * 0.6))
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: normalizedSceneUrls,
sku_replacement: [],
} satisfies AgentGenerationProjectResultCollections;
}
if (
skuReferenceCount > 0 &&
normalizedSceneUrls.length >= Math.max(4, Math.floor(skuReferenceCount * 0.6))
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: [],
sku_replacement: normalizedSceneUrls,
} satisfies AgentGenerationProjectResultCollections;
}
}
return params.resultCollections;
}
function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspaceState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const attachments = Array.isArray(raw.attachments)
? raw.attachments
.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null;
}
const record = item as Record<string, unknown>;
const url = String(record.url || "").trim();
if (!url) return null;
return {
url,
name: String(record.name || "").trim() || null,
contentType: String(record.contentType || "").trim() || null,
assetId: String(record.assetId || "").trim() || null,
};
})
.slice(0, 16)
: [];
const rawImageParams =
raw.imageParams && typeof raw.imageParams === "object" && !Array.isArray(raw.imageParams)
? (raw.imageParams as Record<string, unknown>)
: {};
const imageCount = Number(rawImageParams.imageCount);
return {
input: String(raw.input || "").trim(),
attachments,
selectedModelId: String(raw.selectedModelId || "").trim() || "auto",
imageParams: {
aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto",
imageSize: String(rawImageParams.imageSize || "").trim() || "2K",
imageCount: Number.isFinite(imageCount) ? Math.max(1, Math.min(8, Math.round(imageCount))) : 1,
},
};
}
function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
inputs: {
primarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).primarySku || "" : "").trim(),
secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).secondarySku || "" : "").trim(),
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
officialSecondCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).officialSecondCategory || "" : "").trim(),
officialThirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).officialThirdCategory || "" : "").trim(),
thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).thirdCategory || "" : "").trim(),
fourthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fourthCategory || "" : "").trim(),
fifthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fifthCategory || "" : "").trim(),
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
},
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),
appearanceRecognitionEnabled: raw.appearanceRecognitionEnabled === true,
selections:
raw.selections && typeof raw.selections === "object" && !Array.isArray(raw.selections)
? (raw.selections as Record<string, string>)
: {},
selectedBrandModel:
raw.selectedBrandModel && typeof raw.selectedBrandModel === "object" && !Array.isArray(raw.selectedBrandModel)
? {
...(raw.selectedBrandModel as Record<string, unknown>),
brandName: String((raw.selectedBrandModel as Record<string, unknown>).brandName || "").trim(),
id: String((raw.selectedBrandModel as Record<string, unknown>).id || "").trim(),
imageUrl: String((raw.selectedBrandModel as Record<string, unknown>).imageUrl || "").trim(),
assetId: String((raw.selectedBrandModel as Record<string, unknown>).assetId || "").trim() || null,
displayName: String((raw.selectedBrandModel as Record<string, unknown>).displayName || "").trim() || null,
mimeType: String((raw.selectedBrandModel as Record<string, unknown>).mimeType || "").trim() || null,
sortOrder: Number.isFinite(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))
? Math.max(0, Math.min(999, Math.round(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))))
: 0,
}
: null,
selectedAgentPresetId: String(raw.selectedAgentPresetId || "").trim() || null,
currentStage: raw.currentStage === "mode" ? "mode" : "workflow",
agentModeState: normalizeAgentModeWorkspaceState(raw.agentModeState),
};
}
export function normalizeAgentGenerationProjectPersistedState(
value: unknown
): AgentGenerationProjectPersistedState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const resultImageUrls = Array.isArray(raw.resultImageUrls)
? Array.from(
new Set(
raw.resultImageUrls
.map((item) => String(item || "").trim())
.filter((item) => /^https?:\/\//i.test(item))
)
).slice(0, 64)
: [];
const workflowState = normalizeWorkflowState(raw.workflowState);
const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections);
const replacementResultState = normalizeAgentGenerationProjectReplacementResultState(
raw.replacementResultState
);
const resultCollections = reconcileReplacementResultCollections({
replacementResultState,
resultCollections: normalizeMigratedResultCollections({
workflowState,
resultImageUrls,
resultCollections: normalizeResultCollections(raw.resultCollections),
}),
});
return {
workflowState,
workspaceState: normalizeWorkspaceState(raw.workspaceState),
resultImageUrls,
resultCollections,
deletedResultCollections,
replacementResultState,
};
}