inspect-ai 0.3.63__py3-none-any.whl → 0.3.64__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 (178) hide show
  1. inspect_ai/_cli/cache.py +8 -7
  2. inspect_ai/_cli/common.py +0 -12
  3. inspect_ai/_cli/eval.py +32 -4
  4. inspect_ai/_cli/info.py +1 -0
  5. inspect_ai/_cli/list.py +1 -1
  6. inspect_ai/_cli/log.py +2 -0
  7. inspect_ai/_cli/sandbox.py +4 -1
  8. inspect_ai/_cli/score.py +181 -32
  9. inspect_ai/_cli/trace.py +2 -0
  10. inspect_ai/_cli/view.py +4 -2
  11. inspect_ai/_display/core/config.py +7 -1
  12. inspect_ai/_display/textual/widgets/samples.py +4 -3
  13. inspect_ai/_display/textual/widgets/sandbox.py +6 -0
  14. inspect_ai/_eval/__init__.py +0 -0
  15. inspect_ai/_eval/eval.py +100 -97
  16. inspect_ai/_eval/evalset.py +69 -69
  17. inspect_ai/_eval/loader.py +122 -12
  18. inspect_ai/_eval/registry.py +1 -1
  19. inspect_ai/_eval/run.py +14 -0
  20. inspect_ai/_eval/score.py +125 -36
  21. inspect_ai/_eval/task/log.py +105 -4
  22. inspect_ai/_eval/task/results.py +92 -38
  23. inspect_ai/_eval/task/run.py +6 -2
  24. inspect_ai/_eval/task/sandbox.py +35 -2
  25. inspect_ai/_eval/task/task.py +49 -46
  26. inspect_ai/_util/__init__.py +0 -0
  27. inspect_ai/_util/constants.py +1 -1
  28. inspect_ai/_util/content.py +8 -0
  29. inspect_ai/_util/error.py +2 -0
  30. inspect_ai/_util/file.py +15 -1
  31. inspect_ai/_util/logger.py +4 -2
  32. inspect_ai/_util/registry.py +7 -1
  33. inspect_ai/_view/view.py +1 -2
  34. inspect_ai/_view/www/App.css +5 -0
  35. inspect_ai/_view/www/README.md +1 -1
  36. inspect_ai/_view/www/dist/assets/index.css +39 -11
  37. inspect_ai/_view/www/dist/assets/index.js +504 -501
  38. inspect_ai/_view/www/log-schema.json +86 -73
  39. inspect_ai/_view/www/package.json +1 -1
  40. inspect_ai/_view/www/src/App.tsx +1 -0
  41. inspect_ai/_view/www/src/components/AnsiDisplay.tsx +1 -1
  42. inspect_ai/_view/www/src/components/JsonPanel.tsx +1 -1
  43. inspect_ai/_view/www/src/components/LargeModal.tsx +39 -49
  44. inspect_ai/_view/www/src/components/NavPills.tsx +3 -1
  45. inspect_ai/_view/www/src/components/TabSet.tsx +19 -4
  46. inspect_ai/_view/www/src/logfile/remoteLogFile.ts +0 -1
  47. inspect_ai/_view/www/src/metadata/MetaDataGrid.tsx +1 -1
  48. inspect_ai/_view/www/src/metadata/MetaDataView.tsx +1 -1
  49. inspect_ai/_view/www/src/plan/PlanDetailView.tsx +17 -2
  50. inspect_ai/_view/www/src/plan/SolverDetailView.tsx +1 -1
  51. inspect_ai/_view/www/src/samples/SampleDisplay.tsx +9 -4
  52. inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +4 -2
  53. inspect_ai/_view/www/src/samples/SamplesTools.tsx +16 -24
  54. inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +1 -1
  55. inspect_ai/_view/www/src/samples/chat/ChatView.tsx +1 -0
  56. inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +27 -13
  57. inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +19 -17
  58. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +12 -10
  59. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +56 -66
  60. inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.tsx +12 -5
  61. inspect_ai/_view/www/src/samples/chat/tools/tool.ts +21 -36
  62. inspect_ai/_view/www/src/samples/descriptor/samplesDescriptor.tsx +3 -1
  63. inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +27 -25
  64. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +5 -1
  65. inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +1 -1
  66. inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +2 -2
  67. inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +9 -5
  68. inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +1 -1
  69. inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +5 -4
  70. inspect_ai/_view/www/src/samples/transcript/event/EventNavs.tsx +1 -0
  71. inspect_ai/_view/www/src/samples/transcript/event/EventPanel.tsx +1 -0
  72. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +17 -6
  73. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +14 -19
  74. inspect_ai/_view/www/src/types/log.d.ts +107 -19
  75. inspect_ai/_view/www/src/usage/ModelTokenTable.tsx +7 -1
  76. inspect_ai/_view/www/src/usage/ModelUsagePanel.tsx +5 -3
  77. inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +25 -27
  78. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +12 -11
  79. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.module.css +25 -2
  80. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +60 -36
  81. inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +4 -0
  82. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoreView.tsx +6 -4
  83. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoresView.tsx +16 -14
  84. inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +9 -19
  85. inspect_ai/_view/www/src/workspace/utils.ts +34 -0
  86. inspect_ai/approval/_approval.py +2 -0
  87. inspect_ai/approval/_approver.py +4 -4
  88. inspect_ai/approval/_auto.py +1 -1
  89. inspect_ai/approval/_human/approver.py +3 -0
  90. inspect_ai/approval/_policy.py +5 -0
  91. inspect_ai/approval/_registry.py +2 -2
  92. inspect_ai/dataset/_dataset.py +36 -45
  93. inspect_ai/dataset/_sources/__init__.py +0 -0
  94. inspect_ai/dataset/_sources/csv.py +13 -13
  95. inspect_ai/dataset/_sources/hf.py +29 -29
  96. inspect_ai/dataset/_sources/json.py +10 -10
  97. inspect_ai/log/__init__.py +2 -0
  98. inspect_ai/log/_convert.py +3 -3
  99. inspect_ai/log/_file.py +24 -9
  100. inspect_ai/log/_log.py +98 -7
  101. inspect_ai/log/_message.py +3 -1
  102. inspect_ai/log/_recorders/file.py +4 -0
  103. inspect_ai/log/_recorders/recorder.py +3 -0
  104. inspect_ai/log/_transcript.py +19 -8
  105. inspect_ai/model/__init__.py +2 -0
  106. inspect_ai/model/_cache.py +39 -21
  107. inspect_ai/model/_call_tools.py +2 -2
  108. inspect_ai/model/_chat_message.py +14 -4
  109. inspect_ai/model/_generate_config.py +1 -1
  110. inspect_ai/model/_model.py +31 -24
  111. inspect_ai/model/_model_output.py +14 -1
  112. inspect_ai/model/_openai.py +10 -18
  113. inspect_ai/model/_providers/google.py +9 -5
  114. inspect_ai/model/_providers/openai.py +5 -9
  115. inspect_ai/model/_providers/openrouter.py +1 -1
  116. inspect_ai/scorer/__init__.py +6 -1
  117. inspect_ai/scorer/_answer.py +1 -1
  118. inspect_ai/scorer/_classification.py +4 -0
  119. inspect_ai/scorer/_match.py +4 -5
  120. inspect_ai/scorer/_metric.py +87 -28
  121. inspect_ai/scorer/_metrics/__init__.py +3 -3
  122. inspect_ai/scorer/_metrics/accuracy.py +8 -10
  123. inspect_ai/scorer/_metrics/mean.py +3 -17
  124. inspect_ai/scorer/_metrics/std.py +111 -30
  125. inspect_ai/scorer/_model.py +12 -12
  126. inspect_ai/scorer/_pattern.py +3 -3
  127. inspect_ai/scorer/_reducer/reducer.py +36 -21
  128. inspect_ai/scorer/_reducer/registry.py +2 -2
  129. inspect_ai/scorer/_reducer/types.py +7 -1
  130. inspect_ai/scorer/_score.py +11 -1
  131. inspect_ai/scorer/_scorer.py +110 -16
  132. inspect_ai/solver/__init__.py +1 -1
  133. inspect_ai/solver/_basic_agent.py +19 -22
  134. inspect_ai/solver/_bridge/__init__.py +0 -3
  135. inspect_ai/solver/_bridge/bridge.py +3 -3
  136. inspect_ai/solver/_chain.py +1 -2
  137. inspect_ai/solver/_critique.py +3 -3
  138. inspect_ai/solver/_fork.py +2 -2
  139. inspect_ai/solver/_human_agent/__init__.py +0 -0
  140. inspect_ai/solver/_human_agent/agent.py +5 -8
  141. inspect_ai/solver/_human_agent/commands/clock.py +14 -10
  142. inspect_ai/solver/_human_agent/commands/note.py +1 -1
  143. inspect_ai/solver/_human_agent/commands/score.py +0 -11
  144. inspect_ai/solver/_multiple_choice.py +15 -18
  145. inspect_ai/solver/_prompt.py +7 -7
  146. inspect_ai/solver/_solver.py +53 -52
  147. inspect_ai/solver/_task_state.py +80 -69
  148. inspect_ai/solver/_use_tools.py +9 -9
  149. inspect_ai/tool/__init__.py +2 -1
  150. inspect_ai/tool/_tool.py +43 -14
  151. inspect_ai/tool/_tool_call.py +6 -2
  152. inspect_ai/tool/_tool_choice.py +3 -1
  153. inspect_ai/tool/_tool_def.py +10 -8
  154. inspect_ai/tool/_tool_params.py +24 -0
  155. inspect_ai/tool/_tool_with.py +7 -7
  156. inspect_ai/tool/_tools/__init__.py +0 -0
  157. inspect_ai/tool/_tools/_computer/_common.py +2 -2
  158. inspect_ai/tool/_tools/_computer/_computer.py +11 -0
  159. inspect_ai/tool/_tools/_execute.py +15 -9
  160. inspect_ai/tool/_tools/_web_browser/_resources/README.md +2 -2
  161. inspect_ai/tool/_tools/_web_browser/_web_browser.py +5 -3
  162. inspect_ai/tool/_tools/_web_search.py +7 -5
  163. inspect_ai/util/_concurrency.py +3 -3
  164. inspect_ai/util/_panel.py +2 -0
  165. inspect_ai/util/_resource.py +12 -12
  166. inspect_ai/util/_sandbox/docker/compose.py +23 -20
  167. inspect_ai/util/_sandbox/docker/config.py +2 -1
  168. inspect_ai/util/_sandbox/docker/docker.py +10 -1
  169. inspect_ai/util/_sandbox/docker/service.py +100 -0
  170. inspect_ai/util/_sandbox/environment.py +99 -96
  171. inspect_ai/util/_subprocess.py +5 -3
  172. inspect_ai/util/_subtask.py +15 -16
  173. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/LICENSE +1 -1
  174. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/METADATA +10 -6
  175. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/RECORD +178 -171
  176. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/WHEEL +0 -0
  177. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/entry_points.txt +0 -0
  178. {inspect_ai-0.3.63.dist-info → inspect_ai-0.3.64.dist-info}/top_level.txt +0 -0
