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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ __pycache__
**/*.pyc
venv
.venv
**/venv
**/.venv

# System-specific files
.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ and this project adheres to
- 💄(ui) review ui for part of the project
- 🐛(fix) Fix streaming crash with OpenAI-compatible APIs
- 🐛(fix) strip thinking part for models without reasoning support
- ✨(dev) setup Tilt for local development
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the right changelog section for this entry.

At Line 35, this item reads as an addition, not a fix. Moving it to ### Added (or ### Changed) will keep release notes consistent.

✍️ Suggested changelog adjustment
 ### Fixed
 
 - 🐛(fix) new conversation in project button max size
 - 💄(ui) little fix margin top
 - 💄(ui) review ui for part of the project
 - 🐛(fix) Fix streaming crash with OpenAI-compatible APIs
-- ✨(dev) setup Tilt for local development
+
+### Added
+
+- ✨(dev) setup Tilt for local development
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- ✨(dev) setup Tilt for local development
### Fixed
- 🐛(fix) new conversation in project button max size
- 💄(ui) little fix margin top
- 💄(ui) review ui for part of the project
- 🐛(fix) Fix streaming crash with OpenAI-compatible APIs
### Added
- ✨(dev) setup Tilt for local development
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 35, Move the entry "✨(dev) setup Tilt for local
development" out of the current section and place it under the appropriate
changelog header (preferably "### Added" or "### Changed"); update the
surrounding headers so the entry matches the conventional category for new
features/changes, preserving the exact entry text and formatting.



## [0.0.15] - 2026-03-31

Expand Down
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ stop: ## stop the development server using Docker
@$(COMPOSE_E2E) stop
.PHONY: stop

restart: ## restart the development server using Docker
@$(COMPOSE_E2E) restart
.PHONY: restart

# -- Backend

demo: ## flush db then create a demo for load testing purpose
Expand Down Expand Up @@ -368,9 +372,13 @@ build-k8s-cluster: ## build the kubernetes cluster using kind
./bin/start-kind.sh
.PHONY: build-k8s-cluster

start-tilt: ## start the kubernetes cluster using kind
tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
start-tilt: ## start Tilt against the conversations kind cluster
tilt up --namespace=conversations -f ./bin/Tiltfile
.PHONY: start-tilt

stop-tilt: ## stop Tilt and leave the kind cluster running
tilt down --namespace=conversations -f ./bin/Tiltfile
.PHONY: stop-tilt

bump-packages-version: VERSION_TYPE ?= minor
bump-packages-version: ## bump the version of the project - VERSION_TYPE can be "major", "minor", "patch"
Expand All @@ -381,3 +389,4 @@ bump-packages-version: ## bump the version of the project - VERSION_TYPE can be
cd ./src/frontend/packages/eslint-config-conversations/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/i18n/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
.PHONY: bump-packages-version

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ $ make superuser

To run the app locally with breakpoints, read: [Debug Mode](docs/debug_mode.md).

To run the app locally with Tilt instead of docker compose, read: [Running with Tilt](docs/tilt.md).

## Documentation 📚

Additional documentation is available in the `docs/` directory:
Expand Down
25 changes: 23 additions & 2 deletions bin/Tiltfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
load('ext://namespace', 'namespace_create', 'namespace_inject')
load('ext://secret', 'secret_yaml_generic')
namespace_create('conversations')

DEV_ENV = os.getenv('DEV_ENV', 'dev')

def clean_old_images(image_name):
local('docker images -q %s | tail -n +2 | xargs -r docker rmi' % image_name)

