Erweiterung um Demo, Bugfixes, Dokumentation und Änderungen

This commit is contained in:
Tom 2026-10-27 22:35:29 +01:00
parent 4e29388335
commit 603ecefc4a
25 changed files with 341 additions and 83 deletions

View file

@ -14,7 +14,7 @@ cp .env.example .env
uv sync uv sync
uv run aiw gen-key # Ausgabe als AIW_SECRET_KEY in .env eintragen uv run aiw gen-key # Ausgabe als AIW_SECRET_KEY in .env eintragen
uv run aiw migrate uv run aiw migrate
uv run aiw seed-demo # Team "demo": anna (Admin), ben, clara (Demo-Passwort) uv run aiw seed-demo # Team "demo": admin (Plattform-Admin), entwicklung, fachbereich, freigabe
uv run aiw demo # optional: Demo-Workspaces Bestellservice und MCP-Entwicklung uv run aiw demo # optional: Demo-Workspaces Bestellservice und MCP-Entwicklung
uv run aiw web # http://127.0.0.1:8000 uv run aiw web # http://127.0.0.1:8000
uv run aiw worker # zweites Terminal uv run aiw worker # zweites Terminal
@ -29,20 +29,21 @@ scripts/dev.sh --reset # von null mit Demo-Workspaces; löscht alle lokal
Zum Entwickeln mit sichtbaren Logs, Health-Checks, Metriken und Oracle-Quellen: [docs/lokal-starten.md](docs/lokal-starten.md). Vergleich mit OpenCode, Lücken und Arbeitsplan: [docs/opencode-vergleich.md](docs/opencode-vergleich.md). Zum Entwickeln mit sichtbaren Logs, Health-Checks, Metriken und Oracle-Quellen: [docs/lokal-starten.md](docs/lokal-starten.md). Vergleich mit OpenCode, Lücken und Arbeitsplan: [docs/opencode-vergleich.md](docs/opencode-vergleich.md).
Anmelden mit `anna` und dem Passwort aus `AIW_DEMO_PASSWORD`. Weitere Nutzer legt ein Plattform-Admin unter *Administration → Nutzer* an; sie bekommen ein Einmal-Passwort und setzen beim ersten Login ein eigenes. Ohne Demo-Passwort (z. B. auf einem Server) den ersten Admin mit `uv run aiw create-admin <name>` anlegen. Danach einen Workspace anlegen und ein Git-Repository verbinden. Zum Ausprobieren reicht ein lokales Repository, z. B. `file:///pfad/zu/einem/repo` (dafür `AIW_GIT_ALLOW_LOCAL=true` setzen). Der Chat erscheint erst, wenn eine Quelle erfolgreich getestet wurde. Anmelden mit `admin` und dem Passwort aus `AIW_DEMO_PASSWORD`. Weitere Nutzer legt ein Plattform-Admin unter *Administration → Nutzer* an; sie bekommen ein Einmal-Passwort und setzen beim ersten Login ein eigenes. Ohne Demo-Passwort (z. B. auf einem Server) den ersten Admin mit `uv run aiw create-admin <name>` anlegen. Danach einen Workspace anlegen und ein Git-Repository verbinden. Zum Ausprobieren reicht ein lokales Repository, z. B. `file:///pfad/zu/einem/repo` (dafür `AIW_GIT_ALLOW_LOCAL=true` setzen). Der Chat erscheint erst, wenn eine Quelle erfolgreich getestet wurde.
**Demo-Workspaces:** `uv run aiw demo` legt zwei Workspaces an (Inhalte in `app/demo/`, reproduzierbar), je mit einem lokalen Bare-Repository unter `data/demo/` (drei Wochen Verlauf von anna, ben und clara, ein Feature-Branch), einer Datei zur Analyse, einem Dokumentenordner und sechs Beispielfragen. Protokolle sind relativ zu den Commits datiert, damit die Geschichte an jedem Tag stimmt. **Demo-Workspaces:** `uv run aiw demo` legt drei Workspaces an (Inhalte in `app/demo/`, reproduzierbar). Mitglieder sind jeweils die Demo-Nutzer, benannt nach ihrer Rolle: `admin` (Workspace-Admin), `entwicklung` und `fachbereich` (Mitglieder), `freigabe` (Freigeber). Die beiden Workspaces mit Daten haben je ein lokales Bare-Repository unter `data/demo/` (drei Wochen Verlauf von Admin, Entwicklung und Fachbereich, ein Feature-Branch), einer Datei zur Analyse, einem Dokumentenordner und sechs Beispielfragen. Protokolle sind relativ zu den Commits datiert, damit die Geschichte an jedem Tag stimmt.
- **Demo: Bestellservice** (`bestellservice`): kleiner Python-Dienst, `bestellungen_2026.csv` (1200 Bestellungen), *Projektunterlagen* (Protokolle, Wartungsvertrag, Betriebshandbuch). Eingebauter Widerspruch: Der Code rechnet mit 10 % Rabatt ab 500 €, die spätere Abstimmung beschließt 5 % ab 1.000 €. Dazu die **Shop-Datenbank** (`app/demo/shop_db.py`): eine eigene Datenbank `<datenbank>_demo_shop` im PostgreSQL von Bifröst mit Kunden, Artikeln, Bestellungen, Positionen und Retouren (2025/2026) und der nur lesenden Rolle `demo_leser`, angebunden über den PostgreSQL-Treiber. Läuft der DB-Dienst aus `scripts/dev.sh` (Port 8791), registriert die Demo den Treiber (oder nutzt einen vorhandenen mit derselben URL), stuft seine lesenden Tools ein und verbindet die Datenbank; sonst überspringt sie diesen Teil mit Hinweis. In einem schon vorhandenen Demo-Workspace kommt die Datenbank ohne `--reset` dazu. URL und Schlüssel des Dienstes lassen sich mit `AIW_DEMO_DBMCP_URL` und `AIW_DEMO_DBMCP_KEY` ändern. - **Demo: Bestellservice** (`bestellservice`): kleiner Python-Dienst, `bestellungen_2026.csv` (1200 Bestellungen), *Projektunterlagen* (Protokolle, Wartungsvertrag, Betriebshandbuch). Eingebauter Widerspruch: Der Code rechnet mit 10 % Rabatt ab 500 €, die spätere Abstimmung beschließt 5 % ab 1.000 €. Dazu die **Shop-Datenbank** (`app/demo/shop_db.py`): eine eigene Datenbank `<datenbank>_demo_shop` im PostgreSQL von Bifröst mit Kunden, Artikeln, Bestellungen, Positionen und Retouren (2025/2026) und der nur lesenden Rolle `demo_leser`, angebunden über den PostgreSQL-Treiber. Läuft der DB-Dienst aus `scripts/dev.sh` (Port 8791), registriert die Demo den Treiber (oder nutzt einen vorhandenen mit derselben URL), stuft seine lesenden Tools ein und verbindet die Datenbank; sonst überspringt sie diesen Teil mit Hinweis. In einem schon vorhandenen Demo-Workspace kommt die Datenbank ohne `--reset` dazu. URL und Schlüssel des Dienstes lassen sich mit `AIW_DEMO_DBMCP_URL` und `AIW_DEMO_DBMCP_KEY` ändern.
- **Demo: MCP-Entwicklung** (`mcp`): MCP-Server `ticket-mcp` für ein Service-Desk (FastMCP, Streamable HTTP, Tests, `AGENTS.md`), `tool_aufrufe_2026_10.csv` (2000 Aufrufe aus der Testumgebung), *MCP-Unterlagen* (interner Leitfaden, MCP in Kürze, Anbindung an Bifröst, Service-Desk-API, Review-Protokoll). Eingebaut: `close_ticket` ist als `readOnlyHint` markiert, Bifröst würde es also als lesend vorschlagen; `search_tickets` hat keine Obergrenze. Das Review hat beides beanstandet, das Log zeigt die Folgen (große, langsame Suchen). - **Demo: MCP-Entwicklung** (`mcp`): MCP-Server `ticket-mcp` für ein Service-Desk (FastMCP, Streamable HTTP, Tests, `AGENTS.md`), `tool_aufrufe_2026_10.csv` (2000 Aufrufe aus der Testumgebung), *MCP-Unterlagen* (interner Leitfaden, MCP in Kürze, Anbindung an Bifröst, Service-Desk-API, Review-Protokoll). Eingebaut: `close_ticket` ist als `readOnlyHint` markiert, Bifröst würde es also als lesend vorschlagen; `search_tickets` hat keine Obergrenze. Das Review hat beides beanstandet, das Log zeigt die Folgen (große, langsame Suchen).
- **Demo: Nur Chat** (`chat`): keine Datenquellen; die Einstellung *Ohne Datenquellen chatten* ist an. Drei Beispielfragen stehen wörtlich auch im Bestellservice (Wartungsvertrag, Rabatt laut Protokoll, Retouren), dazu allgemeine Aufgaben. So sieht man nebeneinander, was die Datenanbindung ausmacht: Hier sagt die KI, dass sie Vertrag, Code und Datenbank nicht kennt.
Ein zweiter Aufruf ändert nichts (außer die Shop-Datenbank nachzutragen bzw. neu zu befüllen); `aiw demo mcp --reset` legt eine Demo neu an (löscht dabei ihre Chats). Braucht `AIW_GIT_ALLOW_LOCAL=true`; das Modell richtet weiterhin ein Plattform-Admin ein. Die eingebauten Tools stuft der Befehl nach Vorschlag ein, falls noch nicht geschehen. Ein zweiter Aufruf ändert nichts (außer die Shop-Datenbank nachzutragen bzw. neu zu befüllen); `aiw demo mcp --reset` legt eine Demo neu an (löscht dabei ihre Chats). Braucht `AIW_GIT_ALLOW_LOCAL=true`; das Modell richtet weiterhin ein Plattform-Admin ein. Die eingebauten Tools stuft der Befehl nach Vorschlag ein, falls noch nicht geschehen.
**Beispielfragen und Rückmeldungen:** Workspace-Admins hinterlegen unter *Einstellungen → Allgemein* bis zu acht Beispielfragen (`workspaces.settings["examples"]`); Übersicht und leerer Chat zeigen sie, ein Klick stellt die Frage. Unter jeder Antwort fragt Bifröst „Hilfreich?“ (👍/👎, optional Kommentar; `answer_feedback`, eine Rückmeldung je Person und Antwort). *Nutzung und Kosten* zeigt den Anteil hilfreicher Antworten, je Modell und die Kommentare mit Link zur Antwort, dazu einen CSV-Export für die Auswertung des PoC. **Beispielfragen und Rückmeldungen:** Workspace-Admins hinterlegen unter *Einstellungen → Allgemein* bis zu acht Beispielfragen (`workspaces.settings["examples"]`); Übersicht und leerer Chat zeigen sie, ein Klick stellt die Frage. Unter jeder Antwort fragt Bifröst „Hilfreich?“ (👍/👎, optional Kommentar; `answer_feedback`, eine Rückmeldung je Person und Antwort). *Nutzung und Kosten* zeigt den Anteil hilfreicher Antworten, je Modell und die Kommentare mit Link zur Antwort, dazu einen CSV-Export für die Auswertung des PoC.
**Alles integrieren:** Schritt für Schritt, was nach dem Start noch einzurichten ist (Modell, Tools, Munin, Hugin, Dokumenten-Auslese, Bitbucket, Benachrichtigungen), steht in [docs/einrichtung.md](docs/einrichtung.md). **Alles integrieren:** Schritt für Schritt, was nach dem Start noch einzurichten ist (Modell, Tools, Munin, Hugin, Dokumenten-Auslese, Bitbucket, Benachrichtigungen), steht in [docs/einrichtung.md](docs/einrichtung.md), zum Abhaken in [docs/checkliste.md](docs/checkliste.md).
**Modell einrichten** (einmalig, als Plattform-Admin `anna`): unter *Modelle und Zugänge* einen Endpunkt anlegen (`https://api.openai.com/v1`), einen API-Key für *alle Teams* hinterlegen und das Modell `gpt-6-luna` hinzufügen. Das erste Modell wird automatisch Standard. Mit *Verbindung testen* prüfen. Danach unter *Tools* die Git-Tools einstufen (*Vorschläge übernehmen*); bis dahin sind sie gesperrt. Später kommt das AI Gateway als weiterer Endpunkt hinzu, mit einem Key je Team. **Modell einrichten** (einmalig, als Plattform-Admin `admin`): unter *Modelle und Zugänge* einen Endpunkt anlegen (`https://api.openai.com/v1`), einen API-Key für *alle Teams* hinterlegen und das Modell `gpt-6-luna` hinzufügen. Das erste Modell wird automatisch Standard. Mit *Verbindung testen* prüfen. Danach unter *Tools* die Git-Tools einstufen (*Vorschläge übernehmen*); bis dahin sind sie gesperrt. Später kommt das AI Gateway als weiterer Endpunkt hinzu, mit einem Key je Team.
**Schutz vor Prompt Injection (`app/security`):** Die harte Grenze sind Plattform-Regeln, die nicht vom Modell abhängen (angebotene Tools, Policies, Freigaben, Rechte, Push nur durch Menschen). Zusätzlich gehen alle fremden Inhalte (Tool-Ergebnisse, Dateien, Kommentare, Konfliktdateien, Dienst-Anleitungen) eingefasst als `<fremdinhalt …>` an das Modell; reservierte Tags im Inhalt werden entschärft. `signals.py` erkennt typische Injection-Formulierungen, blockiert nichts, sondern protokolliert (`untrusted_signal`, Metrik `aiw_untrusted_signals_total`) und kennzeichnet den Inhalt. Neue Muster dort ergänzen und ein Beispiel in `tests/injection_corpus.py` aufnehmen; `tests/test_security.py` spielt Szenarien mit einem Modell durch, das der Injection folgt, und prüft, dass alle schreibenden Routen ein CSRF-Token verlangen. **Schutz vor Prompt Injection (`app/security`):** Die harte Grenze sind Plattform-Regeln, die nicht vom Modell abhängen (angebotene Tools, Policies, Freigaben, Rechte, Push nur durch Menschen). Zusätzlich gehen alle fremden Inhalte (Tool-Ergebnisse, Dateien, Kommentare, Konfliktdateien, Dienst-Anleitungen) eingefasst als `<fremdinhalt …>` an das Modell; reservierte Tags im Inhalt werden entschärft. `signals.py` erkennt typische Injection-Formulierungen, blockiert nichts, sondern protokolliert (`untrusted_signal`, Metrik `aiw_untrusted_signals_total`) und kennzeichnet den Inhalt. Neue Muster dort ergänzen und ein Beispiel in `tests/injection_corpus.py` aufnehmen; `tests/test_security.py` spielt Szenarien mit einem Modell durch, das der Injection folgt, und prüft, dass alle schreibenden Routen ein CSRF-Token verlangen.