@@ -26,7 +26,7 @@ export const MetaDataGrid: React.FC<MetadataGridProps> = ({
26
26
  const entryEls = entryRecords(entries).map((entry, index) => {
27
27
  const id = `${baseId}-value-${index}`;
28
28
  return (
29
- <Fragment>
29
+ <Fragment key={`${baseId}-record-${index}`}>
30
30
  <div
31
31
  style={{
32
32
  gridColumn: "1 / -1",
@@ -35,7 +35,7 @@ export const MetaDataView: React.FC<MetadataViewProps> = ({
35
35
  const entryEls = (coercedEntries || []).map((entry, index) => {
36
36
  const id = `${baseId}-value-${index}`;
37
37
  return (
38
- <tr>
38
+ <tr key={id}>
39
39
  <td
40
40
  className={clsx(
41
41
  styles.cell,
@@ -125,6 +125,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
125
125
  const scorerPanels = Object.keys(scorers).map((key) => {
126
126
  return (
127
127
  <ScorerDetailView
128
+ key={key}
128
129
  name={key}
129
130
  scores={scorers[key].scores}
130
131
  params={scorers[key].params as Record<string, unknown>}
@@ -159,6 +160,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
159
160
  className: cols === 1 ? styles.oneCol : styles.twoCol,
160
161
  contents: (
161
162
  <MetaDataView
163
+ key={`plan-md-task`}
162
164
  className={"text-size-small"}
163
165
  entries={taskInformation}
164
166
  tableOptions="sm"
@@ -172,6 +174,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
172
174
  className: cols === 1 ? styles.oneCol : styles.twoCol,
173
175
  contents: (
174
176
  <MetaDataView
177
+ key={`plan-md-task-args`}
175
178
  className={"text-size-small"}
176
179
  entries={task_args as Record<string, unknown>}
177
180
  tableOptions="sm"
@@ -185,6 +188,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
185
188
  className: cols === 1 ? styles.oneCol : styles.twoCol,
186
189
  contents: (
187
190
  <MetaDataView
191
+ key={`plan-md-model-args`}
188
192
  className={"text-size-small"}
189
193
  entries={model_args as Record<string, unknown>}
190
194
  tableOptions="sm"
@@ -199,6 +203,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
199
203
  className: cols === 1 ? styles.oneCol : styles.twoCol,
200
204
  contents: (
201
205
  <MetaDataView
206
+ key={`plan-md-config`}
202
207
  className={"text-size-small"}
203
208
  entries={config}
204
209
  tableOptions="sm"
@@ -217,6 +222,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
217
222
  className: cols === 1 ? styles.oneCol : styles.twoCol,
218
223
  contents: (
219
224
  <MetaDataView
225
+ key={`plan-md-generate-config`}
220
226
  className={"text-size-small"}
221
227
  entries={generate_record}
222
228
  tableOptions="sm"
@@ -231,6 +237,7 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
231
237
  className: cols === 1 ? styles.oneCol : styles.twoCol,
232
238
  contents: (
233
239
  <MetaDataView
240
+ key={`plan-md-metadata`}
234
241
  className={"text-size-small"}
235
242
  entries={metadata}
236
243
  tableOptions="sm"
@@ -249,7 +256,11 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
249
256
  >
250
257
  {taskColumns.map((col) => {
251
258
  return (
252
- <PlanColumn title={col.title} className={col.className}>
259
+ <PlanColumn
260
+ title={col.title}
261
+ className={col.className}
262
+ key={`plan-col-${col.title}`}
263
+ >
253
264
  {col.contents}
254
265
  </PlanColumn>
255
266
  );
@@ -259,7 +270,11 @@ export const PlanDetailView: React.FC<PlanDetailViewProps> = ({
259
270
  <div className={clsx(styles.row)}>
260
271
  {metadataColumns.map((col) => {
261
272
  return (
262
- <PlanColumn title={col.title} className={col.className}>
273
+ <PlanColumn
274
+ title={col.title}
275
+ className={col.className}
276
+ key={`plan-col-${col.title}`}
277
+ >
263
278
  {col.contents}
264
279
  </PlanColumn>
265
280
  );
@@ -18,7 +18,7 @@ export const SolversDetailView: React.FC<SolversDetailView> = ({ steps }) => {
18
18
 
19
19
  const details = steps?.map((step, index) => {
20
20
  return (
21
- <Fragment>
21
+ <Fragment key={`solver-step-${index}`}>
22
22
  <DetailStep
23
23
  name={step.solver}
24
24
  className={clsx(styles.items, "text-size-small")}
@@ -77,6 +77,7 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
77
77
  if (!isVscode()) {
78
78
  tools.push(
79
79
  <ToolButton
80
+ key="sample-print-tool"
80
81
  label="Print"
81
82
  icon={ApplicationIcons.copy}
82
83
  onClick={() => {
@@ -101,6 +102,7 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
101
102
  >
102
103
  {sample.events && sample.events.length > 0 ? (
103
104
  <TabPanel
105
+ key={kSampleTranscriptTabId}
104
106
  id={kSampleTranscriptTabId}
105
107
  className="sample-tab"
106
108
  title="Transcript"
@@ -120,6 +122,7 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
120
122
  </TabPanel>
121
123
  ) : null}
122
124
  <TabPanel
125
+ key={kSampleMessagesTabId}
123
126
  id={kSampleMessagesTabId}
124
127
  className={clsx("sample-tab", styles.fullWidth)}
125
128
  title="Messages"
@@ -138,6 +141,7 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
138
141
  </TabPanel>
139
142
  {scorerNames.length === 1 ? (
140
143
  <TabPanel
144
+ key={kSampleScoringTabId}
141
145
  id={kSampleScoringTabId}
142
146
  className="sample-tab"
143
147
  title="Scoring"
@@ -156,6 +160,7 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
156
160
  const tabId = `score-${scorer}`;
157
161
  return (
158
162
  <TabPanel
163
+ key={tabId}
159
164
  id={tabId}
160
165
  className="sample-tab"
161
166
  title={scorer}
@@ -217,11 +222,11 @@ export const SampleDisplay: React.FC<SampleDisplayProps> = ({
217
222
  );
218
223
  };
219
224
 
220
- const metadataViewsForSample = (_id: string, sample: EvalSample) => {
225
+ const metadataViewsForSample = (id: string, sample: EvalSample) => {
221
226
  const sampleMetadatas = [];
222
227
  if (sample.model_usage && Object.keys(sample.model_usage).length > 0) {
223
228
  sampleMetadatas.push(
224
- <Card>
229
+ <Card key={`sample-usage-${id}`}>
225
230
  <CardHeader label="Usage" />
226
231
  <CardBody>
227
232
  <ModelTokenTable
@@ -235,7 +240,7 @@ const metadataViewsForSample = (_id: string, sample: EvalSample) => {
235
240
 
236
241
  if (Object.keys(sample?.metadata).length > 0) {
237
242
  sampleMetadatas.push(
238
- <Card>
243
+ <Card key={`sample-metadata-${id}`}>
239
244
  <CardHeader label="Metadata" />
240
245
  <CardBody>
241
246
  <MetaDataView
@@ -250,7 +255,7 @@ const metadataViewsForSample = (_id: string, sample: EvalSample) => {
250
255
 
251
256
  if (Object.keys(sample?.store).length > 0) {
252
257
  sampleMetadatas.push(
253
- <Card>
258
+ <Card key={`sample-store-${id}`}>
254
259
  <CardHeader label="Store" />
255
260
  <CardBody>
256
261
  <MetaDataView
@@ -143,9 +143,10 @@ export const SampleSummaryView: React.FC<SampleSummaryViewProps> = ({
143
143
  .join(" ")}`,
144
144
  }}
145
145
  >
146
- {columns.map((col) => {
146
+ {columns.map((col, idx) => {
147
147
  return (
148
148
  <div
149
+ key={`sample-summ-lbl-${idx}`}
149
150
  className={clsx(
150
151
  "text-style-label",
151
152
  "text-style-secondary",
@@ -157,9 +158,10 @@ export const SampleSummaryView: React.FC<SampleSummaryViewProps> = ({
157
158
  </div>
158
159
  );
159
160
  })}
160
- {columns.map((col) => {
161
+ {columns.map((col, idx) => {
161
162
  return (
162
163
  <div
164
+ key={`sample-summ-val-${idx}`}
163
165
  className={clsx(
164
166
  styles.wrap,
165
167
  col.clamp ? "three-line-clamp" : undefined,
@@ -1,3 +1,4 @@
1
+ import { Fragment } from "react/jsx-runtime";
1
2
  import { ScoreFilter, ScoreLabel } from "../types";
2
3
  import { SamplesDescriptor } from "./descriptor/samplesDescriptor";
3
4
  import { EpochFilter } from "./sample-tools/EpochFilter";
@@ -32,29 +33,20 @@ export const SampleTools: React.FC<SampleToolsProps> = ({
32
33
  scores,
33
34
  sampleDescriptor,
34
35
  }) => {
35
- const tools = [];
36
-
37
- tools.push(
38
- <SampleFilter
39
- evalDescriptor={sampleDescriptor.evalDescriptor}
40
- scoreFilter={scoreFilter}
41
- setScoreFilter={setScoreFilter}
42
- />,
36
+ return (
37
+ <Fragment>
38
+ <SampleFilter
39
+ evalDescriptor={sampleDescriptor.evalDescriptor}
40
+ scoreFilter={scoreFilter}
41
+ setScoreFilter={setScoreFilter}
42
+ />
43
+ {scores.length > 1 ? (
44
+ <SelectScorer scores={scores} score={score} setScore={setScore} />
45
+ ) : undefined}
46
+ {epochs > 1 ? (
47
+ <EpochFilter epoch={epoch} setEpoch={setEpoch} epochs={epochs} />
48
+ ) : undefined}
49
+ <SortFilter sort={sort} setSort={setSort} epochs={epochs} />
50
+ </Fragment>
43
51
  );
44
-
45
- if (scores.length > 1) {
46
- tools.push(
47
- <SelectScorer scores={scores} score={score} setScore={setScore} />,
48
- );
49
- }
50
-
51
- if (epochs > 1) {
52
- tools.push(
53
- <EpochFilter epoch={epoch} setEpoch={setEpoch} epochs={epochs} />,
54
- );
55
- }
56
-
57
- tools.push(<SortFilter sort={sort} setSort={setSort} epochs={epochs} />);
58
-
59
- return tools;
60
52
  };
@@ -42,7 +42,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
42
42
  {message.role}
43
43
  </div>
44
44
  {message.role === "assistant" && message.reasoning ? (
45
- <Fragment>
45
+ <Fragment key={`${id}-response-label`}>
46
46
  <div className={clsx("text-style-label", "text-style-secondary")}>
47
47
  Reasoning
48
48
  </div>
@@ -32,6 +32,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
32
32
  collapsedMessages.length > 1 && numbered ? index + 1 : undefined;
33
33
  return (
34
34
  <ChatMessageRow
35
+ key={`${id}-msg-${index}`}
35
36
  parentName={id || "chat-view"}
36
37
  number={number}
37
38
  resolvedMessage={msg}
@@ -42,6 +42,7 @@ export const MessageContent: React.FC<MessageContentProps> = ({ contents }) => {
42
42
  return contents.map((content, index) => {
43
43
  if (typeof content === "string") {
44
44
  return messageRenderers["text"].render(
45
+ `text-content-${index}`,
45
46
  {
46
47
  type: "text",
47
48
  text: content,
@@ -52,7 +53,11 @@ export const MessageContent: React.FC<MessageContentProps> = ({ contents }) => {
52
53
  if (content) {
53
54
  const renderer = messageRenderers[content.type];
54
55
  if (renderer) {
55
- return renderer.render(content, index === contents.length - 1);
56
+ return renderer.render(
57
+ `text-${content.type}-${index}`,
58
+ content,
59
+ index === contents.length - 1,
60
+ );
56
61
  } else {
57
62
  console.error(`Unknown message content type '${content.type}'`);
58
63
  }
@@ -65,20 +70,29 @@ export const MessageContent: React.FC<MessageContentProps> = ({ contents }) => {
65
70
  type: "text",
66
71
  text: contents,
67
72
  };
68
- return messageRenderers["text"].render(contentText, true);
73
+ return messageRenderers["text"].render(
74
+ "text-message-content",
75
+ contentText,
76
+ true,
77
+ );
69
78
  }
70
79
  };
71
80
 
72
81
  interface MessageRenderer {
73
- render: (content: ContentType, isLast: boolean) => React.ReactNode;
82
+ render: (
83
+ key: string,
84
+ content: ContentType,
85
+ isLast: boolean,
86
+ ) => React.ReactNode;
74
87
  }
75
88
 
76
89
  const messageRenderers: Record<string, MessageRenderer> = {
77
90
  text: {
78
- render: (content, isLast) => {
91
+ render: (key, content, isLast) => {
79
92
  const c = content as ContentText;
80
93
  return (
81
94
  <MarkdownDiv
95
+ key={key}
82
96
  markdown={c.text}
83
97
  className={isLast ? "no-last-para-padding" : ""}
84
98
  />
@@ -86,39 +100,39 @@ const messageRenderers: Record<string, MessageRenderer> = {
86
100
  },
87
101
  },
88
102
  image: {
89
- render: (content) => {
103
+ render: (key, content) => {
90
104
  const c = content as ContentImage;
91
105
  if (c.image.startsWith("data:")) {
92
- return <img src={c.image} className={styles.contentImage} />;
106
+ return <img src={c.image} className={styles.contentImage} key={key} />;
93
107
  } else {
94
- return <code>{c.image}</code>;
108
+ return <code key={key}>{c.image}</code>;
95
109
  }
96
110
  },
97
111
  },
98
112
  audio: {
99
- render: (content) => {
113
+ render: (key, content) => {
100
114
  const c = content as ContentAudio;
101
115
  return (
102
- <audio controls>
116
+ <audio controls key={key}>
103
117
  <source src={c.audio} type={mimeTypeForFormat(c.format)} />
104
118
  </audio>
105
119
  );
106
120
  },
107
121
  },
108
122
  video: {
109
- render: (content) => {
123
+ render: (key, content) => {
110
124
  const c = content as ContentVideo;
111
125
  return (
112
- <video width="500" height="375" controls>
126
+ <video width="500" height="375" controls key={key}>
113
127
  <source src={c.video} type={mimeTypeForFormat(c.format)} />
114
128
  </video>
115
129
  );
116
130
  },
117
131
  },
118
132
  tool: {
119
- render: (content) => {
133
+ render: (key, content) => {
120
134
  const c = content as ContentTool;
121
- return <ToolOutput output={c.content} />;
135
+ return <ToolOutput output={c.content} key={key} />;
122
136
  },
123
137
  },
124
138
  };
@@ -8,6 +8,7 @@ import { MessageContent } from "./MessageContent";
8
8
  import { resolveToolInput } from "./tools/tool";
9
9
  import { ToolCallView } from "./tools/ToolCallView";
10
10
 
11
+ import { Fragment } from "react";
11
12
  import { ContentTool } from "../../types";
12
13
  import styles from "./MessageContents.module.css";
13
14
 
@@ -27,20 +28,10 @@ export const MessageContents: React.FC<MessageContentsProps> = ({
27
28
  message.tool_calls &&
28
29
  message.tool_calls.length
29
30
  ) {
30
- const result = [];
31
- // If the message contains content, render that.
32
- if (message.content) {
33
- result.push(
34
- <div className={styles.content}>
35
- <MessageContent contents={message.content} />
36
- </div>,
37
- );
38
- }
39
-
40
31
  // Render the tool calls made by this message
41
32
  const toolCalls = message.tool_calls.map((tool_call, idx) => {
42
33
  // Extract tool input
43
- const { input, functionCall, inputType } = resolveToolInput(
34
+ const { input, functionCall, highlightLanguage } = resolveToolInput(
44
35
  tool_call.function,
45
36
  tool_call.arguments,
46
37
  );
@@ -57,23 +48,34 @@ export const MessageContents: React.FC<MessageContentsProps> = ({
57
48
  // Resolve the tool output
58
49
  const resolvedToolOutput = resolveToolMessage(toolMessage);
59
50
  if (toolCallStyle === "compact") {
60
- return <code>tool: {functionCall}</code>;
51
+ return (
52
+ <div key={`tool-call-${idx}`}>
53
+ <code>tool: {functionCall}</code>
54
+ </div>
55
+ );
61
56
  } else {
62
57
  return (
63
58
  <ToolCallView
59
+ key={`tool-call-${idx}`}
64
60
  functionCall={functionCall}
65
61
  input={input}
66
- inputType={inputType}
62
+ highlightLanguage={highlightLanguage}
67
63
  output={resolvedToolOutput}
68
64
  />
69
65
  );
70
66
  }
71
67
  });
72
68
 
73
- if (toolCalls) {
74
- result.push(...toolCalls);
75
- }
76
- return result;
69
+ return (
70
+ <Fragment>
71
+ <div className={styles.content}>
72
+ {message.content ? (
73
+ <MessageContent contents={message.content} />
74
+ ) : undefined}
75
+ </div>
76
+ {toolCalls}
77
+ </Fragment>
78
+ );
77
79
  } else {
78
80
  return <MessageContent contents={message.content} />;
79
81
  }
@@ -1,3 +1,4 @@
1
+ import { useMemo } from "react";
1
2
  import ExpandablePanel from "../../../components/ExpandablePanel";
2
3
  import { ContentTool } from "../../../types";
3
4
  import {
@@ -14,7 +15,7 @@ import { ToolTitle } from "./ToolTitle";
14
15
  interface ToolCallViewProps {
15
16
  functionCall: string;
16
17
  input?: string;
17
- inputType?: string;
18
+ highlightLanguage?: string;
18
19
  view?: ToolCallContent;
19
20
  output:
20
21
  | string
@@ -41,7 +42,7 @@ interface ToolCallViewProps {
41
42
  export const ToolCallView: React.FC<ToolCallViewProps> = ({
42
43
  functionCall,
43
44
  input,
44
- inputType,
45
+ highlightLanguage,
45
46
  view,
46
47
  output,
47
48
  mode,
@@ -76,6 +77,7 @@ export const ToolCallView: React.FC<ToolCallViewProps> = ({
76
77
  const collapse = Array.isArray(output)
77
78
  ? output.every((item) => !isContentImage(item))
78
79
  : !isContentImage(output);
80
+ const normalizedContent = useMemo(() => normalizeContent(output), [output]);
79
81
 
80
82
  return (
81
83
  <div>
@@ -86,14 +88,14 @@ export const ToolCallView: React.FC<ToolCallViewProps> = ({
86
88
  )}
87
89
  <div>
88
90
  <div>
89
- <ToolInput type={inputType} contents={input} view={view} />
90
- {output ? (
91
- <ExpandablePanel collapse={collapse} border={true} lines={15}>
92
- <MessageContent contents={normalizeContent(output)} />
93
- </ExpandablePanel>
94
- ) : (
95
- ""
96
- )}
91
+ <ToolInput
92
+ highlightLanguage={highlightLanguage}
93
+ contents={input}
94
+ toolCallView={view}
95
+ />
96
+ <ExpandablePanel collapse={collapse} border={true} lines={15}>
97
+ <MessageContent contents={normalizedContent} />
98
+ </ExpandablePanel>
97
99
  </div>
98
100
  </div>
99
101
  </div>
@@ -1,86 +1,76 @@
1
1
  import clsx from "clsx";
2
- import murmurhash from "murmurhash";
3
- import { highlightElement, languages } from "prismjs";
4
- import { useEffect, useRef } from "react";
2
+ import { highlightElement } from "prismjs";
3
+ import { memo, useEffect, useRef } from "react";
5
4
  import { MarkdownDiv } from "../../../components/MarkdownDiv";
6
- import { ToolCallContent } from "../../../types/log";
5
+
7
6
  import styles from "./ToolInput.module.css";
8
7
 
8
+ export const useCodeHighlight = (language?: string) => {
9
+ const codeRef = useRef<HTMLElement>(null);
10
+
11
+ useEffect(() => {
12
+ if (codeRef.current && language) {
13
+ highlightElement(codeRef.current);
14
+ }
15
+ }, [language]);
16
+
17
+ return codeRef;
18
+ };
19
+
9
20
  interface ToolInputProps {
10
- type?: string;
11
- contents?: string;
12
- view?: ToolCallContent;
21
+ highlightLanguage?: string;
22
+ contents?: string | object;
23
+ toolCallView?: { content: string };
13
24
  }
25
+ export const ToolInput: React.FC<ToolInputProps> = memo((props) => {
26
+ const { highlightLanguage, contents, toolCallView } = props;
14
27
 
15
- /**
16
- * Renders the ToolInput component.
17
- */
18
- export const ToolInput: React.FC<ToolInputProps> = ({
19
- type,
20
- contents,
21
- view,
22
- }) => {
23
- if (!contents && !view?.content) {
24
- return null;
25
- }
28
+ const codeRef = useCodeHighlight(highlightLanguage);
26
29
 
27
- if (view) {
30
+ if (!contents && !toolCallView?.content) return null;
31
+
32
+ if (toolCallView) {
28
33
  const toolViewRef = useRef<HTMLDivElement>(null);
34
+
29
35
  useEffect(() => {
30
- // Sniff around for code in the view that could be text highlighted
31
- if (toolViewRef.current) {
32
- for (const child of toolViewRef.current.children) {
33
- if (child.tagName === "PRE") {
34
- const childChild = child.firstElementChild;
35
- if (childChild && childChild.tagName === "CODE") {
36
- const hasLanguageClass = Array.from(childChild.classList).some(
37
- (className) => className.startsWith("language-"),
38
- );
39
- if (hasLanguageClass) {
40
- child.classList.add("tool-output");
41
- highlightElement(childChild as HTMLElement);
42
- }
36
+ if (toolCallView?.content && toolViewRef.current) {
37
+ requestAnimationFrame(() => {
38
+ const codeBlocks = toolViewRef.current!.querySelectorAll("pre code");
39
+ codeBlocks.forEach((block) => {
40
+ if (block.className.includes("language-")) {
41
+ block.classList.add("sourceCode");
42
+ highlightElement(block as HTMLElement);
43
43
  }
44
- }
45
- }
44
+ });
45
+ });
46
46
  }
47
- }, [contents, view]);
47
+ }, [toolCallView?.content]);
48
+
48
49
  return (
49
50
  <MarkdownDiv
50
- markdown={view.content}
51
+ markdown={toolCallView.content}
51
52
  ref={toolViewRef}
52
- className={clsx(styles.bottomMargin)}
53
+ className={clsx(styles.bottomMargin, "text-size-small")}
53
54
  />
54
55
  );
55
- } else {
56
- const toolInputRef = useRef(null);
57
- useEffect(() => {
58
- if (type) {
59
- const tokens = languages[type];
60
- if (toolInputRef.current && tokens) {
61
- highlightElement(toolInputRef.current);
62
- }
63
- }
64
- }, [contents, type, view]);
56
+ }
65
57
 
66
- contents =
67
- typeof contents === "object" || Array.isArray(contents)
68
- ? JSON.stringify(contents)
69
- : contents;
70
- const key = murmurhash.v3(contents || "");
58
+ const formattedContent =
59
+ typeof contents === "object" ? JSON.stringify(contents) : contents;
71
60
 
72
- return (
73
- <pre
74
- className={clsx("tool-output", styles.outputPre, styles.bottomMargin)}
61
+ return (
62
+ <pre className={clsx("tool-output", styles.outputPre, styles.bottomMargin)}>
63
+ <code
64
+ ref={codeRef}
65
+ className={clsx(
66
+ "source-code",
67
+ "sourceCode",
68
+ `language-${highlightLanguage}`,
69
+ styles.outputCode,
70
+ )}
75
71
  >
76
- <code
77
- ref={toolInputRef}
78
- key={key}
79
- className={clsx("source-code", `language-${type}`, styles.outputCode)}
80
- >
81
- {contents}
82
- </code>
83
- </pre>
84
- );
85
- }
86
- };
72
+ {formattedContent}
73
+ </code>
74
+ </pre>
75
+ );
76
+ });
@@ -18,21 +18,28 @@ export const ToolOutput: React.FC<ToolOutputProps> = ({ output }) => {
18
18
  // First process an array or object into a string
19
19
  const outputs = [];
20
20
  if (Array.isArray(output)) {
21
- output.forEach((out) => {
21
+ output.forEach((out, idx) => {
22
+ const key = `tool-output-${idx}`;
22
23
  if (out.type === "text") {
23
- outputs.push(<ToolTextOutput text={out.text} />);
24
+ outputs.push(<ToolTextOutput text={out.text} key={key} />);
24
25
  } else {
25
26
  if (out.image.startsWith("data:")) {
26
27
  outputs.push(
27
- <img className={clsx(styles.toolImage)} src={out.image} />,
28
+ <img
29
+ className={clsx(styles.toolImage)}
30
+ src={out.image}
31
+ key={key}
32
+ />,
28
33
  );
29
34
  } else {
30
- outputs.push(<ToolTextOutput text={String(out.image)} />);
35
+ outputs.push(<ToolTextOutput text={String(out.image)} key={key} />);
31
36
  }
32
37
  }
33
38
  });
34
39
  } else {
35
- outputs.push(<ToolTextOutput text={String(output)} />);
40
+ outputs.push(
41
+ <ToolTextOutput text={String(output)} key={"tool-output-single"} />,
42
+ );
36
43
  }
37
44
  return <div className={clsx(styles.output)}>{outputs}</div>;
38
45
  };