docker_build(
'localhost:5001/conversations-backend:latest',
context='..',
Expand All @@ -10,8 +16,13 @@ docker_build(
target = 'backend-production',
live_update=[
sync('../src/backend', '/app'),
run(
'pip install -r /app/requirements.txt',
trigger=['./src/backend/requirements.txt']
)
Comment on lines +19 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the actual path structure relative to the Tiltfile
fd -t f 'requirements.txt' src/backend
# Check the bin directory location relative to repo root
ls -la bin/Tiltfile 2>/dev/null || echo "Tiltfile not at expected location"

Repository: suitenumerique/conversations

Length of output: 1818


🌐 Web query:

Tilt trigger path resolution relative to Tiltfile location

💡 Result:

In Tilt, relative paths in Tiltfile Starlark are evaluated relative to the Tiltfile’s own directory (the file whose Tiltfile is being executed). The result is that a referenced path is resolved against that Tiltfile’s directory, not necessarily against the directory you ran the tilt CLI from. However, for the special case of where the “main/root Tiltfile” lives, Tilt also exposes config.main_dir, which is the directory containing the Tiltfile that started the session. This is commonly used when you have nested Tiltfiles and need a stable anchor for resolving paths relative to the root Tiltfile directory. So, for your question “Tilt trigger path resolution relative to Tiltfile location”: - tilt trigger itself takes a resource name, not a path; it triggers an update for that named resource, and any path resolution behavior comes from how that resource was defined (e.g., via docker_build, k8s_yaml, local_resource, file deps, etc.). - When the resource definition uses relative paths (Tiltfile functions’ path parameters, or any file paths you read/declare as deps), those relative paths are resolved relative to the Tiltfile that defined them. - If you specifically need “relative to the root Tiltfile directory” (useful with nested Tiltfiles), use config.main_dir to build absolute paths from a stable base. Example pattern for root-anchored path building in a nested Tiltfile: repo_root = config.main_dir load_yaml = repo_root + '/path/to/common.yaml' (Constructing the exact string/using it with k8s_yaml/load depends on the API.)

Citations:


Fix trigger path to use correct relative path from Tiltfile location.

The trigger path './src/backend/requirements.txt' is incorrect. Since the Tiltfile is located in the bin/ directory, relative paths are resolved from there. The path './src/backend/requirements.txt' resolves to bin/src/backend/requirements.txt, which does not exist. The sync sources from '../src/backend' (the root-level src/backend/ directory), so the trigger should be '../src/backend/requirements.txt' to reference the correct file and match the sync context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/Tiltfile` around lines 19 - 22, The trigger path in the run(...) call is
incorrect because the Tiltfile lives in bin/, so update the trigger argument in
the run(...) invocation (the string './src/backend/requirements.txt') to the
correct relative path '../src/backend/requirements.txt' so it points at the
root-level src/backend/requirements.txt and matches the existing sync sources
'../src/backend'.

]
)
clean_old_images('localhost:5001/conversations-backend')

docker_build(
'localhost:5001/conversations-frontend:latest',
Expand All @@ -23,11 +34,21 @@ docker_build(
sync('../src/frontend', '/home/frontend'),
]
)
clean_old_images('localhost:5001/conversations-frontend')

k8s_yaml(secret_yaml_generic(
name='secret-dev',
from_env_file='../env.d/development/kube-secret'
))

k8s_yaml(local('cd ../src/helm && helmfile -n conversations -e %s template .' % DEV_ENV))

k8s_resource('conversations-backend-migrate', resource_deps=['postgres-postgresql'])
k8s_resource('minio', port_forwards=['9000:9000', '9001:9001'])
k8s_resource('minio-bucket', resource_deps=['minio'])
k8s_resource('conversations-backend-migrate', resource_deps=['postgresql', 'minio', 'redis'])
k8s_resource('conversations-backend-createsuperuser', resource_deps=['conversations-backend-migrate'])
k8s_resource('conversations-backend', resource_deps=['conversations-backend-migrate'])
k8s_yaml(local('cd ../src/helm && helmfile -n conversations -e dev template .'))
k8s_resource('keycloak', resource_deps=['kc-postgresql'])

migration = '''
set -eu
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/conversations.values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ backend:
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem

# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
Expand Down
97 changes: 97 additions & 0 deletions docs/tilt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Running the app locally with Tilt

[Tilt](https://tilt.dev) orchestrates the local Kubernetes development environment: it builds Docker images, deploys all services via Helm, and keeps everything in sync as you edit code.

## Prerequisites

Install the following tools before getting started:

- [Docker](https://docs.docker.com/get-docker/)
- [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) — local Kubernetes cluster
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [Helm](https://helm.sh/docs/intro/install/) + [Helmfile](https://helmfile.readthedocs.io/en/latest/#installation)
- [mkcert](https://github.com/FiloSottile/mkcert#installation) — local TLS certificates
- [Tilt](https://docs.tilt.dev/install.html)

## Step 1 — Create the Kubernetes cluster

```bash
make build-k8s-cluster
```

This runs `bin/start-kind.sh`, which:

1. Creates a local Docker registry at `localhost:5001`
2. Creates a Kind cluster named `conversations`
3. Installs the ingress-nginx controller
4. Generates mkcert TLS certificates for `*.127.0.0.1.nip.io`

All local domains resolve to `127.0.0.1` via [nip.io](https://nip.io) — no `/etc/hosts` edits needed.

## Step 2 — Configure secrets

Copy the secrets template and fill in the required values:

```bash
cp env.d/development/kube-secret.dist env.d/development/kube-secret
```

Then edit `env.d/development/kube-secret`:

| Variable | Required | Description |
|---|---|---|
| `AI_BASE_URL` | Yes | LLM provider base URL |
| `AI_API_KEY` | Yes | LLM provider API key |
| `ALBERT_API_URL` | No | Albert API URL (if using Albert provider) |
| `ALBERT_API_KEY` | No | Albert API key |
| `BRAVE_API_KEY` | No | Brave Search API key (web search tool) |
| `STT_SERVICE_URL` | No | Speech-to-text service URL |
| `STT_SERVICE_API_KEY` | No | Speech-to-text service API key |
| `STT_WEBHOOK_API_KEY` | No | Bearer token the STT service uses when calling back the transcription webhook |
| `LANGFUSE_SECRET_KEY` | No | Langfuse secret key |
| `LANGFUSE_PUBLIC_KEY` | No | Langfuse public key |
| `LANGFUSE_HOST` | No | Langfuse instance URL |

## Step 3 — Start the app

```bash
make start-tilt
```

Tilt will:

1. Build the backend and frontend Docker images and push them to `localhost:5001`
2. Deploy supporting services (PostgreSQL, Keycloak, MinIO, Redis) via the `extra` Helm chart
3. Deploy the backend and frontend via the `conversations` Helm chart
4. Run database migrations and create a superuser (`admin@example.com` / `admin`)
5. Watch source files and sync changes live

The Tilt dashboard opens at `http://localhost:10350`. Wait for all resources to turn green before accessing the app.

## Accessing the services

| Service | URL | Credentials |
|---|---|---|
| App | `https://conversations.127.0.0.1.nip.io` | via Keycloak |
| Keycloak admin | `https://conversations-keycloak.127.0.0.1.nip.io` | `su` / `su` |
| MinIO console | `http://localhost:9001` | `conversations` / `password` |
| Tilt dashboard | `http://localhost:10350` | — |

## Django management commands

The Tilt dashboard exposes two buttons on the `conversations-backend` resource:

- **Run makemigration** — runs `python manage.py makemigrations`
- **Run database migration** — runs `python manage.py migrate --no-input`
Comment on lines +84 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix command label typo for consistency.

makemigrations is plural; the button label text currently says “Run makemigration”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tilt.md` around lines 84 - 85, Update the button/label text "Run
makemigration" to the plural form "Run makemigrations" to match the actual
command `python manage.py makemigrations`; find and replace the label string
"Run makemigration" (in docs/tilt.md) so it exactly matches the command and
other label styles used for "Run database migration".


## Stopping

```bash
make stop-tilt
```

This shuts down Tilt but leaves the Kind cluster running. To also delete the cluster:

```bash
kind delete cluster --name conversations
```
13 changes: 13 additions & 0 deletions env.d/development/common.dist
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,16 @@ OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
# AI_MODEL=llama

# Speech to Text service
# STT_SERVICE_URL=https://ai-service.example.com
# STT_SERVICE_API_KEY=
# STT_WEBHOOK_API_KEY=

# Langfuse observability
# LANGFUSE_ENABLED=true
# LANGFUSE_SECRET_KEY=
# LANGFUSE_PUBLIC_KEY=
# LANGFUSE_HOST=
# LANGFUSE_DEBUG=false
# LANGFUSE_MEDIA_UPLOAD_ENABLED=false
12 changes: 12 additions & 0 deletions env.d/development/kube-secret.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Secrets — copy this file to kube-secret and fill in real values
AI_BASE_URL=changeme
AI_API_KEY=changeme
ALBERT_API_URL=changeme
ALBERT_API_KEY=changeme
BRAVE_API_KEY=changeme
STT_SERVICE_URL=https://ai-service.example.com
STT_SERVICE_API_KEY=changeme
STT_WEBHOOK_API_KEY=changeme
LANGFUSE_SECRET_KEY=changeme
LANGFUSE_PUBLIC_KEY=changeme
LANGFUSE_HOST=changeme
37 changes: 37 additions & 0 deletions src/backend/chat/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Custom authentication classes for chat webhooks."""

import logging

from django.conf import settings
from django.contrib.auth.models import AnonymousUser

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

logger = logging.getLogger(__name__)


class AiWebhookAuthentication(BaseAuthentication):
"""
Custom authentication class for AI webhook requests.
Validates the API key in the Authorization header.
"""

def authenticate(self, request):
"""
Authenticate the request and return a two-tuple of (user, token).
"""
if not settings.STT_WEBHOOK_API_KEY:
raise AuthenticationFailed("STT_WEBHOOK_API_KEY is not configured.")

authorization_header: str = request.headers.get("Authorization") or ""
token = authorization_header.removeprefix("Bearer ")
if not token or token != settings.STT_WEBHOOK_API_KEY:
logger.warning(
"Authentication failed: Bad Authorization header (ip: %s)",
request.META.get("REMOTE_ADDR"),
)
raise AuthenticationFailed()
Comment on lines +27 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Parse the Authorization scheme case-insensitively.

HTTP auth schemes are case-insensitive, so this rejects valid headers like authorization: bearer <token>. If the STT provider lowercases the scheme, every webhook call will 403. Split once and compare the scheme case-insensitively before checking the token. (httpwg.org)

Possible fix
+import secrets
+
         authorization_header: str = request.headers.get("Authorization") or ""
-        token = authorization_header.removeprefix("Bearer ")
-        if not token or token != settings.STT_WEBHOOK_API_KEY:
+        scheme, _, token = authorization_header.partition(" ")
+        if (
+            scheme.lower() != "bearer"
+            or not token
+            or not secrets.compare_digest(token, settings.STT_WEBHOOK_API_KEY)
+        ):
             logger.warning(
                 "Authentication failed: Bad Authorization header (ip: %s)",
                 request.META.get("REMOTE_ADDR"),
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
authorization_header: str = request.headers.get("Authorization") or ""
token = authorization_header.removeprefix("Bearer ")
if not token or token != settings.STT_WEBHOOK_API_KEY:
logger.warning(
"Authentication failed: Bad Authorization header (ip: %s)",
request.META.get("REMOTE_ADDR"),
)
raise AuthenticationFailed()
authorization_header: str = request.headers.get("Authorization") or ""
scheme, _, token = authorization_header.partition(" ")
if (
scheme.lower() != "bearer"
or not token
or not secrets.compare_digest(token, settings.STT_WEBHOOK_API_KEY)
):
logger.warning(
"Authentication failed: Bad Authorization header (ip: %s)",
request.META.get("REMOTE_ADDR"),
)
raise AuthenticationFailed()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/chat/authentication.py` around lines 27 - 34, The Authorization
parsing currently assumes a capitalized "Bearer" prefix; update the logic in the
authentication flow (the block that reads authorization_header and computes
token) to split the header once (e.g., parts = authorization_header.split(None,
1)), verify that parts[0].lower() == "bearer" before using parts[1] as the
token, and only then compare that token to settings.STT_WEBHOOK_API_KEY;
preserve the existing warning log (logger.warning(...
request.META.get("REMOTE_ADDR")) ) and raise AuthenticationFailed() when the
scheme is wrong, missing, or the token does not match.


# No users are associated with the transcribe webhooks
return AnonymousUser(), None
Loading
Loading