ai-chatbot/lib/agents/agent-project-save-merge.ts

363 lines
12 KiB
TypeScript

import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
import type {
AgentDetailReplacementReferenceItem,
AgentGenerationProjectPersistedState,
AgentGenerationReplacementResultRecord,
AgentGenerationProjectReplacementResultState,
AgentGenerationProjectResultCollections,
AgentGenerationResultModeType,
} from "@/types/agent";
const RESULT_COLLECTION_KEYS = [
"scene_generation",
"detail_generation",
"detail_replacement",
"sku_replacement",
] as const;
const REPLACEMENT_RESULT_KEYS = [
"detail_replacement",
"sku_replacement",
] as const;
function normalizeProjectResultUrlList(urls: string[] | null | undefined) {
return Array.from(
new Set(
(urls || [])
.map((url) => String(url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
)
);
}
function mergeResultCollections(
existing: AgentGenerationProjectResultCollections | null | undefined,
incoming: AgentGenerationProjectResultCollections | null | undefined
): AgentGenerationProjectResultCollections {
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
(next, key) => {
next[key] = normalizeProjectResultUrlList([
...normalizeProjectResultUrlList(existing?.[key]),
...normalizeProjectResultUrlList(incoming?.[key]),
]);
return next;
},
{}
);
}
function getResultCollectionUrlSet(
collections: AgentGenerationProjectResultCollections | null | undefined
) {
return new Set(
RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key]))
);
}
function removeDeletedUrlsFromCollections(
collections: AgentGenerationProjectResultCollections,
deletedUrls: Set<string>
): AgentGenerationProjectResultCollections {
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
(next, key) => {
next[key] = normalizeProjectResultUrlList(collections[key]).filter(
(url) => !deletedUrls.has(url)
);
return next;
},
{}
);
}
function mergeReplacementResultState(
existing: AgentGenerationProjectReplacementResultState | null | undefined,
incoming: AgentGenerationProjectReplacementResultState | null | undefined,
deletedUrls: Set<string>
): AgentGenerationProjectReplacementResultState {
return REPLACEMENT_RESULT_KEYS.reduce<AgentGenerationProjectReplacementResultState>(
(next, key) => {
const existingModeState = existing?.[key] || {};
const incomingModeState = incoming?.[key] || {};
const mergedModeState = Array.from(
new Set([...Object.keys(existingModeState), ...Object.keys(incomingModeState)])
).reduce<Record<string, AgentGenerationReplacementResultRecord>>(
(records, cardId) => {
const existingRecord = existingModeState[cardId];
const incomingRecord = incomingModeState[cardId];
if (!existingRecord) {
if (incomingRecord) {
records[cardId] = incomingRecord;
}
return records;
}
if (!incomingRecord) {
records[cardId] = existingRecord;
return records;
}
const existingUpdatedAt = Number(existingRecord.updatedAt) || 0;
const incomingUpdatedAt = Number(incomingRecord.updatedAt) || 0;
records[cardId] =
incomingUpdatedAt >= existingUpdatedAt ? incomingRecord : existingRecord;
return records;
},
{}
);
next[key] = Object.fromEntries(
Object.entries(mergedModeState).filter(([, record]) => {
const url = String(record?.url || "").trim();
return !url || !deletedUrls.has(url);
})
);
return next;
},
{}
);
}
function getReplacementResultUrls(
state: AgentGenerationProjectReplacementResultState | null | undefined
) {
return REPLACEMENT_RESULT_KEYS.flatMap((key) =>
Object.values(state?.[key] || {})
.map((record) => String(record?.url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
);
}
export function getReplacementResultUrlsFromAgentProjectState(
state: AgentGenerationProjectPersistedState | null | undefined
) {
const normalized = normalizeAgentGenerationProjectPersistedState(state);
return normalizeProjectResultUrlList([
...normalizeProjectResultUrlList(
normalized.resultCollections?.detail_replacement
),
...normalizeProjectResultUrlList(
normalized.resultCollections?.sku_replacement
),
...getReplacementResultUrls(normalized.replacementResultState),
]);
}
export function doesAgentProjectStateCoverReplacementResults(params: {
candidateState: AgentGenerationProjectPersistedState | null | undefined;
authoritativeState: AgentGenerationProjectPersistedState | null | undefined;
}) {
const candidateUrls = new Set(
getReplacementResultUrlsFromAgentProjectState(params.candidateState)
);
return getReplacementResultUrlsFromAgentProjectState(
params.authoritativeState
).every((url) => candidateUrls.has(url));
}
export function getAllResultUrlsFromAgentProjectState(
state: AgentGenerationProjectPersistedState
) {
return normalizeProjectResultUrlList([
...normalizeProjectResultUrlList(state.resultImageUrls),
...RESULT_COLLECTION_KEYS.flatMap((key) =>
normalizeProjectResultUrlList(state.resultCollections?.[key])
),
...getReplacementResultUrls(state.replacementResultState),
]);
}
function hasDetailModeSignals(state: AgentGenerationProjectPersistedState) {
const detailReferenceCount =
state.workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount =
state.workflowState.agentModeState?.skuReplacement?.references?.length || 0;
return (
detailReferenceCount > 0 ||
skuReferenceCount > 0 ||
normalizeProjectResultUrlList(state.resultCollections?.detail_generation).length > 0 ||
normalizeProjectResultUrlList(state.resultCollections?.detail_replacement).length > 0 ||
normalizeProjectResultUrlList(state.resultCollections?.sku_replacement).length > 0 ||
getReplacementResultUrls(state.replacementResultState).length > 0
);
}
function hasDetailReplacementResultSignals(params: {
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
return (
normalizeProjectResultUrlList(params.resultCollections.detail_replacement).length > 0 ||
getReplacementResultUrls({
detail_replacement: params.replacementResultState.detail_replacement,
}).length > 0
);
}
function hasSkuReplacementResultSignals(params: {
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
return (
normalizeProjectResultUrlList(params.resultCollections.sku_replacement).length > 0 ||
getReplacementResultUrls({
sku_replacement: params.replacementResultState.sku_replacement,
}).length > 0
);
}
function mergeWorkflowStateForSave(params: {
existingState: AgentGenerationProjectPersistedState;
incomingState: AgentGenerationProjectPersistedState;
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
const incomingAgentModeState = params.incomingState.workflowState.agentModeState;
const existingAgentModeState = params.existingState.workflowState.agentModeState;
const mergeProtectedReferences = (
existingReferences: AgentDetailReplacementReferenceItem[],
incomingReferences: AgentDetailReplacementReferenceItem[],
resultRecords: Record<string, AgentGenerationReplacementResultRecord>,
hasResultSignals: boolean
) => {
if (!hasResultSignals || existingReferences.length === 0) {
return incomingReferences;
}
const protectedCardIds = new Set(
Object.entries(resultRecords)
.filter(([, record]) => Boolean(record?.url))
.map(([cardId]) => cardId)
);
const existingReferenceIds = new Set(
existingReferences.map((reference) => reference.id)
);
const incomingIsExistingSubset =
incomingReferences.length < existingReferences.length &&
incomingReferences.every((reference) =>
existingReferenceIds.has(reference.id)
);
const protectedExistingReferences =
incomingReferences.length === 0 || incomingIsExistingSubset
? existingReferences
: protectedCardIds.size > 0
? existingReferences.filter((reference) =>
protectedCardIds.has(reference.id)
)
: [];
if (protectedExistingReferences.length === 0) {
return incomingReferences;
}
const protectedById = new Map(
protectedExistingReferences.map((reference) => [reference.id, reference])
);
const mergedReferences = incomingReferences.map(
(reference) => protectedById.get(reference.id) || reference
);
const mergedIds = new Set(mergedReferences.map((reference) => reference.id));
protectedExistingReferences.forEach((reference) => {
if (!mergedIds.has(reference.id)) {
mergedReferences.push(reference);
}
});
return mergedReferences;
};
const detailReferences = mergeProtectedReferences(
existingAgentModeState?.detailReplacement?.references || [],
incomingAgentModeState?.detailReplacement?.references || [],
params.replacementResultState.detail_replacement || {},
hasDetailReplacementResultSignals(params)
);
const skuReferences = mergeProtectedReferences(
existingAgentModeState?.skuReplacement?.references || [],
incomingAgentModeState?.skuReplacement?.references || [],
params.replacementResultState.sku_replacement || {},
hasSkuReplacementResultSignals(params)
);
return {
...params.incomingState.workflowState,
agentModeState: {
...incomingAgentModeState,
detailReplacement: {
...incomingAgentModeState?.detailReplacement,
references: detailReferences,
},
skuReplacement: {
...incomingAgentModeState?.skuReplacement,
references: skuReferences,
},
},
};
}
export function mergeAgentProjectStateForSave(params: {
existingState: AgentGenerationProjectPersistedState;
incomingState: unknown;
}): AgentGenerationProjectPersistedState {
const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState);
const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState);
const deletedResultCollections = mergeResultCollections(
existingState.deletedResultCollections,
incomingState.deletedResultCollections
);
const deletedUrls = getResultCollectionUrlSet(deletedResultCollections);
const replacementResultState = mergeReplacementResultState(
existingState.replacementResultState,
incomingState.replacementResultState,
deletedUrls
);
const resultCollections = removeDeletedUrlsFromCollections(
mergeResultCollections(existingState.resultCollections, incomingState.resultCollections),
deletedUrls
);
const workflowState = mergeWorkflowStateForSave({
existingState,
incomingState,
resultCollections,
replacementResultState,
});
return normalizeAgentGenerationProjectPersistedState({
...incomingState,
workflowState,
resultCollections,
deletedResultCollections,
replacementResultState,
resultImageUrls: normalizeProjectResultUrlList([
...getAllResultUrlsFromAgentProjectState(existingState),
...getAllResultUrlsFromAgentProjectState(incomingState),
...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []),
...getReplacementResultUrls(replacementResultState),
]).filter((url) => !deletedUrls.has(url)),
});
}
export function appendAgentProjectResultForSave(params: {
existingState: AgentGenerationProjectPersistedState;
modeType: AgentGenerationResultModeType;
imageUrl: string;
}): AgentGenerationProjectPersistedState {
const existingState = normalizeAgentGenerationProjectPersistedState(
params.existingState
);
const imageUrl = String(params.imageUrl || "").trim();
if (!/^https?:\/\//i.test(imageUrl)) {
return existingState;
}
const resultCollections: AgentGenerationProjectResultCollections = {
...existingState.resultCollections,
[params.modeType]: normalizeProjectResultUrlList([
...(existingState.resultCollections?.[params.modeType] || []),
imageUrl,
]),
};
return mergeAgentProjectStateForSave({
existingState,
incomingState: {
...existingState,
resultCollections,
},
});
}