Files
property-match/src/domain/teamAgent.ts
T
Benjamin SutterandClaude Opus 5 30daa77937 feat(livia): Runde 10 Prompt 2 — eine Leadliste, dynamische Quellen, zeitliche Relevanz
Livia arbeitet nicht mehr neben dem Leadbestand, sondern in ihn hinein.

- Systemzugänge tragen Titel, Beschreibung und Adresse; die Liste zeigt je
  Eintrag nur noch Aktiv/Inaktiv und Lesen/Schreiben — kein Berechtigungsbalken,
  kein Auge-Icon, keine Lese-/Schreibgruppen
- Die Quellenregistry ist dynamisch: Der Lauf liest genau die aktiven
  Lesezugänge mit Adresse aus Livias Personalblatt. Für die drei gepflegten
  Quellen greift weiterhin ihr eigener Leser, für alles andere ein allgemeiner —
  eine Übersichtsseite, bis zu zehn Unterseiten, kein Spider. Freie Adressen
  werden auf http/https geprüft und gegen interne Netze gesperrt
- Manuell abgelegte PDF- und Word-Dokumente fliessen in denselben Lauf. Text
  wird beim Ablegen nativ extrahiert (ZIP + Flate über DecompressionStream,
  ohne neue Abhängigkeit), Datei und Text liegen in IndexedDB
- Neuer harter Filter «Zeitliche Relevanz»: das Modell beurteilt die
  Ereigniszeit der Veränderung, nicht das Publikationsdatum. «unknown» fällt
  bewusst nicht durch — ein erfundenes Datum wäre die schlechtere Antwort
- Quellentypen und Beobachtungsraum sind Chips mit Freitext; jeder Wert lässt
  sich einzeln entfernen, auch die vorgegebenen
- Info-Knopf bei «Zeitliche Relevanz» und «Mindestrelevanz» erklärt, wie der
  Wert entsteht — ohne erfundene Prozentgewichte
- Gefundene Leads gehen in den zentralen Signalbestand statt in eine zweite
  Ergebnisliste; die Herkunft steht am Signal (origin: RESEARCH_RUN)
- Der Lauf erzeugt einen echten Protokolleintrag mit den Zahlen des Laufs
- Lead-Detail: zeitliche Einordnung mit Fundstelle, semantisches Suchprofil bei
  Chancen mit «Matching starten», und bei Risiko ausdrücklich «Kein internes
  Objekt eindeutig zugeordnet» statt einer beliebigen Mock-Immobilie
- Aufgabenliste nach §11 bereinigt, «Unsichere Signale in Review Queue legen»
  von Nora übernommen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:42:59 +02:00

