Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ __pycache__
**/static/2022-articles/*.json
heritrix/jobs/*
.vscode
backups/**
backups/**
**/.env
3 changes: 3 additions & 0 deletions chat/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
node_modules
frontend/node_modules
171 changes: 171 additions & 0 deletions chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Bron Chat

Bron Chat is een tool voor journalisten en onderzoekers. Onze missie: openbare overheidsinformatie makkelijk en snel doorzoekbaar maken door middel van een AI chat. Dagelijks werken wij aan het uitbreiden en verbeteren van Bron chat. De tool is op het moment in beta.

## Development

Deze code is ontwikkeld door [linksmith](https://github.com/linksmith), tijdens SVDJ Incubator 2024-2025 programma van de SVDJ in samenwerking met Open State Foundation.

## 🌟 Functies

- **AI-gestuurde inzichten**: Ontdek verbanden en patronen in overheidsdata
- **Uitgebreide documentendatabase**: Doorzoek 3,5 miljoen overheidsdocumenten op één centrale plek
- **Bronverwijzingen**: Directe links naar originele documenten en downloadbare pdf's
- **Samenwerking**: Deel je zoekresultaten eenvoudig met collega's via deelbare links
- **Transparante data**: Ontsloten door Open State Foundation, een onafhankelijke stichting zonder winstoogmerk
- **Speciaal voor journalisten**: Een betrouwbare, flexibele tool ontwikkeld door SVDJ Incubator

## 🚀 Projectstructuur

Het project bestaat uit twee hoofdcomponenten:

### Backend (FastAPI)

- Gebouwd met FastAPI, een modern Python webframework
- Integreert met LLM-diensten (Cohere en LiteLLM)
- Gebruikt Qdrant voor vector search
- MySQL-database voor het opslaan van sessies en berichten
- Implementeert streaming responses voor real-time chat

### Frontend (SvelteKit)

- Gebouwd met SvelteKit, een modern JavaScript framework
- Responsieve UI met Tailwind CSS
- Real-time chatinterface
- Documentweergave en deelmogelijkheden

## 🛠️ Technologiestack

### Backend
- Python 3.x
- FastAPI
- SQLAlchemy
- Qdrant (Vector Database)
- Cohere/LiteLLM (LLM Services)
- MySQL
- Alembic (Database Migrations)

### Frontend
- SvelteKit
- Tailwind CSS
- TypeScript/JavaScript
- Markdown-weergave

### Infrastructuur
- Docker & Docker Compose
- Traefik (Reverse Proxy)
- Sentry (Error Tracking)
- Phoenix (Observability)

## 🏗️ Ontwikkelingsomgeving

### Vereisten
- Docker en Docker Compose
- Node.js (voor frontend-ontwikkeling)
- Python 3.x (voor backend-ontwikkeling)

### Omgevingsvariabelen
Maak een `.env`-bestand aan in de hoofdmap met de volgende variabelen:

```
# Algemeen
ENVIRONMENT=development
PUBLIC_API_URL=http://localhost:8000/api

# Database
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_DATABASE=bron_chat
MYSQL_USER=bron_user
MYSQL_PASSWORD=your_password

# Qdrant
QDRANT_HOST=qdrant
QDRANT_PORT=6333

# LLM Services
COHERE_API_KEY=your_cohere_api_key
LLM_SERVICE=cohere # of litellm

# Toegestane oorsprong
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8000
```

### De applicatie draaien

#### Ontwikkelingsmodus
```bash
# Start de applicatie in ontwikkelingsmodus met hot-reloading
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
```

#### Productiemodus
```bash
# Start de applicatie in productiemodus als achtergrondproces
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```

#### Stagingmodus
```bash
# Start de applicatie in stagingmodus als achtergrondproces
docker-compose -f docker-compose.yml -f docker-compose.stag.yml up -d
```

## 📝 API-documentatie

Bij het draaien van de applicatie is de API-documentatie beschikbaar op:
- Ontwikkeling: http://localhost:8000/docs
- Productie/Staging: https://your-domain.com/docs

## 👥 Team

Bron Chat is ontwikkeld door een team van SVDJ Incubator dat bestaat uit:
- Jeremy Crowlesmith [linksmith](https://github.com/linksmith)
- Henri Bouwmeester
- Joost van de Loo

Het project maakt gebruik van de data van Bron, een product van Open State Foundation.

## 🔮 Toekomstvisie

Bron Chat democratiseert het doen van onderzoek, doordat zoeken in openbare overheidsdata nu flexibel en makkelijk wordt voor alle soorten journalisten en onderzoekers, zowel landelijk als in de regio. Hierdoor kan de journalistiek met minder middelen meer bereiken.

Onze visie voor Bron Chat, en diensten die er mogelijk in de toekomst nog bij gaan komen, is dat iedere journalist in Nederland moet kunnen onderzoeken op een hoog niveau.

## 📄 Licentie

Dit project is gelicenseerd onder de MIT-licentie:

```
MIT License

Copyright (c) 2025 Bron

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```


---

## 🤝 SVDJ Incubator 2024-2025

Bron chat is tot stand gekomen tijdens de SVDJ Incubator 2024-2025. De SVDJ Incubator is een subsidie- en begeleidingsprogramma van het Stimuleringsfonds voor de Journalistiek (SVDJ) gericht op het vinden van oplossingen voor gedeelde vraagstukken binnen de journalistieke sector.

Het SVDJ stimuleert met kennisdeling, begeleiding en subsidie een onafhankelijke, diverse en toekomstbestendige journalistieke infrastructuur in Nederland.

Voor vragen over de SVDJ Incubator of de oplossingen die hieruit voort zijn gekomen, ga naar www.svdj.nl/incubator.
26 changes: 26 additions & 0 deletions chat/backend/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3.9

# Install locales package and other necessary tools
RUN apt-get update && apt-get install -y locales locales-all

# Generate and set the nl_NL.UTF-8 locale
RUN sed -i '/nl_NL.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen nl_NL.UTF-8 && \
update-locale LANG=nl_NL.UTF-8 LC_ALL=nl_NL.UTF-8

# Set the locale environment variables
ENV LANG nl_NL.UTF-8
ENV LANGUAGE nl_NL:nl
ENV LC_ALL nl_NL.UTF-8
ENV PYTHONPATH=/app/backend

WORKDIR /app/backend

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY ./app ./app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
26 changes: 26 additions & 0 deletions chat/backend/Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3.9

# Install locales package and other necessary tools
RUN apt-get update && apt-get install -y locales locales-all

# Generate and set the nl_NL.UTF-8 locale
RUN sed -i '/nl_NL.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen nl_NL.UTF-8 && \
update-locale LANG=nl_NL.UTF-8 LC_ALL=nl_NL.UTF-8

# Set the locale environment variables
ENV LANG nl_NL.UTF-8
ENV LANGUAGE nl_NL:nl
ENV LC_ALL nl_NL.UTF-8
ENV PYTHONPATH=/app/backend

WORKDIR /app/backend

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY ./app ./app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
37 changes: 37 additions & 0 deletions chat/backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[alembic]
script_location = app/migrations
#sqlalchemy.url = mysql://%(MYSQL_USER)s:%(MYSQL_PASSWORD)s@%(MYSQL_HOST)s:%(MYSQL_PORT)s/%(MYSQL_DATABASE)s

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Empty file added chat/backend/app/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions chat/backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
QDRANT_HOST: str = os.getenv("QDRANT_HOST", "host.docker.internal")
QDRANT_PORT: int = int(os.getenv("QDRANT_PORT", 6333))
DATABASE_URL: str = f"mysql://{os.getenv('MYSQL_USER')}:{os.getenv('MYSQL_PASSWORD')}@{os.getenv('MYSQL_HOST', 'mysql')}/{os.getenv('MYSQL_DATABASE')}"
COHERE_API_KEY: str = os.getenv("COHERE_API_KEY")
COHERE_EMBED_MODEL: str = os.getenv("COHERE_EMBED_MODEL")
COHERE_RERANK_MODEL: str = os.getenv("COHERE_RERANK_MODEL")
SPARSE_EMBED_MODEL: str = os.getenv("SPARSE_EMBED_MODEL")
QDRANT_HYBRID_SEARCH_TIMEOUT: int = int(os.getenv("QDRANT_HYBRID_SEARCH_TIMEOUT"))
EMBEDDING_QUANTIZATION: str = os.getenv("EMBEDDING_QUANTIZATION")
QDRANT_COLLECTION: str = os.getenv("QDRANT_COLLECTION")

QDRANT_SPARSE_RETRIEVE_LIMIT: int = int(os.getenv("QDRANT_SPARSE_RETRIEVE_LIMIT"))
QDRANT_DENSE_RETRIEVE_LIMIT: int = int(os.getenv("QDRANT_DENSE_RETRIEVE_LIMIT"))
QDRANT_HYBRID_RETRIEVE_LIMIT: int = int(os.getenv("QDRANT_HYBRID_RETRIEVE_LIMIT"))
RERANK_DOC_RETRIEVE_LIMIT: int = int(os.getenv("RERANK_DOC_RETRIEVE_LIMIT"))
MMR_DOC_RETRIEVE_LIMIT: int = int(os.getenv("MMR_DOC_RETRIEVE_LIMIT"))
RERANK_RELEVANCE_THRESHOLD: float = float(os.getenv("RERANK_RELEVANCE_THRESHOLD"))
MMR_DOC_LAMBDA_PARAM: float = float(os.getenv("MMR_DOC_LAMBDA_PARAM"))

ALLOWED_ORIGINS: str = os.getenv("ALLOWED_ORIGINS", "").split(",")
ENVIRONMENT: str = os.getenv("ENVIRONMENT")
# Qdrant settings
QDRANT_POOL_SIZE: int = int(os.getenv("QDRANT_POOL_SIZE"))
QDRANT_POOL_TIMEOUT: int = int(os.getenv("QDRANT_POOL_TIMEOUT"))
QDRANT_TIMEOUT: int = int(os.getenv("QDRANT_TIMEOUT"))
SENTRY_DSN: str = os.getenv("SENTRY_DSN")

OTEL_EXPORTER_OTLP_HEADER: str = os.getenv("OTEL_EXPORTER_OTLP_HEADER")
PHOENIX_CLIENT_HEADERS: str = os.getenv("PHOENIX_CLIENT_HEADERS")
PHOENIX_COLLECTOR_ENDPOINT: str = os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
PHOENIX_TRACER_ENDPOINT: str = os.getenv("PHOENIX_TRACER_ENDPOINT")
PHOENIX_PROJECT_NAME: str = os.getenv("PHOENIX_PROJECT_NAME")
LLM_SERVICE: str = os.getenv("LLM_SERVICE", "cohere")

settings = Settings()
70 changes: 70 additions & 0 deletions chat/backend/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from .config import settings
import logging

logger = logging.getLogger(__name__)

# Configure the connection pool
engine = create_engine(
settings.DATABASE_URL,
pool_size=10, # Increase from default of 5
max_overflow=20, # Increase from default of 10
pool_timeout=60, # Increase timeout
pool_pre_ping=True, # Enable connection health checks
pool_recycle=28000 # Recycle connections after 1 hour
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def init_db():
# Import all models explicitly to ensure they're registered with SQLAlchemy
from .models import (
Session,
SessionFeedback,
Message,
Document,
MessageFeedback,
DocumentFeedback,
MessageDocument

)

def table_exists(table_name):
try:
with engine.connect() as conn:
return engine.dialect.has_table(conn, table_name)
except Exception:
return False

try:
# Create all tables that don't exist
Base.metadata.create_all(bind=engine)
logger.info("Database tables initialized")

# Log which tables exist for debugging
existing_tables = [name for name in Base.metadata.tables.keys()]
logger.info(f"Existing tables: {existing_tables}")

except Exception as e:
logger.error(f"Error during database initialization: {str(e)}", exc_info=True)

def get_db():
db = SessionLocal()
try:
yield db
except Exception:
try:
db.rollback()
except Exception:
pass # Ignore rollback errors
raise
finally:
try:
db.close()
except Exception:
pass # Ignore close errors

Loading