fixup! 🎉(conversations) bootstrap backend & frontend

This commit is contained in:
Quentin BEY
2025-07-07 18:01:04 +02:00
parent 02477e9e8d
commit 9606370e88
11 changed files with 551 additions and 29 deletions
+5 -1
View File
@@ -141,10 +141,14 @@ run-backend: ## Start only the backend application and all needed services
@$(COMPOSE) up --force-recreate -d nginx ml-flow app-dev
.PHONY: run-backend
run-frontend: ## Start only the frontend application
@$(COMPOSE) up --force-recreate -d frontend-development
.PHONY: run-frontend
run: ## start the wsgi (production) and development server
run:
@$(MAKE) run-backend
@$(COMPOSE) up --force-recreate -d frontend-development
@$(MAKE) run-frontend
.PHONY: run
run-e2e: ## start the e2e server
@@ -80,9 +80,10 @@ class AIAgentService:
self.conversation = conversation
@staticmethod
def _convert_to_openai_messages( # noqa: PLR0912
def _convert_to_openai_messages(
messages: List[UIMessage],
) -> List[ResponseInputItemParam]:
"""Convert UI messages to OpenAI format."""
openai_messages = []
for message in messages:
@@ -92,7 +93,12 @@ class AIAgentService:
for part in message.parts:
match part.type:
case "text":
content_parts.append({"type": "input_text", "text": part.text})
content_parts.append(
{
"type": "input_text" if message.role == "user" else "output_text",
"text": part.text,
}
)
case "image":
content_parts.append(
@@ -146,13 +152,14 @@ class AIAgentService:
}
)
# Add tool calls separately
for tool_call in tool_calls:
openai_messages.append(tool_call)
# Add message with content parts if there are any
if content_parts:
openai_messages.append({"role": message.role, "content": content_parts})
openai_messages.append(
{"role": message.role, "content": content_parts, "type": "message"}
)
# Add tool calls separately, will be merged by `items_to_messages`
openai_messages += tool_calls
return openai_messages
@@ -219,7 +226,7 @@ class AIAgentService:
name=settings.AI_AGENT_NAME,
instructions=settings.AI_AGENT_INSTRUCTIONS,
model=self.model,
#tools=[agent_get_current_weather],
tools=[agent_get_current_weather],
mcp_servers=initialized_mcp_servers,
)
result = Runner.run_streamed(
View File
@@ -0,0 +1,316 @@
"""Tests for OpenAI client message conversion."""
import pytest
from chat.ai_sdk_types import (
Attachment,
TextUIPart,
ToolInvocationResult,
ToolInvocationUIPart,
UIMessage,
)
from chat.clients.openai import AIAgentService
@pytest.fixture(autouse=True)
def openai_settings(settings):
"""Create a mock AIAgentService for testing."""
settings.AI_BASE_URL = "http://test.url"
settings.AI_API_KEY = "test_key"
settings.AI_MODEL = "test_model"
def test_convert_simple_text_messages():
"""Test conversion of simple text messages"""
messages = [
UIMessage(
id="user1",
content="Hello, how are you?",
role="user",
parts=[TextUIPart(type="text", text="Hello, how are you?")],
),
UIMessage(
id="assistant1",
content="I'm doing well, thank you!",
role="assistant",
parts=[TextUIPart(type="text", text="I'm doing well, thank you!")],
),
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [{"type": "input_text", "text": "Hello, how are you?"}],
},
{
"role": "assistant",
"type": "message",
"content": [{"type": "output_text", "text": "I'm doing well, thank you!"}],
},
]
def test_convert_messages_with_tool_invocations():
"""Test conversion of messages with tool invocations"""
messages = [
UIMessage(
id="user1",
content="What's the weather in Paris?",
role="user",
parts=[TextUIPart(type="text", text="What's the weather in Paris?")],
),
UIMessage(
id="assistant1",
content="The current weather in Paris is 22°C.",
role="assistant",
toolInvocations=[
ToolInvocationResult(
toolCallId="tool1",
toolName="get_current_weather",
args={"location": "Paris", "unit": "celsius"},
result="{'location': 'Paris', 'temperature': 22, 'unit': 'celsius'}",
state="result",
step=0,
)
],
parts=[
ToolInvocationUIPart(
type="tool-invocation",
toolInvocation=ToolInvocationResult(
toolCallId="tool1",
toolName="get_current_weather",
args={"location": "Paris", "unit": "celsius"},
result="{'location': 'Paris', 'temperature': 22, 'unit': 'celsius'}",
state="result",
step=0,
),
),
TextUIPart(type="text", text="The current weather in Paris is 22°C."),
],
),
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [{"type": "input_text", "text": "What's the weather in Paris?"}],
},
{
"role": "assistant",
"type": "message",
"content": [{"type": "output_text", "text": "The current weather in Paris is 22°C."}],
},
{
"type": "function_call",
"call_id": "tool1",
"name": "get_current_weather",
"arguments": '{"location": "Paris", "unit": "celsius"}',
"status": "result",
},
]
def test_convert_messages_with_images():
"""Test conversion of messages with images"""
messages = [
UIMessage(
id="user1",
content="What is in this image?",
role="user",
parts=[
TextUIPart(type="text", text="What is in this image?"),
],
experimental_attachments=[
Attachment(
contentType="image/jpeg",
url="data:image/jpeg;base64,/9j/4AAQSkZJRg==",
name="image.jpg",
)
],
)
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [
{"type": "input_text", "text": "What is in this image?"},
{"type": "input_image", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRg=="},
],
}
]
def test_convert_messages_with_files():
"""Test conversion of messages with files"""
messages = [
UIMessage(
id="user1",
content="Here is the file",
role="user",
parts=[
TextUIPart(type="text", text="Here is the file"),
],
experimental_attachments=[
Attachment(
contentType="text/plain",
url="data:text/plain;base64,SGVsbG8gV29ybGQ=",
name="example.txt",
)
],
)
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [
{"type": "input_text", "text": "Here is the file"},
{
"type": "input_file",
"file_data": "data:text/plain;base64,SGVsbG8gV29ybGQ=",
"filename": "example.txt",
},
],
}
]
def test_convert_messages_with_experimental_attachments():
"""Test conversion of messages with experimental attachments"""
messages = [
UIMessage(
id="user1",
content="Check these attachments",
role="user",
parts=[TextUIPart(type="text", text="Check these attachments")],
experimental_attachments=[
Attachment(
contentType="image/jpeg",
url="data:image/jpeg;base64,/9j/4AAQSkZJRg==",
name="image.jpg",
),
Attachment(
contentType="text/plain",
url="data:text/plain;base64,SGVsbG8gV29ybGQ=",
name="example.txt",
),
],
)
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [
{"type": "input_text", "text": "Check these attachments"},
{"type": "input_image", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRg=="},
{
"type": "input_file",
"file_data": "data:text/plain;base64,SGVsbG8gV29ybGQ=",
"filename": "example.txt",
},
],
}
]
def test_convert_complex_message_combination():
"""Test conversion of complex combination of message types"""
messages = [
UIMessage(
id="user1",
content="What is this image and what's the weather in Paris?",
role="user",
parts=[
TextUIPart(type="text", text="What is this image and what's the weather in Paris?"),
],
experimental_attachments=[
Attachment(
contentType="image/jpeg",
url="data:image/jpeg;base64,/9j/4AAQSkZJRg==",
name="eiffel_tower.jpg",
)
],
),
UIMessage(
id="assistant1",
content="This is the Eiffel Tower. The current weather in Paris is 22°C.",
role="assistant",
toolInvocations=[
ToolInvocationResult(
toolCallId="tool1",
toolName="get_current_weather",
args={"location": "Paris", "unit": "celsius"},
result="{'location': 'Paris', 'temperature': 22, 'unit': 'celsius'}",
state="result",
step=0,
)
],
parts=[
ToolInvocationUIPart(
type="tool-invocation",
toolInvocation=ToolInvocationResult(
toolCallId="tool1",
toolName="get_current_weather",
args={"location": "Paris", "unit": "celsius"},
result="{'location': 'Paris', 'temperature': 22, 'unit': 'celsius'}",
state="result",
step=0,
),
),
TextUIPart(
type="text",
text="This is the Eiffel Tower. The current weather in Paris is 22°C.",
),
],
),
]
result = AIAgentService._convert_to_openai_messages(messages)
assert result == [
{
"role": "user",
"type": "message",
"content": [
{
"type": "input_text",
"text": "What is this image and what's the weather in Paris?",
},
{"type": "input_image", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRg=="},
],
},
{
"role": "assistant",
"type": "message",
"content": [
{
"type": "output_text",
"text": "This is the Eiffel Tower. The current weather in Paris is 22°C.",
}
],
},
{
"type": "function_call",
"call_id": "tool1",
"name": "get_current_weather",
"arguments": '{"location": "Paris", "unit": "celsius"}',
"status": "result",
},
]
+1 -1
View File
@@ -13,7 +13,7 @@ from core.filters import remove_accents
from chat import models, serializers
from chat.ai_sdk_types import UIMessage
from chat.clients.agent import AIAgentService
from chat.clients.openai import AIAgentService
logger = logging.getLogger(__name__)
+6 -1
View File
@@ -467,7 +467,12 @@ class Base(Configuration):
environ_prefix=None,
)
AI_AGENT_INSTRUCTIONS = values.Value(
"You are a helpful assistant",
(
"You are a helpful assistant. "
"Escape formulas or any math notation between `$$`, "
"like `$$x^2 + y^2 = z^2$$` or `$$C_l$$`. "
"You can use Markdown to format your answers. "
),
environ_name="AI_AGENT_INSTRUCTIONS",
environ_prefix=None,
)
@@ -46,7 +46,9 @@
"react-intersection-observer": "9.16.0",
"react-markdown": "10.1.0",
"react-select": "5.10.1",
"rehype-katex": "7.0.1",
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"styled-components": "6.1.18",
"use-debounce": "10.0.4",
"zod": "^3.25.67",
@@ -76,3 +76,8 @@
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}
html,
body {
overflow: hidden;
}
@@ -1,11 +1,14 @@
import { Message } from '@ai-sdk/ui-utils';
import { Message, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
import { Loader } from '@openfun/cunningham-react';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Markdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { Box, BoxButton, Icon, Text } from '@/components';
import { DropdownMenu } from '@/components/DropdownMenu';
@@ -213,30 +216,35 @@ export const Chat = ({
<Box $direction="column" $gap="2">
{message.content && (
<Markdown
remarkPlugins={[remarkGfm]}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => <Text {...props} />,
p: ({ node, ...props }) => (
<Text $display="inline" {...props} />
),
}}
>
{message.content}
</Markdown>
)}
<Box $direction="row" $gap="2">
{message.toolInvocations?.map((toolInvocation) => (
<Box
as="pre"
key={toolInvocation.toolCallId}
$background="var(--c--theme--colors--greyscale-100)"
$color="var(--c--theme--colors--greyscale-500)"
$padding={{ all: 'sm' }}
$radius="md"
$css="font-family: monospace; font-size: 0.9em;"
>
{`${toolInvocation.toolName}(${JSON.stringify(toolInvocation.args, null, 2)})`}
</Box>
))}
{message.parts
?.filter((part) => part.type === 'tool-invocation')
.map((part: ToolInvocationUIPart) => (
<Box
as="pre"
key={part.toolInvocation.toolCallId}
$background="var(--c--theme--colors--greyscale-100)"
$color="var(--c--theme--colors--greyscale-500)"
$padding={{ all: 'sm' }}
$radius="md"
$css="font-family: monospace; font-size: 0.9em;"
>
{`${part.toolInvocation.toolName}(${JSON.stringify(part.toolInvocation.args, null, 2)})`}
</Box>
))}
{/* Show attachments if present */}
{message.experimental_attachments?.map(
(attachment: Attachment, index: number) =>
@@ -276,7 +284,7 @@ export const Chat = ({
$direction="row"
$align="center"
$justify="center"
$gap="2"
$gap="1rem"
$margin={{ top: 'base' }}
>
<Loader size="small" />
+176 -1
View File
@@ -5509,6 +5509,11 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/katex@^0.16.0":
version "0.16.7"
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868"
integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==
"@types/[email protected]":
version "4.17.17"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz"
@@ -6888,6 +6893,11 @@ commander@^7.2.0:
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
common-tags@^1.8.0:
version "1.8.2"
resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz"
@@ -8560,6 +8570,65 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hast-util-from-dom@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz#c3c92fbd8d4e1c1625edeb3a773952b9e4ad64a8"
integrity sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==
dependencies:
"@types/hast" "^3.0.0"
hastscript "^9.0.0"
web-namespaces "^2.0.0"
hast-util-from-html-isomorphic@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz#b31baee386a899a2472326a3c5692f29f86d1d3c"
integrity sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==
dependencies:
"@types/hast" "^3.0.0"
hast-util-from-dom "^5.0.0"
hast-util-from-html "^2.0.0"
unist-util-remove-position "^5.0.0"
hast-util-from-html@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz#485c74785358beb80c4ba6346299311ac4c49c82"
integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==
dependencies:
"@types/hast" "^3.0.0"
devlop "^1.1.0"
hast-util-from-parse5 "^8.0.0"
parse5 "^7.0.0"
vfile "^6.0.0"
vfile-message "^4.0.0"
hast-util-from-parse5@^8.0.0:
version "8.0.3"
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e"
integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
devlop "^1.0.0"
hastscript "^9.0.0"
property-information "^7.0.0"
vfile "^6.0.0"
vfile-location "^5.0.0"
web-namespaces "^2.0.0"
hast-util-is-element@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz#6e31a6532c217e5b533848c7e52c9d9369ca0932"
integrity sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==
dependencies:
"@types/hast" "^3.0.0"
hast-util-parse-selector@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
dependencies:
"@types/hast" "^3.0.0"
hast-util-to-jsx-runtime@^2.0.0:
version "2.3.6"
resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz"
@@ -8581,6 +8650,16 @@ hast-util-to-jsx-runtime@^2.0.0:
unist-util-position "^5.0.0"
vfile-message "^4.0.0"
hast-util-to-text@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz#57b676931e71bf9cb852453678495b3080bfae3e"
integrity sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
hast-util-is-element "^3.0.0"
unist-util-find-after "^5.0.0"
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz"
@@ -8588,6 +8667,17 @@ hast-util-whitespace@^3.0.0:
dependencies:
"@types/hast" "^3.0.0"
hastscript@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff"
integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==
dependencies:
"@types/hast" "^3.0.0"
comma-separated-tokens "^2.0.0"
hast-util-parse-selector "^4.0.0"
property-information "^7.0.0"
space-separated-tokens "^2.0.0"
heimdalljs-logger@^0.1.10, heimdalljs-logger@^0.1.7:
version "0.1.10"
resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7"
@@ -9761,6 +9851,13 @@ jsonpointer@^5.0.0:
object.assign "^4.1.4"
object.values "^1.1.6"
katex@^0.16.0:
version "0.16.22"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.22.tgz#d2b3d66464b1e6d69e6463b28a86ced5a02c5ccd"
integrity sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==
dependencies:
commander "^8.3.0"
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -10089,6 +10186,19 @@ mdast-util-gfm@^3.0.0:
mdast-util-gfm-task-list-item "^2.0.0"
mdast-util-to-markdown "^2.0.0"
mdast-util-math@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-math/-/mdast-util-math-3.0.0.tgz#8d79dd3baf8ab8ac781f62b8853768190b9a00b0"
integrity sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
longest-streak "^3.0.0"
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.1.0"
unist-util-remove-position "^5.0.0"
mdast-util-mdx-expression@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz"
@@ -10154,7 +10264,7 @@ mdast-util-to-hast@^13.0.0:
unist-util-visit "^5.0.0"
vfile "^6.0.0"
mdast-util-to-markdown@^2.0.0:
mdast-util-to-markdown@^2.0.0, mdast-util-to-markdown@^2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz"
integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==
@@ -10322,6 +10432,19 @@ micromark-extension-gfm@^3.0.0:
micromark-util-combine-extensions "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-math@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz#c42ee3b1dd5a9a03584e83dd8f08e3de510212c1"
integrity sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==
dependencies:
"@types/katex" "^0.16.0"
devlop "^1.0.0"
katex "^0.16.0"
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-destination@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz"
@@ -11872,6 +11995,19 @@ regjsparser@^0.12.0:
dependencies:
jsesc "~3.0.2"
[email protected]:
version "7.0.1"
resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-7.0.1.tgz#832e6d7af2744a228981d1b0fe89483a9e7c93a1"
integrity sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==
dependencies:
"@types/hast" "^3.0.0"
"@types/katex" "^0.16.0"
hast-util-from-html-isomorphic "^2.0.0"
hast-util-to-text "^4.0.0"
katex "^0.16.0"
unist-util-visit-parents "^6.0.0"
vfile "^6.0.0"
[email protected]:
version "4.0.1"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b"
@@ -11884,6 +12020,16 @@ [email protected]:
remark-stringify "^11.0.0"
unified "^11.0.0"
[email protected]:
version "6.0.0"
resolved "https://registry.yarnpkg.com/remark-math/-/remark-math-6.0.0.tgz#0acdf74675f1c195fea6efffa78582f7ed7fc0d7"
integrity sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-math "^3.0.0"
micromark-extension-math "^3.0.0"
unified "^11.0.0"
remark-parse@^11.0.0:
version "11.0.0"
resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz"
@@ -13212,6 +13358,14 @@ unique-string@^2.0.0:
dependencies:
crypto-random-string "^2.0.0"
unist-util-find-after@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz#3fccc1b086b56f34c8b798e1ff90b5c54468e896"
integrity sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz"
@@ -13226,6 +13380,14 @@ unist-util-position@^5.0.0:
dependencies:
"@types/unist" "^3.0.0"
unist-util-remove-position@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163"
integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==
dependencies:
"@types/unist" "^3.0.0"
unist-util-visit "^5.0.0"
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz"
@@ -13387,6 +13549,14 @@ value-or-function@^4.0.0:
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-4.0.0.tgz#70836b6a876a010dc3a2b884e7902e9db064378d"
integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==
vfile-location@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
dependencies:
"@types/unist" "^3.0.0"
vfile "^6.0.0"
vfile-message@^4.0.0:
version "4.0.2"
resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz"
@@ -13506,6 +13676,11 @@ watchpack@^2.4.1:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
web-namespaces@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
web-vitals@^4.2.4:
version "4.2.4"
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz"