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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .chainlit/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ edit_message = true

[UI]
# Name of the assistant.
name = "React-to-me"
name = "React-to-Me"

# default_theme = "dark"

Expand Down
78 changes: 40 additions & 38 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
# Use an official Python runtime as a parent image
FROM python:3.12-slim
# Use official python-slim image
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim

# Set the working directory in the container
WORKDIR /app

# Set environment variables for the virtual environment path
ENV VENV_PATH="/app/.venv"
# Create non-root user and group. The image used to run everything as root; a
# compromise in chainlit or any dependency then owned the container.
RUN \
groupadd -g 1000 appgroup && \
useradd -m -u 3001 -g appgroup appuser && \
chown -R appuser /app && \
chmod -R 700 /app
USER appuser:appgroup

# Install Python dependencies
COPY --chown=appuser --chmod=400 poetry.lock /app/
COPY --chown=appuser --chmod=700 pyproject.toml /app/
ARG POETRY_VERSION=1.8.4
ENV POETRY_VENV="/home/appuser/.poetry"
RUN \
python -m venv $POETRY_VENV && \
$POETRY_VENV/bin/pip install -U pip setuptools && \
$POETRY_VENV/bin/pip install poetry~=$POETRY_VERSION && \
$POETRY_VENV/bin/poetry config virtualenvs.in-project true && \
$POETRY_VENV/bin/poetry install --no-root --without dev && \
rm -rf $POETRY_VENV

# NLTK data, at build time. BM25 tokenises with
# word_tokenize(..., language="english"), which needs punkt_tab -- without it
# every retrieval either downloads it on first use or fails. It lands in
# appuser's home because the download runs as appuser.
RUN /app/.venv/bin/python -m nltk.downloader punkt_tab

# Copy essential application files
COPY --chown=appuser --chmod=700 .chainlit/ /app/.chainlit/
COPY --chown=appuser --chmod=400 bin/ /app/bin/
COPY --chown=appuser --chmod=400 public/ /app/public/
COPY --chown=appuser --chmod=700 src/ /app/src/
COPY --chown=appuser --chmod=400 chainlit.md /app/
COPY --chown=appuser --chmod=400 config_default.yml /app/
COPY --chown=appuser --chmod=400 LICENSE /app/

# Set PYTHONPATH environment variable to include the src directory
ENV PYTHONPATH="/app/src"
# Install system dependencies
# libpq5 is for python package psycopg
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Copy the requirements file into the container
COPY pyproject.toml poetry.lock ./

# Install specific versions of filelock, virtualenv, and poetry
RUN pip install filelock==3.15.4 virtualenv==20.26.6 poetry==1.8.4

# Set poetry to create virtual environment in the project directory
RUN poetry config virtualenvs.in-project true

# Install dependencies without dev dependencies
RUN poetry install --no-root --without dev

# Download NLTK data
RUN poetry run python -m nltk.downloader punkt_tab

# Adjust PATH to include the virtual environment's bin directory
ENV PATH="${VENV_PATH}/bin:${PATH}"

# Copy the rest of the application code into the container
COPY . .

# Ensure the virtual environment is activated in the shell
ENV PATH="/app/.venv/bin:$PATH"

