diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e4fa568..8b48845 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - name: Validate OpenAPI schema run: | - uv run --no-project --with 'fastapi>=0.115' --with 'pydantic>=2' python -c "import sys; sys.path.insert(0, '.'); from app.main import app; s = app.openapi(); assert '/predict' in s['paths'] and '/predict/bulk' in s['paths']; print('openapi ok:', list(s['paths']))" + uv run --no-project --with 'fastapi>=0.115' --with 'pydantic>=2' python -c "import sys; sys.path.insert(0, '.'); from app.main import app; s = app.openapi(); assert '/v1/systemone' in s['paths'] and '/predict' in s['paths']; print('openapi ok:', list(s['paths']))" # Pull requests: build the image once and check it boots. test: @@ -54,7 +54,7 @@ jobs: - name: Container smoke test run: | - docker run --rm --entrypoint python laya-api:test -c "from app.main import app; s = app.openapi(); assert '/predict' in s['paths']; print('container ok')" + docker run --rm --entrypoint python laya-api:test -c "from app.main import app; s = app.openapi(); assert '/v1/systemone' in s['paths'] and '/predict' in s['paths']; print('container ok')" # Nightly / manual / release: real model download + inference. smoke: @@ -66,17 +66,20 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build image + - name: Build image with baked english model uses: docker/build-push-action@v6 with: context: . load: true - tags: laya-api:test - cache-from: type=gha,scope=amd64 + tags: laya-api:smoke + build-args: | + PRELOAD_MODEL=1 + MODELS=english + cache-from: type=gha,scope=amd64-english - name: Full inference smoke test run: | - docker run -d --name laya-api -p 8000:8000 -e API_KEYS=test laya-api:test + docker run -d --name laya-api -p 8000:8000 -e API_KEYS=test laya-api:smoke for i in $(seq 1 150); do if curl -sf http://localhost:8000/healthz >/dev/null 2>&1; then break; fi sleep 2 @@ -85,27 +88,46 @@ jobs: curl -sf -X POST http://localhost:8000/predict \ -H 'X-API-Key: test' -H 'Content-Type: application/json' \ -d '{"state":"I was billed twice. Please refund.","questions":{"refund":{"type":"noul","instructions":"Does the customer ask for money back?"}}}' + curl -sf -X POST http://localhost:8000/v1/systemone \ + -H 'X-API-Key: test' -H 'Content-Type: application/json' \ + -d '{"state":"I was billed twice. Please refund.","model":"laya-english","questions":{"refund":{"type":"noul","instructions":"Does the customer ask for money back?"}}}' docker logs laya-api docker rm -f laya-api - # Build each platform natively (no QEMU) and push by digest. + # Build each platform and model variant natively (no QEMU) and push by digest. build: needs: lint if: github.event_name != 'pull_request' - runs-on: ${{ matrix.runner }} + runs-on: ${{ matrix.platform_info.runner }} permissions: contents: read packages: write strategy: fail-fast: false matrix: - include: + platform_info: - platform: linux/amd64 runner: ubuntu-latest scope: amd64 - platform: linux/arm64 runner: ubuntu-24.04-arm scope: arm64 + variant: + - name: default + preload: "0" + models: "english" + - name: english + preload: "1" + models: "english" + - name: multilingual + preload: "1" + models: "multilingual" + - name: typed-decisions + preload: "1" + models: "typed-decisions" + - name: all + preload: "1" + models: "english,multilingual,typed-decisions" steps: - uses: actions/checkout@v4 @@ -124,10 +146,13 @@ jobs: uses: docker/build-push-action@v6 with: context: . - platforms: ${{ matrix.platform }} + platforms: ${{ matrix.platform_info.platform }} + build-args: | + PRELOAD_MODEL=${{ matrix.variant.preload }} + MODELS=${{ matrix.variant.models }} outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=gha,scope=${{ matrix.scope }} - cache-to: type=gha,mode=max,scope=${{ matrix.scope }} + cache-from: type=gha,scope=${{ matrix.platform_info.scope }}-${{ matrix.variant.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform_info.scope }}-${{ matrix.variant.name }} - name: Export digest run: | @@ -138,24 +163,33 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v4 with: - name: digests-${{ matrix.scope }} + name: digests-${{ matrix.variant.name }}-${{ matrix.platform_info.scope }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 - # Combine the per-platform digests into one multi-arch manifest list. + # Combine per-platform digests into multi-arch manifest lists for each variant. merge: needs: build runs-on: ubuntu-latest permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + variant: + - name: default + - name: english + - name: multilingual + - name: typed-decisions + - name: all steps: - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests - pattern: digests-* + pattern: digests-${{ matrix.variant.name }}-* merge-multiple: true - name: Set up Docker Buildx @@ -174,10 +208,12 @@ jobs: with: images: ${{ env.IMAGE }} tags: | - type=raw,value=latest + type=raw,value=${{ matrix.variant.name == 'default' && 'latest' || matrix.variant.name }},enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + flavor: | + suffix=${{ matrix.variant.name == 'default' && '' || format('-{0}', matrix.variant.name) }} - name: Create manifest list and push working-directory: /tmp/digests @@ -186,4 +222,4 @@ jobs: $(printf '${{ env.IMAGE }}@sha256:%s ' *) - name: Inspect image - run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }} + run: docker buildx imagetools inspect $(jq -r '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON") diff --git a/Dockerfile b/Dockerfile index 5fdcf4c..319f928 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,20 +23,22 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY --chown=appuser:appuser app ./app +# Prepare the cache before downloading so the model layer is not duplicated by +# a later recursive chown. +RUN mkdir -p /data/hf && chown -R appuser:appuser /app /data/hf + +USER appuser + # Optional: bake the checkpoint(s) into the image for instant/offline startup. # Build with --build-arg PRELOAD_MODEL=1 (adds ~1 GB, needs network at build). ARG PRELOAD_MODEL=0 ARG MODELS=english +ENV MODELS=${MODELS} RUN if [ "$PRELOAD_MODEL" = "1" ]; then \ MODELS="$MODELS" uv run --no-dev python -c \ "import os; from laya import Router; Router().preload([m.strip() for m in os.environ['MODELS'].split(',') if m.strip()])"; \ fi -# Own the app and the HF cache mount so a fresh named volume inherits appuser. -RUN mkdir -p /data/hf && chown -R appuser:appuser /app /data/hf - -USER appuser - EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=5 \ diff --git a/Makefile b/Makefile index a854939..9bb9d23 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PORT ?= 8000 -.PHONY: help run format check fix openapi up down build logs +.PHONY: help run format check fix openapi up down build build-model logs help: @echo "make run - run the API locally (uvicorn --reload on $(PORT))" @@ -11,6 +11,7 @@ help: @echo "make up - docker compose up --build (detached)" @echo "make down - docker compose down" @echo "make build - docker build the image" + @echo "make build-model MODEL=english - build an image with one model baked in" @echo "make logs - follow container logs" run: @@ -37,5 +38,8 @@ down: build: docker build -t laya-api:latest . +build-model: + docker build --build-arg PRELOAD_MODEL=1 --build-arg MODELS=$(MODEL) -t laya-api:$(MODEL) . + logs: docker compose logs -f diff --git a/README.md b/README.md index c33e592..12f9426 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Dockerized [Laya](https://huggingface.co/convaiinnovations/laya) prediction service: loads one or more checkpoints behind a router, then serves typed decisions (choice / score / noul) over HTTP, auto-routed by language or pinned -with `model`. +with `model`. Also supports TypeSafe API compatibility via `/v1/systemone`. Built and published for `linux/amd64` and `linux/arm64`. @@ -18,6 +18,8 @@ Built and published for `linux/amd64` and `linux/arm64`. `typed-decisions`, auto-selected by language or pinned per request. - 🎯 **Typed Decisions**: `choice` / `score` / `noul` questions with calibrated probabilities, confidence and action probability. +- 🤝 **TypeSafe API Compatibility**: drop-in `/v1/systemone` endpoint compatible + with TypeSafe request/response schemas. - 🔐 **Timing-Safe Auth**: API keys (`X-API-Key` / `Authorization: Bearer`) and HTTP Basic, compared in constant time. - 📑 **Interactive OpenAPI Docs**: Swagger UI (`/docs`), ReDoc (`/redoc`) and the @@ -47,8 +49,23 @@ docker pull ghcr.io/chneau/laya docker run -d -p 8000:8000 -e API_KEYS=key1 -v hf-cache:/data/hf ghcr.io/chneau/laya ``` -First start downloads the checkpoint (~1 GB) into the `hf-cache` volume. Bake it -into the image for instant/offline startup with `PRELOAD_MODEL=1`. +### 🏷️ Docker Image Tags & Model Variants + +Multi-architecture images (`linux/amd64` and `linux/arm64`) are published to GitHub Container Registry under several tags: + +| Image Tag | Preloaded Models | Image Size | Description | +| :--- | :--- | :--- | :--- | +| `ghcr.io/chneau/laya:latest` (or `v0.6.0`) | None (Dynamic) | ~300 MB | **Slim / Default**: Small image size. Downloads model on first run into `/data/hf`. | +| `ghcr.io/chneau/laya:english` | `english` | ~1.3 GB | **Instant Startup (English)**: Pre-baked English checkpoint, offline-ready. | +| `ghcr.io/chneau/laya:multilingual` | `multilingual` | ~1.8 GB | **Instant Startup (Multilingual)**: Pre-baked multilingual checkpoint. | +| `ghcr.io/chneau/laya:typed-decisions` | `typed-decisions` | ~1.3 GB | **Instant Startup (Typed Decisions)**: Pre-baked typed decisions checkpoint. | +| `ghcr.io/chneau/laya:all` | All 3 models | ~3.5 GB | **Full Bundle**: All checkpoints pre-baked for zero-latency multi-model routing. | + +#### Running a Pre-baked Image (Instant Startup & Air-gapped / Offline) + +```bash +docker run -d -p 8000:8000 -e API_KEYS=key1 ghcr.io/chneau/laya:english +``` ### Check Health @@ -76,6 +93,7 @@ curl http://localhost:8000/healthz | `POST` | `/email/state` | yes* | Clean + structure an email as a state | | `POST` | `/predict` | yes* | Typed questions over one state | | `POST` | `/predict/bulk` | yes* | Same questions over many states | +| `POST` | `/v1/systemone` | yes* | SystemOne / TypeSafe compatible prediction | \* Enforced only when `API_KEYS` and/or `BASIC_AUTH` is set. Any of these works: @@ -89,6 +107,8 @@ curl http://localhost:8000/healthz ## 🧠 Predicting +### Standard Prediction (`POST /predict`) + ```bash curl -X POST localhost:8000/predict -H 'X-API-Key: key1' -H 'Content-Type: application/json' -d '{ "state": "I was billed twice. Please refund the duplicate today.", @@ -130,13 +150,44 @@ with per-state errors isolated as `{"ok": false, "error": "..."}`. --- +### SystemOne / TypeSafe Compatible Prediction (`POST /v1/systemone`) + +```bash +curl -X POST localhost:8000/v1/systemone -H 'Authorization: Bearer key1' -H 'Content-Type: application/json' -d '{ + "state": "I was billed twice. Please refund the duplicate today.", + "model": "laya-english", + "questions": { + "department": {"type": "choice", "instructions": "Which team?", "criteria": {"billing": "refunds", "technical": "bugs", "sales": "purchases"}}, + "urgency": {"type": "score", "instructions": "How urgent?", "criteria": ["not urgent", "soon", "critical"]}, + "refund": {"type": "noul", "instructions": "Does the customer ask for money back?"} + } +}' +``` + +```json +{ + "model": "laya-english", + "answers": { + "department": {"type": "choice", "choice": "billing", "probabilities": {"billing": 0.96, "technical": 0.02, "sales": 0.02}, "confidence": 0.82}, + "urgency": {"type": "score", "score": 1.36, "legend": {"0": "not urgent", "1": "soon", "2": "critical"}, "confidence": 0.09}, + "refund": {"type": "noul", "noul": 0.82} + }, + "usage": {"input_tokens": 132, "output_tokens": 0} +} +``` + +Supported model names for TypeSafe requests: +`laya-english`, `laya-multilingual`, `laya-typed-decisions`. + +--- + ## ⚙️ Configuration & Environment Variables | Variable | Default | Description | | --- | --- | --- | | `API_KEYS` | *(empty)* | Comma-separated keys; empty disables API-key auth. | | `BASIC_AUTH` | *(empty)* | Comma-separated `user:password` pairs. | -| `MAX_BULK_ITEMS` | `256` | Max states per `/predict/bulk`. | +| `MAX_BULK_ITEMS` | *(empty / unlimited)* | Optional limit on states per `/predict/bulk` (unlimited by default). | | `PORT` | `8000` | HTTP port (host and container). | | `MODELS` | `english` | Checkpoints to preload: `english`, `multilingual`, `typed-decisions`. | | `MODEL_ID` | `convaiinnovations/laya` | Optional repo override (mirror/local path). | diff --git a/app/main.py b/app/main.py index e37647e..3df929d 100644 --- a/app/main.py +++ b/app/main.py @@ -26,7 +26,8 @@ BASIC_AUTH.append((_user.strip(), _password.strip())) AUTH_ENABLED = bool(API_KEYS or BASIC_AUTH) -MAX_BULK_ITEMS = int(os.environ.get("MAX_BULK_ITEMS", "256")) +_max_bulk_env = os.environ.get("MAX_BULK_ITEMS", "").strip() +MAX_BULK_ITEMS: int | None = int(_max_bulk_env) if _max_bulk_env and _max_bulk_env != "0" else None # Checkpoints to keep resident at startup (comma-separated). MODELS = [m.strip() for m in os.environ.get("MODELS", "english").split(",") if m.strip()] @@ -37,6 +38,11 @@ AVAILABLE_MODELS = ("english", "multilingual", "typed-decisions") ModelName = Literal["english", "multilingual", "typed-decisions"] +SystemOneModelName = Literal[ + "laya-english", + "laya-multilingual", + "laya-typed-decisions", +] _state: dict[str, Any] = {} _lock = threading.Lock() @@ -65,7 +71,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="Laya API", - version="0.5.0", + version="0.6.0", lifespan=lifespan, description=( "Run Laya typed decisions (choice / score / noul) over text, JSON objects " @@ -81,9 +87,9 @@ async def lifespan(app: FastAPI): ) State = str | dict[str, Any] | list[Any] +InstructionValue = str | dict[str, Any] | list[Any] CriteriaValue = Any # str, number, bool, list or dict; rendered as compact JSON -_PRESET_NAMES = ("triage", "email", "guard", "moderation", "router") PresetName = Literal["triage", "email", "guard", "moderation", "router"] _presets_cache: dict[str, dict[str, Any]] = {} @@ -117,7 +123,9 @@ class ChoiceQuestion(BaseModel): """Pick one labelled option, e.g. a routing department.""" type: Literal["choice"] - instructions: str = Field(..., description="What to decide, phrased as a question.") + instructions: InstructionValue = Field( + ..., description="What to decide, phrased as a question." + ) criteria: dict[str, CriteriaValue] | list[CriteriaValue] | None = Field( default=None, description=( @@ -132,7 +140,7 @@ class ScoreQuestion(BaseModel): """Rate on an ordered scale, e.g. urgency.""" type: Literal["score"] - instructions: str + instructions: InstructionValue criteria: list[CriteriaValue] = Field( ..., description="Ordered levels, lowest first. Each may be any JSON value.", @@ -144,7 +152,7 @@ class NoulQuestion(BaseModel): """Yes/no decision without a learned neutral class (n-o-u-l).""" type: Literal["noul"] - instructions: str + instructions: InstructionValue criteria: dict[str, CriteriaValue] | None = Field( default=None, description="Optional `false`/`true` descriptions (any JSON value).", @@ -329,6 +337,55 @@ class PredictResponse(BaseModel): ) +class SystemOneNoulAnswer(BaseModel): + type: Literal["noul"] + noul: float + + +class SystemOneChoiceAnswer(BaseModel): + type: Literal["choice"] + choice: str + probabilities: dict[str, float] + confidence: float + + +class SystemOneScoreAnswer(BaseModel): + type: Literal["score"] + score: float + legend: dict[str, str] + probabilities: dict[str, float] + confidence: float + + +SystemOneAnswer = Annotated[ + SystemOneNoulAnswer | SystemOneChoiceAnswer | SystemOneScoreAnswer, + Field(discriminator="type"), +] + + +class SystemOneRequest(BaseModel): + model_config = ConfigDict(extra="ignore") + + state: State = Field(..., description="The content to evaluate: a string, dict, or list.") + model: str = Field( + ..., + description="System One model ID, e.g. laya-english, laya-multilingual, typed-decisions.", + ) + questions: dict[str, Question] = Field(..., description="Typed questions map.") + session_id: str | None = Field(default=None, description="Optional session identifier.") + user: str | None = Field(default=None, description="Optional user identifier.") + + +class SystemOneResponse(BaseModel): + model_config = ConfigDict(extra="allow") + + id: str = Field(default_factory=lambda: f"gen-laya-{secrets.token_hex(12)}") + model: str + provider: str = "Laya" + answers: dict[str, SystemOneAnswer] + usage: Usage + + class BulkItemResult(BaseModel): ok: bool result: PredictResponse | None = None @@ -415,9 +472,12 @@ def _router() -> Any: return router -def _dump(questions: dict[str, Question]) -> dict[str, dict[str, Any]]: +def _dump(questions: dict[str, Any]) -> dict[str, dict[str, Any]]: """Laya's runtime expects plain dicts, not Pydantic models.""" - return {qid: q.model_dump(exclude_none=True) for qid, q in questions.items()} + return { + qid: q.model_dump(exclude_none=True) if hasattr(q, "model_dump") else q + for qid, q in questions.items() + } def _resolve( @@ -428,6 +488,31 @@ def _resolve( return _dump(questions or {}) +_SYSTEMONE_MODELS: dict[str, ModelName] = { + "english": "english", + "laya-english": "english", + "multilingual": "multilingual", + "laya-multilingual": "multilingual", + "typed-decisions": "typed-decisions", + "laya-typed-decisions": "typed-decisions", +} + + +def _resolve_systemone_model(model_name: str) -> ModelName: + name = model_name.lower().strip() + if name.startswith("typesafe/"): + name = name.removeprefix("typesafe/") + if name.startswith("laya/"): + name = name.removeprefix("laya/") + if name in _SYSTEMONE_MODELS: + return _SYSTEMONE_MODELS[name] + if "multi" in name: + return "multilingual" + if "decision" in name: + return "typed-decisions" + return "english" + + _AUTH_RESPONSES: dict[int | str, dict[str, Any]] = { 401: {"description": "Missing or invalid credentials"}, 503: {"description": "Model not loaded yet"}, @@ -553,7 +638,7 @@ def predict_bulk(request: BulkPredictRequest) -> BulkPredictResponse: questions = _resolve(request.questions, request.preset) jobs = [(state, questions, request.model) for state in request.states or []] - if len(jobs) > MAX_BULK_ITEMS: + if MAX_BULK_ITEMS is not None and len(jobs) > MAX_BULK_ITEMS: raise HTTPException( status_code=422, detail=f"Too many states: {len(jobs)} > MAX_BULK_ITEMS={MAX_BULK_ITEMS}", @@ -577,6 +662,39 @@ def predict_bulk(request: BulkPredictRequest) -> BulkPredictResponse: return BulkPredictResponse(count=len(results), results=results) +@app.post( + "/v1/systemone", + tags=["systemone", "predict"], + response_model=SystemOneResponse, + summary="SystemOne / TypeSafe-compatible prediction", + dependencies=[Depends(require_auth)], + responses=_AUTH_RESPONSES, +) +@app.post( + "/systemone", + include_in_schema=False, + response_model=SystemOneResponse, + dependencies=[Depends(require_auth)], + responses=_AUTH_RESPONSES, +) +def systemone_predict(request: SystemOneRequest) -> SystemOneResponse: + """Evaluate a SystemOne / TypeSafe-shaped request using a Laya checkpoint.""" + router = _router() + model = _resolve_systemone_model(request.model) + with _lock: + result = router.predict( + request.state, + _dump(request.questions), + model=model, + ) + validated = PredictResponse.model_validate(result) + return SystemOneResponse( + model=request.model, + answers={key: answer.model_dump() for key, answer in validated.answers.items()}, + usage=validated.usage, + ) + + # ------------------------------------------------------------------------ openapi def custom_openapi() -> dict[str, Any]: if app.openapi_schema: diff --git a/docker-compose.yml b/docker-compose.yml index 631a7be..aae3d87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: PORT: ${PORT:-8000} API_KEYS: ${API_KEYS:-change-me} BASIC_AUTH: ${BASIC_AUTH:-} - MAX_BULK_ITEMS: ${MAX_BULK_ITEMS:-256} + MAX_BULK_ITEMS: ${MAX_BULK_ITEMS:-} MODELS: ${MODELS:-english} MODEL_ID: ${MODEL_ID:-convaiinnovations/laya} MODEL_SUBFOLDER: ${MODEL_SUBFOLDER:-} diff --git a/openapi.json b/openapi.json index bbdf8dc..ee24a08 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Laya API", "description": "Run Laya typed decisions (choice / score / noul) over text, JSON objects or conversation turns, using your own questions or a built-in preset. Requests are auto-routed to the best checkpoint, or pinned with `model`.\n\nAuthenticate with an API key (`X-API-Key` or `Authorization: Bearer`) or HTTP Basic, whichever is configured on the server.", - "version": "0.5.0" + "version": "0.6.0" }, "paths": { "/healthz": { @@ -369,6 +369,66 @@ } ] } + }, + "/v1/systemone": { + "post": { + "tags": [ + "systemone", + "predict" + ], + "summary": "SystemOne / TypeSafe-compatible prediction", + "description": "Evaluate a SystemOne / TypeSafe-shaped request using a Laya checkpoint.", + "operationId": "systemone_predict_v1_systemone_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemOneRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemOneResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid credentials" + }, + "503": { + "description": "Model not loaded yet" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + }, + { + "BasicAuth": [] + } + ] + } } }, "components": { @@ -725,7 +785,19 @@ "title": "Type" }, "instructions": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + } + ], "title": "Instructions", "description": "What to decide, phrased as a question." }, @@ -950,7 +1022,19 @@ "title": "Type" }, "instructions": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + } + ], "title": "Instructions" }, "criteria": { @@ -1242,7 +1326,19 @@ "title": "Type" }, "instructions": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + } + ], "title": "Instructions" }, "criteria": { @@ -1270,6 +1366,233 @@ "title": "ScoreQuestion", "description": "Rate on an ordered scale, e.g. urgency." }, + "SystemOneChoiceAnswer": { + "properties": { + "type": { + "type": "string", + "const": "choice", + "title": "Type" + }, + "choice": { + "type": "string", + "title": "Choice" + }, + "probabilities": { + "additionalProperties": { + "type": "number" + }, + "type": "object", + "title": "Probabilities" + }, + "confidence": { + "type": "number", + "title": "Confidence" + } + }, + "type": "object", + "required": [ + "type", + "choice", + "probabilities", + "confidence" + ], + "title": "SystemOneChoiceAnswer" + }, + "SystemOneNoulAnswer": { + "properties": { + "type": { + "type": "string", + "const": "noul", + "title": "Type" + }, + "noul": { + "type": "number", + "title": "Noul" + } + }, + "type": "object", + "required": [ + "type", + "noul" + ], + "title": "SystemOneNoulAnswer" + }, + "SystemOneRequest": { + "properties": { + "state": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "items": {}, + "type": "array" + } + ], + "title": "State", + "description": "The content to evaluate: a string, dict, or list." + }, + "model": { + "type": "string", + "title": "Model", + "description": "System One model ID, e.g. laya-english, laya-multilingual, typed-decisions." + }, + "questions": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChoiceQuestion" + }, + { + "$ref": "#/components/schemas/ScoreQuestion" + }, + { + "$ref": "#/components/schemas/NoulQuestion" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "choice": "#/components/schemas/ChoiceQuestion", + "noul": "#/components/schemas/NoulQuestion", + "score": "#/components/schemas/ScoreQuestion" + } + } + }, + "type": "object", + "title": "Questions", + "description": "Typed questions map." + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "Optional session identifier." + }, + "user": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User", + "description": "Optional user identifier." + } + }, + "type": "object", + "required": [ + "state", + "model", + "questions" + ], + "title": "SystemOneRequest" + }, + "SystemOneResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "model": { + "type": "string", + "title": "Model" + }, + "provider": { + "type": "string", + "title": "Provider", + "default": "Laya" + }, + "answers": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemOneNoulAnswer" + }, + { + "$ref": "#/components/schemas/SystemOneChoiceAnswer" + }, + { + "$ref": "#/components/schemas/SystemOneScoreAnswer" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "choice": "#/components/schemas/SystemOneChoiceAnswer", + "noul": "#/components/schemas/SystemOneNoulAnswer", + "score": "#/components/schemas/SystemOneScoreAnswer" + } + } + }, + "type": "object", + "title": "Answers" + }, + "usage": { + "$ref": "#/components/schemas/Usage" + } + }, + "additionalProperties": true, + "type": "object", + "required": [ + "model", + "answers", + "usage" + ], + "title": "SystemOneResponse" + }, + "SystemOneScoreAnswer": { + "properties": { + "type": { + "type": "string", + "const": "score", + "title": "Type" + }, + "score": { + "type": "number", + "title": "Score" + }, + "legend": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Legend" + }, + "probabilities": { + "additionalProperties": { + "type": "number" + }, + "type": "object", + "title": "Probabilities" + }, + "confidence": { + "type": "number", + "title": "Confidence" + } + }, + "type": "object", + "required": [ + "type", + "score", + "legend", + "probabilities", + "confidence" + ], + "title": "SystemOneScoreAnswer" + }, "Usage": { "properties": { "input_tokens": {