Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ KUBEFLOW_LLAMA_STACK_URL=<your-llama-stack-url>
KUBEFLOW_PIPELINES_ENDPOINT=<your-kfp-endpoint>
KUBEFLOW_NAMESPACE=<your-namespace>
KUBEFLOW_BASE_IMAGE=quay.io/diegosquayorg/my-ragas-provider-image:latest
KUBEFLOW_PIPELINES_TOKEN=<your-pipelines-token>
KUBEFLOW_RESULTS_S3_PREFIX=s3://my-bucket/ragas-results
KUBEFLOW_S3_CREDENTIALS_SECRET_NAME=<secret-name>
```
Expand All @@ -61,6 +62,7 @@ Where:
- `KUBEFLOW_LLAMA_STACK_URL`: The URL of the llama stack server that the remote provider will use to run the evaluation (LLM generations and embeddings, etc.). If you are running Llama Stack locally, you can use [ngrok](https://ngrok.com/) to expose it to the remote provider.
- `KUBEFLOW_PIPELINES_ENDPOINT`: You can get this via `kubectl get routes -A | grep -i pipeline` on your Kubernetes cluster.
- `KUBEFLOW_NAMESPACE`: The name of the data science project where the Kubeflow Pipelines server is running.
- `KUBEFLOW_PIPELINES_TOKEN`: Kubeflow Pipelines token with access to submit pipelines. If not provided, the token will be read from the local kubeconfig file.
- `KUBEFLOW_BASE_IMAGE`: The image used to run the Ragas evaluation in the remote provider. See `Containerfile` for details. There is a public version of this image at `quay.io/diegosquayorg/my-ragas-provider-image:latest`.
- `KUBEFLOW_RESULTS_S3_PREFIX`: S3 location (bucket and prefix folder) where evaluation results will be stored, e.g., `s3://my-bucket/ragas-results`.
- `KUBEFLOW_S3_CREDENTIALS_SECRET_NAME`: Name of the Kubernetes secret containing AWS credentials with write access to the S3 bucket. Create with:
Expand Down
1 change: 1 addition & 0 deletions distribution/run-remote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ providers:
namespace: ${env.KUBEFLOW_NAMESPACE}
llama_stack_url: ${env.KUBEFLOW_LLAMA_STACK_URL}
base_image: ${env.KUBEFLOW_BASE_IMAGE}
pipelines_token: ${env.KUBEFLOW_PIPELINES_TOKEN:=}
datasetio:
- provider_id: localfs
provider_type: inline::localfs
Expand Down
7 changes: 7 additions & 0 deletions docs/modules/ROOT/pages/remote-provider.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ KUBEFLOW_NAMESPACE=<your-namespace>
# Container image for remote execution
KUBEFLOW_BASE_IMAGE=quay.io/diegosquayorg/my-ragas-provider-image:latest

# Authentication token for Kubeflow Pipelines
KUBEFLOW_PIPELINES_TOKEN=<your-pipelines-token>

# S3 configuration for storing evaluation results
KUBEFLOW_RESULTS_S3_PREFIX=s3://my-bucket/ragas-results
KUBEFLOW_S3_CREDENTIALS_SECRET_NAME=<secret-name>
Expand Down Expand Up @@ -170,6 +173,9 @@ The name of the data science project where the Kubeflow Pipelines server is runn
`KUBEFLOW_BASE_IMAGE`::
The container image used to run the Ragas evaluation in the remote provider. See the `Containerfile` in the repository root for details on building a custom image.

`KUBEFLOW_PIPELINES_TOKEN`::
Kubeflow Pipelines token with access to submit pipelines. If not provided, the token will be read from the local kubeconfig file. This token is used to authenticate with the Kubeflow Pipelines API for pipeline submission and monitoring.

`KUBEFLOW_RESULTS_S3_PREFIX`::
The S3 location (bucket and prefix) where evaluation results will be stored. This should be a folder path, e.g., `s3://my-bucket/ragas-results`. The remote provider will write evaluation outputs to this location.

Expand Down Expand Up @@ -207,6 +213,7 @@ eval:
namespace: ${env.KUBEFLOW_NAMESPACE}
llama_stack_url: ${env.KUBEFLOW_LLAMA_STACK_URL}
base_image: ${env.KUBEFLOW_BASE_IMAGE}
pipelines_token: ${env.KUBEFLOW_PIPELINES_TOKEN:=}
----