294 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Property On — «Teamübersicht»: digitale Mitarbeiter.
*
* Namensgebung: Der Entitätstyp heisst `TeamAgent`, nicht `Agent`. Im Repo existiert
* bereits `PowerOn` (AI-Backend-Proxy) und Claude-Code legt Worktrees unter `agent-*`
* ab — ein nackter Typ `Agent` wäre in Suchen praktisch nicht auffindbar. Alle
* Satellitentypen tragen deshalb das Präfix `Agent*`; generische Namen wie `Status`,
* `Priority` oder `Channel` sind tabu, weil `src/domain/index.ts` per `export *`
* bündelt und dort bereits `ReviewStatus`, `SourceType`, `RiskLevel` u. a. liegen.
*
* Fachliche Wahrheit ist der Agenten-Katalog: Rollen, Personalnummern und
* Zuständigkeiten der fünf Kernagenten werden nicht verändert.
*/
// ── Status & Autonomie ────────────────────────────────────────────────────────
export const AgentStatus = {
ACTIVE: 'ACTIVE',
BUILDING: 'BUILDING',
PAUSED: 'PAUSED',
} as const
export type AgentStatus = typeof AgentStatus[keyof typeof AgentStatus]
export const AgentAutonomyLevel = {
AUTONOMOUS: 'AUTONOMOUS',
APPROVAL_REQUIRED: 'APPROVAL_REQUIRED',
PROPOSAL_ONLY: 'PROPOSAL_ONLY',
} as const
export type AgentAutonomyLevel = typeof AgentAutonomyLevel[keyof typeof AgentAutonomyLevel]
// ── Kanäle ────────────────────────────────────────────────────────────────────
export const AgentChannelType = {
EMAIL: 'EMAIL',
TEAMS: 'TEAMS',
WHATSAPP: 'WHATSAPP',
WORKSPACE_CHAT: 'WORKSPACE_CHAT',
CALENDAR: 'CALENDAR',
WEB_FORM: 'WEB_FORM',
PORTAL_LEAD_MAIL: 'PORTAL_LEAD_MAIL',
PHONE_VOICE: 'PHONE_VOICE',
PHONE_NOTE: 'PHONE_NOTE',
AUDIO_OUTPUT: 'AUDIO_OUTPUT',
STRUCTURED_MAIL_FALLBACK: 'STRUCTURED_MAIL_FALLBACK',
} as const
export type AgentChannelType = typeof AgentChannelType[keyof typeof AgentChannelType]
export const AgentChannelDirection = {
INBOUND: 'INBOUND',
OUTBOUND: 'OUTBOUND',
BOTH: 'BOTH',
} as const
export type AgentChannelDirection = typeof AgentChannelDirection[keyof typeof AgentChannelDirection]
export const AgentConnectionStatus = {
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
ROADMAP: 'ROADMAP',
} as const
export type AgentConnectionStatus = typeof AgentConnectionStatus[keyof typeof AgentConnectionStatus]
/** Simulierte Kanalkonfiguration — es werden nie echte Zugangsdaten erfasst. */
export interface AgentChannelConfig {
displayName: string
senderAddress?: string
inboxAddress?: string
defaultRecipients: string[]
autoReplyEnabled: boolean
}
export interface AgentChannel {
id: string
type: AgentChannelType
direction: AgentChannelDirection
description: string
status: AgentConnectionStatus
enabled: boolean
/** Kennzeichnet im Katalog als «optional» geführte Kanäle. */
optional?: boolean
config: AgentChannelConfig
}
// ── Systeme ───────────────────────────────────────────────────────────────────
export const AgentSystemType = {
M365_EXCHANGE: 'M365_EXCHANGE',
OUTLOOK: 'OUTLOOK',
SHAREPOINT: 'SHAREPOINT',
ONEDRIVE: 'ONEDRIVE',
FILESHARE: 'FILESHARE',
DMS: 'DMS',
CALENDAR: 'CALENDAR',
IMMOTOP2: 'IMMOTOP2',
ABACUS_ABAIMMO: 'ABACUS_ABAIMMO',
RIMO_R5: 'RIMO_R5',
GARAIO_REM: 'GARAIO_REM',
CRM: 'CRM',
ZEFIX: 'ZEFIX',
SHAB: 'SHAB',
PUBLIC_WEB: 'PUBLIC_WEB',
PORTAL_LEAD_INBOX: 'PORTAL_LEAD_INBOX',
WORKSPACE: 'WORKSPACE',
} as const
export type AgentSystemType = typeof AgentSystemType[keyof typeof AgentSystemType]
export const AgentAccessLevel = {
READ: 'READ',
WRITE: 'WRITE',
READ_WRITE: 'READ_WRITE',
} as const
export type AgentAccessLevel = typeof AgentAccessLevel[keyof typeof AgentAccessLevel]
export interface AgentSystem {
id: string
type: AgentSystemType
access: AgentAccessLevel
status: AgentConnectionStatus
/**
* Ob der Mitarbeiter diesen Zugang derzeit nutzt. Optional, weil der Katalog
* die Zugänge ohne Schalter führt — fehlt das Feld, gilt der Zugang als aktiv.
*/
enabled?: boolean
/**
* Eigener Titel der Quelle (Runde 10, §2.1). Der Katalog benennt seine
* Zugänge über `type`; selbst angebundene Quellen haben keinen passenden
* Typ und tragen ihren Namen deshalb hier. Fehlt der Titel, gilt das Label
* des Typs — so bleiben die Katalogeinträge unverändert.
*/
title?: string
/**
* Adresse der Quelle. Bei einer aktiven Lesequelle ist das die Seite, die
* Livias Recherchelauf tatsächlich abruft — der angezeigte Link und der
* gelesene Link sind damit derselbe Wert.
*/
url?: string
/** Wofür der Agent dieses System nutzt — in Alltagssprache, ohne KI-Jargon. */
usage: string
/** Klartext zur Berechtigung, z. B. «Schreibt erst nach Freigabe». */
permissionNote: string
}
// ── Aufgaben ──────────────────────────────────────────────────────────────────
export interface AgentTask {
id: string
title: string
description: string
enabled: boolean
/** Zeitplan oder auslösendes Ereignis, z. B. «täglich 06:00» oder «bei Eingang». */
schedule: string
requiresApproval: boolean
dependsOnChannel?: AgentChannelType
dependsOnSystem?: AgentSystemType
}
// ── Einstellungen ─────────────────────────────────────────────────────────────
export const AgentSettingKind = {
TEXT: 'TEXT',
NUMBER: 'NUMBER',
SELECT: 'SELECT',
MULTI_SELECT: 'MULTI_SELECT',
BOOLEAN: 'BOOLEAN',
TIME: 'TIME',
/**
* Mehrfachauswahl als Chips, jeder einzeln entfernbar (Runde 10, §§2.7–2.9).
*
* Der Unterschied zu `MULTI_SELECT` ist nicht bloss die Darstellung: Ein
* Auswahlfeld kann nur anbieten, was jemand vorher als Option hinterlegt
* hat. Quellentypen und Beobachtungsräume sind aber genau das, was sich von
* Haus zu Haus unterscheidet — sie brauchen ein Feld, in das man schreiben
* kann, was man meint.
*/
CHIPS: 'CHIPS',
/** Abgelegte Dokumente als Datenbasis der Recherche (Runde 10, §2.6). */
DOCUMENTS: 'DOCUMENTS',
} as const
export type AgentSettingKind = typeof AgentSettingKind[keyof typeof AgentSettingKind]
export const AgentSettingGroup = {
GENERAL: 'GENERAL',
APPROVAL: 'APPROVAL',
DELIVERY: 'DELIVERY',
SPECIFIC: 'SPECIFIC',
} as const
export type AgentSettingGroup = typeof AgentSettingGroup[keyof typeof AgentSettingGroup]
export interface AgentSettingOption {
value: string
label: string
}
export interface AgentSetting {
id: string
label: string
description?: string
kind: AgentSettingKind
group: AgentSettingGroup
value: string | number | boolean | string[]
options?: AgentSettingOption[]
unit?: string
min?: number
max?: number
/** Fachlich nicht abschaltbar — z. B. Sinas Quellenzwang. */
locked?: boolean
lockedReason?: string
/**
* Erklärt auf Knopfdruck, wie ein Wert zustande kommt (Runde 10, §2.10).
*
* Gehört ans Datenmodell und nicht in die Komponente: Was ein Schwellenwert
* bedeutet, weiss das Personalblatt, nicht das Eingabefeld. Und ein Text,
* der eine Berechnung beschreibt, muss neben der Berechnung stehen, damit
* beide zusammen geändert werden.
*/
infoText?: string
/**
* Nur bei `CHIPS`: ob eigene Werte erfasst werden dürfen. Bei einer
* fachlich abgeschlossenen Liste — etwa den drei Zeitfenstern — bleibt es
* aus, sonst entstünde ein vierter Wert, den niemand auswerten kann.
*/
allowCustom?: boolean
}
// ── Kennzahlen & Profil ───────────────────────────────────────────────────────
export interface AgentMetric {
id: string
label: string
value: string
hint?: string
}
/** Informationsbox unter dem Kopfbereich des Personalblatts (§7.6). */
export interface AgentProfile {
purpose: string
input: string[]
coreFlow: string[]
output: string[]
}
// ── Entität ───────────────────────────────────────────────────────────────────
/**
* Farbton des Avatars. Bewusst ein Token-Schlüssel statt eines Hex-Werts —
* `npm run check:tokens` liegt über dem Schwellwert und darf nicht wachsen.
*/
export const AgentAvatarTone = {
BRAND: 'BRAND',
SUCCESS: 'SUCCESS',
INFO: 'INFO',
SIGNAL: 'SIGNAL',
WARNING: 'WARNING',
GOLD: 'GOLD',
SLATE: 'SLATE',
} as const
export type AgentAvatarTone = typeof AgentAvatarTone[keyof typeof AgentAvatarTone]
export interface TeamAgent {
id: string
name: string
role: string
personnelNumber: string
department: string
email: string
avatarTone: AgentAvatarTone
status: AgentStatus
autonomyLevel: AgentAutonomyLevel
/** Ein Satz für Karte und Listeneintrag. */
shortDescription: string
/** Freitext zur Autonomie, wo der Katalog differenziert («autonom beim Erinnern, Freigabe bei Eskalationen»). */
autonomyNote: string
profile: AgentProfile
responsibilities: string[]
tasks: AgentTask[]
channels: AgentChannel[]
systems: AgentSystem[]
settings: AgentSetting[]
metrics: AgentMetric[]
/** Kurzkennzahl für die Agentenkarte auf der Teamübersicht. */
headlineMetricId: string
lastRun?: string
}
// ── Schreib-Inputs ────────────────────────────────────────────────────────────
export interface UpdateTeamAgentInput {
status?: AgentStatus
tasks?: AgentTask[]
channels?: AgentChannel[]
/** Systeme sind seit Runde 10 (§2) genauso bearbeitbar wie Aufgaben und Kanäle. */
systems?: AgentSystem[]
settings?: AgentSetting[]
}