feat(asset-ai): add contextual analysis and unmet-demand feedback
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import pool from "../../db.js";
|
||||
import type {
|
||||
DemandGap,
|
||||
GapReason,
|
||||
GapStatus,
|
||||
} from "../../../shared/assets/assistant.js";
|
||||
// Store the unmet request and concise analysis context for product feedback, not full sessions or result rows.
|
||||
let ready: Promise<void> | undefined;
|
||||
const purge = () =>
|
||||
pool.query(
|
||||
"DELETE FROM bi_asset_ai_gaps WHERE last_seen < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 90 DAY)",
|
||||
);
|
||||
const SCHEMA = `CREATE TABLE IF NOT EXISTS bi_asset_ai_gaps (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id VARCHAR(64) NOT NULL, fingerprint CHAR(64) NOT NULL,
|
||||
demand VARCHAR(600) NOT NULL, reason VARCHAR(24) NOT NULL, status VARCHAR(24) NOT NULL DEFAULT 'open',
|
||||
occurrences INT NOT NULL DEFAULT 1, first_seen DATETIME NOT NULL, last_seen DATETIME NOT NULL,
|
||||
UNIQUE KEY user_demand (user_id, fingerprint), KEY last_seen (last_seen)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
|
||||
async function ensure() {
|
||||
if (!ready)
|
||||
ready = (async () => {
|
||||
// Precreated production tables need no DDL permission.
|
||||
try {
|
||||
await pool.query("SELECT id FROM bi_asset_ai_gaps LIMIT 0");
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code !== "ER_NO_SUCH_TABLE")
|
||||
throw error;
|
||||
await pool.query(SCHEMA);
|
||||
}
|
||||
await purge();
|
||||
setInterval(
|
||||
() => {
|
||||
void purge().catch(() =>
|
||||
console.warn("asset_ai_gap_retention_failed"),
|
||||
);
|
||||
},
|
||||
60 * 60 * 1000,
|
||||
).unref();
|
||||
})().catch((error) => {
|
||||
ready = undefined;
|
||||
throw error;
|
||||
});
|
||||
await ready;
|
||||
}
|
||||
export const gapStore = {
|
||||
async record(userId: string, demand: string, reason: GapReason) {
|
||||
await ensure();
|
||||
const fingerprint = createHash("sha256")
|
||||
.update(reason + "|" + demand)
|
||||
.digest("hex");
|
||||
await pool.query(
|
||||
`INSERT INTO bi_asset_ai_gaps (user_id,fingerprint,demand,reason,first_seen,last_seen)
|
||||
VALUES (?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE
|
||||
occurrences=occurrences+1, last_seen=UTC_TIMESTAMP(), status=IF(status='resolved','open',status)`,
|
||||
[userId, fingerprint, demand, reason],
|
||||
);
|
||||
},
|
||||
async list(userId: string | null): Promise<DemandGap[]> {
|
||||
await ensure();
|
||||
await purge();
|
||||
const [rows] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT id,demand,reason,status,occurrences,
|
||||
DATE_FORMAT(first_seen,'%Y-%m-%dT%H:%i:%sZ') firstSeen, DATE_FORMAT(last_seen,'%Y-%m-%dT%H:%i:%sZ') lastSeen
|
||||
FROM bi_asset_ai_gaps ${userId ? "WHERE user_id = ?" : ""} ORDER BY last_seen DESC LIMIT 200`,
|
||||
userId ? [userId] : [],
|
||||
);
|
||||
return rows as DemandGap[];
|
||||
},
|
||||
async update(id: number, status: GapStatus) {
|
||||
await ensure();
|
||||
await pool.query("UPDATE bi_asset_ai_gaps SET status=? WHERE id=?", [
|
||||
status,
|
||||
id,
|
||||
]);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user