Spaces:
Running
Running
Reuse reference reads within session feedback generation
Browse files- src/agent-session.ts +23 -10
- tests/agent-chat.test.ts +4 -0
src/agent-session.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { env } from "./config.js";
|
|
| 4 |
type ActionResult = { id: string; label: string; rawAnalytics?: Record<string, unknown>; inActionMetrics?: { name: string; value: unknown; unit?: string }[] };
|
| 5 |
type SessionResult = { id: string; playerId: string; actions: ActionResult[] };
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
function numericMetrics(action: ActionResult) {
|
| 8 |
const values = new Map<string, number>();
|
| 9 |
const metrics = [...Object.entries(action.rawAnalytics ?? {}).map(([name, value]) => ({ name, value })), ...(action.inActionMetrics ?? [])];
|
|
@@ -54,20 +61,25 @@ export async function analysisResults(sessionId: string, userId: string, isAdmin
|
|
| 54 |
return await response.json() as SessionResult;
|
| 55 |
}
|
| 56 |
|
| 57 |
-
export async function actionReferences(db: SupabaseClient, current: ActionResult, actionType: string, currentSession?: SessionResult) {
|
| 58 |
const closest = [];
|
| 59 |
if (!numericMetrics(current).size) return [];
|
| 60 |
-
const referenceResults =
|
| 61 |
if (currentSession) referenceResults.set(currentSession.id, currentSession.actions);
|
| 62 |
for (let from = 0; ; from += 100) {
|
| 63 |
-
const
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
.
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
const referenceRows = (examples ?? []).filter((row: any) =>
|
| 70 |
-
(row.annotations_notes ?? []).some((note: { note: string }) => typeof note.note === "string" && note.note.trim().length > 0))
|
| 71 |
.map((row: any) => ({ ...row, actions: Array.isArray(row.actions) ? row.actions[0] : row.actions }));
|
| 72 |
const sessions = new Map<string, string>();
|
| 73 |
for (const row of referenceRows) {
|
|
@@ -125,10 +137,11 @@ export async function generateSessionFeedback(db: SupabaseClient, sessionId: str
|
|
| 125 |
if (actionsError) throw new Error(`Unable to load completed actions: ${actionsError.message}`);
|
| 126 |
|
| 127 |
const actions = [];
|
|
|
|
| 128 |
for (const action of currentActions ?? []) {
|
| 129 |
const current = actionResults.get(action.action_id);
|
| 130 |
if (!current) continue;
|
| 131 |
-
const references = await actionReferences(db, current, action.type, result);
|
| 132 |
actions.push({ action_id: action.action_id, action_type: action.type,
|
| 133 |
results: { metrics: current.inActionMetrics ?? [], analytics: current.rawAnalytics ?? {} }, references });
|
| 134 |
}
|
|
|
|
| 4 |
type ActionResult = { id: string; label: string; rawAnalytics?: Record<string, unknown>; inActionMetrics?: { name: string; value: unknown; unit?: string }[] };
|
| 5 |
type SessionResult = { id: string; playerId: string; actions: ActionResult[] };
|
| 6 |
|
| 7 |
+
// Scoped to one generation: reuse reads without serving stale coach annotations
|
| 8 |
+
// to a later request or retaining player data in a process-wide cache.
|
| 9 |
+
class ReferenceCache {
|
| 10 |
+
results = new Map<string, ActionResult[] | null>();
|
| 11 |
+
pages = new Map<string, any[]>();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
function numericMetrics(action: ActionResult) {
|
| 15 |
const values = new Map<string, number>();
|
| 16 |
const metrics = [...Object.entries(action.rawAnalytics ?? {}).map(([name, value]) => ({ name, value })), ...(action.inActionMetrics ?? [])];
|
|
|
|
| 61 |
return await response.json() as SessionResult;
|
| 62 |
}
|
| 63 |
|
| 64 |
+
export async function actionReferences(db: SupabaseClient, current: ActionResult, actionType: string, currentSession?: SessionResult, cache = new ReferenceCache()) {
|
| 65 |
const closest = [];
|
| 66 |
if (!numericMetrics(current).size) return [];
|
| 67 |
+
const referenceResults = cache.results;
|
| 68 |
if (currentSession) referenceResults.set(currentSession.id, currentSession.actions);
|
| 69 |
for (let from = 0; ; from += 100) {
|
| 70 |
+
const pageKey = JSON.stringify([actionType, from]);
|
| 71 |
+
let examples = cache.pages.get(pageKey);
|
| 72 |
+
if (!examples) {
|
| 73 |
+
const { data, error: examplesError } = await db.from("annotations")
|
| 74 |
+
.select("action_id,score,annotations_notes!inner(note),actions!inner(action_id,session_id,type,sessions!inner(user_id,player_id,players!inner(gender,bdate,height,weight,position)))")
|
| 75 |
+
.eq("actions.type", actionType)
|
| 76 |
+
.order("action_id").range(from, from + 99);
|
| 77 |
+
if (examplesError) throw new Error(`Unable to load human annotations: ${examplesError.message}`);
|
| 78 |
+
examples = data ?? [];
|
| 79 |
+
cache.pages.set(pageKey, examples);
|
| 80 |
+
}
|
| 81 |
const referenceRows = (examples ?? []).filter((row: any) =>
|
| 82 |
+
row.action_id !== current.id && (row.annotations_notes ?? []).some((note: { note: string }) => typeof note.note === "string" && note.note.trim().length > 0))
|
| 83 |
.map((row: any) => ({ ...row, actions: Array.isArray(row.actions) ? row.actions[0] : row.actions }));
|
| 84 |
const sessions = new Map<string, string>();
|
| 85 |
for (const row of referenceRows) {
|
|
|
|
| 137 |
if (actionsError) throw new Error(`Unable to load completed actions: ${actionsError.message}`);
|
| 138 |
|
| 139 |
const actions = [];
|
| 140 |
+
const referenceCache = new ReferenceCache();
|
| 141 |
for (const action of currentActions ?? []) {
|
| 142 |
const current = actionResults.get(action.action_id);
|
| 143 |
if (!current) continue;
|
| 144 |
+
const references = await actionReferences(db, current, action.type, result, referenceCache);
|
| 145 |
actions.push({ action_id: action.action_id, action_type: action.type,
|
| 146 |
results: { metrics: current.inActionMetrics ?? [], analytics: current.rawAnalytics ?? {} }, references });
|
| 147 |
}
|
tests/agent-chat.test.ts
CHANGED
|
@@ -250,10 +250,14 @@ test("action chat sends measurements and annotated references on every message a
|
|
| 250 |
assert.equal(actionDistance(sample({ active_foot_speed: 10, ball_speed: 10, touch_frame: 1 }), { id: "metric", label: "Pass",
|
| 251 |
inActionMetrics: [{ name: "Active Foot Speed", value: 10 }, { name: "Ball Speed", value: 9999 }, { name: "Touch Frame", value: 9999 }] }), 0);
|
| 252 |
const db = createClient(upstreamOrigin, "test-service", { auth: { persistSession: false } });
|
|
|
|
| 253 |
await generateSessionFeedback(db, sessionId, owner);
|
|
|
|
| 254 |
assert.deepEqual(generations[0].actions.find((item) => item.action_id === actionId)?.references.map((item) => item.action_id), expectedReferences);
|
| 255 |
assert.deepEqual(generations[0].actions.find((item) => item.action_id === otherActionId)?.references.map((item) => item.action_id), ["shoot-reference"]);
|
| 256 |
assert.equal(mutations, 6, "automatic generation still persists feedback for both actions and the session");
|
|
|
|
|
|
|
| 257 |
} finally {
|
| 258 |
if (child.exitCode === null && child.signalCode === null) {
|
| 259 |
const exited = once(child, "exit");
|
|
|
|
| 250 |
assert.equal(actionDistance(sample({ active_foot_speed: 10, ball_speed: 10, touch_frame: 1 }), { id: "metric", label: "Pass",
|
| 251 |
inActionMetrics: [{ name: "Active Foot Speed", value: 10 }, { name: "Ball Speed", value: 9999 }, { name: "Touch Frame", value: 9999 }] }), 0);
|
| 252 |
const db = createClient(upstreamOrigin, "test-service", { auth: { persistSession: false } });
|
| 253 |
+
const readsBeforeGeneration = referenceReads;
|
| 254 |
await generateSessionFeedback(db, sessionId, owner);
|
| 255 |
+
assert.equal(referenceReads - readsBeforeGeneration, 2, "all actions in a generation share reference session downloads");
|
| 256 |
assert.deepEqual(generations[0].actions.find((item) => item.action_id === actionId)?.references.map((item) => item.action_id), expectedReferences);
|
| 257 |
assert.deepEqual(generations[0].actions.find((item) => item.action_id === otherActionId)?.references.map((item) => item.action_id), ["shoot-reference"]);
|
| 258 |
assert.equal(mutations, 6, "automatic generation still persists feedback for both actions and the session");
|
| 259 |
+
await generateSessionFeedback(db, sessionId, owner);
|
| 260 |
+
assert.equal(referenceReads - readsBeforeGeneration, 4, "a new generation fetches fresh reference measurements");
|
| 261 |
} finally {
|
| 262 |
if (child.exitCode === null && child.signalCode === null) {
|
| 263 |
const exited = once(child, "exit");
|