"""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. """ import io import os import shutil import subprocess import tempfile from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta from pathlib import Path from sqlalchemy import select 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")} # Commit: (Tage zurück, Autor, Nachricht, {Pfad: Inhalt}); Branch: (Name, Tage zurück, Autor, Nachricht, Dateien), # abgezweigt vom vorletzten Commit auf main Commit = tuple[int, str, str, dict[str, str]] def day(days: int) -> date: """Datum relativ zu heute: Protokolle passen so zeitlich zu den Commits, egal wann die Demo angelegt wird.""" return date.today() - timedelta(days=days) @dataclass(frozen=True) class Demo: key: str 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] # 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 def git(cwd: Path, *args: str, env: dict | None = None) -> None: subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, env={**os.environ, **(env or {})}) def write_files(root: Path, files: dict[str, str]) -> None: for path, content in files.items(): target = root / path target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content) def commit(work: Path, days: int, author: str, message: str) -> None: name, email = AUTHORS[author] when = (datetime.now().astimezone() - timedelta(days=days)).replace(hour=10, minute=15, second=0, microsecond=0).isoformat() git(work, "add", "-A") git(work, "commit", "-q", "-m", message, env={ "GIT_AUTHOR_NAME": name, "GIT_AUTHOR_EMAIL": email, "GIT_AUTHOR_DATE": when, "GIT_COMMITTER_NAME": name, "GIT_COMMITTER_EMAIL": email, "GIT_COMMITTER_DATE": when}) def build_repo(demo: Demo, target: Path) -> None: """Bare-Repository mit Verlauf und einem Feature-Branch.""" if target.exists(): shutil.rmtree(target) target.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory() as tmp: work = Path(tmp) / "work" work.mkdir() git(work, "init", "-q", "-b", "main") for days, author, message, files in demo.commits: write_files(work, files) commit(work, days, author, message) branch, days, author, message, files = demo.branch git(work, "checkout", "-q", "-b", branch, "main~1") write_files(work, files) commit(work, days, author, message) git(work, "checkout", "-q", "main") git(work.parent, "clone", "-q", "--bare", str(work), str(target.resolve())) def repo_path(settings: Settings, demo: Demo) -> Path: return Path(settings.git_cache_dir).parent / "demo" / f"{demo.repo}.git" def all_demos() -> dict[str, Demo]: from app.demo import bestellservice, mcp return {d.key: d for d in (bestellservice.DEMO, mcp.DEMO)} def run(settings: Settings, keys: list[str] | None = None, reset: bool = False) -> int: from app.cli import seed_demo from app.identity.models import User from app.identity.service import principal_for from app.tools import service as tools demos = all_demos() chosen = [demos[k] for k in keys] if keys else list(demos.values()) seed_demo(settings) 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)) 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).") return 0 finally: db.dispose() 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 from app.workspaces import removal from app.workspaces import service as workspaces from app.workspaces.models import Workspace vault = Vault(settings.secret_key) with db.session() as session: team = session.scalar(select(Team).where(Team.slug == "demo")) existing = session.scalar(select(Workspace).where(Workspace.team_id == team.id, Workspace.name == demo.name, Workspace.deleted.is_(None))) if existing is not None and not reset: print(f"„{demo.name}“ existiert schon (ID {existing.id}). Neu anlegen mit: aiw demo {demo.key} --reset") existing_id = existing.id else: existing_id = None if existing is not None and reset: removal.remove(session, settings, existing, None) print(f"Alten Workspace „{demo.name}“ gelöscht.") if existing_id is not None: if demo.extra: demo.extra(db, settings, user_id, existing_id) return with db.session() as session: 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") workspaces.set_examples(session, principal, workspace_id, "\n".join(demo.examples)) model = ai.effective_model(session, workspace) print(f"„{demo.name}“ angelegt (ID {workspace_id}), Mitglieder anna (Admin), ben, clara.") # Repository if settings.git_allow_local: target = repo_path(settings, demo) build_repo(demo, target) with db.session() as session: source = sources.create_git(session, principal, settings, vault, workspace_id, name=demo.repo, url=target.resolve().as_uri(), branch="main", token="", exposure_confirmed=True) source_id = source.id with db.session() as session: source = sources.require_testable(session, principal, source_id) result = sources.run_test(source, settings, vault) with db.session() as session: 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: 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}.") # Dokumentenordner (Textdateien, brauchen keine Dokumenten-Auslese) with db.session() as session: folder = documents.create(session, principal, workspace_id, demo.folder, documents.WORKSPACE, True) folder_id = folder.id modified = int(datetime.now().timestamp() * 1000) with db.session() as session: for path, text in demo.documents.items(): 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“.")