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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,4 @@ examples/start_and_test.sh
# Old tutorials directory (superseded by examples/tutorial_*.py)
tutorials/
site/
.claude/
8 changes: 7 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ repos:
- id: check-toml
- id: check-yaml
args: [--unsafe] # Allow custom tags
# Helm chart templates use Go templating ({{ ... }}) — not valid
# YAML before render. Exclude them from the pure-YAML check.
exclude: ^deploy/helm/.*/templates/
- id: destroyed-symlinks
- id: detect-private-key
- id: end-of-file-fixer
Expand Down Expand Up @@ -137,7 +140,10 @@ repos:
hooks:
- id: pretty-format-yaml
args: [--autofix, --indent=2]
exclude: ^\.github/
# Helm templates use Go templating ({{ ... }}) — not valid YAML
# before render. They live under deploy/helm/*/templates/. The
# GitHub Actions workflow YAMLs use anchors the formatter mangles.
exclude: ^(\.github/|deploy/helm/.*/templates/)

# ==========================================================================
# Markdown
Expand Down
80 changes: 80 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Multi-stage Dockerfile for a locus AgentServer deployment.
#
# Build:
# docker build -t locus-agent:latest .
#
# Run (with an OCI session-token mount and a bearer-token API key):
# docker run -p 8080:8080 \
# -v ~/.oci:/home/locus/.oci:ro \
# -e OCI_PROFILE=DEFAULT \
# -e LOCUS_SERVER_API_KEY=secret \
# locus-agent:latest
#
# The image is built around `locus.server.AgentServer`. Replace
# `your_app:server.app` in the CMD with the import path of your own
# AgentServer instance.

# -----------------------------------------------------------------------------
# Stage 1 — builder. Resolves wheels for locus[oci,server,checkpoints].
# -----------------------------------------------------------------------------
FROM python:3.12-slim AS builder

ENV PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PYTHONDONTWRITEBYTECODE=1

# Build deps only — gcc for native checkpointer drivers (psycopg, etc.).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build

# Copy minimal package metadata first to maximize layer caching.
COPY pyproject.toml README.md LICENSE.txt ./
COPY src/ ./src/

# Install locus + the OCI provider + the AgentServer extras + the OCI
# bucket checkpointer. Add ``[telemetry,rag,redis,postgresql]`` to taste.
RUN pip install --user --no-cache-dir ".[oci,server,checkpoints]"

# -----------------------------------------------------------------------------
# Stage 2 — runtime. Slim image; non-root user; readonly OCI config mount.
# -----------------------------------------------------------------------------
FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/home/locus/.local/bin:$PATH

# Runtime deps only (libpq for asyncpg). curl is used by HEALTHCHECK.
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Non-root user. Mount ~/.oci on top of /home/locus/.oci at runtime.
RUN useradd --create-home --shell /bin/bash --uid 10001 locus
USER locus
WORKDIR /home/locus

# Copy the wheels installed in stage 1.
COPY --from=builder --chown=locus:locus /root/.local /home/locus/.local

# Default port — override with `-p` or in your orchestrator's manifest.
EXPOSE 8080

# Liveness check — the AgentServer's /health endpoint always returns
# 200 unless the process is dead. Override with a readiness probe at
# the orchestrator layer if you want richer signals.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl --fail --silent http://localhost:8080/health || exit 1

# Replace ``your_app:server.app`` with the import path of your own
# AgentServer instance. The placeholder example below assumes a module
# called ``app.py`` at /home/locus/app.py exposing a ``server`` symbol.
#
# An example ``app.py`` lives in deploy/oci-functions/app.py.
CMD ["uvicorn", "app:server.app", "--host", "0.0.0.0", "--port", "8080"]
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,31 @@ each tutorial is a working program against a real model.
| **Production** | [`19_guardrails_security`](examples/tutorial_19_guardrails_security.py) · [`20_checkpoint_backends`](examples/tutorial_20_checkpoint_backends.py) · [`28_agent_server`](examples/tutorial_28_agent_server.py) · [`37_termination`](examples/tutorial_37_termination.py) |
| **OCI** | [`29_model_providers`](examples/tutorial_29_model_providers.py) · [`40_oci_dac`](examples/tutorial_40_oci_dac.py) — Dedicated AI Cluster endpoints |

## Deploy

`AgentServer` is a drop-in FastAPI app. The repo ships a turn-key
deployment story:

- Multi-stage [`Dockerfile`](Dockerfile) — non-root user, `HEALTHCHECK`
on `/health`, `pip install ".[oci,server,checkpoints]"`.
- Helm chart at [`deploy/helm/locus-agent/`](deploy/helm/locus-agent/) —
Deployment, Service, ServiceAccount (with workload-identity hooks),
Secret, HPA, Ingress, all driven by `values.yaml`.
- `pip install "locus[oci,server]"` for production installs.

```bash
docker build -t iad.ocir.io/$NAMESPACE/locus-agent:0.1.0 .
helm install locus-agent ./deploy/helm/locus-agent \
--set image.repository=iad.ocir.io/$NAMESPACE/locus-agent \
--set image.tag=0.1.0 \
--set auth.apiKey=$(openssl rand -hex 16) \
--set ociBucket.enabled=true \
--set ociBucket.bucketName=locus-threads \
--set ociBucket.namespace=$NAMESPACE
```

