inspect-ai 0.3.103__py3-none-any.whl → 0.3.105__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.
- inspect_ai/_cli/common.py +2 -1
- inspect_ai/_cli/eval.py +2 -2
- inspect_ai/_display/core/active.py +3 -0
- inspect_ai/_display/core/config.py +1 -0
- inspect_ai/_display/core/panel.py +21 -13
- inspect_ai/_display/core/results.py +3 -7
- inspect_ai/_display/core/rich.py +3 -5
- inspect_ai/_display/log/__init__.py +0 -0
- inspect_ai/_display/log/display.py +173 -0
- inspect_ai/_display/plain/display.py +2 -2
- inspect_ai/_display/rich/display.py +2 -4
- inspect_ai/_display/textual/app.py +1 -6
- inspect_ai/_display/textual/widgets/task_detail.py +3 -14
- inspect_ai/_display/textual/widgets/tasks.py +1 -1
- inspect_ai/_eval/eval.py +1 -1
- inspect_ai/_eval/evalset.py +3 -3
- inspect_ai/_eval/registry.py +6 -1
- inspect_ai/_eval/run.py +5 -1
- inspect_ai/_eval/task/constants.py +1 -0
- inspect_ai/_eval/task/log.py +2 -0
- inspect_ai/_eval/task/run.py +65 -39
- inspect_ai/_util/citation.py +88 -0
- inspect_ai/_util/content.py +24 -2
- inspect_ai/_util/json.py +17 -2
- inspect_ai/_util/registry.py +19 -4
- inspect_ai/_view/schema.py +0 -6
- inspect_ai/_view/server.py +17 -0
- inspect_ai/_view/www/dist/assets/index.css +93 -31
- inspect_ai/_view/www/dist/assets/index.js +10639 -10011
- inspect_ai/_view/www/log-schema.json +418 -1
- inspect_ai/_view/www/node_modules/flatted/python/flatted.py +149 -0
- inspect_ai/_view/www/node_modules/katex/src/fonts/generate_fonts.py +58 -0
- inspect_ai/_view/www/node_modules/katex/src/metrics/extract_tfms.py +114 -0
- inspect_ai/_view/www/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
- inspect_ai/_view/www/node_modules/katex/src/metrics/format_json.py +28 -0
- inspect_ai/_view/www/node_modules/katex/src/metrics/parse_tfm.py +211 -0
- inspect_ai/_view/www/package.json +2 -2
- inspect_ai/_view/www/src/@types/log.d.ts +140 -39
- inspect_ai/_view/www/src/app/content/RecordTree.tsx +13 -0
- inspect_ai/_view/www/src/app/log-view/LogView.tsx +1 -1
- inspect_ai/_view/www/src/app/routing/logNavigation.ts +31 -0
- inspect_ai/_view/www/src/app/routing/{navigationHooks.ts → sampleNavigation.ts} +39 -86
- inspect_ai/_view/www/src/app/samples/SampleDialog.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/chat/ChatMessage.module.css +4 -0
- inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +17 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageCitations.module.css +16 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageCitations.tsx +63 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageContent.module.css +6 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +174 -25
- inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +21 -3
- inspect_ai/_view/www/src/app/samples/chat/content-data/ContentDataView.module.css +7 -0
- inspect_ai/_view/www/src/app/samples/chat/content-data/ContentDataView.tsx +111 -0
- inspect_ai/_view/www/src/app/samples/chat/content-data/WebSearch.module.css +10 -0
- inspect_ai/_view/www/src/app/samples/chat/content-data/WebSearch.tsx +14 -0
- inspect_ai/_view/www/src/app/samples/chat/content-data/WebSearchResults.module.css +19 -0
- inspect_ai/_view/www/src/app/samples/chat/content-data/WebSearchResults.tsx +49 -0
- inspect_ai/_view/www/src/app/samples/chat/messages.ts +7 -1
- inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +12 -2
- inspect_ai/_view/www/src/app/samples/chat/types.ts +4 -0
- inspect_ai/_view/www/src/app/samples/list/SampleList.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/sample-tools/filters.ts +26 -0
- inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/SampleFilter.tsx +14 -3
- inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/completions.ts +359 -7
- inspect_ai/_view/www/src/app/samples/sample-tools/sample-filter/language.ts +6 -0
- inspect_ai/_view/www/src/app/samples/sampleLimit.ts +2 -2
- inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +4 -4
- inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +1 -1
- inspect_ai/_view/www/src/client/api/api-browser.ts +25 -0
- inspect_ai/_view/www/src/client/api/api-http.ts +3 -0
- inspect_ai/_view/www/src/client/api/api-vscode.ts +6 -0
- inspect_ai/_view/www/src/client/api/client-api.ts +3 -0
- inspect_ai/_view/www/src/client/api/jsonrpc.ts +1 -0
- inspect_ai/_view/www/src/client/api/types.ts +3 -0
- inspect_ai/_view/www/src/components/MarkdownDiv.tsx +15 -2
- inspect_ai/_view/www/src/state/samplePolling.ts +17 -1
- inspect_ai/_view/www/src/tests/README.md +2 -2
- inspect_ai/_view/www/src/utils/git.ts +3 -1
- inspect_ai/_view/www/src/utils/html.ts +6 -0
- inspect_ai/agent/_handoff.py +8 -5
- inspect_ai/agent/_react.py +5 -5
- inspect_ai/dataset/_dataset.py +1 -1
- inspect_ai/log/_condense.py +5 -0
- inspect_ai/log/_file.py +4 -1
- inspect_ai/log/_log.py +9 -4
- inspect_ai/log/_recorders/json.py +4 -2
- inspect_ai/log/_samples.py +5 -0
- inspect_ai/log/_util.py +2 -0
- inspect_ai/model/__init__.py +14 -0
- inspect_ai/model/_call_tools.py +17 -8
- inspect_ai/model/_chat_message.py +3 -0
- inspect_ai/model/_openai_responses.py +80 -34
- inspect_ai/model/_providers/_anthropic_citations.py +158 -0
- inspect_ai/model/_providers/_google_citations.py +100 -0
- inspect_ai/model/_providers/anthropic.py +219 -36
- inspect_ai/model/_providers/google.py +98 -22
- inspect_ai/model/_providers/mistral.py +20 -7
- inspect_ai/model/_providers/openai.py +11 -10
- inspect_ai/model/_providers/openai_compatible.py +3 -2
- inspect_ai/model/_providers/openai_responses.py +2 -5
- inspect_ai/model/_providers/perplexity.py +123 -0
- inspect_ai/model/_providers/providers.py +13 -2
- inspect_ai/model/_providers/vertex.py +3 -0
- inspect_ai/model/_trim.py +5 -0
- inspect_ai/tool/__init__.py +14 -0
- inspect_ai/tool/_mcp/_mcp.py +5 -2
- inspect_ai/tool/_mcp/sampling.py +19 -3
- inspect_ai/tool/_mcp/server.py +1 -1
- inspect_ai/tool/_tool.py +10 -1
- inspect_ai/tool/_tools/_web_search/_base_http_provider.py +104 -0
- inspect_ai/tool/_tools/_web_search/_exa.py +78 -0
- inspect_ai/tool/_tools/_web_search/_google.py +22 -25
- inspect_ai/tool/_tools/_web_search/_tavily.py +47 -65
- inspect_ai/tool/_tools/_web_search/_web_search.py +83 -36
- inspect_ai/tool/_tools/_web_search/_web_search_provider.py +7 -0
- inspect_ai/util/__init__.py +8 -0
- inspect_ai/util/_background.py +64 -0
- inspect_ai/util/_display.py +11 -2
- inspect_ai/util/_limit.py +72 -5
- inspect_ai/util/_sandbox/__init__.py +2 -0
- inspect_ai/util/_sandbox/docker/compose.py +2 -2
- inspect_ai/util/_sandbox/service.py +28 -7
- inspect_ai/util/_span.py +12 -1
- inspect_ai/util/_subprocess.py +51 -38
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/METADATA +2 -2
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/RECORD +134 -109
- /inspect_ai/model/{_openai_computer_use.py → _providers/_openai_computer_use.py} +0 -0
- /inspect_ai/model/{_openai_web_search.py → _providers/_openai_web_search.py} +0 -0
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/WHEEL +0 -0
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.103.dist-info → inspect_ai-0.3.105.dist-info}/top_level.txt +0 -0
@@ -3,84 +3,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
3
3
|
import { useFilteredSamples } from "../../state/hooks";
|
4
4
|
import { useStore } from "../../state/store";
|
5
5
|
import { directoryRelativeUrl } from "../../utils/uri";
|
6
|
-
import {
|
7
|
-
|
8
|
-
export const useLogNavigation = () => {
|
9
|
-
const navigate = useNavigate();
|
10
|
-
const { logPath } = useParams<{ logPath: string }>();
|
11
|
-
const logs = useStore((state) => state.logs.logs);
|
12
|
-
const loadedLog = useStore((state) => state.log.loadedLog);
|
13
|
-
|
14
|
-
const selectTab = useCallback(
|
15
|
-
(tabId: string) => {
|
16
|
-
// Only update URL if we have a loaded log
|
17
|
-
if (loadedLog && logPath) {
|
18
|
-
// We already have the logPath from params, just navigate to the tab
|
19
|
-
const url = logUrlRaw(logPath, tabId);
|
20
|
-
navigate(url);
|
21
|
-
} else if (loadedLog) {
|
22
|
-
// Fallback to constructing the path if needed
|
23
|
-
const url = logUrl(loadedLog, logs.log_dir, tabId);
|
24
|
-
navigate(url);
|
25
|
-
}
|
26
|
-
},
|
27
|
-
[loadedLog, logPath, logs.log_dir, navigate],
|
28
|
-
);
|
29
|
-
|
30
|
-
return {
|
31
|
-
selectTab,
|
32
|
-
};
|
33
|
-
};
|
34
|
-
|
35
|
-
export const useSampleUrl = () => {
|
36
|
-
const { logPath, tabId, sampleTabId } = useParams<{
|
37
|
-
logPath?: string;
|
38
|
-
tabId?: string;
|
39
|
-
sampleTabId?: string;
|
40
|
-
}>();
|
41
|
-
|
42
|
-
const logDirectory = useStore((state) => state.logs.logs.log_dir);
|
43
|
-
|
44
|
-
const selectedLogFile = useStore((state) => state.logs.selectedLogFile);
|
45
|
-
|
46
|
-
// Helper function to resolve the log path for URLs
|
47
|
-
const resolveLogPath = useCallback(() => {
|
48
|
-
// If we have a logPath from URL params, use that
|
49
|
-
if (logPath) {
|
50
|
-
return logPath;
|
51
|
-
}
|
52
|
-
|
53
|
-
if (selectedLogFile) {
|
54
|
-
return directoryRelativeUrl(selectedLogFile, logDirectory);
|
55
|
-
}
|
56
|
-
|
57
|
-
return undefined;
|
58
|
-
}, [logPath, selectedLogFile, logDirectory]);
|
59
|
-
|
60
|
-
// Get a sample URL for a specific sample
|
61
|
-
const getSampleUrl = useCallback(
|
62
|
-
(
|
63
|
-
sampleId: string | number,
|
64
|
-
epoch: number,
|
65
|
-
specificSampleTabId?: string,
|
66
|
-
) => {
|
67
|
-
const resolvedPath = resolveLogPath();
|
68
|
-
if (resolvedPath) {
|
69
|
-
const currentSampleTabId = specificSampleTabId || sampleTabId;
|
70
|
-
const url = sampleUrl(
|
71
|
-
resolvedPath,
|
72
|
-
sampleId,
|
73
|
-
epoch,
|
74
|
-
currentSampleTabId,
|
75
|
-
);
|
76
|
-
return url;
|
77
|
-
}
|
78
|
-
return undefined;
|
79
|
-
},
|
80
|
-
[resolveLogPath, tabId, sampleTabId],
|
81
|
-
);
|
82
|
-
return getSampleUrl;
|
83
|
-
};
|
6
|
+
import { logUrlRaw, sampleUrl } from "./url";
|
84
7
|
|
85
8
|
/**
|
86
9
|
* Hook that provides sample navigation utilities with proper URL handling
|
@@ -127,6 +50,7 @@ export const useSampleNavigation = () => {
|
|
127
50
|
const setShowingSampleDialog = useStore(
|
128
51
|
(state) => state.appActions.setShowingSampleDialog,
|
129
52
|
);
|
53
|
+
const showingSampleDialog = useStore((state) => state.app.dialogs.sample);
|
130
54
|
|
131
55
|
// Navigate to a specific sample with index
|
132
56
|
const showSample = useCallback(
|
@@ -163,22 +87,51 @@ export const useSampleNavigation = () => {
|
|
163
87
|
],
|
164
88
|
);
|
165
89
|
|
90
|
+
const navigateSampleIndex = useCallback(
|
91
|
+
(index: number) => {
|
92
|
+
if (index > -1 && index < sampleSummaries.length) {
|
93
|
+
if (showingSampleDialog) {
|
94
|
+
const resolvedPath = resolveLogPath();
|
95
|
+
if (resolvedPath) {
|
96
|
+
const summary = sampleSummaries[index];
|
97
|
+
const url = sampleUrl(
|
98
|
+
resolvedPath,
|
99
|
+
summary.id,
|
100
|
+
summary.epoch,
|
101
|
+
sampleTabId,
|
102
|
+
);
|
103
|
+
|
104
|
+
// Navigate to the sample URL
|
105
|
+
navigate(url);
|
106
|
+
}
|
107
|
+
} else {
|
108
|
+
selectSample(index);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
},
|
112
|
+
[
|
113
|
+
selectedSampleIndex,
|
114
|
+
showSample,
|
115
|
+
sampleTabId,
|
116
|
+
sampleSummaries,
|
117
|
+
showingSampleDialog,
|
118
|
+
resolveLogPath,
|
119
|
+
navigate,
|
120
|
+
],
|
121
|
+
);
|
122
|
+
|
166
123
|
// Navigate to the next sample
|
167
124
|
const nextSample = useCallback(() => {
|
168
125
|
const itemsCount = sampleSummaries.length;
|
169
126
|
const next = Math.min(selectedSampleIndex + 1, itemsCount - 1);
|
170
|
-
|
171
|
-
|
172
|
-
}
|
173
|
-
}, [selectedSampleIndex, showSample, sampleTabId]);
|
127
|
+
navigateSampleIndex(next);
|
128
|
+
}, [selectedSampleIndex, navigateSampleIndex, sampleSummaries]);
|
174
129
|
|
175
130
|
// Navigate to the previous sample
|
176
131
|
const previousSample = useCallback(() => {
|
177
132
|
const prev = selectedSampleIndex - 1;
|
178
|
-
|
179
|
-
|
180
|
-
}
|
181
|
-
}, [selectedSampleIndex, showSample, sampleTabId]);
|
133
|
+
navigateSampleIndex(prev);
|
134
|
+
}, [selectedSampleIndex, navigateSampleIndex]);
|
182
135
|
|
183
136
|
// Get a sample URL for a specific sample
|
184
137
|
const getSampleUrl = useCallback(
|
@@ -6,9 +6,9 @@ import { ErrorPanel } from "../../components/ErrorPanel";
|
|
6
6
|
import { useLogSelection, usePrevious, useSampleData } from "../../state/hooks";
|
7
7
|
import { useStatefulScrollPosition } from "../../state/scrolling";
|
8
8
|
import { useStore } from "../../state/store";
|
9
|
-
import { useSampleNavigation } from "../routing/navigationHooks";
|
10
9
|
import { SampleDisplay } from "./SampleDisplay";
|
11
10
|
|
11
|
+
import { useSampleNavigation } from "../routing/sampleNavigation";
|
12
12
|
import styles from "./SampleDialog.module.css";
|
13
13
|
|
14
14
|
interface SampleDialogProps {
|
@@ -37,7 +37,7 @@ import { formatTime } from "../../utils/format";
|
|
37
37
|
import { estimateSize } from "../../utils/json";
|
38
38
|
import { printHeadingHtml, printHtml } from "../../utils/print";
|
39
39
|
import { RecordTree } from "../content/RecordTree";
|
40
|
-
import { useSampleDetailNavigation } from "../routing/
|
40
|
+
import { useSampleDetailNavigation } from "../routing/sampleNavigation";
|
41
41
|
import { sampleUrl } from "../routing/url";
|
42
42
|
import { ModelTokenTable } from "../usage/ModelTokenTable";
|
43
43
|
import { ChatViewVirtualList } from "./chat/ChatViewVirtualList";
|
@@ -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
|
);
|
@@ -0,0 +1,16 @@
|
|
1
|
+
.citations {
|
2
|
+
margin-top: 1em;
|
3
|
+
margin-bottom: 1em;
|
4
|
+
display: grid;
|
5
|
+
grid-template-columns: max-content 1fr;
|
6
|
+
column-gap: 0.5em;
|
7
|
+
}
|
8
|
+
|
9
|
+
a.citationLink {
|
10
|
+
display: block;
|
11
|
+
color: var(--bs-body);
|
12
|
+
text-decoration: none;
|
13
|
+
}
|
14
|
+
a.citationLink:hover {
|
15
|
+
text-decoration: underline;
|
16
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import clsx from "clsx";
|
2
|
+
import { FC, Fragment, PropsWithChildren, ReactElement } from "react";
|
3
|
+
import { Citation } from "./types";
|
4
|
+
|
5
|
+
import { decodeHtmlEntities } from "../../../utils/html";
|
6
|
+
import styles from "./MessageCitations.module.css";
|
7
|
+
import { UrlCitation as UrlCitationType } from "../../../@types/log";
|
8
|
+
|
9
|
+
export interface MessageCitationsProps {
|
10
|
+
citations: Citation[];
|
11
|
+
}
|
12
|
+
|
13
|
+
export const MessageCitations: FC<MessageCitationsProps> = ({ citations }) => {
|
14
|
+
if (citations.length === 0) {
|
15
|
+
return undefined;
|
16
|
+
}
|
17
|
+
|
18
|
+
return (
|
19
|
+
<div className={clsx(styles.citations, "text-size-smallest")}>
|
20
|
+
{citations.map((citation, index) => (
|
21
|
+
<Fragment key={index}>
|
22
|
+
<span>{index + 1}</span>
|
23
|
+
<MessageCitation citation={citation} />
|
24
|
+
</Fragment>
|
25
|
+
))}
|
26
|
+
</div>
|
27
|
+
);
|
28
|
+
};
|
29
|
+
|
30
|
+
interface MessageCitationProps {
|
31
|
+
citation: Citation;
|
32
|
+
}
|
33
|
+
|
34
|
+
const MessageCitation: FC<MessageCitationProps> = ({ citation }) => {
|
35
|
+
const innards = decodeHtmlEntities(
|
36
|
+
citation.title ??
|
37
|
+
(typeof citation.cited_text === "string" ? citation.cited_text : ""),
|
38
|
+
);
|
39
|
+
return citation.type === "url" ? (
|
40
|
+
<UrlCitation citation={citation}>{innards}</UrlCitation>
|
41
|
+
) : (
|
42
|
+
<OtherCitation>{innards}</OtherCitation>
|
43
|
+
);
|
44
|
+
};
|
45
|
+
|
46
|
+
const UrlCitation: FC<PropsWithChildren<{ citation: UrlCitationType }>> = ({
|
47
|
+
children,
|
48
|
+
citation,
|
49
|
+
}): ReactElement => (
|
50
|
+
<a
|
51
|
+
href={citation.url}
|
52
|
+
target="_blank"
|
53
|
+
rel="noopener noreferrer"
|
54
|
+
className={clsx(styles.citationLink)}
|
55
|
+
title={`${citation.cited_text || ""}\n${citation.url}`}
|
56
|
+
>
|
57
|
+
{children}
|
58
|
+
</a>
|
59
|
+
);
|
60
|
+
|
61
|
+
const OtherCitation: FC<PropsWithChildren> = ({ children }): ReactElement => (
|
62
|
+
<>{children}</>
|
63
|
+
);
|
@@ -3,6 +3,7 @@ import clsx from "clsx";
|
|
3
3
|
import { FC, ReactNode } from "react";
|
4
4
|
import {
|
5
5
|
ContentAudio,
|
6
|
+
ContentData,
|
6
7
|
ContentImage,
|
7
8
|
ContentReasoning,
|
8
9
|
ContentText,
|
@@ -13,40 +14,42 @@ import {
|
|
13
14
|
import { ContentTool } from "../../../app/types";
|
14
15
|
import ExpandablePanel from "../../../components/ExpandablePanel";
|
15
16
|
import { MarkdownDiv } from "../../../components/MarkdownDiv";
|
17
|
+
import { ContentDataView } from "./content-data/ContentDataView";
|
18
|
+
import { MessageCitations } from "./MessageCitations";
|
16
19
|
import styles from "./MessageContent.module.css";
|
20
|
+
import { MessagesContext } from "./MessageContents";
|
17
21
|
import { ToolOutput } from "./tools/ToolOutput";
|
22
|
+
import { Citation } from "./types";
|
18
23
|
|
19
|
-
type
|
20
|
-
| string
|
21
|
-
| string[]
|
24
|
+
type ContentObject =
|
22
25
|
| ContentText
|
23
26
|
| ContentReasoning
|
24
27
|
| ContentImage
|
25
28
|
| ContentAudio
|
26
29
|
| ContentVideo
|
27
|
-
| ContentTool
|
30
|
+
| ContentTool
|
31
|
+
| ContentData;
|
32
|
+
|
33
|
+
type ContentType = string | string[] | ContentObject;
|
34
|
+
|
35
|
+
type Contents = string | string[] | ContentObject[];
|
28
36
|
|
29
37
|
interface MessageContentProps {
|
30
|
-
contents:
|
31
|
-
|
32
|
-
| string[]
|
33
|
-
| (
|
34
|
-
| ContentText
|
35
|
-
| ContentReasoning
|
36
|
-
| ContentImage
|
37
|
-
| ContentAudio
|
38
|
-
| ContentVideo
|
39
|
-
| ContentTool
|
40
|
-
)[];
|
38
|
+
contents: Contents;
|
39
|
+
context: MessagesContext;
|
41
40
|
}
|
42
41
|
|
43
42
|
/**
|
44
43
|
* Renders message content based on its type.
|
45
44
|
* Supports rendering strings, images, and tools using specific renderers.
|
46
45
|
*/
|
47
|
-
export const MessageContent: FC<MessageContentProps> = ({
|
48
|
-
|
49
|
-
|
46
|
+
export const MessageContent: FC<MessageContentProps> = ({
|
47
|
+
contents,
|
48
|
+
context,
|
49
|
+
}) => {
|
50
|
+
const normalized = normalizeContent(contents);
|
51
|
+
if (Array.isArray(normalized)) {
|
52
|
+
return normalized.map((content, index) => {
|
50
53
|
if (typeof content === "string") {
|
51
54
|
return messageRenderers["text"].render(
|
52
55
|
`text-content-${index}`,
|
@@ -55,8 +58,10 @@ export const MessageContent: FC<MessageContentProps> = ({ contents }) => {
|
|
55
58
|
text: content,
|
56
59
|
refusal: null,
|
57
60
|
internal: null,
|
61
|
+
citations: null,
|
58
62
|
},
|
59
63
|
index === contents.length - 1,
|
64
|
+
context,
|
60
65
|
);
|
61
66
|
} else {
|
62
67
|
if (content) {
|
@@ -66,6 +71,7 @@ export const MessageContent: FC<MessageContentProps> = ({ contents }) => {
|
|
66
71
|
`text-${content.type}-${index}`,
|
67
72
|
content,
|
68
73
|
index === contents.length - 1,
|
74
|
+
context,
|
69
75
|
);
|
70
76
|
} else {
|
71
77
|
console.error(`Unknown message content type '${content.type}'`);
|
@@ -77,32 +83,52 @@ export const MessageContent: FC<MessageContentProps> = ({ contents }) => {
|
|
77
83
|
// This is a simple string
|
78
84
|
const contentText: ContentText = {
|
79
85
|
type: "text",
|
80
|
-
text:
|
86
|
+
text: normalized,
|
81
87
|
refusal: null,
|
82
88
|
internal: null,
|
89
|
+
citations: null,
|
83
90
|
};
|
84
91
|
return messageRenderers["text"].render(
|
85
92
|
"text-message-content",
|
86
93
|
contentText,
|
87
94
|
true,
|
95
|
+
context,
|
88
96
|
);
|
89
97
|
}
|
90
98
|
};
|
91
99
|
|
92
100
|
interface MessageRenderer {
|
93
|
-
render: (
|
101
|
+
render: (
|
102
|
+
key: string,
|
103
|
+
content: ContentType,
|
104
|
+
isLast: boolean,
|
105
|
+
context: MessagesContext,
|
106
|
+
) => ReactNode;
|
94
107
|
}
|
95
108
|
|
96
109
|
const messageRenderers: Record<string, MessageRenderer> = {
|
97
110
|
text: {
|
98
111
|
render: (key, content, isLast) => {
|
112
|
+
// The context provides a way to share context between different
|
113
|
+
// rendering. In this case, we'll use it to keep track of citations
|
99
114
|
const c = content as ContentText;
|
115
|
+
const cites = c.citations ?? [];
|
116
|
+
|
117
|
+
if (!c.text && !cites.length) {
|
118
|
+
return undefined;
|
119
|
+
}
|
120
|
+
|
100
121
|
return (
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
122
|
+
<>
|
123
|
+
<MarkdownDiv
|
124
|
+
key={key}
|
125
|
+
markdown={c.text || ""}
|
126
|
+
className={isLast ? "no-last-para-padding" : ""}
|
127
|
+
/>
|
128
|
+
{c.citations ? (
|
129
|
+
<MessageCitations citations={c.citations as Citation[]} />
|
130
|
+
) : undefined}
|
131
|
+
</>
|
106
132
|
);
|
107
133
|
},
|
108
134
|
},
|
@@ -172,6 +198,12 @@ const messageRenderers: Record<string, MessageRenderer> = {
|
|
172
198
|
return <ToolOutput output={c.content} key={key} />;
|
173
199
|
},
|
174
200
|
},
|
201
|
+
data: {
|
202
|
+
render: (key, content) => {
|
203
|
+
const c = content as ContentData;
|
204
|
+
return <ContentDataView id={key} contentData={c} />;
|
205
|
+
},
|
206
|
+
},
|
175
207
|
};
|
176
208
|
|
177
209
|
/**
|
@@ -190,5 +222,122 @@ const mimeTypeForFormat = (format: Format1 | Format2): string => {
|
|
190
222
|
return "video/mp4";
|
191
223
|
case "mpeg":
|
192
224
|
return "video/mpeg";
|
225
|
+
default:
|
226
|
+
return "video/mp4"; // Default to mp4 for unknown formats
|
227
|
+
}
|
228
|
+
};
|
229
|
+
|
230
|
+
// This collapses sequential runs of text content into a single text content,
|
231
|
+
// adding citations as superscript counters at the end of the text for each block
|
232
|
+
// containing citations. The citations are then attached to the content where
|
233
|
+
// they can be rendered separately (with coordinating numbers).
|
234
|
+
const normalizeContent = (contents: Contents): Contents => {
|
235
|
+
// its a string
|
236
|
+
if (typeof contents === "string") {
|
237
|
+
return contents;
|
238
|
+
}
|
239
|
+
|
240
|
+
// its an array of strings
|
241
|
+
if (contents.length > 0 && typeof contents[0] === "string") {
|
242
|
+
return contents;
|
243
|
+
}
|
244
|
+
|
245
|
+
const result: ContentObject[] = [];
|
246
|
+
const collection: ContentText[] = [];
|
247
|
+
|
248
|
+
const collect = () => {
|
249
|
+
if (collection.length > 0) {
|
250
|
+
// Flatten the citations from the collection
|
251
|
+
const filteredCitations = collection.flatMap((c) => c.citations || []);
|
252
|
+
// Render citations as superscript counters
|
253
|
+
let citeCount = 0;
|
254
|
+
const textWithCites = collection
|
255
|
+
.map((c) => {
|
256
|
+
// separate the cites into those with a position and those without
|
257
|
+
// sort by end_index (to allow for numbering to not affect indexes)
|
258
|
+
// Type guard function to check if cited_text is a range
|
259
|
+
const positionalCites = (c.citations ?? [])
|
260
|
+
.filter(isCitationWithRange)
|
261
|
+
.sort((a, b) => b.cited_text[1] - a.cited_text[1]);
|
262
|
+
|
263
|
+
const endCites = c.citations?.filter(
|
264
|
+
(citation) => !isCitationWithRange(citation),
|
265
|
+
);
|
266
|
+
|
267
|
+
// Process cites with positions
|
268
|
+
let textWithCites = c.text;
|
269
|
+
for (let i = 0; i < positionalCites.length; i++) {
|
270
|
+
const end_index = positionalCites[i].cited_text[1];
|
271
|
+
|
272
|
+
textWithCites =
|
273
|
+
textWithCites.slice(0, end_index) +
|
274
|
+
`<sup>${positionalCites.length - i}</sup>` +
|
275
|
+
textWithCites.slice(end_index);
|
276
|
+
}
|
277
|
+
citeCount = citeCount + positionalCites.length;
|
278
|
+
|
279
|
+
// Process cites without positions (they just attach to the end of the content)
|
280
|
+
const citeText = endCites?.map((_citation) => `${++citeCount}`);
|
281
|
+
let inlineCites = "";
|
282
|
+
if (citeText && citeText.length > 0) {
|
283
|
+
inlineCites = `<sup>${citeText.join(",")}</sup>`;
|
284
|
+
}
|
285
|
+
return (textWithCites || "") + inlineCites;
|
286
|
+
})
|
287
|
+
.join("");
|
288
|
+
|
289
|
+
// Flatten the text from the collection into a single text content
|
290
|
+
result.push({
|
291
|
+
type: "text",
|
292
|
+
text: textWithCites,
|
293
|
+
refusal: null,
|
294
|
+
internal: null,
|
295
|
+
citations: filteredCitations,
|
296
|
+
});
|
297
|
+
collection.length = 0;
|
298
|
+
}
|
299
|
+
};
|
300
|
+
|
301
|
+
for (const content of contents) {
|
302
|
+
if (typeof content === "string") {
|
303
|
+
// this shouldn't happen, but if it does
|
304
|
+
// just convert it to a text content
|
305
|
+
result.push({
|
306
|
+
type: "text",
|
307
|
+
text: content,
|
308
|
+
refusal: null,
|
309
|
+
internal: null,
|
310
|
+
citations: null,
|
311
|
+
});
|
312
|
+
continue;
|
313
|
+
}
|
314
|
+
|
315
|
+
if (content.type === "text") {
|
316
|
+
// Collect text until we hit a non-text content
|
317
|
+
collection.push(content);
|
318
|
+
continue;
|
319
|
+
} else {
|
320
|
+
// collect any text content before this non-text content
|
321
|
+
collect();
|
322
|
+
result.push(content);
|
323
|
+
}
|
193
324
|
}
|
325
|
+
|
326
|
+
// collect any remaining text content
|
327
|
+
collect();
|
328
|
+
|
329
|
+
return result;
|
194
330
|
};
|
331
|
+
|
332
|
+
// This is a helper that makes Omit<> work with a union type by distributing
|
333
|
+
// the omit over the union members.
|
334
|
+
export type DistributiveOmit<TObj, TKey extends PropertyKey> = TObj extends any
|
335
|
+
? Omit<TObj, TKey>
|
336
|
+
: never;
|
337
|
+
|
338
|
+
/** Type guard that allows narrowing down to Citations whose `cited_text` is a range */
|
339
|
+
const isCitationWithRange = (
|
340
|
+
citation: Citation,
|
341
|
+
): citation is DistributiveOmit<Citation, "cited_text"> & {
|
342
|
+
cited_text: [number, number];
|
343
|
+
} => Array.isArray(citation.cited_text);
|
@@ -12,7 +12,7 @@ import clsx from "clsx";
|
|
12
12
|
import { FC, Fragment } from "react";
|
13
13
|
import { ContentTool } from "../../../app/types";
|
14
14
|
import styles from "./MessageContents.module.css";
|
15
|
-
import { ChatViewToolCallStyle } from "./types";
|
15
|
+
import { ChatViewToolCallStyle, Citation } from "./types";
|
16
16
|
|
17
17
|
interface MessageContentsProps {
|
18
18
|
id: string;
|
@@ -21,12 +21,24 @@ interface MessageContentsProps {
|
|
21
21
|
toolCallStyle: ChatViewToolCallStyle;
|
22
22
|
}
|
23
23
|
|
24
|
+
export interface MessagesContext {
|
25
|
+
citations: Citation[];
|
26
|
+
}
|
27
|
+
|
28
|
+
export const defaultContext = () => {
|
29
|
+
return {
|
30
|
+
citeOffset: 0,
|
31
|
+
citations: [],
|
32
|
+
};
|
33
|
+
};
|
34
|
+
|
24
35
|
export const MessageContents: FC<MessageContentsProps> = ({
|
25
36
|
id,
|
26
37
|
message,
|
27
38
|
toolMessages,
|
28
39
|
toolCallStyle,
|
29
40
|
}) => {
|
41
|
+
const context: MessagesContext = defaultContext();
|
30
42
|
if (
|
31
43
|
message.role === "assistant" &&
|
32
44
|
message.tool_calls &&
|
@@ -79,14 +91,18 @@ export const MessageContents: FC<MessageContentsProps> = ({
|
|
79
91
|
<Fragment>
|
80
92
|
{message.content && (
|
81
93
|
<div className={styles.content}>
|
82
|
-
<MessageContent contents={message.content} />
|
94
|
+
<MessageContent contents={message.content} context={context} />
|
83
95
|
</div>
|
84
96
|
)}
|
85
97
|
{toolCalls}
|
86
98
|
</Fragment>
|
87
99
|
);
|
88
100
|
} else {
|
89
|
-
return
|
101
|
+
return (
|
102
|
+
<>
|
103
|
+
<MessageContent contents={message.content} context={context} />
|
104
|
+
</>
|
105
|
+
);
|
90
106
|
}
|
91
107
|
};
|
92
108
|
|
@@ -109,6 +125,7 @@ const resolveToolMessage = (toolMessage?: ChatMessageTool): ContentTool[] => {
|
|
109
125
|
text: content,
|
110
126
|
refusal: null,
|
111
127
|
internal: null,
|
128
|
+
citations: null,
|
112
129
|
},
|
113
130
|
],
|
114
131
|
},
|
@@ -125,6 +142,7 @@ const resolveToolMessage = (toolMessage?: ChatMessageTool): ContentTool[] => {
|
|
125
142
|
text: con,
|
126
143
|
refusal: null,
|
127
144
|
internal: null,
|
145
|
+
citations: null,
|
128
146
|
},
|
129
147
|
],
|
130
148
|
} as ContentTool;
|