feat(asset-ai): add contextual analysis and unmet-demand feedback
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
DIMENSIONS,
|
||||
type Dimension,
|
||||
type QueryPlan,
|
||||
} from "../../../shared/assets/assistant.js";
|
||||
import type { Vehicle } from "../../types.js";
|
||||
import { toRow } from "./model.js";
|
||||
|
||||
// Kept as an input/context adapter for existing callers. Natural language is no longer
|
||||
// reduced to a closed vocabulary: the conversation planner needs the actual question.
|
||||
export type SafeInput = {
|
||||
message: string;
|
||||
tokens: string[];
|
||||
entities: { token: string; dimension: Dimension; value: string }[];
|
||||
catalog: Partial<Record<Dimension, string[]>>;
|
||||
demand: string;
|
||||
unknown: boolean;
|
||||
sensitive: boolean;
|
||||
};
|
||||
export function projectQuestion(
|
||||
message: string,
|
||||
vehicles: Vehicle[],
|
||||
previous?: QueryPlan,
|
||||
): SafeInput {
|
||||
const catalog: SafeInput["catalog"] = {};
|
||||
for (const dimension of DIMENSIONS) {
|
||||
const values = [
|
||||
...new Set(vehicles.map((v) => toRow(v)[dimension]).filter(Boolean)),
|
||||
];
|
||||
// Prefer mentioned/current values if a large catalog must be truncated.
|
||||
values.sort(
|
||||
(a, b) =>
|
||||
Number(
|
||||
message.includes(b) || previous?.filters[dimension]?.includes(b),
|
||||
) -
|
||||
Number(
|
||||
message.includes(a) || previous?.filters[dimension]?.includes(a),
|
||||
),
|
||||
);
|
||||
catalog[dimension] = values.slice(0, 150);
|
||||
}
|
||||
const entities: SafeInput["entities"] = [];
|
||||
for (const dimension of DIMENSIONS)
|
||||
for (const value of catalog[dimension] || []) {
|
||||
if (
|
||||
message.includes(value) ||
|
||||
(dimension === "city" &&
|
||||
value.endsWith("市") &&
|
||||
message.includes(value.slice(0, -1))) ||
|
||||
previous?.filters[dimension]?.includes(value)
|
||||
) {
|
||||
entities.push({ token: `E${entities.length + 1}`, dimension, value });
|
||||
}
|
||||
}
|
||||
const mentioned = entities.filter(
|
||||
(e) =>
|
||||
message.includes(e.value) ||
|
||||
(e.dimension === "city" &&
|
||||
e.value.endsWith("市") &&
|
||||
message.includes(e.value.slice(0, -1))),
|
||||
);
|
||||
return {
|
||||
message,
|
||||
tokens: [
|
||||
message,
|
||||
...mentioned.map((e) => e.token),
|
||||
...(message.match(/20\d\d-\d{2}-\d{2}/g) || []),
|
||||
],
|
||||
entities,
|
||||
catalog,
|
||||
// Demand feedback records the request instead of silently dropping unknown words.
|
||||
demand: message.replace(/sk-[a-z0-9]{8,}/gi, "[凭据已隐藏]").slice(0, 500),
|
||||
unknown: false,
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
export function externalContext(safe: SafeInput, previous?: QueryPlan) {
|
||||
return {
|
||||
message: safe.message,
|
||||
catalog: safe.catalog,
|
||||
previous: previous || null,
|
||||
};
|
||||
}
|
||||
export function restoreEntities(plan: QueryPlan, safe: SafeInput): QueryPlan {
|
||||
return {
|
||||
...plan,
|
||||
filters: Object.fromEntries(
|
||||
Object.entries(plan.filters).map(([key, values]) => [
|
||||
key,
|
||||
values.map(
|
||||
(value) =>
|
||||
safe.entities.find((e) => e.token === value && e.dimension === key)
|
||||
?.value || value,
|
||||
),
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user