inspect-ai 0.3.104__py3-none-any.whl → 0.3.106__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 (46) hide show
  1. inspect_ai/_eval/context.py +5 -0
  2. inspect_ai/_eval/eval.py +113 -1
  3. inspect_ai/_eval/evalset.py +1 -1
  4. inspect_ai/_eval/task/run.py +64 -38
  5. inspect_ai/_util/eval_task_group.py +15 -0
  6. inspect_ai/_view/server.py +17 -0
  7. inspect_ai/_view/www/dist/assets/index.css +33 -29
  8. inspect_ai/_view/www/dist/assets/index.js +559 -247
  9. inspect_ai/_view/www/src/app/samples/chat/ChatMessage.module.css +4 -0
  10. inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +17 -0
  11. inspect_ai/_view/www/src/app/samples/sample-tools/filters.ts +26 -0
  12. inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/SampleFilter.tsx +14 -3
  13. inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/completions.ts +359 -7
  14. inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/language.ts +6 -0
  15. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +1 -1
  16. inspect_ai/_view/www/src/client/api/api-browser.ts +25 -0
  17. inspect_ai/_view/www/src/client/api/api-http.ts +3 -0
  18. inspect_ai/_view/www/src/client/api/api-vscode.ts +6 -0
  19. inspect_ai/_view/www/src/client/api/client-api.ts +3 -0
  20. inspect_ai/_view/www/src/client/api/jsonrpc.ts +1 -0
  21. inspect_ai/_view/www/src/client/api/types.ts +3 -0
  22. inspect_ai/_view/www/src/state/samplePolling.ts +17 -1
  23. inspect_ai/agent/_handoff.py +5 -2
  24. inspect_ai/agent/_react.py +43 -20
  25. inspect_ai/dataset/_dataset.py +1 -1
  26. inspect_ai/log/_samples.py +5 -0
  27. inspect_ai/model/_call_tools.py +4 -4
  28. inspect_ai/model/_providers/_openai_web_search.py +1 -1
  29. inspect_ai/model/_providers/anthropic.py +23 -2
  30. inspect_ai/model/_providers/google.py +5 -1
  31. inspect_ai/model/_providers/groq.py +5 -0
  32. inspect_ai/model/_providers/perplexity.py +27 -1
  33. inspect_ai/model/_providers/providers.py +1 -1
  34. inspect_ai/tool/_tools/_web_search/_web_search.py +8 -3
  35. inspect_ai/util/__init__.py +8 -0
  36. inspect_ai/util/_background.py +64 -0
  37. inspect_ai/util/_limit.py +72 -5
  38. inspect_ai/util/_sandbox/__init__.py +2 -0
  39. inspect_ai/util/_sandbox/service.py +28 -7
  40. inspect_ai/util/_subprocess.py +51 -38
  41. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/METADATA +1 -1
  42. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/RECORD +46 -44
  43. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/WHEEL +0 -0
  44. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/entry_points.txt +0 -0
  45. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/licenses/LICENSE +0 -0
  46. {inspect_ai-0.3.104.dist-info → inspect_ai-0.3.106.dist-info}/top_level.txt +0 -0
@@ -35,3 +35,7 @@
35
35
  .copyLink:hover {
36
36
  opacity: 1;
37
37
  }
38
+
39
+ .metadataLabel {
40
+ padding-top: 1em;
41
+ }
@@ -8,7 +8,9 @@ import {
8
8
  } from "../../../@types/log";
9
9
  import { CopyButton } from "../../../components/CopyButton";
10
10
  import ExpandablePanel from "../../../components/ExpandablePanel";
11
+ import { LabeledValue } from "../../../components/LabeledValue";
11
12
  import { ApplicationIcons } from "../../appearance/icons";