View file

@ -112,6 +112,10 @@ def instructions(workspace: Workspace, sources: list[DataSource], offered: list[
access += ("Hochgeladene Dateien liegen als Tabellen vor; Tools dafür: " + ", ".join(t.name for t in data) access += ("Hochgeladene Dateien liegen als Tabellen vor; Tools dafür: " + ", ".join(t.name for t in data)
+ ". Erst data_list_tables, dann gezielt mit data_query (DuckDB-SQL) auswerten; Zahlen nur aus " + ". Erst data_list_tables, dann gezielt mit data_query (DuckDB-SQL) auswerten; Zahlen nur aus "
"Abfrageergebnissen nennen und die verwendete Abfrage kurz angeben.\n") "Abfrageergebnissen nennen und die verwendete Abfrage kurz angeben.\n")
if not sources and not data and not services and not any(t.kind == "docs" for t in offered):
access += ("In diesem Workspace sind keine Daten angebunden: Du kennst weder Code noch Dokumente oder Daten "
"des Unternehmens. Antworte aus allgemeinem Wissen und sage bei Fragen zu internen Systemen, "
"Projekten oder Zahlen offen, dass du sie hier nicht einsehen kannst.\n")
if services: if services:
access += ("Tools von MCP-Diensten, auch Datenbanken des Workspace: " + ", ".join(t.name for t in services) access += ("Tools von MCP-Diensten, auch Datenbanken des Workspace: " + ", ".join(t.name for t in services)
+ ". Nutze sie nur, " + ". Nutze sie nur, "

View file

@ -29,11 +29,12 @@ def migrate(settings: Settings) -> None:
def seed_demo(settings: Settings) -> None: def seed_demo(settings: Settings) -> None:
"""Demo-Daten: ein Team mit drei Nutzern. Mehrfach ausführbar.""" """Demo-Daten: ein Team mit vier Nutzern, benannt nach ihrer Rolle. Mehrfach ausführbar."""
from app.identity.models import Team, TeamMembership, User from app.identity.models import Team, TeamMembership, User
db = Database(settings.database_url) db = Database(settings.database_url)
people = [("anna", "Anna Admin", True), ("ben", "Ben Entwickler", False), ("clara", "Clara Fachbereich", False)] people = [("admin", "Admin", True), ("entwicklung", "Entwicklung", False), ("fachbereich", "Fachbereich", False),
("freigabe", "Freigabe", False)]
with db.session() as session: with db.session() as session:
team = session.scalar(select(Team).where(Team.slug == "demo")) team = session.scalar(select(Team).where(Team.slug == "demo"))
if team is None: if team is None:
@ -53,7 +54,8 @@ def seed_demo(settings: Settings) -> None:
if other is None: if other is None:
session.add(Team(slug="andere", name="Anderes Team")) session.add(Team(slug="andere", name="Anderes Team"))
db.dispose() db.dispose()
print("Demo-Team mit anna (Admin), ben und clara angelegt. Passwort: AIW_DEMO_PASSWORD") print("Demo-Team mit admin (Plattform-Admin), entwicklung, fachbereich und freigabe angelegt. "
"Passwort: AIW_DEMO_PASSWORD")
def create_admin_user(settings: Settings, username: str) -> None: def create_admin_user(settings: Settings, username: str) -> None:
@ -131,8 +133,8 @@ def main() -> None:
sub.add_parser("migrate", help="Datenbank auf den neuesten Stand bringen") sub.add_parser("migrate", help="Datenbank auf den neuesten Stand bringen")
sub.add_parser("seed-demo", help="Demo-Team und Demo-Nutzer anlegen") sub.add_parser("seed-demo", help="Demo-Team und Demo-Nutzer anlegen")
demo = sub.add_parser("demo", help="Demo-Workspaces mit Repository, Daten, Dokumenten und Beispielfragen") demo = sub.add_parser("demo", help="Demo-Workspaces mit Repository, Daten, Dokumenten und Beispielfragen")
demo.add_argument("names", nargs="*", choices=["bestellservice", "mcp"], metavar="name", demo.add_argument("names", nargs="*", choices=["bestellservice", "mcp", "chat"], metavar="name",
help="bestellservice, mcp (ohne Angabe: alle)") help="bestellservice, mcp, chat (ohne Angabe: alle)")
demo.add_argument("--reset", action="store_true", help="vorhandene Demo-Workspaces löschen und neu anlegen") demo.add_argument("--reset", action="store_true", help="vorhandene Demo-Workspaces löschen und neu anlegen")
web = sub.add_parser("web", help="Web-Prozess starten") web = sub.add_parser("web", help="Web-Prozess starten")
web.add_argument("--host", default="127.0.0.1") web.add_argument("--host", default="127.0.0.1")

View file

@ -151,8 +151,13 @@ def save_test(session: Session, principal: Principal, source_id: int, result: Te
def chat_ready(session: Session, workspace_id: int) -> bool: def chat_ready(session: Session, workspace_id: int) -> bool:
"""Context first (→ Konzept 1): Chat erst mit mindestens einer erfolgreich getesteten Quelle, """Context first (→ Konzept 1): Chat erst mit mindestens einer erfolgreich getesteten Quelle,
einer erfolgreich eingelesenen Datei oder einem abgeglichenen Ordner.""" einer erfolgreich eingelesenen Datei oder einem abgeglichenen Ordner; oder wenn der Workspace ausdrücklich
ohne Datenquellen chatten darf."""
from app.documents.models import DocFolder from app.documents.models import DocFolder
from app.workspaces.models import Workspace
workspace = session.get(Workspace, workspace_id)
if workspace is not None and (workspace.settings or {}).get("chat_only"):
return True
return session.scalar(select(DataSource.id).where( return session.scalar(select(DataSource.id).where(
DataSource.workspace_id == workspace_id, DataSource.status == "ok").limit(1)) is not None \ DataSource.workspace_id == workspace_id, DataSource.status == "ok").limit(1)) is not None \
or has_tables(session, workspace_id) \ or has_tables(session, workspace_id) \

View file

@ -1,6 +1,6 @@
"""Demo-Workspaces für Vorführungen und den PoC (`aiw demo`): Repository, Datei zur Analyse, Dokumentenordner, """Demo-Workspaces für Vorführungen und den PoC (`aiw demo`): Repository, Datei zur Analyse, Dokumentenordner,
Beispielfragen. Jede Demo legt alles reproduzierbar an (gleiche Daten bei jedem Lauf); die Inhalte stehen in Beispielfragen; oder ein Workspace ganz ohne Daten (nur Chat) zum Vergleich. Jede Demo legt alles reproduzierbar an
den Modulen dieses Pakets, hier nur der gemeinsame Ablauf. (gleiche Daten bei jedem Lauf); die Inhalte stehen in den Modulen dieses Pakets, hier nur der gemeinsame Ablauf.
""" """
import io import io
@ -19,8 +19,12 @@ from app.core.config import Settings
from app.core.crypto import Vault from app.core.crypto import Vault
from app.core.database import Database from app.core.database import Database
AUTHORS = {"ben": ("Ben Entwickler", "ben@demo.example"), "clara": ("Clara Fachbereich", "clara@demo.example"), # Demo-Nutzer (aiw seed-demo) heißen nach ihrer Rolle; so auch die Autoren der Commits
"anna": ("Anna Admin", "anna@demo.example")} ADMIN = "admin"
MEMBERS = {"entwicklung": "member", "fachbereich": "member", "freigabe": "approver"}
AUTHORS = {"entwicklung": ("Entwicklung", "entwicklung@demo.example"),
"fachbereich": ("Fachbereich", "fachbereich@demo.example"), "admin": ("Admin", "admin@demo.example")}
LOGIN = "Anmelden als admin, entwicklung, fachbereich oder freigabe (Passwort AIW_DEMO_PASSWORD)."
# Commit: (Tage zurück, Autor, Nachricht, {Pfad: Inhalt}); Branch: (Name, Tage zurück, Autor, Nachricht, Dateien), # Commit: (Tage zurück, Autor, Nachricht, {Pfad: Inhalt}); Branch: (Name, Tage zurück, Autor, Nachricht, Dateien),
# abgezweigt vom vorletzten Commit auf main # abgezweigt vom vorletzten Commit auf main
@ -38,12 +42,13 @@ class Demo:
name: str name: str
description: str description: str
examples: list[str] examples: list[str]
repo: str # Alles Folgende optional: ohne Repository, Datei und Ordner ist es ein Workspace nur für den Chat
commits: list[Commit] repo: str = ""
branch: tuple[str, int, str, str, dict[str, str]] commits: tuple[Commit, ...] | list[Commit] = ()
upload: tuple[str, Callable[[], bytes]] branch: tuple[str, int, str, str, dict[str, str]] | None = None
folder: str upload: tuple[str, Callable[[], bytes]] | None = None
documents: dict[str, str] folder: str = ""
documents: dict[str, str] | None = None
# Zusätzlich anbinden (z. B. Beispieldatenbank): (db, settings, user_id, workspace_id), auch bei vorhandenem # Zusätzlich anbinden (z. B. Beispieldatenbank): (db, settings, user_id, workspace_id), auch bei vorhandenem
# Workspace # Workspace
extra: Callable[[Database, Settings, int, int], None] | None = None extra: Callable[[Database, Settings, int, int], None] | None = None
@ -95,8 +100,8 @@ def repo_path(settings: Settings, demo: Demo) -> Path:
def all_demos() -> dict[str, Demo]: def all_demos() -> dict[str, Demo]:
from app.demo import bestellservice, mcp from app.demo import bestellservice, chat, mcp
return {d.key: d for d in (bestellservice.DEMO, mcp.DEMO)} return {d.key: d for d in (bestellservice.DEMO, mcp.DEMO, chat.DEMO)}
def run(settings: Settings, keys: list[str] | None = None, reset: bool = False) -> int: def run(settings: Settings, keys: list[str] | None = None, reset: bool = False) -> int:
@ -111,13 +116,13 @@ def run(settings: Settings, keys: list[str] | None = None, reset: bool = False)
db = Database(settings.database_url) db = Database(settings.database_url)
try: try:
with db.session() as session: with db.session() as session:
anna = session.scalar(select(User).where(User.username == "anna")) admin = session.scalar(select(User).where(User.username == ADMIN))
classified = tools.classify_suggested(session, principal_for(session, anna.id)) classified = tools.classify_suggested(session, principal_for(session, admin.id))
if classified: if classified:
print(f"{classified} eingebaute Tools nach Vorschlag eingestuft (wie „Vorschläge übernehmen“ unter Tools).") print(f"{classified} eingebaute Tools nach Vorschlag eingestuft (wie „Vorschläge übernehmen“ unter Tools).")
for demo in chosen: for demo in chosen:
create(db, settings, demo, anna.id, reset) create(db, settings, demo, admin.id, reset)
print("Fertig. Anmelden als anna, ben oder clara (Passwort AIW_DEMO_PASSWORD).") print(f"Fertig. {LOGIN}")
return 0 return 0
finally: finally:
db.dispose() db.dispose()
@ -126,7 +131,6 @@ def run(settings: Settings, keys: list[str] | None = None, reset: bool = False)
def create(db: Database, settings: Settings, demo: Demo, user_id: int, reset: bool) -> None: def create(db: Database, settings: Settings, demo: Demo, user_id: int, reset: bool) -> None:
from app.ai import service as ai from app.ai import service as ai
from app.data_sources import service as sources from app.data_sources import service as sources
from app.documents import service as documents
from app.identity.models import Team from app.identity.models import Team
from app.identity.service import principal_for from app.identity.service import principal_for
from app.uploads import service as uploads from app.uploads import service as uploads
@ -155,14 +159,19 @@ def create(db: Database, settings: Settings, demo: Demo, user_id: int, reset: bo
principal = principal_for(session, user_id) principal = principal_for(session, user_id)
workspace = workspaces.create(session, principal, team.id, demo.name, demo.description) workspace = workspaces.create(session, principal, team.id, demo.name, demo.description)
workspace_id = workspace.id workspace_id = workspace.id
for username in ("ben", "clara"): for username, role in MEMBERS.items():
workspaces.set_member(session, principal, workspace_id, username, "member") workspaces.set_member(session, principal, workspace_id, username, role)
workspaces.set_examples(session, principal, workspace_id, "\n".join(demo.examples)) workspaces.set_examples(session, principal, workspace_id, "\n".join(demo.examples))
if not demo.repo and not demo.upload and not demo.folder:
workspaces.set_chat_only(session, principal, workspace_id, True)
model = ai.effective_model(session, workspace) model = ai.effective_model(session, workspace)
print(f"„{demo.name}“ angelegt (ID {workspace_id}), Mitglieder anna (Admin), ben, clara.") print(f"„{demo.name}“ angelegt (ID {workspace_id}), Mitglieder admin (Admin), entwicklung, fachbereich, "
"freigabe (Freigeber).")
if not demo.repo and not demo.upload and not demo.folder:
print(" Keine Datenquellen: Chat ohne Kontext eingeschaltet (Einstellungen → Allgemein).")
# Repository # Repository
if settings.git_allow_local: if demo.repo and settings.git_allow_local:
target = repo_path(settings, demo) target = repo_path(settings, demo)
build_repo(demo, target) build_repo(demo, target)
with db.session() as session: with db.session() as session:
@ -177,16 +186,28 @@ def create(db: Database, settings: Settings, demo: Demo, user_id: int, reset: bo
sources.save_test(session, principal, source_id, result) sources.save_test(session, principal, source_id, result)
print(f" Repository {demo.repo} ({len(demo.commits)} Commits auf main, Branch {demo.branch[0]}): " print(f" Repository {demo.repo} ({len(demo.commits)} Commits auf main, Branch {demo.branch[0]}): "
f"{'verbunden' if result.ok else 'Fehler: ' + result.message}") f"{'verbunden' if result.ok else 'Fehler: ' + result.message}")
else: elif demo.repo:
print(" Repository übersprungen: lokale Git-Quellen sind aus (AIW_GIT_ALLOW_LOCAL=true für die Demo).") print(" Repository übersprungen: lokale Git-Quellen sind aus (AIW_GIT_ALLOW_LOCAL=true für die Demo).")
# Datei zur Analyse # Datei zur Analyse
filename, generate = demo.upload if demo.upload:
with db.session() as session: filename, generate = demo.upload
upload = uploads.save(session, principal, settings, workspace_id, filename, io.BytesIO(generate())) with db.session() as session:
print(f" Datei {filename}: {upload.row_count} Zeilen als Tabelle {upload.table_name}.") upload = uploads.save(session, principal, settings, workspace_id, filename, io.BytesIO(generate()))
print(f" Datei {filename}: {upload.row_count} Zeilen als Tabelle {upload.table_name}.")
# Dokumentenordner (Textdateien, brauchen keine Dokumenten-Auslese) # Dokumentenordner (Textdateien, brauchen keine Dokumenten-Auslese)
if demo.folder:
add_folder(db, settings, principal, workspace_id, demo)
if demo.extra:
demo.extra(db, settings, user_id, workspace_id)
if model is None:
print(" Hinweis: Noch kein Modell eingerichtet; das macht ein Plattform-Admin unter „Modelle und Zugänge“.")
def add_folder(db: Database, settings: Settings, principal, workspace_id: int, demo: Demo) -> None:
from app.documents import service as documents
with db.session() as session: with db.session() as session:
folder = documents.create(session, principal, workspace_id, demo.folder, documents.WORKSPACE, True) folder = documents.create(session, principal, workspace_id, demo.folder, documents.WORKSPACE, True)
folder_id = folder.id folder_id = folder.id
@ -196,7 +217,3 @@ def create(db: Database, settings: Settings, demo: Demo, user_id: int, reset: bo
documents.put(session, principal, settings, folder_id, path, modified, io.BytesIO(text.encode())) documents.put(session, principal, settings, folder_id, path, modified, io.BytesIO(text.encode()))
documents.prune(session, principal, settings, folder_id, list(demo.documents)) documents.prune(session, principal, settings, folder_id, list(demo.documents))
print(f" Ordner {demo.folder}: {len(demo.documents)} Dokumente (für den ganzen Workspace).") print(f" Ordner {demo.folder}: {len(demo.documents)} Dokumente (für den ganzen Workspace).")
if demo.extra:
demo.extra(db, settings, user_id, workspace_id)
if model is None:
print(" Hinweis: Noch kein Modell eingerichtet; das macht ein Plattform-Admin unter „Modelle und Zugänge“.")

View file

@ -203,20 +203,20 @@ def einloesen(code: str, betrag: int) -> int:
''' '''
COMMITS = [ COMMITS = [
(21, "ben", "Grundgerüst Bestellservice", { (21, "entwicklung", "Grundgerüst Bestellservice", {
"README.md": README, "pyproject.toml": PYPROJECT, "bestellservice/__init__.py": "", "README.md": README, "pyproject.toml": PYPROJECT, "bestellservice/__init__.py": "",
"bestellservice/bestellung.py": BESTELLUNG_START, "bestellservice/api.py": API}), "bestellservice/bestellung.py": BESTELLUNG_START, "bestellservice/api.py": API}),
(18, "clara", "Versandkosten: frei ab 50 Euro", { (18, "fachbereich", "Versandkosten: frei ab 50 Euro", {
"bestellservice/versand.py": VERSAND, "tests/test_versand.py": TEST_VERSAND}), "bestellservice/versand.py": VERSAND, "tests/test_versand.py": TEST_VERSAND}),
(12, "ben", "Rabattstaffel einführen", { (12, "entwicklung", "Rabattstaffel einführen", {
"bestellservice/rabatt.py": RABATT, "tests/test_rabatt.py": TEST_RABATT, "bestellservice/rabatt.py": RABATT, "tests/test_rabatt.py": TEST_RABATT,
"bestellservice/bestellung.py": BESTELLUNG.replace(" storniert: bool = False\n", "")}), "bestellservice/bestellung.py": BESTELLUNG.replace(" storniert: bool = False\n", "")}),
(6, "anna", "Projektregeln in AGENTS.md festhalten", {"AGENTS.md": AGENTS}), (6, "admin", "Projektregeln in AGENTS.md festhalten", {"AGENTS.md": AGENTS}),
(3, "ben", "API: Bestellung stornieren", { (3, "entwicklung", "API: Bestellung stornieren", {
"bestellservice/api.py": API_STORNO, "bestellservice/bestellung.py": BESTELLUNG}), "bestellservice/api.py": API_STORNO, "bestellservice/bestellung.py": BESTELLUNG}),
(1, "clara", "README: Betrieb ergänzen", {"README.md": README + BETRIEB}), (1, "fachbereich", "README: Betrieb ergänzen", {"README.md": README + BETRIEB}),
] ]
BRANCH = ("feature/gutscheine", 2, "ben", "Gutscheine: erster Entwurf", BRANCH = ("feature/gutscheine", 2, "entwicklung", "Gutscheine: erster Entwurf",
{"bestellservice/gutschein.py": GUTSCHEIN}) {"bestellservice/gutschein.py": GUTSCHEIN})
# --- Projektunterlagen (Pfad, Text) ----------------------------------------------------------------------------- # --- Projektunterlagen (Pfad, Text) -----------------------------------------------------------------------------

24
app/demo/chat.py Normal file
View file

@ -0,0 +1,24 @@
"""Demo „Nur Chat“: ein Workspace ohne Repository, Datei, Ordner und Datenbank. Zum Vergleich mit den Demos mit
Datenanbindung: Allgemeines geht, zu internen Systemen kann die KI nur sagen, dass sie sie nicht kennt. Einige
Beispielfragen stehen wörtlich auch im Bestellservice, damit man die Antworten nebeneinanderlegen kann.
"""
from app.demo import Demo, bestellservice, shop_db
NAME = "Demo: Nur Chat"
EXAMPLES = [
# Wörtlich wie im Bestellservice: Wartungsvertrag (Dokument), Rabatt (Code gegen Protokoll), Retouren (Datenbank)
bestellservice.EXAMPLES[3],
bestellservice.EXAMPLES[4],
shop_db.EXAMPLES[0],
"Formuliere eine freundliche E-Mail an einen Kunden, dessen Lieferung sich um eine Woche verzögert.",
"Erkläre in fünf Sätzen, was ein MCP-Server ist und wofür man ihn braucht.",
"Schreibe eine Python-Funktion, die eine Rabattstaffel nach Bestellwert berechnet, mit Tests.",
"Welche Vor- und Nachteile haben Rabattstaffeln im Onlinehandel?",
]
DEMO = Demo(
key="chat", name=NAME,
description="Reiner Chat ohne Datenquellen: Die KI antwortet nur aus allgemeinem Wissen. Zum Vergleich mit den "
"Demo-Workspaces mit Repository, Dateien, Dokumenten und Datenbank.",
examples=EXAMPLES)

View file

@ -259,10 +259,10 @@ def get_attachment(ticket_id: str, attachment_id: str) -> dict:
''' '''
COMMITS = [ COMMITS = [
(20, "ben", "Grundgerüst ticket-mcp mit search_tickets und get_ticket", { (20, "entwicklung", "Grundgerüst ticket-mcp mit search_tickets und get_ticket", {
"README.md": README, "pyproject.toml": PYPROJECT, "ticket_mcp/__init__.py": "", "README.md": README, "pyproject.toml": PYPROJECT, "ticket_mcp/__init__.py": "",
"ticket_mcp/client.py": CLIENT, "ticket_mcp/server.py": SERVER_START}), "ticket_mcp/client.py": CLIENT, "ticket_mcp/server.py": SERVER_START}),
(17, "ben", "Tests mit Fake-Client", { (17, "entwicklung", "Tests mit Fake-Client", {
"tests/__init__.py": "", "tests/fake.py": FAKE, "tests/__init__.py": "", "tests/fake.py": FAKE,
"tests/test_tools.py": TESTS.replace(''' "tests/test_tools.py": TESTS.replace('''
@ -270,14 +270,14 @@ def test_kommentar(fake):
server.add_comment("SD-1", "Erledigt?") server.add_comment("SD-1", "Erledigt?")
assert fake.tickets["SD-1"]["comments"] == ["Erledigt?"] assert fake.tickets["SD-1"]["comments"] == ["Erledigt?"]
''', "")}), ''', "")}),
(14, "clara", "add_comment: Kommentare an Tickets", { (14, "fachbereich", "add_comment: Kommentare an Tickets", {
"ticket_mcp/server.py": SERVER_WRITE, "tests/test_tools.py": TESTS}), "ticket_mcp/server.py": SERVER_WRITE, "tests/test_tools.py": TESTS}),
(9, "anna", "Projektregeln in AGENTS.md", {"AGENTS.md": AGENTS}), (9, "admin", "Projektregeln in AGENTS.md", {"AGENTS.md": AGENTS}),
(5, "ben", "close_ticket: Tickets schließen", { (5, "entwicklung", "close_ticket: Tickets schließen", {
"ticket_mcp/server.py": SERVER, "tests/test_tools.py": TESTS_CLOSE}), "ticket_mcp/server.py": SERVER, "tests/test_tools.py": TESTS_CLOSE}),
(2, "clara", "Dockerfile für den Betrieb", {"Dockerfile": DOCKERFILE}), (2, "fachbereich", "Dockerfile für den Betrieb", {"Dockerfile": DOCKERFILE}),
] ]
BRANCH = ("feature/anhaenge", 1, "ben", "get_attachment: Anhänge als Base64 (Entwurf)", BRANCH = ("feature/anhaenge", 1, "entwicklung", "get_attachment: Anhänge als Base64 (Entwurf)",
{"ticket_mcp/attachments.py": ATTACHMENT}) {"ticket_mcp/attachments.py": ATTACHMENT})
# --- Unterlagen ------------------------------------------------------------------------------------------------- # --- Unterlagen -------------------------------------------------------------------------------------------------

View file

@ -6,7 +6,7 @@ Probleme, die häufiger vorkommen, und was dagegen hilft.
### Im Workspace gibt es keinen Chat ### Im Workspace gibt es keinen Chat
Der Chat erscheint erst, wenn eine Datenquelle erfolgreich getestet oder eine Datei hochgeladen wurde. Ein Workspace-Admin prüft unter *Daten*, ob die Quelle den Status „ok“ hat, und testet sie ggf. erneut. Der Chat erscheint erst, wenn eine Datenquelle erfolgreich getestet oder eine Datei hochgeladen wurde. Ein Workspace-Admin prüft unter *Daten*, ob die Quelle den Status „ok“ hat, und testet sie ggf. erneut. Soll der Workspace ganz ohne Daten auskommen, kann der Admin *Ohne Datenquellen chatten* einschalten.
### Die Antwort bleibt bei „wartet“ ### Die Antwort bleibt bei „wartet“

View file

@ -65,7 +65,7 @@ DB-Dienst für PostgreSQL oder Oracle mit URL und Schlüssel registrieren, *Tool
Danach können Teammitglieder Workspaces anlegen und einrichten (siehe [Workspace startklar machen](/hilfe/workspace-admin#workspace-startklar-machen)). Danach können Teammitglieder Workspaces anlegen und einrichten (siehe [Workspace startklar machen](/hilfe/workspace-admin#workspace-startklar-machen)).
**Für Vorführungen:** `aiw demo` auf dem Server legt zwei Workspaces mit Beispiel-Repository, Datei, Dokumenten und Beispielfragen an (Team „demo“, Nutzer anna, ben, clara): „Demo: Bestellservice“ (Fachanwendung mit Bestelldaten und, wenn der PostgreSQL-DB-Dienst läuft, einer angebundenen Shop-Datenbank) und „Demo: MCP-Entwicklung“ (eigener MCP-Server mit Leitfaden und Aufruf-Log). `aiw demo mcp` legt nur eine an, `--reset` legt sie neu an. Dafür müssen lokale Git-Quellen erlaubt sein (`AIW_GIT_ALLOW_LOCAL=true`). **Für Vorführungen:** `aiw demo` auf dem Server legt drei Workspaces mit Beispielfragen an (Team „demo“, Nutzer nach Rolle: admin, entwicklung, fachbereich, freigabe): „Demo: Bestellservice“ (Fachanwendung mit Repository, Bestelldaten, Dokumenten und, wenn der PostgreSQL-DB-Dienst läuft, einer angebundenen Shop-Datenbank), „Demo: MCP-Entwicklung“ (eigener MCP-Server mit Leitfaden und Aufruf-Log) und „Demo: Nur Chat“ (ohne Datenquellen, zum Vergleich). `aiw demo mcp` legt nur eine an, `--reset` legt sie neu an. Dafür müssen lokale Git-Quellen erlaubt sein (`AIW_GIT_ALLOW_LOCAL=true`).
## Teams und Speicherkontingent ## Teams und Speicherkontingent

View file

@ -51,5 +51,5 @@ Mehr dazu: [Für Plattform-Admins](/hilfe/plattform-admin).
## Wo finde ich was? ## Wo finde ich was?
- **Seitenleiste links:** *Workspaces*, *Freigaben* (mit Zähler offener Anträge), für Plattform-Admins die *Administration*. In einem Workspace zusätzlich *Übersicht*, *Daten*, ggf. *Gedächtnis*, für Admins *Einstellungen*, *Nutzung und Kosten*, *Runs*, *Audit-Log* sowie die letzten Chats. - **Seitenleiste links:** *Workspaces*, *Freigaben* (mit Zähler offener Anträge), für Plattform-Admins die *Administration*. In einem Workspace zusätzlich *Übersicht*, *Daten*, ggf. *Gedächtnis*, für Admins *Einstellungen*, *Nutzung und Kosten*, *Runs*, *Audit-Log* sowie die letzten Chats.
- **Leiste oben:** der aktuelle Workspace, *Hilfe* (führt zur passenden Stelle für die aktuelle Seite), dein Name (Profil, Passwort, Benachrichtigungen), Hell/Dunkel und *Abmelden*. - **Leiste oben:** links der Knopf zum Ein- und Ausblenden der Seitenleiste (auch Strg+B bzw. Cmd+B; der Zustand bleibt gemerkt), der aktuelle Workspace, *Hilfe* (führt zur passenden Stelle für die aktuelle Seite), dein Name (Profil, Passwort, Benachrichtigungen), Hell/Dunkel und *Abmelden*.
- **Hilfe durchsuchen:** Das Suchfeld links in der Hilfe findet Abschnitte über alle Seiten. - **Hilfe durchsuchen:** Das Suchfeld links in der Hilfe findet Abschnitte über alle Seiten.

View file

@ -35,7 +35,7 @@ Es muss immer mindestens ein Admin bleiben. Freigeber lohnen sich, damit Anträg
## Datenquellen ## Datenquellen
Datenquellen verbindest, testest und bearbeitest nur du als Admin, unter *Daten*. Der Chat steht erst bereit, wenn eine Quelle erfolgreich getestet oder eine Datei hochgeladen wurde. Datenquellen verbindest, testest und bearbeitest nur du als Admin, unter *Daten*. Der Chat steht erst bereit, wenn eine Quelle erfolgreich getestet oder eine Datei hochgeladen wurde, außer du schaltest [Ohne Datenquellen chatten](#ohne-datenquellen-chatten) ein.
Beim Verbinden bestätigst du, dass alle Mitglieder die Inhalte der Quelle über die KI sehen dürfen. Der Zugriff erfolgt mit einem technischen Zugang, nicht mit den Rechten der einzelnen Mitglieder. Beim Verbinden bestätigst du, dass alle Mitglieder die Inhalte der Quelle über die KI sehen dürfen. Der Zugriff erfolgt mit einem technischen Zugang, nicht mit den Rechten der einzelnen Mitglieder.
@ -66,6 +66,10 @@ Ordner verbinden dürfen alle Mitglieder (siehe [Ordner abgleichen](/hilfe/nutze
Unter *Einstellungen → Allgemein → Beispielfragen* bis zu acht Fragen, eine je Zeile. Sie erscheinen in der Übersicht unter *Ausprobieren* und in neuen Chats; ein Klick stellt die Frage. Gute Beispielfragen zeigen, was der Workspace kann, etwa je eine zum Repository, zu den Daten und zu den Dokumenten. Unter *Einstellungen → Allgemein → Beispielfragen* bis zu acht Fragen, eine je Zeile. Sie erscheinen in der Übersicht unter *Ausprobieren* und in neuen Chats; ein Klick stellt die Frage. Gute Beispielfragen zeigen, was der Workspace kann, etwa je eine zum Repository, zu den Daten und zu den Dokumenten.
## Ohne Datenquellen chatten
Unter *Einstellungen → Allgemein* lässt sich der Chat auch ohne verbundene Quelle erlauben. Die KI antwortet dann nur aus allgemeinem Wissen: Texte formulieren, Begriffe erklären, Code-Beispiele schreiben. Fragt jemand nach internen Systemen, Verträgen oder Zahlen, sagt sie, dass sie diese hier nicht einsehen kann. Gedacht für reine Chat-Workspaces oder um den Unterschied zu Workspaces mit Daten zu zeigen. Verbindest du später eine Quelle, nutzt die KI sie wie gewohnt. Ein- und Ausschalten steht im Audit-Log.
## Modell ## Modell
Unter *Einstellungen → Allgemein → Modell* wählst du das Modell des Workspace aus den freigegebenen Modellen. Ohne Auswahl gilt das Standardmodell der Plattform. Unter *Einstellungen → Allgemein → Modell* wählst du das Modell des Workspace aus den freigegebenen Modellen. Ohne Auswahl gilt das Standardmodell der Plattform.

View file

@ -140,6 +140,26 @@
top: 0; top: 0;
height: 100dvh; height: 100dvh;
} }
/* Seitenleiste ausgeblendet (Knopf in der Topbar): Inhalt volle Breite, Markenname in die Topbar */
[data-sidebar="collapsed"] .aiw-layout {
grid-template-columns: 1fr;
}
[data-sidebar="collapsed"] .aiw-sidebar {
display: none;
}
[data-sidebar="collapsed"] .aiw-topbar-brand {
display: inline-flex;
}
}
/* Schmal blendet ☰ die Seitenleiste als Overlay ein; der Knopf zum Ausblenden gilt nur breit */
@media (max-width: 64rem) {
.aiw-sidebar-toggle {
display: none;
}
} }
@media (max-width: 64rem) { @media (max-width: 64rem) {

View file

@ -113,6 +113,30 @@ document.addEventListener('click', function (e) {
}); });
}); });
// <button data-sidebar-toggle>: Seitenleiste ein-/ausblenden (auch Strg/Cmd+B). Der Zustand liegt in einem Cookie,
// damit der Server die Seite gleich richtig ausliefert (kein Flackern beim Laden).
(function () {
function toggle() {
var root = document.documentElement;
var collapsed = root.dataset.sidebar !== 'collapsed';
if (collapsed) root.dataset.sidebar = 'collapsed'; else delete root.dataset.sidebar;
document.cookie = 'aiw_sidebar=' + (collapsed ? 'collapsed' : 'open') + '; path=/; max-age=31536000; SameSite=Lax';
document.querySelectorAll('[data-sidebar-toggle]').forEach(function (b) {
b.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
});
}
document.addEventListener('click', function (e) {
if (e.target.closest('[data-sidebar-toggle]')) toggle();
});
document.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'b'
&& window.matchMedia('(min-width: 64.0625rem)').matches) {
e.preventDefault();
toggle();
}
});
})();
// <textarea data-submit-on-enter>: Strg/Cmd+Enter sendet das Formular // <textarea data-submit-on-enter>: Strg/Cmd+Enter sendet das Formular
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && e.target.matches('textarea[data-submit-on-enter]')) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && e.target.matches('textarea[data-submit-on-enter]')) {

View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" data-theme="light"> {% set collapsed = request.cookies.get('aiw_sidebar') == 'collapsed' %}
<html lang="de" data-theme="light"{% if collapsed %} data-sidebar="collapsed"{% endif %}>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -68,6 +69,11 @@
<div class="min-w-0"> <div class="min-w-0">
<header class="aiw-topbar"> <header class="aiw-topbar">
<button class="btn btn-ghost btn-icon aiw-menu" data-action="sidebar-open" aria-label="Menü">☰</button> <button class="btn btn-ghost btn-icon aiw-menu" data-action="sidebar-open" aria-label="Menü">☰</button>
<button class="btn btn-ghost btn-icon btn-sm aiw-sidebar-toggle" type="button" data-sidebar-toggle aria-controls="sidebar"
aria-expanded="{{ 'false' if collapsed else 'true' }}" aria-label="Seitenleiste ein- oder ausblenden"
title="Seitenleiste ein-/ausblenden (Strg+B)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="9" y1="4" x2="9" y2="20"/></svg>
</button>
<a href="/workspaces" class="aiw-topbar-brand" title="Bifröst – AI Workspace">Bifröst</a> <a href="/workspaces" class="aiw-topbar-brand" title="Bifröst – AI Workspace">Bifröst</a>
{% if trail %} {% if trail %}
<nav class="breadcrumbs aiw-crumbs" aria-label="Pfad"> <nav class="breadcrumbs aiw-crumbs" aria-label="Pfad">

View file

@ -26,6 +26,11 @@
</div></section> </div></section>
{% endif %} {% endif %}
{% if chat_only and not (sources or uploads or folders) %}
<p class="aiw-context-line text-sm"><span class="text-muted">Kontext:</span>
<span class="badge badge-default">Nur Chat, keine Datenquellen</span>
<span class="text-muted">Die KI antwortet aus allgemeinem Wissen und kennt weder Code noch Daten.</span></p>
{% endif %}
{% if sources or uploads or folders %} {% if sources or uploads or folders %}
<p class="aiw-context-line text-sm"> <p class="aiw-context-line text-sm">
<span class="text-muted">Kontext:</span> <span class="text-muted">Kontext:</span>

View file

@ -54,6 +54,19 @@
</div> </div>
</section> </section>
<section class="card" id="chat-only">
<div class="card-header"><h2 class="text-lg">Ohne Datenquellen chatten</h2></div>
<div class="card-body">
<form method="post" action="{{ wbase }}/chat-only" class="stack stack-sm aiw-form">
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
<label class="cluster"><input type="checkbox" name="enabled" value="1" {% if workspace.settings and workspace.settings.get('chat_only') %}checked{% endif %}>
<span>Chat auch ohne verbundene Quelle erlauben</span></label>
<p class="form-hint">Normalerweise startet der Chat erst, wenn eine Quelle verbunden ist, denn ohne Kontext antwortet die KI nur aus allgemeinem Wissen. Für einen reinen Chat (Texte formulieren, allgemeine Fragen) oder um den Unterschied zu zeigen, hier einschalten. Verbundene Quellen nutzt die KI trotzdem.</p>
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
</form>
</div>
</section>
<section class="card" id="approvals"> <section class="card" id="approvals">
<div class="card-header"><h2 class="text-lg">Freigaben</h2></div> <div class="card-header"><h2 class="text-lg">Freigaben</h2></div>
<div class="card-body stack stack-sm"> <div class="card-body stack stack-sm">
@ -177,7 +190,7 @@
<section class="card" id="tools"> <section class="card" id="tools">
<div class="card-header"><h2 class="text-lg">Tools</h2></div> <div class="card-header"><h2 class="text-lg">Tools</h2></div>
<div class="card-body stack"> <div class="card-body stack">
<p class="text-sm text-muted">Was das Modell in diesem Workspace darf. Die Einstufung legt der Plattform-Admin fest; hier lässt sie sich nur verschärfen („wie global“ übernimmt die Einstufung unverändert). Aufrufe von Tools mit „Freigabe nötig“ warten auf die Entscheidung eines Workspace-Admins.</p> <p class="text-sm text-muted">Was das Modell in diesem Workspace darf. Die Einstufung legt der Plattform-Admin fest; hier lässt sie sich nur verschärfen („wie global“ übernimmt die Einstufung unverändert). Aufrufe von Tools mit „Freigabe nötig“ warten auf die Entscheidung eines Workspace-Admins oder Freigebers.</p>
{% if saved_tool %}<div class="alert alert-success" role="status"><div class="alert-content"><p>Gespeichert: <code>{{ saved_tool.definition.name }}</code> ist jetzt {{ labels[saved_tool.verdict.decision] }} ({{ 'Vorgabe des Workspace' if saved_tool.override else 'wie global' }}).</p></div></div>{% endif %} {% if saved_tool %}<div class="alert alert-success" role="status"><div class="alert-content"><p>Gespeichert: <code>{{ saved_tool.definition.name }}</code> ist jetzt {{ labels[saved_tool.verdict.decision] }} ({{ 'Vorgabe des Workspace' if saved_tool.override else 'wie global' }}).</p></div></div>{% endif %}
<div class="table-wrap"><table> <div class="table-wrap"><table>
<thead><tr><th>Tool</th><th>Wirksam</th><th>Workspace-Vorgabe</th></tr></thead> <thead><tr><th>Tool</th><th>Wirksam</th><th>Workspace-Vorgabe</th></tr></thead>

View file

@ -106,6 +106,7 @@ def router() -> APIRouter:
"uploads": uploads.list_for(session, principal, workspace_id), "uploads": uploads.list_for(session, principal, workspace_id),
"effective_model": ai.effective_model(session, context["workspace"]), "effective_model": ai.effective_model(session, context["workspace"]),
"examples": workspaces.examples(context["workspace"]), "examples": workspaces.examples(context["workspace"]),
"chat_only": workspaces.chat_only(context["workspace"]),
"folders": documents.folders(session, principal, workspace_id), "folders": documents.folders(session, principal, workspace_id),
}) })
return web.render(request, "workspace.html", context) return web.render(request, "workspace.html", context)
@ -176,6 +177,13 @@ def router() -> APIRouter:
workspaces.set_examples(session, principal, workspace_id, examples) workspaces.set_examples(session, principal, workspace_id, examples)
return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1#examples") return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1#examples")
@r.post("/workspaces/{workspace_id}/chat-only")
def chat_only(request: Request, principal: web.Checked, workspace_id: int,
enabled: Annotated[str, Form()] = "") -> Response:
with web.db(request).session() as session:
workspaces.set_chat_only(session, principal, workspace_id, enabled == "1")
return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1#chat-only")
@r.post("/workspaces/{workspace_id}/tool-rounds") @r.post("/workspaces/{workspace_id}/tool-rounds")
def tool_rounds(request: Request, principal: web.Checked, workspace_id: int, def tool_rounds(request: Request, principal: web.Checked, workspace_id: int,
value: Annotated[str, Form()] = "") -> Response: value: Annotated[str, Form()] = "") -> Response:

