Shoraky commited on
Commit
d4e2c1f
·
1 Parent(s): 1e2bd68

Reuse reference reads within session feedback generation

Browse files
Files changed (2) hide show
  1. src/agent-session.ts +23 -10
  2. 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 = new Map<string, ActionResult[] | null>();
61
  if (currentSession) referenceResults.set(currentSession.id, currentSession.actions);
62
  for (let from = 0; ; from += 100) {
63
- const { data: examples, error: examplesError } = await db.from("annotations")
64
- .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)))")
65
- .eq("actions.type", actionType)
66
- .neq("action_id", current.id)
67
- .order("action_id").range(from, from + 99);
68
- if (examplesError) throw new Error(`Unable to load human annotations: ${examplesError.message}`);
 
 
 
 
 
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");