Merge branch 'main' into chroma

This commit is contained in:
Bwook (Byoungwook) Kim 2025-08-20 18:10:21 +09:00 committed by GitHub
commit f23ea0445a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 2354 additions and 342 deletions

2
.github/TRIAGERS.md vendored
View file

@ -1,2 +1,2 @@
# This file documents Triage members in the Llama Stack community
@bbrowning @franciscojavierarceo @leseb
@franciscojavierarceo

View file

@ -36,7 +36,7 @@ runs:
- name: Run Integration Tests
shell: bash
run: |
./scripts/integration-tests.sh \
uv run --no-sync ./scripts/integration-tests.sh \
--stack-config '${{ inputs.stack-config }}' \
--provider '${{ inputs.provider }}' \
--test-subdirs '${{ inputs.test-subdirs }}' \

View file

@ -16,14 +16,16 @@ runs:
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
with:
python-version: ${{ inputs.python-version }}
activate-environment: true
version: 0.7.6
- name: Install dependencies
shell: bash
run: |
echo "Updating project dependencies via uv sync"
uv sync --all-groups
uv pip install ollama faiss-cpu
echo "Installing ad-hoc dependencies"
uv pip install faiss-cpu
# Install llama-stack-client-python based on the client-version input
if [ "${{ inputs.client-version }}" = "latest" ]; then
@ -37,4 +39,5 @@ runs:
exit 1
fi
uv pip install -e .
echo "Installed llama packages"
uv pip list | grep llama

View file

@ -42,7 +42,22 @@ runs:
- name: Build Llama Stack
shell: bash
run: |
uv run llama stack build --template ci-tests --image-type venv
# Install llama-stack-client-python based on the client-version input
if [ "${{ inputs.client-version }}" = "latest" ]; then
echo "Installing latest llama-stack-client-python from main branch"
export LLAMA_STACK_CLIENT_DIR=git+https://github.com/llamastack/llama-stack-client-python.git@main
elif [ "${{ inputs.client-version }}" = "published" ]; then
echo "Installing published llama-stack-client-python from PyPI"
unset LLAMA_STACK_CLIENT_DIR
else
echo "Invalid client-version: ${{ inputs.client-version }}"
exit 1
fi
echo "Building Llama Stack"
LLAMA_STACK_DIR=. \
uv run --no-sync llama stack build --template ci-tests --image-type venv
- name: Configure git for commits
shell: bash

View file

@ -18,5 +18,6 @@ Llama Stack uses GitHub Actions for Continuous Integration (CI). Below is a tabl
| Close stale issues and PRs | [stale_bot.yml](stale_bot.yml) | Run the Stale Bot action |
| Test External Providers Installed via Module | [test-external-provider-module.yml](test-external-provider-module.yml) | Test External Provider installation via Python module |
| Test External API and Providers | [test-external.yml](test-external.yml) | Test the External API and Provider mechanisms |
| UI Tests | [ui-unit-tests.yml](ui-unit-tests.yml) | Run the UI test suite |
| Unit Tests | [unit-tests.yml](unit-tests.yml) | Run the unit test suite |
| Update ReadTheDocs | [update-readthedocs.yml](update-readthedocs.yml) | Update the Llama Stack ReadTheDocs site |

View file

@ -30,7 +30,8 @@ jobs:
- name: Build a single provider
run: |
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --template starter --image-type container --image-name test
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync \
llama stack build --template starter --image-type container --image-name test
- name: Run installer end-to-end
run: |

View file

@ -10,6 +10,7 @@ on:
paths:
- 'distributions/**'
- 'llama_stack/**'
- '!llama_stack/ui/**'
- 'tests/integration/**'
- 'uv.lock'
- 'pyproject.toml'

View file

@ -10,6 +10,7 @@ on:
types: [opened, synchronize, reopened]
paths:
- 'llama_stack/**'
- '!llama_stack/ui/**'
- 'tests/**'
- 'uv.lock'
- 'pyproject.toml'

View file

@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- 'llama_stack/**'
- '!llama_stack/ui/**'
- 'tests/integration/vector_io/**'
- 'uv.lock'
- 'pyproject.toml'
@ -143,7 +144,7 @@ jobs:
- name: Build Llama Stack
run: |
uv run llama stack build --template ci-tests --image-type venv
uv run --no-sync llama stack build --template ci-tests --image-type venv
- name: Check Storage and Memory Available Before Tests
if: ${{ always() }}
@ -166,7 +167,8 @@ jobs:
ENABLE_WEAVIATE: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'true' || '' }}
WEAVIATE_CLUSTER_URL: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'localhost:8080' || '' }}
run: |
uv run pytest -sv --stack-config="files=inline::localfs,inference=inline::sentence-transformers,vector_io=${{ matrix.vector-io-provider }}" \
uv run --no-sync \
pytest -sv --stack-config="files=inline::localfs,inference=inline::sentence-transformers,vector_io=${{ matrix.vector-io-provider }}" \
tests/integration/vector_io \
--embedding-model inline::sentence-transformers/all-MiniLM-L6-v2

View file

@ -36,6 +36,17 @@ jobs:
**/requirements*.txt
.pre-commit-config.yaml
- name: Set up Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'llama_stack/ui/'
- name: Install npm dependencies
run: npm ci
working-directory: llama_stack/ui
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
continue-on-error: true
env:

View file

@ -9,6 +9,8 @@ on:
pull_request:
branches:
- main
paths-ignore:
- 'llama_stack/ui/**'
jobs:
build:

View file

@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- 'llama_stack/**'
- '!llama_stack/ui/**'
- 'tests/integration/**'
- 'uv.lock'
- 'pyproject.toml'
@ -43,11 +44,11 @@ jobs:
- name: Print distro dependencies
run: |
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --config tests/external/build.yaml --print-deps-only
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml --print-deps-only
- name: Build distro from config file
run: |
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --config tests/external/build.yaml
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml
- name: Start Llama Stack server in background
if: ${{ matrix.image-type }} == 'venv'

55
.github/workflows/ui-unit-tests.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: UI Tests
run-name: Run the UI test suite
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
paths:
- 'llama_stack/ui/**'
- '.github/workflows/ui-unit-tests.yml' # This workflow
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ui-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [22]
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: 'llama_stack/ui/package-lock.json'
- name: Install dependencies
working-directory: llama_stack/ui
run: npm ci
- name: Run linting
working-directory: llama_stack/ui
run: npm run lint
- name: Run format check
working-directory: llama_stack/ui
run: npm run format:check
- name: Run unit tests
working-directory: llama_stack/ui
env:
CI: true
run: npm test -- --coverage --watchAll=false --passWithNoTests

View file

@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- 'llama_stack/**'
- '!llama_stack/ui/**'
- 'tests/unit/**'
- 'uv.lock'
- 'pyproject.toml'

View file

@ -148,7 +148,7 @@ repos:
files: ^.github/workflows/.*$
- id: ui-prettier
name: Format UI code with Prettier
entry: bash -c 'cd llama_stack/ui && npm run format'
entry: bash -c 'cd llama_stack/ui && npm ci && npm run format'
language: system
files: ^llama_stack/ui/.*\.(ts|tsx)$
pass_filenames: false

View file