View file

@ -157,6 +157,25 @@ def tool_rounds(workspace: Workspace, settings) -> int:
return min(rounds, settings.max_tool_rounds_limit) return min(rounds, settings.max_tool_rounds_limit)
def chat_only(workspace: Workspace) -> bool:
"""Chat ohne Datenquellen erlaubt (Ausnahme vom Grundsatz „Context first“)."""
return bool((workspace.settings or {}).get("chat_only"))
def set_chat_only(session: Session, principal: Principal, workspace_id: int, enabled: bool) -> Workspace:
workspace = require_admin(session, principal, workspace_id).workspace
current = dict(workspace.settings or {})
if enabled:
current["chat_only"] = True
else:
current.pop("chat_only", None)
if current != (workspace.settings or {}):
workspace.settings = current
audit.record(session, "workspace.chat_only_set", user_id=principal.user_id, workspace_id=workspace_id,
target_kind="workspace", target_id=workspace_id, enabled=enabled)
return workspace
def set_tool_rounds(session: Session, principal: Principal, settings, workspace_id: int, value: str) -> Workspace: def set_tool_rounds(session: Session, principal: Principal, settings, workspace_id: int, value: str) -> Workspace:
"""Leer = Standard; sonst 4 bis AIW_MAX_TOOL_ROUNDS_LIMIT.""" """Leer = Standard; sonst 4 bis AIW_MAX_TOOL_ROUNDS_LIMIT."""
workspace = require_admin(session, principal, workspace_id).workspace workspace = require_admin(session, principal, workspace_id).workspace