[Full deploy guide →](https://oracle-samples.github.io/locus/how-to/deploy/)

## Repo layout

```text
Expand Down
11 changes: 11 additions & 0 deletions deploy/helm/locus-agent/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Patterns to ignore when building Helm packages.
.DS_Store
.git/
.gitignore
.idea/
.vscode/
*.tmproj
*.swp
*.bak
*.tgz
README.md.bak
20 changes: 20 additions & 0 deletions deploy/helm/locus-agent/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v2
name: locus-agent
description: |
A locus AgentServer deployment. Wraps any locus.Agent in a FastAPI app
with /invoke, /stream (SSE), /threads/{id}, and /health endpoints, plus
per-principal thread isolation when api_key is configured.
type: application
version: 0.1.0
appVersion: 0.1.0
home: https://github.com/oracle-samples/locus
sources:
- https://github.com/oracle-samples/locus
keywords:
- oci
- agent
- llm
- generative-ai
maintainers:
- name: locus contributors
url: https://github.com/oracle-samples/locus
69 changes: 69 additions & 0 deletions deploy/helm/locus-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# locus-agent Helm chart

Deploys a [locus](https://github.com/oracle-samples/locus) `AgentServer`
on Kubernetes / OKE. Wraps any `locus.Agent` in a FastAPI app exposing
`/invoke`, `/stream` (SSE), `/threads/{id}`, and `/health`.

## Quick start

```bash
helm install locus-agent ./deploy/helm/locus-agent \
--set image.repository=ghcr.io/your-org/locus-agent \
--set image.tag=v0.1.0 \
--set auth.apiKey=$(openssl rand -hex 16) \
--set ociBucket.enabled=true \
--set ociBucket.bucketName=locus-threads \
--set ociBucket.namespace=<your-tenancy-namespace>
```

## Values

See `values.yaml` for the full set. Notable knobs:

| Key | Default | Purpose |
|---|---|---|
| `replicaCount` | `2` | Replicas (scale via HPA when enabled). |
| `auth.apiKey` | `""` | Bearer-token API key. Use `auth.existingSecret` instead in prod. |
| `serviceAccount.annotations` | `{}` | Add OCI workload-identity annotations here. |
| `probes.liveness.path` | `/health` | Liveness endpoint. |
| `ociBucket.enabled` | `false` | Wire OCI Object Storage as the checkpointer backend. |
| `autoscaling.enabled` | `false` | Render an HPA. |
| `ingress.enabled` | `false` | Render an Ingress. |

## Auth

The chart expects a bearer-token secret named in
`auth.existingSecret` or auto-created from `auth.apiKey`. The
container reads it from `LOCUS_SERVER_API_KEY` and passes it to
`AgentServer(api_key=...)`. Per-principal thread namespacing is
enforced server-side — two API keys can't read each other's threads.

## OCI workload identity

Preferred over static `apiKey`: enable workload identity on the OKE
node pool, then add the IAM role annotation to the chart's service
account:

```yaml
serviceAccount:
annotations:
workload.identity.oci.oraclecloud.com/role: arn:oci:...
```

The `OCIBucketBackend` will pick up `instance_principal` /
`resource_principal` automatically — no static credentials needed.

## What you still own

- The `app.py` module the container runs (see the `Dockerfile`'s `CMD`).
This is where you instantiate your `Agent` + `AgentServer`.
- The image build + registry push.
- Network policy, monitoring (Prometheus scrape configs, Grafana
dashboards), and observability (OTLP exporter env vars).

## See also

- [`docs/how-to/deploy.md`](../../../docs/how-to/deploy.md) — full
deployment walkthrough.
- [`docs/concepts/server.md`](../../../docs/concepts/server.md) —
the `AgentServer` API.
66 changes: 66 additions & 0 deletions deploy/helm/locus-agent/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "locus-agent.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Fully-qualified app name.
*/}}
{{- define "locus-agent.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Common labels.
*/}}
{{- define "locus-agent.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "locus-agent.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

{{/*
Selector labels.
*/}}
{{- define "locus-agent.selectorLabels" -}}
app.kubernetes.io/name: {{ include "locus-agent.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

{{/*
Service account name. Returns the explicit name or auto-derives.
*/}}
{{- define "locus-agent.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "locus-agent.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}

{{/*
Bearer-token secret name. References an existing secret if provided,
otherwise the chart-managed one.
*/}}
{{- define "locus-agent.authSecretName" -}}
{{- if .Values.auth.existingSecret -}}
{{- .Values.auth.existingSecret -}}
{{- else -}}
{{- printf "%s-auth" (include "locus-agent.fullname" .) -}}
{{- end -}}
{{- end -}}
82 changes: 82 additions & 0 deletions deploy/helm/locus-agent/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "locus-agent.fullname" . }}
labels:
{{- include "locus-agent.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "locus-agent.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "locus-agent.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "locus-agent.serviceAccountName" . }}
containers:
- name: locus-agent
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: LOCUS_SERVER_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "locus-agent.authSecretName" . }}
key: {{ .Values.auth.secretKey }}
{{- if .Values.ociBucket.enabled }}
- name: LOCUS_OCI_BUCKET_NAME
value: {{ .Values.ociBucket.bucketName | quote }}
- name: LOCUS_OCI_NAMESPACE
value: {{ .Values.ociBucket.namespace | quote }}
- name: LOCUS_OCI_BUCKET_PREFIX
value: {{ .Values.ociBucket.prefix | quote }}
{{- end }}
{{- with .Values.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
Loading
Loading