@adminforth/agent 1.1.0 → 1.2.0

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.
@@ -0,0 +1,92 @@
1
+ import { AIMessage } from "@langchain/core/messages";
2
+ import { createMiddleware } from "langchain";
3
+
4
+ type OpenAiResponsesMetadata = {
5
+ id?: string;
6
+ };
7
+
8
+ type OpenAiResponsesContext = {
9
+ sessionId: string;
10
+ turnId: string;
11
+ };
12
+
13
+ function getTurnKey(context: OpenAiResponsesContext) {
14
+ return `${context.sessionId}:${context.turnId}`;
15
+ }
16
+
17
+ function getResponseId(message: AIMessage) {
18
+ const metadata = message.response_metadata as OpenAiResponsesMetadata | undefined;
19
+ return metadata?.id ?? null;
20
+ }
21
+
22
+ function getPreviousResponseId(modelSettings?: Record<string, unknown>) {
23
+ return (modelSettings as { previous_response_id?: string } | undefined)
24
+ ?.previous_response_id;
25
+ }
26
+
27
+ function getContinuationMessages<T extends { response_metadata?: unknown }>(
28
+ messages: T[],
29
+ previousResponseId: string,
30
+ ) {
31
+ let continuationStartIndex: number | null = null;
32
+
33
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
34
+ const message = messages[index];
35
+
36
+ if (
37
+ AIMessage.isInstance(message) &&
38
+ (message.response_metadata as OpenAiResponsesMetadata | undefined)?.id ===
39
+ previousResponseId
40
+ ) {
41
+ continuationStartIndex = index + 1;
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (continuationStartIndex === null) {
47
+ return null;
48
+ }
49
+
50
+ return messages.slice(continuationStartIndex);
51
+ }
52
+
53
+ export function createOpenAiResponsesContinuationMiddleware() {
54
+ const responseIdsByTurn = new Map<string, string>();
55
+
56
+ return createMiddleware({
57
+ name: "OpenAiResponsesContinuationMiddleware",
58
+ async wrapModelCall(request, handler) {
59
+ const context = request.runtime.context as OpenAiResponsesContext;
60
+ const turnKey = getTurnKey(context);
61
+ const previousResponseId =
62
+ getPreviousResponseId(request.modelSettings) ??
63
+ responseIdsByTurn.get(turnKey);
64
+ const continuationMessages = previousResponseId
65
+ ? getContinuationMessages(request.messages, previousResponseId)
66
+ : null;
67
+
68
+ const response = await handler(
69
+ previousResponseId && continuationMessages
70
+ ? {
71
+ ...request,
72
+ messages: continuationMessages,
73
+ modelSettings: {
74
+ ...request.modelSettings,
75
+ previous_response_id: previousResponseId,
76
+ },
77
+ }
78
+ : request,
79
+ ) as AIMessage;
80
+
81
+ const responseId = getResponseId(response);
82
+
83
+ if (responseId) {
84
+ responseIdsByTurn.set(turnKey, responseId);
85
+ } else {
86
+ responseIdsByTurn.delete(turnKey);
87
+ }
88
+
89
+ return response;
90
+ },
91
+ });
92
+ }
@@ -23,6 +23,8 @@ export type SequenceDebug = {
23
23
  prompt: string;
24
24
  reasoning: string;
25
25
  text: string;
26
+ cachedTokens: number;
27
+ responseId: string | null;
26
28
  toolCalls: SequenceDebugToolCall[];
27
29
  endedAt: string;
28
30
  resultType: SequenceDebugResultType;
@@ -37,9 +39,21 @@ type PendingSequenceDebug = Omit<SequenceDebug, "toolCalls" | "endedAt" | "resul
37
39
  type SequenceDebugModelCall = {
38
40
  reasoning: string;
39
41
  text: string;
42
+ cachedTokens: number;
43
+ responseId: string | null;
40
44
  resultType: SequenceDebugResultType;
41
45
  };
42
46
 
47
+ type OpenAiUsageMetadata = {
48
+ input_token_details?: {
49
+ cache_read?: number;
50
+ };
51
+ };
52
+
53
+ type OpenAiResponseMetadata = {
54
+ id?: string;
55
+ };
56
+
43
57
  export type SequenceDebugModelCallSink = {
44
58
  handleModelCallStart: (prompt: string) => void;
45
59
  handleModelCallComplete: (params: SequenceDebugModelCall) => void;
@@ -58,6 +72,8 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
58
72
  prompt: "",
59
73
  reasoning: "",
60
74
  text: "",
75
+ cachedTokens: 0,
76
+ responseId: null,
61
77
  toolCalls: [],
62
78
  pendingToolCalls: 0,
63
79
  resultType: null,
@@ -83,6 +99,8 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
83
99
  prompt: sequence.prompt,
84
100
  reasoning: sequence.reasoning,
85
101
  text: sequence.text,
102
+ cachedTokens: sequence.cachedTokens,
103
+ responseId: sequence.responseId,
86
104
  toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
87
105
  endedAt: new Date().toISOString(),
88
106
  resultType: sequence.resultType ?? "final_text",
@@ -172,6 +190,12 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
172
190
  return {
173
191
  reasoning,
174
192
  text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
193
+ cachedTokens:
194
+ (message.usage_metadata as OpenAiUsageMetadata | undefined)
195
+ ?.input_token_details?.cache_read ?? 0,
196
+ responseId:
197
+ (message.response_metadata as OpenAiResponseMetadata | undefined)?.id ??
198
+ null,
175
199
  resultType: hasToolCallSignal(message) ? "tool_calls" : "final_text",
176
200
  };
177
201
  }
@@ -216,6 +240,8 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
216
240
  const sequenceDebug = ensureSequenceDebug();
217
241
  sequenceDebug.reasoning = params.reasoning;
218
242
  sequenceDebug.text = params.text;
243
+ sequenceDebug.cachedTokens = params.cachedTokens;
244
+ sequenceDebug.responseId = params.responseId;
219
245
  sequenceDebug.resultType = params.resultType;
220
246
 
221
247
  if (
@@ -11,6 +11,7 @@ import {
11
11
  createSequenceDebugMiddleware,
12
12
  type SequenceDebugModelCallSink,
13
13
  } from "./middleware/sequenceDebug.js";
14
+ import { createOpenAiResponsesContinuationMiddleware } from "./middleware/openAiResponsesContinuation.js";
14
15
  import type { ApiBasedTool } from "../apiBasedTools.js";
15
16
  import type { ToolCallEventSink } from "./toolCallEvents.js";
16
17
 
@@ -199,12 +200,18 @@ export function createAgentChatModel(params: {
199
200
  const baseURL = options.baseURL ?? options.baseUrl;
200
201
  const reasoning = normalizeReasoning(params.reasoning);
201
202
 
203
+ // @ts-ignore
202
204
  return new ChatOpenAI({
203
205
  apiKey: options.openAiApiKey,
204
206
  model,
205
207
  maxTokens: params.maxTokens,
206
208
  useResponsesApi: true,
207
209
  outputVersion: "v1",
210
+
211
+ promptCacheKey: `adminforth-agent:${model}:system-v1:tools-v1`,
212
+
213
+ promptCacheRetention: "in_memory",
214
+
208
215
  ...(reasoning ? { reasoning } : {}),
209
216
  ...(typeof options.timeoutMs === "number"
210
217
  ? { timeout: options.timeoutMs }
@@ -250,16 +257,19 @@ export async function callAgent(params: {
250
257
 
251
258
  const tools = await createAgentTools(customComponentsDir, apiBasedTools);
252
259
  const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools);
260
+ const openAiResponsesContinuationMiddleware =
261
+ createOpenAiResponsesContinuationMiddleware();
253
262
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(
254
263
  sequenceDebugSink,
255
264
  );
256
265
 
257
266
  const middleware = [
258
267
  apiBasedToolsMiddleware,
268
+ openAiResponsesContinuationMiddleware,
259
269
  sequenceDebugMiddleware,
260
270
  summarizationMiddleware({
261
271
  model: summaryModel,
262
- trigger: { tokens: 1024 * 8 },
272
+ trigger: { tokens: 1024 * 128 },
263
273
  keep: { messages: 10 },
264
274
  }),
265
275
  ] as const;
package/build.log CHANGED
@@ -21,10 +21,12 @@ custom/incremark_code_renderers/incremarkCodeHighlight.ts
21
21
  custom/incremark_code_renderers/incremarkRenderer.ts
22
22
  custom/incremark_code_renderers/renderIncremarkMarkdown.ts
23
23
  custom/skills/
24
+ custom/skills/data-analytics/
25
+ custom/skills/data-analytics/SKILL.md
24
26
  custom/skills/fetch_data/
25
27
  custom/skills/fetch_data/SKILL.md
26
28
  custom/skills/mutate_data/
27
29
  custom/skills/mutate_data/SKILL.md
28
30
 
29
- sent 136,273 bytes received 367 bytes 273,280.00 bytes/sec
30
- total size is 134,767 speedup is 0.99
31
+ sent 166,886 bytes received 394 bytes 334,560.00 bytes/sec
32
+ total size is 165,300 speedup is 0.99
@@ -17,9 +17,10 @@
17
17
 
18
18
  </div>
19
19
  <AutoScrollContainer
20
- enabled
20
+ :enabled="!showScrollToBottomButton"
21
21
  class="flex flex-col overflow-y-auto border-t border-gray-200 dark:border-gray-700"
22
22
  ref="scrollContainer"
23
+ :threshold="10"
23
24
  behavior="smooth"
24
25
  >
25
26
 
@@ -57,8 +58,8 @@
57
58
  v-if="props.messages.length === 0"
58
59
  class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
59
60
  >
60
- <p>Start the conversation</p>
61
- <p class="tracking-normal text-base text">Give any input to begin</p>
61
+ <p>{{ $t('Start the conversation') }}</p>
62
+ <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
62
63
  </div>
63
64
  </AutoScrollContainer>
64
65
  </template>
@@ -151,20 +152,16 @@ const groupToolCallParts = (message: IMessage) => {
151
152
  if(!part?.toolInfo) {
152
153
  continue;
153
154
  }
154
- console.log('part', part);
155
155
  if (part.toolInfo.toolName === currentToolName) {
156
- console.log('grouping part with tool name', currentToolName);
157
156
  groupedParts[groupedParts.length - 1].groupedTools.push(part);
158
157
  continue;
159
158
  }
160
159
  currentToolName = part.toolInfo.toolName;
161
- console.log('starting new group with tool name', currentToolName);
162
160
  groupedParts.push({
163
161
  title: currentToolName,
164
162
  groupedTools: [part]
165
163
  });
166
164
  }
167
- console.log('groupedParts', groupedParts);
168
165
  return groupedParts;
169
166
  }
170
167
 
@@ -1,13 +1,16 @@
1
1
  <template>
2
2
  <div
3
- class="max-w-[80%] flex px-4 py-2 m-2 rounded-xl border border-gray-200 dark:border-gray-700"
3
+ class="max-w-[80%] flex px-4 m-2 rounded-xl border border-gray-200 dark:border-gray-700"
4
4
  @click="handleMarkdownLinkClick"
5
- :class="props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
5
+ :class="[
6
+ hasVegaLite ? 'w-full' : '',
7
+ props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
6
8
  : isTypeReasoning || isTypeToolCall ? 'bg-transparent border-none self-start'
7
- : 'bg-blue-100 dark:bg-blue-700/10 self-start'"
9
+ : 'bg-blue-100 dark:bg-blue-700/10 self-start'
10
+ ]"
8
11
  >
9
12
  <IncremarkContent
10
- class="text-wrap break-words max-w-full"
13
+ class="text-wrap break-words w-full max-w-full"
11
14
  v-if="content && props.type === 'text'"
12
15
  :content="content"
13
16
  :is-finished="isFinished"
@@ -88,6 +91,7 @@
88
91
  const content = computed(() => props.message)
89
92
  const isFinished = computed(() => props.state === 'done')
90
93
  const isThoughtsExpanded = ref(false)
94
+ const hasVegaLite = computed(() => props.type === 'text' && props.message.includes('```vega-lite'))
91
95
 
92
96
  const isTypeReasoning = computed(() => props.type === 'reasoning')
93
97
  const isTypeToolCall = computed(() => props.type === 'data-tool-call')
@@ -6,14 +6,14 @@
6
6
  "
7
7
  >
8
8
  <h3 :class="h3Style">{{ $t('Chat history') }}</h3>
9
- <Button @click="agentStore.createPreSession()" :disabled="agentStore.isResponseInProgress" class="w-[360px] mx-4 my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200">
9
+ <Button @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false)" :disabled="agentStore.isResponseInProgress" class="w-[360px] mx-4 my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200">
10
10
  <IconPlusOutline class="w-5 h-5" />
11
11
  {{ $t('New chat') }}
12
12
  </Button>
13
13
  <div class="w-full border-b border-gray-200 dark:border-gray-700"/>
14
14
  <div class="absolute w-full h-full flex flex-col items-center justify-center bg-gray-100/50 dark:bg-gray-700/50 z-10" v-if="agentStore.isResponseInProgress">
15
15
  <Spinner class="w-8 h-8" v-if="agentStore.isResponseInProgress" />
16
- <p class="mt-2 text-gray-800 dark:text-gray-200">generation in progress...</p>
16
+ <p class="mt-2 text-gray-800 dark:text-gray-200">{{ $t('Generation in progress...') }}</p>
17
17
  </div>
18
18
  <div v-for="group in groupedSessions" :key="group.dayKey" class="w-full py-2">
19
19
  <div class="px-4 pb-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">
@@ -38,7 +38,7 @@
38
38
  v-if="!groupedSessions || groupedSessions.length === 0"
39
39
  class="w-full h-full flex items-center justify-center text-gray-800 dark:text-gray-200"
40
40
  >
41
- There is no previous chat sessions
41
+ {{ $t('There are no previous chat sessions') }}
42
42
  </p>
43
43
  </div>
44
44
  </template>
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div v-if="props.data?.toolInfo" class="inline-flex m-2 max-w-[80%] flex-col gap-3 rounded-xl p-2 text-lightListTableHeadingText dark:text-darkListTableHeadingText">
2
+ <div
3
+ class="inline-flex m-2 max-w-[80%] flex-col gap-3 rounded-xl p-2 cursor-pointer text-lightListTableHeadingText dark:text-darkListTableHeadingText hover:opacity-75"
4
+ @click="isInputOutputExpanded = !isInputOutputExpanded"
5
+ >
3
6
  <div class="flex items-center gap-3">
4
7
  <div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white/70 dark:bg-blue-700/20">
5
8
  <Spinner v-if="isRunning" class="h-4 w-4" />
@@ -18,8 +21,7 @@
18
21
  <IconAngleDownOutline
19
22
  v-if="hasToolSections"
20
23
  :class="isInputOutputExpanded ? 'rotate-180' : 'rotate-0'"
21
- class="cursor-pointer transition-transform duration-200 hover:scale-105 hover:opacity-75"
22
- @click="isInputOutputExpanded = !isInputOutputExpanded"
24
+ class="cursor-pointer transition-transform duration-200 hover:scale-105"
23
25
  />
24
26
  </div>
25
27
  <transition name="expand">
@@ -1,8 +1,9 @@
1
1
  <template>
2
2
  <template v-for="group in props.toolGroup" :key="group.title">
3
- <div v-if="group.groupedTools.length > 1" class="mb-4 flex flex-col">
4
- <div class="flex items-center gap-2 p-2 m-2 cursor-pointer hover:opacity-75 break-all font-mono text-sm leading-5" @click="toggleGroup(group.title)">
5
- - {{ group.title }} {{ 'x' + group.groupedTools.length }}
3
+ <div v-if="group.groupedTools.length > 1" class="flex flex-col">
4
+ <div class="flex items-center gap-2 p-2 m-2 cursor-pointer hover:opacity-75 break-all font-mono text-sm leading-5 text-lightListTableHeadingText dark:text-darkListTableHeadingText" @click="toggleGroup(group.title)">
5
+ <IconMinusOutline class="w-6 h-6 p-1"/>
6
+ {{ group.title }} {{ 'x' + group.groupedTools.length }}
6
7
  <IconAngleDownOutline
7
8
  class="transition-transform duration-200 hover:scale-105 hover:opacity-75"
8
9
  :class="expandedGroups.includes(group.title) ? 'rotate-180' : 'rotate-0'"
@@ -10,7 +11,7 @@
10
11
  </div>
11
12
  <transition name="expand">
12
13
  <div v-show="expandedGroups.includes(group.title)" class="flex flex-col">
13
- <ToolRenderer v-for="part in group.groupedTools" :key="part.text + part.type" :data="part" />
14
+ <ToolRenderer v-for="part in group.groupedTools" :key="part.text + part.type" :data="part" class="ml-8"/>
14
15
  </div>
15
16
  </transition>
16
17
  </div>
@@ -24,7 +25,7 @@ import { Tool } from 'langchain';
24
25
  import ToolRenderer from './ToolRenderer.vue';
25
26
  import type { IPart } from './types';
26
27
  import { ref } from 'vue';
27
- import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
28
+ import { IconAngleDownOutline, IconMinusOutline } from '@iconify-prerendered/vue-flowbite';
28
29
 
29
30
  const props = defineProps<{
30
31
  toolGroup: {
@@ -18,7 +18,13 @@
18
18
 
19
19
  <div class="incremark-shiki-body">
20
20
  <div
21
- v-if="renderedHtml"
21
+ v-if="shouldRenderVega && !renderedHtml"
22
+ ref="vegaContainer"
23
+ class="incremark-vega"
24
+ />
25
+
26
+ <div
27
+ v-else-if="renderedHtml"
22
28
  class="incremark-shiki-html"
23
29
  v-html="renderedHtml"
24
30
  />
@@ -31,6 +37,7 @@
31
37
  <script setup lang="ts">
32
38
  import type { Code } from 'mdast';
33
39
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
40
+ import embed from 'vega-embed';
34
41
 
35
42
  import { highlightCodeSnippetHtml, type IncremarkCodeTheme } from './incremarkCodeHighlight';
36
43
 
@@ -53,15 +60,18 @@ const props = withDefaults(defineProps<{
53
60
  const renderedHtml = ref('');
54
61
  const copied = ref(false);
55
62
  const prefersDarkMode = ref(isDarkDocument());
63
+ const vegaContainer = ref<HTMLDivElement | null>(null);
56
64
 
57
65
  let copyResetTimeout: number | null = null;
58
66
  let renderRequestId = 0;
59
67
  let scheduledFrameId: number | null = null;
60
68
  let themeObserver: MutationObserver | null = null;
69
+ let vegaResult: { finalize: () => void } | null = null;
61
70
 
62
71
  const sourceCode = computed(() => props.node.value ?? '');
63
72
  const language = computed(() => props.node.lang?.trim().toLowerCase() || 'text');
64
- const languageLabel = computed(() => props.node.lang?.trim() || 'text');
73
+ const languageLabel = computed(() => language.value === 'vega-lite' ? '' : props.node.lang?.trim() || 'text');
74
+ const shouldRenderVega = computed(() => language.value === 'vega-lite' && props.blockStatus === 'completed');
65
75
  const codeTheme = computed<IncremarkCodeTheme>(() => {
66
76
  const requestedTheme = props.theme ?? (prefersDarkMode.value ? props.darkTheme : props.lightTheme);
67
77
 
@@ -77,7 +87,7 @@ const codeTheme = computed<IncremarkCodeTheme>(() => {
77
87
  });
78
88
 
79
89
  watch(
80
- [sourceCode, language, codeTheme, () => props.disableHighlight],
90
+ [sourceCode, language, codeTheme, () => props.disableHighlight, () => props.blockStatus],
81
91
  () => {
82
92
  scheduleHighlight();
83
93
  },
@@ -86,6 +96,7 @@ watch(
86
96
 
87
97
  onMounted(() => {
88
98
  if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') {
99
+ scheduleHighlight();
89
100
  return;
90
101
  }
91
102
 
@@ -97,10 +108,13 @@ onMounted(() => {
97
108
  attributes: true,
98
109
  attributeFilter: ['class'],
99
110
  });
111
+
112
+ scheduleHighlight();
100
113
  });
101
114
 
102
115
  onBeforeUnmount(() => {
103
116
  renderRequestId += 1;
117
+ clearVega();
104
118
 
105
119
  if (copyResetTimeout !== null) {
106
120
  window.clearTimeout(copyResetTimeout);
@@ -154,6 +168,45 @@ function scheduleHighlight() {
154
168
  async function renderHighlight() {
155
169
  const requestId = ++renderRequestId;
156
170
 
171
+ if (shouldRenderVega.value) {
172
+ renderedHtml.value = '';
173
+
174
+ if (!sourceCode.value || !vegaContainer.value) {
175
+ return;
176
+ }
177
+
178
+ try {
179
+ clearVega();
180
+ const spec = JSON.parse(sourceCode.value);
181
+
182
+ if (spec.width == null) {
183
+ spec.width = 'container';
184
+ }
185
+
186
+ if (spec.autosize == null) {
187
+ spec.autosize = { type: 'fit-x', contains: 'padding' };
188
+ }
189
+
190
+ const result = await embed(vegaContainer.value, spec, {
191
+ actions: false,
192
+ renderer: 'svg',
193
+ });
194
+
195
+ if (requestId !== renderRequestId) {
196
+ result.finalize();
197
+ return;
198
+ }
199
+
200
+ vegaResult = result;
201
+ return;
202
+ } catch (error) {
203
+ clearVega();
204
+ console.error('Failed to render Vega-Lite block', error);
205
+ }
206
+ } else {
207
+ clearVega();
208
+ }
209
+
157
210
  if (!sourceCode.value || props.disableHighlight) {
158
211
  renderedHtml.value = '';
159
212
  return;
@@ -177,6 +230,15 @@ async function renderHighlight() {
177
230
  function isDarkDocument(): boolean {
178
231
  return typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
179
232
  }
233
+
234
+ function clearVega() {
235
+ vegaResult?.finalize();
236
+ vegaResult = null;
237
+
238
+ if (vegaContainer.value) {
239
+ vegaContainer.value.innerHTML = '';
240
+ }
241
+ }
180
242
  </script>
181
243
 
182
244
  <style scoped>
@@ -265,6 +327,11 @@ function isDarkDocument(): boolean {
265
327
  overflow-x: auto;
266
328
  }
267
329
 
330
+ .incremark-vega {
331
+ padding: 18px;
332
+ width: 100%;
333
+ }
334
+
268
335
  .incremark-shiki-fallback {
269
336
  margin: 0;
270
337
  padding: 18px;
@@ -298,4 +365,12 @@ function isDarkDocument(): boolean {
298
365
  :deep(.incremark-shiki-html .line) {
299
366
  min-height: 1.65em;
300
367
  }
368
+
369
+ :deep(.incremark-vega .vega-embed) {
370
+ width: 100%;
371
+ }
372
+
373
+ :deep(.incremark-vega){
374
+ padding: 0;
375
+ }
301
376
  </style>
@@ -21,6 +21,7 @@
21
21
  "ai": "^6.0.158",
22
22
  "dompurify": "^3.3.3",
23
23
  "katex": "^0.16.45",
24
- "marked": "^18.0.0"
24
+ "marked": "^18.0.0",
25
+ "vega-embed": "^7.1.0"
25
26
  }
26
27
  }