2 Technische Documentatie
Wiki Bot edited this page 2026-05-12 21:23:35 +00:00

TenderBot — Technische Documentatie

Architectuur

TenderBot is opgebouwd uit drie hoofdcomponenten:

┌─────────────────────────────────────────────────────────────┐
│  Browser / API-client                                       │
└──────────────────────────┬──────────────────────────────────┘
                           │ HTTPS
                  ┌────────▼────────┐
                  │ Traefik Ingress │   (Kubernetes)
                  └────────┬────────┘
                           │
                  ┌────────▼─────────┐
                  │  FastAPI service │   (uvicorn, Python 3.11+)
                  │  tenderbot.api   │
                  └─┬──────────────┬─┘
                    │              │
              ┌─────▼─────┐  ┌─────▼──────┐
              │  SQLite   │  │ Groq API   │
              │ (WAL, PV) │  │ llama-3.3  │
              └───────────┘  └────────────┘
  • FastAPI service (tenderbot.api) — HTTP-laag, middleware voor request-logging, lifecycle management voor DB-initialisatie.
  • Bot-laag (tenderbot.bot) — sessie-, prompt- en LLM-orchestratie. Lazy-init van de Groq client.
  • Persistentielaag (tenderbot.db) — SQLite met WAL-journal, threadsafe via threading.Lock. Eén proces-gedeelde connectie met check_same_thread=False.
  • Kubernetes — deployment achter Traefik ingress; database op een PersistentVolume (/opt/tenderbot/tenderbot.db).
  • Groq — externe inference provider die het llama-3.3-70b-versatile model serveert.

Statische frontend-assets worden geserveerd vanuit tenderbot/static/ op / en /static.

Datamodellen

Alle tabellen worden bij applicatie-startup aangemaakt via init_db(). De database draait in WAL-modus met foreign keys ingeschakeld.

logs — Applicatielogs

Kolom Type Beschrijving
id INTEGER PK AUTOINC Primaire sleutel
timestamp TEXT (ISO-8601 UTC) Tijdstempel
level TEXT INFO, WARNING, ERROR, …
logger TEXT, default tenderbot Naam van de logger
message TEXT Logregel
method TEXT (nullable) HTTP-methode (bij request-log)
path TEXT (nullable) HTTP-pad
status_code INTEGER (nullable) HTTP-status
duration_ms REAL (nullable) Duur van de request
extra TEXT (nullable) JSON-blob met extra velden

Indexen: idx_logs_timestamp, idx_logs_level.

audit_trail — Audit-events

Kolom Type Beschrijving
id INTEGER PK AUTOINC Primaire sleutel
timestamp TEXT (ISO-8601 UTC) Tijdstempel
action TEXT Action key, bv. session.create, chat.message
session_id TEXT (nullable) Optionele sessie-referentie
user_id TEXT (nullable) Optionele gebruiker
detail TEXT (nullable) JSON-blob met details

Indexen: idx_audit_session, idx_audit_timestamp.

ai_sessions — Chatsessies

Kolom Type Beschrijving
session_id TEXT PK UUIDv4
created_at TEXT (ISO-8601 UTC) Aanmaakmoment
updated_at TEXT (ISO-8601 UTC) Laatste activiteit
message_count INTEGER, default 0 Aantal berichten
context TEXT (nullable) Vrije string of JSON-string
username TEXT (nullable) Gebruiker die de sessie aanmaakte

ai_messages — Chatberichten

Kolom Type Beschrijving
id INTEGER PK AUTOINC Primaire sleutel
session_id TEXT, FK → ai_sessions(session_id) ON DELETE CASCADE Sessie
role TEXT user of assistant
content TEXT Berichttekst
created_at TEXT (ISO-8601 UTC) Tijdstempel

Index: idx_messages_session.

Migratiestrategie: init_db() voert idempotente ALTER TABLE ADD COLUMN uit voor context en username op ai_sessions, zodat oudere databases worden bijgewerkt zonder data-verlies.

Context schema

Bij POST /sessions kan een optioneel context-object worden meegegeven. Dit wordt in ai_sessions.context opgeslagen als JSON-string (bij dict/list) of als platte tekst.

Voor elke chat-request leest bot.chat() de context op en geeft deze door aan _format_context() in bot.py. Deze functie:

  1. Probeert de string als JSON te parsen.
  2. Bij een dict-object herkent zij de vaste sleutels beschrijving, aanbestedingstype, sector en eisen_categorieen.
  3. Voor eisen_categorieen (lijst van dicts of strings) wordt een genummerde opsomming gegenereerd met optionele toelichting.
  4. Voegt automatisch de instructie toe: "Stel voor iedere categorie concrete, SMART-geformuleerde eisen op wanneer de gebruiker hierom vraagt."
  5. Onbekende sleutels worden generiek als key: value-regels toegevoegd.

Voorbeeld gestructureerde context

{
  "beschrijving": "Aanbesteding HR-software voor middelgrote gemeente",
  "aanbestedingstype": "Europese openbare procedure",
  "sector": "ICT / HR",
  "eisen_categorieen": [
    {"categorie": "Functioneel", "toelichting": "Salarisadministratie en verlof"},
    {"categorie": "Security", "toelichting": "ISO 27001, NEN 7510"},
    {"categorie": "Duurzaamheid"}
  ],
  "geschatte_waarde": "€ 850.000"
}

Dit wordt geformatteerd tot:

