mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-27 20:55:45 +00:00
Compare commits
12 Commits
experiment
...
default_py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2036847a2d | ||
|
|
9ff1ac862e | ||
|
|
e4179629ae | ||
|
|
4077c20def | ||
|
|
4d256c5666 | ||
|
|
2e53496f46 | ||
|
|
63a206706a | ||
|
|
28427b3e5f | ||
|
|
3cafcd8a5e | ||
|
|
f2c50b7bb5 | ||
|
|
6b28c6bbfc | ||
|
|
226e801665 |
@@ -18,6 +18,14 @@ inputs:
|
||||
description: "Optional NIGHTLY_LLM_API_BASE"
|
||||
required: false
|
||||
default: ""
|
||||
api-version:
|
||||
description: "Optional NIGHTLY_LLM_API_VERSION"
|
||||
required: false
|
||||
default: ""
|
||||
deployment-name:
|
||||
description: "Optional NIGHTLY_LLM_DEPLOYMENT_NAME"
|
||||
required: false
|
||||
default: ""
|
||||
custom-config-json:
|
||||
description: "Optional NIGHTLY_LLM_CUSTOM_CONFIG_JSON"
|
||||
required: false
|
||||
@@ -84,6 +92,8 @@ runs:
|
||||
NIGHTLY_LLM_PROVIDER: ${{ inputs.provider }}
|
||||
NIGHTLY_LLM_API_KEY: ${{ inputs.provider-api-key }}
|
||||
NIGHTLY_LLM_API_BASE: ${{ inputs.api-base }}
|
||||
NIGHTLY_LLM_API_VERSION: ${{ inputs.api-version }}
|
||||
NIGHTLY_LLM_DEPLOYMENT_NAME: ${{ inputs.deployment-name }}
|
||||
NIGHTLY_LLM_CUSTOM_CONFIG_JSON: ${{ inputs.custom-config-json }}
|
||||
NIGHTLY_LLM_STRICT: ${{ inputs.strict }}
|
||||
RUNS_ON_ECR_CACHE: ${{ inputs.runs-on-ecr-cache }}
|
||||
@@ -112,6 +122,8 @@ runs:
|
||||
-e NIGHTLY_LLM_MODELS="${MODELS}" \
|
||||
-e NIGHTLY_LLM_API_KEY="${NIGHTLY_LLM_API_KEY}" \
|
||||
-e NIGHTLY_LLM_API_BASE="${NIGHTLY_LLM_API_BASE}" \
|
||||
-e NIGHTLY_LLM_API_VERSION="${NIGHTLY_LLM_API_VERSION}" \
|
||||
-e NIGHTLY_LLM_DEPLOYMENT_NAME="${NIGHTLY_LLM_DEPLOYMENT_NAME}" \
|
||||
-e NIGHTLY_LLM_CUSTOM_CONFIG_JSON="${NIGHTLY_LLM_CUSTOM_CONFIG_JSON}" \
|
||||
-e NIGHTLY_LLM_STRICT="${NIGHTLY_LLM_STRICT}" \
|
||||
${RUNS_ON_ECR_CACHE}:nightly-llm-it-${RUN_ID} \
|
||||
|
||||
@@ -20,12 +20,19 @@ jobs:
|
||||
anthropic_models: ${{ vars.NIGHTLY_LLM_ANTHROPIC_MODELS }}
|
||||
bedrock_models: ${{ vars.NIGHTLY_LLM_BEDROCK_MODELS }}
|
||||
vertex_ai_models: ${{ vars.NIGHTLY_LLM_VERTEX_AI_MODELS }}
|
||||
azure_models: ${{ vars.NIGHTLY_LLM_AZURE_MODELS }}
|
||||
azure_api_base: ${{ vars.NIGHTLY_LLM_AZURE_API_BASE }}
|
||||
ollama_models: ${{ vars.NIGHTLY_LLM_OLLAMA_MODELS }}
|
||||
openrouter_models: ${{ vars.NIGHTLY_LLM_OPENROUTER_MODELS }}
|
||||
strict: true
|
||||
secrets:
|
||||
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bedrock_api_key: ${{ secrets.BEDROCK_API_KEY }}
|
||||
vertex_ai_custom_config_json: ${{ secrets.NIGHTLY_LLM_VERTEX_AI_CUSTOM_CONFIG_JSON }}
|
||||
azure_api_key: ${{ secrets.AZURE_API_KEY }}
|
||||
ollama_api_key: ${{ secrets.OLLAMA_API_KEY }}
|
||||
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
|
||||
@@ -23,6 +23,26 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
azure_models:
|
||||
description: "Comma-separated models for azure"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
ollama_models:
|
||||
description: "Comma-separated models for ollama_chat"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
openrouter_models:
|
||||
description: "Comma-separated models for openrouter"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
azure_api_base:
|
||||
description: "API base for azure provider"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
strict:
|
||||
description: "Default NIGHTLY_LLM_STRICT passed to tests"
|
||||
required: false
|
||||
@@ -37,6 +57,12 @@ on:
|
||||
required: false
|
||||
vertex_ai_custom_config_json:
|
||||
required: false
|
||||
azure_api_key:
|
||||
required: false
|
||||
ollama_api_key:
|
||||
required: false
|
||||
openrouter_api_key:
|
||||
required: false
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_TOKEN:
|
||||
@@ -146,21 +172,57 @@ jobs:
|
||||
models: ${{ inputs.openai_models }}
|
||||
api_key_secret: openai_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: ""
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: true
|
||||
- provider: anthropic
|
||||
models: ${{ inputs.anthropic_models }}
|
||||
api_key_secret: anthropic_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: ""
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: true
|
||||
- provider: bedrock
|
||||
models: ${{ inputs.bedrock_models }}
|
||||
api_key_secret: bedrock_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: ""
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: false
|
||||
- provider: vertex_ai
|
||||
models: ${{ inputs.vertex_ai_models }}
|
||||
api_key_secret: ""
|
||||
custom_config_secret: vertex_ai_custom_config_json
|
||||
api_base: ""
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: false
|
||||
- provider: azure
|
||||
models: ${{ inputs.azure_models }}
|
||||
api_key_secret: azure_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: ${{ inputs.azure_api_base }}
|
||||
api_version: "2025-04-01-preview"
|
||||
deployment_name: ""
|
||||
required: false
|
||||
- provider: ollama_chat
|
||||
models: ${{ inputs.ollama_models }}
|
||||
api_key_secret: ollama_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: "https://ollama.com"
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: false
|
||||
- provider: openrouter
|
||||
models: ${{ inputs.openrouter_models }}
|
||||
api_key_secret: openrouter_api_key
|
||||
custom_config_secret: ""
|
||||
api_base: "https://openrouter.ai/api/v1"
|
||||
api_version: ""
|
||||
deployment_name: ""
|
||||
required: false
|
||||
runs-on:
|
||||
- runs-on
|
||||
@@ -183,6 +245,9 @@ jobs:
|
||||
models: ${{ matrix.models }}
|
||||
provider-api-key: ${{ matrix.api_key_secret && secrets[matrix.api_key_secret] || '' }}
|
||||
strict: ${{ inputs.strict && 'true' || 'false' }}
|
||||
api-base: ${{ matrix.api_base }}
|
||||
api-version: ${{ matrix.api_version }}
|
||||
deployment-name: ${{ matrix.deployment_name }}
|
||||
custom-config-json: ${{ matrix.custom_config_secret && secrets[matrix.custom_config_secret] || '' }}
|
||||
runs-on-ecr-cache: ${{ env.RUNS_ON_ECR_CACHE }}
|
||||
run-id: ${{ github.run_id }}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""add python tool on default
|
||||
|
||||
Revision ID: 57122d037335
|
||||
Revises: c0c937d5c9e5
|
||||
Create Date: 2026-02-27 10:10:40.124925
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "57122d037335"
|
||||
down_revision = "c0c937d5c9e5"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
PYTHON_TOOL_NAME = "python"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Look up the PythonTool id
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM tool WHERE name = :name"),
|
||||
{"name": PYTHON_TOOL_NAME},
|
||||
).fetchone()
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
tool_id = result[0]
|
||||
|
||||
# Attach to the default persona (id=0) if not already attached
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO persona__tool (persona_id, tool_id)
|
||||
VALUES (0, :tool_id)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
),
|
||||
{"tool_id": tool_id},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM tool WHERE name = :name"),
|
||||
{"name": PYTHON_TOOL_NAME},
|
||||
).fetchone()
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM persona__tool
|
||||
WHERE persona_id = 0 AND tool_id = :tool_id
|
||||
"""
|
||||
),
|
||||
{"tool_id": result[0]},
|
||||
)
|
||||
@@ -11,6 +11,7 @@ SQLAlchemy connection pool metrics are registered separately via
|
||||
"""
|
||||
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
from prometheus_fastapi_instrumentator.metrics import default as default_metrics
|
||||
from sqlalchemy.exc import TimeoutError as SATimeoutError
|
||||
from starlette.applications import Starlette
|
||||
|
||||
@@ -60,6 +61,14 @@ def setup_prometheus_metrics(app: Starlette) -> None:
|
||||
excluded_handlers=_EXCLUDED_HANDLERS,
|
||||
)
|
||||
|
||||
# Explicitly create the default metrics (http_requests_total,
|
||||
# http_request_duration_seconds, etc.) and add them first. The library
|
||||
# skips creating defaults when ANY custom instrumentations are registered
|
||||
# via .add(), so we must include them ourselves.
|
||||
default_callback = default_metrics(latency_lowr_buckets=_LATENCY_BUCKETS)
|
||||
if default_callback:
|
||||
instrumentator.add(default_callback)
|
||||
|
||||
instrumentator.add(slow_request_callback)
|
||||
instrumentator.add(per_tenant_request_callback)
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ _ENV_PROVIDER = "NIGHTLY_LLM_PROVIDER"
|
||||
_ENV_MODELS = "NIGHTLY_LLM_MODELS"
|
||||
_ENV_API_KEY = "NIGHTLY_LLM_API_KEY"
|
||||
_ENV_API_BASE = "NIGHTLY_LLM_API_BASE"
|
||||
_ENV_API_VERSION = "NIGHTLY_LLM_API_VERSION"
|
||||
_ENV_DEPLOYMENT_NAME = "NIGHTLY_LLM_DEPLOYMENT_NAME"
|
||||
_ENV_CUSTOM_CONFIG_JSON = "NIGHTLY_LLM_CUSTOM_CONFIG_JSON"
|
||||
_ENV_STRICT = "NIGHTLY_LLM_STRICT"
|
||||
|
||||
@@ -34,6 +36,8 @@ class NightlyProviderConfig(BaseModel):
|
||||
model_names: list[str]
|
||||
api_key: str | None
|
||||
api_base: str | None
|
||||
api_version: str | None
|
||||
deployment_name: str | None
|
||||
custom_config: dict[str, str] | None
|
||||
strict: bool
|
||||
|
||||
@@ -66,6 +70,8 @@ def _load_provider_config() -> NightlyProviderConfig:
|
||||
model_names = _parse_models_env(_ENV_MODELS)
|
||||
api_key = os.environ.get(_ENV_API_KEY) or None
|
||||
api_base = os.environ.get(_ENV_API_BASE) or None
|
||||
api_version = os.environ.get(_ENV_API_VERSION) or None
|
||||
deployment_name = os.environ.get(_ENV_DEPLOYMENT_NAME) or None
|
||||
strict = _env_true(_ENV_STRICT, default=False)
|
||||
|
||||
custom_config: dict[str, str] | None = None
|
||||
@@ -84,6 +90,8 @@ def _load_provider_config() -> NightlyProviderConfig:
|
||||
model_names=model_names,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
api_version=api_version,
|
||||
deployment_name=deployment_name,
|
||||
custom_config=custom_config,
|
||||
strict=strict,
|
||||
)
|
||||
@@ -124,6 +132,22 @@ def _validate_provider_config(config: NightlyProviderConfig) -> None:
|
||||
message=(f"{_ENV_API_BASE} is required for provider '{config.provider}'"),
|
||||
)
|
||||
|
||||
if config.provider == "azure":
|
||||
if not config.api_base:
|
||||
_skip_or_fail(
|
||||
strict=config.strict,
|
||||
message=(
|
||||
f"{_ENV_API_BASE} is required for provider '{config.provider}'"
|
||||
),
|
||||
)
|
||||
if not config.api_version:
|
||||
_skip_or_fail(
|
||||
strict=config.strict,
|
||||
message=(
|
||||
f"{_ENV_API_VERSION} is required for provider '{config.provider}'"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _assert_integration_mode_enabled() -> None:
|
||||
assert (
|
||||
@@ -162,6 +186,8 @@ def _create_provider_payload(
|
||||
model_name: str,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
api_version: str | None,
|
||||
deployment_name: str | None,
|
||||
custom_config: dict[str, str] | None,
|
||||
) -> dict:
|
||||
return {
|
||||
@@ -169,6 +195,8 @@ def _create_provider_payload(
|
||||
"provider": provider,
|
||||
"api_key": api_key,
|
||||
"api_base": api_base,
|
||||
"api_version": api_version,
|
||||
"deployment_name": deployment_name,
|
||||
"custom_config": custom_config,
|
||||
"default_model_name": model_name,
|
||||
"is_public": True,
|
||||
@@ -270,6 +298,8 @@ def _create_and_test_provider_for_model(
|
||||
model_name=model_name,
|
||||
api_key=config.api_key,
|
||||
api_base=resolved_api_base,
|
||||
api_version=config.api_version,
|
||||
deployment_name=config.deployment_name,
|
||||
custom_config=config.custom_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ def test_cold_startup_default_assistant() -> None:
|
||||
assert (
|
||||
"read_file" in tool_names
|
||||
), "Default assistant should have FileReaderTool attached"
|
||||
assert (
|
||||
"python" in tool_names
|
||||
), "Default assistant should have PythonTool attached"
|
||||
|
||||
# Also verify by display names for clarity
|
||||
assert (
|
||||
@@ -86,8 +89,11 @@ def test_cold_startup_default_assistant() -> None:
|
||||
assert (
|
||||
"File Reader" in tool_display_names
|
||||
), "Default assistant should have File Reader tool"
|
||||
|
||||
# Should have exactly 5 tools
|
||||
assert (
|
||||
len(tool_associations) == 5
|
||||
), f"Default assistant should have exactly 5 tools attached, got {len(tool_associations)}"
|
||||
"Code Interpreter" in tool_display_names
|
||||
), "Default assistant should have Code Interpreter tool"
|
||||
|
||||
# Should have exactly 6 tools
|
||||
assert (
|
||||
len(tool_associations) == 6
|
||||
), f"Default assistant should have exactly 6 tools attached, got {len(tool_associations)}"
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_setup_attaches_instrumentator_to_app() -> None:
|
||||
inprogress_labels=True,
|
||||
excluded_handlers=["/health", "/metrics", "/openapi.json"],
|
||||
)
|
||||
assert mock_instance.add.call_count == 2
|
||||
assert mock_instance.add.call_count == 3
|
||||
mock_instance.instrument.assert_called_once_with(
|
||||
app,
|
||||
latency_lowr_buckets=(
|
||||
|
||||
@@ -163,3 +163,16 @@ Add clear comments:
|
||||
- Any TODOs you add in the code must be accompanied by either the name/username
|
||||
of the owner of that TODO, or an issue number for an issue referencing that
|
||||
piece of work.
|
||||
- Avoid module-level logic that runs on import, which leads to import-time side
|
||||
effects. Essentially every piece of meaningful logic should exist within some
|
||||
function that has to be explicitly invoked. Acceptable exceptions to this may
|
||||
include loading environment variables or setting up loggers.
|
||||
- If you find yourself needing something like this, you may want that logic to
|
||||
exist in a file dedicated for manual execution (contains `if __name__ ==
|
||||
"__main__":`) which should not be imported by anything else.
|
||||
- Related to the above, do not conflate Python scripts you intend to run from
|
||||
the command line (contains `if __name__ == "__main__":`) with modules you
|
||||
intend to import from elsewhere. If for some unlikely reason they have to be
|
||||
the same file, any logic specific to executing the file (including imports)
|
||||
should be contained in the `if __name__ == "__main__":` block.
|
||||
- Generally these executable files exist in `backend/scripts/`.
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Main() {
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgMcp}
|
||||
title="MCP Actions"
|
||||
description="Connect MCP (Model Context Protocol) servers to add custom actions and tools for your assistants."
|
||||
description="Connect MCP (Model Context Protocol) servers to add custom actions and tools for your agents."
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Main() {
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgActions}
|
||||
title="OpenAPI Actions"
|
||||
description="Connect OpenAPI servers to add custom actions and tools for your assistants."
|
||||
description="Connect OpenAPI servers to add custom actions and tools for your agents."
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
|
||||
@@ -170,7 +170,7 @@ export function PersonasTable({
|
||||
{deleteModalOpen && personaToDelete && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgAlertCircle}
|
||||
title="Delete Assistant"
|
||||
title="Delete Agent"
|
||||
onClose={closeDeleteModal}
|
||||
submit={<Button onClick={handleDeletePersona}>Delete</Button>}
|
||||
>
|
||||
@@ -183,15 +183,15 @@ export function PersonasTable({
|
||||
const isDefault = personaToToggleDefault.is_default_persona;
|
||||
|
||||
const title = isDefault
|
||||
? "Remove Featured Assistant"
|
||||
: "Set Featured Assistant";
|
||||
? "Remove Featured Agent"
|
||||
: "Set Featured Agent";
|
||||
const buttonText = isDefault ? "Remove Feature" : "Set as Featured";
|
||||
const text = isDefault
|
||||
? `Are you sure you want to remove the featured status of ${personaToToggleDefault.name}?`
|
||||
: `Are you sure you want to set the featured status of ${personaToToggleDefault.name}?`;
|
||||
const additionalText = isDefault
|
||||
? `Removing "${personaToToggleDefault.name}" as a featured assistant will not affect its visibility or accessibility.`
|
||||
: `Setting "${personaToToggleDefault.name}" as a featured assistant will make it public and visible to all users. This action cannot be undone.`;
|
||||
? `Removing "${personaToToggleDefault.name}" as a featured agent will not affect its visibility or accessibility.`
|
||||
: `Setting "${personaToToggleDefault.name}" as a featured agent will make it public and visible to all users. This action cannot be undone.`;
|
||||
|
||||
return (
|
||||
<ConfirmationModalLayout
|
||||
@@ -217,7 +217,7 @@ export function PersonasTable({
|
||||
"Name",
|
||||
"Description",
|
||||
"Type",
|
||||
"Featured Assistant",
|
||||
"Featured Agent",
|
||||
"Is Visible",
|
||||
"Delete",
|
||||
]}
|
||||
|
||||
@@ -47,8 +47,8 @@ function MainContent({
|
||||
return (
|
||||
<div>
|
||||
<Text className="mb-2">
|
||||
Assistants are a way to build custom search/question-answering
|
||||
experiences for different use cases.
|
||||
Agents are a way to build custom search/question-answering experiences
|
||||
for different use cases.
|
||||
</Text>
|
||||
<Text className="mt-2">They allow you to customize:</Text>
|
||||
<div className="text-sm">
|
||||
@@ -63,21 +63,21 @@ function MainContent({
|
||||
<div>
|
||||
<Separator />
|
||||
|
||||
<Title>Create an Assistant</Title>
|
||||
<Title>Create an Agent</Title>
|
||||
<CreateButton href="/app/agents/create?admin=true">
|
||||
New Assistant
|
||||
New Agent
|
||||
</CreateButton>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Title>Existing Assistants</Title>
|
||||
<Title>Existing Agents</Title>
|
||||
{totalItems > 0 ? (
|
||||
<>
|
||||
<SubLabel>
|
||||
Assistants will be displayed as options on the Chat / Search
|
||||
interfaces in the order they are displayed below. Assistants
|
||||
marked as hidden will not be displayed. Editable assistants are
|
||||
shown at the top.
|
||||
Agents will be displayed as options on the Chat / Search
|
||||
interfaces in the order they are displayed below. Agents marked as
|
||||
hidden will not be displayed. Editable agents are shown at the
|
||||
top.
|
||||
</SubLabel>
|
||||
<PersonasTable
|
||||
personas={customPersonas}
|
||||
@@ -96,21 +96,21 @@ function MainContent({
|
||||
) : (
|
||||
<div className="mt-6 p-8 border border-border rounded-lg bg-background-weak text-center">
|
||||
<Text className="text-lg font-medium mb-2">
|
||||
No custom assistants yet
|
||||
No custom agents yet
|
||||
</Text>
|
||||
<Text className="text-subtle mb-3">
|
||||
Create your first assistant to:
|
||||
Create your first agent to:
|
||||
</Text>
|
||||
<ul className="text-subtle text-sm list-disc text-left inline-block mb-3">
|
||||
<li>Build department-specific knowledge bases</li>
|
||||
<li>Create specialized research assistants</li>
|
||||
<li>Create specialized research agents</li>
|
||||
<li>Set up compliance and policy advisors</li>
|
||||
</ul>
|
||||
<Text className="text-subtle text-sm mb-4">
|
||||
...and so much more!
|
||||
</Text>
|
||||
<CreateButton href="/app/agents/create?admin=true">
|
||||
Create Your First Assistant
|
||||
Create Your First Agent
|
||||
</CreateButton>
|
||||
</div>
|
||||
)}
|
||||
@@ -128,13 +128,13 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageTitle icon={SvgOnyxOctagon} title="Assistants" />
|
||||
<AdminPageTitle icon={SvgOnyxOctagon} title="Agents" />
|
||||
|
||||
{isLoading && <ThreeDotsLoader />}
|
||||
|
||||
{error && (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to load assistants"
|
||||
errorTitle="Failed to load agents"
|
||||
errorMsg={
|
||||
error?.info?.message ||
|
||||
error?.info?.detail ||
|
||||
|
||||
@@ -156,7 +156,7 @@ export const SlackChannelConfigCreationForm = ({
|
||||
is: "assistant",
|
||||
then: (schema) =>
|
||||
schema.required(
|
||||
"A persona is required when using the'Assistant' knowledge source"
|
||||
"An agent is required when using the 'Agent' knowledge source"
|
||||
),
|
||||
}),
|
||||
standard_answer_categories: Yup.array(),
|
||||
|
||||
@@ -224,14 +224,14 @@ export function SlackChannelConfigFormFields({
|
||||
<RadioGroupItemField
|
||||
value="assistant"
|
||||
id="assistant"
|
||||
label="Search Assistant"
|
||||
label="Search Agent"
|
||||
sublabel="Control both the documents and the prompt to use for answering questions"
|
||||
/>
|
||||
<RadioGroupItemField
|
||||
value="non_search_assistant"
|
||||
id="non_search_assistant"
|
||||
label="Non-Search Assistant"
|
||||
sublabel="Chat with an assistant that does not use documents"
|
||||
label="Non-Search Agent"
|
||||
sublabel="Chat with an agent that does not use documents"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
@@ -327,15 +327,15 @@ export function SlackChannelConfigFormFields({
|
||||
<div className="mt-4">
|
||||
<SubLabel>
|
||||
<>
|
||||
Select the search-enabled assistant OnyxBot will use while
|
||||
answering questions in Slack.
|
||||
Select the search-enabled agent OnyxBot will use while answering
|
||||
questions in Slack.
|
||||
{syncEnabledAssistants.length > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-sm text-text-dark/80">
|
||||
Note: Some of your assistants have auto-synced connectors
|
||||
in their document sets. You cannot select these assistants
|
||||
as they will not be able to answer questions in Slack.{" "}
|
||||
Note: Some of your agents have auto-synced connectors in
|
||||
their document sets. You cannot select these agents as
|
||||
they will not be able to answer questions in Slack.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -349,7 +349,7 @@ export function SlackChannelConfigFormFields({
|
||||
{viewSyncEnabledAssistants
|
||||
? "Hide un-selectable "
|
||||
: "View all "}
|
||||
assistants
|
||||
agents
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
@@ -367,7 +367,7 @@ export function SlackChannelConfigFormFields({
|
||||
{viewSyncEnabledAssistants && syncEnabledAssistants.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-text-dark/80">
|
||||
Un-selectable assistants:
|
||||
Un-selectable agents:
|
||||
</p>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{syncEnabledAssistants.map(
|
||||
@@ -394,15 +394,15 @@ export function SlackChannelConfigFormFields({
|
||||
<div className="mt-4">
|
||||
<SubLabel>
|
||||
<>
|
||||
Select the non-search assistant OnyxBot will use while answering
|
||||
Select the non-search agent OnyxBot will use while answering
|
||||
questions in Slack.
|
||||
{syncEnabledAssistants.length > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-sm text-text-dark/80">
|
||||
Note: Some of your assistants have auto-synced connectors
|
||||
in their document sets. You cannot select these assistants
|
||||
as they will not be able to answer questions in Slack.{" "}
|
||||
Note: Some of your agents have auto-synced connectors in
|
||||
their document sets. You cannot select these agents as
|
||||
they will not be able to answer questions in Slack.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -416,7 +416,7 @@ export function SlackChannelConfigFormFields({
|
||||
{viewSyncEnabledAssistants
|
||||
? "Hide un-selectable "
|
||||
: "View all "}
|
||||
assistants
|
||||
agents
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
@@ -524,7 +524,7 @@ export function SlackChannelConfigFormFields({
|
||||
name="is_ephemeral"
|
||||
label="Respond to user in a private (ephemeral) message"
|
||||
tooltip="If set, OnyxBot will respond only to the user in a private (ephemeral) message. If you also
|
||||
chose 'Search' Assistant above, selecting this option will make documents that are private to the user
|
||||
chose 'Search' Agent above, selecting this option will make documents that are private to the user
|
||||
available for their queries."
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import CodeInterpreterPage from "@/refresh-pages/admin/CodeInterpreterPage";
|
||||
|
||||
export default function Page() {
|
||||
return <CodeInterpreterPage />;
|
||||
}
|
||||
@@ -39,10 +39,10 @@ export function AdvancedOptions({
|
||||
agents={agents}
|
||||
isLoading={agentsLoading}
|
||||
error={agentsError}
|
||||
label="Assistant Whitelist"
|
||||
subtext="Restrict this provider to specific assistants."
|
||||
label="Agent Whitelist"
|
||||
subtext="Restrict this provider to specific agents."
|
||||
disabled={formikProps.values.is_public}
|
||||
disabledMessage="This LLM Provider is public and available to all assistants."
|
||||
disabledMessage="This LLM Provider is public and available to all agents."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -299,11 +299,11 @@ export default function Page({ params }: Props) {
|
||||
});
|
||||
refreshGuild();
|
||||
toast.success(
|
||||
personaId ? "Default assistant updated" : "Default assistant cleared"
|
||||
personaId ? "Default agent updated" : "Default agent cleared"
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : "Failed to update assistant"
|
||||
err instanceof Error ? err.message : "Failed to update agent"
|
||||
);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
@@ -355,7 +355,7 @@ export default function Page({ params }: Props) {
|
||||
<InputSelect.Trigger placeholder="Select agent" />
|
||||
<InputSelect.Content>
|
||||
<InputSelect.Item value="default">
|
||||
Default Assistant
|
||||
Default Agent
|
||||
</InputSelect.Item>
|
||||
{personas.map((persona) => (
|
||||
<InputSelect.Item
|
||||
|
||||
@@ -427,7 +427,7 @@ export const GroupDisplay = ({
|
||||
|
||||
<Separator />
|
||||
|
||||
<h2 className="text-xl font-bold mt-8 mb-2">Assistants</h2>
|
||||
<h2 className="text-xl font-bold mt-8 mb-2">Agents</h2>
|
||||
|
||||
<div>
|
||||
{userGroup.document_sets.length > 0 ? (
|
||||
@@ -445,7 +445,7 @@ export const GroupDisplay = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Text>No Assistants in this group...</Text>
|
||||
<Text>No Agents in this group...</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -152,14 +152,14 @@ export function PersonaMessagesChart({
|
||||
} else if (selectedPersonaId === undefined) {
|
||||
content = (
|
||||
<div className="h-80 text-text-500 flex flex-col">
|
||||
<p className="m-auto">Select an assistant to view analytics</p>
|
||||
<p className="m-auto">Select an agent to view analytics</p>
|
||||
</div>
|
||||
);
|
||||
} else if (!personaMessagesData?.length) {
|
||||
content = (
|
||||
<div className="h-80 text-text-500 flex flex-col">
|
||||
<p className="m-auto">
|
||||
No data found for selected assistant in the specified time range
|
||||
No data found for selected agent in the specified time range
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -178,11 +178,9 @@ export function PersonaMessagesChart({
|
||||
|
||||
return (
|
||||
<CardSection className="mt-8">
|
||||
<Title>Assistant Analytics</Title>
|
||||
<Title>Agent Analytics</Title>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Text>
|
||||
Messages and unique users per day for the selected assistant
|
||||
</Text>
|
||||
<Text>Messages and unique users per day for the selected agent</Text>
|
||||
<div className="flex items-center gap-4">
|
||||
<Select
|
||||
value={selectedPersonaId?.toString() ?? ""}
|
||||
@@ -191,14 +189,14 @@ export function PersonaMessagesChart({
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex w-full max-w-xs">
|
||||
<SelectValue placeholder="Select an assistant to display" />
|
||||
<SelectValue placeholder="Select an agent to display" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<div className="flex items-center px-2 pb-2 sticky top-0 bg-background border-b">
|
||||
<Search className="h-4 w-4 mr-2 shrink-0 opacity-50" />
|
||||
<input
|
||||
className="flex h-8 w-full rounded-sm bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Search assistants..."
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -146,7 +146,7 @@ export function AssistantStats({ assistantId }: { assistantId: number }) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<p className="text-base font-normal text-2xl">Assistant Analytics</p>
|
||||
<p className="text-base font-normal text-2xl">Agent Analytics</p>
|
||||
<AdminDateRangeSelector
|
||||
value={dateRange}
|
||||
onValueChange={setDateRange}
|
||||
|
||||
@@ -12,17 +12,17 @@ export default function NoAssistantModal() {
|
||||
return (
|
||||
<Modal open>
|
||||
<Modal.Content width="sm" height="sm">
|
||||
<Modal.Header icon={SvgUser} title="No Assistant Available" />
|
||||
<Modal.Header icon={SvgUser} title="No Agent Available" />
|
||||
<Modal.Body>
|
||||
<Text as="p">
|
||||
You currently have no assistant configured. To use this feature, you
|
||||
You currently have no agent configured. To use this feature, you
|
||||
need to take action.
|
||||
</Text>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
<Text as="p">
|
||||
As an administrator, you can create a new assistant by visiting
|
||||
the admin panel.
|
||||
As an administrator, you can create a new agent by visiting the
|
||||
admin panel.
|
||||
</Text>
|
||||
<Button className="w-full" href="/admin/assistants">
|
||||
Go to Admin Panel
|
||||
@@ -30,8 +30,7 @@ export default function NoAssistantModal() {
|
||||
</>
|
||||
) : (
|
||||
<Text as="p">
|
||||
Please contact your administrator to configure an assistant for
|
||||
you.
|
||||
Please contact your administrator to configure an agent for you.
|
||||
</Text>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
44
web/src/hooks/useCodeInterpreter.ts
Normal file
44
web/src/hooks/useCodeInterpreter.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
const HEALTH_ENDPOINT = "/api/admin/code-interpreter/health";
|
||||
const STATUS_ENDPOINT = "/api/admin/code-interpreter";
|
||||
|
||||
interface CodeInterpreterHealth {
|
||||
healthy: boolean;
|
||||
}
|
||||
|
||||
interface CodeInterpreterStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export default function useCodeInterpreter() {
|
||||
const {
|
||||
data: healthData,
|
||||
error: healthError,
|
||||
isLoading: isHealthLoading,
|
||||
mutate: refetchHealth,
|
||||
} = useSWR<CodeInterpreterHealth>(HEALTH_ENDPOINT, errorHandlingFetcher, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
const {
|
||||
data: statusData,
|
||||
error: statusError,
|
||||
isLoading: isStatusLoading,
|
||||
mutate: refetchStatus,
|
||||
} = useSWR<CodeInterpreterStatus>(STATUS_ENDPOINT, errorHandlingFetcher);
|
||||
|
||||
function refetch() {
|
||||
refetchHealth();
|
||||
refetchStatus();
|
||||
}
|
||||
|
||||
return {
|
||||
isHealthy: healthData?.healthy ?? false,
|
||||
isEnabled: statusData?.enabled ?? false,
|
||||
isLoading: isHealthLoading || isStatusLoading,
|
||||
error: healthError || statusError,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
15
web/src/lib/admin/code-interpreter/svc.ts
Normal file
15
web/src/lib/admin/code-interpreter/svc.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
const UPDATE_ENDPOINT = "/api/admin/code-interpreter";
|
||||
|
||||
interface CodeInterpreterUpdateRequest {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export async function updateCodeInterpreter(
|
||||
request: CodeInterpreterUpdateRequest
|
||||
): Promise<Response> {
|
||||
return fetch(UPDATE_ENDPOINT, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
@@ -179,7 +179,7 @@ export default function ActionLineItem({
|
||||
)}
|
||||
|
||||
{isSearchToolAndNotInProject && (
|
||||
<IconButton
|
||||
<Button
|
||||
icon={
|
||||
isSearchToolWithNoConnectors ? SvgSettings : SvgChevronRight
|
||||
}
|
||||
@@ -188,11 +188,8 @@ export default function ActionLineItem({
|
||||
router.push("/admin/add-connector");
|
||||
else onSourceManagementOpen?.();
|
||||
})}
|
||||
internal
|
||||
className={cn(
|
||||
isSearchToolWithNoConnectors &&
|
||||
"invisible group-hover/LineItem:visible"
|
||||
)}
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
tooltip={
|
||||
isSearchToolWithNoConnectors
|
||||
? "Add Connectors"
|
||||
|
||||
@@ -425,7 +425,7 @@ export default function AgentsNavigationPage() {
|
||||
>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgOnyxOctagon}
|
||||
title="Agents & Assistants"
|
||||
title="Agents"
|
||||
description="Customize AI behavior and knowledge for you and your team's use cases."
|
||||
rightChildren={
|
||||
<Button
|
||||
|
||||
241
web/src/refresh-pages/admin/CodeInterpreterPage.tsx
Normal file
241
web/src/refresh-pages/admin/CodeInterpreterPage.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { Card, type CardProps } from "@/refresh-components/cards";
|
||||
import {
|
||||
SvgArrowExchange,
|
||||
SvgCheckCircle,
|
||||
SvgRefreshCw,
|
||||
SvgTerminal,
|
||||
SvgUnplug,
|
||||
SvgXOctagon,
|
||||
} from "@opal/icons";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
|
||||
import useCodeInterpreter from "@/hooks/useCodeInterpreter";
|
||||
import { updateCodeInterpreter } from "@/lib/admin/code-interpreter/svc";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
|
||||
interface CodeInterpreterCardProps {
|
||||
variant?: CardProps["variant"];
|
||||
title: string;
|
||||
middleText?: string;
|
||||
strikethrough?: boolean;
|
||||
rightContent: React.ReactNode;
|
||||
}
|
||||
|
||||
function CodeInterpreterCard({
|
||||
variant,
|
||||
title,
|
||||
middleText,
|
||||
strikethrough,
|
||||
rightContent,
|
||||
}: CodeInterpreterCardProps) {
|
||||
return (
|
||||
// TODO (@raunakab): Allow Content to accept strikethrough and middleText
|
||||
<Card variant={variant} padding={0.5}>
|
||||
<ContentAction
|
||||
icon={SvgTerminal}
|
||||
title={middleText ? `${title} ${middleText}` : title}
|
||||
description="Built-in Python runtime"
|
||||
variant="section"
|
||||
sizePreset="main-ui"
|
||||
rightChildren={rightContent}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckingStatus() {
|
||||
return (
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={0.25}
|
||||
padding={0.5}
|
||||
>
|
||||
<Text mainUiAction text03>
|
||||
Checking...
|
||||
</Text>
|
||||
<SimpleLoader />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
healthy: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function ConnectionStatus({ healthy, isLoading }: ConnectionStatusProps) {
|
||||
if (isLoading) {
|
||||
return <CheckingStatus />;
|
||||
}
|
||||
|
||||
const label = healthy ? "Connected" : "Connection Lost";
|
||||
const Icon = healthy ? SvgCheckCircle : SvgXOctagon;
|
||||
const iconColor = healthy ? "text-status-success-05" : "text-status-error-05";
|
||||
|
||||
return (
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={0.25}
|
||||
padding={0.5}
|
||||
>
|
||||
<Text mainUiAction text03>
|
||||
{label}
|
||||
</Text>
|
||||
<Icon size={16} className={iconColor} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onDisconnect: () => void;
|
||||
onRefresh: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ActionButtons({
|
||||
onDisconnect,
|
||||
onRefresh,
|
||||
disabled,
|
||||
}: ActionButtonsProps) {
|
||||
return (
|
||||
<Section
|
||||
flexDirection="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={0.25}
|
||||
padding={0.25}
|
||||
>
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgUnplug}
|
||||
onClick={onDisconnect}
|
||||
tooltip="Disconnect"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgRefreshCw}
|
||||
onClick={onRefresh}
|
||||
tooltip="Refresh"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CodeInterpreterPage() {
|
||||
const { isHealthy, isEnabled, isLoading, refetch } = useCodeInterpreter();
|
||||
const [showDisconnectModal, setShowDisconnectModal] = useState(false);
|
||||
const [isReconnecting, setIsReconnecting] = useState(false);
|
||||
|
||||
async function handleToggle(enabled: boolean) {
|
||||
const action = enabled ? "reconnect" : "disconnect";
|
||||
setIsReconnecting(enabled);
|
||||
try {
|
||||
const response = await updateCodeInterpreter({ enabled });
|
||||
if (!response.ok) {
|
||||
toast.error(`Failed to ${action} Code Interpreter`);
|
||||
return;
|
||||
}
|
||||
setShowDisconnectModal(false);
|
||||
refetch();
|
||||
} finally {
|
||||
setIsReconnecting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgTerminal}
|
||||
title="Code Interpreter"
|
||||
description="Safe and sandboxed Python runtime available to your LLM. See docs for more details."
|
||||
separator
|
||||
/>
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
{isEnabled || isLoading ? (
|
||||
<CodeInterpreterCard
|
||||
title="Code Interpreter"
|
||||
variant={isHealthy ? "primary" : "secondary"}
|
||||
strikethrough={!isHealthy}
|
||||
rightContent={
|
||||
<Section
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="end"
|
||||
gap={0}
|
||||
padding={0}
|
||||
>
|
||||
<ConnectionStatus healthy={isHealthy} isLoading={isLoading} />
|
||||
<ActionButtons
|
||||
onDisconnect={() => setShowDisconnectModal(true)}
|
||||
onRefresh={refetch}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CodeInterpreterCard
|
||||
variant="secondary"
|
||||
title="Code Interpreter"
|
||||
middleText="(Disconnected)"
|
||||
strikethrough={true}
|
||||
rightContent={
|
||||
<Section flexDirection="row" alignItems="center" padding={0.5}>
|
||||
{isReconnecting ? (
|
||||
<CheckingStatus />
|
||||
) : (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
rightIcon={SvgArrowExchange}
|
||||
onClick={() => handleToggle(true)}
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
)}
|
||||
</Section>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SettingsLayouts.Body>
|
||||
|
||||
{showDisconnectModal && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUnplug}
|
||||
title="Disconnect Code Interpreter"
|
||||
onClose={() => setShowDisconnectModal(false)}
|
||||
submit={
|
||||
<Button variant="danger" onClick={() => handleToggle(false)}>
|
||||
Disconnect
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Text as="p" text03>
|
||||
All running sessions connected to{" "}
|
||||
<Text as="span" mainContentEmphasis text03>
|
||||
Code Interpreter
|
||||
</Text>{" "}
|
||||
will stop working. Note that this will not remove any data from your
|
||||
runtime. You can reconnect to this runtime later if needed.
|
||||
</Text>
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
</SettingsLayouts.Root>
|
||||
);
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export default function NewTenantModal({
|
||||
: `Your request to join ${tenantInfo.number_of_users} other users of ${APP_DOMAIN} has been approved.`;
|
||||
|
||||
const description = isInvite
|
||||
? `By accepting this invitation, you will join the existing ${APP_DOMAIN} team and lose access to your current team. Note: you will lose access to your current assistants, prompts, chats, and connected sources.`
|
||||
? `By accepting this invitation, you will join the existing ${APP_DOMAIN} team and lose access to your current team. Note: you will lose access to your current agents, prompts, chats, and connected sources.`
|
||||
: `To finish joining your team, please reauthenticate with ${user?.email}.`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
SvgPaintBrush,
|
||||
SvgDiscordMono,
|
||||
SvgWallet,
|
||||
SvgTerminal,
|
||||
} from "@opal/icons";
|
||||
import SvgMcp from "@opal/icons/mcp";
|
||||
import UserAvatarPopover from "@/sections/sidebar/UserAvatarPopover";
|
||||
@@ -91,7 +92,7 @@ const custom_assistants_items = (
|
||||
) => {
|
||||
const items = [
|
||||
{
|
||||
name: "Assistants",
|
||||
name: "Agents",
|
||||
icon: SvgOnyxOctagon,
|
||||
link: "/admin/assistants",
|
||||
},
|
||||
@@ -165,7 +166,7 @@ const collections = (
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "Custom Assistants",
|
||||
name: "Custom Agents",
|
||||
items: custom_assistants_items(isCurator, enableEnterprise),
|
||||
},
|
||||
...(isCurator && enableEnterprise
|
||||
@@ -207,6 +208,11 @@ const collections = (
|
||||
icon: SvgImage,
|
||||
link: "/admin/configuration/image-generation",
|
||||
},
|
||||
{
|
||||
name: "Code Interpreter",
|
||||
icon: SvgTerminal,
|
||||
link: "/admin/configuration/code-interpreter",
|
||||
},
|
||||
...(!enableCloud && vectorDbEnabled
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -29,12 +29,12 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
|
||||
pageTitle: "Add Connector",
|
||||
},
|
||||
{
|
||||
name: "Custom Assistants - Assistants",
|
||||
name: "Custom Agents - Agents",
|
||||
path: "assistants",
|
||||
pageTitle: "Assistants",
|
||||
pageTitle: "Agents",
|
||||
options: {
|
||||
paragraphText:
|
||||
"Assistants are a way to build custom search/question-answering experiences for different use cases.",
|
||||
"Agents are a way to build custom search/question-answering experiences for different use cases.",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -52,7 +52,7 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom Assistants - Slack Bots",
|
||||
name: "Custom Agents - Slack Bots",
|
||||
path: "bots",
|
||||
pageTitle: "Slack Bots",
|
||||
options: {
|
||||
@@ -61,7 +61,7 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom Assistants - Standard Answers",
|
||||
name: "Custom Agents - Standard Answers",
|
||||
path: "standard-answer",
|
||||
pageTitle: "Standard Answers",
|
||||
},
|
||||
@@ -101,12 +101,12 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
|
||||
pageTitle: "Search Settings",
|
||||
},
|
||||
{
|
||||
name: "Custom Assistants - MCP Actions",
|
||||
name: "Custom Agents - MCP Actions",
|
||||
path: "actions/mcp",
|
||||
pageTitle: "MCP Actions",
|
||||
},
|
||||
{
|
||||
name: "Custom Assistants - OpenAPI Actions",
|
||||
name: "Custom Agents - OpenAPI Actions",
|
||||
path: "actions/open-api",
|
||||
pageTitle: "OpenAPI Actions",
|
||||
},
|
||||
|
||||
268
web/tests/e2e/admin/code-interpreter/code_interpreter.spec.ts
Normal file
268
web/tests/e2e/admin/code-interpreter/code_interpreter.spec.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { loginAs } from "@tests/e2e/utils/auth";
|
||||
|
||||
const CODE_INTERPRETER_URL = "/admin/configuration/code-interpreter";
|
||||
const API_STATUS_URL = "**/api/admin/code-interpreter";
|
||||
const API_HEALTH_URL = "**/api/admin/code-interpreter/health";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Intercept the status (GET /) and health (GET /health) endpoints with the
|
||||
* given values so the page renders deterministically.
|
||||
*
|
||||
* Also handles PUT requests — by default they succeed (200). Pass
|
||||
* `putStatus` to simulate failures.
|
||||
*/
|
||||
async function mockCodeInterpreterApi(
|
||||
page: Page,
|
||||
opts: { enabled: boolean; healthy: boolean; putStatus?: number }
|
||||
) {
|
||||
const putStatus = opts.putStatus ?? 200;
|
||||
|
||||
await page.route(API_HEALTH_URL, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ healthy: opts.healthy }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(API_STATUS_URL, async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
await route.fulfill({
|
||||
status: putStatus,
|
||||
contentType: "application/json",
|
||||
body:
|
||||
putStatus >= 400
|
||||
? JSON.stringify({ detail: "Server Error" })
|
||||
: JSON.stringify(null),
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ enabled: opts.enabled }),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The disconnect icon button is an icon-only opal Button whose tooltip text
|
||||
* is not exposed as an accessible name. Locate it by finding the first
|
||||
* icon-only button (no label span) inside the card area.
|
||||
*/
|
||||
function getDisconnectIconButton(page: Page) {
|
||||
return page
|
||||
.locator("button:has(.opal-button):not(:has(.opal-button-label))")
|
||||
.first();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Code Interpreter Admin Page", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.context().clearCookies();
|
||||
await loginAs(page, "admin");
|
||||
});
|
||||
|
||||
test("page loads with header and description", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: true, healthy: true });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.locator('[aria-label="admin-page-title"]')).toHaveText(
|
||||
/^Code Interpreter/,
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
await expect(page.getByText("Built-in Python runtime")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Connected status when enabled and healthy", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: true, healthy: true });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByText("Connected")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("shows Connection Lost when enabled but unhealthy", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: true, healthy: false });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByText("Connection Lost")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("shows Reconnect button when disabled", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: false, healthy: false });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByText("(Disconnected)")).toBeVisible();
|
||||
});
|
||||
|
||||
test("disconnect flow opens modal and sends PUT request", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: true, healthy: true });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByText("Connected")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click the disconnect icon button
|
||||
await getDisconnectIconButton(page).click();
|
||||
|
||||
// Modal should appear
|
||||
await expect(page.getByText("Disconnect Code Interpreter")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("All running sessions connected to")
|
||||
).toBeVisible();
|
||||
|
||||
// Click the danger Disconnect button in the modal
|
||||
const modal = page.getByRole("dialog");
|
||||
await modal.getByRole("button", { name: "Disconnect" }).click();
|
||||
|
||||
// Modal should close after successful disconnect
|
||||
await expect(page.getByText("Disconnect Code Interpreter")).not.toBeVisible(
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
test("disconnect modal can be closed without disconnecting", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: true, healthy: true });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByText("Connected")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Open modal
|
||||
await getDisconnectIconButton(page).click();
|
||||
await expect(page.getByText("Disconnect Code Interpreter")).toBeVisible();
|
||||
|
||||
// Close modal via Cancel button
|
||||
const modal = page.getByRole("dialog");
|
||||
await modal.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Modal should be gone, page still shows Connected
|
||||
await expect(
|
||||
page.getByText("Disconnect Code Interpreter")
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText("Connected")).toBeVisible();
|
||||
});
|
||||
|
||||
test("reconnect flow sends PUT with enabled=true", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, { enabled: false, healthy: false });
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Intercept the PUT and verify the payload
|
||||
const putPromise = page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().includes("/api/admin/code-interpreter") &&
|
||||
req.method() === "PUT"
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Reconnect" }).click();
|
||||
|
||||
const putReq = await putPromise;
|
||||
expect(putReq.postDataJSON()).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
test("shows Checking... while reconnect is in progress", async ({ page }) => {
|
||||
// Use a single route handler that delays PUT responses
|
||||
await page.route(API_HEALTH_URL, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ healthy: false }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(API_STATUS_URL, async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(null),
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Reconnect" }).click();
|
||||
|
||||
// Should show Checking... while the request is in flight
|
||||
await expect(page.getByText("Checking...")).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test("shows error toast when disconnect fails", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, {
|
||||
enabled: true,
|
||||
healthy: true,
|
||||
putStatus: 500,
|
||||
});
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByText("Connected")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Open modal and click disconnect
|
||||
await getDisconnectIconButton(page).click();
|
||||
const modal = page.getByRole("dialog");
|
||||
await modal.getByRole("button", { name: "Disconnect" }).click();
|
||||
|
||||
// Error toast should appear
|
||||
await expect(
|
||||
page.getByText("Failed to disconnect Code Interpreter")
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("shows error toast when reconnect fails", async ({ page }) => {
|
||||
await mockCodeInterpreterApi(page, {
|
||||
enabled: false,
|
||||
healthy: false,
|
||||
putStatus: 500,
|
||||
});
|
||||
await page.goto(CODE_INTERPRETER_URL);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Reconnect" }).click();
|
||||
|
||||
// Error toast should appear
|
||||
await expect(
|
||||
page.getByText("Failed to reconnect Code Interpreter")
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Reconnect button should reappear (not stuck in Checking...)
|
||||
await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,7 +46,7 @@ test.skip("User changes password and logs in with new password", async ({
|
||||
|
||||
// Verify successful login
|
||||
await expect(page).toHaveURL("http://localhost:3000/app");
|
||||
await expect(page.getByText("Explore Assistants")).toBeVisible();
|
||||
await expect(page.getByText("Explore Agents")).toBeVisible();
|
||||
});
|
||||
|
||||
test.use({ storageState: "admin2_auth.json" });
|
||||
@@ -115,5 +115,5 @@ test.skip("Admin resets own password and logs in with new password", async ({
|
||||
|
||||
// Verify successful login
|
||||
await expect(page).toHaveURL("http://localhost:3000/app");
|
||||
await expect(page.getByText("Explore Assistants")).toBeVisible();
|
||||
await expect(page.getByText("Explore Agents")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ import { OnyxApiClient } from "@tests/e2e/utils/onyxApiClient";
|
||||
|
||||
// Tool-related test selectors now imported from shared utils
|
||||
|
||||
test.describe("Default Assistant Tests", () => {
|
||||
test.describe("Default Agent Tests", () => {
|
||||
let imageGenConfigId: string | null = null;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
@@ -69,7 +69,7 @@ test.describe("Default Assistant Tests", () => {
|
||||
});
|
||||
|
||||
test.describe("Greeting Message Display", () => {
|
||||
test("should display greeting message when opening new chat with default assistant", async ({
|
||||
test("should display greeting message when opening new chat with default agent", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Look for greeting message - should be one from the predefined list
|
||||
@@ -95,23 +95,21 @@ test.describe("Default Assistant Tests", () => {
|
||||
expect(GREETING_MESSAGES).toContain(greetingAfterReload?.trim());
|
||||
});
|
||||
|
||||
test("greeting should only appear for default assistant", async ({
|
||||
page,
|
||||
}) => {
|
||||
// First verify greeting appears for default assistant
|
||||
test("greeting should only appear for default agent", async ({ page }) => {
|
||||
// First verify greeting appears for default agent
|
||||
const greetingElement = await page.waitForSelector(
|
||||
'[data-testid="onyx-logo"]',
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(greetingElement).toBeTruthy();
|
||||
|
||||
// Create a custom assistant to test non-default behavior
|
||||
// Create a custom agent to test non-default behavior
|
||||
await page.getByTestId("AppSidebar/more-agents").click();
|
||||
await page.getByLabel("AgentsPage/new-agent-button").click();
|
||||
await page
|
||||
.locator('input[name="name"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
await page.locator('input[name="name"]').fill("Custom Test Assistant");
|
||||
await page.locator('input[name="name"]').fill("Custom Test Agent");
|
||||
await page
|
||||
.locator('textarea[name="description"]')
|
||||
.fill("Test Description");
|
||||
@@ -120,17 +118,17 @@ test.describe("Default Assistant Tests", () => {
|
||||
.fill("Test Instructions");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
// Wait for assistant to be created and selected
|
||||
await verifyAssistantIsChosen(page, "Custom Test Assistant");
|
||||
// Wait for agent to be created and selected
|
||||
await verifyAssistantIsChosen(page, "Custom Test Agent");
|
||||
|
||||
// Greeting should NOT appear for custom assistant
|
||||
// Greeting should NOT appear for custom agent
|
||||
const customGreeting = await page.$('[data-testid="onyx-logo"]');
|
||||
expect(customGreeting).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Default Assistant Branding", () => {
|
||||
test("should display Onyx logo for default assistant", async ({ page }) => {
|
||||
test.describe("Default Agent Branding", () => {
|
||||
test("should display Onyx logo for default agent", async ({ page }) => {
|
||||
// Look for Onyx logo
|
||||
const logoElement = await page.waitForSelector(
|
||||
'[data-testid="onyx-logo"]',
|
||||
@@ -138,23 +136,23 @@ test.describe("Default Assistant Tests", () => {
|
||||
);
|
||||
expect(logoElement).toBeTruthy();
|
||||
|
||||
// Should NOT show assistant name for default assistant
|
||||
// Should NOT show agent name for default agent
|
||||
const assistantNameElement = await page.$(
|
||||
'[data-testid="assistant-name-display"]'
|
||||
);
|
||||
expect(assistantNameElement).toBeNull();
|
||||
});
|
||||
|
||||
test("custom assistants should show name and icon instead of logo", async ({
|
||||
test("custom agents should show name and icon instead of logo", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create a custom assistant
|
||||
// Create a custom agent
|
||||
await page.getByTestId("AppSidebar/more-agents").click();
|
||||
await page.getByLabel("AgentsPage/new-agent-button").click();
|
||||
await page
|
||||
.locator('input[name="name"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
await page.locator('input[name="name"]').fill("Custom Assistant");
|
||||
await page.locator('input[name="name"]').fill("Custom Agent");
|
||||
await page
|
||||
.locator('textarea[name="description"]')
|
||||
.fill("Test Description");
|
||||
@@ -163,16 +161,16 @@ test.describe("Default Assistant Tests", () => {
|
||||
.fill("Test Instructions");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
// Wait for assistant to be created and selected
|
||||
await verifyAssistantIsChosen(page, "Custom Assistant");
|
||||
// Wait for agent to be created and selected
|
||||
await verifyAssistantIsChosen(page, "Custom Agent");
|
||||
|
||||
// Should show assistant name and icon, not Onyx logo
|
||||
// Should show agent name and icon, not Onyx logo
|
||||
const assistantNameElement = await page.waitForSelector(
|
||||
'[data-testid="assistant-name-display"]',
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
const nameText = await assistantNameElement.textContent();
|
||||
expect(nameText).toContain("Custom Assistant");
|
||||
expect(nameText).toContain("Custom Agent");
|
||||
|
||||
// Onyx logo should NOT be shown
|
||||
const logoElement = await page.$('[data-testid="onyx-logo"]');
|
||||
@@ -181,10 +179,8 @@ test.describe("Default Assistant Tests", () => {
|
||||
});
|
||||
|
||||
test.describe("Starter Messages", () => {
|
||||
test("default assistant should NOT have starter messages", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Check that starter messages container does not exist for default assistant
|
||||
test("default agent should NOT have starter messages", async ({ page }) => {
|
||||
// Check that starter messages container does not exist for default agent
|
||||
const starterMessagesContainer = await page.$(
|
||||
'[data-testid="starter-messages"]'
|
||||
);
|
||||
@@ -195,18 +191,14 @@ test.describe("Default Assistant Tests", () => {
|
||||
expect(starterButtons.length).toBe(0);
|
||||
});
|
||||
|
||||
test("custom assistants should display starter messages", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create a custom assistant with starter messages
|
||||
test("custom agents should display starter messages", async ({ page }) => {
|
||||
// Create a custom agent with starter messages
|
||||
await page.getByTestId("AppSidebar/more-agents").click();
|
||||
await page.getByLabel("AgentsPage/new-agent-button").click();
|
||||
await page
|
||||
.locator('input[name="name"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
await page
|
||||
.locator('input[name="name"]')
|
||||
.fill("Test Assistant with Starters");
|
||||
await page.locator('input[name="name"]').fill("Test Agent with Starters");
|
||||
await page
|
||||
.locator('textarea[name="description"]')
|
||||
.fill("Test Description");
|
||||
@@ -219,9 +211,9 @@ test.describe("Default Assistant Tests", () => {
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
// Wait for assistant to be created and selected
|
||||
await verifyAssistantIsChosen(page, "Test Assistant with Starters");
|
||||
await verifyAssistantIsChosen(page, "Test Agent with Starters");
|
||||
|
||||
// Starter messages container might exist but be empty for custom assistants
|
||||
// Starter messages container might exist but be empty for custom agents
|
||||
const starterMessagesContainer = await page.$(
|
||||
'[data-testid="starter-messages"]'
|
||||
);
|
||||
@@ -230,24 +222,22 @@ test.describe("Default Assistant Tests", () => {
|
||||
const starterButtons = await page.$$(
|
||||
'[data-testid^="starter-message-"]'
|
||||
);
|
||||
// Custom assistant without configured starter messages should have none
|
||||
// Custom agent without configured starter messages should have none
|
||||
expect(starterButtons.length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Assistant Selection", () => {
|
||||
test("default assistant should be selected for new chats", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Verify the input placeholder indicates default assistant (Onyx)
|
||||
test.describe("Agent Selection", () => {
|
||||
test("default agent should be selected for new chats", async ({ page }) => {
|
||||
// Verify the input placeholder indicates default agent (Onyx)
|
||||
await verifyDefaultAssistantIsChosen(page);
|
||||
});
|
||||
|
||||
test("default assistant should NOT appear in assistant selector", async ({
|
||||
test("default agent should NOT appear in agent selector", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Open assistant selector
|
||||
// Open agent selector
|
||||
await page.getByTestId("AppSidebar/more-agents").click();
|
||||
|
||||
// Wait for modal or assistant list to appear
|
||||
@@ -256,13 +246,13 @@ test.describe("Default Assistant Tests", () => {
|
||||
.getByLabel("AgentsPage/new-agent-button")
|
||||
.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
// Look for default assistant by name - it should NOT be there
|
||||
// Look for default agent by name - it should NOT be there
|
||||
const assistantElements = await page.$$('[data-testid^="assistant-"]');
|
||||
const assistantTexts = await Promise.all(
|
||||
assistantElements.map((el) => el.textContent())
|
||||
);
|
||||
|
||||
// Check that "Assistant" (the default assistant name) is not in the list
|
||||
// Check that the default agent is not in the list
|
||||
const hasDefaultAssistant = assistantTexts.some(
|
||||
(text) =>
|
||||
text?.includes("Assistant") &&
|
||||
@@ -275,16 +265,16 @@ test.describe("Default Assistant Tests", () => {
|
||||
await page.keyboard.press("Escape");
|
||||
});
|
||||
|
||||
test("should be able to switch from default to custom assistant", async ({
|
||||
test("should be able to switch from default to custom agent", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create a custom assistant
|
||||
// Create a custom agent
|
||||
await page.getByTestId("AppSidebar/more-agents").click();
|
||||
await page.getByLabel("AgentsPage/new-agent-button").click();
|
||||
await page
|
||||
.locator('input[name="name"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
await page.locator('input[name="name"]').fill("Switch Test Assistant");
|
||||
await page.locator('input[name="name"]').fill("Switch Test Agent");
|
||||
await page
|
||||
.locator('textarea[name="description"]')
|
||||
.fill("Test Description");
|
||||
@@ -293,13 +283,13 @@ test.describe("Default Assistant Tests", () => {
|
||||
.fill("Test Instructions");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
// Verify switched to custom assistant
|
||||
await verifyAssistantIsChosen(page, "Switch Test Assistant");
|
||||
// Verify switched to custom agent
|
||||
await verifyAssistantIsChosen(page, "Switch Test Agent");
|
||||
|
||||
// Start new chat to go back to default
|
||||
await startNewChat(page);
|
||||
|
||||
// Should be back to default assistant
|
||||
// Should be back to default agent
|
||||
await verifyDefaultAssistantIsChosen(page);
|
||||
});
|
||||
});
|
||||
@@ -379,7 +369,7 @@ test.describe("Default Assistant Tests", () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Enable the tools in default assistant config via API
|
||||
// Enable the tools in default agent config via API
|
||||
// Get current tools to find their IDs
|
||||
const toolsListResp = await page.request.get(
|
||||
"http://localhost:3000/api/tool"
|
||||
@@ -542,7 +532,7 @@ test.describe("Default Assistant Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("End-to-End Default Assistant Flow", () => {
|
||||
test.describe("End-to-End Default Agent Flow", () => {
|
||||
let imageGenConfigId: string | null = null;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
@@ -584,7 +574,7 @@ test.describe("End-to-End Default Assistant Flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("complete user journey with default assistant", async ({ page }) => {
|
||||
test("complete user journey with default agent", async ({ page }) => {
|
||||
// Clear cookies and log in as a random user
|
||||
await page.context().clearCookies();
|
||||
await loginAsRandomUser(page);
|
||||
@@ -611,7 +601,7 @@ test.describe("End-to-End Default Assistant Flow", () => {
|
||||
// Start a new chat
|
||||
await startNewChat(page);
|
||||
|
||||
// Verify we're back to default assistant with greeting
|
||||
// Verify we're back to default agent with greeting
|
||||
await expect(page.locator('[data-testid="onyx-logo"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user