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

215 lines
12 KiB
Python

"""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"]),
"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}/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