"""Workspace-Übersicht, Anlegen, Detailseite und Mitglieder.""" from typing import Annotated from urllib.parse import urlencode from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, Response from app import web from app.ai import service as ai from app.chat import service as chats from app.core.errors import Forbidden, Invalid from app.data_sources import service as sources from app.documents import service as documents from app.hugin import routes as hugin_routes from app.hugin import service as hugin from app.hugin import tools as hugin_tools from app.identity.models import Team from app.mcp import databases from app.mcp import service as mcp from app.tools import service as tools from app.uploads import service as uploads from app.workspaces import removal from app.workspaces import service as workspaces # Reiter der Workspace-Einstellungen SETTINGS_TABS = {"general": "Allgemein", "members": "Mitglieder", "tools": "Tools und Regeln", "mcp": "MCP-Server", "hugin": "Hugin"} def router() -> APIRouter: r = APIRouter() @r.get("/") def home(principal: web.User) -> Response: return web.redirect("/workspaces") @r.get("/workspaces") def overview(request: Request, principal: web.User, deleted: str = "", memory: str = "", trashed: str = "") -> HTMLResponse: with web.db(request).session() as session: memberships = workspaces.list_for(session, principal) teams = {t.id: t.name for t in workspaces.teams_for(session, principal)} context = {"memberships": memberships, "teams": teams, "can_create": bool(principal.team_ids), "stats": workspaces.stats(session, [m.workspace.id for m in memberships]), "trash": removal.trashed(session, principal, web.settings(request)), "trash_days": web.settings(request).workspace_trash_days, "deleted": deleted[:200], "memory": memory[:500], "trashed": trashed[:200]} return web.render(request, "workspaces.html", context) @r.post("/workspaces/{workspace_id}/delete") def delete(request: Request, principal: web.Checked, workspace_id: int, confirmation: Annotated[str, Form()] = "") -> Response: with web.db(request).session() as session: workspace = removal.trash(session, principal, workspace_id, confirmation) return web.redirect(f"/workspaces?{urlencode({'trashed': workspace.name})}#trash") @r.post("/workspaces/{workspace_id}/restore") def restore(request: Request, principal: web.Checked, workspace_id: int) -> Response: with web.db(request).session() as session: removal.restore(session, principal, workspace_id) return web.redirect(f"/workspaces/{workspace_id}") @r.post("/workspaces/{workspace_id}/purge") def purge(request: Request, principal: web.Checked, workspace_id: int) -> Response: with web.db(request).session() as session: removed = removal.purge(session, principal, web.settings(request), workspace_id) query = {"deleted": removed.name} if removed.memory_servers: query["memory"] = ", ".join(removed.memory_servers) return web.redirect(f"/workspaces?{urlencode(query)}") @r.get("/workspaces/new") def new(request: Request, principal: web.User) -> HTMLResponse: with web.db(request).session() as session: teams = workspaces.teams_for(session, principal) return web.render(request, "workspace_new.html", {"teams": teams, "form": {}}) @r.post("/workspaces") def create(request: Request, principal: web.Checked, team_id: Annotated[int, Form()], name: Annotated[str, Form()], description: Annotated[str, Form()] = "") -> Response: try: with web.db(request).session() as session: workspace = workspaces.create(session, principal, team_id, name, description) except Invalid as e: with web.db(request).session() as session: teams = workspaces.teams_for(session, principal) form = {"team_id": team_id, "name": name, "description": description} return web.render(request, "workspace_new.html", {"teams": teams, "form": form, "error": str(e)}, 400) return web.redirect(f"/workspaces/{workspace.id}") def base(session, principal, workspace_id: int) -> dict: membership = workspaces.get(session, principal, workspace_id) return {"membership": membership, "workspace": membership.workspace, "current_workspace": membership.workspace, "team": session.get(Team, membership.workspace.team_id)} @r.get("/workspaces/{workspace_id}") def detail(request: Request, principal: web.User, workspace_id: int, q: str = "") -> HTMLResponse: with web.db(request).session() as session: context = base(session, principal, workspace_id) context.update({ "sources": sources.list_for(session, principal, workspace_id), "chats": chats.overview(session, principal, workspace_id, q), "q": q, "ready": sources.chat_ready(session, workspace_id), "uploads": uploads.list_for(session, principal, workspace_id), "effective_model": ai.effective_model(session, context["workspace"]), "examples": workspaces.examples(context["workspace"]), "chat_only": workspaces.chat_only(context["workspace"]), "folders": documents.folders(session, principal, workspace_id), }) return web.render(request, "workspace.html", context) @r.get("/workspaces/{workspace_id}/data") def data(request: Request, principal: web.User, workspace_id: int) -> HTMLResponse: with web.db(request).session() as session: context = base(session, principal, workspace_id) context.update({ "sources": sources.list_for(session, principal, workspace_id), "uploads": uploads.list_for(session, principal, workspace_id), "upload_max_mb": web.settings(request).upload_max_mb, "storage": uploads.storage(session, web.settings(request), context["workspace"]), "ready": sources.chat_ready(session, workspace_id), }) folders = documents.folders(session, principal, workspace_id) context.update({"folders": folders, "folder_summaries": documents.summaries(session, [f.id for f in folders]), "scopes": documents.SCOPES}) return web.render(request, "workspace_data.html", context) @r.get("/workspaces/{workspace_id}/settings") def settings_page(request: Request, principal: web.User, workspace_id: int, tab: str = "general", tool: str = "", saved: str = "") -> HTMLResponse: with web.db(request).session() as session: context = base(session, principal, workspace_id) if not context["membership"].is_admin: raise Forbidden source_list = sources.list_for(session, principal, workspace_id) upload_list = uploads.list_for(session, principal, workspace_id) kinds = ({s.kind for s in source_list if s.status == "ok"} | mcp.kinds(session, workspace_id) | ({"uploads"} if any(u.status == "ok" for u in upload_list) else set()) | hugin_tools.kinds(hugin.load(session, workspace_id))) context.update({ "sources": source_list, "members": workspaces.members(session, principal, workspace_id), "models": ai.models(session, active_only=True), "effective_model": ai.effective_model(session, context["workspace"]), "tool_rounds": workspaces.tool_rounds(context["workspace"], web.settings(request)), "rounds_default": web.settings(request).max_tool_rounds, "rounds_limit": web.settings(request).max_tool_rounds_limit, "tools": tools.overview(session, context["workspace"], kinds), "mcp_servers": mcp.workspace_servers(session, workspace_id), "connections": mcp.connections(session, workspace_id), "db_drivers": databases.drivers(session), "source_rules": tools.source_policies(session, workspace_id), "memory_enabled": any(s.role == "memory" for s, _ in mcp.enabled(session, workspace_id)), "trash_days": web.settings(request).workspace_trash_days, "saved": bool(saved), "tab": tab if tab in SETTINGS_TABS else "general", "tabs": SETTINGS_TABS, "hugin": hugin_routes.tab_context(session, web.settings(request), context["workspace"]), }) # Nach dem Setzen einer Vorgabe: was jetzt für das Tool gilt context["saved_tool"] = next((t for t in context["tools"] if t.definition.name == tool), None) return web.render(request, "workspace_settings.html", context) @r.post("/workspaces/{workspace_id}/settings") def update(request: Request, principal: web.Checked, workspace_id: int, name: Annotated[str, Form()], description: Annotated[str, Form()] = "") -> Response: with web.db(request).session() as session: workspaces.update(session, principal, workspace_id, name, description) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1") @r.post("/workspaces/{workspace_id}/examples") def set_examples(request: Request, principal: web.Checked, workspace_id: int, examples: Annotated[str, Form()] = "") -> Response: with web.db(request).session() as session: workspaces.set_examples(session, principal, workspace_id, 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") def tool_rounds(request: Request, principal: web.Checked, workspace_id: int, value: Annotated[str, Form()] = "") -> Response: with web.db(request).session() as session: workspaces.set_tool_rounds(session, principal, web.settings(request), workspace_id, value) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1#answers") @r.post("/workspaces/{workspace_id}/approval-mode") def approval_mode(request: Request, principal: web.Checked, workspace_id: int, mode: Annotated[str, Form()]) -> Response: with web.db(request).session() as session: workspaces.set_approval_mode(session, principal, workspace_id, mode) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1#approvals") @r.post("/workspaces/{workspace_id}/members") def set_member(request: Request, principal: web.Checked, workspace_id: int, username: Annotated[str, Form()], role: Annotated[str, Form()]) -> Response: with web.db(request).session() as session: workspaces.set_member(session, principal, workspace_id, username, role) if web.is_htmx(request): return Response(headers={"HX-Refresh": "true"}) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=members&saved=1") @r.post("/workspaces/{workspace_id}/members/{user_id}/role") def set_role(request: Request, principal: web.Checked, workspace_id: int, user_id: int, role: Annotated[str, Form()]) -> Response: with web.db(request).session() as session: workspaces.set_role(session, principal, workspace_id, user_id, role) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=members&saved=1") @r.post("/workspaces/{workspace_id}/members/{user_id}/remove") def remove_member(request: Request, principal: web.Checked, workspace_id: int, user_id: int) -> Response: with web.db(request).session() as session: workspaces.remove_member(session, principal, workspace_id, user_id) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=members&saved=1") return r