Opdracht: Aanbesteding HR-software voor middelgrote gemeente
Type: Europese openbare procedure
Sector: ICT / HR

Eisencategorieën voor deze aanbesteding:
  1. Functioneel — Salarisadministratie en verlof
  2. Security — ISO 27001, NEN 7510
  3. Duurzaamheid

Stel voor iedere categorie concrete, SMART-geformuleerde eisen op wanneer de gebruiker hierom vraagt.
geschatte_waarde: € 850.000

Sessiebeheer

Aanmaken

new_session(context, username) in bot.py:

  1. Genereert een UUIDv4 als session_id.
  2. Serialiseert context naar JSON (bij dict/list) of laat een string ongewijzigd.
  3. Roept db.create_session() aan (INSERT OR IGNORE).
  4. Schrijft een session.create audit-event.

Bericht opslaan

chat(session_id, user_message):

  1. Maakt de sessie aan als die nog niet bestaat (defensief).
  2. Slaat het user-bericht op via db.add_message(); tegelijk wordt message_count opgehoogd en updated_at bijgewerkt.
  3. Logt een chat.message audit-event met rol en content.
  4. Bouwt de prompt: system prompt + (optionele) geformatteerde context + volledige ai_messages-historie.
  5. Roept Groq aan, slaat het assistant-bericht op, logt opnieuw een audit-event.

Geschiedenis ophalen

db.get_session_messages() geeft None terug als de sessie niet bestaat (→ 404 in de API-laag), anders een lijst {role, content}-dicts geordend op id.

Verwijderen

db.remove_session() doet DELETE FROM ai_sessions WHERE session_id = ?. De foreign key met ON DELETE CASCADE zorgt dat alle ai_messages automatisch worden opgeruimd. Bij succes wordt een session.delete audit-event geschreven.

Audit trail

De audit_trail-tabel legt vast wie wat wanneer deed. Alle events bevatten een UTC ISO-timestamp.

Action Trigger session_id user_id detail
session.create bot.new_session() username (optioneel) {"context": ..., "username": ...}
session.delete bot.delete_session()
chat.message User-bericht in bot.chat() {"role": "user", "content": "..."}
chat.message Assistant-respons in bot.chat() {"role": "assistant", "content": "..."}

Daarnaast registreert de log_requests-middleware elke HTTP-request in logs (methode, pad, status, duur in ms).

AI integratie

System prompt

De system prompt (SYSTEM_PROMPT in bot.py) definieert TenderBot als gespecialiseerde Nederlandse aanbestedingsassistent. De prompt bevat:

  • Juridisch kader: Aw 2012, ARW 2016, Gids Proportionaliteit, EU-richtlijnen 2014/24/EU en 2014/25/EU, actuele drempelwaarden.
  • Procedures: openbaar, niet-openbaar, MCD, innovatiepartnerschap, onderhandelingsprocedure, (meervoudig/enkelvoudig) onderhands.
  • PvE-structuur: SMART-criteria, harde eisen vs. wensen, functionele vs. technische eisen.
  • Selectie- en gunningscriteria: uitsluitingsgronden, geschiktheidseisen, BPKV/EMVI, LCC/TCO.
  • Aanbestedingsdocumenten, duurzaamheid (MVI, SROI, CO₂-Prestatieladder) en praktische hulp.
  • Strikte afbakening: vragen buiten het aanbestedingsdomein worden afgewezen met een vast standaardantwoord.

Context-injectie

Voor elk chat-verzoek wordt de sessiecontext (indien aanwezig) door _format_context() omgezet naar leesbare tekst en geappend aan de system prompt onder de kop ## Context voor deze sessie. Dit gebeurt bij elke request, zodat de context altijd vers wordt meegegeven zonder in de berichtenhistorie te zitten.

Promptopbouw

messages = [
  {"role": "system", "content": SYSTEM_PROMPT + "\n\n## Context voor deze sessie\n" + formatted_context},
  ... volledige ai_messages historie ...
]

Groq configuratie

Parameter Waarde
model llama-3.3-70b-versatile
max_tokens 4096
Auth GROQ_API_KEY (env var)
Client Lazy-geïnitialiseerd singleton (_get_client())

Deploy workflow

TenderBot volgt een gepromoveerde branching-strategie:

feature branch ──► PR ──► accp ──► (deploy-accp) ──► test ──► merge ──► master ──► (deploy-prod)
  1. Feature ontwikkeling — werk in een feature-branch; PR richting accp.
  2. Push naar accp — triggert de deploy-accp pipeline:
    • Build Docker image, tag met commit-SHA + accp.
    • Deploy naar de ACCP-namespace in Kubernetes via Traefik ingress (tenderbot-accp.example.nl).
    • Smoke-tests op /health en /sessions.
  3. Acceptatietest — functionele en regressietests in de ACCP-omgeving; review van wiki/changelog.
  4. Promotie naar productie — merge accpmaster.
  5. Push naar master — triggert deploy-prod:
    • Image opnieuw getagd als prod en vX.Y.Z.
    • Rolling update in de PROD-namespace (tenderbot.example.nl).
    • Database-migraties draaien automatisch bij startup via init_db() (idempotent).
  6. Post-deploy — bewaking via de logs-tabel en Kubernetes liveness/readiness probes op /health.

Het versienummer in api.py (version="1.1.0" na deze release) wordt per release bijgewerkt en weergegeven in Swagger UI.