mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-12 02:12:42 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe76f00a6 | ||
|
|
9af9148ca7 |
@@ -818,7 +818,10 @@ def translate_history_to_llm_format(
|
||||
)
|
||||
]
|
||||
|
||||
# Add image parts
|
||||
# Add image parts. Each image is preceded by a text tag
|
||||
# carrying its file_id so the LLM can reference the image by
|
||||
# ID when calling tools like generate_image (which expects
|
||||
# reference_image_file_ids to edit a specific image).
|
||||
for img_file in msg.image_files:
|
||||
if img_file.file_type == ChatFileType.IMAGE:
|
||||
try:
|
||||
@@ -826,6 +829,12 @@ def translate_history_to_llm_format(
|
||||
base64_data = img_file.to_base64()
|
||||
image_url = f"data:{image_type};base64,{base64_data}"
|
||||
|
||||
content_parts.append(
|
||||
TextContentPart(
|
||||
type="text",
|
||||
text=f"[attached image — file_id: {img_file.file_id}]",
|
||||
)
|
||||
)
|
||||
image_part = ImageContentPart(
|
||||
type="image_url",
|
||||
image_url=ImageUrlDetail(
|
||||
|
||||
@@ -64,9 +64,20 @@ IMPORTANT: each call to this tool is independent. Variables from previous calls
|
||||
|
||||
GENERATE_IMAGE_GUIDANCE = """
|
||||
## generate_image
|
||||
NEVER use generate_image unless the user specifically requests an image.
|
||||
For edits/variations of a previously generated image, pass `reference_image_file_ids` with
|
||||
the `file_id` values returned by earlier `generate_image` tool results.
|
||||
NEVER use generate_image unless the user specifically requests an image or asks to
|
||||
edit/modify an existing image in the conversation.
|
||||
To edit, modify, restyle, or create a variation of an image already in the
|
||||
conversation, put that image's file_id in `reference_image_file_ids`. File IDs come
|
||||
from two places, and both can be passed the same way:
|
||||
- Images the user attached to a message carry a `[attached image — file_id: <id>]`
|
||||
tag immediately before the image content. Copy the id out of that tag.
|
||||
- Images produced by previous `generate_image` calls have their file_id in that
|
||||
call's tool response JSON.
|
||||
Only pass file_ids that actually appear in the conversation — never invent or guess
|
||||
one. Leave `reference_image_file_ids` unset for a brand-new generation that doesn't
|
||||
edit any existing image (for example when the user attached an image for context but
|
||||
asked for a completely unrelated new picture). The first file_id in the list is the
|
||||
primary edit source; any later file_ids are additional reference context.
|
||||
""".lstrip()
|
||||
|
||||
MEMORY_GUIDANCE = """
|
||||
|
||||
@@ -208,12 +208,6 @@ class PythonToolOverrideKwargs(BaseModel):
|
||||
chat_files: list[ChatFile] = []
|
||||
|
||||
|
||||
class ImageGenerationToolOverrideKwargs(BaseModel):
|
||||
"""Override kwargs for image generation tool calls."""
|
||||
|
||||
recent_generated_image_file_ids: list[str] = []
|
||||
|
||||
|
||||
class SearchToolRunContext(BaseModel):
|
||||
emitter: Emitter
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ from onyx.server.query_and_chat.streaming_models import ImageGenerationToolHeart
|
||||
from onyx.server.query_and_chat.streaming_models import ImageGenerationToolStart
|
||||
from onyx.server.query_and_chat.streaming_models import Packet
|
||||
from onyx.tools.interface import Tool
|
||||
from onyx.tools.models import ImageGenerationToolOverrideKwargs
|
||||
from onyx.tools.models import ToolCallException
|
||||
from onyx.tools.models import ToolExecutionException
|
||||
from onyx.tools.models import ToolResponse
|
||||
@@ -48,9 +47,16 @@ PROMPT_FIELD = "prompt"
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD = "reference_image_file_ids"
|
||||
|
||||
|
||||
class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
class ImageGenerationTool(Tool[None]):
|
||||
NAME = "generate_image"
|
||||
DESCRIPTION = "Generate an image based on a prompt. Do not use unless the user specifically requests an image."
|
||||
DESCRIPTION = (
|
||||
"Generate a new image from a prompt, or edit/modify existing images"
|
||||
" from this conversation. To edit existing images — whether the user"
|
||||
" attached them or they were produced by a previous generate_image"
|
||||
" call — pass their file_id values in `reference_image_file_ids`."
|
||||
" Do not use unless the user specifically requests an image or asks"
|
||||
" to edit an image."
|
||||
)
|
||||
DISPLAY_NAME = "Image Generation"
|
||||
|
||||
def __init__(
|
||||
@@ -142,8 +148,14 @@ class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD: {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"Optional image file IDs to use as reference context for edits/variations. "
|
||||
"Use the file_id values returned by previous generate_image calls."
|
||||
"Optional list of image file_id values to edit/modify/use as reference."
|
||||
" Accepts file_ids from two sources, with the same mechanics for both:"
|
||||
" (1) images the user attached to a user message — their file_id appears"
|
||||
" in the tag `[attached image — file_id: <id>]` right before the image"
|
||||
" in that message; (2) images returned by previous generate_image tool"
|
||||
" calls — their file_id appears in that call's response JSON. Leave"
|
||||
" unset/empty for a brand-new generation unrelated to any existing image."
|
||||
" The first file_id in the list is treated as the primary edit source."
|
||||
),
|
||||
"items": {
|
||||
"type": "string",
|
||||
@@ -254,41 +266,31 @@ class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
def _resolve_reference_image_file_ids(
|
||||
self,
|
||||
llm_kwargs: dict[str, Any],
|
||||
override_kwargs: ImageGenerationToolOverrideKwargs | None,
|
||||
) -> list[str]:
|
||||
raw_reference_ids = llm_kwargs.get(REFERENCE_IMAGE_FILE_IDS_FIELD)
|
||||
if raw_reference_ids is not None:
|
||||
if not isinstance(raw_reference_ids, list) or not all(
|
||||
isinstance(file_id, str) for file_id in raw_reference_ids
|
||||
):
|
||||
raise ToolCallException(
|
||||
message=(
|
||||
f"Invalid {REFERENCE_IMAGE_FILE_IDS_FIELD}: expected array of strings, got {type(raw_reference_ids)}"
|
||||
),
|
||||
llm_facing_message=(
|
||||
f"The '{REFERENCE_IMAGE_FILE_IDS_FIELD}' field must be an array of file_id strings."
|
||||
),
|
||||
)
|
||||
reference_image_file_ids = [
|
||||
file_id.strip() for file_id in raw_reference_ids if file_id.strip()
|
||||
]
|
||||
elif (
|
||||
override_kwargs
|
||||
and override_kwargs.recent_generated_image_file_ids
|
||||
and self.img_provider.supports_reference_images
|
||||
):
|
||||
# If no explicit reference was provided, default to the most recently generated image.
|
||||
reference_image_file_ids = [
|
||||
override_kwargs.recent_generated_image_file_ids[-1]
|
||||
]
|
||||
else:
|
||||
reference_image_file_ids = []
|
||||
if raw_reference_ids is None:
|
||||
# No references requested — plain generation.
|
||||
return []
|
||||
|
||||
# Deduplicate while preserving order.
|
||||
if not isinstance(raw_reference_ids, list) or not all(
|
||||
isinstance(file_id, str) for file_id in raw_reference_ids
|
||||
):
|
||||
raise ToolCallException(
|
||||
message=(
|
||||
f"Invalid {REFERENCE_IMAGE_FILE_IDS_FIELD}: expected array of strings, got {type(raw_reference_ids)}"
|
||||
),
|
||||
llm_facing_message=(
|
||||
f"The '{REFERENCE_IMAGE_FILE_IDS_FIELD}' field must be an array of file_id strings."
|
||||
),
|
||||
)
|
||||
|
||||
# Deduplicate while preserving order (first occurrence wins, so the
|
||||
# LLM's intended "primary edit source" stays at index 0).
|
||||
deduped_reference_image_ids: list[str] = []
|
||||
seen_ids: set[str] = set()
|
||||
for file_id in reference_image_file_ids:
|
||||
if file_id in seen_ids:
|
||||
for file_id in raw_reference_ids:
|
||||
file_id = file_id.strip()
|
||||
if not file_id or file_id in seen_ids:
|
||||
continue
|
||||
seen_ids.add(file_id)
|
||||
deduped_reference_image_ids.append(file_id)
|
||||
@@ -302,14 +304,14 @@ class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
f"Reference images requested but provider '{self.provider}' does not support image-editing context."
|
||||
),
|
||||
llm_facing_message=(
|
||||
"This image provider does not support editing from previous image context. "
|
||||
"This image provider does not support editing from existing images. "
|
||||
"Try text-only generation, or switch to a provider/model that supports image edits."
|
||||
),
|
||||
)
|
||||
|
||||
max_reference_images = self.img_provider.max_reference_images
|
||||
if max_reference_images > 0:
|
||||
return deduped_reference_image_ids[-max_reference_images:]
|
||||
return deduped_reference_image_ids[:max_reference_images]
|
||||
return deduped_reference_image_ids
|
||||
|
||||
def _load_reference_images(
|
||||
@@ -358,7 +360,7 @@ class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
def run(
|
||||
self,
|
||||
placement: Placement,
|
||||
override_kwargs: ImageGenerationToolOverrideKwargs | None = None,
|
||||
override_kwargs: None = None, # noqa: ARG002
|
||||
**llm_kwargs: Any,
|
||||
) -> ToolResponse:
|
||||
if PROMPT_FIELD not in llm_kwargs:
|
||||
@@ -373,7 +375,6 @@ class ImageGenerationTool(Tool[ImageGenerationToolOverrideKwargs | None]):
|
||||
shape = ImageShape(llm_kwargs.get("shape", ImageShape.SQUARE.value))
|
||||
reference_image_file_ids = self._resolve_reference_image_file_ids(
|
||||
llm_kwargs=llm_kwargs,
|
||||
override_kwargs=override_kwargs,
|
||||
)
|
||||
reference_images = self._load_reference_images(reference_image_file_ids)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
@@ -14,7 +13,6 @@ from onyx.server.query_and_chat.streaming_models import SectionEnd
|
||||
from onyx.tools.interface import Tool
|
||||
from onyx.tools.models import ChatFile
|
||||
from onyx.tools.models import ChatMinimalTextMessage
|
||||
from onyx.tools.models import ImageGenerationToolOverrideKwargs
|
||||
from onyx.tools.models import OpenURLToolOverrideKwargs
|
||||
from onyx.tools.models import ParallelToolCallResponse
|
||||
from onyx.tools.models import PythonToolOverrideKwargs
|
||||
@@ -24,9 +22,6 @@ from onyx.tools.models import ToolCallKickoff
|
||||
from onyx.tools.models import ToolExecutionException
|
||||
from onyx.tools.models import ToolResponse
|
||||
from onyx.tools.models import WebSearchToolOverrideKwargs
|
||||
from onyx.tools.tool_implementations.images.image_generation_tool import (
|
||||
ImageGenerationTool,
|
||||
)
|
||||
from onyx.tools.tool_implementations.memory.memory_tool import MemoryTool
|
||||
from onyx.tools.tool_implementations.memory.memory_tool import MemoryToolOverrideKwargs
|
||||
from onyx.tools.tool_implementations.open_url.open_url_tool import OpenURLTool
|
||||
@@ -110,63 +105,6 @@ def _merge_tool_calls(tool_calls: list[ToolCallKickoff]) -> list[ToolCallKickoff
|
||||
return merged_calls
|
||||
|
||||
|
||||
def _extract_image_file_ids_from_tool_response_message(
|
||||
message: str,
|
||||
) -> list[str]:
|
||||
try:
|
||||
parsed_message = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
parsed_items: list[Any] = (
|
||||
parsed_message if isinstance(parsed_message, list) else [parsed_message]
|
||||
)
|
||||
file_ids: list[str] = []
|
||||
for item in parsed_items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
file_id = item.get("file_id")
|
||||
if isinstance(file_id, str):
|
||||
file_ids.append(file_id)
|
||||
|
||||
return file_ids
|
||||
|
||||
|
||||
def _extract_recent_generated_image_file_ids(
|
||||
message_history: list[ChatMessageSimple],
|
||||
) -> list[str]:
|
||||
tool_name_by_tool_call_id: dict[str, str] = {}
|
||||
recent_image_file_ids: list[str] = []
|
||||
seen_file_ids: set[str] = set()
|
||||
|
||||
for message in message_history:
|
||||
if message.message_type == MessageType.ASSISTANT and message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
tool_name_by_tool_call_id[tool_call.tool_call_id] = tool_call.tool_name
|
||||
continue
|
||||
|
||||
if (
|
||||
message.message_type != MessageType.TOOL_CALL_RESPONSE
|
||||
or not message.tool_call_id
|
||||
):
|
||||
continue
|
||||
|
||||
tool_name = tool_name_by_tool_call_id.get(message.tool_call_id)
|
||||
if tool_name != ImageGenerationTool.NAME:
|
||||
continue
|
||||
|
||||
for file_id in _extract_image_file_ids_from_tool_response_message(
|
||||
message.message
|
||||
):
|
||||
if file_id in seen_file_ids:
|
||||
continue
|
||||
seen_file_ids.add(file_id)
|
||||
recent_image_file_ids.append(file_id)
|
||||
|
||||
return recent_image_file_ids
|
||||
|
||||
|
||||
def _safe_run_single_tool(
|
||||
tool: Tool,
|
||||
tool_call: ToolCallKickoff,
|
||||
@@ -386,9 +324,6 @@ def run_tool_calls(
|
||||
url_to_citation: dict[str, int] = {
|
||||
url: citation_num for citation_num, url in citation_mapping.items()
|
||||
}
|
||||
recent_generated_image_file_ids = _extract_recent_generated_image_file_ids(
|
||||
message_history
|
||||
)
|
||||
|
||||
# Prepare all tool calls with their override_kwargs
|
||||
# Each tool gets a unique starting citation number to avoid conflicts when running in parallel
|
||||
@@ -405,7 +340,6 @@ def run_tool_calls(
|
||||
| WebSearchToolOverrideKwargs
|
||||
| OpenURLToolOverrideKwargs
|
||||
| PythonToolOverrideKwargs
|
||||
| ImageGenerationToolOverrideKwargs
|
||||
| MemoryToolOverrideKwargs
|
||||
| None
|
||||
) = None
|
||||
@@ -454,10 +388,6 @@ def run_tool_calls(
|
||||
override_kwargs = PythonToolOverrideKwargs(
|
||||
chat_files=chat_files or [],
|
||||
)
|
||||
elif isinstance(tool, ImageGenerationTool):
|
||||
override_kwargs = ImageGenerationToolOverrideKwargs(
|
||||
recent_generated_image_file_ids=recent_generated_image_file_ids
|
||||
)
|
||||
elif isinstance(tool, MemoryTool):
|
||||
override_kwargs = MemoryToolOverrideKwargs(
|
||||
user_name=(
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Tests for ``ImageGenerationTool._resolve_reference_image_file_ids``.
|
||||
|
||||
The resolver turns the LLM's ``reference_image_file_ids`` argument into a
|
||||
cleaned list of file IDs to hand to ``_load_reference_images``. It trusts
|
||||
the LLM's picks — the LLM can only see file IDs that actually appear in
|
||||
the conversation (via ``[attached image — file_id: <id>]`` tags on user
|
||||
messages and the JSON returned by prior generate_image calls), so we
|
||||
don't re-validate against an allow-list in the tool itself.
|
||||
"""
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from onyx.tools.models import ToolCallException
|
||||
from onyx.tools.tool_implementations.images.image_generation_tool import (
|
||||
ImageGenerationTool,
|
||||
)
|
||||
from onyx.tools.tool_implementations.images.image_generation_tool import (
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD,
|
||||
)
|
||||
|
||||
|
||||
def _make_tool(
|
||||
supports_reference_images: bool = True,
|
||||
max_reference_images: int = 16,
|
||||
) -> ImageGenerationTool:
|
||||
"""Construct a tool with a mock provider so no credentials/network are needed."""
|
||||
with patch(
|
||||
"onyx.tools.tool_implementations.images.image_generation_tool.get_image_generation_provider"
|
||||
) as mock_get_provider:
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.supports_reference_images = supports_reference_images
|
||||
mock_provider.max_reference_images = max_reference_images
|
||||
mock_get_provider.return_value = mock_provider
|
||||
|
||||
return ImageGenerationTool(
|
||||
image_generation_credentials=MagicMock(),
|
||||
tool_id=1,
|
||||
emitter=MagicMock(),
|
||||
model="gpt-image-1",
|
||||
provider="openai",
|
||||
)
|
||||
|
||||
|
||||
class TestResolveReferenceImageFileIds:
|
||||
def test_unset_returns_empty_plain_generation(self) -> None:
|
||||
tool = _make_tool()
|
||||
assert tool._resolve_reference_image_file_ids(llm_kwargs={}) == []
|
||||
|
||||
def test_empty_list_is_treated_like_unset(self) -> None:
|
||||
tool = _make_tool()
|
||||
result = tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={REFERENCE_IMAGE_FILE_IDS_FIELD: []},
|
||||
)
|
||||
assert result == []
|
||||
|
||||
def test_passes_llm_supplied_ids_through(self) -> None:
|
||||
tool = _make_tool()
|
||||
result = tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={REFERENCE_IMAGE_FILE_IDS_FIELD: ["upload-1", "gen-1"]},
|
||||
)
|
||||
# Order preserved — first entry is the primary edit source.
|
||||
assert result == ["upload-1", "gen-1"]
|
||||
|
||||
def test_invalid_shape_raises(self) -> None:
|
||||
tool = _make_tool()
|
||||
with pytest.raises(ToolCallException):
|
||||
tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={REFERENCE_IMAGE_FILE_IDS_FIELD: "not-a-list"},
|
||||
)
|
||||
|
||||
def test_non_string_element_raises(self) -> None:
|
||||
tool = _make_tool()
|
||||
with pytest.raises(ToolCallException):
|
||||
tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={REFERENCE_IMAGE_FILE_IDS_FIELD: ["ok", 123]},
|
||||
)
|
||||
|
||||
def test_deduplicates_preserving_first_occurrence(self) -> None:
|
||||
tool = _make_tool()
|
||||
result = tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD: ["gen-1", "gen-2", "gen-1"]
|
||||
},
|
||||
)
|
||||
assert result == ["gen-1", "gen-2"]
|
||||
|
||||
def test_strips_whitespace_and_skips_empty_strings(self) -> None:
|
||||
tool = _make_tool()
|
||||
result = tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD: [" gen-1 ", "", " "]
|
||||
},
|
||||
)
|
||||
assert result == ["gen-1"]
|
||||
|
||||
def test_provider_without_reference_support_raises(self) -> None:
|
||||
tool = _make_tool(supports_reference_images=False)
|
||||
with pytest.raises(ToolCallException):
|
||||
tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={REFERENCE_IMAGE_FILE_IDS_FIELD: ["gen-1"]},
|
||||
)
|
||||
|
||||
def test_truncates_to_provider_max_preserving_head(self) -> None:
|
||||
"""When the LLM lists more images than the provider allows, keep the
|
||||
HEAD of the list (the primary edit source + earliest extras) rather
|
||||
than the tail, since the LLM put the most important one first."""
|
||||
tool = _make_tool(max_reference_images=2)
|
||||
result = tool._resolve_reference_image_file_ids(
|
||||
llm_kwargs={
|
||||
REFERENCE_IMAGE_FILE_IDS_FIELD: ["a", "b", "c", "d"]
|
||||
},
|
||||
)
|
||||
assert result == ["a", "b"]
|
||||
@@ -1,10 +1,5 @@
|
||||
from onyx.chat.models import ChatMessageSimple
|
||||
from onyx.chat.models import ToolCallSimple
|
||||
from onyx.configs.constants import MessageType
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
from onyx.tools.models import ToolCallKickoff
|
||||
from onyx.tools.tool_runner import _extract_image_file_ids_from_tool_response_message
|
||||
from onyx.tools.tool_runner import _extract_recent_generated_image_file_ids
|
||||
from onyx.tools.tool_runner import _merge_tool_calls
|
||||
|
||||
|
||||
@@ -313,61 +308,3 @@ class TestMergeToolCalls:
|
||||
# String should be converted to list item
|
||||
assert result[0].tool_args["queries"] == ["single_query", "q2"]
|
||||
|
||||
|
||||
class TestImageHistoryExtraction:
|
||||
def test_extracts_image_file_ids_from_json_response(self) -> None:
|
||||
msg = '[{"file_id":"img-1","revised_prompt":"v1"},{"file_id":"img-2","revised_prompt":"v2"}]'
|
||||
assert _extract_image_file_ids_from_tool_response_message(msg) == [
|
||||
"img-1",
|
||||
"img-2",
|
||||
]
|
||||
|
||||
def test_extracts_recent_generated_image_ids_from_history(self) -> None:
|
||||
history = [
|
||||
ChatMessageSimple(
|
||||
message="",
|
||||
token_count=1,
|
||||
message_type=MessageType.ASSISTANT,
|
||||
tool_calls=[
|
||||
ToolCallSimple(
|
||||
tool_call_id="call_1",
|
||||
tool_name="generate_image",
|
||||
tool_arguments={"prompt": "test"},
|
||||
token_count=1,
|
||||
)
|
||||
],
|
||||
),
|
||||
ChatMessageSimple(
|
||||
message='[{"file_id":"img-1","revised_prompt":"r1"}]',
|
||||
token_count=1,
|
||||
message_type=MessageType.TOOL_CALL_RESPONSE,
|
||||
tool_call_id="call_1",
|
||||
),
|
||||
]
|
||||
|
||||
assert _extract_recent_generated_image_file_ids(history) == ["img-1"]
|
||||
|
||||
def test_ignores_non_image_tool_responses(self) -> None:
|
||||
history = [
|
||||
ChatMessageSimple(
|
||||
message="",
|
||||
token_count=1,
|
||||
message_type=MessageType.ASSISTANT,
|
||||
tool_calls=[
|
||||
ToolCallSimple(
|
||||
tool_call_id="call_1",
|
||||
tool_name="web_search",
|
||||
tool_arguments={"queries": ["q"]},
|
||||
token_count=1,
|
||||
)
|
||||
],
|
||||
),
|
||||
ChatMessageSimple(
|
||||
message='[{"file_id":"img-1","revised_prompt":"r1"}]',
|
||||
token_count=1,
|
||||
message_type=MessageType.TOOL_CALL_RESPONSE,
|
||||
tool_call_id="call_1",
|
||||
),
|
||||
]
|
||||
|
||||
assert _extract_recent_generated_image_file_ids(history) == []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgCheck } from "@opal/icons";
|
||||
|
||||
export interface AlreadyPickedModalProps {
|
||||
@@ -17,7 +18,7 @@ export default function AlreadyPickedModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgCheck}
|
||||
title={`${model.model_name} already chosen`}
|
||||
title={markdown(`*${model.model_name}* already chosen`)}
|
||||
description="You can select a different one if you want!"
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getFormattedProviderName,
|
||||
} from "@/components/embedding/interfaces";
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { testEmbedding } from "@/app/admin/embeddings/pages/utils";
|
||||
@@ -172,9 +173,11 @@ export default function ChangeCredentialsModal({
|
||||
<Modal.Content>
|
||||
<Modal.Header
|
||||
icon={SvgSettings}
|
||||
title={`Modify your ${getFormattedProviderName(
|
||||
provider.provider_type
|
||||
)} ${isProxy ? "Configuration" : "key"}`}
|
||||
title={markdown(
|
||||
`Modify your *${getFormattedProviderName(
|
||||
provider.provider_type
|
||||
)}* ${isProxy ? "configuration" : "key"}`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getFormattedProviderName,
|
||||
} from "../../../../components/embedding/interfaces";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
export interface DeleteCredentialsModalProps {
|
||||
modelProvider: CloudEmbeddingProvider;
|
||||
@@ -24,9 +25,11 @@ export default function DeleteCredentialsModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgTrash}
|
||||
title={`Delete ${getFormattedProviderName(
|
||||
modelProvider.provider_type
|
||||
)} Credentials?`}
|
||||
title={markdown(
|
||||
`Delete *${getFormattedProviderName(
|
||||
modelProvider.provider_type
|
||||
)}* credentials?`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/embedding/interfaces";
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
export interface ProviderCreationModalProps {
|
||||
@@ -185,9 +186,11 @@ export default function ProviderCreationModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgSettings}
|
||||
title={`Configure ${getFormattedProviderName(
|
||||
selectedProvider.provider_type
|
||||
)}`}
|
||||
title={markdown(
|
||||
`Configure *${getFormattedProviderName(
|
||||
selectedProvider.provider_type
|
||||
)}*`
|
||||
)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -2,6 +2,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { CloudEmbeddingModel } from "@/components/embedding/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
|
||||
export interface SelectModelModalProps {
|
||||
@@ -20,7 +21,7 @@ export default function SelectModelModal({
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header
|
||||
icon={SvgServer}
|
||||
title={`Select ${model.model_name}`}
|
||||
title={markdown(`Select *${model.model_name}*`)}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
import EmbeddingModelSelection from "../EmbeddingModelSelectionForm";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
@@ -538,7 +539,9 @@ export default function EmbeddingForm() {
|
||||
<Modal.Content>
|
||||
<Modal.Header
|
||||
icon={SvgAlertTriangle}
|
||||
title={`Are you sure you want to select ${selectedProvider.model_name}?`}
|
||||
title={markdown(
|
||||
`Are you sure you want to select *${selectedProvider.model_name}*?`
|
||||
)}
|
||||
onClose={() => setShowPoorModel(false)}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -137,7 +137,7 @@ function DeleteConfirmModal({ hook, onDelete }: DeleteConfirmModalProps) {
|
||||
<Modal.Header
|
||||
// TODO(@raunakab): replace the colour of this SVG with red.
|
||||
icon={SvgTrash}
|
||||
title={`Delete ${hook.name}`}
|
||||
title={markdown(`Delete *${hook.name}*`)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Modal.Body>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Section, AttachmentItemLayout } from "@/layouts/general-layouts";
|
||||
import { Content, ContentAction } from "@opal/layouts";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
@@ -1556,7 +1557,7 @@ function FederatedConnectorCard({
|
||||
{showDisconnectConfirmation && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${sourceMetadata.displayName}`}
|
||||
title={markdown(`Disconnect *${sourceMetadata.displayName}*`)}
|
||||
onClose={() => setShowDisconnectConfirmation(false)}
|
||||
submit={
|
||||
<Button
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
// TODO(@raunakab): migrate to Opal LineItemButton once it supports danger variant
|
||||
import LineItem from "@/refresh-components/buttons/LineItem";
|
||||
import { cn } from "@opal/utils";
|
||||
import { cn, markdown } from "@opal/utils";
|
||||
import {
|
||||
SvgMoreHorizontal,
|
||||
SvgEdit,
|
||||
@@ -341,7 +341,7 @@ export default function AgentRowActions({
|
||||
{unlistOpen && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgEyeOff}
|
||||
title={`Unlist ${agent.name}`}
|
||||
title={markdown(`Unlist *${agent.name}*`)}
|
||||
onClose={isSubmitting ? undefined : () => setUnlistOpen(false)}
|
||||
submit={
|
||||
<Button
|
||||
|
||||
@@ -347,7 +347,7 @@ export default function ImageGenerationContent() {
|
||||
{disconnectProvider && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectProvider.title}`}
|
||||
title={markdown(`Disconnect *${disconnectProvider.title}*`)}
|
||||
description="This will remove the stored credentials for this provider."
|
||||
onClose={() => {
|
||||
setDisconnectProvider(null);
|
||||
|
||||
@@ -201,7 +201,7 @@ function VoiceDisconnectModal({
|
||||
return (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectTarget.providerLabel}`}
|
||||
title={markdown(`Disconnect *${disconnectTarget.providerLabel}*`)}
|
||||
description="Voice models"
|
||||
onClose={onClose}
|
||||
submit={
|
||||
|
||||
@@ -9,6 +9,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
|
||||
import { SvgArrowExchange } from "@opal/icons";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgOnyxLogo } from "@opal/logos";
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
@@ -81,7 +82,7 @@ export const WebProviderSetupModal = memo(
|
||||
<Modal.Content width="sm" preventAccidentalClose>
|
||||
<Modal.Header
|
||||
icon={LogoArrangement}
|
||||
title={`Set up ${providerLabel}`}
|
||||
title={markdown(`Set up *${providerLabel}*`)}
|
||||
description={description}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Text from "@/refresh-components/texts/Text";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { Content, Card } from "@opal/layouts";
|
||||
import { markdown } from "@opal/utils";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher, FetchError } from "@/lib/fetcher";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
@@ -146,7 +147,7 @@ function WebSearchDisconnectModal({
|
||||
return (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title={`Disconnect ${disconnectTarget.label}`}
|
||||
title={markdown(`Disconnect *${disconnectTarget.label}*`)}
|
||||
description="This will remove the stored credentials for this provider."
|
||||
onClose={onClose}
|
||||
submit={
|
||||
|
||||
@@ -5,6 +5,7 @@ import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { SvgUnplug } from "@opal/icons";
|
||||
interface DisconnectEntityModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -51,7 +52,7 @@ export default function DisconnectEntityModal({
|
||||
icon={({ className }) => (
|
||||
<SvgUnplug className={cn(className, "stroke-action-danger-05")} />
|
||||
)}
|
||||
title={`Disconnect ${name}`}
|
||||
title={markdown(`Disconnect *${name}*`)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import InputSelect from "@/refresh-components/inputs/InputSelect";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
|
||||
import { Button } from "@opal/components";
|
||||
import { markdown } from "@opal/utils";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Formik, Form } from "formik";
|
||||
@@ -317,7 +318,11 @@ export default function MCPAuthenticationModal({
|
||||
<Modal.Content width="sm" height="lg" skipOverlay={skipOverlay}>
|
||||
<Modal.Header
|
||||
icon={SvgArrowExchange}
|
||||
title={`Authenticate ${mcpServer?.name || "MCP Server"}`}
|
||||
title={
|
||||
mcpServer
|
||||
? markdown(`Authenticate *${mcpServer.name}*`)
|
||||
: "Authenticate MCP Server"
|
||||
}
|
||||
description="Authenticate your connection to start using the MCP server."
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { Formik, Form, useFormikContext } from "formik";
|
||||
import type { FormikConfig } from "formik";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { useAgents } from "@/hooks/useAgents";
|
||||
@@ -720,7 +721,7 @@ function ModalWrapperInner({
|
||||
} = getProvider(providerName);
|
||||
|
||||
const title = llmProvider
|
||||
? `Configure "${llmProvider.name}"`
|
||||
? markdown(`Configure *${llmProvider.name}*`)
|
||||
: `Set up ${providerProductName}`;
|
||||
const description =
|
||||
descriptionOverride ??
|
||||
|
||||
Reference in New Issue
Block a user