inspect-ai 0.3.81__py3-none-any.whl → 0.3.82__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. inspect_ai/_cli/eval.py +35 -2
  2. inspect_ai/_cli/util.py +44 -1
  3. inspect_ai/_display/core/config.py +1 -1
  4. inspect_ai/_display/core/display.py +13 -4
  5. inspect_ai/_display/core/results.py +1 -1
  6. inspect_ai/_display/textual/widgets/task_detail.py +5 -4
  7. inspect_ai/_eval/eval.py +38 -1
  8. inspect_ai/_eval/evalset.py +5 -0
  9. inspect_ai/_eval/run.py +5 -2
  10. inspect_ai/_eval/task/log.py +53 -6
  11. inspect_ai/_eval/task/run.py +51 -10
  12. inspect_ai/_util/constants.py +2 -0
  13. inspect_ai/_util/file.py +17 -1
  14. inspect_ai/_util/json.py +36 -1
  15. inspect_ai/_view/server.py +113 -1
  16. inspect_ai/_view/www/App.css +1 -1
  17. inspect_ai/_view/www/dist/assets/index.css +518 -296
  18. inspect_ai/_view/www/dist/assets/index.js +38803 -36307
  19. inspect_ai/_view/www/eslint.config.mjs +1 -1
  20. inspect_ai/_view/www/log-schema.json +13 -0
  21. inspect_ai/_view/www/node_modules/flatted/python/flatted.py +149 -0
  22. inspect_ai/_view/www/package.json +8 -2
  23. inspect_ai/_view/www/src/App.tsx +151 -855
  24. inspect_ai/_view/www/src/api/api-browser.ts +176 -5
  25. inspect_ai/_view/www/src/api/api-vscode.ts +75 -1
  26. inspect_ai/_view/www/src/api/client-api.ts +66 -10
  27. inspect_ai/_view/www/src/api/jsonrpc.ts +2 -0
  28. inspect_ai/_view/www/src/api/types.ts +107 -2
  29. inspect_ai/_view/www/src/appearance/icons.ts +1 -0
  30. inspect_ai/_view/www/src/components/AsciinemaPlayer.tsx +3 -3
  31. inspect_ai/_view/www/src/components/DownloadPanel.tsx +2 -2
  32. inspect_ai/_view/www/src/components/ExpandablePanel.tsx +56 -61
  33. inspect_ai/_view/www/src/components/FindBand.tsx +17 -9
  34. inspect_ai/_view/www/src/components/HumanBaselineView.tsx +1 -1
  35. inspect_ai/_view/www/src/components/JsonPanel.tsx +14 -24
  36. inspect_ai/_view/www/src/components/LargeModal.tsx +2 -35
  37. inspect_ai/_view/www/src/components/LightboxCarousel.tsx +27 -11
  38. inspect_ai/_view/www/src/components/LiveVirtualList.module.css +11 -0
  39. inspect_ai/_view/www/src/components/LiveVirtualList.tsx +177 -0
  40. inspect_ai/_view/www/src/components/MarkdownDiv.tsx +3 -3
  41. inspect_ai/_view/www/src/components/MessageBand.tsx +14 -9
  42. inspect_ai/_view/www/src/components/MorePopOver.tsx +3 -3
  43. inspect_ai/_view/www/src/components/NavPills.tsx +20 -8
  44. inspect_ai/_view/www/src/components/NoContentsPanel.module.css +12 -0
  45. inspect_ai/_view/www/src/components/NoContentsPanel.tsx +20 -0
  46. inspect_ai/_view/www/src/components/ProgressBar.module.css +5 -4
  47. inspect_ai/_view/www/src/components/ProgressBar.tsx +3 -2
  48. inspect_ai/_view/www/src/components/PulsingDots.module.css +81 -0
  49. inspect_ai/_view/www/src/components/PulsingDots.tsx +45 -0
  50. inspect_ai/_view/www/src/components/TabSet.tsx +4 -37
  51. inspect_ai/_view/www/src/components/ToolButton.tsx +3 -4
  52. inspect_ai/_view/www/src/index.tsx +26 -94
  53. inspect_ai/_view/www/src/logfile/remoteLogFile.ts +9 -1
  54. inspect_ai/_view/www/src/logfile/remoteZipFile.ts +30 -4
  55. inspect_ai/_view/www/src/metadata/RenderedContent.tsx +4 -6
  56. inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +1 -1
  57. inspect_ai/_view/www/src/samples/InlineSampleDisplay.module.css +9 -1
  58. inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +67 -28
  59. inspect_ai/_view/www/src/samples/SampleDialog.tsx +51 -22
  60. inspect_ai/_view/www/src/samples/SampleDisplay.module.css +4 -0
  61. inspect_ai/_view/www/src/samples/SampleDisplay.tsx +144 -90
  62. inspect_ai/_view/www/src/samples/SampleSummaryView.module.css +4 -0
  63. inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +82 -35
  64. inspect_ai/_view/www/src/samples/SamplesTools.tsx +23 -30
  65. inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +2 -1
  66. inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +1 -1
  67. inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +45 -53
  68. inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +4 -1
  69. inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +3 -0
  70. inspect_ai/_view/www/src/samples/chat/messages.ts +34 -0
  71. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.module.css +3 -0
  72. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +10 -1
  73. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +22 -46
  74. inspect_ai/_view/www/src/samples/descriptor/samplesDescriptor.tsx +25 -17
  75. inspect_ai/_view/www/src/samples/descriptor/score/ObjectScoreDescriptor.tsx +2 -1
  76. inspect_ai/_view/www/src/samples/descriptor/types.ts +6 -5
  77. inspect_ai/_view/www/src/samples/list/SampleFooter.module.css +21 -3
  78. inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +20 -1
  79. inspect_ai/_view/www/src/samples/list/SampleList.tsx +105 -85
  80. inspect_ai/_view/www/src/samples/list/SampleRow.module.css +6 -0
  81. inspect_ai/_view/www/src/samples/list/SampleRow.tsx +27 -14
  82. inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +29 -18
  83. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +28 -28
  84. inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +19 -9
  85. inspect_ai/_view/www/src/samples/sampleDataAdapter.ts +33 -0
  86. inspect_ai/_view/www/src/samples/sampleLimit.ts +2 -2
  87. inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +7 -9
  88. inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +7 -11
  89. inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +0 -13
  90. inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +0 -13
  91. inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +0 -13
  92. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +4 -0
  93. inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +10 -24
  94. inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +0 -13
  95. inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +4 -22
  96. inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +15 -24
  97. inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +0 -13
  98. inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +6 -28
  99. inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +24 -34
  100. inspect_ai/_view/www/src/samples/transcript/ToolEventView.module.css +4 -0
  101. inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +8 -13
  102. inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +197 -338
  103. inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.module.css +16 -0
  104. inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.tsx +44 -0
  105. inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +7 -4
  106. inspect_ai/_view/www/src/samples/transcript/event/EventPanel.tsx +52 -58
  107. inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.module.css +23 -0
  108. inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.tsx +27 -0
  109. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +30 -1
  110. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +102 -72
  111. inspect_ai/_view/www/src/scoring/utils.ts +87 -0
  112. inspect_ai/_view/www/src/state/appSlice.ts +244 -0
  113. inspect_ai/_view/www/src/state/hooks.ts +397 -0
  114. inspect_ai/_view/www/src/state/logPolling.ts +196 -0
  115. inspect_ai/_view/www/src/state/logSlice.ts +214 -0
  116. inspect_ai/_view/www/src/state/logsPolling.ts +118 -0
  117. inspect_ai/_view/www/src/state/logsSlice.ts +181 -0
  118. inspect_ai/_view/www/src/state/samplePolling.ts +311 -0
  119. inspect_ai/_view/www/src/state/sampleSlice.ts +127 -0
  120. inspect_ai/_view/www/src/state/sampleUtils.ts +21 -0
  121. inspect_ai/_view/www/src/state/scrolling.ts +206 -0
  122. inspect_ai/_view/www/src/state/store.ts +168 -0
  123. inspect_ai/_view/www/src/state/store_filter.ts +84 -0
  124. inspect_ai/_view/www/src/state/utils.ts +23 -0
  125. inspect_ai/_view/www/src/storage/index.ts +26 -0
  126. inspect_ai/_view/www/src/types/log.d.ts +2 -0
  127. inspect_ai/_view/www/src/types.ts +94 -32
  128. inspect_ai/_view/www/src/utils/attachments.ts +58 -23
  129. inspect_ai/_view/www/src/utils/logger.ts +52 -0
  130. inspect_ai/_view/www/src/utils/polling.ts +100 -0
  131. inspect_ai/_view/www/src/utils/react.ts +30 -0
  132. inspect_ai/_view/www/src/utils/vscode.ts +1 -1
  133. inspect_ai/_view/www/src/workspace/WorkSpace.tsx +181 -216
  134. inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +11 -53
  135. inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +8 -18
  136. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.module.css +1 -0
  137. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +40 -22
  138. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.module.css +0 -1
  139. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +98 -39
  140. inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.module.css +32 -0
  141. inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.tsx +32 -0
  142. inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +11 -13
  143. inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +6 -2
  144. inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +4 -4
  145. inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +28 -13
  146. inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +5 -10
  147. inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +4 -4
  148. inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.module.css +22 -0
  149. inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.tsx +19 -0
  150. inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +110 -115
  151. inspect_ai/_view/www/src/workspace/tabs/grouping.ts +37 -5
  152. inspect_ai/_view/www/src/workspace/tabs/types.ts +4 -0
  153. inspect_ai/_view/www/src/workspace/types.ts +4 -3
  154. inspect_ai/_view/www/src/workspace/utils.ts +4 -4
  155. inspect_ai/_view/www/vite.config.js +6 -0
  156. inspect_ai/_view/www/yarn.lock +370 -354
  157. inspect_ai/log/_condense.py +26 -0
  158. inspect_ai/log/_log.py +6 -3
  159. inspect_ai/log/_recorders/buffer/__init__.py +14 -0
  160. inspect_ai/log/_recorders/buffer/buffer.py +30 -0
  161. inspect_ai/log/_recorders/buffer/database.py +685 -0
  162. inspect_ai/log/_recorders/buffer/filestore.py +259 -0
  163. inspect_ai/log/_recorders/buffer/types.py +84 -0
  164. inspect_ai/log/_recorders/eval.py +2 -11
  165. inspect_ai/log/_recorders/types.py +30 -0
  166. inspect_ai/log/_transcript.py +27 -1
  167. inspect_ai/model/_call_tools.py +1 -0
  168. inspect_ai/model/_generate_config.py +2 -2
  169. inspect_ai/model/_model.py +1 -0
  170. inspect_ai/tool/_tool_support_helpers.py +4 -4
  171. inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -1
  172. inspect_ai/util/_subtask.py +1 -0
  173. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/METADATA +1 -1
  174. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/RECORD +178 -138
  175. inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +0 -22
  176. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/WHEEL +0 -0
  177. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/entry_points.txt +0 -0
  178. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/licenses/LICENSE +0 -0
  179. {inspect_ai-0.3.81.dist-info → inspect_ai-0.3.82.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,311 @@
1
+ import {
2
+ AttachmentData,
3
+ EventData,
4
+ SampleData,
5
+ SampleSummary,
6
+ } from "../api/types";
7
+ import { Event } from "../types";
8
+ import { resolveAttachments } from "../utils/attachments";
9
+ import { createLogger } from "../utils/logger";
10
+ import { createPolling } from "../utils/polling";
11
+ import { resolveSample } from "./sampleUtils"; // Import the shared utility
12
+ import { StoreState } from "./store";
13
+
14
+ const log = createLogger("samplePolling");
15
+
16
+ const kNoId = -1;
17
+ const kPollingInterval = 2;
18
+ const kPollingMaxRetries = 10;
19
+
20
+ // Keeps the state for polling (the last ids for events
21
+ // and attachments, the attachments and events, and
22
+ // a mapping from eventIds to event indexes to enable
23
+ // replacing events)
24
+ interface PollingState {
25
+ eventId: number;
26
+ attachmentId: number;
27
+
28
+ attachments: Record<string, string>;
29
+
30
+ eventMapping: Record<string, number>;
31
+ events: Event[];
32
+ }
33
+
34
+ export function createSamplePolling(
35
+ get: () => StoreState,
36
+ set: (fn: (state: StoreState) => void) => void,
37
+ ) {
38
+ // The polling function that will be returned
39
+ let currentPolling: ReturnType<typeof createPolling> | null = null;
40
+
41
+ // handle aborts
42
+ let abortController: AbortController;
43
+
44
+ // The inintial polling state
45
+ const pollingState: PollingState = {
46
+ eventId: kNoId,
47
+ attachmentId: kNoId,
48
+
49
+ eventMapping: {},
50
+ attachments: {},
51
+ events: [],
52
+ };
53
+
54
+ // Function to start polling for a specific log file
55
+ const startPolling = (logFile: string, summary: SampleSummary) => {
56
+ // Create a unique identifier for this polling session
57
+ const pollingId = `${logFile}:${summary.id}-${summary.epoch}`;
58
+ log.debug(`Start Polling ${pollingId}`);
59
+
60
+ // If we're already polling this resource, don't restart
61
+ if (currentPolling && currentPolling.name === pollingId) {
62
+ log.debug(`Aleady polling, ignoring start`);
63
+ return;
64
+ }
65
+
66
+ // Stop any existing polling first
67
+ if (currentPolling) {
68
+ log.debug(`Resetting existing polling`);
69
+ currentPolling.stop();
70
+
71
+ // Clear any current running events
72
+ set((state) => {
73
+ state.sample.runningEvents = [];
74
+ });
75
+
76
+ // Reset the current polling state
77
+ resetPollingState(pollingState);
78
+ }
79
+ abortController = new AbortController();
80
+
81
+ // Create the polling callback
82
+ log.debug(`Polling sample: ${summary.id}-${summary.epoch}`);
83
+ const pollCallback = async () => {
84
+ const state = get();
85
+
86
+ // Get the api
87
+ const api = state.api;
88
+ if (!api) {
89
+ throw new Error("Required API is missing");
90
+ }
91
+
92
+ if (!api.get_log_sample_data) {
93
+ throw new Error("Required API get_log_sample_data is undefined.");
94
+ }
95
+
96
+ if (abortController.signal.aborted) {
97
+ return false;
98
+ }
99
+
100
+ // Fetch sample data
101
+ const eventId = pollingState.eventId;
102
+ const attachmentId = pollingState.attachmentId;
103
+ const sampleDataResponse = await api.get_log_sample_data(
104
+ logFile,
105
+ summary.id,
106
+ summary.epoch,
107
+ eventId,
108
+ attachmentId,
109
+ );
110
+
111
+ if (abortController.signal.aborted) {
112
+ return false;
113
+ }
114
+
115
+ if (sampleDataResponse?.status === "NotFound") {
116
+ // A 404 from the server means that this sample
117
+ // has been flushed to the main eval file, no events
118
+ // are available and we should retrieve the data from the
119
+ // sample file itself.
120
+
121
+ // Stop polling since we now have the complete sample
122
+ stopPolling();
123
+
124
+ // Also fetch a fresh sample and clear the runnning Events
125
+ // (if there were ever running events)
126
+ if (state.sample.runningEvents.length > 0) {
127
+ try {
128
+ log.debug(
129
+ `LOADING COMPLETED SAMPLE AFTER FLUSH: ${summary.id}-${summary.epoch}`,
130
+ );
131
+ const sample = await api.get_log_sample(
132
+ logFile,
133
+ summary.id,
134
+ summary.epoch,
135
+ );
136
+
137
+ if (sample) {
138
+ const migratedSample = resolveSample(sample);
139
+
140
+ // Update the store with the completed sample
141
+ set((state) => {
142
+ state.sample.selectedSample = migratedSample;
143
+ state.sample.sampleStatus = "ok";
144
+ state.sample.runningEvents = [];
145
+ });
146
+ } else {
147
+ set((state) => {
148
+ state.sample.sampleStatus = "error";
149
+ state.sample.sampleError = new Error(
150
+ "Unable to load sample - an unknown error occurred",
151
+ );
152
+ state.sample.runningEvents = [];
153
+ });
154
+ }
155
+ } catch (e) {
156
+ set((state) => {
157
+ state.sample.sampleError = e as Error;
158
+ state.sample.sampleStatus = "error";
159
+ state.sample.runningEvents = [];
160
+ });
161
+ }
162
+ }
163
+
164
+ return false;
165
+ }
166
+
167
+ if (
168
+ sampleDataResponse?.status === "OK" &&
169
+ sampleDataResponse.sampleData
170
+ ) {
171
+ if (abortController.signal.aborted) {
172
+ return false;
173
+ }
174
+
175
+ if (sampleDataResponse.sampleData) {
176
+ // Process attachments
177
+ processAttachments(sampleDataResponse.sampleData, pollingState);
178
+
179
+ // Process events
180
+ const processedEvents = processEvents(
181
+ sampleDataResponse.sampleData,
182
+ pollingState,
183
+ );
184
+
185
+ // update max attachment id
186
+ if (sampleDataResponse.sampleData.attachments.length > 0) {
187
+ const maxAttachment = findMaxId(
188
+ sampleDataResponse.sampleData.attachments,
189
+ pollingState.attachmentId,
190
+ );
191
+ log.debug(`New max attachment ${maxAttachment}`);
192
+ pollingState.attachmentId = maxAttachment;
193
+ }
194
+
195
+ // update max event id
196
+ if (sampleDataResponse.sampleData.events.length > 0) {
197
+ const maxEvent = findMaxId(
198
+ sampleDataResponse.sampleData.events,
199
+ pollingState.eventId,
200
+ );
201
+ log.debug(`New max event ${maxEvent}`);
202
+ pollingState.eventId = maxEvent;
203
+ }
204
+
205
+ // Update the running events (ensure identity of runningEvents fails equality)
206
+ if (processedEvents) {
207
+ set((state) => {
208
+ state.sample.runningEvents = [...pollingState.events];
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ // Continue polling
215
+ return true;
216
+ };
217
+
218
+ // Create the polling instance
219
+ const polling = createPolling(pollingId, pollCallback, {
220
+ maxRetries: kPollingMaxRetries,
221
+ interval: kPollingInterval,
222
+ });
223
+
224
+ // Store the polling instance and start it
225
+ currentPolling = polling;
226
+ polling.start();
227
+ };
228
+
229
+ // Stop polling
230
+ const stopPolling = () => {
231
+ if (currentPolling) {
232
+ currentPolling.stop();
233
+ currentPolling = null;
234
+ }
235
+ };
236
+
237
+ const cleanup = () => {
238
+ log.debug(`CLEANUP`);
239
+ abortController.abort();
240
+ stopPolling();
241
+ };
242
+
243
+ return {
244
+ startPolling,
245
+ stopPolling,
246
+ cleanup,
247
+ };
248
+ }
249
+
250
+ const resetPollingState = (state: PollingState) => {
251
+ state.eventId = -1;
252
+ state.attachmentId = -1;
253
+ state.eventMapping = {};
254
+ state.attachments = {};
255
+ state.events = [];
256
+ };
257
+
258
+ function processAttachments(
259
+ sampleData: SampleData,
260
+ pollingState: PollingState,
261
+ ) {
262
+ log.debug(`Processing ${sampleData.attachments.length} attachments`);
263
+ Object.values(sampleData.attachments).forEach((v) => {
264
+ pollingState.attachments[v.hash] = v.content;
265
+ });
266
+ }
267
+
268
+ function processEvents(sampleData: SampleData, pollingState: PollingState) {
269
+ // Go through each event and resolve it, either appending or replacing
270
+ log.debug(`Processing ${sampleData.events.length} events`);
271
+ if (sampleData.events.length === 0) {
272
+ return false;
273
+ }
274
+
275
+ for (const eventData of sampleData.events) {
276
+ // Identify if this event id already has an event in the event list
277
+ const existingIndex = pollingState.eventMapping[eventData.event_id];
278
+
279
+ // Resolve attachments within this event
280
+ const resolvedEvent = resolveAttachments<Event>(
281
+ eventData.event,
282
+ pollingState.attachments,
283
+ );
284
+
285
+ if (existingIndex) {
286
+ // There is an existing event in the stream, replace it
287
+ log.debug(`Replace event ${existingIndex}`);
288
+ pollingState.events[existingIndex] = resolvedEvent;
289
+ } else {
290
+ // This is a new event, add to the event list and note
291
+ // its position
292
+ log.debug(`New event ${pollingState.events.length}`);
293
+
294
+ const currentIndex = pollingState.events.length;
295
+ pollingState.eventMapping[eventData.event_id] = currentIndex;
296
+ pollingState.events.push(resolvedEvent);
297
+ }
298
+ }
299
+ return true;
300
+ }
301
+
302
+ const findMaxId = (
303
+ items: EventData[] | AttachmentData[],
304
+ currentMax: number,
305
+ ) => {
306
+ if (items.length > 0) {
307
+ const newMax = Math.max(...items.map((i) => i.id), currentMax);
308
+ return newMax;
309
+ }
310
+ return currentMax;
311
+ };
@@ -0,0 +1,127 @@
1
+ import { SampleSummary } from "../api/types";
2
+ import { kSampleMessagesTabId } from "../constants";
3
+ import { SampleState, SampleStatus } from "../types";
4
+ import { EvalSample } from "../types/log";
5
+ import { createLogger } from "../utils/logger";
6
+ import { createSamplePolling } from "./samplePolling";
7
+ import { resolveSample } from "./sampleUtils"; // Import the shared utility
8
+ import { StoreState } from "./store";
9
+
10
+ const log = createLogger("sampleSlice");
11
+
12
+ export interface SampleSlice {
13
+ sample: SampleState;
14
+ sampleActions: {
15
+ // The actual sample data
16
+ setSelectedSample: (sample: EvalSample) => void;
17
+ clearSelectedSample: () => void;
18
+ setSampleStatus: (status: SampleStatus) => void;
19
+ setSampleError: (error: Error | undefined) => void;
20
+
21
+ // Loading
22
+ loadSample: (
23
+ logFile: string,
24
+ sampleSummary: SampleSummary,
25
+ ) => Promise<void>;
26
+ };
27
+ }
28
+
29
+ const initialState: SampleState = {
30
+ selectedSample: undefined,
31
+ sampleStatus: "ok",
32
+ sampleError: undefined,
33
+
34
+ // The resolved events
35
+ runningEvents: [],
36
+ };
37
+
38
+ export const createSampleSlice = (
39
+ set: (fn: (state: StoreState) => void) => void,
40
+ get: () => StoreState,
41
+ _store: any,
42
+ ): [SampleSlice, () => void] => {
43
+ // The sample poller
44
+ const samplePolling = createSamplePolling(get, set);
45
+
46
+ const slice = {
47
+ // Actions
48
+ sample: initialState,
49
+ sampleActions: {
50
+ setSelectedSample: (sample: EvalSample) => {
51
+ set((state) => {
52
+ state.sample.selectedSample = sample;
53
+ });
54
+ if (sample.events.length < 1) {
55
+ // If there are no events, use the messages tab as the default
56
+ get().appActions.setSampleTab(kSampleMessagesTabId);
57
+ }
58
+ },
59
+ clearSelectedSample: () =>
60
+ set((state) => {
61
+ state.sample.selectedSample = undefined;
62
+ }),
63
+ setSampleStatus: (status: SampleStatus) =>
64
+ set((state) => {
65
+ state.sample.sampleStatus = status;
66
+ }),
67
+ setSampleError: (error: Error | undefined) =>
68
+ set((state) => {
69
+ state.sample.sampleError = error;
70
+ }),
71
+ loadSample: async (logFile: string, sampleSummary: SampleSummary) => {
72
+ const sampleActions = get().sampleActions;
73
+
74
+ sampleActions.setSampleError(undefined);
75
+ sampleActions.setSampleStatus("loading");
76
+ try {
77
+ if (sampleSummary.completed !== false) {
78
+ log.debug(
79
+ `LOADING COMPLETED SAMPLE: ${sampleSummary.id}-${sampleSummary.epoch}`,
80
+ );
81
+ const sample = await get().api?.get_log_sample(
82
+ logFile,
83
+ sampleSummary.id,
84
+ sampleSummary.epoch,
85
+ );
86
+ if (sample) {
87
+ const migratedSample = resolveSample(sample);
88
+ sampleActions.setSelectedSample(migratedSample);
89
+ sampleActions.setSampleStatus("ok");
90
+ } else {
91
+ sampleActions.setSampleStatus("error");
92
+ throw new Error(
93
+ "Unable to load sample - an unknown error occurred",
94
+ );
95
+ }
96
+ } else {
97
+ log.debug(
98
+ `POLLING RUNNING SAMPLE: ${sampleSummary.id}-${sampleSummary.epoch}`,
99
+ );
100
+
101
+ // Poll running sample
102
+ samplePolling.startPolling(logFile, sampleSummary);
103
+ sampleActions.setSampleStatus("streaming");
104
+ }
105
+ } catch (e) {
106
+ sampleActions.setSampleError(e as Error);
107
+ sampleActions.setSampleStatus("error");
108
+ }
109
+ },
110
+ },
111
+ } as const;
112
+
113
+ const cleanup = () => {
114
+ samplePolling.cleanup();
115
+ };
116
+ return [slice, cleanup];
117
+ };
118
+
119
+ export const initializeSampleSlice = (
120
+ set: (fn: (state: StoreState) => void) => void,
121
+ ) => {
122
+ set((state) => {
123
+ if (!state.sample) {
124
+ state.sample = initialState;
125
+ }
126
+ });
127
+ };
@@ -0,0 +1,21 @@
1
+ import { EvalSample } from "../types/log";
2
+ import { resolveAttachments } from "../utils/attachments";
3
+
4
+ /**
5
+ * Migrates and resolves attachments for a sample
6
+ */
7
+ export const resolveSample = (sample: any): EvalSample => {
8
+ sample = { ...sample };
9
+
10
+ // Migrates old versions of samples to the new structure
11
+ if (sample.transcript) {
12
+ sample.events = sample.transcript.events;
13
+ sample.attachments = sample.transcript.content;
14
+ }
15
+ sample.attachments = sample.attachments || {};
16
+ sample.input = resolveAttachments(sample.input, sample.attachments);
17
+ sample.messages = resolveAttachments(sample.messages, sample.attachments);
18
+ sample.events = resolveAttachments(sample.events, sample.attachments);
19
+ sample.attachments = {};
20
+ return sample;
21
+ };
@@ -0,0 +1,206 @@
1
+ import { RefObject, useCallback, useEffect, useRef } from "react";
2
+ import { StateCallback, StateSnapshot, VirtuosoHandle } from "react-virtuoso";
3
+ import { createLogger } from "../utils/logger";
4
+ import { debounce } from "../utils/sync";
5
+ import { useStore } from "./store";
6
+
7
+ const log = createLogger("scrolling");
8
+
9
+ export function useStatefulScrollPosition<
10
+ T extends HTMLElement = HTMLDivElement,
11
+ >(
12
+ elementRef: RefObject<T | null>,
13
+ elementKey: string,
14
+ delay = 500,
15
+ scrollable = true,
16
+ ) {
17
+ const getScrollPosition = useStore(
18
+ (state) => state.appActions.getScrollPosition,
19
+ );
20
+ const setScrollPosition = useStore(
21
+ (state) => state.appActions.setScrollPosition,
22
+ );
23
+
24
+ // Create debounced scroll handler
25
+ const handleScroll = useCallback(
26
+ debounce((e: Event) => {
27
+ const target = e.target as HTMLElement;
28
+ const position = target.scrollTop;
29
+ log.debug(`Storing scroll position`, elementKey, position);
30
+ setScrollPosition(elementKey, position);
31
+ }, delay),
32
+ [elementKey, setScrollPosition, delay],
33
+ );
34
+
35
+ // Function to manually restore scroll position
36
+ const restoreScrollPosition = useCallback(() => {
37
+ const element = elementRef.current;
38
+ const savedPosition = getScrollPosition(elementKey);
39
+
40
+ if (element && savedPosition !== undefined) {
41
+ requestAnimationFrame(() => {
42
+ element.scrollTop = savedPosition;
43
+
44
+ requestAnimationFrame(() => {
45
+ if (element.scrollTop !== savedPosition) {
46
+ element.scrollTop = savedPosition;
47
+ }
48
+ });
49
+ });
50
+ }
51
+ }, [elementKey, getScrollPosition, elementRef]);
52
+
53
+ // Set up scroll listener and restore position on mount
54
+ useEffect(() => {
55
+ const element = elementRef.current;
56
+ if (!element || !scrollable) {
57
+ return;
58
+ }
59
+ log.debug(`Restore Scroll Hook`, elementKey);
60
+
61
+ // Restore scroll position on mount
62
+ const savedPosition = getScrollPosition(elementKey);
63
+ if (savedPosition !== undefined) {
64
+ log.debug(`Restoring scroll position`, savedPosition);
65
+ // Ensure the element has fully rendered
66
+ requestAnimationFrame(() => {
67
+ if (element.scrollTop !== savedPosition) {
68
+ element.scrollTop = savedPosition;
69
+ }
70
+ });
71
+ }
72
+
73
+ // Set up scroll listener
74
+ if (element.addEventListener) {
75
+ element.addEventListener("scroll", handleScroll);
76
+ } else {
77
+ log.warn("Element has no way to add event listener", element);
78
+ }
79
+
80
+ // Clean up
81
+ return () => {
82
+ if (element.removeEventListener) {
83
+ element.removeEventListener("scroll", handleScroll);
84
+ } else {
85
+ log.warn("Element has no way to remove event listener", element);
86
+ }
87
+ };
88
+ }, [elementKey, elementRef, handleScroll]);
89
+
90
+ return { restoreScrollPosition };
91
+ }
92
+
93
+ // Define a type for the debounced function that includes the cancel method
94
+ type DebouncedFunction<T extends (...args: any[]) => any> = T & {
95
+ cancel: () => void;
96
+ flush: () => void;
97
+ };
98
+
99
+ export const useVirtuosoState = (
100
+ virtuosoRef: RefObject<VirtuosoHandle | null>,
101
+ elementKey: string,
102
+ delay = 1000,
103
+ ) => {
104
+ // Use useCallback to stabilize the selectors
105
+ const restoreState = useStore(
106
+ useCallback((state) => state.app.listPositions[elementKey], [elementKey]),
107
+ );
108
+
109
+ const setListPosition = useStore(
110
+ useCallback((state) => state.appActions.setListPosition, []),
111
+ );
112
+
113
+ const clearListPosition = useStore(
114
+ useCallback((state) => state.appActions.clearListPosition, []),
115
+ );
116
+
117
+ // Properly type the debounced function ref
118
+ const debouncedFnRef = useRef<DebouncedFunction<
119
+ (isScrolling: boolean) => void
120
+ > | null>(null);
121
+
122
+ // Create the state change handler
123
+ const handleStateChange: StateCallback = useCallback(
124
+ (state: StateSnapshot) => {
125
+ log.debug(`Storing list state: [${elementKey}]`, state);
126
+ setListPosition(elementKey, state);
127
+ },
128
+ [elementKey, setListPosition],
129
+ );
130
+
131
+ // Setup the debounced function once
132
+ useEffect(() => {
133
+ debouncedFnRef.current = debounce((isScrolling: boolean) => {
134
+ log.debug("List scroll", isScrolling);
135
+ const element = virtuosoRef.current;
136
+ if (!element) {
137
+ return;
138
+ }
139
+ element.getState(handleStateChange);
140
+ }, delay) as DebouncedFunction<(isScrolling: boolean) => void>;
141
+
142
+ return () => {
143
+ // Clear the stored position when component unmounts
144
+ clearListPosition(elementKey);
145
+ };
146
+ }, [delay, elementKey, handleStateChange, clearListPosition, virtuosoRef]);
147
+
148
+ // Return a stable function reference that uses the ref internally
149
+ const isScrolling = useCallback((scrolling: boolean) => {
150
+ if (!scrolling) {
151
+ return;
152
+ }
153
+
154
+ if (debouncedFnRef.current) {
155
+ debouncedFnRef.current(scrolling);
156
+ }
157
+ }, []);
158
+
159
+ // Use a state to prevent re-rendering just because the restore
160
+ // state changes
161
+ const stateRef = useRef(restoreState);
162
+ useEffect(() => {
163
+ stateRef.current = restoreState;
164
+ }, [restoreState]);
165
+
166
+ const getRestoreState = useCallback(() => stateRef.current, []);
167
+
168
+ return { getRestoreState, isScrolling };
169
+ };
170
+
171
+ export function useRafThrottle<T extends (...args: any[]) => any>(
172
+ callback: T,
173
+ dependencies: any[] = [],
174
+ ): (...args: Parameters<T>) => void {
175
+ const rafRef = useRef<number | null>(null);
176
+ const callbackRef = useRef<T>(callback);
177
+
178
+ // Update the callback ref when the callback changes
179
+ useEffect(() => {
180
+ callbackRef.current = callback;
181
+ }, [callback, ...dependencies]);
182
+
183
+ const throttledCallback = useCallback((...args: Parameters<T>) => {
184
+ // Skip if we already have a frame queued
185
+ if (rafRef.current) {
186
+ return;
187
+ }
188
+
189
+ rafRef.current = requestAnimationFrame(() => {
190
+ callbackRef.current(...args);
191
+ rafRef.current = null;
192
+ });
193
+ }, []);
194
+
195
+ // Clean up any pending animation frame on unmount
196
+ useEffect(() => {
197
+ return () => {
198
+ if (rafRef.current) {
199
+ cancelAnimationFrame(rafRef.current);
200
+ rafRef.current = null;
201
+ }
202
+ };
203
+ }, []);
204
+
205
+ return throttledCallback;
206
+ }