To run with the sample distribution:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "llama-stack-provider-ragas"
version = "0.3.4"
version = "0.3.5"
description = "Ragas evaluation as an out-of-tree Llama Stack provider"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
9 changes: 9 additions & 0 deletions src/llama_stack_provider_ragas/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ class KubeflowConfig(BaseModel):
description=(
"Base image for Kubeflow pipeline components. "
"If not provided via env var, the image name will be read from a ConfigMap specified in constants.py."
"NOTE: this field is accessed via env var, but is here for completeness."
),
default=None,
)

pipelines_token: str | None = Field(
description=(
"Kubeflow Pipelines token with access to submit pipelines. "
"If not provided via env var, the token will be read from the local kubeconfig file."
),
default=None,
)
36 changes: 15 additions & 21 deletions src/llama_stack_provider_ragas/remote/kubeflow/components.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import logging
import os
from typing import List # noqa

from dotenv import load_dotenv
from kfp import dsl
from kubernetes import client
from kubernetes.client.exceptions import ApiException

from ...constants import (
DEFAULT_RAGAS_PROVIDER_IMAGE,
KUBEFLOW_CANDIDATE_NAMESPACES,
RAGAS_PROVIDER_IMAGE_CONFIGMAP_KEY,
RAGAS_PROVIDER_IMAGE_CONFIGMAP_NAME,
)
from .utils import _load_kube_config

load_dotenv()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def get_base_image() -> str:
"""Get base image from env, fallback to k8s ConfigMap, fallback to default image."""

import logging
import os

from kubernetes import client, config
from kubernetes.client.exceptions import ApiException

from llama_stack_provider_ragas.constants import (
DEFAULT_RAGAS_PROVIDER_IMAGE,
KUBEFLOW_CANDIDATE_NAMESPACES,
RAGAS_PROVIDER_IMAGE_CONFIGMAP_KEY,
RAGAS_PROVIDER_IMAGE_CONFIGMAP_NAME,
)
Comment on lines -12 to -23
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these imports were unnecessarily inside the scope of the function making it look like a kfp component but it is actually not.


if (base_image := os.environ.get("KUBEFLOW_BASE_IMAGE")) is not None:
return base_image

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

try:
config.load_incluster_config()
except config.ConfigException:
config.load_kube_config()

_load_kube_config()
api = client.CoreV1Api()

for candidate_namespace in KUBEFLOW_CANDIDATE_NAMESPACES:
Expand Down
19 changes: 19 additions & 0 deletions src/llama_stack_provider_ragas/remote/kubeflow/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import logging

logger = logging.getLogger(__name__)


def _load_kube_config():
from kubernetes import config
from kubernetes.client.configuration import Configuration

kube_config = Configuration()

try:
config.load_incluster_config(client_configuration=kube_config)
logger.info("Loaded in-cluster Kubernetes configuration")
Comment on lines +12 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Catching ConfigException without handling other exceptions may miss other errors.

Other exceptions, such as file not found or permission errors, may also occur when loading kubeconfig. Consider handling these cases to improve robustness.

except config.ConfigException:
config.load_kube_config(client_configuration=kube_config)
logger.info("Loaded Kubernetes configuration from kubeconfig file")

return kube_config
16 changes: 9 additions & 7 deletions src/llama_stack_provider_ragas/remote/ragas_remote_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def kfp_client(self):
try:
import kfp

token = self._get_token()
token = self._get_kfp_token()
if not token:
raise RagasEvaluationError(
"No token found. Please run `oc login` and try again."
Expand Down Expand Up @@ -108,14 +108,16 @@ def kfp_client(self):

return self._kfp_client

def _get_token(self) -> str:
def _get_kfp_token(self) -> str:
if self.config.kubeflow_config.pipelines_token:
logger.info("Using KUBEFLOW_PIPELINES_TOKEN from config")
return self.config.kubeflow_config.pipelines_token

try:
from kubernetes.client.configuration import Configuration
from kubernetes.config.kube_config import load_kube_config
from .kubeflow.utils import _load_kube_config

config = Configuration()
load_kube_config(client_configuration=config)
token = str(config.api_key["authorization"].split(" ")[-1])
kube_config = _load_kube_config()
token = str(kube_config.api_key["authorization"].split(" ")[-1])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Splitting the authorization header may be brittle if the format changes.

The code relies on a fixed 'Bearer ' format, which may not always be present. Please add validation or error handling to manage unexpected or missing formats.

except ImportError as e:
raise RagasEvaluationError(
"Kubernetes client is not installed. Install with: pip install .[remote]"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.