13
+ import { RecordTree } from "../../content/RecordTree";
12
14
  import {
13
15
  supportsLinking,
14
16
  toFullUrl,
@@ -77,6 +79,21 @@ export const ChatMessage: FC<ChatMessageProps> = ({
77
79
  toolCallStyle={toolCallStyle}
78
80
  />
79
81
  </ExpandablePanel>
82
+
83
+ {message.metadata && Object.keys(message.metadata).length > 0 ? (
84
+ <LabeledValue
85
+ label="Metadata"
86
+ className={clsx(styles.metadataLabel, "text-size-smaller")}
87
+ >
88
+ <RecordTree
89
+ record={message.metadata}
90
+ id={`${id}-metadata`}
91
+ defaultExpandLevel={1}
92
+ />
93
+ </LabeledValue>
94
+ ) : (
95
+ ""
96
+ )}
80
97
  </div>
81
98
  </div>
82
99
  );
@@ -5,6 +5,7 @@ import { SampleSummary } from "../../../client/api/types";
5
5
  import { kScoreTypeBoolean } from "../../../constants";
6
6
  import { inputString } from "../../../utils/format";
7
7
  import { EvalDescriptor, ScoreDescriptor } from "../descriptor/types";
8
+ import { kSampleMetadataPrefix } from "./sample-filter/language";
8
9
 
9
10
  export interface SampleFilterItem {
10
11
  shortName?: string;
@@ -100,10 +101,25 @@ const scoreVariables = (
100
101
  return variables;
101
102
  };
102
103
 
104
+ const getNestedPropertyValue = (obj: any, path: string): any => {
105
+ const keys = path.split(".");
106
+ let current = obj;
107
+ for (const key of keys) {
108
+ if (current && typeof current === "object" && key in current) {
109
+ current = current[key];
110
+ } else {
111
+ return undefined;
112
+ }
113
+ }
114
+ return current;
115
+ };
116
+
103
117
  const sampleVariables = (sample: SampleSummary): Record<string, unknown> => {
104
118
  return {
105
119
  has_error: !!sample.error,
106
120
  has_retries: sample.retries !== undefined && sample.retries > 0,
121
+ id: sample.id,
122
+ metadata: sample.metadata,
107
123
  };
108
124
  };
109
125
 
@@ -217,6 +233,12 @@ export const filterExpression = (
217
233
  const value = get(name);
218
234
  return value;
219
235
  }
236
+ // Handle metadata property access
237
+ if (name.startsWith(kSampleMetadataPrefix)) {
238
+ const propertyPath = name.substring(kSampleMetadataPrefix.length);
239
+ const metadata = sample.metadata || {};
240
+ return getNestedPropertyValue(metadata, propertyPath);
241
+ }
220
242
  // Score variables exist only if the sample completed successfully.
221
243
  return sample.error ? undefined : get(name);
222
244
  };
@@ -240,6 +262,10 @@ export const filterExpression = (
240
262
  const errorObj = error as any as Record<string, unknown>;
241
263
  const propertyName: string = (errorObj["propertyName"] as string) || "";
242
264
  if (propertyName) {
265
+ // Don't show errors for metadata properties - they might not exist in all samples
266
+ if (propertyName.startsWith(kSampleMetadataPrefix)) {
267
+ return { matches: false, error: undefined };
268
+ }
243
269
  const regex = new RegExp(`\\b${propertyName}\\b`);
244
270
  const match = regex.exec(filterValue);
245
271
  if (match) {
@@ -34,6 +34,8 @@ Filter samples by:
34
34
  • Samples with errors: has_error
35
35
  • Input, target and error regex search: input_contains, target_contains, error_contains
36
36
  • Samples that have been retried: has_retries
37
+ • Sample Id: e.g. "id == 'sample123'"
38
+ • Sample metadata: e.g. "metadata.key == 'value'"
37
39
 
38
40
  Supported expressions:
39
41
  • Arithmetic: +, -, *, /, mod, ^
@@ -93,6 +95,12 @@ const editorTheme = EditorView.theme({
93
95
  ".cm-scroller": {
94
96
  overflow: "hidden",
95
97
  },
98
+ ".cm-line": {
99
+ "font-size": "var(--inspect-font-size-smallest) !important",
100
+ },
101
+ ".token": {
102
+ "font-size": "var(--inspect-font-size-smallest) !important",
103
+ },
96
104
  });
97
105
 
98
106
  const ensureOneLine = (tr: Transaction): TransactionSpec => {
@@ -145,6 +153,9 @@ export const SampleFilter: FC<SampleFilterProps> = () => {
145
153
 
146
154
  const filter = useStore((state) => state.log.filter);
147
155
  const filterError = useStore((state) => state.log.filterError);
156
+ const samples = useStore(
157
+ (state) => state.log.selectedLogSummary?.sampleSummaries,
158
+ );
148
159
  const setFilter = useStore((state) => state.logActions.setFilter);
149
160
 
150
161
  const handleFocus = useCallback((event: FocusEvent, view: EditorView) => {
@@ -156,10 +167,10 @@ export const SampleFilter: FC<SampleFilterProps> = () => {
156
167
  const makeAutocompletion = useCallback(
157
168
  () =>
158
169
  autocompletion({
159
- override: [(context) => getCompletions(context, filterItems)],
170
+ override: [(context) => getCompletions(context, filterItems, samples)],
160
171
  activateOnCompletion: (c) => c.label.endsWith(" "),
161
172
  }),
162
- [],
173
+ [filterItems, samples],
163
174
  );
164
175
 
165
176
  const makeLinter = useCallback(
@@ -240,7 +251,7 @@ export const SampleFilter: FC<SampleFilterProps> = () => {
240
251
  effects:
241
252
  autocompletionCompartment.current.reconfigure(makeAutocompletion()),
242
253
  });
243
- }, [filterItems]);
254
+ }, [filterItems, samples]);
244
255
 
245
256
  useEffect(() => {
246
257
  editorViewRef.current?.dispatch({
@@ -3,8 +3,10 @@ import {
3
3
  CompletionContext,
4
4
  CompletionResult,
5
5
  CompletionSection,
6
+ startCompletion,
6
7
  } from "@codemirror/autocomplete";
7
8
  import { EditorView } from "codemirror";
9
+ import { SampleSummary } from "../../../../client/api/types";
8
10
  import {
9
11
  kScoreTypeBoolean,
10
12
  kScoreTypeCategorical,
@@ -15,6 +17,8 @@ import {
15
17
  import { SampleFilterItem } from "../filters";
16
18
  import {
17
19
  KEYWORDS,
20
+ kSampleIdVariable,
21
+ kSampleMetadataVariable,
18
22
  MATH_FUNCTIONS,
19
23
  SAMPLE_FUNCTIONS,
20
24
  SAMPLE_VARIABLES,
@@ -53,6 +57,34 @@ const applyWithCall = (
53
57
  });
54
58
  };
55
59
 
60
+ const applyWithDot = (
61
+ view: EditorView,
62
+ completion: Completion,
63
+ from: number,
64
+ to: number,
65
+ ): void => {
66
+ view.dispatch({
67
+ changes: { from, to, insert: `${completion.label}.` },
68
+ selection: { anchor: from + completion.label.length + 1 },
69
+ });
70
+ // trigger completion
71
+ setTimeout(() => startCompletion(view), 0);
72
+ };
73
+
74
+ const applyWithSpace = (
75
+ view: EditorView,
76
+ completion: Completion,
77
+ from: number,
78
+ to: number,
79
+ ): void => {
80
+ view.dispatch({
81
+ changes: { from, to, insert: `${completion.label} ` },
82
+ selection: { anchor: from + completion.label.length + 1 },
83
+ });
84
+ // trigger completion
85
+ setTimeout(() => startCompletion(view), 0);
86
+ };
87
+
56
88
  const makeKeywordCompletion = (k: string): Completion => ({
57
89
  label: k,
58
90
  type: "keyword",
@@ -88,6 +120,12 @@ const makeSampleVariableCompletion = ([label, info]: [
88
120
  label,
89
121
  type: "variable",
90
122
  info,
123
+ apply:
124
+ label === kSampleMetadataVariable
125
+ ? applyWithDot
126
+ : label === kSampleIdVariable
127
+ ? applyWithSpace
128
+ : undefined,
91
129
  boost: 10,
92
130
  });
93
131
 
@@ -120,6 +158,210 @@ const getMemberScoreItems = (
120
158
  ): SampleFilterItem[] =>
121
159
  filterItems.filter((item) => item?.qualifiedName?.startsWith(`${scorer}.`));
122
160
 
161
+ const getSampleIds = (samples: SampleSummary[]): Set<string | number> => {
162
+ const ids = new Set<string | number>();
163
+ for (const sample of samples) {
164
+ ids.add(sample.id);
165
+ }
166
+ return ids;
167
+ };
168
+
169
+ const getMetadataPropertyValues = (
170
+ samples: SampleSummary[],
171
+ propertyPath: string,
172
+ ): Set<any> => {
173
+ const values = new Set<any>();
174
+ for (const sample of samples) {
175
+ if (sample.metadata) {
176
+ const value = getNestedProperty(sample.metadata, propertyPath);
177
+ if (value !== undefined && value !== null) {
178
+ values.add(value);
179
+ }
180
+ }
181
+ }
182
+ return values;
183
+ };
184
+
185
+ const getNestedProperty = (obj: any, path: string): any => {
186
+ const keys = path.split(".");
187
+ let current = obj;
188
+ for (const key of keys) {
189
+ if (current && typeof current === "object" && key in current) {
190
+ current = current[key];
191
+ } else {
192
+ return undefined;
193
+ }
194
+ }
195
+ return current;
196
+ };
197
+
198
+ const buildMetadataPath = (
199
+ tokens: Token[],
200
+ currentTokenIndex: number,
201
+ ): string | null => {
202
+ // Walk backwards to build the metadata path
203
+ // For "metadata." return ""
204
+ // For "metadata.config." return "config"
205
+ // For "metadata.config.timeout." return "config.timeout"
206
+
207
+ const parts: string[] = [];
208
+
209
+ // Start after the first dot
210
+ let index = 2;
211
+
212
+ // Look for the metadata root by walking backwards
213
+ while (index <= currentTokenIndex) {
214
+ const token = tokens[currentTokenIndex - index];
215
+
216
+ if (token?.text === kSampleMetadataVariable) {
217
+ // Found metadata root, return the path
218
+ return parts.reverse().join(".");
219
+ } else if (token?.type === "variable") {
220
+ // Found a variable token, add to path
221
+ parts.push(token.text);
222
+ // Skip the expected dot
223
+ index++;
224
+ if (tokens[currentTokenIndex - index]?.text === ".") {
225
+ // Move past the dot
226
+ index++;
227
+ } else {
228
+ // No dot, not a valid path
229
+ break;
230
+ }
231
+ } else {
232
+ // Hit non-variable, non-metadata token
233
+ break;
234
+ }
235
+ }
236
+
237
+ // Didn't find metadata root
238
+ return null;
239
+ };
240
+
241
+ const getMetadataKeysForPath = (
242
+ samples: SampleSummary[],
243
+ parentPath: string,
244
+ ): Set<string> => {
245
+ const keys = new Set<string>();
246
+ for (const sample of samples) {
247
+ if (sample.metadata) {
248
+ const parentObj = parentPath
249
+ ? getNestedProperty(sample.metadata, parentPath)
250
+ : sample.metadata;
251
+ if (
252
+ parentObj &&
253
+ typeof parentObj === "object" &&
254
+ !Array.isArray(parentObj)
255
+ ) {
256
+ for (const key of Object.keys(parentObj)) {
257
+ keys.add(key);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return keys;
263
+ };
264
+
265
+ const buildMetadataPropertyPath = (
266
+ tokens: Token[],
267
+ currentTokenIndex: number,
268
+ ): string | null => {
269
+ // Walk backwards to build the full metadata property path
270
+ // e.g., for "metadata.difficulty ==" we want to return "difficulty"
271
+ // e.g., for "metadata.config.timeout ==" we want to return "config.timeout"
272
+ const parts: string[] = [];
273
+
274
+ // Start after the dot
275
+ let index = 2;
276
+
277
+ // Collect the property path by walking backwards
278
+ while (index <= currentTokenIndex) {
279
+ const token = tokens[currentTokenIndex - index];
280
+ if (!token) break;
281
+
282
+ if (token.type === "variable") {
283
+ if (token.text === kSampleMetadataVariable) {
284
+ // Found the metadata root, return the path
285
+ return parts.reverse().join(".");
286
+ } else {
287
+ parts.push(token.text);
288
+ }
289
+ } else if (token.text !== ".") {
290
+ // Hit a non-dot, non-variable token, not a metadata path
291
+ break;
292
+ }
293
+ index++;
294
+ }
295
+
296
+ return null;
297
+ };
298
+
299
+ const isMetadataProperty = (
300
+ tokens: Token[],
301
+ currentTokenIndex: number,
302
+ ): boolean => {
303
+ // Check if the current variable is part of a metadata property access
304
+ // e.g., for "metadata.difficulty" return true
305
+
306
+ // For metadata.difficulty, tokens are: [metadata, ., difficulty]
307
+ // currentTokenIndex points after difficulty, so prevToken(1) = difficulty
308
+ // We need to check if we can trace back to metadata
309
+
310
+ // Start by looking at prevToken(2) which should be "."
311
+ let index = 2;
312
+
313
+ // Walk backwards looking for metadata root
314
+ while (index <= currentTokenIndex) {
315
+ const token = tokens[currentTokenIndex - index];
316
+ if (!token) break;
317
+
318
+ if (token.text === kSampleMetadataVariable) {
319
+ return true;
320
+ } else if (token.text === "." || token.type === "variable") {
321
+ index++;
322
+ } else {
323
+ break; // Hit a non-metadata token
324
+ }
325
+ }
326
+
327
+ return false;
328
+ };
329
+
330
+ const makeMetadataKeyCompletion = (key: string): Completion => ({
331
+ label: key,
332
+ type: "property",
333
+ info: `Metadata property: ${key}`,
334
+ boost: 25,
335
+ });
336
+
337
+ const makeSampleIdCompletion = (id: string | number): Completion => ({
338
+ label: typeof id === "string" ? `"${id}"` : String(id),
339
+ type: "text",
340
+ info: `Sample ID: ${id}`,
341
+ boost: 25,
342
+ });
343
+
344
+ const makeMetadataValueCompletion = (value: any): Completion => {
345
+ let label: string;
346
+ if (typeof value === "string") {
347
+ label = `"${value}"`;
348
+ } else if (typeof value === "boolean") {
349
+ // Use filter expression constants for booleans
350
+ label = value ? "True" : "False";
351
+ } else if (value === null) {
352
+ label = "None";
353
+ } else {
354
+ label = String(value);
355
+ }
356
+
357
+ return {
358
+ label,
359
+ type: "text",
360
+ info: `Metadata value: ${value}`,
361
+ boost: 25,
362
+ };
363
+ };
364
+
123
365
  /**
124
366
  * Generates completions for the filter expression. The main goal is to make the
125
367
  * sample filter intuitive for beginners and to provide a smooth experience for
@@ -137,6 +379,7 @@ const getMemberScoreItems = (
137
379
  export function getCompletions(
138
380
  context: CompletionContext,
139
381
  filterItems: SampleFilterItem[],
382
+ samples?: SampleSummary[],
140
383
  ): CompletionResult | null {
141
384
  const keywordCompletionItems = KEYWORDS.map(makeKeywordCompletion);
142
385
  const mathFunctionCompletionItems = MATH_FUNCTIONS.map(
@@ -145,7 +388,22 @@ export function getCompletions(
145
388
  const sampleFunctionCompletionItems = SAMPLE_FUNCTIONS.map(
146
389
  makeSampleFunctionCompletion,
147
390
  );
148
- const sampleVariableCompletionItems = SAMPLE_VARIABLES.map(
391
+ // Filter sample variables based on available data
392
+ const availableSampleVariables = SAMPLE_VARIABLES.filter(([label]) => {
393
+ if (label === kSampleMetadataVariable) {
394
+ // Only include metadata if at least one sample has metadata
395
+ return (
396
+ samples &&
397
+ samples.some(
398
+ (sample) =>
399
+ sample.metadata && Object.keys(sample.metadata).length > 0,
400
+ )
401
+ );
402
+ }
403
+ return true;
404
+ });
405
+
406
+ const sampleVariableCompletionItems = availableSampleVariables.map(
149
407
  makeSampleVariableCompletion,
150
408
  );
151
409
  const variableCompletionItems = filterItems.map((item) =>
@@ -279,7 +537,7 @@ export function getCompletions(
279
537
  autoSpaceAfter: completingAtEnd,
280
538
  });
281
539
 
282
- const descreteRelationCompletions = () =>
540
+ const discreteRelationCompletions = () =>
283
541
  makeCompletions(["==", "!=", "in", "not in"].map(makeKeywordCompletion), {
284
542
  enforceOrder: true,
285
543
  autoSpaceAfter: completingAtEnd,
@@ -305,9 +563,22 @@ export function getCompletions(
305
563
 
306
564
  // Member access
307
565
  if (prevToken(1)?.text === ".") {
308
- const scorer = prevToken(2)?.text;
309
- if (scorer) {
310
- return memberAccessCompletions(getMemberScoreItems(filterItems, scorer));
566
+ const varName = prevToken(2)?.text;
567
+
568
+ // Check if this is metadata property access (metadata.* or metadata.*.*)
569
+ const metadataPath = buildMetadataPath(tokens, currentTokenIndex);
570
+ if (metadataPath !== null && samples) {
571
+ // Get completions for the current metadata path
572
+ const metadataKeys = Array.from(
573
+ getMetadataKeysForPath(samples, metadataPath),
574
+ );
575
+ const metadataCompletions = metadataKeys.map(makeMetadataKeyCompletion);
576
+ return makeCompletions(metadataCompletions, {
577
+ autocompleteInTheMiddle: true,
578
+ includeDefault: false,
579
+ });
580
+ } else if (varName) {
581
+ return memberAccessCompletions(getMemberScoreItems(filterItems, varName));
311
582
  }
312
583
  }
313
584
 
@@ -328,12 +599,31 @@ export function getCompletions(
328
599
 
329
600
  // Variable type-based relation suggestions
330
601
  if (prevToken(1)?.type === "variable") {
331
- const scoreType = findFilterItem(1)?.scoreType || "";
602
+ const varName = prevToken(1)?.text;
332
603
 
604
+ // Check if this is a metadata property access (metadata.property or metadata.nested.property)
605
+ if (isMetadataProperty(tokens, currentTokenIndex)) {
606
+ // This is metadata.property - provide custom relation completions
607
+ return customRelationCompletions();
608
+ }
609
+
610
+ // Handle sample variables specially
611
+ if (varName === kSampleIdVariable) {
612
+ return discreteRelationCompletions();
613
+ }
614
+ if (varName === kSampleMetadataVariable) {
615
+ return customRelationCompletions();
616
+ }
617
+ if (varName === "has_error" || varName === "has_retries") {
618
+ return logicalOpCompletions();
619
+ }
620
+
621
+ // Handle score variables
622
+ const scoreType = findFilterItem(1)?.scoreType || "";
333
623
  switch (scoreType) {
334
624
  case kScoreTypePassFail:
335
625
  case kScoreTypeCategorical:
336
- return descreteRelationCompletions();
626
+ return discreteRelationCompletions();
337
627
  case kScoreTypeNumeric:
338
628
  return continuousRelationCompletions();
339
629
  case kScoreTypeOther:
@@ -347,6 +637,68 @@ export function getCompletions(
347
637
 
348
638
  // RHS comparison suggestions
349
639
  if (prevToken(1)?.type === "relation") {
640
+ const varName = prevToken(2)?.text;
641
+
642
+ // Check if this is a metadata property comparison (relation after metadata.property or metadata.nested.property)
643
+ const metadataPropertyPath = buildMetadataPropertyPath(
644
+ tokens,
645
+ currentTokenIndex,
646
+ );
647
+ if (metadataPropertyPath !== null && samples) {
648
+ // This is metadata.property == ... - provide value completions for this property
649
+ const metadataValues = Array.from(
650
+ getMetadataPropertyValues(samples, metadataPropertyPath),
651
+ );
652
+
653
+ // Get the current query for prefix filtering
654
+ const currentQuery = currentToken?.text || "";
655
+
656
+ // Pre-filter values to only show prefix matches
657
+ const filteredValues = currentQuery
658
+ ? metadataValues.filter((value) => {
659
+ const label =
660
+ typeof value === "string"
661
+ ? `"${value}"`
662
+ : typeof value === "boolean"
663
+ ? value
664
+ ? "True"
665
+ : "False"
666
+ : value === null
667
+ ? "None"
668
+ : String(value);
669
+ return label.toLowerCase().startsWith(currentQuery.toLowerCase());
670
+ })
671
+ : metadataValues;
672
+
673
+ const metadataValueCompletions = filteredValues.map(
674
+ makeMetadataValueCompletion,
675
+ );
676
+ return makeCompletions(metadataValueCompletions, {
677
+ includeDefault: false,
678
+ });
679
+ }
680
+
681
+ // Sample ID completions
682
+ if (varName === kSampleIdVariable && samples) {
683
+ const sampleIds = Array.from(getSampleIds(samples));
684
+
685
+ // Get the current query for prefix filtering
686
+ const currentQuery = currentToken?.text || "";
687
+
688
+ // Pre-filter IDs to only show prefix matches
689
+ const filteredIds = currentQuery
690
+ ? sampleIds.filter((id) => {
691
+ const label = typeof id === "string" ? `"${id}"` : String(id);
692
+ return label.toLowerCase().startsWith(currentQuery.toLowerCase());
693
+ })
694
+ : sampleIds;
695
+
696
+ const sampleIdCompletions = filteredIds.map(makeSampleIdCompletion);
697
+ return makeCompletions(sampleIdCompletions, {
698
+ includeDefault: false,
699
+ });
700
+ }
701
+
350
702
  const item = findFilterItem(2);
351
703
  if (item?.categories?.length) {
352
704
  return rhsCompletions(item.categories);
@@ -1,3 +1,7 @@
1
+ export const kSampleIdVariable = "id";
2
+ export const kSampleMetadataVariable = "metadata";
3
+ export const kSampleMetadataPrefix = kSampleMetadataVariable + ".";
4
+
1
5
  export const KEYWORDS: string[] = ["and", "or", "not", "in", "not in", "mod"];
2
6
 
3
7
  export const MATH_FUNCTIONS: [string, string][] = [
@@ -16,6 +20,8 @@ export const MATH_FUNCTIONS: [string, string][] = [
16
20
  export const SAMPLE_VARIABLES: [string, string][] = [
17
21
  ["has_error", "Checks if the sample has an error"],
18
22
  ["has_retries", "Checks if the sample has been retried"],
23
+ [kSampleIdVariable, "The unique identifier of the sample"],
24
+ [kSampleMetadataVariable, "Metadata associated with the sample"],
19
25
  ];
20
26
 
21
27
  export const SAMPLE_FUNCTIONS: [string, string][] = [
@@ -49,7 +49,7 @@ export const OutlineRow: FC<OutlineRowProps> = ({
49
49
  <div
50
50
  className={clsx(
51
51
  styles.eventRow,
52
- "text-size-smallest",
52
+ "text-size-smaller",
53
53
  selected ? styles.selected : "",
54
54
  )}
55
55
  style={{ paddingLeft: `${node.depth * 0.4}em` }}
@@ -155,6 +155,29 @@ async function eval_log_sample_data(
155
155
  return result;
156
156
  }
157
157
 
158
+ async function log_message(log_file: string, message: string) {
159
+ const params = new URLSearchParams();
160
+ params.append("log_file", log_file);
161
+ params.append("message", message);
162
+
163
+ const request: Request<void> = {
164
+ headers: {
165
+ "Content-Type": "text/plain",
166
+ },
167
+ parse: async (text: string) => {
168
+ if (text !== "") {
169
+ throw new Error(`Unexpected response from log_message: ${text}`);
170
+ }
171
+ return;
172
+ },
173
+ };
174
+ await apiRequest<void>(
175
+ "GET",
176
+ `/api/log-message?${params.toString()}`,
177
+ request,
178
+ );
179
+ }
180
+
158
181
  interface Request<T> {
159
182
  headers?: Record<string, string>;
160
183
  body?: string;
@@ -288,7 +311,9 @@ const browserApi: LogViewAPI = {
288
311
  eval_log_size,
289
312
  eval_log_bytes,
290
313
  eval_log_headers,
314
+ log_message,
291
315
  download_file,
316
+
292
317
  open_log_file,
293
318
  eval_pending_samples,
294
319
  eval_log_sample_data,
@@ -70,6 +70,9 @@ function simpleHttpAPI(logInfo: LogInfo): LogViewAPI {
70
70
 
71
71
  return undefined;
72
72
  },
73
+ log_message: async (log_file: string, message: string) => {
74
+ console.log(`[CLIENT MESSAGE] (${log_file}): ${message}`);
75
+ },
73
76
  eval_log: async (
74
77
  log_file: string,
75
78
  _headerOnly?: number,
@@ -8,6 +8,7 @@ import {
8
8
  kMethodEvalLogHeaders,
9
9
  kMethodEvalLogs,
10
10
  kMethodEvalLogSize,
11
+ kMethodLogMessage,
11
12
  kMethodPendingSamples,
12
13
  kMethodSampleData,
13
14
  webViewJsonRpcClient,
@@ -147,6 +148,10 @@ async function eval_log_sample_data(
147
148
  }
148
149
  }
149
150
 
151
+ async function log_message(log_file: string, message: string): Promise<void> {
152
+ await vscodeClient(kMethodLogMessage, [log_file, message]);
153
+ }
154
+
150
155
  async function download_file() {
151
156
  throw Error("Downloading files is not supported in VS Code");
152
157
  }
@@ -167,6 +172,7 @@ const api: LogViewAPI = {
167
172
  eval_log_size,
168
173
  eval_log_bytes,
169
174
  eval_log_headers,
175
+ log_message,
170
176
  download_file,
171
177
  open_log_file,
172
178
  eval_pending_samples,