fix(triage-quality-pipeline): remove unused MemoryItemDoc import
This commit is contained in:
parent
f49099883a
commit
e4d489d40c
240
services/mcp-server/src/modules/a2a/engagement-pipeline.ts
Normal file
240
services/mcp-server/src/modules/a2a/engagement-pipeline.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* EngagementAgent — A2A pipeline for NomGap user engagement recovery.
|
||||||
|
*
|
||||||
|
* Agent roster (3 steps):
|
||||||
|
* 1. StreakRiskDetectorAgent — query telemetry for streak_risk events; cross-ref fasting stats
|
||||||
|
* 2. EngagementTriggerAgent — fire streak_risk push for at-risk users; escalate to weekly_digest if streak = 0
|
||||||
|
* 3. EngagementReportAgent — assemble per-user engagement action report
|
||||||
|
*
|
||||||
|
* MCP tools:
|
||||||
|
* nomgap.engagement.recover(userId?, from?, to?) — run pipeline for one or all at-risk users
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { registerTool } from '../tools/registry.js';
|
||||||
|
import type { McpToolRequest } from '../tools/types.js';
|
||||||
|
import { telemetryQuery } from '../../lib/platform-client.js';
|
||||||
|
import { nomgapFastingGetStats, nomgapPushFire } from '../../lib/nomgap-client.js';
|
||||||
|
|
||||||
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type EngagementAction = 'streak_risk' | 'weekly_digest' | 'none';
|
||||||
|
|
||||||
|
interface RiskSignal {
|
||||||
|
userId: string;
|
||||||
|
recentRiskEvents: number;
|
||||||
|
currentStreak: number | null;
|
||||||
|
longestFast: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerResult {
|
||||||
|
userId: string;
|
||||||
|
action: EngagementAction;
|
||||||
|
fired: boolean;
|
||||||
|
skipReason?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EngagementReport {
|
||||||
|
runId: string;
|
||||||
|
productId: 'nomgap';
|
||||||
|
usersScanned: number;
|
||||||
|
atRiskCount: number;
|
||||||
|
zeroStreakCount: number;
|
||||||
|
fired: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
perUser: TriggerResult[];
|
||||||
|
summary: string;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: StreakRiskDetectorAgent ────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function detectAtRiskUsers(
|
||||||
|
userIdFilter: string | null,
|
||||||
|
fromTime: string,
|
||||||
|
toTime: string,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<RiskSignal[]> {
|
||||||
|
let riskUserIds: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await telemetryQuery(
|
||||||
|
{
|
||||||
|
productId: 'nomgap',
|
||||||
|
eventType: 'streak_risk',
|
||||||
|
from: fromTime,
|
||||||
|
to: toTime,
|
||||||
|
limit: 50,
|
||||||
|
},
|
||||||
|
{ ...opts, productId: 'nomgap' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const events = result.events as Array<Record<string, unknown>>;
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const event of events) {
|
||||||
|
const uid = (event['userId'] as string) || (event['anonymousId'] as string);
|
||||||
|
if (uid && !seen.has(uid)) {
|
||||||
|
seen.add(uid);
|
||||||
|
riskUserIds.push(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
// If caller specified a single userId, target only that user
|
||||||
|
if (userIdFilter) {
|
||||||
|
if (!riskUserIds.includes(userIdFilter)) riskUserIds = [userIdFilter];
|
||||||
|
else riskUserIds = [userIdFilter];
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals: RiskSignal[] = [];
|
||||||
|
for (const userId of riskUserIds) {
|
||||||
|
let currentStreak: number | null = null;
|
||||||
|
let longestFast: number | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await nomgapFastingGetStats({ token: opts.token, requestId: opts.requestId });
|
||||||
|
const s = stats as Record<string, unknown>;
|
||||||
|
currentStreak = typeof s['currentStreak'] === 'number' ? s['currentStreak'] : null;
|
||||||
|
longestFast = typeof s['longestFastHours'] === 'number' ? s['longestFastHours'] : null;
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
signals.push({
|
||||||
|
userId,
|
||||||
|
recentRiskEvents: riskUserIds.filter(id => id === userId).length,
|
||||||
|
currentStreak,
|
||||||
|
longestFast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: EngagementTriggerAgent ────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function fireTrigger(
|
||||||
|
signal: RiskSignal,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<TriggerResult> {
|
||||||
|
const { userId, currentStreak } = signal;
|
||||||
|
|
||||||
|
// Escalate to weekly_digest if streak has dropped to zero
|
||||||
|
const action: EngagementAction = currentStreak === 0 ? 'weekly_digest' : 'streak_risk';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nomgapPushFire(
|
||||||
|
{ type: action, userId },
|
||||||
|
{ token: opts.token, requestId: opts.requestId }
|
||||||
|
);
|
||||||
|
return { userId, action, fired: true };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
action,
|
||||||
|
fired: false,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: EngagementReportAgent ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
function buildEngagementReport(
|
||||||
|
runId: string,
|
||||||
|
signals: RiskSignal[],
|
||||||
|
results: TriggerResult[]
|
||||||
|
): EngagementReport {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const fired = results.filter(r => r.fired).length;
|
||||||
|
const skipped = results.filter(r => !r.fired && !r.error).length;
|
||||||
|
const failed = results.filter(r => !!r.error).length;
|
||||||
|
const zeroStreakCount = signals.filter(s => s.currentStreak === 0).length;
|
||||||
|
|
||||||
|
const summary =
|
||||||
|
signals.length === 0
|
||||||
|
? 'No at-risk users detected in the specified window.'
|
||||||
|
: `${fired} engagement push(es) fired for ${signals.length} at-risk user(s). ${zeroStreakCount} escalated to weekly_digest (streak = 0). ${failed} failed.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
runId,
|
||||||
|
productId: 'nomgap',
|
||||||
|
usersScanned: signals.length,
|
||||||
|
atRiskCount: signals.length,
|
||||||
|
zeroStreakCount,
|
||||||
|
fired,
|
||||||
|
skipped,
|
||||||
|
failed,
|
||||||
|
perUser: results,
|
||||||
|
summary,
|
||||||
|
generatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runEngagementPipeline(
|
||||||
|
userIdFilter: string | null,
|
||||||
|
fromTime: string,
|
||||||
|
toTime: string,
|
||||||
|
req: McpToolRequest
|
||||||
|
): Promise<EngagementReport> {
|
||||||
|
const runId = randomUUID();
|
||||||
|
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'detect', userIdFilter }, 'StreakRiskDetectorAgent start');
|
||||||
|
const signals = await detectAtRiskUsers(userIdFilter, fromTime, toTime, opts);
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'detect', atRiskCount: signals.length },
|
||||||
|
'StreakRiskDetectorAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'trigger', count: signals.length }, 'EngagementTriggerAgent start');
|
||||||
|
const results: TriggerResult[] = [];
|
||||||
|
for (const signal of signals) {
|
||||||
|
const result = await fireTrigger(signal, opts);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'trigger', fired: results.filter(r => r.fired).length },
|
||||||
|
'EngagementTriggerAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'report' }, 'EngagementReportAgent start');
|
||||||
|
const report = buildEngagementReport(runId, signals, results);
|
||||||
|
req.log.info({ runId, stepId: 'report', summary: report.summary }, 'EngagementReportAgent done');
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
registerTool({
|
||||||
|
name: 'nomgap.engagement.recover',
|
||||||
|
description:
|
||||||
|
'A2A pipeline: detects at-risk NomGap users from streak_risk telemetry events, fires streak_risk push notifications (or weekly_digest for zero-streak users), and returns a per-user engagement action report. Optionally target a single userId. Requires admin role.',
|
||||||
|
requiredRole: 'admin',
|
||||||
|
inputSchema: z.object({
|
||||||
|
userId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Target a specific user (omit to process all at-risk users from telemetry)'),
|
||||||
|
from: z
|
||||||
|
.string()
|
||||||
|
.datetime()
|
||||||
|
.optional()
|
||||||
|
.describe('Start of risk signal window (default: 24h ago)'),
|
||||||
|
to: z.string().datetime().optional().describe('End of risk signal window (default: now)'),
|
||||||
|
}),
|
||||||
|
async execute(args, req) {
|
||||||
|
const now = new Date();
|
||||||
|
const toTime = args.to ?? now.toISOString();
|
||||||
|
const fromTime = args.from ?? new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
return runEngagementPipeline(args.userId ?? null, fromTime, toTime, req);
|
||||||
|
},
|
||||||
|
});
|
||||||
225
services/mcp-server/src/modules/a2a/memory-curation-pipeline.ts
Normal file
225
services/mcp-server/src/modules/a2a/memory-curation-pipeline.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* MemoryCurationAgent — A2A pipeline for JarvisJr agent memory hygiene.
|
||||||
|
*
|
||||||
|
* Agent roster (3 steps):
|
||||||
|
* 1. AgentInventoryAgent — list all agents owned by a user
|
||||||
|
* 2. MemoryScanAgent — per agent: list memories below importance threshold
|
||||||
|
* 3. CurationReportAgent — prune stale entries + return per-agent curation report
|
||||||
|
*
|
||||||
|
* MCP tools:
|
||||||
|
* jarvis.memory.curate(importanceThreshold?, dryRun?) — run pipeline across all agents
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { registerTool } from '../tools/registry.js';
|
||||||
|
import type { McpToolRequest } from '../tools/types.js';
|
||||||
|
import {
|
||||||
|
jarvisAgentsList,
|
||||||
|
jarvisMemoryList,
|
||||||
|
jarvisMemoryPrune,
|
||||||
|
type JarvisMemoryDoc,
|
||||||
|
} from '../../lib/jarvis-client.js';
|
||||||
|
import { config } from '../../lib/config.js';
|
||||||
|
|
||||||
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface AgentMemoryScan {
|
||||||
|
agentId: string;
|
||||||
|
agentName: string;
|
||||||
|
totalMemories: number;
|
||||||
|
staleMemories: JarvisMemoryDoc[];
|
||||||
|
expiredMemories: JarvisMemoryDoc[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurationAction {
|
||||||
|
agentId: string;
|
||||||
|
agentName: string;
|
||||||
|
staleCount: number;
|
||||||
|
expiredCount: number;
|
||||||
|
pruned: number;
|
||||||
|
skipped: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryCurationReport {
|
||||||
|
runId: string;
|
||||||
|
productId: 'jarvisjr';
|
||||||
|
importanceThreshold: number;
|
||||||
|
dryRun: boolean;
|
||||||
|
agentsScanned: number;
|
||||||
|
totalStaleFound: number;
|
||||||
|
totalExpiredFound: number;
|
||||||
|
totalPruned: number;
|
||||||
|
perAgent: CurationAction[];
|
||||||
|
summary: string;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: AgentInventoryAgent ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function listAgents(opts: {
|
||||||
|
token?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}): Promise<Array<{ id: string; name: string }>> {
|
||||||
|
try {
|
||||||
|
const result = await jarvisAgentsList({ limit: config.QUERY_MAX_LIMIT }, opts);
|
||||||
|
return result.agents.map(a => ({ id: a.id, name: a.name }));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: MemoryScanAgent ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function scanAgentMemory(
|
||||||
|
agentId: string,
|
||||||
|
agentName: string,
|
||||||
|
importanceThreshold: number,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<AgentMemoryScan> {
|
||||||
|
try {
|
||||||
|
const result = await jarvisMemoryList(agentId, { limit: config.QUERY_MAX_LIMIT }, opts);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const staleMemories = result.memories.filter(m => m.importance < importanceThreshold);
|
||||||
|
const expiredMemories = result.memories.filter(m => m.expiresAt && new Date(m.expiresAt) < now);
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
agentName,
|
||||||
|
totalMemories: result.total,
|
||||||
|
staleMemories,
|
||||||
|
expiredMemories,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { agentId, agentName, totalMemories: 0, staleMemories: [], expiredMemories: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: CurationReportAgent ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function curateAgentMemory(
|
||||||
|
scan: AgentMemoryScan,
|
||||||
|
dryRun: boolean,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<CurationAction> {
|
||||||
|
const staleCount = scan.staleMemories.length;
|
||||||
|
const expiredCount = scan.expiredMemories.length;
|
||||||
|
|
||||||
|
if (dryRun || (staleCount === 0 && expiredCount === 0)) {
|
||||||
|
return {
|
||||||
|
agentId: scan.agentId,
|
||||||
|
agentName: scan.agentName,
|
||||||
|
staleCount,
|
||||||
|
expiredCount,
|
||||||
|
pruned: 0,
|
||||||
|
skipped: dryRun || (staleCount === 0 && expiredCount === 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let pruned = 0;
|
||||||
|
try {
|
||||||
|
const result = await jarvisMemoryPrune(scan.agentId, opts);
|
||||||
|
pruned = result.pruned;
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentId: scan.agentId,
|
||||||
|
agentName: scan.agentName,
|
||||||
|
staleCount,
|
||||||
|
expiredCount,
|
||||||
|
pruned,
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runMemoryCurationPipeline(
|
||||||
|
importanceThreshold: number,
|
||||||
|
dryRun: boolean,
|
||||||
|
req: McpToolRequest
|
||||||
|
): Promise<MemoryCurationReport> {
|
||||||
|
const runId = randomUUID();
|
||||||
|
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||||
|
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'inventory', dryRun, importanceThreshold },
|
||||||
|
'AgentInventoryAgent start'
|
||||||
|
);
|
||||||
|
const agents = await listAgents(opts);
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'inventory', agentCount: agents.length },
|
||||||
|
'AgentInventoryAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'scan', agentCount: agents.length }, 'MemoryScanAgent start');
|
||||||
|
const scans: AgentMemoryScan[] = [];
|
||||||
|
for (const agent of agents) {
|
||||||
|
const scan = await scanAgentMemory(agent.id, agent.name, importanceThreshold, opts);
|
||||||
|
scans.push(scan);
|
||||||
|
}
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'scan', totalStale: scans.reduce((s, x) => s + x.staleMemories.length, 0) },
|
||||||
|
'MemoryScanAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'curate', dryRun }, 'CurationReportAgent start');
|
||||||
|
const actions: CurationAction[] = [];
|
||||||
|
for (const scan of scans) {
|
||||||
|
const action = await curateAgentMemory(scan, dryRun, opts);
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalStaleFound = actions.reduce((s, a) => s + a.staleCount, 0);
|
||||||
|
const totalExpiredFound = actions.reduce((s, a) => s + a.expiredCount, 0);
|
||||||
|
const totalPruned = actions.reduce((s, a) => s + a.pruned, 0);
|
||||||
|
|
||||||
|
const summary = dryRun
|
||||||
|
? `DRY RUN: ${totalStaleFound} stale + ${totalExpiredFound} expired memories found across ${agents.length} agents. No pruning performed.`
|
||||||
|
: totalPruned > 0
|
||||||
|
? `Pruned ${totalPruned} entries across ${actions.filter(a => !a.skipped).length} agents (${totalStaleFound} stale, ${totalExpiredFound} expired found).`
|
||||||
|
: `No memories required pruning across ${agents.length} agents.`;
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'curate', totalPruned, summary }, 'CurationReportAgent done');
|
||||||
|
|
||||||
|
return {
|
||||||
|
runId,
|
||||||
|
productId: 'jarvisjr',
|
||||||
|
importanceThreshold,
|
||||||
|
dryRun,
|
||||||
|
agentsScanned: agents.length,
|
||||||
|
totalStaleFound,
|
||||||
|
totalExpiredFound,
|
||||||
|
totalPruned,
|
||||||
|
perAgent: actions,
|
||||||
|
summary,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
registerTool({
|
||||||
|
name: 'jarvis.memory.curate',
|
||||||
|
description:
|
||||||
|
'A2A pipeline: scans all JarvisJr coaching agents, identifies memories below an importance threshold or past their expiry date, prunes stale entries, and returns a per-agent hygiene report. Use dryRun=true to preview without pruning. Requires admin role.',
|
||||||
|
requiredRole: 'admin',
|
||||||
|
inputSchema: z.object({
|
||||||
|
importanceThreshold: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(1)
|
||||||
|
.default(0.3)
|
||||||
|
.describe('Memories with importance below this value are considered stale (default 0.3)'),
|
||||||
|
dryRun: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe('If true, scan and report without pruning any memories'),
|
||||||
|
}),
|
||||||
|
async execute(args, req) {
|
||||||
|
return runMemoryCurationPipeline(args.importanceThreshold, args.dryRun, req);
|
||||||
|
},
|
||||||
|
});
|
||||||
252
services/mcp-server/src/modules/a2a/triage-quality-pipeline.ts
Normal file
252
services/mcp-server/src/modules/a2a/triage-quality-pipeline.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/**
|
||||||
|
* TriageQualityAgent — A2A pipeline for MindLyst memory triage quality improvement.
|
||||||
|
*
|
||||||
|
* Agent roster (3 steps):
|
||||||
|
* 1. LowConfidenceCollectorAgent — list memory items, filter for confidenceScore below threshold
|
||||||
|
* 2. RetriageAgent — re-run extraction on each low-confidence item
|
||||||
|
* 3. TriageQualityReportAgent — compare old vs new routing, auto-reassign if brainId changed
|
||||||
|
*
|
||||||
|
* MCP tools:
|
||||||
|
* mindlyst.memory.triageQuality(confidenceThreshold?, dryRun?) — run pipeline
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { registerTool } from '../tools/registry.js';
|
||||||
|
import type { McpToolRequest } from '../tools/types.js';
|
||||||
|
import {
|
||||||
|
mindlystMemoryList,
|
||||||
|
mindlystMemoryRetriage,
|
||||||
|
mindlystMemoryReassign,
|
||||||
|
} from '../../lib/mindlyst-client.js';
|
||||||
|
import { config } from '../../lib/config.js';
|
||||||
|
|
||||||
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface LowConfidenceItem {
|
||||||
|
id: string;
|
||||||
|
confidenceScore: number;
|
||||||
|
currentBrainId: string | null;
|
||||||
|
contentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RetriageResult {
|
||||||
|
itemId: string;
|
||||||
|
oldConfidenceScore: number;
|
||||||
|
newConfidenceScore: number;
|
||||||
|
oldBrainId: string | null;
|
||||||
|
newBrainId: string | null;
|
||||||
|
brainChanged: boolean;
|
||||||
|
reassigned: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriageQualityReport {
|
||||||
|
runId: string;
|
||||||
|
productId: 'mindlyst';
|
||||||
|
confidenceThreshold: number;
|
||||||
|
dryRun: boolean;
|
||||||
|
totalFetched: number;
|
||||||
|
lowConfidenceCount: number;
|
||||||
|
retriaged: number;
|
||||||
|
improved: number;
|
||||||
|
brainChanges: number;
|
||||||
|
reassigned: number;
|
||||||
|
failed: number;
|
||||||
|
perItem: RetriageResult[];
|
||||||
|
summary: string;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: LowConfidenceCollectorAgent ───────────────────────────────────────
|
||||||
|
|
||||||
|
async function collectLowConfidenceItems(
|
||||||
|
confidenceThreshold: number,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<{ items: LowConfidenceItem[]; totalFetched: number }> {
|
||||||
|
try {
|
||||||
|
const result = await mindlystMemoryList({ limit: config.QUERY_MAX_LIMIT }, opts);
|
||||||
|
|
||||||
|
const allItems = result.items;
|
||||||
|
const lowConfidence = allItems
|
||||||
|
.filter(item => item.triageResult.confidenceScore < confidenceThreshold)
|
||||||
|
.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
confidenceScore: item.triageResult.confidenceScore,
|
||||||
|
currentBrainId: item.brainIds[0] ?? null,
|
||||||
|
contentType: item.triageResult.contentType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { items: lowConfidence, totalFetched: allItems.length };
|
||||||
|
} catch {
|
||||||
|
return { items: [], totalFetched: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: RetriageAgent ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function retriageItem(
|
||||||
|
item: LowConfidenceItem,
|
||||||
|
dryRun: boolean,
|
||||||
|
opts: { token?: string; requestId?: string }
|
||||||
|
): Promise<RetriageResult> {
|
||||||
|
if (dryRun) {
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
oldConfidenceScore: item.confidenceScore,
|
||||||
|
newConfidenceScore: item.confidenceScore,
|
||||||
|
oldBrainId: item.currentBrainId,
|
||||||
|
newBrainId: item.currentBrainId,
|
||||||
|
brainChanged: false,
|
||||||
|
reassigned: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await mindlystMemoryRetriage(item.id, opts);
|
||||||
|
const newConfidence = updated.triageResult.confidenceScore;
|
||||||
|
const newBrainId = updated.triageResult.suggestedBrainId ?? updated.brainIds[0] ?? null;
|
||||||
|
const brainChanged = newBrainId !== null && newBrainId !== item.currentBrainId;
|
||||||
|
|
||||||
|
let reassigned = false;
|
||||||
|
if (brainChanged && newBrainId) {
|
||||||
|
try {
|
||||||
|
await mindlystMemoryReassign(item.id, newBrainId, opts);
|
||||||
|
reassigned = true;
|
||||||
|
} catch {
|
||||||
|
// best-effort reassignment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
oldConfidenceScore: item.confidenceScore,
|
||||||
|
newConfidenceScore: newConfidence,
|
||||||
|
oldBrainId: item.currentBrainId,
|
||||||
|
newBrainId,
|
||||||
|
brainChanged,
|
||||||
|
reassigned,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
oldConfidenceScore: item.confidenceScore,
|
||||||
|
newConfidenceScore: item.confidenceScore,
|
||||||
|
oldBrainId: item.currentBrainId,
|
||||||
|
newBrainId: item.currentBrainId,
|
||||||
|
brainChanged: false,
|
||||||
|
reassigned: false,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: TriageQualityReportAgent ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function buildTriageReport(
|
||||||
|
runId: string,
|
||||||
|
confidenceThreshold: number,
|
||||||
|
dryRun: boolean,
|
||||||
|
totalFetched: number,
|
||||||
|
results: RetriageResult[]
|
||||||
|
): TriageQualityReport {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const improved = results.filter(
|
||||||
|
r => r.newConfidenceScore > r.oldConfidenceScore && !r.error
|
||||||
|
).length;
|
||||||
|
const brainChanges = results.filter(r => r.brainChanged).length;
|
||||||
|
const reassigned = results.filter(r => r.reassigned).length;
|
||||||
|
const failed = results.filter(r => !!r.error).length;
|
||||||
|
|
||||||
|
const summary = dryRun
|
||||||
|
? `DRY RUN: ${results.length} items below confidence threshold ${confidenceThreshold} found. No retriaging performed.`
|
||||||
|
: results.length === 0
|
||||||
|
? `All ${totalFetched} memory items meet confidence threshold ${confidenceThreshold}. No action needed.`
|
||||||
|
: `Retriaged ${results.length - failed}/${results.length} items. ${improved} improved confidence, ${brainChanges} brain routing changes, ${reassigned} items reassigned. ${failed} failed.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
runId,
|
||||||
|
productId: 'mindlyst',
|
||||||
|
confidenceThreshold,
|
||||||
|
dryRun,
|
||||||
|
totalFetched,
|
||||||
|
lowConfidenceCount: results.length,
|
||||||
|
retriaged: results.length - failed,
|
||||||
|
improved,
|
||||||
|
brainChanges,
|
||||||
|
reassigned,
|
||||||
|
failed,
|
||||||
|
perItem: results,
|
||||||
|
summary,
|
||||||
|
generatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runTriageQualityPipeline(
|
||||||
|
confidenceThreshold: number,
|
||||||
|
dryRun: boolean,
|
||||||
|
req: McpToolRequest
|
||||||
|
): Promise<TriageQualityReport> {
|
||||||
|
const runId = randomUUID();
|
||||||
|
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||||
|
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'collect', confidenceThreshold, dryRun },
|
||||||
|
'LowConfidenceCollectorAgent start'
|
||||||
|
);
|
||||||
|
const { items, totalFetched } = await collectLowConfidenceItems(confidenceThreshold, opts);
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'collect', totalFetched, lowConfidenceCount: items.length },
|
||||||
|
'LowConfidenceCollectorAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'retriage', count: items.length, dryRun }, 'RetriageAgent start');
|
||||||
|
const results: RetriageResult[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const result = await retriageItem(item, dryRun, opts);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
req.log.info(
|
||||||
|
{
|
||||||
|
runId,
|
||||||
|
stepId: 'retriage',
|
||||||
|
improved: results.filter(r => r.newConfidenceScore > r.oldConfidenceScore).length,
|
||||||
|
},
|
||||||
|
'RetriageAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
req.log.info({ runId, stepId: 'report' }, 'TriageQualityReportAgent start');
|
||||||
|
const report = buildTriageReport(runId, confidenceThreshold, dryRun, totalFetched, results);
|
||||||
|
req.log.info(
|
||||||
|
{ runId, stepId: 'report', summary: report.summary },
|
||||||
|
'TriageQualityReportAgent done'
|
||||||
|
);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
registerTool({
|
||||||
|
name: 'mindlyst.memory.triageQuality',
|
||||||
|
description:
|
||||||
|
'A2A pipeline: finds MindLyst memory items with confidenceScore below a threshold, re-runs extraction on each, and auto-reassigns items where brain routing has changed. Returns a quality improvement report with confidence deltas and brain reassignment counts. Use dryRun=true to preview. Requires admin role.',
|
||||||
|
requiredRole: 'admin',
|
||||||
|
inputSchema: z.object({
|
||||||
|
confidenceThreshold: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(1)
|
||||||
|
.default(0.5)
|
||||||
|
.describe('Items with confidenceScore below this are re-triaged (default 0.5)'),
|
||||||
|
dryRun: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe('If true, collect and count only — do not retriage or reassign'),
|
||||||
|
}),
|
||||||
|
async execute(args, req) {
|
||||||
|
return runTriageQualityPipeline(args.confidenceThreshold, args.dryRun, req);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -42,6 +42,9 @@ import './modules/a2a/sync-diagnostics-pipeline.js';
|
|||||||
import './modules/a2a/transcript-extraction-pipeline.js';
|
import './modules/a2a/transcript-extraction-pipeline.js';
|
||||||
import './modules/a2a/sync-conflict-pipeline.js';
|
import './modules/a2a/sync-conflict-pipeline.js';
|
||||||
import './modules/a2a/route-safety-pipeline.js';
|
import './modules/a2a/route-safety-pipeline.js';
|
||||||
|
import './modules/a2a/memory-curation-pipeline.js';
|
||||||
|
import './modules/a2a/engagement-pipeline.js';
|
||||||
|
import './modules/a2a/triage-quality-pipeline.js';
|
||||||
import './modules/mindlyst/mindlyst-tools.js';
|
import './modules/mindlyst/mindlyst-tools.js';
|
||||||
import './modules/lysnrai/lysnrai-tools.js';
|
import './modules/lysnrai/lysnrai-tools.js';
|
||||||
import './modules/jarvis/jarvis-tools.js';
|
import './modules/jarvis/jarvis-tools.js';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user