bifroest/app/demo/__init__.py
2026-10-27 08:34:17 +01:00

202 lines
8.9 KiB
Python

"""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“.")