52
docs/checkliste.md Normal file
View file

@ -0,0 +1,52 @@
# Checkliste: Bifröst lokal startklar
Kurzfassung zum Abhaken. Details je Schritt: [Einrichtung nach dem Start](einrichtung.md); Klickwege auch in der Online-Hilfe (*Für Plattform-Admins → Ersteinrichtung*, *Für Workspace-Admins → Workspace startklar machen*).
Anmelden als `admin`, Passwort aus `AIW_DEMO_PASSWORD` (Standard `demo`), unter http://127.0.0.1:8765.
## Basis (Pflicht)
- [ ] **Von null starten:** `scripts/dev.sh --reset` → Datenbank, Demo-Nutzer (admin, entwicklung, fachbereich, freigabe), drei Demo-Workspaces, Shop-Datenbank mit PostgreSQL-Treiber
- [ ] **Modell:** *Administration → Modelle und Zugänge* → Endpunkt, API-Key (alle Teams), Modell, *Verbindung testen*
- Klappt, wenn eine Beispielfrage im Bestellservice beantwortet wird
- [ ] **Tools:** *Administration → Tools* → nichts steht auf „nicht eingestuft“ (sonst *Vorschläge übernehmen*)
- Klappt, wenn unter der Antwort Tool-Schritte erscheinen
Damit funktionieren alle drei Demos vollständig.
## Integrationen (nach Bedarf)
- [ ] **Munin (Gedächtnis):** *Administration → MCP-Server* → Server mit Art *Gedächtnis*, *Tools abrufen*, *Empfehlung übernehmen*; je Workspace *Einstellungen → MCP-Server* → mit Munin-Key freischalten
- Klappt, wenn „Merk dir …“ einen Eintrag unter *Gedächtnis* erzeugt
- [ ] **Hugin (Regeln, Skills, Befehle):** lokal `scripts/demo.sh` im Repository `hugin`; *Administration → Hugin* → Host `http://127.0.0.1:8080`; je Workspace *Einstellungen → Hugin* → Integrations-Key aus `.demo/keys.txt`, Pakete *Abgleichen*, *Übernehmen*; Tools `load_skill`, `read_skill_file`, `hugin_feedback` einstufen
- Klappt, wenn `/` im Chat Befehle vorschlägt
- [ ] **Dokumenten-Auslese:** *Administration → MCP-Server* → Server mit Art *Dokumenten-Auslese*, *Tools abrufen*, Tool und Parameter (Inhalt Base64, Dateiname) wählen
- Klappt, wenn eine PDF im abgeglichenen Ordner „lesbar“ wird
- [ ] **Bitbucket:** *Daten → Git-Repository* → HTTPS-URL, Token, *Pull Requests* „Bitbucket Data Center“; Host in `AIW_GIT_ALLOWED_HOSTS`, Rechte auf `ai/*`
- Klappt, wenn unter *Änderungen* „Pull Request erstellen“ geht
- [ ] **E-Mail:** `AIW_SMTP_*` und `AIW_PUBLIC_URL` in `.env`, `scripts/dev.sh` neu starten; Adresse im *Profil*
- Klappt, wenn ein Freigabeantrag eine E-Mail auslöst (*Administration → Benachrichtigungen*)
- [ ] **Teams/Slack:** je Workspace *Einstellungen → Allgemein → Benachrichtigungen* → Webhook-URL
- [ ] **Oracle:** `scripts/dev.sh --oracle`; *Administration → Datenbank-Treiber* → URL und Schlüssel aus der Skript-Ausgabe, *Tools abrufen*, einstufen; im Workspace Datenbank mit Service `FREEPDB1` verbinden
## Zum Schluss durchprobieren
- [ ] Bestellservice: Beispielfragen zu Repository, Bestelldaten (Diagramm), Wartungsvertrag, Shop-Datenbank, Rabatt-Änderung
- [ ] Dieselbe Frage in „Demo: Nur Chat“: Die KI sagt, dass sie Vertrag, Code und Datenbank nicht kennt
- [ ] Freigabe zeigen (Einstellungen siehe unten), als `entwicklung` auslösen, als `freigabe` unter *Freigaben* entscheiden
## Hinweis: Freigaben für die Demo einstellen
Ab Werk braucht in den Demos kein Tool eine Freigabe: Lesen ist erlaubt, Commits landen nur auf dem Chat-Branch. Für eine Vorführung im Bestellservice, als `admin` unter *Einstellungen → Tools und Regeln*:
| Einstellung | Wo | Was man damit zeigt |
|---|---|---|
| **`commit_change` nur mit Freigabe** für das Repository `bestellservice` | *Regeln je Quelle* | Codeänderungen erst nach Entscheidung. Beispielfrage „Setze die neue Rabattregel … um“: Die Antwort pausiert, Antrag mit Diff, nach „Freigeben“ geht sie weiter |
| **Vier Augen** an derselben Regel | *Regeln je Quelle* | Wer auslöst, kann nicht selbst freigeben. Als `admin` fragen: Der Antrag wartet auf `freigabe` |
| **`shop__query` auf „Freigabe nötig“** | *Tools* | Auch Lesen kann geschützt sein (z. B. Produktivdaten): Die KI bekommt das Abfrageergebnis erst nach der Freigabe; der Antrag zeigt das SQL |
| optional: **Globaler Approver** | *Administration → Nutzer* (`freigabe` markieren), *Administration → Tools* (Freigabestufe „global“, z. B. bei `merge_target`) | Entscheidung außerhalb des Workspace. Gilt plattformweit, nach der Demo zurückstellen |
Empfehlung für eine kurze Vorführung: die ersten beiden Zeilen. `shop__query` mit Freigabe ist eindrucksvoll, bremst aber jede Datenbankfrage; danach wieder auf „wie global“ stellen. Der Modus *Jede Schreibaktion in ein Fremdsystem* (*Einstellungen → Allgemein → Freigaben*) wirkt in den Demos kaum, weil ihre Tools nicht nach außen schreiben; er lohnt sich erst mit schreibenden MCP-Servern oder `hugin_feedback`.
Mit E-Mail (siehe oben) bekommt `freigabe` zusätzlich eine Nachricht zu jedem Antrag.
- [ ] Antworten mit 👍/👎 bewerten → *Nutzung und Kosten* zeigt Verbrauch und Rückmeldungen

View file

@ -2,13 +2,13 @@
Diese Anleitung setzt dort an, wo `scripts/dev.sh --reset` (oder `--setup` plus `uv run aiw demo`) aufhört. Danach gibt es schon: Diese Anleitung setzt dort an, wo `scripts/dev.sh --reset` (oder `--setup` plus `uv run aiw demo`) aufhört. Danach gibt es schon:
- Demo-Team mit anna (Plattform-Admin), ben und clara, Passwort aus `AIW_DEMO_PASSWORD` - Demo-Team mit den Nutzern admin (Plattform-Admin), entwicklung, fachbereich und freigabe (Freigeber in den Demo-Workspaces), Passwort aus `AIW_DEMO_PASSWORD`
- die Workspaces „Demo: Bestellservice“ (mit Shop-Datenbank) und „Demo: MCP-Entwicklung“ - die Workspaces „Demo: Bestellservice“ (mit Shop-Datenbank), „Demo: MCP-Entwicklung“ und „Demo: Nur Chat“ (ohne Datenquellen, zum Vergleich)
- den Datenbank-Treiber PostgreSQL und die eingebauten Tools, nach Vorschlag eingestuft - den Datenbank-Treiber PostgreSQL und die eingebauten Tools, nach Vorschlag eingestuft
Diese Fassung ist für Entwicklung und Betrieb: mit `.env`, lokalen Diensten und Ports. Die reinen Klickwege stehen auch in der Online-Hilfe: *Für Plattform-Admins → Ersteinrichtung* (`/hilfe/plattform-admin#ersteinrichtung`) und *Für Workspace-Admins → Workspace startklar machen* (`/hilfe/workspace-admin#workspace-startklar-machen`). Ändert sich ein Klickweg, beide Stellen anpassen. Zum Abhaken in kurz: [Checkliste](checkliste.md). Diese Fassung ist für Entwicklung und Betrieb: mit `.env`, lokalen Diensten und Ports. Die reinen Klickwege stehen auch in der Online-Hilfe: *Für Plattform-Admins → Ersteinrichtung* (`/hilfe/plattform-admin#ersteinrichtung`) und *Für Workspace-Admins → Workspace startklar machen* (`/hilfe/workspace-admin#workspace-startklar-machen`). Ändert sich ein Klickweg, beide Stellen anpassen.
Alles Weitere richtest du als `anna` unter http://127.0.0.1:8765 ein. Reihenfolge: erst das Pflichtprogramm (1–2), dann, was du brauchst (3–8). Änderungen an `.env` wirken erst nach einem Neustart von `scripts/dev.sh`. Alles Weitere richtest du als `admin` unter http://127.0.0.1:8765 ein. Reihenfolge: erst das Pflichtprogramm (1–2), dann, was du brauchst (3–8). Änderungen an `.env` wirken erst nach einem Neustart von `scripts/dev.sh`.
| Nr. | Was | Pflicht? | Ohne geht nicht … | | Nr. | Was | Pflicht? | Ohne geht nicht … |
|---|---|---|---| |---|---|---|---|

View file

@ -26,13 +26,13 @@ scripts/dev.sh --reset --oracle # zusätzlich die Oracle-Daten löschen un
1. **Prüfen:** Läuft auf dem Web-Port noch etwas (Bifröst), bricht das Skript ab. Ohne Terminal und ohne `--yes` bricht es ebenfalls ab, statt auf eine Antwort zu warten. 1. **Prüfen:** Läuft auf dem Web-Port noch etwas (Bifröst), bricht das Skript ab. Ohne Terminal und ohne `--yes` bricht es ebenfalls ab, statt auf eine Antwort zu warten.
2. **Löschen:** PostgreSQL-Container und -Volume (`ai-workspace_aiw-postgres`), also Workspaces, Chats, Nutzer, Modelle, API-Keys, Einstellungen und die Shop-Datenbank der Demo; dazu `data/` (Uploads, Git-Cache, Demo-Repositories) und die Ablagen aus `AIW_UPLOAD_DIR` und `AIW_GIT_CACHE_DIR`, sofern sie im Projekt liegen. Oracle nur mit `--oracle`. 2. **Löschen:** PostgreSQL-Container und -Volume (`ai-workspace_aiw-postgres`), also Workspaces, Chats, Nutzer, Modelle, API-Keys, Einstellungen und die Shop-Datenbank der Demo; dazu `data/` (Uploads, Git-Cache, Demo-Repositories) und die Ablagen aus `AIW_UPLOAD_DIR` und `AIW_GIT_CACHE_DIR`, sofern sie im Projekt liegen. Oracle nur mit `--oracle`.
3. **Aufsetzen wie `--setup`:** Container starten, Migrationen, Demo-Nutzer anna, ben, clara. 3. **Aufsetzen wie `--setup`:** Container starten, Migrationen, Demo-Nutzer admin (Plattform-Admin), entwicklung, fachbereich, freigabe.
4. **Demo:** DB-Dienst PostgreSQL starten, dann `aiw demo`, also beide Demo-Workspaces samt Shop-Datenbank und registriertem PostgreSQL-Treiber. 4. **Demo:** DB-Dienst PostgreSQL starten, dann `aiw demo`, also beide Demo-Workspaces samt Shop-Datenbank und registriertem PostgreSQL-Treiber.
5. **Starten:** Web und Worker wie gewohnt. 5. **Starten:** Web und Worker wie gewohnt.
Was bleibt: `.env` (Master-Key, Demo-Passwort, eigene Einstellungen) und die Docker-Images. Danach fehlt nur das Modell: als `anna` unter *Administration → Modelle und Zugänge* Endpunkt, API-Key und Modell anlegen. Wer auch die `.env` neu will, löscht sie vorher; `--reset` legt sie dann aus `.env.example` an. Was bleibt: `.env` (Master-Key, Demo-Passwort, eigene Einstellungen) und die Docker-Images. Danach fehlt nur das Modell: als `admin` unter *Administration → Modelle und Zugänge* Endpunkt, API-Key und Modell anlegen. Wer auch die `.env` neu will, löscht sie vorher; `--reset` legt sie dann aus `.env.example` an.
Was danach noch einzurichten ist (Modell, Munin, Hugin, Dokumenten-Auslese, Bitbucket, Benachrichtigungen): [Einrichtung nach dem Start](einrichtung.md). Was danach noch einzurichten ist (Modell, Munin, Hugin, Dokumenten-Auslese, Bitbucket, Benachrichtigungen): kurz in der [Checkliste](checkliste.md), ausführlich in [Einrichtung nach dem Start](einrichtung.md).
Nur die Demos neu, alles andere behalten: `uv run aiw demo --reset` (löscht dabei die Chats der Demo-Workspaces). Nur die Demos neu, alles andere behalten: `uv run aiw demo --reset` (löscht dabei die Chats der Demo-Workspaces).
@ -53,7 +53,7 @@ AIW_MCP_ALLOW_LOCAL=true AIW_DB_ALLOW_LOCAL=true AIW_METRICS_TOKEN=lokal-token-1
uv run aiw web --port 8765 uv run aiw web --port 8765
``` ```
Die App läuft unter http://127.0.0.1:8765, Anmeldung zum Beispiel mit `anna`. Die App läuft unter http://127.0.0.1:8765, Anmeldung zum Beispiel mit `admin`.
## 3. Worker (Terminal 2) ## 3. Worker (Terminal 2)

View file

@ -131,9 +131,10 @@ Stand: 25.10.2026. Reihenfolge so gewählt, dass spätere Schritte auf früheren
## 9d. Demo-Workspace, Beispielfragen, Rückmeldungen ## 9d. Demo-Workspace, Beispielfragen, Rückmeldungen
- [ ] Von null: Bifröst stoppen, `scripts/dev.sh --reset` → Rückfrage, „ja“ → alte Daten weg, Demo-Nutzer und beide Demo-Workspaces da, Hinweis „Modell einrichten“. Läuft Bifröst noch, bricht `--reset` ab; ohne Terminal und ohne `--yes` ebenso. - [ ] Von null: Bifröst stoppen, `scripts/dev.sh --reset` → Rückfrage, „ja“ → alte Daten weg, Demo-Nutzer und beide Demo-Workspaces da, Hinweis „Modell einrichten“. Läuft Bifröst noch, bricht `--reset` ab; ohne Terminal und ohne `--yes` ebenso.
- [ ] `uv run aiw demo` → „Demo: Bestellservice“ (Repository mit 6 Commits und Branch `feature/gutscheine`, `bestellungen_2026.csv` mit 1200 Zeilen, *Projektunterlagen* mit 5 Dokumenten) und „Demo: MCP-Entwicklung“ (Repository `ticket-mcp` mit Branch `feature/anhaenge`, `tool_aufrufe_2026_10.csv` mit 2000 Zeilen, *MCP-Unterlagen* mit 5 Dokumenten), je sechs Beispielfragen. Mit laufendem `scripts/dev.sh` zusätzlich im Bestellservice die *Shop-Datenbank* (Treiber PostgreSQL registriert bzw. wiederverwendet, Status verbunden, zwei weitere Beispielfragen); ohne DB-Dienst Hinweis „Datenbank übersprungen“. Zweiter Aufruf ändert nichts; `aiw demo mcp --reset` legt nur diese neu an. - [ ] `uv run aiw demo` → „Demo: Bestellservice“ (Repository mit 6 Commits und Branch `feature/gutscheine`, `bestellungen_2026.csv` mit 1200 Zeilen, *Projektunterlagen* mit 5 Dokumenten) und „Demo: MCP-Entwicklung“ (Repository `ticket-mcp` mit Branch `feature/anhaenge`, `tool_aufrufe_2026_10.csv` mit 2000 Zeilen, *MCP-Unterlagen* mit 5 Dokumenten), je sechs Beispielfragen, sowie „Demo: Nur Chat“ ohne Datenquellen (Kontext-Zeile „Nur Chat, keine Datenquellen“, sieben Beispielfragen). Mitglieder überall admin (Admin), entwicklung, fachbereich, freigabe (Freigeber). Mit laufendem `scripts/dev.sh` zusätzlich im Bestellservice die *Shop-Datenbank* (Treiber PostgreSQL registriert bzw. wiederverwendet, Status verbunden, zwei weitere Beispielfragen); ohne DB-Dienst Hinweis „Datenbank übersprungen“. Zweiter Aufruf ändert nichts (drei Mal „existiert schon“); `aiw demo mcp --reset` legt nur diese neu an.
- [ ] Übersicht → *Ausprobieren*: Klick auf eine Frage startet einen Chat mit genau dieser Frage. Leerer Chat zeigt dieselben Fragen unter „Zum Beispiel“. - [ ] Übersicht → *Ausprobieren*: Klick auf eine Frage startet einen Chat mit genau dieser Frage. Leerer Chat zeigt dieselben Fragen unter „Zum Beispiel“.
- [ ] Bestellservice durchspielen: Aufbau (Repo), letzte zwei Wochen (Commits), Umsatz je Region (Tabelle + Diagramm), Wartungsvertrag (Dokumente), Rabatt Code vs. Protokoll (findet den Widerspruch 10 % ab 500 € vs. 5 % ab 1.000 €), Umsetzen (Commit auf `ai/…`, Test angepasst). - [ ] Bestellservice durchspielen: Aufbau (Repo), letzte zwei Wochen (Commits), Umsatz je Region (Tabelle + Diagramm), Wartungsvertrag (Dokumente), Rabatt Code vs. Protokoll (findet den Widerspruch 10 % ab 500 € vs. 5 % ab 1.000 €), Umsetzen (Commit auf `ai/…`, Test angepasst).
- [ ] Vergleich: dieselbe Frage (Wartungsvertrag, Rabatt, Retouren) in „Demo: Nur Chat“ und im Bestellservice → im Nur-Chat sagt die KI, dass sie Vertrag, Code bzw. Datenbank nicht kennt, und erfindet nichts; im Bestellservice Antwort mit Quellen. In einem neuen Workspace ohne Quelle: kein „Neuer Chat“; *Einstellungen → Ohne Datenquellen chatten* an → Chat möglich, Eintrag im Audit-Log; wieder aus → kein neuer Chat.
- [ ] Shop-Datenbank: „Welche Kunden haben 2026 am meisten retourniert …?“ → `shop__list_tables`/`shop__query`, Tabelle mit Kunden und Gründen; Umsatz Gewerbe/Privat je Quartal als Diagramm; Bitte „lösche die Retouren“ → Abfrage abgewiesen (nur lesend). - [ ] Shop-Datenbank: „Welche Kunden haben 2026 am meisten retourniert …?“ → `shop__list_tables`/`shop__query`, Tabelle mit Kunden und Gründen; Umsatz Gewerbe/Privat je Quartal als Diagramm; Bitte „lösche die Retouren“ → Abfrage abgewiesen (nur lesend).
- [ ] MCP-Entwicklung durchspielen: Annotations prüfen (findet `close_ticket` mit `readOnlyHint=True` und begründet mit Leitfaden bzw. Bifröst-Einstufung), Review vs. Code (close_ticket, limit/cursor, list_my_tickets offen), langsamste Tools (search_tickets, Timeouts bei großem `limit`, Diagramm), Anbindung an Bifröst, Umsetzen (Annotations, `Field(ge=1, le=50)`, cursor, Tests), Entwurf `list_my_tickets` nach API-Beschreibung. - [ ] MCP-Entwicklung durchspielen: Annotations prüfen (findet `close_ticket` mit `readOnlyHint=True` und begründet mit Leitfaden bzw. Bifröst-Einstufung), Review vs. Code (close_ticket, limit/cursor, list_my_tickets offen), langsamste Tools (search_tickets, Timeouts bei großem `limit`, Diagramm), Anbindung an Bifröst, Umsetzen (Annotations, `Field(ge=1, le=50)`, cursor, Tests), Entwurf `list_my_tickets` nach API-Beschreibung.
- [ ] *Einstellungen → Allgemein → Beispielfragen*: ändern, leeren (Abschnitt verschwindet), mehr als 8 → Fehlermeldung. - [ ] *Einstellungen → Allgemein → Beispielfragen*: ändern, leeren (Abschnitt verschwindet), mehr als 8 → Fehlermeldung.

View file

@ -102,7 +102,7 @@ if $SETUP; then
printf 'AIW_SECRET_KEY=%s\n' "$key" >> .env printf 'AIW_SECRET_KEY=%s\n' "$key" >> .env
fi fi
if [ -z "$(env_value AIW_DEMO_PASSWORD)" ]; then if [ -z "$(env_value AIW_DEMO_PASSWORD)" ]; then
echo "Hinweis: AIW_DEMO_PASSWORD in .env setzen, sonst ist die Demo-Anmeldung (anna, ben, clara) aus." echo "Hinweis: AIW_DEMO_PASSWORD in .env setzen, sonst ist die Demo-Anmeldung (admin, entwicklung, fachbereich, freigabe) aus."
fi fi
fi fi
@ -166,8 +166,8 @@ say "Worker starten"
PIDS+=($!) PIDS+=($!)
if $RESET; then if $RESET; then
say "Fertig von null. Noch offen: als anna ein Modell einrichten (Administration → Modelle und Zugänge)." say "Fertig von null. Noch offen: als admin ein Modell einrichten (Administration → Modelle und Zugänge)."
say "Alles Weitere (Munin, Hugin, Dokumenten-Auslese, Bitbucket …): docs/einrichtung.md" say "Alles Weitere (Munin, Hugin, Dokumenten-Auslese, Bitbucket …): docs/checkliste.md"
say "DB-Dienste (PostgreSQL ist für die Demo schon registriert):" say "DB-Dienste (PostgreSQL ist für die Demo schon registriert):"
else else
say "DB-Dienste, einmalig registrieren unter Administration → Datenbank-Treiber, dann Tools abrufen und einstufen:" say "DB-Dienste, einmalig registrieren unter Administration → Datenbank-Treiber, dann Tools abrufen und einstufen:"

View file

@ -3,20 +3,22 @@
import subprocess import subprocess
import pytest import pytest
from sqlalchemy import create_engine, select, text from sqlalchemy import create_engine, func, select, text
from sqlalchemy.engine import make_url from sqlalchemy.engine import make_url
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from app import demo from app import demo
from app.audit.models import AuditEvent
from app.chat.models import ChatSession, Message from app.chat.models import ChatSession, Message
from app.data_sources.models import DataSource from app.data_sources.models import DataSource
from app.data_sources.service import chat_ready
from app.demo import shop_db from app.demo import shop_db
from app.documents.models import DocFile from app.documents.models import DocFile
from app.feedback.models import AnswerFeedback from app.feedback.models import AnswerFeedback
from app.identity.models import User from app.identity.models import User
from app.tasks.worker import Worker from app.tasks.worker import Worker
from app.uploads.models import Upload from app.uploads.models import Upload
from app.workspaces.models import Workspace from app.workspaces.models import Workspace, WorkspaceMembership
from helpers import connect_repo, create_workspace from helpers import connect_repo, create_workspace
@ -115,44 +117,95 @@ def test_beispielfragen(answered, db, settings, fake_provider):
def test_demo_workspaces(db, people, settings, capsys, monkeypatch): def test_demo_workspaces(db, people, settings, capsys, monkeypatch):
# Ohne DB-Dienst: die Beispieldatenbank wird mit Hinweis übersprungen (eigener Test unten) # Ohne DB-Dienst: die Beispieldatenbank wird mit Hinweis übersprungen (eigener Test unten)
monkeypatch.setattr(shop_db, "reachable", lambda: False) monkeypatch.setattr(shop_db, "reachable", lambda: False)
with db.session() as session:
session.get(User, people["anna"]).is_admin = True
demos = demo.all_demos() demos = demo.all_demos()
assert demo.run(settings) == 0 assert demo.run(settings) == 0
first = {} first = {}
with db.session() as session: with db.session() as session:
# Nutzer heißen nach ihrer Rolle; freigabe ist Freigeber in jeder Demo
admin = session.scalar(select(User).where(User.username == "admin"))
assert admin.is_admin and admin.display_name == "Admin"
for d in demos.values(): for d in demos.values():
workspace = session.scalar(select(Workspace).where(Workspace.name == d.name)) workspace = session.scalar(select(Workspace).where(Workspace.name == d.name))
assert workspace.settings["examples"] == d.examples assert workspace.settings["examples"] == d.examples
roles = dict(session.execute(select(User.username, WorkspaceMembership.role).join(
WorkspaceMembership, WorkspaceMembership.user_id == User.id).where(
WorkspaceMembership.workspace_id == workspace.id)).all())
assert roles == {"admin": "admin", "entwicklung": "member", "fachbereich": "member",
"freigabe": "approver"}
first[d.key] = workspace.id
if d.key == "chat":
# Nur Chat: keine Quelle, keine Datei, kein Ordner, trotzdem bereit
assert workspace.settings["chat_only"] is True
assert session.scalar(select(DataSource).where(DataSource.workspace_id == workspace.id)) is None
assert session.scalar(select(Upload).where(Upload.workspace_id == workspace.id)) is None
assert chat_ready(session, workspace.id)
continue
assert "chat_only" not in workspace.settings
source = session.scalar(select(DataSource).where(DataSource.workspace_id == workspace.id)) source = session.scalar(select(DataSource).where(DataSource.workspace_id == workspace.id))
assert source.status == "ok" and source.name == d.repo assert source.status == "ok" and source.name == d.repo
upload = session.scalar(select(Upload).where(Upload.workspace_id == workspace.id)) upload = session.scalar(select(Upload).where(Upload.workspace_id == workspace.id))
assert upload.status == "ok" and upload.filename == d.upload[0] assert upload.status == "ok" and upload.filename == d.upload[0]
first[d.key] = workspace.id assert len(session.scalars(select(DocFile)).all()) == sum(len(d.documents or {}) for d in demos.values())
assert len(session.scalars(select(DocFile)).all()) == sum(len(d.documents) for d in demos.values())
out = capsys.readouterr().out out = capsys.readouterr().out
assert "Datenbank übersprungen: kein DB-Dienst" in out assert "Datenbank übersprungen: kein DB-Dienst" in out
assert out.count("angelegt (ID") == 2 and out.count("verbunden") == 2 and "1200 Zeilen" in out assert out.count("angelegt (ID") == 3 and out.count("verbunden") == 2 and "1200 Zeilen" in out
assert "Keine Datenquellen: Chat ohne Kontext eingeschaltet" in out
# Zweiter Lauf lässt alles, wie es ist; --reset legt nur die gewählte Demo neu an # Zweiter Lauf lässt alles, wie es ist; --reset legt nur die gewählte Demo neu an
assert demo.run(settings) == 0 and capsys.readouterr().out.count("existiert schon") == 2 assert demo.run(settings) == 0 and capsys.readouterr().out.count("existiert schon") == 3
assert demo.run(settings, ["mcp"], reset=True) == 0 assert demo.run(settings, ["mcp"], reset=True) == 0
with db.session() as session: with db.session() as session:
ids = {d.key: session.scalar(select(Workspace.id).where(Workspace.name == d.name)) for d in demos.values()} ids = {d.key: session.scalar(select(Workspace.id).where(Workspace.name == d.name)) for d in demos.values()}
assert ids["bestellservice"] == first["bestellservice"] and ids["mcp"] != first["mcp"] assert ids["bestellservice"] == first["bestellservice"] and ids["mcp"] != first["mcp"]
# Repositories mit Verlauf und Feature-Branch; Daten reproduzierbar # Repositories mit Verlauf und Feature-Branch, Autoren nach Rolle; Daten reproduzierbar
for d in demos.values(): for d in (d for d in demos.values() if d.repo):
repo = demo.repo_path(settings, d) repo = demo.repo_path(settings, d)
log = subprocess.run(["git", "log", "--format=%s", "main"], capture_output=True, text=True, cwd=repo) log = subprocess.run(["git", "log", "--format=%s", "main"], capture_output=True, text=True, cwd=repo)
assert log.stdout.splitlines()[0] == d.commits[-1][2] and len(log.stdout.splitlines()) == len(d.commits) assert log.stdout.splitlines()[0] == d.commits[-1][2] and len(log.stdout.splitlines()) == len(d.commits)
branches = subprocess.run(["git", "branch", "--format=%(refname:short)"], capture_output=True, text=True, branches = subprocess.run(["git", "branch", "--format=%(refname:short)"], capture_output=True, text=True,
cwd=repo).stdout.split() cwd=repo).stdout.split()
assert d.branch[0] in branches assert d.branch[0] in branches
authors = subprocess.run(["git", "log", "--format=%an", "--all"], capture_output=True, text=True,
cwd=repo).stdout.split()
assert set(authors) <= {"Admin", "Entwicklung", "Fachbereich"}
assert d.upload[1]() == d.upload[1]() assert d.upload[1]() == d.upload[1]()
# Der eingebaute Fehler der MCP-Demo steht im Code, die Korrektur nur im Protokoll # Der eingebaute Fehler der MCP-Demo steht im Code, die Korrektur nur im Protokoll
server = demos["mcp"].commits[-2][3]["ticket_mcp/server.py"] server = demos["mcp"].commits[-2][3]["ticket_mcp/server.py"]
assert 'title="Ticket schließen", readOnlyHint=True' in server and "limit: int = 20" in server assert 'title="Ticket schließen", readOnlyHint=True' in server and "limit: int = 20" in server
def test_chat_ohne_datenquellen(answered, db, browser, people, fake_provider, settings):
anna, ben = answered["anna"], answered["ben"]
workspace_id = create_workspace(anna, people["team_demo"], name="Nur Chat")
anna.post(f"/workspaces/{workspace_id}/members", {"username": "ben", "role": "member"})
# Ohne Quelle kein Chat (Context first) …
assert "Erste Schritte" in ben.get(f"/workspaces/{workspace_id}").text
assert anna.post(f"/workspaces/{workspace_id}/chats").status_code == 400
# … außer der Admin schaltet es ausdrücklich ein
assert ben.post(f"/workspaces/{workspace_id}/chat-only", {"enabled": "1"}).status_code == 403
anna.post(f"/workspaces/{workspace_id}/chat-only", {"enabled": "1"})
assert 'name="enabled" value="1" checked' in anna.get(f"/workspaces/{workspace_id}/settings").text
page = ben.get(f"/workspaces/{workspace_id}").text
assert "Nur Chat, keine Datenquellen" in page and "Erste Schritte" not in page
chat_id = int(ben.post(f"/workspaces/{workspace_id}/chats").headers["location"].rsplit("/", 1)[1])
fake_provider.script = ["Allgemeine Antwort."]
ben.post(f"/chats/{chat_id}/messages", {"content": "Was ist ein MCP-Server?"}, headers={"HX-Request": "true"})
Worker(db, settings).run_once()
prompt = fake_provider.calls[-1]["instructions"]
assert "keine Daten angebunden" in prompt
# Wieder aus: kein neuer Chat; im Audit-Log steht beides
anna.post(f"/workspaces/{workspace_id}/chat-only", {})
assert anna.post(f"/workspaces/{workspace_id}/chats").status_code == 400
with db.session() as session:
assert session.scalar(select(func.count()).select_from(AuditEvent).where(
AuditEvent.action == "workspace.chat_only_set")) == 2
# Workspaces mit Daten bekommen den Hinweis nicht
chat = answered["chat_id"]
fake_provider.script = ["Antwort."]
anna.post(f"/chats/{chat}/messages", {"content": "Und jetzt?"}, headers={"HX-Request": "true"})
Worker(db, settings).run_once()
assert "keine Daten angebunden" not in fake_provider.calls[-1]["instructions"]
def test_beispieldatenbank(settings): def test_beispieldatenbank(settings):
name = shop_db.build(settings) name = shop_db.build(settings)
url = make_url(settings.database_url) url = make_url(settings.database_url)