@ -23,12 +23,7 @@ new_vector_database
```{include} ../../../tests/README.md
```
## Benchmarking
```{include} ../../../docs/source/distributions/k8s-benchmark/README.md
```
### Advanced Topics
## Advanced Topics
For developers who need deeper understanding of the testing system internals:
@ -37,3 +32,8 @@ For developers who need deeper understanding of the testing system internals:
testing/record-replay
```
### Benchmarking
```{include} ../../../docs/source/distributions/k8s-benchmark/README.md
```

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#

View file

@ -1,207 +0,0 @@
#!/bin/bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
LLAMA_STACK_DIR=${LLAMA_STACK_DIR:-}
LLAMA_STACK_CLIENT_DIR=${LLAMA_STACK_CLIENT_DIR:-}
TEST_PYPI_VERSION=${TEST_PYPI_VERSION:-}
PYPI_VERSION=${PYPI_VERSION:-}
# This timeout (in seconds) is necessary when installing PyTorch via uv since it's likely to time out
# Reference: https://github.com/astral-sh/uv/pull/1694
UV_HTTP_TIMEOUT=${UV_HTTP_TIMEOUT:-500}
set -euo pipefail
# Define color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
source "$SCRIPT_DIR/common.sh"
# Usage function
usage() {
echo "Usage: $0 --env-name <conda_env_name> --build-file-path <build_file_path> --normal-deps <pip_dependencies> [--external-provider-deps <external_provider_deps>] [--optional-deps <special_pip_deps>]"
echo "Example: $0 --env-name my-conda-env --build-file-path ./my-stack-build.yaml --normal-deps 'numpy pandas scipy' --external-provider-deps 'foo' --optional-deps 'bar'"
exit 1
}
# Parse arguments
env_name=""
build_file_path=""
normal_deps=""
external_provider_deps=""
optional_deps=""
while [[ $# -gt 0 ]]; do
key="$1"
case "$key" in
--env-name)
if [[ -z "$2" || "$2" == --* ]]; then
echo "Error: --env-name requires a string value" >&2
usage
fi
env_name="$2"
shift 2
;;
--build-file-path)
if [[ -z "$2" || "$2" == --* ]]; then
echo "Error: --build-file-path requires a string value" >&2
usage
fi
build_file_path="$2"
shift 2
;;
--normal-deps)
if [[ -z "$2" || "$2" == --* ]]; then
echo "Error: --normal-deps requires a string value" >&2
usage
fi
normal_deps="$2"
shift 2
;;
--external-provider-deps)
if [[ -z "$2" || "$2" == --* ]]; then
echo "Error: --external-provider-deps requires a string value" >&2
usage
fi
external_provider_deps="$2"
shift 2
;;
--optional-deps)
if [[ -z "$2" || "$2" == --* ]]; then
echo "Error: --optional-deps requires a string value" >&2
usage
fi
optional_deps="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
# Check required arguments
if [[ -z "$env_name" || -z "$build_file_path" || -z "$normal_deps" ]]; then
echo "Error: --env-name, --build-file-path, and --normal-deps are required." >&2
usage
fi
if [ -n "$LLAMA_STACK_DIR" ]; then
echo "Using llama-stack-dir=$LLAMA_STACK_DIR"
fi
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
echo "Using llama-stack-client-dir=$LLAMA_STACK_CLIENT_DIR"
fi
ensure_conda_env_python310() {
# Use only global variables set by flag parser
local python_version="3.12"
if ! is_command_available conda; then
printf "${RED}Error: conda command not found. Is Conda installed and in your PATH?${NC}" >&2
exit 1
fi
if conda env list | grep -q "^${env_name} "; then
printf "Conda environment '${env_name}' exists. Checking Python version...\n"
current_version=$(conda run -n "${env_name}" python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
if [ "$current_version" = "$python_version" ]; then
printf "Environment '${env_name}' already has Python ${python_version}. No action needed.\n"
else
printf "Updating environment '${env_name}' to Python ${python_version}...\n"
conda install -n "${env_name}" python="${python_version}" -y
fi
else
printf "Conda environment '${env_name}' does not exist. Creating with Python ${python_version}...\n"
conda create -n "${env_name}" python="${python_version}" -y
fi
eval "$(conda shell.bash hook)"
conda deactivate && conda activate "${env_name}"
"$CONDA_PREFIX"/bin/pip install uv
if [ -n "$TEST_PYPI_VERSION" ]; then
uv pip install fastapi libcst
uv pip install --extra-index-url https://test.pypi.org/simple/ \
llama-stack=="$TEST_PYPI_VERSION" \
"$normal_deps"
if [ -n "$optional_deps" ]; then
IFS='#' read -ra parts <<<"$optional_deps"
for part in "${parts[@]}"; do
echo "$part"
uv pip install $part
done
fi
if [ -n "$external_provider_deps" ]; then
IFS='#' read -ra parts <<<"$external_provider_deps"
for part in "${parts[@]}"; do
echo "$part"
uv pip install "$part"
done
fi
else
if [ -n "$LLAMA_STACK_DIR" ]; then
if [ ! -d "$LLAMA_STACK_DIR" ]; then
printf "${RED}Warning: LLAMA_STACK_DIR is set but directory does not exist: $LLAMA_STACK_DIR${NC}\n" >&2
exit 1
fi
printf "Installing from LLAMA_STACK_DIR: $LLAMA_STACK_DIR\n"
uv pip install --no-cache-dir -e "$LLAMA_STACK_DIR"
else
PYPI_VERSION="${PYPI_VERSION:-}"
if [ -n "$PYPI_VERSION" ]; then
SPEC_VERSION="llama-stack==${PYPI_VERSION}"
else
SPEC_VERSION="llama-stack"
fi
uv pip install --no-cache-dir "$SPEC_VERSION"
fi
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ]; then
printf "${RED}Warning: LLAMA_STACK_CLIENT_DIR is set but directory does not exist: $LLAMA_STACK_CLIENT_DIR${NC}\n" >&2
exit 1
fi
printf "Installing from LLAMA_STACK_CLIENT_DIR: $LLAMA_STACK_CLIENT_DIR\n"
uv pip install --no-cache-dir -e "$LLAMA_STACK_CLIENT_DIR"
fi
printf "Installing pip dependencies\n"
uv pip install $normal_deps
if [ -n "$optional_deps" ]; then
IFS='#' read -ra parts <<<"$optional_deps"
for part in "${parts[@]}"; do
echo "$part"
uv pip install $part
done
fi
if [ -n "$external_provider_deps" ]; then
IFS='#' read -ra parts <<<"$external_provider_deps"
for part in "${parts[@]}"; do
echo "Getting provider spec for module: $part and installing dependencies"
package_name=$(echo "$part" | sed 's/[<>=!].*//')
python3 -c "
import importlib
import sys
try:
module = importlib.import_module(f'$package_name.provider')
spec = module.get_provider_spec()
if hasattr(spec, 'pip_packages') and spec.pip_packages:
print('\\n'.join(spec.pip_packages))
except Exception as e:
print(f'Error getting provider spec for $package_name: {e}', file=sys.stderr)
" | uv pip install -r -
done
fi
fi
mv "$build_file_path" "$CONDA_PREFIX"/llamastack-build.yaml
echo "Build spec configuration saved at $CONDA_PREFIX/llamastack-build.yaml"
}
ensure_conda_env_python310 "$env_name" "$build_file_path" "$normal_deps" "$optional_deps" "$external_provider_deps"

View file

@ -151,23 +151,37 @@ run() {
fi
else
if [ -n "$LLAMA_STACK_DIR" ]; then
if [ ! -d "$LLAMA_STACK_DIR" ]; then
# only warn if DIR does not start with "git+"
if [ ! -d "$LLAMA_STACK_DIR" ] && [[ "$LLAMA_STACK_DIR" != git+* ]]; then
printf "${RED}Warning: LLAMA_STACK_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_DIR" >&2
exit 1
fi
printf "Installing from LLAMA_STACK_DIR: %s\n" "$LLAMA_STACK_DIR"
uv pip install --no-cache-dir -e "$LLAMA_STACK_DIR"
# editable only if LLAMA_STACK_DIR does not start with "git+"
if [[ "$LLAMA_STACK_DIR" != git+* ]]; then
EDITABLE="-e"
else
EDITABLE=""
fi
uv pip install --no-cache-dir $EDITABLE "$LLAMA_STACK_DIR"
else
uv pip install --no-cache-dir llama-stack
fi
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ]; then
# only warn if DIR does not start with "git+"
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ] && [[ "$LLAMA_STACK_CLIENT_DIR" != git+* ]]; then
printf "${RED}Warning: LLAMA_STACK_CLIENT_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_CLIENT_DIR" >&2
exit 1
fi
printf "Installing from LLAMA_STACK_CLIENT_DIR: %s\n" "$LLAMA_STACK_CLIENT_DIR"
uv pip install --no-cache-dir -e "$LLAMA_STACK_CLIENT_DIR"
# editable only if LLAMA_STACK_CLIENT_DIR does not start with "git+"
if [[ "$LLAMA_STACK_CLIENT_DIR" != git+* ]]; then
EDITABLE="-e"
else
EDITABLE=""
fi
uv pip install --no-cache-dir $EDITABLE "$LLAMA_STACK_CLIENT_DIR"
fi
printf "Installing pip dependencies\n"

View file

@ -6,9 +6,7 @@
from typing import Any
from llama_stack.apis.inference import (
Message,
)
from llama_stack.apis.inference import Message
from llama_stack.apis.safety import RunShieldResponse, Safety
from llama_stack.apis.safety.safety import ModerationObject
from llama_stack.apis.shields import Shield
@ -68,6 +66,7 @@ class SafetyRouter(Safety):
list_shields_response = await self.routing_table.list_shields()
matches = [s.identifier for s in list_shields_response.data if model == s.provider_resource_id]
if not matches:
raise ValueError(f"No shield associated with provider_resource id {model}")
if len(matches) > 1:

View file

@ -28,6 +28,7 @@ distribution_spec:
- provider_type: inline::localfs
safety:
- provider_type: inline::llama-guard
- provider_type: inline::code-scanner
agents:
- provider_type: inline::meta-reference
telemetry:

View file

@ -135,6 +135,8 @@ providers:
provider_type: inline::llama-guard
config:
excluded_categories: []
- provider_id: code-scanner
provider_type: inline::code-scanner
agents:
- provider_id: meta-reference
provider_type: inline::meta-reference
@ -223,6 +225,9 @@ shields:
- shield_id: llama-guard
provider_id: ${env.SAFETY_MODEL:+llama-guard}
provider_shield_id: ${env.SAFETY_MODEL:=}
- shield_id: code-scanner
provider_id: ${env.CODE_SCANNER_MODEL:+code-scanner}
provider_shield_id: ${env.CODE_SCANNER_MODEL:=}
vector_dbs: []
datasets: []
scoring_fns: []

View file

@ -28,6 +28,7 @@ distribution_spec:
- provider_type: inline::localfs
safety:
- provider_type: inline::llama-guard
- provider_type: inline::code-scanner
agents:
- provider_type: inline::meta-reference
telemetry:

View file

@ -135,6 +135,8 @@ providers:
provider_type: inline::llama-guard
config:
excluded_categories: []
- provider_id: code-scanner
provider_type: inline::code-scanner
agents:
- provider_id: meta-reference
provider_type: inline::meta-reference
@ -223,6 +225,9 @@ shields:
- shield_id: llama-guard
provider_id: ${env.SAFETY_MODEL:+llama-guard}
provider_shield_id: ${env.SAFETY_MODEL:=}
- shield_id: code-scanner
provider_id: ${env.CODE_SCANNER_MODEL:+code-scanner}
provider_shield_id: ${env.CODE_SCANNER_MODEL:=}
vector_dbs: []
datasets: []
scoring_fns: []

View file

@ -15,19 +15,14 @@ from llama_stack.core.datatypes import (
ToolGroupInput,
)
from llama_stack.core.utils.dynamic import instantiate_class_type
from llama_stack.distributions.template import (
DistributionTemplate,
RunConfigSettings,
)
from llama_stack.distributions.template import DistributionTemplate, RunConfigSettings
from llama_stack.providers.datatypes import RemoteProviderSpec
from llama_stack.providers.inline.files.localfs.config import LocalfsFilesImplConfig
from llama_stack.providers.inline.inference.sentence_transformers import (
SentenceTransformersInferenceConfig,
)
from llama_stack.providers.inline.vector_io.faiss.config import FaissVectorIOConfig
from llama_stack.providers.inline.vector_io.milvus.config import (
MilvusVectorIOConfig,
)
from llama_stack.providers.inline.vector_io.milvus.config import MilvusVectorIOConfig
from llama_stack.providers.inline.vector_io.sqlite_vec.config import (
SQLiteVectorIOConfig,
)
@ -119,7 +114,10 @@ def get_distribution_template() -> DistributionTemplate:
BuildProvider(provider_type="remote::pgvector"),
],
"files": [BuildProvider(provider_type="inline::localfs")],
"safety": [BuildProvider(provider_type="inline::llama-guard")],
"safety": [
BuildProvider(provider_type="inline::llama-guard"),
BuildProvider(provider_type="inline::code-scanner"),
],
"agents": [BuildProvider(provider_type="inline::meta-reference")],
"telemetry": [BuildProvider(provider_type="inline::meta-reference")],
"post_training": [BuildProvider(provider_type="inline::huggingface")],
@ -170,6 +168,11 @@ def get_distribution_template() -> DistributionTemplate:
provider_id="${env.SAFETY_MODEL:+llama-guard}",
provider_shield_id="${env.SAFETY_MODEL:=}",
),
ShieldInput(
shield_id="code-scanner",
provider_id="${env.CODE_SCANNER_MODEL:+code-scanner}",
provider_shield_id="${env.CODE_SCANNER_MODEL:=}",
),
]
return DistributionTemplate(

View file

@ -7,13 +7,11 @@
import logging
import os
import re
import sys
from logging.config import dictConfig
from rich.console import Console
from rich.errors import MarkupError
from rich.logging import RichHandler
from termcolor import cprint
from llama_stack.core.datatypes import LoggingConfig
@ -66,7 +64,6 @@ def config_to_category_levels(category: str, level: str):
category_levels["root"] = level_value
elif category in CATEGORIES:
category_levels[category] = level_value
logging.info(f"Setting '{category}' category to level '{level}'.")
else:
logging.warning(f"Unknown logging category: {category}. No changes made.")
return category_levels
@ -256,7 +253,6 @@ def get_logger(
env_config = os.environ.get("LLAMA_STACK_LOGGING", "")
if env_config:
cprint(f"Environment variable LLAMA_STACK_LOGGING found: {env_config}", color="yellow", file=sys.stderr)
_category_levels.update(parse_environment_config(env_config))
log_file = os.environ.get("LLAMA_STACK_LOG_FILE")

View file

@ -5,7 +5,11 @@
# the root directory of this source tree.
import logging
from typing import Any
import uuid
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from codeshield.cs import CodeShieldScanResult
from llama_stack.apis.inference import Message
from llama_stack.apis.safety import (
@ -14,6 +18,7 @@ from llama_stack.apis.safety import (
SafetyViolation,
ViolationLevel,
)
from llama_stack.apis.safety.safety import ModerationObject, ModerationObjectResults
from llama_stack.apis.shields import Shield
from llama_stack.providers.utils.inference.prompt_adapter import (
interleaved_content_as_str,
@ -24,8 +29,8 @@ from .config import CodeScannerConfig
log = logging.getLogger(__name__)
ALLOWED_CODE_SCANNER_MODEL_IDS = [
"CodeScanner",
"CodeShield",
"code-scanner",
"code-shield",
]
@ -69,3 +74,55 @@ class MetaReferenceCodeScannerSafetyImpl(Safety):
metadata={"violation_type": ",".join([issue.pattern_id for issue in result.issues_found])},
)
return RunShieldResponse(violation=violation)
def get_moderation_object_results(self, scan_result: "CodeShieldScanResult") -> ModerationObjectResults:
categories = {}
category_scores = {}
category_applied_input_types = {}
flagged = scan_result.is_insecure
user_message = None
metadata = {}
if scan_result.is_insecure:
pattern_ids = [issue.pattern_id for issue in scan_result.issues_found]
categories = dict.fromkeys(pattern_ids, True)
category_scores = dict.fromkeys(pattern_ids, 1.0)
category_applied_input_types = {key: ["text"] for key in pattern_ids}
user_message = f"Security concerns detected in the code. {scan_result.recommended_treatment.name}: {', '.join([issue.description for issue in scan_result.issues_found])}"
metadata = {"violation_type": ",".join([issue.pattern_id for issue in scan_result.issues_found])}
return ModerationObjectResults(
flagged=flagged,
categories=categories,
category_scores=category_scores,
category_applied_input_types=category_applied_input_types,
user_message=user_message,
metadata=metadata,
)
async def run_moderation(self, input: str | list[str], model: str) -> ModerationObject:
inputs = input if isinstance(input, list) else [input]
results = []
from codeshield.cs import CodeShield
for text_input in inputs:
log.info(f"Running CodeScannerShield moderation on input: {text_input[:100]}...")
try:
scan_result = await CodeShield.scan_code(text_input)
moderation_result = self.get_moderation_object_results(scan_result)
except Exception as e:
log.error(f"CodeShield.scan_code failed: {e}")
# create safe fallback response on scanner failure to avoid blocking legitimate requests
moderation_result = ModerationObjectResults(
flagged=False,
categories={},
category_scores={},
category_applied_input_types={},
user_message=None,
metadata={"scanner_error": str(e)},
)
results.append(moderation_result)
return ModerationObject(id=str(uuid.uuid4()), model=model, results=results)

View file

@ -11,11 +11,7 @@ from string import Template
from typing import Any
from llama_stack.apis.common.content_types import ImageContentItem, TextContentItem
from llama_stack.apis.inference import (
Inference,
Message,
UserMessage,
)
from llama_stack.apis.inference import Inference, Message, UserMessage
from llama_stack.apis.safety import (
RunShieldResponse,
Safety,
@ -72,7 +68,6 @@ SAFETY_CATEGORIES_TO_CODE_MAP = {
}
SAFETY_CODE_TO_CATEGORIES_MAP = {v: k for k, v in SAFETY_CATEGORIES_TO_CODE_MAP.items()}
DEFAULT_LG_V3_SAFETY_CATEGORIES = [
CAT_VIOLENT_CRIMES,
CAT_NON_VIOLENT_CRIMES,
@ -460,7 +455,7 @@ class LlamaGuardShield:
def is_content_safe(self, response: str, unsafe_code: str | None = None) -> bool:
"""Check if content is safe based on response and unsafe code."""
if response.strip() == SAFE_RESPONSE:
if response.strip().lower().startswith(SAFE_RESPONSE):
return True
if unsafe_code:

View file

@ -413,15 +413,6 @@ class MilvusVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO, VectorDBsProtocolP
index = await self._get_and_cache_vector_db_index(vector_db_id)
if not index:
raise VectorStoreNotFoundError(vector_db_id)
if params and params.get("mode") == "keyword":
# Check if this is inline Milvus (Milvus-Lite)
if hasattr(self.config, "db_path"):
raise NotImplementedError(
"Keyword search is not supported in Milvus-Lite. "
"Please use a remote Milvus server for keyword search functionality."
)
return await index.query_chunks(query, params)
async def delete_chunks(self, store_id: str, chunks_for_deletion: list[ChunkForDeletion]) -> None:

View file

@ -31,9 +31,15 @@ from openai.types.chat import (
from openai.types.chat import (
ChatCompletionContentPartTextParam as OpenAIChatCompletionContentPartTextParam,
)
from openai.types.chat import (
try:
from openai.types.chat import (
ChatCompletionMessageFunctionToolCall as OpenAIChatCompletionMessageFunctionToolCall,
)
)
except ImportError:
from openai.types.chat.chat_completion_message_tool_call import (
ChatCompletionMessageToolCall as OpenAIChatCompletionMessageFunctionToolCall,
)
from openai.types.chat import (
ChatCompletionMessageParam as OpenAIChatCompletionMessage,
)

View file

@ -75,6 +75,8 @@ class PostgresKVStoreConfig(CommonConfig):
db: str = "llamastack"
user: str
password: str | None = None
ssl_mode: str | None = None
ca_cert_path: str | None = None
table_name: str = "llamastack_kvstore"
@classmethod

View file

@ -30,6 +30,8 @@ class PostgresKVStoreImpl(KVStore):
database=self.config.db,
user=self.config.user,
password=self.config.password,
sslmode=self.config.ssl_mode,
sslrootcert=self.config.ca_cert_path,
)
self.conn.autocommit = True
self.cursor = self.conn.cursor(cursor_factory=DictCursor)

View file

@ -261,7 +261,7 @@ async def _patched_inference_method(original_method, self, client_type, endpoint
else:
raise RuntimeError(
f"No recorded response found for request hash: {request_hash}\n"
f"Endpoint: {endpoint}\n"
f"Request: {method} {url} {body}\n"
f"Model: {body.get('model', 'unknown')}\n"
f"To record this response, run with LLAMA_STACK_INFERENCE_MODE=record"
)

View file

@ -0,0 +1,425 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import ContentDetailPage from "./page";
import { VectorStoreContentItem } from "@/lib/contents-api";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
const mockPush = jest.fn();
const mockParams = {
id: "vs_123",
fileId: "file_456",
contentId: "content_789",
};
jest.mock("next/navigation", () => ({
useParams: () => mockParams,
useRouter: () => ({
push: mockPush,
}),
}));
const mockClient = {
vectorStores: {
retrieve: jest.fn(),
files: {
retrieve: jest.fn(),
},
},
};
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
}));
const mockContentsAPI = {
listContents: jest.fn(),
updateContent: jest.fn(),
deleteContent: jest.fn(),
};
jest.mock("@/lib/contents-api", () => ({
ContentsAPI: jest.fn(() => mockContentsAPI),
}));
const originalConfirm = window.confirm;
describe("ContentDetailPage", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 5 },
usage_bytes: 1024,
metadata: {
provider_id: "test_provider",
},
};
const mockFile: VectorStoreFile = {
id: "file_456",
status: "completed",
created_at: 1710001000,
usage_bytes: 512,
chunking_strategy: { type: "fixed_size" },
};
const mockContent: VectorStoreContentItem = {
id: "content_789",
object: "vector_store.content",
content: "This is test content for the vector store.",
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
metadata: {
chunk_window: "0-45",
content_length: 45,
custom_field: "custom_value",
},
created_timestamp: 1710002000,
};
beforeEach(() => {
jest.clearAllMocks();
window.confirm = jest.fn();
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
mockContentsAPI.listContents.mockResolvedValue({
data: [mockContent],
});
});
afterEach(() => {
window.confirm = originalConfirm;
});
describe("Loading and Error States", () => {
test("renders loading skeleton while fetching data", () => {
mockClient.vectorStores.retrieve.mockImplementation(
() => new Promise(() => {})
);
const { container } = render(<ContentDetailPage />);
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders error message when API calls fail", async () => {
const error = new Error("Network error");
mockClient.vectorStores.retrieve.mockRejectedValue(error);
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText(/Error loading details for ID content_789/)
).toBeInTheDocument();
expect(screen.getByText(/Network error/)).toBeInTheDocument();
});
});
test("renders not found when content doesn't exist", async () => {
mockContentsAPI.listContents.mockResolvedValue({
data: [],
});
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText(/Content content_789 not found/)
).toBeInTheDocument();
});
});
});
describe("Content Display", () => {
test("renders content details correctly", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(screen.getByText("Content: content_789")).toBeInTheDocument();
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const contentIdTexts = screen.getAllByText("content_789");
expect(contentIdTexts.length).toBeGreaterThan(0);
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
const storeIdTexts = screen.getAllByText("vs_123");
expect(storeIdTexts.length).toBeGreaterThan(0);
expect(screen.getByText("vector_store.content")).toBeInTheDocument();
const positionTexts = screen.getAllByText("0-45");
expect(positionTexts.length).toBeGreaterThan(0);
});
test("renders embedding information when available", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText(/0.100000, 0.200000, 0.300000/)
).toBeInTheDocument();
});
});
test("handles content without embedding", async () => {
const contentWithoutEmbedding = {
...mockContent,
embedding: undefined,
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithoutEmbedding],
});
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("No embedding available for this content.")
).toBeInTheDocument();
});
});
test("renders metadata correctly", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(screen.getByText("chunk_window:")).toBeInTheDocument();
const positionTexts = screen.getAllByText("0-45");
expect(positionTexts.length).toBeGreaterThan(0);
expect(screen.getByText("content_length:")).toBeInTheDocument();
expect(screen.getByText("custom_field:")).toBeInTheDocument();
expect(screen.getByText("custom_value")).toBeInTheDocument();
});
});
});
describe("Edit Functionality", () => {
test("enables edit mode when edit button is clicked", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
const editButton = editButtons[0];
fireEvent.click(editButton);
expect(
screen.getByDisplayValue("This is test content for the vector store.")
).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Save/ })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Cancel/ })
).toBeInTheDocument();
});
test("cancels edit mode and resets content", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
const editButton = editButtons[0];
fireEvent.click(editButton);
const textarea = screen.getByDisplayValue(
"This is test content for the vector store."
);
fireEvent.change(textarea, { target: { value: "Modified content" } });
const cancelButton = screen.getByRole("button", { name: /Cancel/ });
fireEvent.click(cancelButton);
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
expect(
screen.queryByDisplayValue("Modified content")
).not.toBeInTheDocument();
});
test("saves content changes", async () => {
const updatedContent = { ...mockContent, content: "Updated content" };
mockContentsAPI.updateContent.mockResolvedValue(updatedContent);
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
const editButton = editButtons[0];
fireEvent.click(editButton);
const textarea = screen.getByDisplayValue(
"This is test content for the vector store."
);
fireEvent.change(textarea, { target: { value: "Updated content" } });
const saveButton = screen.getByRole("button", { name: /Save/ });
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockContentsAPI.updateContent).toHaveBeenCalledWith(
"vs_123",
"file_456",
"content_789",
{ content: "Updated content" }
);
});
});
});
describe("Delete Functionality", () => {
test("shows confirmation dialog before deleting", async () => {
window.confirm = jest.fn().mockReturnValue(false);
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const deleteButton = screen.getByRole("button", { name: /Delete/ });
fireEvent.click(deleteButton);
expect(window.confirm).toHaveBeenCalledWith(
"Are you sure you want to delete this content?"
);
expect(mockContentsAPI.deleteContent).not.toHaveBeenCalled();
});
test("deletes content when confirmed", async () => {
window.confirm = jest.fn().mockReturnValue(true);
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const deleteButton = screen.getByRole("button", { name: /Delete/ });
fireEvent.click(deleteButton);
await waitFor(() => {
expect(mockContentsAPI.deleteContent).toHaveBeenCalledWith(
"vs_123",
"file_456",
"content_789"
);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456/contents"
);
});
});
});
describe("Embedding Edit Functionality", () => {
test("enables embedding edit mode", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
expect(
screen.getByText("This is test content for the vector store.")
).toBeInTheDocument();
});
const embeddingEditButtons = screen.getAllByRole("button", {
name: /Edit/,
});
expect(embeddingEditButtons.length).toBeGreaterThanOrEqual(1);
});
test.skip("cancels embedding edit mode", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
// skip vector text check, just verify test completes
});
const embeddingEditButtons = screen.getAllByRole("button", {
name: /Edit/,
});
const embeddingEditButton = embeddingEditButtons[1];
fireEvent.click(embeddingEditButton);
const cancelButtons = screen.getAllByRole("button", { name: /Cancel/ });
expect(cancelButtons.length).toBeGreaterThan(0);
expect(
screen.queryByDisplayValue(/0.1,0.2,0.3,0.4,0.5/)
).not.toBeInTheDocument();
});
});
describe("Breadcrumb Navigation", () => {
test("renders correct breadcrumb structure", async () => {
render(<ContentDetailPage />);
await waitFor(() => {
const vectorStoreTexts = screen.getAllByText("Vector Stores");
expect(vectorStoreTexts.length).toBeGreaterThan(0);
const storeNameTexts = screen.getAllByText("Test Vector Store");
expect(storeNameTexts.length).toBeGreaterThan(0);
const contentsTexts = screen.getAllByText("Contents");
expect(contentsTexts.length).toBeGreaterThan(0);
});
});
});
describe("Content Utilities", () => {
test("handles different content types correctly", async () => {
const contentWithObjectType = {
...mockContent,
content: { type: "text", text: "Text object content" },
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithObjectType],
});
render(<ContentDetailPage />);
await waitFor(() => {
expect(screen.getByText("Text object content")).toBeInTheDocument();
});
});
test("handles string content type", async () => {
const contentWithStringType = {
...mockContent,
content: "Simple string content",
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithStringType],
});
render(<ContentDetailPage />);
await waitFor(() => {
expect(screen.getByText("Simple string content")).toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,481 @@
import React from "react";
import {
render,
screen,
fireEvent,
waitFor,
act,
} from "@testing-library/react";
import "@testing-library/jest-dom";
import ContentsListPage from "./page";
import { VectorStoreContentItem } from "@/lib/contents-api";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
const mockPush = jest.fn();
const mockParams = {
id: "vs_123",
fileId: "file_456",
};
jest.mock("next/navigation", () => ({
useParams: () => mockParams,
useRouter: () => ({
push: mockPush,
}),
}));
const mockClient = {
vectorStores: {
retrieve: jest.fn(),
files: {
retrieve: jest.fn(),
},
},
};
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
}));
const mockContentsAPI = {
listContents: jest.fn(),
deleteContent: jest.fn(),
};
jest.mock("@/lib/contents-api", () => ({
ContentsAPI: jest.fn(() => mockContentsAPI),
}));
describe("ContentsListPage", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 5 },
usage_bytes: 1024,
metadata: {
provider_id: "test_provider",
},
};
const mockFile: VectorStoreFile = {
id: "file_456",
status: "completed",
created_at: 1710001000,
usage_bytes: 512,
chunking_strategy: { type: "fixed_size" },
};
const mockContents: VectorStoreContentItem[] = [
{
id: "content_1",
object: "vector_store.content",
content: "First piece of content for testing.",
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
metadata: {
chunk_window: "0-35",
content_length: 35,
},
created_timestamp: 1710002000,
},
{
id: "content_2",
object: "vector_store.content",
content:
"Second piece of content with longer text for testing truncation and display.",
embedding: [0.6, 0.7, 0.8],
metadata: {
chunk_window: "36-95",
content_length: 85,
},
created_timestamp: 1710003000,
},
{
id: "content_3",
object: "vector_store.content",
content: "Third content without embedding.",
embedding: undefined,
metadata: {
content_length: 33,
},
created_timestamp: 1710004000,
},
];
beforeEach(() => {
jest.clearAllMocks();
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
mockContentsAPI.listContents.mockResolvedValue({
data: mockContents,
});
});
describe("Loading and Error States", () => {
test("renders loading skeleton while fetching store data", async () => {
mockClient.vectorStores.retrieve.mockImplementation(
() => new Promise(() => {})
);
await act(async () => {
render(<ContentsListPage />);
});
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders error message when store API call fails", async () => {
const error = new Error("Failed to load store");
mockClient.vectorStores.retrieve.mockRejectedValue(error);
await act(async () => {
render(<ContentsListPage />);
});
await waitFor(() => {
expect(
screen.getByText(/Error loading details for ID vs_123/)
).toBeInTheDocument();
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
});
});
test("renders not found when store doesn't exist", async () => {
mockClient.vectorStores.retrieve.mockResolvedValue(null);
await act(async () => {
render(<ContentsListPage />);
});
await waitFor(() => {
expect(
screen.getByText(/No details found for ID: vs_123/)
).toBeInTheDocument();
});
});
test("renders contents loading skeleton", async () => {
mockContentsAPI.listContents.mockImplementation(
() => new Promise(() => {})
);
const { container } = render(<ContentsListPage />);
await waitFor(() => {
expect(
screen.getByText("Contents in File: file_456")
).toBeInTheDocument();
});
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders contents error message", async () => {
const error = new Error("Failed to load contents");
mockContentsAPI.listContents.mockRejectedValue(error);
render(<ContentsListPage />);
await waitFor(() => {
expect(
screen.getByText("Error loading contents: Failed to load contents")
).toBeInTheDocument();
});
});
});
describe("Contents Table Display", () => {
test("renders contents table with correct headers", async () => {
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
expect(screen.getByText("Contents in this file")).toBeInTheDocument();
});
// Check table headers
expect(screen.getByText("Content ID")).toBeInTheDocument();
expect(screen.getByText("Content Preview")).toBeInTheDocument();
expect(screen.getByText("Embedding")).toBeInTheDocument();
expect(screen.getByText("Position")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
expect(screen.getByText("Actions")).toBeInTheDocument();
});
test("renders content data correctly", async () => {
render(<ContentsListPage />);
await waitFor(() => {
// Check first content row
expect(screen.getByText("content_1...")).toBeInTheDocument();
expect(
screen.getByText("First piece of content for testing.")
).toBeInTheDocument();
expect(
screen.getByText("[0.100, 0.200, 0.300...] (5D)")
).toBeInTheDocument();
expect(screen.getByText("0-35")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710002000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("content_2...")).toBeInTheDocument();
expect(
screen.getByText(/Second piece of content with longer text/)
).toBeInTheDocument();
expect(
screen.getByText("[0.600, 0.700, 0.800...] (3D)")
).toBeInTheDocument();
expect(screen.getByText("36-95")).toBeInTheDocument();
expect(screen.getByText("content_3...")).toBeInTheDocument();
expect(
screen.getByText("Third content without embedding.")
).toBeInTheDocument();
expect(screen.getByText("No embedding")).toBeInTheDocument();
expect(screen.getByText("33 chars")).toBeInTheDocument();
});
});
test("handles empty contents list", async () => {
mockContentsAPI.listContents.mockResolvedValue({
data: [],
});
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (0)")).toBeInTheDocument();
expect(
screen.getByText("No contents found for this file.")
).toBeInTheDocument();
});
});
test("truncates long content IDs", async () => {
const longIdContent = {
...mockContents[0],
id: "very_long_content_id_that_should_be_truncated_123456789",
};
mockContentsAPI.listContents.mockResolvedValue({
data: [longIdContent],
});
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("very_long_...")).toBeInTheDocument();
});
});
});
describe("Content Navigation", () => {
test("navigates to content detail when content ID is clicked", async () => {
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("content_1...")).toBeInTheDocument();
});
const contentLink = screen.getByRole("button", { name: "content_1..." });
fireEvent.click(contentLink);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
);
});
test("navigates to content detail when view button is clicked", async () => {
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
});
const viewButtons = screen.getAllByTitle("View content details");
fireEvent.click(viewButtons[0]);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
);
});
test("navigates to content detail when edit button is clicked", async () => {
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
});
const editButtons = screen.getAllByTitle("Edit content");
fireEvent.click(editButtons[0]);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
);
});
});
describe("Content Deletion", () => {
test("deletes content when delete button is clicked", async () => {
mockContentsAPI.deleteContent.mockResolvedValue(undefined);
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
});
const deleteButtons = screen.getAllByTitle("Delete content");
fireEvent.click(deleteButtons[0]);
await waitFor(() => {
expect(mockContentsAPI.deleteContent).toHaveBeenCalledWith(
"vs_123",
"file_456",
"content_1"
);
});
await waitFor(() => {
expect(screen.getByText("Content Chunks (2)")).toBeInTheDocument();
});
expect(screen.queryByText("content_1...")).not.toBeInTheDocument();
});
test("handles delete error gracefully", async () => {
const consoleError = jest
.spyOn(console, "error")
.mockImplementation(() => {});
mockContentsAPI.deleteContent.mockRejectedValue(
new Error("Delete failed")
);
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
});
const deleteButtons = screen.getAllByTitle("Delete content");
fireEvent.click(deleteButtons[0]);
await waitFor(() => {
expect(consoleError).toHaveBeenCalledWith(
"Failed to delete content:",
expect.any(Error)
);
});
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
expect(screen.getByText("content_1...")).toBeInTheDocument();
consoleError.mockRestore();
});
});
describe("Breadcrumb Navigation", () => {
test("renders correct breadcrumb structure", async () => {
render(<ContentsListPage />);
await waitFor(() => {
const vectorStoreTexts = screen.getAllByText("Vector Stores");
expect(vectorStoreTexts.length).toBeGreaterThan(0);
const storeNameTexts = screen.getAllByText("Test Vector Store");
expect(storeNameTexts.length).toBeGreaterThan(0);
const filesTexts = screen.getAllByText("Files");
expect(filesTexts.length).toBeGreaterThan(0);
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
const contentsTexts = screen.getAllByText("Contents");
expect(contentsTexts.length).toBeGreaterThan(0);
});
});
});
describe("Sidebar Properties", () => {
test("renders file and store properties", async () => {
render(<ContentsListPage />);
await waitFor(() => {
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
const storeIdTexts = screen.getAllByText("vs_123");
expect(storeIdTexts.length).toBeGreaterThan(0);
const storeNameTexts = screen.getAllByText("Test Vector Store");
expect(storeNameTexts.length).toBeGreaterThan(0);
expect(screen.getByText("completed")).toBeInTheDocument();
expect(screen.getByText("512")).toBeInTheDocument();
expect(screen.getByText("fixed_size")).toBeInTheDocument();
expect(screen.getByText("test_provider")).toBeInTheDocument();
});
});
});
describe("Content Text Utilities", () => {
test("handles different content formats correctly", async () => {
const contentWithObject = {
...mockContents[0],
content: { type: "text", text: "Object format content" },
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithObject],
});
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Object format content")).toBeInTheDocument();
});
});
test("handles string content format", async () => {
const contentWithString = {
...mockContents[0],
content: "String format content",
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithString],
});
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("String format content")).toBeInTheDocument();
});
});
test("handles unknown content format", async () => {
const contentWithUnknown = {
...mockContents[0],
content: { unknown: "format" },
};
mockContentsAPI.listContents.mockResolvedValue({
data: [contentWithUnknown],
});
render(<ContentsListPage />);
await waitFor(() => {
expect(screen.getByText("Content Chunks (1)")).toBeInTheDocument();
});
const contentCells = screen.getAllByRole("cell");
const contentPreviewCell = contentCells.find(cell =>
cell.querySelector("p[title]")
);
expect(contentPreviewCell?.querySelector("p")?.textContent).toBe("");
});
});
});

View file

@ -52,8 +52,10 @@ export default function ContentsListPage() {
const [file, setFile] = useState<VectorStoreFile | null>(null);
const [contents, setContents] = useState<VectorStoreContentItem[]>([]);
const [isLoadingStore, setIsLoadingStore] = useState(true);
const [isLoadingFile, setIsLoadingFile] = useState(true);
const [isLoadingContents, setIsLoadingContents] = useState(true);
const [errorStore, setErrorStore] = useState<Error | null>(null);
const [errorFile, setErrorFile] = useState<Error | null>(null);
const [errorContents, setErrorContents] = useState<Error | null>(null);
useEffect(() => {
@ -175,7 +177,13 @@ export default function ContentsListPage() {
<CardTitle>Content Chunks ({contents.length})</CardTitle>
</CardHeader>
<CardContent>
{isLoadingContents ? (
{isLoadingFile ? (
<Skeleton className="h-4 w-full" />
) : errorFile ? (
<div className="text-destructive text-sm">
Error loading file: {errorFile.message}
</div>
) : isLoadingContents ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />

View file

@ -0,0 +1,458 @@
import React from "react";
import {
render,
screen,
fireEvent,
waitFor,
act,
} from "@testing-library/react";
import "@testing-library/jest-dom";
import FileDetailPage from "./page";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type {
VectorStoreFile,
FileContentResponse,
} from "llama-stack-client/resources/vector-stores/files";
const mockPush = jest.fn();
const mockParams = {
id: "vs_123",
fileId: "file_456",
};
jest.mock("next/navigation", () => ({
useParams: () => mockParams,
useRouter: () => ({
push: mockPush,
}),
}));
const mockClient = {
vectorStores: {
retrieve: jest.fn(),
files: {
retrieve: jest.fn(),
content: jest.fn(),
},
},
};
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
}));
describe("FileDetailPage", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 5 },
usage_bytes: 1024,
metadata: {
provider_id: "test_provider",
},
};
const mockFile: VectorStoreFile = {
id: "file_456",
status: "completed",
created_at: 1710001000,
usage_bytes: 2048,
chunking_strategy: { type: "fixed_size" },
};
const mockFileContent: FileContentResponse = {
content: [
{ text: "First chunk of file content." },
{
text: "Second chunk with more detailed information about the content.",
},
{ text: "Third and final chunk of the file." },
],
};
beforeEach(() => {
jest.clearAllMocks();
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
mockClient.vectorStores.files.content.mockResolvedValue(mockFileContent);
});
describe("Loading and Error States", () => {
test("renders loading skeleton while fetching store data", async () => {
mockClient.vectorStores.retrieve.mockImplementation(
() => new Promise(() => {})
);
await act(async () => {
await act(async () => {
render(<FileDetailPage />);
});
});
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders error message when store API call fails", async () => {
const error = new Error("Failed to load store");
mockClient.vectorStores.retrieve.mockRejectedValue(error);
await act(async () => {
await act(async () => {
render(<FileDetailPage />);
});
});
await waitFor(() => {
expect(
screen.getByText(/Error loading details for ID vs_123/)
).toBeInTheDocument();
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
});
});
test("renders not found when store doesn't exist", async () => {
mockClient.vectorStores.retrieve.mockResolvedValue(null);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText(/No details found for ID: vs_123/)
).toBeInTheDocument();
});
});
test("renders file loading skeleton", async () => {
mockClient.vectorStores.files.retrieve.mockImplementation(
() => new Promise(() => {})
);
const { container } = render(<FileDetailPage />);
await waitFor(() => {
expect(screen.getByText("File: file_456")).toBeInTheDocument();
});
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders file error message", async () => {
const error = new Error("Failed to load file");
mockClient.vectorStores.files.retrieve.mockRejectedValue(error);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText("Error loading file: Failed to load file")
).toBeInTheDocument();
});
});
test("renders content error message", async () => {
const error = new Error("Failed to load contents");
mockClient.vectorStores.files.content.mockRejectedValue(error);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText(
"Error loading content summary: Failed to load contents"
)
).toBeInTheDocument();
});
});
});
describe("File Information Display", () => {
test("renders file details correctly", async () => {
await act(async () => {
await act(async () => {
render(<FileDetailPage />);
});
});
await waitFor(() => {
expect(screen.getByText("File: file_456")).toBeInTheDocument();
expect(screen.getByText("File Information")).toBeInTheDocument();
expect(screen.getByText("File Details")).toBeInTheDocument();
});
const statusTexts = screen.getAllByText("Status:");
expect(statusTexts.length).toBeGreaterThan(0);
const completedTexts = screen.getAllByText("completed");
expect(completedTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Size:")).toBeInTheDocument();
expect(screen.getByText("2048 bytes")).toBeInTheDocument();
const createdTexts = screen.getAllByText("Created:");
expect(createdTexts.length).toBeGreaterThan(0);
const dateTexts = screen.getAllByText(
new Date(1710001000 * 1000).toLocaleString()
);
expect(dateTexts.length).toBeGreaterThan(0);
const strategyTexts = screen.getAllByText("Content Strategy:");
expect(strategyTexts.length).toBeGreaterThan(0);
const fixedSizeTexts = screen.getAllByText("fixed_size");
expect(fixedSizeTexts.length).toBeGreaterThan(0);
});
test("handles missing file data", async () => {
mockClient.vectorStores.files.retrieve.mockResolvedValue(null);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(screen.getByText("File not found.")).toBeInTheDocument();
});
});
});
describe("Content Summary Display", () => {
test("renders content summary correctly", async () => {
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(screen.getByText("Content Summary")).toBeInTheDocument();
expect(screen.getByText("Content Items:")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("Total Characters:")).toBeInTheDocument();
const totalChars = mockFileContent.content.reduce(
(total, item) => total + item.text.length,
0
);
expect(screen.getByText(totalChars.toString())).toBeInTheDocument();
expect(screen.getByText("Preview:")).toBeInTheDocument();
expect(
screen.getByText(/First chunk of file content\./)
).toBeInTheDocument();
});
});
test("handles empty content", async () => {
mockClient.vectorStores.files.content.mockResolvedValue({
content: [],
});
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText("No contents found for this file.")
).toBeInTheDocument();
});
});
test("truncates long content preview", async () => {
const longContent = {
content: [
{
text: "This is a very long piece of content that should be truncated after 200 characters to ensure the preview doesn't take up too much space in the UI and remains readable and manageable for users viewing the file details page.",
},
],
};
mockClient.vectorStores.files.content.mockResolvedValue(longContent);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText(/This is a very long piece of content/)
).toBeInTheDocument();
expect(screen.getByText(/\.\.\.$/)).toBeInTheDocument();
});
});
});
describe("Navigation and Actions", () => {
test("navigates to contents list when View Contents button is clicked", async () => {
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(screen.getByText("Actions")).toBeInTheDocument();
});
const viewContentsButton = screen.getByRole("button", {
name: /View Contents/,
});
fireEvent.click(viewContentsButton);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456/contents"
);
});
test("View Contents button is styled correctly", async () => {
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
const button = screen.getByRole("button", { name: /View Contents/ });
expect(button).toHaveClass("flex", "items-center", "gap-2");
});
});
});
describe("Breadcrumb Navigation", () => {
test("renders correct breadcrumb structure", async () => {
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
const vectorStoresTexts = screen.getAllByText("Vector Stores");
expect(vectorStoresTexts.length).toBeGreaterThan(0);
const storeNameTexts = screen.getAllByText("Test Vector Store");
expect(storeNameTexts.length).toBeGreaterThan(0);
const filesTexts = screen.getAllByText("Files");
expect(filesTexts.length).toBeGreaterThan(0);
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
});
});
test("uses store ID when store name is not available", async () => {
const storeWithoutName = { ...mockStore, name: "" };
mockClient.vectorStores.retrieve.mockResolvedValue(storeWithoutName);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
const storeIdTexts = screen.getAllByText("vs_123");
expect(storeIdTexts.length).toBeGreaterThan(0);
});
});
});
describe("Sidebar Properties", () => {
test.skip("renders file and store properties correctly", async () => {
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(screen.getByText("File ID")).toBeInTheDocument();
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Vector Store ID")).toBeInTheDocument();
const storeIdTexts = screen.getAllByText("vs_123");
expect(storeIdTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Status")).toBeInTheDocument();
const completedTexts = screen.getAllByText("completed");
expect(completedTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
const usageTexts = screen.getAllByText("2048");
expect(usageTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Content Strategy")).toBeInTheDocument();
const fixedSizeTexts = screen.getAllByText("fixed_size");
expect(fixedSizeTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Store Name")).toBeInTheDocument();
const storeNameTexts = screen.getAllByText("Test Vector Store");
expect(storeNameTexts.length).toBeGreaterThan(0);
expect(screen.getByText("Provider ID")).toBeInTheDocument();
expect(screen.getByText("test_provider")).toBeInTheDocument();
});
});
test("handles missing optional properties", async () => {
const minimalFile = {
id: "file_456",
status: "completed",
created_at: 1710001000,
usage_bytes: 2048,
chunking_strategy: { type: "fixed_size" },
};
const minimalStore = {
...mockStore,
name: "",
metadata: {},
};
mockClient.vectorStores.files.retrieve.mockResolvedValue(minimalFile);
mockClient.vectorStores.retrieve.mockResolvedValue(minimalStore);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
const fileIdTexts = screen.getAllByText("file_456");
expect(fileIdTexts.length).toBeGreaterThan(0);
const storeIdTexts = screen.getAllByText("vs_123");
expect(storeIdTexts.length).toBeGreaterThan(0);
});
expect(screen.getByText("File: file_456")).toBeInTheDocument();
});
});
describe("Loading States for Individual Sections", () => {
test("shows loading skeleton for content while file loads", async () => {
mockClient.vectorStores.files.content.mockImplementation(
() => new Promise(() => {})
);
const { container } = render(<FileDetailPage />);
await waitFor(() => {
expect(screen.getByText("Content Summary")).toBeInTheDocument();
});
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
});
describe("Error Handling", () => {
test("handles multiple simultaneous errors gracefully", async () => {
mockClient.vectorStores.files.retrieve.mockRejectedValue(
new Error("File error")
);
mockClient.vectorStores.files.content.mockRejectedValue(
new Error("Content error")
);
await act(async () => {
render(<FileDetailPage />);
});
await waitFor(() => {
expect(
screen.getByText("Error loading file: File error")
).toBeInTheDocument();
expect(
screen.getByText("Error loading content summary: Content error")
).toBeInTheDocument();
});
});
});
});

View file

@ -187,6 +187,7 @@ const COMPONENTS = {
code: ({
children,
className,
...rest
}: {
children: React.ReactNode;
className?: string;

View file

@ -0,0 +1,315 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import { VectorStoreDetailView } from "./vector-store-detail";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
const mockPush = jest.fn();
jest.mock("next/navigation", () => ({
useRouter: () => ({
push: mockPush,
}),
}));
describe("VectorStoreDetailView", () => {
const defaultProps = {
store: null,
files: [],
isLoadingStore: false,
isLoadingFiles: false,
errorStore: null,
errorFiles: null,
id: "test_vector_store_id",
};
beforeEach(() => {
mockPush.mockClear();
});
describe("Loading States", () => {
test("renders loading skeleton when store is loading", () => {
const { container } = render(
<VectorStoreDetailView {...defaultProps} isLoadingStore={true} />
);
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
test("renders files loading skeleton when files are loading", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 5 },
usage_bytes: 1024,
metadata: {
provider_id: "test_provider",
provider_vector_db_id: "test_db_id",
},
};
const { container } = render(
<VectorStoreDetailView
{...defaultProps}
store={mockStore}
isLoadingFiles={true}
/>
);
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
expect(screen.getByText("Files")).toBeInTheDocument();
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
expect(skeletons.length).toBeGreaterThan(0);
});
});
describe("Error States", () => {
test("renders error message when store error occurs", () => {
render(
<VectorStoreDetailView
{...defaultProps}
errorStore={{ name: "Error", message: "Failed to load store" }}
/>
);
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
expect(
screen.getByText(/Error loading details for ID test_vector_store_id/)
).toBeInTheDocument();
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
});
test("renders files error when files fail to load", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 5 },
usage_bytes: 1024,
metadata: {
provider_id: "test_provider",
provider_vector_db_id: "test_db_id",
},
};
render(
<VectorStoreDetailView
{...defaultProps}
store={mockStore}
errorFiles={{ name: "Error", message: "Failed to load files" }}
/>
);
expect(screen.getByText("Files")).toBeInTheDocument();
expect(
screen.getByText("Error loading files: Failed to load files")
).toBeInTheDocument();
});
});
describe("Not Found State", () => {
test("renders not found message when store is null", () => {
render(<VectorStoreDetailView {...defaultProps} store={null} />);
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
expect(
screen.getByText(/No details found for ID: test_vector_store_id/)
).toBeInTheDocument();
});
});
describe("Store Data Rendering", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 3 },
usage_bytes: 2048,
metadata: {
provider_id: "test_provider",
provider_vector_db_id: "test_db_id",
},
};
test("renders store properties correctly", () => {
render(<VectorStoreDetailView {...defaultProps} store={mockStore} />);
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
expect(screen.getByText("vs_123")).toBeInTheDocument();
expect(screen.getByText("Test Vector Store")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("ready")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("2048")).toBeInTheDocument();
expect(screen.getByText("test_provider")).toBeInTheDocument();
expect(screen.getByText("test_db_id")).toBeInTheDocument();
});
test("handles empty/missing optional fields", () => {
const minimalStore: VectorStore = {
id: "vs_minimal",
name: "",
created_at: 1710000000,
status: "ready",
file_counts: { total: 0 },
usage_bytes: 0,
metadata: {},
};
render(<VectorStoreDetailView {...defaultProps} store={minimalStore} />);
expect(screen.getByText("vs_minimal")).toBeInTheDocument();
expect(screen.getByText("ready")).toBeInTheDocument();
const zeroTexts = screen.getAllByText("0");
expect(zeroTexts.length).toBeGreaterThanOrEqual(2);
});
test("shows empty files message when no files", () => {
render(
<VectorStoreDetailView {...defaultProps} store={mockStore} files={[]} />
);
expect(screen.getByText("Files")).toBeInTheDocument();
expect(
screen.getByText("No files in this vector store.")
).toBeInTheDocument();
});
});
describe("Files Table", () => {
const mockStore: VectorStore = {
id: "vs_123",
name: "Test Vector Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 2 },
usage_bytes: 2048,
metadata: {},
};
const mockFiles: VectorStoreFile[] = [
{
id: "file_123",
status: "completed",
created_at: 1710001000,
usage_bytes: 1024,
},
{
id: "file_456",
status: "processing",
created_at: 1710002000,
usage_bytes: 512,
},
];
test("renders files table with correct data", () => {
render(
<VectorStoreDetailView
{...defaultProps}
store={mockStore}
files={mockFiles}
/>
);
expect(screen.getByText("Files")).toBeInTheDocument();
expect(
screen.getByText("Files in this vector store")
).toBeInTheDocument();
expect(screen.getByText("ID")).toBeInTheDocument();
expect(screen.getByText("Status")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
expect(screen.getByText("file_123")).toBeInTheDocument();
expect(screen.getByText("completed")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710001000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("1024")).toBeInTheDocument();
expect(screen.getByText("file_456")).toBeInTheDocument();
expect(screen.getByText("processing")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710002000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("512")).toBeInTheDocument();
});
test("file ID links are clickable and navigate correctly", () => {
render(
<VectorStoreDetailView
{...defaultProps}
store={mockStore}
files={mockFiles}
id="vs_123"
/>
);
const fileButton = screen.getByRole("button", { name: "file_123" });
expect(fileButton).toBeInTheDocument();
fireEvent.click(fileButton);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_123"
);
});
test("handles multiple file clicks correctly", () => {
render(
<VectorStoreDetailView
{...defaultProps}
store={mockStore}
files={mockFiles}
id="vs_123"
/>
);
const file1Button = screen.getByRole("button", { name: "file_123" });
const file2Button = screen.getByRole("button", { name: "file_456" });
fireEvent.click(file1Button);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_123"
);
fireEvent.click(file2Button);
expect(mockPush).toHaveBeenCalledWith(
"/logs/vector-stores/vs_123/files/file_456"
);
expect(mockPush).toHaveBeenCalledTimes(2);
});
});
describe("Layout Structure", () => {
const mockStore: VectorStore = {
id: "vs_layout_test",
name: "Layout Test Store",
created_at: 1710000000,
status: "ready",
file_counts: { total: 1 },
usage_bytes: 1024,
metadata: {},
};
test("renders main content and sidebar in correct layout", () => {
render(<VectorStoreDetailView {...defaultProps} store={mockStore} />);
expect(screen.getByText("Files")).toBeInTheDocument();
expect(screen.getByText("vs_layout_test")).toBeInTheDocument();
expect(screen.getByText("Layout Test Store")).toBeInTheDocument();
expect(screen.getByText("ready")).toBeInTheDocument();
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("1024")).toBeInTheDocument();
});
});
});

View file

@ -23,7 +23,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.18.2",
"llama-stack-client": "^0.2.17",
"llama-stack-client": "^0.2.18",
"lucide-react": "^0.510.0",
"next": "15.3.3",
"next-auth": "^4.24.11",

View file

@ -7,7 +7,7 @@ required-version = ">=0.7.0"
[project]
name = "llama_stack"
version = "0.2.17"
version = "0.2.18"
authors = [{ name = "Meta Llama", email = "llama-oss@meta.com" }]
description = "Llama Stack"
readme = "README.md"
@ -31,9 +31,9 @@ dependencies = [
"huggingface-hub>=0.34.0,<1.0",
"jinja2>=3.1.6",
"jsonschema",
"llama-stack-client>=0.2.17",
"llama-stack-client>=0.2.18",
"llama-api-client>=0.1.2",
"openai>=1.99.6",
"openai>=1.99.6,<1.100.0",
"prompt-toolkit",
"python-dotenv",
"python-jose[cryptography]",
@ -56,7 +56,7 @@ dependencies = [
ui = [
"streamlit",
"pandas",
"llama-stack-client>=0.2.17",
"llama-stack-client>=0.2.18",
"streamlit-option-menu",
]
@ -93,6 +93,7 @@ unit = [
"blobfile",
"faiss-cpu",
"pymilvus>=2.5.12",
"milvus-lite>=2.5.0",
"litellm",
"together",
"coverage",
@ -118,6 +119,7 @@ test = [
"sqlalchemy[asyncio]>=2.0.41",
"requests",
"pymilvus>=2.5.12",
"milvus-lite>=2.5.0",
"weaviate-client>=4.16.4",
]
docs = [

View file

@ -111,6 +111,9 @@ echo "Inference Mode: $INFERENCE_MODE"
echo "Test Pattern: $TEST_PATTERN"
echo ""
echo "Checking llama packages"
uv pip list | grep llama
# Check storage and memory before tests
echo "=== System Resources Before Tests ==="
free -h 2>/dev/null || echo "free command not available"
@ -133,6 +136,10 @@ else
EXTRA_PARAMS=""
fi
THIS_DIR=$(dirname "$0")
ROOT_DIR="$THIS_DIR/.."
cd $ROOT_DIR
# Set recording directory
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings/vision"
@ -140,10 +147,27 @@ else
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings"
fi
# check if "llama" and "pytest" are available. this script does not use `uv run` given
# it can be used in a pre-release environment where we have not been able to tell
# uv about pre-release dependencies properly (yet).
if ! command -v llama &> /dev/null; then
echo "llama could not be found, ensure llama-stack is installed"
exit 1
fi
if ! command -v pytest &> /dev/null; then
echo "pytest could not be found, ensure pytest is installed"
exit 1
fi
# Start Llama Stack Server if needed
if [[ "$STACK_CONFIG" == *"server:"* ]]; then
# check if server is already running
if curl -s http://localhost:8321/v1/health 2>/dev/null | grep -q "OK"; then
echo "Llama Stack Server is already running, skipping start"
else
echo "=== Starting Llama Stack Server ==="
nohup uv run llama stack run ci-tests --image-type venv > server.log 2>&1 &
nohup llama stack run ci-tests --image-type venv > server.log 2>&1 &
echo "Waiting for Llama Stack Server to start..."
for i in {1..30}; do
@ -160,6 +184,7 @@ if [[ "$STACK_CONFIG" == *"server:"* ]]; then
sleep 1
done
echo ""
fi
fi
# Run tests
@ -180,7 +205,7 @@ fi
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
echo "Running vision tests..."
set +e
uv run pytest -s -v tests/integration/inference/test_vision_inference.py \
pytest -s -v tests/integration/inference/test_vision_inference.py \
--stack-config="$STACK_CONFIG" \
-k "$PYTEST_PATTERN" \
--vision-model=ollama/llama3.2-vision:11b \
@ -248,7 +273,7 @@ echo "=== Running all collected tests in a single pytest command ==="
echo "Total test files: $(echo $TEST_FILES | wc -w)"
set +e
uv run pytest -s -v $TEST_FILES \
pytest -s -v $TEST_FILES \
--stack-config="$STACK_CONFIG" \
-k "$PYTEST_PATTERN" \
--text-model="$TEXT_MODEL" \

View file

@ -133,24 +133,15 @@ def test_agent_simple(llama_stack_client, agent_config):
assert "I can't" in logs_str
@pytest.mark.skip(reason="this test was disabled for a long time, and now has turned flaky")
def test_agent_name(llama_stack_client, text_model_id):
agent_name = f"test-agent-{uuid4()}"
try:
agent = Agent(
llama_stack_client,
model=text_model_id,
instructions="You are a helpful assistant",
name=agent_name,
)
except TypeError:
agent = Agent(
llama_stack_client,
model=text_model_id,
instructions="You are a helpful assistant",
)
return
session_id = agent.create_session(f"test-session-{uuid4()}")
agent.create_turn(

View file

@ -14,7 +14,7 @@
"models": [
{
"model": "nomic-embed-text:latest",
"modified_at": "2025-08-15T21:55:08.088554Z",
"modified_at": "2025-08-18T12:47:56.732989-07:00",
"digest": "0a109f422b47e3a30ba2b10eca18548e944e8a23073ee3f3e947efcf3c45e59f",
"size": 274302450,
"details": {
@ -28,9 +28,41 @@
"quantization_level": "F16"
}
},
{
"model": "llama3.2-vision:11b",
"modified_at": "2025-07-30T18:45:02.517873-07:00",
"digest": "6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e",
"size": 7816589186,
"details": {
"parent_model": "",
"format": "gguf",
"family": "mllama",
"families": [
"mllama"
],
"parameter_size": "10.7B",
"quantization_level": "Q4_K_M"
}
},
{
"model": "llama3.2-vision:latest",
"modified_at": "2025-07-29T20:18:47.920468-07:00",
"digest": "6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e",
"size": 7816589186,
"details": {
"parent_model": "",
"format": "gguf",
"family": "mllama",
"families": [
"mllama"
],
"parameter_size": "10.7B",
"quantization_level": "Q4_K_M"
}
},
{
"model": "llama-guard3:1b",
"modified_at": "2025-07-31T04:44:58Z",
"modified_at": "2025-07-25T14:39:44.978630-07:00",
"digest": "494147e06bf99e10dbe67b63a07ac81c162f18ef3341aa3390007ac828571b3b",
"size": 1600181919,
"details": {
@ -46,7 +78,7 @@
},
{
"model": "all-minilm:l6-v2",
"modified_at": "2025-07-31T04:42:15Z",
"modified_at": "2025-07-24T15:15:11.129290-07:00",
"digest": "1b226e2802dbb772b5fc32a58f103ca1804ef7501331012de126ab22f67475ef",
"size": 45960996,
"details": {
@ -60,9 +92,57 @@
"quantization_level": "F16"
}
},
{
"model": "llama3.2:1b",
"modified_at": "2025-07-17T22:02:24.953208-07:00",
"digest": "baf6a787fdffd633537aa2eb51cfd54cb93ff08e28040095462bb63daf552878",
"size": 1321098329,
"details": {
"parent_model": "",
"format": "gguf",
"family": "llama",
"families": [
"llama"
],
"parameter_size": "1.2B",
"quantization_level": "Q8_0"
}
},
{
"model": "all-minilm:latest",
"modified_at": "2025-06-03T16:50:10.946583-07:00",
"digest": "1b226e2802dbb772b5fc32a58f103ca1804ef7501331012de126ab22f67475ef",
"size": 45960996,
"details": {
"parent_model": "",
"format": "gguf",
"family": "bert",
"families": [
"bert"
],
"parameter_size": "23M",
"quantization_level": "F16"
}
},
{
"model": "llama3.2:3b",
"modified_at": "2025-05-01T11:15:23.797447-07:00",
"digest": "a80c4f17acd55265feec403c7aef86be0c25983ab279d83f3bcd3abbcb5b8b72",
"size": 2019393189,
"details": {
"parent_model": "",
"format": "gguf",
"family": "llama",
"families": [
"llama"
],
"parameter_size": "3.2B",
"quantization_level": "Q4_K_M"
}
},
{
"model": "llama3.2:3b-instruct-fp16",
"modified_at": "2025-07-31T04:42:05Z",
"modified_at": "2025-04-30T15:33:48.939665-07:00",
"digest": "195a8c01d91ec3cb1e0aad4624a51f2602c51fa7d96110f8ab5a20c84081804d",
"size": 6433703586,
"details": {

View file

@ -0,0 +1,203 @@
{
"request": {
"method": "POST",
"url": "http://localhost:11434/api/generate",
"headers": {},
"body": {
"model": "llama3.2:3b-instruct-fp16",
"raw": true,
"prompt": "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nYou are a helpful assistant<|eot_id|><|start_header_id|>user<|end_header_id|>\n\nGive me a sentence that contains the word: hello<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n",
"options": {
"temperature": 0.0
},
"stream": true
},
"endpoint": "/api/generate",
"model": "llama3.2:3b-instruct-fp16"
},
"response": {
"body": [
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.267146Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": "Hello",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.309006Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": ",",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.351179Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " how",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.393262Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " can",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.436079Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " I",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.478393Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " assist",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.520608Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " you",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.562885Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": " today",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.604683Z",
"done": false,
"done_reason": null,
"total_duration": null,
"load_duration": null,
"prompt_eval_count": null,
"prompt_eval_duration": null,
"eval_count": null,
"eval_duration": null,
"response": "?",
"thinking": null,
"context": null
}
},
{
"__type__": "ollama._types.GenerateResponse",
"__data__": {
"model": "llama3.2:3b-instruct-fp16",
"created_at": "2025-08-18T19:47:58.646586Z",
"done": true,
"done_reason": "stop",
"total_duration": 1011323917,
"load_duration": 76575458,
"prompt_eval_count": 31,
"prompt_eval_duration": 553259250,
"eval_count": 10,
"eval_duration": 380302792,
"response": "",
"thinking": null,
"context": null
}
}
],
"is_streaming": true
}
}

View file

@ -11,7 +11,26 @@
"body": {
"__type__": "ollama._types.ProcessResponse",
"__data__": {
"models": []
"models": [
{
"model": "llama3.2:3b-instruct-fp16",
"name": "llama3.2:3b-instruct-fp16",
"digest": "195a8c01d91ec3cb1e0aad4624a51f2602c51fa7d96110f8ab5a20c84081804d",
"expires_at": "2025-08-18T13:47:44.262256-07:00",
"size": 7919570944,
"size_vram": 7919570944,
"details": {
"parent_model": "",
"format": "gguf",
"family": "llama",
"families": [
"llama"
],
"parameter_size": "3.2B",
"quantization_level": "F16"
}
}
]
}
},
"is_streaming": false

View file

@ -27,10 +27,10 @@ def data_url_from_image(file_path):
@pytest.fixture(scope="session")
def code_scanner_shield_id(available_shields):
if "CodeScanner" in available_shields:
return "CodeScanner"
if "code-scanner" in available_shields:
return "code-scanner"
pytest.skip("CodeScanner shield is not available. Skipping.")
pytest.skip("code-scanner shield is not available. Skipping.")
def test_unsafe_examples(client_with_models, shield_id):
@ -163,6 +163,60 @@ def test_safety_with_code_scanner(client_with_models, code_scanner_shield_id, mo
assert response.violation.user_message == "Sorry, I found security concerns in the code."
def test_unsafe_input_with_code_scanner_run_moderation(client_with_models, code_scanner_shield_id, model_providers):
code_safety_enabled = len(CODE_SCANNER_ENABLED_PROVIDERS & model_providers) > 0
if not code_safety_enabled:
pytest.skip(f"Testing code scanner shields is not supported for model_providers {model_providers}")
insecure_code = [
"""
Here is an example of code that hashes a password using MD5:
```
import hashlib
password = 'securepassword123'.encode()
hashed_password = hashlib.md5(password).hexdigest()
print("Insecure hashed password:", hashed_password)
```
"""
]
moderation_object = client_with_models.moderations.create(
input=insecure_code,
model=code_scanner_shield_id,
)
assert moderation_object.results[0].flagged is True, f"Code scanner should have flagged {insecure_code} as insecure"
assert all(value is True for value in moderation_object.results[0].categories.values()), (
"Code scanner shield should have detected code insecure category"
)
def test_safe_input_with_code_scanner_run_moderation(client_with_models, code_scanner_shield_id, model_providers):
code_safety_enabled = len(CODE_SCANNER_ENABLED_PROVIDERS & model_providers) > 0
if not code_safety_enabled:
pytest.skip(f"Testing code scanner shields is not supported for model_providers {model_providers}")
secure_code = [
"""
Extract the first 5 characters from a string:
```
text = "Hello World"
first_five = text[:5]
print(first_five) # Output: "Hello"
# Safe handling for strings shorter than 5 characters
def get_first_five(text):
return text[:5] if text else ""
```
"""
]
moderation_object = client_with_models.moderations.create(
input=secure_code,
model=code_scanner_shield_id,
)
assert moderation_object.results[0].flagged is False, "Code scanner should not have flagged the code as insecure"
# We can use an instance of the LlamaGuard shield to detect attempts to misuse
# the interpreter as this is one of the existing categories it checks for
def test_safety_with_code_interpreter_abuse(client_with_models, shield_id):

View file

@ -56,6 +56,7 @@ def skip_if_provider_doesnt_support_openai_vector_stores_search(client_with_mode
"keyword": [
"inline::sqlite-vec",
"remote::milvus",
"inline::milvus",
],
"hybrid": [
"inline::sqlite-vec",

View file

@ -45,7 +45,6 @@ from llama_stack.providers.inline.agents.meta_reference.responses.utils import (
class TestConvertChatChoiceToResponseMessage:
@pytest.mark.asyncio
async def test_convert_string_content(self):
choice = OpenAIChoice(
message=OpenAIAssistantMessageParam(content="Test message"),
@ -61,7 +60,6 @@ class TestConvertChatChoiceToResponseMessage:
assert isinstance(result.content[0], OpenAIResponseOutputMessageContentOutputText)
assert result.content[0].text == "Test message"
@pytest.mark.asyncio
async def test_convert_text_param_content(self):
choice = OpenAIChoice(
message=OpenAIAssistantMessageParam(
@ -78,12 +76,10 @@ class TestConvertChatChoiceToResponseMessage:
class TestConvertResponseContentToChatContent:
@pytest.mark.asyncio
async def test_convert_string_content(self):
result = await convert_response_content_to_chat_content("Simple string")
assert result == "Simple string"
@pytest.mark.asyncio
async def test_convert_text_content_parts(self):
content = [
OpenAIResponseInputMessageContentText(text="First part"),
@ -98,7 +94,6 @@ class TestConvertResponseContentToChatContent:
assert isinstance(result[1], OpenAIChatCompletionContentPartTextParam)
assert result[1].text == "Second part"
@pytest.mark.asyncio
async def test_convert_image_content(self):
content = [OpenAIResponseInputMessageContentImage(image_url="https://example.com/image.jpg", detail="high")]
@ -111,7 +106,6 @@ class TestConvertResponseContentToChatContent:
class TestConvertResponseInputToChatMessages:
@pytest.mark.asyncio
async def test_convert_string_input(self):
result = await convert_response_input_to_chat_messages("User message")
@ -119,7 +113,6 @@ class TestConvertResponseInputToChatMessages:
assert isinstance(result[0], OpenAIUserMessageParam)
assert result[0].content == "User message"
@pytest.mark.asyncio
async def test_convert_function_tool_call_output(self):
input_items = [
OpenAIResponseInputFunctionToolCallOutput(
@ -135,7 +128,6 @@ class TestConvertResponseInputToChatMessages:
assert result[0].content == "Tool output"
assert result[0].tool_call_id == "call_123"
@pytest.mark.asyncio
async def test_convert_function_tool_call(self):
input_items = [
OpenAIResponseOutputMessageFunctionToolCall(
@ -154,7 +146,6 @@ class TestConvertResponseInputToChatMessages:
assert result[0].tool_calls[0].function.name == "test_function"
assert result[0].tool_calls[0].function.arguments == '{"param": "value"}'
@pytest.mark.asyncio
async def test_convert_response_message(self):
input_items = [
OpenAIResponseMessage(
@ -173,7 +164,6 @@ class TestConvertResponseInputToChatMessages:
class TestConvertResponseTextToChatResponseFormat:
@pytest.mark.asyncio
async def test_convert_text_format(self):
text = OpenAIResponseText(format=OpenAIResponseTextFormat(type="text"))
result = await convert_response_text_to_chat_response_format(text)
@ -181,14 +171,12 @@ class TestConvertResponseTextToChatResponseFormat:
assert isinstance(result, OpenAIResponseFormatText)
assert result.type == "text"
@pytest.mark.asyncio
async def test_convert_json_object_format(self):
text = OpenAIResponseText(format={"type": "json_object"})
result = await convert_response_text_to_chat_response_format(text)
assert isinstance(result, OpenAIResponseFormatJSONObject)
@pytest.mark.asyncio
async def test_convert_json_schema_format(self):
schema_def = {"type": "object", "properties": {"test": {"type": "string"}}}
text = OpenAIResponseText(
@ -204,7 +192,6 @@ class TestConvertResponseTextToChatResponseFormat:
assert result.json_schema["name"] == "test_schema"
assert result.json_schema["schema"] == schema_def
@pytest.mark.asyncio
async def test_default_text_format(self):
text = OpenAIResponseText()
result = await convert_response_text_to_chat_response_format(text)
@ -214,27 +201,22 @@ class TestConvertResponseTextToChatResponseFormat:
class TestGetMessageTypeByRole:
@pytest.mark.asyncio
async def test_user_role(self):
result = await get_message_type_by_role("user")
assert result == OpenAIUserMessageParam
@pytest.mark.asyncio
async def test_system_role(self):
result = await get_message_type_by_role("system")
assert result == OpenAISystemMessageParam
@pytest.mark.asyncio
async def test_assistant_role(self):
result = await get_message_type_by_role("assistant")
assert result == OpenAIAssistantMessageParam
@pytest.mark.asyncio
async def test_developer_role(self):
result = await get_message_type_by_role("developer")
assert result == OpenAIDeveloperMessageParam
@pytest.mark.asyncio
async def test_unknown_role(self):
result = await get_message_type_by_role("unknown")
assert result is None

18
uv.lock generated
View file

@ -1719,7 +1719,7 @@ wheels = [
[[package]]
name = "llama-stack"
version = "0.2.17"
version = "0.2.18"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@ -1809,6 +1809,7 @@ test = [
{ name = "chardet" },
{ name = "datasets" },
{ name = "mcp" },
{ name = "milvus-lite" },
{ name = "openai" },
{ name = "pymilvus" },
{ name = "pypdf" },
@ -1831,6 +1832,7 @@ unit = [
{ name = "faiss-cpu" },
{ name = "litellm" },
{ name = "mcp" },
{ name = "milvus-lite" },
{ name = "ollama" },
{ name = "openai" },
{ name = "pymilvus" },
@ -1854,9 +1856,9 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "jsonschema" },
{ name = "llama-api-client", specifier = ">=0.1.2" },
{ name = "llama-stack-client", specifier = ">=0.2.17" },
{ name = "llama-stack-client", marker = "extra == 'ui'", specifier = ">=0.2.17" },
{ name = "openai", specifier = ">=1.99.6" },
{ name = "llama-stack-client", specifier = ">=0.2.18" },
{ name = "llama-stack-client", marker = "extra == 'ui'", specifier = ">=0.2.18" },
{ name = "openai", specifier = ">=1.99.6,<1.100.0" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" },
{ name = "opentelemetry-sdk", specifier = ">=1.30.0" },
{ name = "pandas", marker = "extra == 'ui'" },
@ -1925,6 +1927,7 @@ test = [
{ name = "chardet" },
{ name = "datasets" },
{ name = "mcp" },
{ name = "milvus-lite", specifier = ">=2.5.0" },
{ name = "openai" },
{ name = "pymilvus", specifier = ">=2.5.12" },
{ name = "pypdf" },
@ -1946,6 +1949,7 @@ unit = [
{ name = "faiss-cpu" },
{ name = "litellm" },
{ name = "mcp" },
{ name = "milvus-lite", specifier = ">=2.5.0" },
{ name = "ollama" },
{ name = "openai" },
{ name = "pymilvus", specifier = ">=2.5.12" },
@ -1959,7 +1963,7 @@ unit = [
[[package]]
name = "llama-stack-client"
version = "0.2.17"
version = "0.2.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -1978,9 +1982,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/bb2949d6a5c494d21da0c185d426e25eaa8016f8287b689249afc6c96fb5/llama_stack_client-0.2.17.tar.gz", hash = "sha256:1fe2070133c6356761e394fa346045e9b6b567d4c63157b9bc6be89b9a6e7a41", size = 257636, upload-time = "2025-08-05T01:42:55.911Z" }
sdist = { url = "https://files.pythonhosted.org/packages/69/da/5e5a745495f8a2b8ef24fc4d01fe9031aa2277c36447cb22192ec8c8cc1e/llama_stack_client-0.2.18.tar.gz", hash = "sha256:860c885c9e549445178ac55cc9422e6e2a91215ac7aff5aaccfb42f3ce07e79e", size = 277284, upload-time = "2025-08-19T22:12:09.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/fc/5eccc86b83c5ced3a3bca071d250a86ccafa4ff17546cf781deb7758ab74/llama_stack_client-0.2.17-py3-none-any.whl", hash = "sha256:336c32f8688700ff64717b8109f405dc87a990fbe310c2027ac9ed6d39d67d16", size = 350329, upload-time = "2025-08-05T01:42:54.381Z" },
{ url = "https://files.pythonhosted.org/packages/0a/e4/e97f8fdd8a07aa1efc7f7e37b5657d84357b664bf70dd1885a437edc0699/llama_stack_client-0.2.18-py3-none-any.whl", hash = "sha256:90f827d5476f7fc15fd993f1863af6a6e72bd064646bf6a99435eb43a1327f70", size = 367586, upload-time = "2025-08-19T22:12:07.899Z" },
]
[[package]]