Improve UMS candidate scoring and snapshot materialization for sparse data.
Lower term threshold, tune promotion scores, project candidate fallback in snapshot, and add rebuild-ums-snapshot script for production backfill. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Rebuild UMS candidates + snapshot from existing signals (no new evidence ingest).
|
||||||
|
*
|
||||||
|
* UMS_DATABASE_URL=... node scripts/rebuild-ums-snapshot.mjs [user_id]
|
||||||
|
*/
|
||||||
|
import { createUmsPool, isUmsDatabaseConfigured } from '../user-model-service/db.mjs';
|
||||||
|
import { mergeCandidatesFromSignals } from '../user-model-service/candidates.mjs';
|
||||||
|
import { materializeProfileAndSnapshot } from '../user-model-service/snapshot.mjs';
|
||||||
|
import { resolveCanonicalUserId } from '../user-model-service/canonical-user.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!isUmsDatabaseConfigured()) {
|
||||||
|
console.error('Set UMS_DATABASE_URL');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const userId = resolveCanonicalUserId(
|
||||||
|
process.argv[2] ?? process.env.UMS_REBUILD_USER_ID ?? 'a70ff537-8908-486e-9b6c-042e07cc25db',
|
||||||
|
);
|
||||||
|
const pool = createUmsPool();
|
||||||
|
try {
|
||||||
|
const candidates = await mergeCandidatesFromSignals(pool, userId);
|
||||||
|
const snapshot = await materializeProfileAndSnapshot(pool, userId, { reason: 'rebuild_script' });
|
||||||
|
console.log('rebuild OK:', {
|
||||||
|
user_id: userId,
|
||||||
|
candidates_touched: candidates,
|
||||||
|
profile_version: snapshot.profile_version,
|
||||||
|
byte_size: snapshot.byte_size,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -12,6 +12,14 @@ function slugEntityName(name) {
|
|||||||
return String(name).trim().slice(0, 128);
|
return String(name).trim().slice(0, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_TERM_COUNT = Number(process.env.UMS_CANDIDATE_MIN_TERM_COUNT ?? 3);
|
||||||
|
|
||||||
|
function isProjectLikeTerm(term) {
|
||||||
|
if (/^[A-Z][a-zA-Z0-9]+$/.test(term)) return true;
|
||||||
|
if (/meinput|memind|input|agent|rime|fcitx|tkmind|goosed|pgvector/i.test(term)) return true;
|
||||||
|
return term.includes('Input') || term.includes('Mind');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V0.1:从 term_frequency signals 推断 project/focus candidates
|
* V0.1:从 term_frequency signals 推断 project/focus candidates
|
||||||
* @param {import('mysql2/promise').Pool} pool
|
* @param {import('mysql2/promise').Pool} pool
|
||||||
@@ -43,13 +51,13 @@ export async function mergeCandidatesFromSignals(pool, userId) {
|
|||||||
let touched = 0;
|
let touched = 0;
|
||||||
const ts = nowMs();
|
const ts = nowMs();
|
||||||
for (const [term, stats] of termCounts) {
|
for (const [term, stats] of termCounts) {
|
||||||
if (stats.count < 5) continue;
|
if (stats.count < MIN_TERM_COUNT) continue;
|
||||||
if (term.length < 2) continue;
|
if (term.length < 2) continue;
|
||||||
|
|
||||||
const isProjectLike = /^[A-Z][a-zA-Z0-9]+$/.test(term) || term.includes('Input') || term.includes('Mind');
|
const isProjectLike = isProjectLikeTerm(term);
|
||||||
const candidateType = isProjectLike ? 'project' : 'focus';
|
const candidateType = isProjectLike ? 'project' : 'focus';
|
||||||
const confidence = Math.min(0.99, 0.4 + stats.count * 0.03);
|
const confidence = Math.min(0.99, 0.4 + stats.count * 0.05);
|
||||||
const promotionScore = Math.min(0.99, confidence * Math.min(1, stats.count / 20));
|
const promotionScore = Math.min(0.99, 0.3 + stats.count * 0.1);
|
||||||
const hypothesis = isProjectLike
|
const hypothesis = isProjectLike
|
||||||
? { type: 'project', name: term, status: 'active' }
|
? { type: 'project', name: term, status: 'active' }
|
||||||
: { type: 'focus', topic: term };
|
: { type: 'focus', topic: term };
|
||||||
@@ -166,3 +174,5 @@ async function upsertProjectGraph(pool, userId, name, confidence, evidenceIds, w
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { isProjectLikeTerm, MIN_TERM_COUNT };
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ export async function materializeProfileAndSnapshot(pool, userId, options = {})
|
|||||||
[userId],
|
[userId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [projectCandidates] = await pool.query(
|
||||||
|
`SELECT hypothesis_json, confidence, promotion_score, last_seen_at
|
||||||
|
FROM um_candidates
|
||||||
|
WHERE user_id = ? AND candidate_type = 'project' AND status IN ('open', 'accepted', 'observed')
|
||||||
|
ORDER BY promotion_score DESC
|
||||||
|
LIMIT 8`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
|
||||||
const [versionRows] = await pool.query(
|
const [versionRows] = await pool.query(
|
||||||
`SELECT COALESCE(MAX(profile_version), 0) AS v FROM um_profile_versions WHERE user_id = ?`,
|
`SELECT COALESCE(MAX(profile_version), 0) AS v FROM um_profile_versions WHERE user_id = ?`,
|
||||||
[userId],
|
[userId],
|
||||||
@@ -55,6 +64,20 @@ export async function materializeProfileAndSnapshot(pool, userId, options = {})
|
|||||||
last_seen: row.last_seen_at,
|
last_seen: row.last_seen_at,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (activeProjects.length === 0 && projectCandidates.length > 0) {
|
||||||
|
for (const row of projectCandidates) {
|
||||||
|
const h = typeof row.hypothesis_json === 'string' ? JSON.parse(row.hypothesis_json) : row.hypothesis_json;
|
||||||
|
const name = h.name ?? h.topic ?? 'unknown';
|
||||||
|
activeProjects.push({
|
||||||
|
id: `project_${String(name).toLowerCase().replace(/[^a-z0-9]+/g, '_')}`,
|
||||||
|
name,
|
||||||
|
status: h.status ?? 'active',
|
||||||
|
confidence: Number(row.confidence),
|
||||||
|
last_seen: row.last_seen_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const recentFocus = focusCandidates.map((row) => {
|
const recentFocus = focusCandidates.map((row) => {
|
||||||
const h = typeof row.hypothesis_json === 'string' ? JSON.parse(row.hypothesis_json) : row.hypothesis_json;
|
const h = typeof row.hypothesis_json === 'string' ? JSON.parse(row.hypothesis_json) : row.hypothesis_json;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user