diff --git a/.env.example b/.env.example index ccaf0103..c8952277 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres SENTRY_DSN= +OTEL_ENABLED=false # Configure these with your own Docker registry images diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c9d6c144..c17287b7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,7 +42,6 @@ jobs: AUTH_TOKEN_HASH="$(echo -n "ci-test-token" | sha256sum | awk '{print $1}')" fi sed -i "s|AUTH_TOKEN=\"\"|AUTH_TOKEN=\"${AUTH_TOKEN_HASH}\"|" .env.test - sed -i "s|GUARDRAILS_HUB_API_KEY=\"\"|GUARDRAILS_HUB_API_KEY=\"${{ secrets.GUARDRAILS_HUB_API_KEY }}\"|" .env.test cp .env.test .env - name: Install uv @@ -55,15 +54,6 @@ jobs: run: uv sync working-directory: backend - - name: Install Guardrails hub validators - env: - GUARDRAILS_HUB_API_KEY: ${{ secrets.GUARDRAILS_HUB_API_KEY }} - run: | - source .venv/bin/activate - chmod +x scripts/install_guardrails_from_hub.sh - scripts/install_guardrails_from_hub.sh - working-directory: backend - - name: Activate virtual environment and run Alembic migrations run: | source .venv/bin/activate diff --git a/.gitignore b/.gitignore index 70a7a62c..55ad202a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ backend/app/evaluation/outputs/**/*.json # marimo related artefacts backend/notebooks/*.csv -**/__marimo__/ \ No newline at end of file +**/__marimo__/ + +backend/logs/app.log \ No newline at end of file diff --git a/backend/.guardrails/hub_registry.json b/backend/.guardrails/hub_registry.json index 8b19e387..83841623 100644 --- a/backend/.guardrails/hub_registry.json +++ b/backend/.guardrails/hub_registry.json @@ -6,7 +6,7 @@ "exports": [ "BanList" ], - "installed_at": "2026-08-18T06:55:12.094065+00:00", + "installed_at": "2026-08-26T05:41:11.480494+00:00", "package_name": "guardrails-grhub-ban-list" }, "guardrails/llm_critic": { @@ -14,7 +14,7 @@ "exports": [ "LLMCritic" ], - "installed_at": "2026-08-18T06:57:19.392503+00:00", + "installed_at": "2026-08-26T05:41:14.741349+00:00", "package_name": "guardrails-grhub-llm-critic" }, "guardrails/llamaguard_7b": { @@ -22,7 +22,7 @@ "exports": [ "LlamaGuard7B" ], - "installed_at": "2026-08-18T06:56:38.901794+00:00", + "installed_at": "2026-08-26T05:41:18.742264+00:00", "package_name": "guardrails-grhub-llamaguard-7b" }, "guardrails/profanity_free": { @@ -30,7 +30,7 @@ "exports": [ "ProfanityFree" ], - "installed_at": "2026-08-18T06:58:01.196192+00:00", + "installed_at": "2026-08-26T05:41:26.279899+00:00", "package_name": "guardrails-grhub-profanity-free" }, "guardrails/nsfw_text": { @@ -38,7 +38,7 @@ "exports": [ "NSFWText" ], - "installed_at": "2026-08-18T07:03:49.839668+00:00", + "installed_at": "2026-08-26T05:41:36.879459+00:00", "package_name": "guardrails-grhub-nsfw-text" } } diff --git a/backend/Dockerfile b/backend/Dockerfile index 07984290..9197ec7e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -58,8 +58,8 @@ ENV HF_HOME=/app/hf_cache RUN --mount=type=secret,id=HF_TOKEN \ HF_TOKEN="$(cat /run/secrets/HF_TOKEN 2>/dev/null || true)" \ /app/.venv/bin/python -c "from transformers import AutoTokenizer, AutoModelForSequenceClassification; \ -AutoTokenizer.from_pretrained('textdetox/xlmr-large-toxicity-classifier', cache_dir='/app/hf_cache'); \ -AutoModelForSequenceClassification.from_pretrained('textdetox/xlmr-large-toxicity-classifier', cache_dir='/app/hf_cache')" +AutoTokenizer.from_pretrained('michellejieli/NSFW_text_classifier', cache_dir='/app/hf_cache'); \ +AutoModelForSequenceClassification.from_pretrained('michellejieli/NSFW_text_classifier', cache_dir='/app/hf_cache')" RUN --mount=type=secret,id=HF_TOKEN \ HF_TOKEN="$(cat /run/secrets/HF_TOKEN 2>/dev/null || true)" \ @@ -67,13 +67,6 @@ RUN --mount=type=secret,id=HF_TOKEN \ AutoTokenizer.from_pretrained('dslim/bert-base-NER-uncased', cache_dir='/app/hf_cache'); \ AutoModelForTokenClassification.from_pretrained('dslim/bert-base-NER-uncased', cache_dir='/app/hf_cache')" -# Pre-install Guardrails hub validators so container startup is not blocked by downloads -RUN --mount=type=secret,id=GUARDRAILS_HUB_API_KEY \ - GUARDRAILS_HUB_API_KEY="$(cat /run/secrets/GUARDRAILS_HUB_API_KEY 2>/dev/null || true)" \ - ENABLE_METRICS="false" \ - ENABLE_REMOTE_INFERENCING="true" \ - /app/scripts/install_guardrails_from_hub.sh - # ------------------------------- # Entrypoint (runtime setup) # ------------------------------- diff --git a/backend/app/alembic/versions/011_rename_validator_log_request_id.py b/backend/app/alembic/versions/011_rename_validator_log_request_id.py new file mode 100644 index 00000000..63a490dc --- /dev/null +++ b/backend/app/alembic/versions/011_rename_validator_log_request_id.py @@ -0,0 +1,35 @@ +"""Rename validator_log.request_id to request_log_id + +The column is a foreign key to request_log.id (the surrogate PK), not to +request_log.request_id (the caller-supplied business id) - the old name +made it easy to join on the wrong column. + +Revision ID: 011 +Revises: 010 +Create Date: 2026-09-04 00:00:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "011" +down_revision = "010" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.alter_column("validator_log", "request_id", new_column_name="request_log_id") + op.execute( + "ALTER INDEX idx_validator_log_request_id RENAME TO idx_validator_log_request_log_id" + ) + + +def downgrade() -> None: + op.execute( + "ALTER INDEX idx_validator_log_request_log_id RENAME TO idx_validator_log_request_id" + ) + op.alter_column("validator_log", "request_log_id", new_column_name="request_id") diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 34ca5580..7804869d 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -50,7 +50,7 @@ def check_source_ip(request: Request) -> None: if client_ip not in allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Forbidden", + detail=f"Source IP '{client_ip}' is not permitted to access this service.", ) diff --git a/backend/app/api/routes/guardrails.py b/backend/app/api/routes/guardrails.py index d6158334..b4a53a45 100644 --- a/backend/app/api/routes/guardrails.py +++ b/backend/app/api/routes/guardrails.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, HTTPException from guardrails.guard import Guard from guardrails.validators import FailResult, PassResult +from opentelemetry import trace from sqlmodel import Session from app.api.deps import AuthDep, SessionDep, TenantContext @@ -44,6 +45,8 @@ from app.utils import APIResponse, load_description logger = logging.getLogger(__name__) +exec_logger = logging.getLogger("guardrails.execution") +tracer = trace.get_tracer(__name__) router = APIRouter(prefix="/guardrails", tags=["guardrails"]) @@ -339,7 +342,31 @@ def _finalize( try: guard = build_guard(validators) - result = guard.validate(data) + exec_logger.info( + "request=%s starting guardrails execution (validators=%s) input=%r", + request_log_id, + [v.type for v in validators], + _strip_for_log(data), + ) + # The only manual span in business code; per-validator detail lives + # in validator_log, joinable via kaapi.request_log_id. + with tracer.start_as_current_span( + "guardrails.validate", + attributes={ + "kaapi.request_log_id": str(request_log_id), + "kaapi.validator_types": [v.type for v in validators], + }, + ) as span: + result = guard.validate(data) + span.set_attribute("guardrails.passed", result.validated_output is not None) + exec_logger.info( + "request=%s guardrails execution finished (passed=%s) output=%r", + request_log_id, + result.validated_output is not None, + _strip_for_log(result.validated_output) + if result.validated_output is not None + else None, + ) # Case 1: validation passed OR failed-with-fix (on_fail=FIX) if result.validated_output is not None: @@ -407,6 +434,13 @@ def _redact_input(error_message: str, input_text: str) -> str: return error_message.replace(input_text, "") +def _strip_for_log(value, limit: int = 120) -> str: + text = " ".join(str(value).split()) + if len(text) > limit: + return f"{text[:limit]}…" + return text + + def _map_validator_configs( guard: Guard, validator_configs: list[ValidatorConfigItem] | None ) -> dict[str, ValidatorConfigItem]: @@ -455,15 +489,14 @@ def add_validator_logs( config_by_alias = _map_validator_configs(guard, validator_configs) + total_steps = len(iteration.outputs.validator_logs) + for order, log in enumerate(iteration.outputs.validator_logs, start=1): result = log.validation_result if result is None: continue - if suppress_pass_logs and isinstance(result, PassResult): - continue - error_message = None if isinstance(result, FailResult): error_message = result.error_message @@ -481,6 +514,33 @@ def add_validator_logs( stage = config.stage.value if config.stage else Stage.Input.value meta = config.model_dump(mode="json") + duration_ms = None + start_time = getattr(log, "start_time", None) + end_time = getattr(log, "end_time", None) + if start_time and end_time: + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + exec_logger.info( + "request=%s step=%d/%d validator=%s stage=%s outcome=%s " + "start=%s end=%s duration_ms=%s input=%r output=%r", + request_log_id, + order, + total_steps, + log.validator_name, + stage, + result.outcome, + start_time.isoformat() if start_time else None, + end_time.isoformat() if end_time else None, + duration_ms, + _strip_for_log(log.value_before_validation), + _strip_for_log(log.value_after_validation) + if log.value_after_validation is not None + else None, + ) + + if suppress_pass_logs and isinstance(result, PassResult): + continue + # Verdict detail the validator attached to its result (e.g. topic # relevance scope_score/reasoning); stored beside the config dump. result_metadata = getattr(result, "metadata", None) @@ -492,14 +552,8 @@ def add_validator_logs( json.dumps(result_metadata, default=str) ) - duration_ms = None - start_time = getattr(log, "start_time", None) - end_time = getattr(log, "end_time", None) - if start_time and end_time: - duration_ms = int((end_time - start_time).total_seconds() * 1000) - validator_log = ValidatorLog( - request_id=request_log_id, + request_log_id=request_log_id, organization_id=auth.organization_id, project_id=auth.project_id, name=log.validator_name, diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 85463882..5ab29b1e 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -49,6 +49,14 @@ class Settings(BaseSettings): AUTH_TOKEN: str PROJECT_NAME: str SENTRY_DSN: HttpUrl | None = None + SENTRY_TRACES_SAMPLE_RATE: float = 0.1 + OTEL_ENABLED: bool = False + OTEL_SERVICE_NAME: str = "kaapi-guardrails" + # Streams app logs to CloudWatch on top of terminal/file logging. + # No-op unless set (see app/core/logging_config.py). + CLOUDWATCH_LOG_GROUP: str | None = None + CLOUDWATCH_LOG_STREAM: str | None = None + AWS_REGION: str | None = None POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str diff --git a/backend/app/core/logging_config.py b/backend/app/core/logging_config.py new file mode 100644 index 00000000..47c552c8 --- /dev/null +++ b/backend/app/core/logging_config.py @@ -0,0 +1,66 @@ +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +LOG_DIR = Path(__file__).resolve().parents[2] / "logs" +LOG_FILE = LOG_DIR / "app.log" + +LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s" + + +def setup_logging() -> None: + root = logging.getLogger() + root.setLevel(logging.INFO) + root.handlers.clear() + + formatter = logging.Formatter(LOG_FORMAT) + + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(formatter) + root.addHandler(stream_handler) + + LOG_DIR.mkdir(parents=True, exist_ok=True) + file_handler = RotatingFileHandler( + LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5 + ) + file_handler.setFormatter(formatter) + root.addHandler(file_handler) + + if settings.CLOUDWATCH_LOG_GROUP: + cloudwatch_handler = _build_cloudwatch_handler(settings.CLOUDWATCH_LOG_GROUP) + if cloudwatch_handler is not None: + cloudwatch_handler.setFormatter(formatter) + root.addHandler(cloudwatch_handler) + + +def _build_cloudwatch_handler(log_group: str) -> logging.Handler | None: + try: + import boto3 + import watchtower + except ImportError: + logger.warning( + "CLOUDWATCH_LOG_GROUP is set but watchtower/boto3 aren't installed; " + "skipping CloudWatch log streaming." + ) + return None + + try: + boto3_client = boto3.client("logs", region_name=settings.AWS_REGION) + return watchtower.CloudWatchLogHandler( + log_group_name=log_group, + log_stream_name=settings.CLOUDWATCH_LOG_STREAM + or f"{settings.PROJECT_NAME}-{settings.ENVIRONMENT}", + boto3_client=boto3_client, + create_log_group=True, + ) + except Exception: + logger.warning( + "Failed to initialize CloudWatch log handler; continuing without it.", + exc_info=True, + ) + return None diff --git a/backend/app/core/middleware.py b/backend/app/core/middleware.py index 90013f3f..1952d15c 100644 --- a/backend/app/core/middleware.py +++ b/backend/app/core/middleware.py @@ -1,6 +1,8 @@ import logging import time +import sentry_sdk +from asgi_correlation_id import correlation_id from fastapi import Request, Response logger = logging.getLogger("http_request_logger") @@ -8,6 +10,20 @@ async def http_request_logger(request: Request, call_next) -> Response: start_time = time.time() + + # Tag Sentry events with the request's correlation id and tenant. + # set_tag is a safe no-op when Sentry is not initialized (tests, local). + request_id = correlation_id.get() + if request_id: + sentry_sdk.set_tag("correlation_id", request_id) + for header, tag in ( + ("X-ORGANIZATION-ID", "kaapi.organization_id"), + ("X-PROJECT-ID", "kaapi.project_id"), + ): + value = request.headers.get(header) + if value: + sentry_sdk.set_tag(tag, value) + try: response = await call_next(request) except Exception as e: diff --git a/backend/app/core/telemetry.py b/backend/app/core/telemetry.py new file mode 100644 index 00000000..f152cdb4 --- /dev/null +++ b/backend/app/core/telemetry.py @@ -0,0 +1,81 @@ +"""Single home for all observability wiring: Sentry (errors/logs) + OTel (traces). + +Spans are produced only by the auto-instrumentors below plus two manual sites: +the guardrails.validate span in the guardrails route and the traced litellm +completion wrapper in llm_utils. Do not add instrumentation elsewhere. +""" + +import logging + +import sentry_sdk +from fastapi import FastAPI +from guardrails.settings import settings as guardrails_settings +from opentelemetry import trace +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.propagate import set_global_textmap +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from sentry_sdk.integrations.opentelemetry import SentryPropagator, SentrySpanProcessor + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +# Health checks are uptime noise, not traffic worth tracing. +EXCLUDED_URLS = "utils/health-check" + + +def _scrub_event(event: dict, hint: dict) -> dict: + # Request bodies are end-user text (potential PII). The full payload is + # already persisted in request_log.metadata; Sentry only needs the pointer. + request = event.get("request") + if isinstance(request, dict): + request.pop("data", None) + return event + + +def setup_telemetry(app: FastAPI) -> None: + """Initialize Sentry + OpenTelemetry. Call once from main.py. + + No-op unless OTEL_ENABLED is set, without a SENTRY_DSN, or in the + testing environment, so tests and local runs stay instrumentation-free. + """ + if not settings.OTEL_ENABLED or not settings.SENTRY_DSN or settings.ENVIRONMENT == "testing": + return + + sentry_sdk.init( + dsn=str(settings.SENTRY_DSN), + environment=settings.ENVIRONMENT, + # Spans come exclusively from OTel; Sentry's own span creation is off. + instrumenter="otel", + traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE, + send_default_pii=False, + # Guardrails input/output is end-user text; never attach request/response + # bodies to error events or trace transactions. + max_request_body_size="never", + enable_logs=True, # INFO+ log records are shipped to Sentry Logs + before_send=_scrub_event, + ) + + # guardrails-ai emits its own "guard"/"step"/"call"/".validate" + # spans via the same global tracer; too granular for our traces. + guardrails_settings.disable_tracing = True + + resource = Resource.create({SERVICE_NAME: settings.OTEL_SERVICE_NAME}) + provider = TracerProvider(resource=resource) + provider.add_span_processor(SentrySpanProcessor()) + trace.set_tracer_provider(provider) + set_global_textmap(SentryPropagator()) + + FastAPIInstrumentor.instrument_app( + app, excluded_urls=EXCLUDED_URLS, exclude_spans=["receive", "send"] + ) + HTTPXClientInstrumentor().instrument() + RequestsInstrumentor().instrument() + # No SQLAlchemyInstrumentor: DB spans are noise, not useful here. + LoggingInstrumentor().instrument(set_logging_format=False) + + logger.info("Telemetry initialized (environment=%s)", settings.ENVIRONMENT) diff --git a/backend/app/core/validators/README.md b/backend/app/core/validators/README.md index ab961eed..85e7a52f 100644 --- a/backend/app/core/validators/README.md +++ b/backend/app/core/validators/README.md @@ -425,7 +425,7 @@ Code: What it does: -- Classifies text as NSFW (not safe for work) using a [HuggingFace transformer model](https://huggingface.co/textdetox/xlmr-large-toxicity-classifier). +- Classifies text as NSFW (not safe for work) using a [HuggingFace transformer model](https://huggingface.co/michellejieli/NSFW_text_classifier). - Validates at the sentence level by default; fails if any sentence exceeds the configured threshold. Why this is used: @@ -446,7 +446,7 @@ Parameters / customization: - `"sentence"`: each sentence is classified independently; validation fails if **any** sentence exceeds the threshold. Preferred when inputs are multi-sentence and you want to catch a single offensive sentence without failing the whole message. - `"full"`: the entire text is passed as one unit for classification. Use when inputs are short (single-sentence messages or responses) or when you want to evaluate overall tone rather than per-sentence content. - `device: str | None` (default: `"cpu"`) — inference device (`"cpu"` or `"cuda"`) -- `model_name: str | None` (default: `"textdetox/xlmr-large-toxicity-classifier"`) — HuggingFace model identifier used for classification. Other acceptable value: `"michellejieli/NSFW_text_classifier"` +- `model_name: str | None` (default: `"michellejieli/NSFW_text_classifier"`) — HuggingFace model identifier used for classification. Must emit an `"NSFW"` label — `guardrails_ai.nsfw_text.NSFWText.is_nsfw()` only matches `label == "NSFW"`, so a differently-labeled classifier (e.g. a toxicity/hate-speech model) will never fail validation regardless of score. - `on_fail` Notes / limitations: @@ -592,6 +592,39 @@ Tuning strategy: - Review false positives/false negatives by validator and stage. - Iterate on per-validator parameters (`severity`, `threshold`, `categories`, `banned_words`). +## Convention: `use_local=True` for every `guardrails_ai` validator + +Guardrails Hub's hosted inference (`hub.api.guardrailsai.com`) and its CLI/registry sunset on +2026-08-25. Every validator here now runs from either a `guardrails_ai.*` PyPI package or fully +custom in-repo code — none of them should ever call out to that sunset service. + +The footgun: `guardrails.validators.Validator.__init__` only forces local inference when a config +explicitly passes `use_local=True`. If it's omitted, the base class falls back to +`not get_use_remote_inference(settings.rc)` — i.e. it silently follows whatever +`use_remote_inferencing` is set to in the developer's own `~/.guardrailsrc`, a machine-level file +outside this repo. Some `guardrails_ai` validators declare `has_guardrails_endpoint=True` and have +a real remote code path (e.g. `NSFWText`); for those, an unset `use_local` on a machine with +`use_remote_inferencing=true` means the validator quietly tries to hit the sunset Hub endpoint +instead of running the local model. + +**Rule: before adding or upgrading a `guardrails_ai.*` validator, check whether it declares +`has_guardrails_endpoint=True` in its `@register_validator(...)` decorator (grep the installed +package). If it does, its config's `build()` must pass `use_local=True` explicitly** — that's +the only way to force local inference regardless of what a teammate's `~/.guardrailsrc` says. +`NSFWText` is the current example (`nsfw_text_safety_validator_config.py`); its own +`__init__` reads `self.use_local` to decide whether to load the HF pipeline locally or fall +through to a remote call against the (sunset) Hub inference endpoint. + +This is **not** a blanket rule to add `use_local=True` everywhere — most validator constructors +here don't even accept it. `BanList`'s `__init__` has no `**kwargs` at all and raises +`TypeError: unexpected keyword argument 'use_local'` if you pass it. `ProfanityFree` and +`LLMCritic` accept it silently via `**kwargs` but never read it — neither declares +`has_guardrails_endpoint=True`, so it's a no-op either way. Check the constructor and the +decorator before adding the kwarg to a new validator's `build()`. The fully custom in-repo +validators (`pii_remover`, `gender_assumption_bias`, `uli_slur_match`, `topic_relevance`, +`topic_relevance_llm`, `answer_relevance_custom_llm`) have no hub/remote code path at all and +don't need it. + ## Related Files - `backend/app/core/validators/validators.json` diff --git a/backend/app/core/validators/answer_relevance_custom_llm.py b/backend/app/core/validators/answer_relevance_custom_llm.py index 4c562857..829b885c 100644 --- a/backend/app/core/validators/answer_relevance_custom_llm.py +++ b/backend/app/core/validators/answer_relevance_custom_llm.py @@ -10,8 +10,6 @@ Validator, register_validator, ) -from litellm import completion - from app.core.config import settings from app.core.constants import ( ANSWER_RELEVANCE_EMPTY_FIELDS_ERROR, @@ -20,6 +18,7 @@ ANSWER_RELEVANCE_NOT_RELEVANT_ERROR, ANSWER_RELEVANCE_UNEXPECTED_RESPONSE_TEMPLATE, ) +from app.core.validators.llm_utils import traced_completion as completion DEFAULT_PROMPT_TEMPLATE = ( "Query: {query}\n" diff --git a/backend/app/core/validators/config/ban_list_safety_validator_config.py b/backend/app/core/validators/config/ban_list_safety_validator_config.py index 6279c3c3..670e3e28 100644 --- a/backend/app/core/validators/config/ban_list_safety_validator_config.py +++ b/backend/app/core/validators/config/ban_list_safety_validator_config.py @@ -1,7 +1,7 @@ from typing import List, Literal, Optional from uuid import UUID -from guardrails.hub import BanList +from guardrails_ai.ban_list import BanList from pydantic import model_validator from app.core.validators.config.base_validator_config import BaseValidatorConfig diff --git a/backend/app/core/validators/config/llamaguard_7b_safety_validator_config.py b/backend/app/core/validators/config/llamaguard_7b_safety_validator_config.py index 54a14098..a94aaefa 100644 --- a/backend/app/core/validators/config/llamaguard_7b_safety_validator_config.py +++ b/backend/app/core/validators/config/llamaguard_7b_safety_validator_config.py @@ -1,9 +1,14 @@ from typing import List, Literal, Optional -from guardrails.hub import LlamaGuard7B - from app.core.validators.config.base_validator_config import BaseValidatorConfig +# guardrails.hub is unavailable: the Guardrails Hub CLI/registry and its hosted +# inference servers were shut down 2026-08-25. The PyPI replacement +# (guardrails-ai-llamaguard-7b) only supports remote inference via a +# self-hosted validation_endpoint, which we don't have yet. Import is deferred +# to build() so this module (and the schema/registry that imports it) stays +# loadable; only actually building this validator fails until that's resolved. + POLICY_NAME_MAP = { "no_violence_hate": "O1", "no_sexual_content": "O2", @@ -32,6 +37,8 @@ def _resolve_policies(self) -> Optional[List[str]]: return resolved def build(self): + from guardrails.hub import LlamaGuard7B + return LlamaGuard7B( policies=self._resolve_policies(), on_fail=self.resolve_on_fail(), # type: ignore[arg-type] diff --git a/backend/app/core/validators/config/llm_critic_safety_validator_config.py b/backend/app/core/validators/config/llm_critic_safety_validator_config.py index 4d31d587..56ec5d9d 100644 --- a/backend/app/core/validators/config/llm_critic_safety_validator_config.py +++ b/backend/app/core/validators/config/llm_critic_safety_validator_config.py @@ -1,6 +1,6 @@ from typing import Literal -from guardrails.hub import LLMCritic +from guardrails_ai.llm_critic import LLMCritic from app.core.config import settings from app.core.constants import LLM_CRITIC_REPHRASE_MESSAGE diff --git a/backend/app/core/validators/config/nsfw_text_safety_validator_config.py b/backend/app/core/validators/config/nsfw_text_safety_validator_config.py index 092fee6e..3ab26290 100644 --- a/backend/app/core/validators/config/nsfw_text_safety_validator_config.py +++ b/backend/app/core/validators/config/nsfw_text_safety_validator_config.py @@ -1,6 +1,6 @@ from typing import Literal, Optional -from guardrails.hub import NSFWText +from guardrails_ai.nsfw_text import NSFWText from app.core.validators.config.base_validator_config import BaseValidatorConfig @@ -10,7 +10,10 @@ class NSFWTextSafetyValidatorConfig(BaseValidatorConfig): threshold: float = 0.8 validation_method: str = "sentence" device: Optional[str] = "cpu" - model_name: Optional[str] = "textdetox/xlmr-large-toxicity-classifier" + # Must be a model whose text-classification pipeline emits a "NSFW" label, + # since NSFWText.is_nsfw() only matches label == "NSFW" (see guardrails_ai.nsfw_text) — + # a toxicity/hate-speech classifier's labels never match, so it would never fail. + model_name: Optional[str] = "michellejieli/NSFW_text_classifier" def build(self): return NSFWText( @@ -19,4 +22,5 @@ def build(self): device=self.device, model_name=self.model_name, on_fail=self.resolve_on_fail(), + use_local=True, ) diff --git a/backend/app/core/validators/config/profanity_free_safety_validator_config.py b/backend/app/core/validators/config/profanity_free_safety_validator_config.py index dd6d774a..3361f2e1 100644 --- a/backend/app/core/validators/config/profanity_free_safety_validator_config.py +++ b/backend/app/core/validators/config/profanity_free_safety_validator_config.py @@ -1,6 +1,6 @@ from typing import Literal -from guardrails.hub import ProfanityFree +from guardrails_ai.profanity_free import ProfanityFree from app.core.validators.config.base_validator_config import BaseValidatorConfig diff --git a/backend/app/core/validators/llm_utils.py b/backend/app/core/validators/llm_utils.py index cd1f5700..533fcfc5 100644 --- a/backend/app/core/validators/llm_utils.py +++ b/backend/app/core/validators/llm_utils.py @@ -1,8 +1,43 @@ -from litellm import get_supported_openai_params +from litellm import completion, get_supported_openai_params +from opentelemetry import trace + +_tracer = trace.get_tracer(__name__) # Passed to litellm/OpenAI to force a strict JSON object response. JSON_OBJECT_RESPONSE_FORMAT = {"type": "json_object"} + +def traced_completion(**kwargs): + """litellm.completion wrapped in an OTel span with GenAI semantic attributes. + + The single choke point for LLM observability — validators import this + (aliased as ``completion``) instead of instrumenting their own calls. + """ + model = str(kwargs.get("model", "unknown")) + with _tracer.start_as_current_span( + f"chat {model}", + attributes={ + "gen_ai.operation.name": "chat", + "gen_ai.system": "litellm", + "gen_ai.request.model": model, + }, + ) as span: + response = completion(**kwargs) + response_model = getattr(response, "model", None) + if response_model: + span.set_attribute("gen_ai.response.model", response_model) + usage = getattr(response, "usage", None) + for attribute, field in ( + ("gen_ai.usage.input_tokens", "prompt_tokens"), + ("gen_ai.usage.output_tokens", "completion_tokens"), + ("gen_ai.usage.total_tokens", "total_tokens"), + ): + value = getattr(usage, field, None) + if isinstance(value, int): + span.set_attribute(attribute, value) + return response + + # Models known to support JSON-object response_format that litellm may not list yet. _KNOWN_JSON_CAPABLE_MODELS = frozenset( { diff --git a/backend/app/core/validators/topic_relevance.py b/backend/app/core/validators/topic_relevance.py index c98d446c..d0aa2c21 100644 --- a/backend/app/core/validators/topic_relevance.py +++ b/backend/app/core/validators/topic_relevance.py @@ -5,7 +5,7 @@ from typing import Callable, Optional from guardrails import OnFailAction -from guardrails.hub import LLMCritic +from guardrails_ai.llm_critic import LLMCritic from guardrails.validators import ( FailResult, PassResult, diff --git a/backend/app/core/validators/topic_relevance_llm.py b/backend/app/core/validators/topic_relevance_llm.py index 866cdd62..bde247db 100644 --- a/backend/app/core/validators/topic_relevance_llm.py +++ b/backend/app/core/validators/topic_relevance_llm.py @@ -13,13 +13,12 @@ Validator, register_validator, ) -from litellm import completion - from app.core.config import settings from app.core.constants import EMPTY_MESSAGE_ERROR, TOPIC_OUT_OF_SCOPE_ERROR from app.core.validators.llm_utils import ( JSON_OBJECT_RESPONSE_FORMAT, supports_response_format, + traced_completion as completion, ) _PROMPTS_DIR = Path(__file__).parent / "prompts" / "topic_relevance_llm" diff --git a/backend/app/core/validators/validators.json b/backend/app/core/validators/validators.json index a1a7b37d..6e08bb75 100644 --- a/backend/app/core/validators/validators.json +++ b/backend/app/core/validators/validators.json @@ -18,12 +18,12 @@ { "type": "ban_list", "version": "0.1.0", - "source": "hub://guardrails/ban_list" + "source": "pypi://guardrails-ai-ban-list" }, { "type": "llm_critic", "version": "0.1.0", - "source": "hub://guardrails/llm_critic" + "source": "pypi://guardrails-ai-llm-critic" }, { "type": "topic_relevance", @@ -43,12 +43,12 @@ { "type": "profanity_free", "version": "0.1.0", - "source": "hub://guardrails/profanity_free" + "source": "pypi://guardrails-ai-profanity-free" }, { "type": "nsfw_text", "version": "0.1.0", - "source": "hub://guardrails/nsfw_text" + "source": "pypi://guardrails-ai-nsfw-text" } ] } \ No newline at end of file diff --git a/backend/app/crud/ban_list.py b/backend/app/crud/ban_list.py index 819cfe7b..5b3fe849 100644 --- a/backend/app/crud/ban_list.py +++ b/backend/app/crud/ban_list.py @@ -1,3 +1,4 @@ +import logging from typing import List, Optional from uuid import UUID @@ -9,6 +10,8 @@ from app.schemas.ban_list import BanListCreate, BanListUpdate from app.utils import now +logger = logging.getLogger(__name__) + class BanListCrud: def create( @@ -29,14 +32,30 @@ def create( session.commit() except IntegrityError: session.rollback() + logger.warning( + "create ban list failed: duplicate (org=%s project=%s)", + organization_id, + project_id, + ) raise HTTPException( 400, "Ban list already exists for the given configuration" ) except Exception: session.rollback() + logger.exception( + "create ban list failed (org=%s project=%s)", + organization_id, + project_id, + ) raise session.refresh(ban_list) + logger.info( + "created ban list id=%s org=%s project=%s", + ban_list.id, + organization_id, + project_id, + ) return ban_list def get( @@ -113,14 +132,17 @@ def update( session.commit() except IntegrityError: session.rollback() + logger.warning("update ban list %s failed: duplicate", ban_list.id) raise HTTPException( 400, "Ban list already exists for the given configuration" ) except Exception: session.rollback() + logger.exception("update ban list %s failed", ban_list.id) raise session.refresh(ban_list) + logger.info("updated ban list id=%s", ban_list.id) return ban_list def delete(self, session: Session, ban_list: BanList): @@ -129,7 +151,9 @@ def delete(self, session: Session, ban_list: BanList): session.commit() except Exception: session.rollback() + logger.exception("delete ban list %s failed", ban_list.id) raise + logger.info("deleted ban list id=%s", ban_list.id) def check_owner( self, ban_list: BanList, organization_id: int, project_id: int diff --git a/backend/app/crud/llm_prompt_config.py b/backend/app/crud/llm_prompt_config.py index 86932dae..177070fc 100644 --- a/backend/app/crud/llm_prompt_config.py +++ b/backend/app/crud/llm_prompt_config.py @@ -1,3 +1,4 @@ +import logging from uuid import UUID from fastapi import HTTPException @@ -14,6 +15,8 @@ ) from app.utils import now +logger = logging.getLogger(__name__) + class LLMPromptConfigCrud: def create( @@ -33,12 +36,28 @@ def create( session.commit() except IntegrityError: session.rollback() + logger.warning( + "create llm prompt config failed: duplicate (org=%s project=%s)", + organization_id, + project_id, + ) raise HTTPException(400, DUPLICATE_LLM_PROMPT_CONFIG_ERROR) except Exception: session.rollback() + logger.exception( + "create llm prompt config failed (org=%s project=%s)", + organization_id, + project_id, + ) raise session.refresh(obj) + logger.info( + "created llm prompt config id=%s org=%s project=%s", + obj.id, + organization_id, + project_id, + ) return obj def get( @@ -114,12 +133,15 @@ def update( session.commit() except IntegrityError: session.rollback() + logger.warning("update llm prompt config %s failed: duplicate", obj.id) raise HTTPException(400, DUPLICATE_LLM_PROMPT_CONFIG_ERROR) except Exception: session.rollback() + logger.exception("update llm prompt config %s failed", obj.id) raise session.refresh(obj) + logger.info("updated llm prompt config id=%s", obj.id) return obj def delete(self, session: Session, obj: LLMPromptConfig) -> None: @@ -128,7 +150,9 @@ def delete(self, session: Session, obj: LLMPromptConfig) -> None: session.commit() except Exception: session.rollback() + logger.exception("delete llm prompt config %s failed", obj.id) raise + logger.info("deleted llm prompt config id=%s", obj.id) llm_prompt_config_crud = LLMPromptConfigCrud() diff --git a/backend/app/crud/validator_config.py b/backend/app/crud/validator_config.py index 5ef9028b..14422327 100644 --- a/backend/app/crud/validator_config.py +++ b/backend/app/crud/validator_config.py @@ -41,12 +41,24 @@ def create( session.commit() except IntegrityError: session.rollback() + logger.warning( + "create validator config failed: duplicate name (org=%s project=%s)", + organization_id, + project_id, + ) raise HTTPException( 400, "Validator configuration with this name already exists", ) session.refresh(obj) + logger.info( + "created validator config id=%s org=%s project=%s type=%s", + obj.id, + organization_id, + project_id, + obj.type, + ) return self.flatten(obj) def list( @@ -111,15 +123,20 @@ def update(self, session: Session, obj: ValidatorConfig, update_data: dict) -> d session.commit() except IntegrityError: session.rollback() + logger.warning( + "update validator config %s failed: duplicate name", obj.id + ) raise HTTPException( 400, "Validator configuration with this name already exists", ) except Exception: session.rollback() + logger.exception("update validator config %s failed", obj.id) raise session.refresh(obj) + logger.info("updated validator config id=%s", obj.id) return self.flatten(obj) def delete(self, session: Session, obj: ValidatorConfig): @@ -128,7 +145,9 @@ def delete(self, session: Session, obj: ValidatorConfig): session.commit() except Exception: session.rollback() + logger.exception("delete validator config %s failed", obj.id) raise + logger.info("deleted validator config id=%s", obj.id) def flatten(self, row: ValidatorConfig) -> dict: base = row.model_dump(exclude={"config"}) diff --git a/backend/app/evaluation/ban_list/run.py b/backend/app/evaluation/ban_list/run.py index cc5fce89..89eebfb2 100644 --- a/backend/app/evaluation/ban_list/run.py +++ b/backend/app/evaluation/ban_list/run.py @@ -1,7 +1,7 @@ from pathlib import Path import pandas as pd -from guardrails.hub import BanList +from guardrails_ai.ban_list import BanList from guardrails.validators import FailResult from app.evaluation.common.helper import ( diff --git a/backend/app/evaluation/toxicity/run.py b/backend/app/evaluation/toxicity/run.py index d9998b45..faf537cb 100644 --- a/backend/app/evaluation/toxicity/run.py +++ b/backend/app/evaluation/toxicity/run.py @@ -1,6 +1,8 @@ from pathlib import Path import pandas as pd -from guardrails.hub import LlamaGuard7B, NSFWText, ProfanityFree +from guardrails.hub import LlamaGuard7B # noqa: not yet migrated, see backend README +from guardrails_ai.nsfw_text import NSFWText +from guardrails_ai.profanity_free import ProfanityFree from guardrails.validators import FailResult from app.evaluation.common.helper import ( diff --git a/backend/app/main.py b/backend/app/main.py index 4aa9ac60..874c61c4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,13 +1,17 @@ from asgi_correlation_id.middleware import CorrelationIdMiddleware -import sentry_sdk from fastapi import FastAPI from fastapi.routing import APIRoute -from app.api.main import api_router -from app.core.config import settings -from app.core.exception_handlers import register_exception_handlers -from app.core.middleware import http_request_logger -from app.load_env import load_environment +from app.core.logging_config import setup_logging + +setup_logging() + +from app.api.main import api_router # noqa: E402 +from app.core.config import settings # noqa: E402 +from app.core.exception_handlers import register_exception_handlers # noqa: E402 +from app.core.middleware import http_request_logger # noqa: E402 +from app.core.telemetry import setup_telemetry # noqa: E402 +from app.load_env import load_environment # noqa: E402 # Load environment variables load_environment() @@ -18,9 +22,6 @@ def custom_generate_unique_id(route: APIRoute) -> str: return f"{tag}-{route.name}" -if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": - sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) - app = FastAPI( title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json", @@ -33,3 +34,5 @@ def custom_generate_unique_id(route: APIRoute) -> str: app.include_router(api_router, prefix=settings.API_V1_STR) register_exception_handlers(app) + +setup_telemetry(app) diff --git a/backend/app/models/logging/validator_log.py b/backend/app/models/logging/validator_log.py index 6ede6b10..7a50b6ab 100644 --- a/backend/app/models/logging/validator_log.py +++ b/backend/app/models/logging/validator_log.py @@ -34,7 +34,7 @@ class ValidatorLog(SQLModel, table=True): sa_column_kwargs={"comment": "Identifier for the project"}, ) - request_id: UUID = Field( + request_log_id: UUID = Field( foreign_key="request_log.id", nullable=False, sa_column_kwargs={"comment": "Foreign key to the associated request log entry"}, diff --git a/backend/app/tests/test_toxicity_hub_validators.py b/backend/app/tests/test_toxicity_hub_validators.py index 48235a79..848c8ac5 100644 --- a/backend/app/tests/test_toxicity_hub_validators.py +++ b/backend/app/tests/test_toxicity_hub_validators.py @@ -28,6 +28,12 @@ # --------------------------------------------------------------------------- +@pytest.mark.skip( + reason="llamaguard_7b is disabled: Guardrails Hub's hosted inference " + "(hub.api.guardrailsai.com) shut down 2026-08-25 and the PyPI replacement " + "has no local-inference support, so LlamaGuard7B is no longer importable " + "until a self-hosted endpoint (or a replacement validator) is wired up." +) class TestLlamaGuard7BSafetyValidatorConfig: def test_build_with_default_policies(self): config = LlamaGuard7BSafetyValidatorConfig(type="llamaguard_7b") @@ -304,7 +310,7 @@ def test_build_with_defaults(self): assert kwargs["threshold"] == 0.8 assert kwargs["validation_method"] == "sentence" assert kwargs["device"] == "cpu" - assert kwargs["model_name"] == "textdetox/xlmr-large-toxicity-classifier" + assert kwargs["model_name"] == "michellejieli/NSFW_text_classifier" def test_build_with_custom_params(self): config = NSFWTextSafetyValidatorConfig( diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 455c0653..5b8cfd56 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -19,7 +19,12 @@ dependencies = [ # Pin bcrypt until passlib supports the latest "bcrypt==4.3.0", "pydantic-settings<3.0.0,>=2.2.1", - "sentry-sdk[fastapi]<2.0.0,>=1.40.6", + "sentry-sdk[fastapi]>=2.35,<3.0", + "opentelemetry-sdk>=1.27", + "opentelemetry-instrumentation-fastapi", + "opentelemetry-instrumentation-httpx", + "opentelemetry-instrumentation-requests", + "opentelemetry-instrumentation-logging", "pyjwt<3.0.0,>=2.8.0", "asgi-correlation-id>=4.3.4", "guardrails-ai", @@ -35,6 +40,10 @@ dependencies = [ "transformers>=5.0.0", "torch>=2.0.0", "guardrails-ai-ban-list>=0.1.0", + "guardrails-ai-llm-critic>=0.1.0", + "guardrails-ai-profanity-free>=0.1.0", + "guardrails-ai-nsfw-text>=0.1.0", + "watchtower>=3.3.1", ] [dependency-groups] diff --git a/backend/scripts/entrypoint.sh b/backend/scripts/entrypoint.sh index e27ec06a..97c9e0fe 100644 --- a/backend/scripts/entrypoint.sh +++ b/backend/scripts/entrypoint.sh @@ -1,8 +1,5 @@ #!/usr/bin/env bash set -euo pipefail -echo "Running Guardrails setup..." -./scripts/install_guardrails_from_hub.sh - echo "Starting FastAPI server..." exec fastapi run --workers 4 app/main.py diff --git a/backend/uv.lock b/backend/uv.lock index ec7ed0b7..885b9015 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -137,6 +137,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] +[[package]] +name = "alt-profanity-check" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "joblib", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/5e/3118eceabf5f2b46208bcf9ff4436964486f26b7ceb03c0e293b27c7fb9c/alt_profanity_check-1.7.2.tar.gz", hash = "sha256:f2569e4cc8dfb1813b384ad59e88c5929216354b68213016acd00d2196357f14", size = 759265, upload-time = "2025-09-11T13:40:55.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/e7/0c4d8470b15cd3160ec2fe157833b12d2172c31d53f6471e51b2c8c9e24b/alt_profanity_check-1.7.2-py3-none-any.whl", hash = "sha256:444cfd8cfd8007a6b54ea96b3695aefcdb1dd7e54e0ed4d2140745b77a0b273a", size = 758613, upload-time = "2025-09-11T13:40:53.865Z" }, +] + +[[package]] +name = "alt-profanity-check" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "joblib", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/2c76f3a39ace363d25dbba5cd908140bfb0a0576dc850a1e1b680916cd59/alt_profanity_check-1.8.0.tar.gz", hash = "sha256:bdfcd0f9ca205065b2dcf05b5cfb5f4fe2528ebe4b0b4d3c6649334ab8159c82", size = 759273, upload-time = "2026-01-14T15:29:35.356Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/95/6e4482366491517b74b75158dbd5fdbeb937e088798e8a5a6aaf212d60e4/alt_profanity_check-1.8.0-py3-none-any.whl", hash = "sha256:341065c14ccc231ddba9f963482ea1899cffbdb2d4db2e459f01bddaeb3a2c23", size = 758642, upload-time = "2026-01-14T15:29:33.802Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -184,11 +224,19 @@ dependencies = [ { name = "ftfy" }, { name = "guardrails-ai" }, { name = "guardrails-ai-ban-list" }, + { name = "guardrails-ai-llm-critic" }, + { name = "guardrails-ai-nsfw-text" }, + { name = "guardrails-ai-profanity-free" }, { name = "httpx" }, { name = "huggingface-hub" }, { name = "jinja2" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-sdk" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "passlib", extra = ["bcrypt"] }, @@ -208,6 +256,7 @@ dependencies = [ { name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, { name = "transformers" }, + { name = "watchtower" }, ] [package.dev-dependencies] @@ -234,10 +283,18 @@ requires-dist = [ { name = "ftfy" }, { name = "guardrails-ai", git = "https://github.com/guardrails-ai/guardrails.git?rev=379ab72ff643d0d9b8dff4f74c956ce29eab341d" }, { name = "guardrails-ai-ban-list", specifier = ">=0.1.0" }, + { name = "guardrails-ai-llm-critic", specifier = ">=0.1.0" }, + { name = "guardrails-ai-nsfw-text", specifier = ">=0.1.0" }, + { name = "guardrails-ai-profanity-free", specifier = ">=0.1.0" }, { name = "httpx", specifier = ">=0.25.1,<1.0.0" }, { name = "huggingface-hub", specifier = ">=1.5.0,<2.0" }, { name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, { name = "numpy", specifier = ">=1.24.0" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-sdk", specifier = ">=1.27" }, { name = "pandas", specifier = ">=2.3.2" }, { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4,<2.0.0" }, { name = "presidio-analyzer", specifier = ">=2.2.360" }, @@ -249,11 +306,12 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.0,<2.0.0" }, { name = "python-multipart", specifier = ">=0.0.7,<1.0.0" }, { name = "scikit-learn", specifier = ">=1.6.0,<2.0.0" }, - { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=1.40.6,<2.0.0" }, + { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.35,<3.0" }, { name = "sqlmodel", specifier = ">=0.0.21,<1.0.0" }, { name = "tenacity", specifier = ">=8.2.3,<9.0.0" }, { name = "torch", specifier = ">=2.0.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "transformers", specifier = ">=5.0.0" }, + { name = "watchtower", specifier = ">=3.3.1" }, ] [package.metadata.requires-dev] @@ -294,6 +352,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl", hash = "sha256:36ce69b06c7d96b4acb89c7556a4c4f01a972463d3d49c675026cbbd08e9a0a2", size = 15262, upload-time = "2024-10-17T11:44:28.739Z" }, ] +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -410,6 +480,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/07/4cdc81a47bf862c0b06d91f1bc6782064e8b69ac9b5d4ff51d97e4ff03da/blis-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:7a0fc4b237a3a453bdc3c7ab48d91439fcd2d013b665c46948d9eaf9c3e45a97", size = 6192624, upload-time = "2025-11-17T12:28:14.197Z" }, ] +[[package]] +name = "boto3" +version = "1.43.87" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/0c/b14374e9458030076cd22ff9381cf86d170f31b648fd901db1d88011094b/boto3-1.43.87.tar.gz", hash = "sha256:8d9521c7c292194b8ce9fb61043d52e45cdba29b5f690981f3eb5e75103ba57d", size = 112691, upload-time = "2026-09-02T19:23:13.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a9/7dd7cc1a2387f3c78f66e49682957b7e7d590706dc6552d108b48125022d/boto3-1.43.87-py3-none-any.whl", hash = "sha256:671cf88a353f887774603980898bc34249d05a994e1c5e24aba4f4e1bbef0951", size = 140026, upload-time = "2026-09-02T19:23:12.044Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.87" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c4/64ebb159810a9840c57659f3ed98439bbc5680dc9708e3b08212deea301a/botocore-1.43.87.tar.gz", hash = "sha256:928598e7275fa70385d7f694d60d59afe115a0b914cc699dbf3eb60954c23bf1", size = 16064894, upload-time = "2026-09-02T19:23:08.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/5c/f15aa7a5da42fc85254f0150df7d377bb965b0c130c68f9701fba88e0df4/botocore-1.43.87-py3-none-any.whl", hash = "sha256:01e1e2255a8f4b959c19b210d74a5a916d13841c0344ceee15d8a3eaa4daa892", size = 15755750, upload-time = "2026-09-02T19:23:05.937Z" }, +] + [[package]] name = "cachetools" version = "7.0.1" @@ -832,6 +930,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/3f/35701c13e1fc7b0895198c8b20068c569a841e0daf8e0b14d1dc0816b28f/cymem-2.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f", size = 38964, upload-time = "2025-11-14T14:58:02.87Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "diff-match-patch" version = "20241021" @@ -1351,6 +1458,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/06/f7e79429d68159af2a9a2504bf09dcc024eabacb036845da63f67c43f295/guardrails_ai_ban_list-0.1.0-py3-none-any.whl", hash = "sha256:c685527dc6f808d43bcc81e587b21e6ce20f4f855f248ec089959458c69b7bde", size = 4407, upload-time = "2026-06-24T19:15:57.353Z" }, ] +[[package]] +name = "guardrails-ai-llm-critic" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "guardrails-ai" }, + { name = "litellm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/3d/575913a2545820f923d1ad5391e79af6b5b848f493fdff6a0a1c370f9ee5/guardrails_ai_llm_critic-0.1.0.tar.gz", hash = "sha256:217f79390193db1e98721986ad845af569b2ac77f9e79cb67c0f2678ec867eb9", size = 6127, upload-time = "2026-06-29T03:56:55.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/66/50d0b4ec17cf944508702b7f5c1da959e6842248fe6d2029b8bfe683f4d5/guardrails_ai_llm_critic-0.1.0-py3-none-any.whl", hash = "sha256:7ddbfbc33ca085677202612548b92dd96cb2d2a19fb1958f119c1f5be195a395", size = 5397, upload-time = "2026-06-29T03:56:54.577Z" }, +] + +[[package]] +name = "guardrails-ai-nsfw-text" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "guardrails-ai" }, + { name = "nltk" }, + { name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/48/0bb6f01642f702ad92bb38cc1758533b3e55c1cc2c310851a7af4d36e317/guardrails_ai_nsfw_text-0.1.0.tar.gz", hash = "sha256:7ca73b4f83af1a9cb4b8f55595c25ce48535c67bf32e9c091f8cb49618d9acb4", size = 7445, upload-time = "2026-07-02T03:21:53.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/18/febbd6227b99d233fff814f09a9c36cca2f89cb4029aa7032313bdda44ad/guardrails_ai_nsfw_text-0.1.0-py3-none-any.whl", hash = "sha256:b99ed23a87ef81eb6186eaea0303c814dbd28e710dfb4d1a5cb90b6e096caa1d", size = 7514, upload-time = "2026-07-02T03:21:51.942Z" }, +] + +[[package]] +name = "guardrails-ai-profanity-free" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alt-profanity-check", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "alt-profanity-check", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "guardrails-ai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/c5/83032bbae451fffb77afbca46904f2254137e4fef0f604b36f092e05440a/guardrails_ai_profanity_free-0.1.0.tar.gz", hash = "sha256:a38915ad059c41fe2d89bfaf2ba52e14985fd98335373daf10f34d302bcaa002", size = 3899, upload-time = "2026-07-03T13:54:03.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b5/89d8d6816abbc49bcdbfa0d7fe93d1950cab018d10d1ee01f2fad48ff3d1/guardrails_ai_profanity_free-0.1.0-py3-none-any.whl", hash = "sha256:ab2ebc20270500c2a6f0b29f9cbd116fe5591cf398b4d5bdce717a71c8974aec", size = 4038, upload-time = "2026-07-03T13:54:02.787Z" }, +] + [[package]] name = "guardrails-ai-types" version = "0.4.0" @@ -1645,10 +1795,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, +] + [[package]] name = "joblib" version = "1.5.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'darwin'", +] sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, @@ -2458,6 +2640,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "nltk" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "defusedxml" }, + { name = "joblib", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "joblib", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -2622,32 +2821,31 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2658,14 +2856,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2676,48 +2874,149 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/03/a529140241addd4d0acc73bafbd6f74691651b92fc0ae9b4513cf80f07fa/opentelemetry_instrumentation_httpx-0.65b0.tar.gz", hash = "sha256:4627aa9c6bb99bf4462c8b565b0ef6aeb9ffad95c6c92868be1ef7895de112ee", size = 26309, upload-time = "2026-07-16T15:26:07.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/0f/c6144096b4914bbf44b43ba21c962e8f333ff045770b50a3e79ed8bd455f/opentelemetry_instrumentation_httpx-0.65b0-py3-none-any.whl", hash = "sha256:400f1b78afa4ee2332b5debe58e1ed1b317913d58812c952576be76660aeadb1", size = 17436, upload-time = "2026-07-16T15:25:15.772Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/84/f46bf7976a81827419b085c9ed14562410c7599e07509786e9f6eb3c5438/opentelemetry_instrumentation_requests-0.65b0.tar.gz", hash = "sha256:1d601548f89236d5ab373c7208a2e1e162a8d6462b5b972f9ad8fb0ed82d7438", size = 18107, upload-time = "2026-07-16T15:26:18.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/42/45/6201afe846263d775cd4fc8d6a87dc8c15eb7921cdade4dca1e1227b04b6/opentelemetry_instrumentation_requests-0.65b0-py3-none-any.whl", hash = "sha256:91688ec0d4d1fed75ea8d026ef2c66274ed9868c22b6be211ef85d832d16f957", size = 13386, upload-time = "2026-07-16T15:25:31.093Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b1" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] [[package]] @@ -3952,6 +4251,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + [[package]] name = "safetensors" version = "0.7.0" @@ -3987,7 +4298,7 @@ resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "joblib", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, @@ -4036,7 +4347,7 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "joblib", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, @@ -4201,15 +4512,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "1.45.1" +version = "2.68.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/28/02c0cd9184f9108e3c52519f9628b215077a3854240e0b17ae845e664855/sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26", size = 244774, upload-time = "2024-07-26T13:48:32.375Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/e7/c504a4bd2d95df2e0ab73714a9161ff1cf6ff1486922685e5f46dfd9eba8/sentry_sdk-2.68.1.tar.gz", hash = "sha256:6a97895230b04bc35d4d8d2e51e3b9e21902dfb0086ccf1f131a80c15c7b997a", size = 1019262, upload-time = "2026-08-24T13:09:38.108Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/9f/105366a122efa93f0cb1914f841747d160788e4d022d0488d2d44c2ba26c/sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1", size = 267163, upload-time = "2024-07-26T13:48:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/2b/28/465ad9382be98f2172e691f5836cf87f936773913ad7ab85ba1ba1d6706e/sentry_sdk-2.68.1-py3-none-any.whl", hash = "sha256:775b78871783a0ffd758276ad01b3bb2b1ebcdad8f9d2f0a7723f76b73c99b65", size = 520851, upload-time = "2026-08-24T13:09:36.186Z" }, ] [package.optional-dependencies] @@ -5058,6 +5369,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] +[[package]] +name = "watchtower" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/e1/40e6940383b7202e7c12343ab58c4a0acd5552217d829a4f0ed19cd9cf0b/watchtower-3.4.0.tar.gz", hash = "sha256:7d3c116aff72a73ce8f6fc0addd1d0daa04d3f9d53d87cedca3a5a65a264bf7d", size = 27128, upload-time = "2025-02-25T15:07:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/ac/7caa56d4cf82e66b5cdc46d7fa6edd0d4bcd407ec33e46f28a8be83cca28/watchtower-3.4.0-py3-none-any.whl", hash = "sha256:5eac65cbf2a7350bb43c3518485230a6135ed7dec7ccb88468828d68ab9fea26", size = 18022, upload-time = "2025-02-25T15:07:02.851Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" diff --git a/docker-compose.yml b/docker-compose.yml index 6c11e7a1..83a4c0ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,6 @@ version: "3.9" secrets: HF_TOKEN: environment: HF_TOKEN - GUARDRAILS_HUB_API_KEY: - environment: GUARDRAILS_HUB_API_KEY services: @@ -16,7 +14,6 @@ services: context: ./backend secrets: - HF_TOKEN - - GUARDRAILS_HUB_API_KEY command: bash scripts/prestart.sh env_file: - .env @@ -32,7 +29,6 @@ services: context: ./backend secrets: - HF_TOKEN - - GUARDRAILS_HUB_API_KEY env_file: - .env extra_hosts: