diff --git a/README.md b/README.md index f3598fa..80f7c08 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ cp .env.example .env uv sync uv run aiw gen-key # Ausgabe als AIW_SECRET_KEY in .env eintragen 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 web # http://127.0.0.1:8000 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). -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 ` 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 ` 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 `_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: 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. **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 `` 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. diff --git a/app/chat/runner.py b/app/chat/runner.py index 1439ee7..e85feca 100644 --- a/app/chat/runner.py +++ b/app/chat/runner.py @@ -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) + ". Erst data_list_tables, dann gezielt mit data_query (DuckDB-SQL) auswerten; Zahlen nur aus " "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: access += ("Tools von MCP-Diensten, auch Datenbanken des Workspace: " + ", ".join(t.name for t in services) + ". Nutze sie nur, " diff --git a/app/cli.py b/app/cli.py index 4c098f1..77e653c 100644 --- a/app/cli.py +++ b/app/cli.py @@ -29,11 +29,12 @@ def migrate(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 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: team = session.scalar(select(Team).where(Team.slug == "demo")) if team is None: @@ -53,7 +54,8 @@ def seed_demo(settings: Settings) -> None: if other is None: session.add(Team(slug="andere", name="Anderes Team")) 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: @@ -131,8 +133,8 @@ def main() -> None: sub.add_parser("migrate", help="Datenbank auf den neuesten Stand bringen") 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.add_argument("names", nargs="*", choices=["bestellservice", "mcp"], metavar="name", - help="bestellservice, mcp (ohne Angabe: alle)") + demo.add_argument("names", nargs="*", choices=["bestellservice", "mcp", "chat"], metavar="name", + help="bestellservice, mcp, chat (ohne Angabe: alle)") 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.add_argument("--host", default="127.0.0.1") diff --git a/app/data_sources/service.py b/app/data_sources/service.py index 895d04e..dcbbcef 100644 --- a/app/data_sources/service.py +++ b/app/data_sources/service.py @@ -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: """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.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( DataSource.workspace_id == workspace_id, DataSource.status == "ok").limit(1)) is not None \ or has_tables(session, workspace_id) \ diff --git a/app/demo/__init__.py b/app/demo/__init__.py index a184bce..477e544 100644 --- a/app/demo/__init__.py +++ b/app/demo/__init__.py @@ -1,6 +1,6 @@ """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 -den Modulen dieses Pakets, hier nur der gemeinsame Ablauf. +Beispielfragen; oder ein Workspace ganz ohne Daten (nur Chat) zum Vergleich. Jede Demo legt alles reproduzierbar an +(gleiche Daten bei jedem Lauf); die Inhalte stehen in den Modulen dieses Pakets, hier nur der gemeinsame Ablauf. """ import io @@ -19,8 +19,12 @@ from app.core.config import Settings from app.core.crypto import Vault from app.core.database import Database -AUTHORS = {"ben": ("Ben Entwickler", "ben@demo.example"), "clara": ("Clara Fachbereich", "clara@demo.example"), - "anna": ("Anna Admin", "anna@demo.example")} +# Demo-Nutzer (aiw seed-demo) heißen nach ihrer Rolle; so auch die Autoren der Commits +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), # abgezweigt vom vorletzten Commit auf main @@ -38,12 +42,13 @@ class Demo: name: str description: str examples: list[str] - repo: str - commits: list[Commit] - branch: tuple[str, int, str, str, dict[str, str]] - upload: tuple[str, Callable[[], bytes]] - folder: str - documents: dict[str, str] + # Alles Folgende optional: ohne Repository, Datei und Ordner ist es ein Workspace nur für den Chat + repo: str = "" + commits: tuple[Commit, ...] | list[Commit] = () + branch: tuple[str, int, str, str, dict[str, str]] | None = None + upload: tuple[str, Callable[[], bytes]] | None = None + folder: str = "" + documents: dict[str, str] | None = None # Zusätzlich anbinden (z. B. Beispieldatenbank): (db, settings, user_id, workspace_id), auch bei vorhandenem # Workspace 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]: - from app.demo import bestellservice, mcp - return {d.key: d for d in (bestellservice.DEMO, mcp.DEMO)} + from app.demo import bestellservice, chat, mcp + 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: @@ -111,13 +116,13 @@ def run(settings: Settings, keys: list[str] | None = None, reset: bool = False) db = Database(settings.database_url) try: with db.session() as session: - anna = session.scalar(select(User).where(User.username == "anna")) - classified = tools.classify_suggested(session, principal_for(session, anna.id)) + admin = session.scalar(select(User).where(User.username == ADMIN)) + classified = tools.classify_suggested(session, principal_for(session, admin.id)) if classified: print(f"{classified} eingebaute Tools nach Vorschlag eingestuft (wie „Vorschläge übernehmen“ unter Tools).") for demo in chosen: - create(db, settings, demo, anna.id, reset) - print("Fertig. Anmelden als anna, ben oder clara (Passwort AIW_DEMO_PASSWORD).") + create(db, settings, demo, admin.id, reset) + print(f"Fertig. {LOGIN}") return 0 finally: 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: from app.ai import service as ai from app.data_sources import service as sources - from app.documents import service as documents from app.identity.models import Team from app.identity.service import principal_for 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) workspace = workspaces.create(session, principal, team.id, demo.name, demo.description) workspace_id = workspace.id - for username in ("ben", "clara"): - workspaces.set_member(session, principal, workspace_id, username, "member") + for username, role in MEMBERS.items(): + workspaces.set_member(session, principal, workspace_id, username, role) 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) - 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 - if settings.git_allow_local: + if demo.repo and settings.git_allow_local: target = repo_path(settings, demo) build_repo(demo, target) 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) print(f" Repository {demo.repo} ({len(demo.commits)} Commits auf main, Branch {demo.branch[0]}): " 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).") # Datei zur Analyse - filename, generate = demo.upload - with db.session() as session: - 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}.") + if demo.upload: + filename, generate = demo.upload + with db.session() as session: + 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) + 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: folder = documents.create(session, principal, workspace_id, demo.folder, documents.WORKSPACE, True) 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.prune(session, principal, settings, folder_id, list(demo.documents)) 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“.") diff --git a/app/demo/bestellservice.py b/app/demo/bestellservice.py index b8f1d2a..423cc22 100644 --- a/app/demo/bestellservice.py +++ b/app/demo/bestellservice.py @@ -203,20 +203,20 @@ def einloesen(code: str, betrag: int) -> int: ''' COMMITS = [ - (21, "ben", "Grundgerüst Bestellservice", { + (21, "entwicklung", "Grundgerüst Bestellservice", { "README.md": README, "pyproject.toml": PYPROJECT, "bestellservice/__init__.py": "", "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}), - (12, "ben", "Rabattstaffel einführen", { + (12, "entwicklung", "Rabattstaffel einführen", { "bestellservice/rabatt.py": RABATT, "tests/test_rabatt.py": TEST_RABATT, "bestellservice/bestellung.py": BESTELLUNG.replace(" storniert: bool = False\n", "")}), - (6, "anna", "Projektregeln in AGENTS.md festhalten", {"AGENTS.md": AGENTS}), - (3, "ben", "API: Bestellung stornieren", { + (6, "admin", "Projektregeln in AGENTS.md festhalten", {"AGENTS.md": AGENTS}), + (3, "entwicklung", "API: Bestellung stornieren", { "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}) # --- Projektunterlagen (Pfad, Text) ----------------------------------------------------------------------------- diff --git a/app/demo/chat.py b/app/demo/chat.py new file mode 100644 index 0000000..a2c2dec --- /dev/null +++ b/app/demo/chat.py @@ -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) diff --git a/app/demo/mcp.py b/app/demo/mcp.py index bd81741..31a91f3 100644 --- a/app/demo/mcp.py +++ b/app/demo/mcp.py @@ -259,10 +259,10 @@ def get_attachment(ticket_id: str, attachment_id: str) -> dict: ''' 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": "", "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/test_tools.py": TESTS.replace(''' @@ -270,14 +270,14 @@ def test_kommentar(fake): server.add_comment("SD-1", "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}), - (9, "anna", "Projektregeln in AGENTS.md", {"AGENTS.md": AGENTS}), - (5, "ben", "close_ticket: Tickets schließen", { + (9, "admin", "Projektregeln in AGENTS.md", {"AGENTS.md": AGENTS}), + (5, "entwicklung", "close_ticket: Tickets schließen", { "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}) # --- Unterlagen ------------------------------------------------------------------------------------------------- diff --git a/app/help/pages/faq.md b/app/help/pages/faq.md index c728a82..4474582 100644 --- a/app/help/pages/faq.md +++ b/app/help/pages/faq.md @@ -6,7 +6,7 @@ Probleme, die häufiger vorkommen, und was dagegen hilft. ### 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“ diff --git a/app/help/pages/plattform-admin.md b/app/help/pages/plattform-admin.md index dd87edf..66b54db 100644 --- a/app/help/pages/plattform-admin.md +++ b/app/help/pages/plattform-admin.md @@ -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)). -**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 diff --git a/app/help/pages/start.md b/app/help/pages/start.md index 7a3d502..8f50a4e 100644 --- a/app/help/pages/start.md +++ b/app/help/pages/start.md @@ -51,5 +51,5 @@ Mehr dazu: [Für Plattform-Admins](/hilfe/plattform-admin). ## 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. -- **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. diff --git a/app/help/pages/workspace-admin.md b/app/help/pages/workspace-admin.md index 6e177a4..4dd3781 100644 --- a/app/help/pages/workspace-admin.md +++ b/app/help/pages/workspace-admin.md @@ -35,7 +35,7 @@ Es muss immer mindestens ein Admin bleiben. Freigeber lohnen sich, damit Anträg ## 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. @@ -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. +## 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 Unter *Einstellungen → Allgemein → Modell* wählst du das Modell des Workspace aus den freigegebenen Modellen. Ohne Auswahl gilt das Standardmodell der Plattform. diff --git a/app/static/css/app.css b/app/static/css/app.css index 0936008..5290f85 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -140,6 +140,26 @@ top: 0; 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) { diff --git a/app/static/js/app.js b/app/static/js/app.js index f068c18..767221c 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -113,6 +113,30 @@ document.addEventListener('click', function (e) { }); }); +//