inspect-ai 0.3.80__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.80.dist-info → inspect_ai-0.3.82.dist-info}/METADATA +2 -2
  174. {inspect_ai-0.3.80.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.80.dist-info → inspect_ai-0.3.82.dist-info}/WHEEL +0 -0
  177. {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/entry_points.txt +0 -0
  178. {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/licenses/LICENSE +0 -0
  179. {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,196 @@
1
+ import { createLogger } from "../utils/logger";
2
+ import { createPolling } from "../utils/polling";
3
+ import { StoreState } from "./store";
4
+
5
+ // The logger
6
+ const log = createLogger("logPolling");
7
+
8
+ const kRetries = 10;
9
+ const kPollingInterval = 2;
10
+
11
+ export function createLogPolling(
12
+ get: () => StoreState,
13
+ set: (fn: (state: StoreState) => void) => void,
14
+ ) {
15
+ // Tracks the currently polling instance
16
+ let currentPolling: ReturnType<typeof createPolling> | null = null;
17
+
18
+ // handle aborts
19
+ let abortController: AbortController;
20
+
21
+ // Refresh logs and clear pending in a single transaction
22
+ const refreshLog = async (logFileName: string, clearPending = false) => {
23
+ if (abortController?.signal.aborted) {
24
+ return false;
25
+ }
26
+
27
+ const state = get();
28
+ const api = state.api;
29
+ const selectedLogFile = state.logsActions.getSelectedLogFile();
30
+
31
+ if (!api || !selectedLogFile) {
32
+ return false;
33
+ }
34
+
35
+ log.debug(`refresh: ${selectedLogFile}`);
36
+
37
+ try {
38
+ const logContents = await api.get_log_summary(selectedLogFile);
39
+
40
+ set((state) => {
41
+ // Set the log summary
42
+ state.log.selectedLogSummary = logContents;
43
+ log.debug(
44
+ `Setting refreshed summary ${logContents.sampleSummaries.length} samples`,
45
+ logContents,
46
+ );
47
+
48
+ // Clear pending summaries if requested
49
+ if (clearPending) {
50
+ const pendingSampleSummaries = state.log.pendingSampleSummaries;
51
+ if ((pendingSampleSummaries?.samples.length || 0) > 0) {
52
+ log.debug(
53
+ `Clearing pending summaries during refresh for: ${logFileName}`,
54
+ );
55
+ state.log.pendingSampleSummaries = {
56
+ samples: [],
57
+ refresh: pendingSampleSummaries?.refresh || 2,
58
+ };
59
+ }
60
+ }
61
+ });
62
+
63
+ return true;
64
+ } catch (error) {
65
+ log.error("Error refreshing log:", error);
66
+ return false;
67
+ }
68
+ };
69
+
70
+ // Function to start polling for a specific log file
71
+ const startPolling = (logFileName: string) => {
72
+ // Stop any existing polling
73
+ if (currentPolling) {
74
+ currentPolling.stop();
75
+ }
76
+
77
+ // note that we're active
78
+ abortController = new AbortController();
79
+
80
+ // Track whether we ever polled
81
+ let loadedPendingSamples = false;
82
+
83
+ // Create a new polling instance
84
+ currentPolling = createPolling(
85
+ `PendingSamples-${logFileName}`,
86
+ async () => {
87
+ if (abortController.signal.aborted) {
88
+ log.debug(`Component unmounted, stopping poll for: ${logFileName}`);
89
+ return false;
90
+ }
91
+
92
+ // The state for polling
93
+ const state = get();
94
+
95
+ // Don't proceed if API doesn't support it
96
+ const api = state.api;
97
+ if (!api?.get_log_pending_samples) {
98
+ return false;
99
+ }
100
+
101
+ if (abortController.signal.aborted) {
102
+ return false;
103
+ }
104
+
105
+ // Fetch pending samples
106
+ log.debug(`Polling running samples: ${logFileName}`);
107
+ const currentEtag = get().log.pendingSampleSummaries?.etag;
108
+ const pendingSamples = await api.get_log_pending_samples(
109
+ logFileName,
110
+ currentEtag,
111
+ );
112
+ log.debug(`Received pending samples`, pendingSamples);
113
+
114
+ if (abortController.signal.aborted) {
115
+ return false;
116
+ }
117
+
118
+ if (pendingSamples.status === "OK" && pendingSamples.pendingSamples) {
119
+ loadedPendingSamples = true;
120
+
121
+ // Update state with new pending samples
122
+ set((state) => {
123
+ state.log.pendingSampleSummaries = pendingSamples.pendingSamples;
124
+ });
125
+
126
+ // Refresh the log to get latest data
127
+ await refreshLog(logFileName, false);
128
+
129
+ // Continue polling
130
+ return true;
131
+ } else if (pendingSamples.status === "NotFound") {
132
+ // The eval has completed (no more events/pending samples will be delivered)
133
+ log.debug(`Stop polling running samples: ${logFileName}`);
134
+
135
+ // Clear pending summaries and refresh in one transaction
136
+ if (loadedPendingSamples) {
137
+ await refreshLog(logFileName, true);
138
+ }
139
+
140
+ // Stop polling
141
+ return false;
142
+ }
143
+
144
+ // Continue polling by default
145
+ return true;
146
+ },
147
+ {
148
+ maxRetries: kRetries,
149
+ interval: get().log.pendingSampleSummaries?.refresh || kPollingInterval,
150
+ },
151
+ );
152
+
153
+ // Start the polling
154
+ currentPolling.start();
155
+ };
156
+
157
+ // Clear pending summaries (now using the transactional approach)
158
+ const clearPendingSummaries = (logFileName: string) => {
159
+ if (abortController.signal.aborted) {
160
+ return false;
161
+ }
162
+
163
+ const pendingSampleSummaries = get().log.pendingSampleSummaries;
164
+ if ((pendingSampleSummaries?.samples.length || 0) > 0) {
165
+ log.debug(`Clear pending: ${logFileName}`);
166
+ return refreshLog(logFileName, true);
167
+ }
168
+
169
+ return false;
170
+ };
171
+
172
+ // Stop polling
173
+ const stopPolling = () => {
174
+ if (currentPolling) {
175
+ currentPolling.stop();
176
+ currentPolling = null;
177
+ }
178
+ };
179
+
180
+ // Method to call when component unmounts
181
+ const cleanup = () => {
182
+ log.debug(`Cleanup`);
183
+ abortController.abort();
184
+ stopPolling();
185
+ };
186
+
187
+ return {
188
+ startPolling,
189
+ stopPolling,
190
+ clearPendingSummaries,
191
+ cleanup,
192
+ // Expose the refresh function so components can use it directly
193
+ refreshLog: (clearPending = false) =>
194
+ refreshLog(get().logsActions.getSelectedLogFile() || "", clearPending),
195
+ };
196
+ }
@@ -0,0 +1,214 @@
1
+ import { EvalSummary, PendingSamples } from "../api/types";
2
+ import { kDefaultSort, kInfoWorkspaceTabId } from "../constants";
3
+ import { ScorerInfo } from "../scoring/utils";
4
+ import { LogState, ScoreFilter, ScoreLabel } from "../types";
5
+ import { createLogger } from "../utils/logger";
6
+ import { createLogPolling } from "./logPolling";
7
+ import { StoreState } from "./store";
8
+
9
+ const log = createLogger("logSlice");
10
+
11
+ export interface LogSlice {
12
+ log: LogState;
13
+ logActions: {
14
+ selectSample: (index: number) => void;
15
+
16
+ // Set the selected log summary
17
+ setSelectedLogSummary: (summary: EvalSummary) => void;
18
+
19
+ // Clear the selected log summary
20
+ clearSelectedLogSummary: () => void;
21
+
22
+ // Update pending sample information
23
+ setPendingSampleSummaries: (samples: PendingSamples) => void;
24
+
25
+ // Set filter criteria
26
+ setFilter: (filter: ScoreFilter) => void;
27
+
28
+ // Set epoch filter
29
+ setEpoch: (epoch: string) => void;
30
+
31
+ // Set sort order and update groupBy
32
+ setSort: (sort: string) => void;
33
+
34
+ // Set score label
35
+ setScore: (score: ScoreLabel) => void;
36
+
37
+ // Set available scores
38
+ setScores: (scores: ScorerInfo[]) => void;
39
+
40
+ // Reset filter state to defaults
41
+ resetFiltering: () => void;
42
+
43
+ // Load log
44
+ loadLog: (logFileName: string) => Promise<void>;
45
+
46
+ // Refresh the current log
47
+ refreshLog: () => Promise<void>;
48
+ };
49
+ }
50
+
51
+ // Initial state
52
+ const initialState = {
53
+ // Log state
54
+ selectedSampleIndex: -1,
55
+ selectedLogSummary: undefined,
56
+ pendingSampleSummaries: undefined,
57
+ loadedLog: undefined,
58
+
59
+ // Filter state
60
+ filter: {},
61
+ epoch: "all",
62
+ sort: kDefaultSort,
63
+ score: undefined,
64
+ scores: undefined,
65
+ };
66
+
67
+ // Create the app slice using StoreState directly
68
+ export const createLogSlice = (
69
+ set: (fn: (state: StoreState) => void) => void,
70
+ get: () => StoreState,
71
+ _store: any,
72
+ ): [LogSlice, () => void] => {
73
+ const logPolling = createLogPolling(get, set);
74
+
75
+ const slice = {
76
+ // State
77
+ log: initialState,
78
+
79
+ // Actions
80
+ logActions: {
81
+ selectSample: (index: number) =>
82
+ set((state) => {
83
+ state.log.selectedSampleIndex = index;
84
+ }),
85
+
86
+ setSelectedLogSummary: (selectedLogSummary: EvalSummary) => {
87
+ set((state) => {
88
+ state.log.selectedLogSummary = selectedLogSummary;
89
+ });
90
+
91
+ if (
92
+ selectedLogSummary.status !== "started" &&
93
+ selectedLogSummary.sampleSummaries.length === 0
94
+ ) {
95
+ // If there are no samples, use the workspace tab id by default
96
+ get().appActions.setWorkspaceTab(kInfoWorkspaceTabId);
97
+ }
98
+ },
99
+
100
+ clearSelectedLogSummary: () => {
101
+ set((state) => {
102
+ state.log.selectedLogSummary = undefined;
103
+ });
104
+ },
105
+ setPendingSampleSummaries: (pendingSampleSummaries: PendingSamples) =>
106
+ set((state) => {
107
+ state.log.pendingSampleSummaries = pendingSampleSummaries;
108
+ }),
109
+
110
+ setFilter: (filter: ScoreFilter) =>
111
+ set((state) => {
112
+ state.log.filter = filter;
113
+ }),
114
+ setEpoch: (epoch: string) =>
115
+ set((state) => {
116
+ state.log.epoch = epoch;
117
+ }),
118
+ setSort: (sort: string) =>
119
+ set((state) => {
120
+ state.log.sort = sort;
121
+ }),
122
+ setScore: (score: ScoreLabel) =>
123
+ set((state) => {
124
+ state.log.score = score;
125
+ }),
126
+ setScores: (scores: ScorerInfo[]) =>
127
+ set((state) => {
128
+ state.log.scores = scores;
129
+ }),
130
+ resetFiltering: () =>
131
+ set((state) => {
132
+ state.log.filter = {};
133
+ state.log.epoch = "all";
134
+ state.log.sort = kDefaultSort;
135
+ state.log.score = undefined;
136
+ }),
137
+
138
+ loadLog: async (logFileName: string) => {
139
+ const state = get();
140
+ const api = state.api;
141
+
142
+ if (!api) {
143
+ console.error("API not initialized in Store");
144
+ return;
145
+ }
146
+
147
+ log.debug(`Load log: ${logFileName}`);
148
+ try {
149
+ const logContents = await api.get_log_summary(logFileName);
150
+ state.logActions.setSelectedLogSummary(logContents);
151
+ state.logActions.setEpoch;
152
+
153
+ // Push the updated header information up
154
+ const header = {
155
+ [logFileName]: {
156
+ version: logContents.version,
157
+ status: logContents.status,
158
+ eval: logContents.eval,
159
+ plan: logContents.plan,
160
+ results:
161
+ logContents.results !== null ? logContents.results : undefined,
162
+ stats: logContents.stats,
163
+ error: logContents.error !== null ? logContents.error : undefined,
164
+ },
165
+ };
166
+
167
+ state.logsActions.updateLogHeaders(header);
168
+ set((state) => {
169
+ state.log.loadedLog = logFileName;
170
+ }),
171
+ // Start polling for pending samples
172
+ logPolling.startPolling(logFileName);
173
+ } catch (error) {
174
+ log.error("Error loading log:", error);
175
+ }
176
+ },
177
+
178
+ refreshLog: async () => {
179
+ const state = get();
180
+ const api = state.api;
181
+ const selectedLogFile = state.logsActions.getSelectedLogFile();
182
+
183
+ if (!api || !selectedLogFile) {
184
+ return;
185
+ }
186
+
187
+ log.debug(`refresh: ${selectedLogFile}`);
188
+ try {
189
+ const logContents = await api.get_log_summary(selectedLogFile);
190
+ state.logActions.setSelectedLogSummary(logContents);
191
+ } catch (error) {
192
+ log.error("Error refreshing log:", error);
193
+ }
194
+ },
195
+ },
196
+ } as const;
197
+
198
+ const cleanup = () => {
199
+ logPolling.cleanup();
200
+ };
201
+
202
+ return [slice, cleanup];
203
+ };
204
+
205
+ // Initialize app slice with StoreState
206
+ export const initalializeLogSlice = (
207
+ set: (fn: (state: StoreState) => void) => void,
208
+ ) => {
209
+ set((state) => {
210
+ if (!state.log) {
211
+ state.log = initialState;
212
+ }
213
+ });
214
+ };
@@ -0,0 +1,118 @@
1
+ import { EvalLogHeader, LogFiles } from "../api/types";
2
+ import { createLogger } from "../utils/logger";
3
+ import { createPolling } from "../utils/polling";
4
+ import { StoreState } from "./store";
5
+
6
+ // The logger
7
+ const log = createLogger("logsPolling");
8
+
9
+ export function createLogsPolling(
10
+ get: () => StoreState,
11
+ _set: (fn: (state: StoreState) => void) => void,
12
+ ) {
13
+ // Tracks the currently polling instance
14
+ let currentPolling: ReturnType<typeof createPolling> | null = null;
15
+
16
+ // Are we active
17
+ let isActive = true;
18
+
19
+ // Function to start polling for a specific log file
20
+ const startPolling = (logFiles: LogFiles) => {
21
+ // Get the api
22
+ const api = get().api;
23
+ if (!api) {
24
+ throw new Error("Failed to start polling - no API");
25
+ }
26
+
27
+ // Stop any existing polling
28
+ if (currentPolling) {
29
+ currentPolling.stop();
30
+ }
31
+
32
+ // note that we're active
33
+ isActive = true;
34
+
35
+ log.debug("LOADING HEADERS");
36
+ get().logsActions.setHeadersLoading(true);
37
+
38
+ // Group into chunks
39
+ const chunkSize = 8;
40
+ const fileLists: string[][] = [];
41
+
42
+ for (let i = 0; i < logFiles.files.length; i += chunkSize) {
43
+ const chunk = logFiles.files
44
+ .slice(i, i + chunkSize)
45
+ .map((logFile) => logFile.name);
46
+ fileLists.push(chunk);
47
+ }
48
+ const totalLen = fileLists.length;
49
+
50
+ // Create a new polling instance
51
+ currentPolling = createPolling(
52
+ `LogHeaders`,
53
+ async () => {
54
+ if (!isActive) {
55
+ get().logsActions.setHeadersLoading(false);
56
+ return false; // Stop polling
57
+ }
58
+
59
+ log.debug(`POLL HEADERS`);
60
+
61
+ const currentFileList = fileLists.shift();
62
+ if (currentFileList) {
63
+ log.debug(
64
+ `LOADING ${totalLen - fileLists.length} of ${totalLen} CHUNKS`,
65
+ );
66
+ const headers = await api.get_log_headers(currentFileList);
67
+ const updatedHeaders: Record<string, EvalLogHeader> = {};
68
+
69
+ headers.forEach((header, index) => {
70
+ const logFile = currentFileList[index];
71
+ updatedHeaders[logFile] = header as EvalLogHeader;
72
+ });
73
+ get().logsActions.updateLogHeaders(updatedHeaders);
74
+ } else {
75
+ // Stop polling
76
+ get().logsActions.setHeadersLoading(false);
77
+ return false;
78
+ }
79
+
80
+ if (!isActive) {
81
+ get().logsActions.setHeadersLoading(false);
82
+ return false; // Stop polling
83
+ }
84
+
85
+ // Continue polling by default
86
+ return true;
87
+ },
88
+ {
89
+ maxRetries: 10,
90
+ interval: 5,
91
+ },
92
+ );
93
+
94
+ // Start the polling
95
+ currentPolling.start();
96
+ };
97
+
98
+ // Stop polling
99
+ const stopPolling = () => {
100
+ if (currentPolling) {
101
+ currentPolling.stop();
102
+ currentPolling = null;
103
+ }
104
+ };
105
+
106
+ // Method to call when component unmounts
107
+ const cleanup = () => {
108
+ log.debug(`CLEANUP`);
109
+ isActive = false;
110
+ stopPolling();
111
+ };
112
+
113
+ return {
114
+ startPolling,
115
+ stopPolling,
116
+ cleanup,
117
+ };
118
+ }
@@ -0,0 +1,181 @@
1
+ import { EvalLogHeader, LogFiles } from "../api/types";
2
+ import { LogsState } from "../types";
3
+ import { createLogger } from "../utils/logger";
4
+ import { createLogsPolling } from "./logsPolling";
5
+ import { StoreState } from "./store";
6
+
7
+ const log = createLogger("Log Slice");
8
+
9
+ export interface LogsSlice {
10
+ logs: LogsState;
11
+ logsActions: {
12
+ // Update State
13
+ setLogs: (logs: LogFiles) => void;
14
+ setLogHeaders: (headers: Record<string, EvalLogHeader>) => void;
15
+ setHeadersLoading: (loading: boolean) => void;
16
+ setSelectedLogIndex: (index: number) => void;
17
+ setSelectedLogFile: (logUrl: string) => void;
18
+ updateLogHeaders: (headers: Record<string, EvalLogHeader>) => void;
19
+
20
+ // Fetch or update logs
21
+ refreshLogs: () => Promise<void>;
22
+ selectLogFile: (logUrl: string) => Promise<void>;
23
+ loadLogs: () => Promise<LogFiles>;
24
+
25
+ // Computed values
26
+ getSelectedLogFile: () => string | undefined;
27
+ };
28
+ }
29
+
30
+ const initialState: LogsState = {
31
+ logs: { log_dir: "", files: [] },
32
+ logHeaders: {},
33
+ headersLoading: false,
34
+ selectedLogIndex: -1,
35
+ };
36
+
37
+ export const createLogsSlice = (
38
+ set: (fn: (state: StoreState) => void) => void,
39
+ get: () => StoreState,
40
+ _store: any,
41
+ ): [LogsSlice, () => void] => {
42
+ const logsPolling = createLogsPolling(get, set);
43
+
44
+ const slice = {
45
+ // State
46
+ logs: initialState,
47
+
48
+ // Actions
49
+ logsActions: {
50
+ setLogs: (logs: LogFiles) => {
51
+ set((state) => {
52
+ state.logs.logs = logs;
53
+ });
54
+
55
+ // If we have files in the logs, load the headers
56
+ if (logs.files.length > 0) {
57
+ // ensure state is updated first
58
+ setTimeout(() => {
59
+ const currentState = get();
60
+ if (!currentState.logs.headersLoading) {
61
+ logsPolling.startPolling(logs);
62
+ }
63
+ }, 100);
64
+ }
65
+ },
66
+ setLogHeaders: (headers: Record<string, EvalLogHeader>) =>
67
+ set((state) => {
68
+ state.logs.logHeaders = headers;
69
+ }),
70
+ setHeadersLoading: (loading: boolean) =>
71
+ set((state) => {
72
+ state.logs.headersLoading = loading;
73
+ }),
74
+ setSelectedLogIndex: (selectedLogIndex: number) => {
75
+ set((state) => {
76
+ state.logs.selectedLogIndex = selectedLogIndex;
77
+ });
78
+ },
79
+ updateLogHeaders: (headers: Record<string, EvalLogHeader>) =>
80
+ set((state) => {
81
+ state.logs.logHeaders = { ...get().logs.logHeaders, ...headers };
82
+ }),
83
+
84
+ setSelectedLogFile: (logUrl: string) => {
85
+ const state = get();
86
+ const index = state.logs.logs.files.findIndex((val) =>
87
+ logUrl.endsWith(val.name),
88
+ );
89
+
90
+ if (index > -1) {
91
+ state.logsActions.setSelectedLogIndex(index);
92
+ }
93
+ },
94
+
95
+ // Helper function to load logs
96
+ loadLogs: async () => {
97
+ const api = get().api;
98
+ if (!api) {
99
+ console.error("API not initialized in LogsStore");
100
+ return { log_dir: "", files: [] };
101
+ }
102
+
103
+ try {
104
+ log.debug("LOADING LOG FILES");
105
+ return await api.get_log_paths();
106
+ } catch (e) {
107
+ console.log(e);
108
+ get().appActions.setStatus({ loading: false, error: e as Error });
109
+ return { log_dir: "", files: [] };
110
+ }
111
+ },
112
+ refreshLogs: async () => {
113
+ log.debug("REFRESH LOGS");
114
+ const state = get();
115
+ const refreshedLogs = await state.logsActions.loadLogs();
116
+
117
+ // Set the logs first
118
+ state.logsActions.setLogs(refreshedLogs || { log_dir: "", files: [] });
119
+
120
+ // Preserve the selected log even if new logs appear
121
+ const currentLog =
122
+ refreshedLogs.files[
123
+ state.logs.selectedLogIndex > -1 ? state.logs.selectedLogIndex : 0
124
+ ];
125
+
126
+ if (currentLog) {
127
+ const newIndex = refreshedLogs?.files.findIndex((file) =>
128
+ currentLog.name.endsWith(file.name),
129
+ );
130
+
131
+ if (newIndex !== undefined && newIndex !== -1) {
132
+ state.logsActions.setSelectedLogIndex(newIndex);
133
+ }
134
+ }
135
+ },
136
+ // Select a specific log file
137
+ selectLogFile: async (logUrl: string) => {
138
+ const state = get();
139
+ const index = state.logs.logs.files.findIndex((val) =>
140
+ val.name.endsWith(logUrl),
141
+ );
142
+
143
+ // It is already loaded
144
+ if (index > -1) {
145
+ state.logsActions.setSelectedLogIndex(index);
146
+ } else {
147
+ // It isn't yet loaded, so refresh the logs and try to load it from there
148
+ const result = await state.logsActions.loadLogs();
149
+ const idx = result?.files.findIndex((file) =>
150
+ logUrl.endsWith(file.name),
151
+ );
152
+
153
+ state.logsActions.setLogs(result || { log_dir: "", files: [] });
154
+ state.logsActions.setSelectedLogIndex(
155
+ idx !== undefined && idx > -1 ? idx : 0,
156
+ );
157
+ }
158
+ },
159
+
160
+ getSelectedLogFile: () => {
161
+ const state = get();
162
+ const file = state.logs.logs.files[state.logs.selectedLogIndex];
163
+ return file !== undefined ? file.name : undefined;
164
+ },
165
+ },
166
+ } as const;
167
+
168
+ const cleanup = () => {};
169
+
170
+ return [slice, cleanup];
171
+ };
172
+
173
+ export const initializeLogsSlice = <T extends LogsSlice>(
174
+ set: (fn: (state: T) => void) => void,
175
+ ) => {
176
+ set((state) => {
177
+ if (!state.logs) {
178
+ state.logs = initialState;
179
+ }
180
+ });
181
+ };