"""Actor creation and directory helpers — User and Actor remain separate concepts.""" from __future__ import annotations from typing import Any, Literal, Optional from psycopg2.extras import RealDictCursor from db import get_connection ActorType = Literal["human", "agent", "working_group", "external_system"] ACTOR_TYPES = frozenset({"human", "agent", "working_group", "external_system"}) def _serialize_actor(row: dict[str, Any], *, include_user_id: bool = False) -> dict[str, Any]: result = { "id": str(row["id"]), "name": row["name"], "actor_type": row["actor_type"], "is_active": bool(row["is_active"]), } if include_user_id and row.get("user_id"): result["user_id"] = str(row["user_id"]) return result def create_actor( *, tenant_id: str, actor_type: ActorType, name: str, user_id: Optional[str] = None, ) -> dict[str, Any]: if actor_type == "human" and not user_id: raise ValueError("Human actors require user_id") if actor_type != "human" and user_id: raise ValueError("Non-human actors must not have user_id") conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ INSERT INTO actors (tenant_id, actor_type, name, user_id) VALUES (%s, %s, %s, %s) RETURNING id, tenant_id, actor_type, name, user_id, is_active, created_at """, (tenant_id, actor_type, name, user_id), ) row = dict(cur.fetchone()) conn.commit() for key in ("id", "tenant_id", "user_id"): if row.get(key): row[key] = str(row[key]) return row finally: conn.close() def get_human_actor(tenant_id: str, user_id: str) -> Optional[dict[str, Any]]: conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, tenant_id, actor_type, name, user_id, is_active, created_at FROM actors WHERE tenant_id = %s AND user_id = %s AND actor_type = 'human' """, (tenant_id, user_id), ) row = cur.fetchone() if not row: return None result = dict(row) for key in ("id", "tenant_id", "user_id"): if result.get(key): result[key] = str(result[key]) return result finally: conn.close() def list_actors( *, tenant_id: str, actor_type: Optional[str] = None, include_inactive: bool = False, q: Optional[str] = None, ) -> list[dict[str, Any]]: if actor_type is not None and actor_type not in ACTOR_TYPES: raise ValueError(f"Ungültiger actor_type: {actor_type}") conditions = ["tenant_id = %s"] params: list[Any] = [tenant_id] if not include_inactive: conditions.append("is_active = TRUE") if actor_type: conditions.append("actor_type = %s") params.append(actor_type) if q: conditions.append("name ILIKE %s") params.append(f"%{q.strip()}%") where = " AND ".join(conditions) conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( f""" SELECT id, actor_type, name, user_id, is_active FROM actors WHERE {where} ORDER BY CASE actor_type WHEN 'human' THEN 0 WHEN 'working_group' THEN 1 WHEN 'agent' THEN 2 ELSE 3 END, name """, params, ) return [_serialize_actor(dict(row)) for row in cur.fetchall()] finally: conn.close() def get_actor(*, tenant_id: str, actor_id: str) -> Optional[dict[str, Any]]: conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, actor_type, name, user_id, is_active FROM actors WHERE id = %s AND tenant_id = %s """, (actor_id, tenant_id), ) row = cur.fetchone() if not row: return None return _serialize_actor(dict(row)) finally: conn.close()