diff --git a/AGENTS.md b/AGENTS.md index 8b7efd1..a964607 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,44 @@ # Agents +Kirche Einfach — AI-assisted church administration. FastAPI backend (`backend/`, Python 3.12, SQLModel, SQLite) + React 19/Vite frontend (`frontend/`). Flow definitions live as JSON files in `backend/flows/` (source of truth); the DB is derived by the sync engine. + +## Commands + +Backend (run from `backend/`): +- `uv sync` — install dependencies +- `uv run pytest` — full test suite (throwaway SQLite file, no external DB needed) +- `uv run alembic upgrade head` — migrate the dev database (`backend/data/`, gitignored) +- `uv run python scripts/sync_flows.py --sync` — reconcile `backend/flows/` into DB + LFX +- `uv run uvicorn app.main:app --port 8000` — run the API (paths resolve from repo root, CWD-independent) + +Frontend (run from `frontend/`): `npm install`, `npm run dev`, `npm run build` (typecheck + build). + +## Definition of done + +- New behavior has tests; `uv run pytest` is green before reporting completion. +- `npm run build` is green when frontend code changed. +- Schema changes ship as a new Alembic revision — never edit an applied migration. + +## Architecture rules + +- `backend/flows/*.json` is the source of truth (one Langflow export per file). Never write flow JSON anywhere else. +- Only `backend/app/flows/sync.py` reconciles files into the DB. CLI, `POST /api/flows/sync`, and the embedded watcher must call it — never duplicate its logic. +- All DB access goes through `backend/app/flows/store.py`. Keep API handlers thin. +- Talk to LFX only via `LfxClient`, to Langflow only via `LangflowClient`. +- Flow JSON must stay a valid Langflow export (`store` requires `data.nodes`/`data.edges`). + +```python +from app.flows.sync import sync_from_directory +report = await sync_from_directory(session, lfx, settings.flows_dir) +``` + +## Boundaries + +- Never commit secrets. `.env` is gitignored; only `.env.example` is tracked. +- Never edit `backend/data/*` (runtime SQLite files) or `__pycache__`. +- Never add a second file-watcher or a second sync path. +- Deleting `backend/flows/.json` deletes the flow including its runs from the DB (by design). + ## Git -When committing: `git add -A && git commit -m ""`. When pushing: `git push` (branch is tracking `origin/main`). +When committing: `git add -A && git commit -m ""` (conventional commits: `feat:`, `fix:`, `chore:`, `docs:`). When pushing: `git push` (branch is tracking `origin/main`). diff --git a/README.md b/README.md index f375cdc..46b87c4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,103 @@ # Kirche Einfach -KI-gestützte Verwaltungsplattform für Kirche. +AI-assisted administration platform for church organizations. -## Stack +Staff describe a request in plain words. The platform guides them through the +process, checks the rules automatically, and asks a human only when something +needs judgment. Every step is documented and traceable. -- **Backend:** Python 3.12, FastAPI, Django, uv -- **Frontend:** React 19, TypeScript, Vite -- **Dev:** Devcontainer, opencode +## Features + +- **Visual flow authoring** — design decision flows in Langflow, then bring + them into the platform. +- **Flows as files** — each flow lives as one JSON file under + `backend/flows/`. The directory is the source of truth: committed to git, + reviewable, diffable. +- **Automatic sync** — the API watches `backend/flows/` and reconciles every + change into the database and the runtime: new files become flows, edits + become new versions, deleted files remove the flow (including its runs). + Same on every startup; also triggerable via `POST /api/flows/sync` or + `uv run python scripts/sync_flows.py --sync`. +- **Versioned runtime** — every change creates an immutable version; versions + deploy to the lightweight LFX runtime and can run synchronously or streamed. +- **Approvals** — a flow can pause and ask a human for a decision. Approving + can automatically start a follow-up ("continuation") flow. +- **Web UI** — three tabs: *Vorgänge* (flows and runs), *Freigaben* + (approvals), *Administration* (import, sync, deploy). + +## Infrastructure + +```mermaid +flowchart LR + subgraph Repo["Git repo — source of truth"] + FLOWS["backend/flows/*.json
one Langflow export per flow"] + end + subgraph Stack["Compose stack (dev + prod)"] + UI["frontend — React UI (3000)"] + API["app — FastAPI (8000)
+ embedded flow-watcher"] + DB[("app.db — SQLite file")] + LF["langflow — authoring UI (7860)
langflow.db — SQLite file"] + LFX["lfx — flow runtime (6004)"] + WT["watchtower — nightly image updates"] + end + UI --> API + FLOWS -->|watch + sync| API + API -->|definitions · versions · runs · approvals| DB + API -->|pull / export| LF + API -->|upload · run · unregister| LFX + LFX -->|approval callbacks| API + WT -.->|updates :latest images| LF +``` + +| Service | Purpose | URL | +| -------------- | ------------------------------------ | -------------------------- | +| React frontend | Web UI (Vite) | http://localhost:3000 | +| FastAPI backend| API + flow orchestration | http://localhost:8000 | +| API docs | Swagger UI | http://localhost:8000/api/docs | +| Langflow | Visual flow authoring | http://localhost:7860 | +| LFX | Flow runtime (internal, no direct UI)| localhost:6004 | +| SQLite files | `backend/data/kirche.db` (app), Langflow keeps its own file | — | + +Why SQLite: a flow run needs only a handful of single-row statements, and +Langflow itself runs on SQLite by default — so the whole stack works without +a database server. Postgres can return later by changing `DATABASE_URL` +(migrations stay portable). ## Getting Started -Open in VS Code → "Reopen in Container". +1. Copy `.env.example` to `.env` and fill in the secrets. +2. Open the repo in VS Code → "Reopen in Container". Setup installs + dependencies, migrates the database, and syncs `backend/flows/`. +3. Run the backend from `backend/`: `uv run uvicorn app.main:app --port 8000`. + Run the frontend from `frontend/`: `npm run dev`. -Backend: `http://localhost:8000` -Frontend: `http://localhost:3000` -API Docs: `http://localhost:8000/api/docs` +Production (self-hosted, internal use — no TLS/reverse proxy included): + +```sh +docker compose -f compose.prod.yml up -d --build +``` + +## Development + +Backend (from `backend/`): + +- `uv run pytest` — full test suite (throwaway SQLite file, no external DB). +- `uv run alembic upgrade head` — migrate the dev database. +- `uv run python scripts/sync_flows.py --sync` — reconcile `backend/flows/`. +- `uv run python scripts/sync_flows.py --pull` — export all Langflow flows + into `backend/flows/` first. +- `uv run python scripts/deploy_flow.py --langflow-id ` — import one + Langflow flow via the API. +- `uv run python scripts/make_flow_fixtures.py` — regenerate the bundled + flows (writes test fixtures and `backend/flows/`). +- `uv run python scripts/smoke_e2e.py` — manual end-to-end smoke. + +Frontend (from `frontend/`): `npm install`, `npm run dev`, `npm run build`. + +Flow workflow: design in Langflow → export into `backend/flows/.json` +(`--pull`, or UI download) → commit → the watcher deploys it automatically. +Deleting a file removes the flow including its runs (git keeps the JSON). + +Container updates: registry images (`langflowai/langflow:latest`) +auto-update nightly via Watchtower (`nickfedor/watchtower`). Locally built +images (`app`, `lfx`, `frontend`) update with `docker compose build --pull`. diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1dbc676..763ad9f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -5,6 +5,7 @@ export interface FlowDefinition { name: string; description: string; status: string; + source_path: string | null; continuation_flow_id: string | null; created_at: string; } @@ -60,6 +61,14 @@ export interface Decision { continuation_run: Run | null; } +export interface SyncReport { + added: string[]; + updated: string[]; + deleted: string[]; + skipped: string[]; + errors: Record; +} + export class ApiError extends Error { status: number; constructor(status: number, message: string) { @@ -135,4 +144,8 @@ export const api = { ), listLangflow: () => req<{ id: string; name: string }[]>("/api/flows/langflow/available"), + syncFlows: () => + req("/api/flows/sync", { + method: "POST", + }), }; diff --git a/frontend/src/components/AdminTab.tsx b/frontend/src/components/AdminTab.tsx index 23908ac..041ff96 100644 --- a/frontend/src/components/AdminTab.tsx +++ b/frontend/src/components/AdminTab.tsx @@ -64,6 +64,26 @@ export default function AdminTab() { } } + async function onSync() { + setError(""); + setInfo(""); + try { + const r = await api.syncFlows(); + const parts = [ + `+${r.added.length}`, + `~${r.updated.length}`, + `-${r.deleted.length}`, + ].join(" "); + const errs = Object.entries(r.errors) + .map(([f, e]) => `${f}: ${e}`) + .join("; "); + setInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`); + await load(); + } catch (e) { + setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e)); + } + } + return (

Administration

@@ -93,11 +113,19 @@ export default function AdminTab() {

Flows + Versionen

+
+ +
{flows.length === 0 &&

Keine Flows vorhanden.

}
    {flows.map((f) => (
  • - {f.name} + {f.name}{" "} + + {f.source_path ? `(${f.source_path})` : "(API)"} +
      {(versions[f.id] ?? []).map((v) => (