99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
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,
|
|
),
|
|
]),
|
|
),
|
|
};
|
|
}
|