202 lines
8.9 KiB
Python
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“.")
|