# Make all files in the bin directory executable
RUN chmod +x bin/*
ENV PYTHONPATH="/app/src"

CMD ["uvicorn", "bin.chat-fastapi:app", "--host", "0.0.0.0", "--port", "8000"]
23 changes: 17 additions & 6 deletions bin/chat-chainlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
from util.config_yml import Config, TriggerEvent
from util.logging import logging
from util.orcid_provider import ORCIDOAuthProvider
from util.secrets import SECRET_NAMES, load_secrets_to_environ, mounted_secrets
from util.secrets import (
SECRET_NAMES,
get_db_uri,
load_secrets_to_environ,
mounted_secrets,
)

load_dotenv()
# Before anything reads os.environ. Docker secrets, where mounted, take
Expand All @@ -45,13 +50,19 @@
llm_graph = AgentGraph(profiles)

POSTGRES_CHAINLIT_DB = os.getenv("POSTGRES_CHAINLIT_DB")
POSTGRES_USER = os.getenv("POSTGRES_USER")
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
S3_BUCKET = os.getenv("S3_BUCKET")
S3_CHAINLIT_PREFIX = os.getenv("S3_CHAINLIT_PREFIX")

if POSTGRES_CHAINLIT_DB and POSTGRES_USER and POSTGRES_PASSWORD:
CHAINLIT_DB_URI = f"postgresql+psycopg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@postgres:5432/{POSTGRES_CHAINLIT_DB}?sslmode=disable"
# Once, not per call. Under Vault every call to get_db_uri mints a fresh
# short-lived credential, so calling it inside get_data_layer -- which chainlit
# invokes per session -- would issue a new lease for every visitor and leave
# them all outstanding until they expire. SQLAlchemy needs its dialect named.
CHAINLIT_DB_URI = get_db_uri(POSTGRES_CHAINLIT_DB, driver="psycopg")

if CHAINLIT_DB_URI:
# A local so the narrowing survives into get_data_layer below: mypy does not
# carry a module global's narrowed type into a nested function.
_chainlit_db_uri: str = CHAINLIT_DB_URI

storage_client: PrefixedS3StorageClient | None
if S3_BUCKET and S3_CHAINLIT_PREFIX:
Expand All @@ -62,7 +73,7 @@
@cl.data_layer
def get_data_layer() -> BaseDataLayer:
return SQLAlchemyDataLayer(
conninfo=CHAINLIT_DB_URI,
conninfo=_chainlit_db_uri,
storage_provider=storage_client,
)

Expand Down
13 changes: 12 additions & 1 deletion bin/chat-fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse

from util.secrets import get_secret, load_secrets_to_environ

load_dotenv()
load_secrets_to_environ(
[
"CHAINLIT_AUTH_SECRET",
"OAUTH_AUTH0_CLIENT_SECRET",
"OAUTH_GOOGLE_CLIENT_SECRET",
"OPENAI_API_KEY",
"TAVILY_API_KEY",
]
)

app = FastAPI()

CHAINLIT_URI = os.getenv("CHAINLIT_URI")
CHAINLIT_URL = os.getenv("CHAINLIT_URL")

CLOUDFLARE_SECRET_KEY = os.getenv("CLOUDFLARE_SECRET_KEY")
CLOUDFLARE_SECRET_KEY = get_secret("CLOUDFLARE_SECRET_KEY")
CLOUDFLARE_SITE_KEY = os.getenv("CLOUDFLARE_SITE_KEY")

ERROR_PAGE_TEMPLATE = Template(
Expand Down
23 changes: 21 additions & 2 deletions bin/export_nologin_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,28 @@
import psycopg
from dotenv import load_dotenv

from util.secrets import get_db_uri

load_dotenv()

LANGGRAPH_NOLOGIN_DB_URI = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@postgres:5432/{os.getenv('POSTGRES_LANGGRAPH_DB')}_no_login?sslmode=disable"

def langgraph_nologin_db_uri() -> str:
"""Resolve the database URI, or stop with a message saying why.

A function, not a module constant: this used to resolve at import
time and raise SystemExit when no database was configured, which
broke CI's "can every entry point be imported" check -- the runner
has no Postgres. Importing a script should do nothing; running it
should fail loudly.
"""
db_name = os.getenv("POSTGRES_LANGGRAPH_DB")
uri = get_db_uri(f"{db_name}_no_login" if db_name else None)
if uri is None:
raise SystemExit(
"POSTGRES_LANGGRAPH_DB is not set, or no Postgres password is available. "
"This script exports from the database; it cannot run without one."
)
return uri


def build_query() -> str:
Expand All @@ -32,7 +51,7 @@ def main(records_dir: Path) -> None:

query: str = build_query()

with psycopg.connect(LANGGRAPH_NOLOGIN_DB_URI) as conn, conn.cursor() as cur:
with psycopg.connect(langgraph_nologin_db_uri()) as conn, conn.cursor() as cur:
cur.execute(query)
header = [col.name for col in cur.description] if cur.description else None
records = cur.fetchall()
Expand Down
23 changes: 21 additions & 2 deletions bin/export_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,28 @@
import psycopg
from dotenv import load_dotenv

from util.secrets import get_db_uri

load_dotenv()

CHAINLIT_DB_URI = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@postgres:5432/{os.getenv('POSTGRES_CHAINLIT_DB')}?sslmode=disable"

def chainlit_db_uri() -> str:
"""Resolve the database URI, or stop with a message saying why.

A function, not a module constant: this used to resolve at import
time and raise SystemExit when no database was configured, which
broke CI's "can every entry point be imported" check -- the runner
has no Postgres. Importing a script should do nothing; running it
should fail loudly.
"""
db_name = os.getenv("POSTGRES_CHAINLIT_DB")
uri = get_db_uri(f"{db_name}" if db_name else None)
if uri is None:
raise SystemExit(
"POSTGRES_CHAINLIT_DB is not set, or no Postgres password is available. "
"This script exports from the database; it cannot run without one."
)
return uri


def build_query() -> str:
Expand Down Expand Up @@ -60,7 +79,7 @@ def main(records_dir: Path) -> None:
since_timestamp: str | None = last_record_timestamp(records_dir)
query: str = build_query()

with psycopg.connect(CHAINLIT_DB_URI) as conn, conn.cursor() as cur:
with psycopg.connect(chainlit_db_uri()) as conn, conn.cursor() as cur:
cur.execute(query, {"since_timestamp": since_timestamp or ""})
header = [col.name for col in cur.description] if cur.description else None
records = cur.fetchall()
Expand Down
Loading
Loading