feat: extend skills with dynamic resolution and RBAC, add API docs
Some checks failed
Deploy to VPS / deploy (push) Failing after 49s

- Add resolveAnalysisMode() to modes/index.ts for dynamic skill lookup
- Extend structured-analysis.ts to use resolveAnalysisMode (supports custom skill prompts and skillId persistence)
- Update structured analyses route to use resolveAnalysisMode instead of hardcoded VALID_MODES set
- Add skills:read/create/edit/delete RBAC permissions to rbac.ts
- Add docs/API_GUIDE.md with full endpoint reference

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO Agent
2026-04-15 08:22:33 +00:00
parent da2a81f081
commit 41fcc5be42
5 changed files with 710 additions and 14 deletions

633
docs/API_GUIDE.md Normal file
View File

@@ -0,0 +1,633 @@
# StageAI API Usage Guide
This guide covers all API endpoints with practical examples for text and office document sources.
## Base URL
```
http://localhost:3000/api
```
---
## 1. Authentication
### Register a New Tenant
Creates a new tenant (law firm) and its first admin user.
```bash
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "Dr. Müller",
"email": "mueller@kanzlei.de",
"password": "securepassword123",
"tenantName": "Kanzlei Müller"
}'
```
**Response (201):**
```json
{
"user": {
"id": "uuid",
"email": "mueller@kanzlei.de",
"name": "Dr. Müller",
"role": "admin"
},
"tenant": {
"id": "uuid",
"name": "Kanzlei Müller",
"slug": "kanzlei-mueller"
}
}
```
### Sign In (Get Session Cookie)
Authentication uses NextAuth.js with the Credentials provider. Sign in to get a session cookie:
```bash
curl -X POST http://localhost:3000/api/auth/callback/credentials \
-H "Content-Type: application/json" \
-d '{"email": "mueller@kanzlei.de", "password": "securepassword123"}' \
-c cookies.txt
```
Use `-b cookies.txt` on subsequent requests to send the session cookie.
### Session Info
```bash
curl http://localhost:3000/api/auth/session -b cookies.txt
```
Returns the current user session including `tenantId`, `userId`, `role`, `email`, and `name`.
> **Note:** Sessions expire after 8 hours. All protected endpoints require a valid session cookie. Tenant isolation is enforced at the database level via Row-Level Security.
---
## 2. Cases (Fälle)
### Create a Case
```bash
curl -X POST http://localhost:3000/api/cases \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"caseNumber": "2024-BSchG-001",
"title": "Nichtverlängerung Solist",
"clientName": "Max Mustermann",
"opposingParty": "Staatstheater Berlin",
"venue": "Bühnenschiedsgericht Berlin",
"status": "active",
"domains": ["nv_buehne", "arbeitsrecht"]
}'
```
**Response (201):**
```json
{
"id": "uuid",
"caseNumber": "2024-BSchG-001",
"title": "Nichtverlängerung Solist",
"clientName": "Max Mustermann",
"status": "active",
"createdAt": "2024-01-15T10:00:00Z"
}
```
### List Cases
```bash
# All cases
curl http://localhost:3000/api/cases -b cookies.txt
# Search + filter
curl "http://localhost:3000/api/cases?q=Mustermann&status=active&limit=10&offset=0" \
-b cookies.txt
```
### Get Case Details
```bash
curl http://localhost:3000/api/cases/{caseId} -b cookies.txt
```
Returns the case along with related analyses and proceedings.
### Update / Delete a Case
```bash
# Update
curl -X PATCH http://localhost:3000/api/cases/{caseId} \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"status": "closed"}'
# Delete
curl -X DELETE http://localhost:3000/api/cases/{caseId} -b cookies.txt
```
---
## 3. Document Upload (Text & Office Sources)
StageAI supports **PDF** and **DOCX** files (max 10 MB). Text is extracted automatically after upload.
### Upload a Generic Document
```bash
curl -X POST http://localhost:3000/api/documents \
-b cookies.txt \
-F "file=@/path/to/urteil.pdf" \
-F "category=entscheidung" \
-F "sourceScope=global"
```
**Parameters:**
| Field | Required | Values |
|-------|----------|--------|
| `file` | Yes | PDF or DOCX file |
| `category` | Yes | `entscheidung`, `norm`, `falldokument`, `sonstiges` |
| `sourceScope` | No | `case` (private to a case) or `global` |
| `caseId` | No | Link to a specific case |
| `decisionId` | No | Link to a specific decision |
| `normInstrumentId` | No | Link to a norm instrument |
**Response (201):**
```json
{
"id": "uuid",
"filename": "urteil.pdf",
"mimeType": "application/pdf",
"category": "entscheidung",
"status": "uploaded",
"createdAt": "2024-01-15T10:30:00Z"
}
```
After upload, text extraction runs asynchronously. Status progresses: `uploaded` -> `extracting` -> `extracted` (or `failed`).
### List Documents
```bash
# All documents
curl "http://localhost:3000/api/documents" -b cookies.txt
# Filter by category and scope
curl "http://localhost:3000/api/documents?category=entscheidung&sourceScope=global&limit=20&offset=0" \
-b cookies.txt
```
---
## 4. Contract Analysis (Vertragsanalyse)
Upload employment contracts (NV Bühne) for AI-powered clause analysis.
### Upload a Contract
```bash
curl -X POST http://localhost:3000/api/contracts \
-b cookies.txt \
-F "file=@/path/to/arbeitsvertrag.pdf" \
-F "caseId=uuid-of-case"
```
**Response (201):**
```json
{
"id": "uuid",
"filename": "arbeitsvertrag.pdf",
"mimeType": "application/pdf",
"status": "uploaded",
"caseId": "uuid-of-case"
}
```
### Trigger Clause Analysis
After uploading, trigger the AI analysis:
```bash
curl -X POST http://localhost:3000/api/contracts/{contractId}/analyze \
-b cookies.txt
```
This extracts text from the document, identifies contract clauses, and compares them against NV Bühne standard clauses. The status progresses: `uploaded` -> `extracting` -> `extracted` -> `analyzing` -> `completed`.
### Get Contract with Analysis Results
```bash
curl http://localhost:3000/api/contracts/{contractId} \
-H "x-tenant-id: your-tenant-id" \
-b cookies.txt
```
**Response (200):**
```json
{
"document": {
"id": "uuid",
"filename": "arbeitsvertrag.pdf",
"status": "completed"
},
"clauses": [
{
"id": "uuid",
"category": "Vergütung",
"extractedText": "Die monatliche Gage beträgt...",
"rating": "standard",
"analysis": "Entspricht der Gagenklasse III NV Bühne.",
"riskScore": 5,
"deviations": []
},
{
"id": "uuid",
"category": "Nichtverlängerung",
"extractedText": "Der Vertrag kann mit einer Frist von...",
"rating": "kritisch",
"analysis": "Die Frist weicht von § 69 NV Bühne ab.",
"riskScore": 85,
"deviations": ["Frist kürzer als tariflich vorgesehen"]
}
]
}
```
**Clause Categories:** Vertragsparteien, Vertragsdauer, Nichtverlängerung, Vergütung, Arbeitszeit, Proben, Gastspiele, Urlaub, Krankheit, Kündigung, Nebentätigkeit, Geheimhaltung, Sonstiges
**Ratings:** `standard` (conforms to NV Bühne), `abweichend` (deviates), `kritisch` (critical deviation), `unbekannt` (unclassifiable)
### List Contracts
```bash
curl "http://localhost:3000/api/contracts?limit=20&offset=0" -b cookies.txt
```
---
## 5. Legal Norms (Normen)
### Create a Norm Instrument
```bash
curl -X POST http://localhost:3000/api/norms \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"type": "tarifvertrag",
"sourceRank": "tarifvertrag",
"abbreviation": "NV Bühne",
"fullTitle": "Normalvertrag Bühne",
"enactedAt": "2023-01-01",
"issuingBody": "GDBA / VdO / DBV"
}'
```
### Import Norm Provisions (Bulk)
```bash
curl -X POST http://localhost:3000/api/norms/import \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"instrumentId": "uuid-of-instrument",
"provisions": [
{
"paragraph": "§ 1",
"title": "Geltungsbereich",
"body": "Dieser Normalvertrag gilt für alle Bühnenmitglieder...",
"validFrom": "2023-01-01",
"domains": ["nv_buehne"]
},
{
"paragraph": "§ 69",
"title": "Nichtverlängerungsmitteilung",
"body": "Die Nichtverlängerungsmitteilung muss bis zum 31. Oktober...",
"validFrom": "2023-01-01",
"domains": ["nv_buehne", "nichtverlängerung"]
}
]
}'
```
### Query Norms (with Temporal Versioning)
Retrieve all paragraphs of an instrument valid on a specific date:
```bash
# Norms valid today (default)
curl http://localhost:3000/api/norms/{instrumentId} -b cookies.txt
# Norms valid on a specific date (Stichtag)
curl "http://localhost:3000/api/norms/{instrumentId}?date=2024-06-15" -b cookies.txt
```
**Response (200):**
```json
{
"instrument": {
"id": "uuid",
"abbreviation": "NV Bühne",
"fullTitle": "Normalvertrag Bühne",
"type": "tarifvertrag"
},
"provisions": [
{
"id": "uuid",
"paragraph": "§ 1",
"title": "Geltungsbereich",
"body": "Dieser Normalvertrag gilt für alle Bühnenmitglieder...",
"validFrom": "2023-01-01",
"validTo": null
}
]
}
```
---
## 6. Decisions (Entscheidungen)
### Create a Decision
```bash
curl -X POST http://localhost:3000/api/decisions \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"type": "schiedsspruch",
"caseReference": "BSchG Berlin 3/2024",
"decisionDate": "2024-03-15",
"court": "Bühnenschiedsgericht Berlin",
"headnote": "Zur Wirksamkeit einer Nichtverlängerungsmitteilung...",
"tenor": "Der Schiedsspruch wird aufgehoben...",
"facts": "Der Kläger ist seit 2018 als Solist...",
"reasoning": "Die Nichtverlängerungsmitteilung ist unwirksam, weil...",
"domains": ["nv_buehne", "nichtverlängerung"],
"keywords": ["Nichtverlängerung", "Solist", "Fristversäumnis"]
}'
```
### Full-Text Search for Decisions
Uses PostgreSQL full-text search with German language support:
```bash
# Search by keyword
curl "http://localhost:3000/api/decisions?q=Vergütung" -b cookies.txt
# Combined filters
curl "http://localhost:3000/api/decisions?q=Nichtverlängerung&court=Bühnenschiedsgericht&type=schiedsspruch&dateFrom=2020-01-01&dateTo=2024-12-31" \
-b cookies.txt
```
### Link Norms to a Decision
```bash
curl -X POST http://localhost:3000/api/decisions/{decisionId}/norms \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"normId": "uuid-of-norm-paragraph",
"applicationType": "angewendet",
"passage": "Rn. 15-18"
}'
```
**Application types:** `angewendet` (applied), `zitiert` (cited), `ausgelegt` (interpreted), `verworfen` (rejected)
---
## 7. AI-Powered Legal Analysis (Analysen)
### Create a Streaming Analysis
```bash
curl -X POST http://localhost:3000/api/analyses \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"mode": "gutachten",
"title": "Wirksamkeit der Nichtverlängerung",
"query": "Ist die Nichtverlängerungsmitteilung vom 15.11.2024 wirksam, wenn der Solist seit 15 Jahren am Haus beschäftigt ist?",
"normIds": ["uuid-of-§69-nv-buehne"],
"decisionIds": ["uuid-of-relevant-decision"],
"documentIds": ["uuid-of-uploaded-document"],
"stichtag": "2024-11-15",
"caseId": "uuid-of-case"
}'
```
The response streams text (the AI-generated analysis). The `X-Analysis-Id` response header contains the analysis ID for later retrieval.
**Analysis Modes:**
| Mode | Purpose | Requires |
|------|---------|----------|
| `gutachten` | Expert legal opinion | Norms |
| `entscheidung` | Decision proposal based on precedent | Decisions |
| `vergleich` | Comparative analysis | Norms or decisions |
| `risiko` | Risk assessment with probability ratings | Any sources |
### Create a Structured Analysis (JSON Output)
```bash
curl -X POST http://localhost:3000/api/analyses/structured \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"mode": "risiko",
"query": "Risikobewertung für die Kündigung eines Chormitglieds nach § 626 BGB",
"normIds": ["uuid1"],
"decisionIds": ["uuid2"]
}'
```
Returns typed JSON with structured fields depending on the mode.
### List & Retrieve Analyses
```bash
# List (metadata only, DSGVO-compliant)
curl "http://localhost:3000/api/analyses?limit=20&offset=0" -b cookies.txt
# Get single analysis with full result and sources
curl http://localhost:3000/api/analyses/{analysisId} \
-H "x-tenant-id: your-tenant-id" \
-b cookies.txt
```
---
## 8. Proceedings (Verfahren)
### Create a Proceeding
```bash
curl -X POST http://localhost:3000/api/proceedings \
-H "Content-Type: application/json" \
-d '{
"tenantId": "your-tenant-id",
"type": "bschgo_bezirk",
"caseId": "uuid-of-case",
"applicant": "Max Mustermann",
"respondent": "Staatstheater Berlin",
"subject": "Nichtverlängerung Spielzeit 2024/25"
}'
```
**Proceeding types:** `bschgo_bezirk`, `bschgo_bund`, `arbgg_erste_instanz`, `arbgg_berufung`, `arbgg_revision`
Workflow steps and initial deadlines are automatically created from templates.
### Check Deadlines
```bash
# All deadlines
curl http://localhost:3000/api/proceedings/{proceedingId}/deadlines
# Only overdue deadlines
curl "http://localhost:3000/api/proceedings/{proceedingId}/deadlines?overdue=true"
# Upcoming in next 14 days
curl "http://localhost:3000/api/proceedings/{proceedingId}/deadlines?upcoming=14"
```
### Advance Proceeding
```bash
curl -X POST http://localhost:3000/api/proceedings/{proceedingId}/advance \
-b cookies.txt
```
---
## 9. NV Bühne Utilities
These are public endpoints (no auth required) for quick calculations.
### Calculate Compensation (Gage)
```bash
# GET (simple query)
curl "http://localhost:3000/api/nv-buehne/compensation?gagenklasse=III&yearsOfService=5&spielzeit=2024/25&fachgruppe=Solo"
# POST (with tenant-specific rules)
curl -X POST http://localhost:3000/api/nv-buehne/compensation \
-H "Content-Type: application/json" \
-d '{
"gagenklasse": "III",
"yearsOfService": 5,
"spielzeit": "2024/25",
"fachgruppe": "Solo",
"tenantId": "uuid",
"fachgruppeId": "uuid"
}'
```
### Check Non-Renewal Deadline (Nichtverlängerungsfrist)
```bash
curl "http://localhost:3000/api/nv-buehne/deadline-check?yearsOfService=15&isOver55=false&spielzeit=2024/25&fachgruppe=Solo&referenceDate=2024-10-31"
```
### Get Current Spielzeit (Season)
```bash
curl "http://localhost:3000/api/nv-buehne/spielzeit"
# or for a specific date:
curl "http://localhost:3000/api/nv-buehne/spielzeit?date=2024-09-01"
```
---
## 10. Settings
### Manage AI Provider API Keys
```bash
# List keys (returns hints only, never full keys)
curl http://localhost:3000/api/settings/api-keys -b cookies.txt
# Add an API key
curl -X POST http://localhost:3000/api/settings/api-keys \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"provider": "anthropic",
"apiKey": "sk-ant-...",
"label": "Production Key"
}'
```
Keys are encrypted at rest with AES-256-GCM.
---
## Pagination
All list endpoints support pagination:
```
?limit=20&offset=0
```
**Response format:**
```json
{
"data": [...],
"pagination": {
"total": 100,
"limit": 20,
"offset": 0,
"hasMore": true
}
}
```
## Error Handling
All errors return JSON:
```json
{
"error": "Human-readable error message"
}
```
| Status | Meaning |
|--------|---------|
| 400 | Validation error (missing/invalid fields) |
| 401 | Not authenticated (missing or expired session) |
| 403 | Permission denied (role lacks required permission) |
| 404 | Resource not found |
| 409 | Conflict (duplicate entry) |
| 413 | File too large (max 10 MB) |
## User Roles & Permissions
| Role | Permissions |
|------|------------|
| `admin` | Full access, manage settings & users |
| `attorney` | Create/edit cases, analyses, norms, decisions |
| `paralegal` | Read access + limited write |
| `viewer` | Read-only |
## Typical Workflow
1. **Register** a tenant and admin user
2. **Configure** AI provider API key in Settings
3. **Create norms** (import NV Bühne provisions)
4. **Upload decisions** (create + link applicable norms)
5. **Create a case** for a client
6. **Upload documents** (court rulings, contracts as PDF/DOCX)
7. **Upload & analyze a contract** to identify clause deviations
8. **Run AI analyses** referencing norms, decisions, and documents
9. **Create proceedings** to track deadlines and workflow steps

View File

@@ -3,11 +3,9 @@
import { type NextRequest } from 'next/server';
import { runStructuredAnalysis } from '@/lib/ai/structured-analysis';
import { AnalyseMode } from '@/types';
import { resolveAnalysisMode } from '@/lib/ai/modes';
import { requirePermission } from '@/lib/auth/rbac';
const VALID_MODES = new Set(Object.values(AnalyseMode));
export async function POST(request: NextRequest) {
const auth = await requirePermission('analyses:create');
if ('response' in auth) return auth.response;
@@ -25,9 +23,17 @@ export async function POST(request: NextRequest) {
additionalContext,
} = body;
if (!mode || !VALID_MODES.has(mode)) {
if (!mode || typeof mode !== 'string') {
return Response.json(
{ error: `Invalid mode. Must be one of: ${[...VALID_MODES].join(', ')}` },
{ error: 'mode is required' },
{ status: 400 },
);
}
const modeConfig = await resolveAnalysisMode(mode, ctx.tenantId);
if (!modeConfig) {
return Response.json(
{ error: `Unbekannter Analysemodus: ${mode}` },
{ status: 400 },
);
}

View File

@@ -2,9 +2,12 @@
// Each mode defines a specific legal analysis workflow
import { AnalyseMode } from '@/types';
import { withTenantDb } from '@/lib/db';
import { skills } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
export interface AnalysisModeConfig {
mode: AnalyseMode;
mode: string;
systemPromptKey: string;
requiresNorms: boolean;
requiresDecisions: boolean;
@@ -12,6 +15,10 @@ export interface AnalysisModeConfig {
label: string;
/** Short description */
description: string;
/** Custom system prompt (for skill-based modes) */
customSystemPrompt?: string;
/** Skill ID if resolved from skills table */
skillId?: string;
}
export const ANALYSIS_MODES: Record<AnalyseMode, AnalysisModeConfig> = {
@@ -48,3 +55,43 @@ export const ANALYSIS_MODES: Record<AnalyseMode, AnalysisModeConfig> = {
description: 'Umfassende Risikoanalyse mit Eintrittswahrscheinlichkeiten und Minderungsstrategien',
},
};
const STANDARD_MODES = new Set<string>(Object.values(AnalyseMode));
/**
* Resolve an analysis mode config — checks hardcoded standard modes first,
* then falls back to the skills table for custom tenant-defined modes.
* Returns null if the mode/slug does not exist.
*/
export async function resolveAnalysisMode(
mode: string,
tenantId: string,
): Promise<AnalysisModeConfig | null> {
// Standard hardcoded mode
if (STANDARD_MODES.has(mode)) {
return ANALYSIS_MODES[mode as AnalyseMode];
}
// Look up custom skill by slug
const skill = await withTenantDb(tenantId, async (tdb) => {
const [row] = await tdb
.select()
.from(skills)
.where(and(eq(skills.slug, mode), eq(skills.isActive, true)))
.limit(1);
return row ?? null;
});
if (!skill) return null;
return {
mode: skill.slug,
systemPromptKey: skill.slug,
requiresNorms: skill.requiresNorms ?? false,
requiresDecisions: skill.requiresDecisions ?? false,
label: skill.name,
description: skill.description ?? '',
customSystemPrompt: skill.systemPrompt,
skillId: skill.id,
};
}

View File

@@ -4,9 +4,8 @@
import { generateText } from 'ai';
import { getModelForTenant } from './providers';
import { SYSTEM_PROMPTS, buildContextBlock, type AnalysisModeKey } from './prompts';
import { ANALYSIS_MODES } from './modes';
import { ANALYSIS_MODES, resolveAnalysisMode } from './modes';
import { STRUCTURED_OUTPUT_INSTRUCTION, type StructuredAnalysisOutput } from './structured-output';
import { AnalyseMode } from '@/types';
import { db } from '@/lib/db';
import { norms, normInstruments, decisions, analyses } from '@/lib/db/schema';
import { eq, and, lte, or, isNull, gte, inArray } from 'drizzle-orm';
@@ -15,7 +14,7 @@ interface StructuredAnalysisInput {
tenantId: string;
userId: string;
caseId?: string;
mode: AnalyseMode;
mode: string;
title: string;
query: string;
normIds?: string[];
@@ -108,8 +107,10 @@ export async function runStructuredAnalysis(
result: StructuredAnalysisOutput;
sources: { normIds: string[]; decisionIds: string[] };
}> {
const modeConfig = ANALYSIS_MODES[input.mode];
const systemPromptKey = modeConfig.systemPromptKey as AnalysisModeKey;
const modeConfig = await resolveAnalysisMode(input.mode, input.tenantId);
if (!modeConfig) {
throw new Error(`Unbekannter Analysemodus: ${input.mode}`);
}
const [normContext, decisionContext] = await Promise.all([
modeConfig.requiresNorms
@@ -132,8 +133,10 @@ export async function runStructuredAnalysis(
const userMessage = messageParts.join('\n\n---\n\n');
// Add structured output instruction to system prompt
const systemPrompt = SYSTEM_PROMPTS[systemPromptKey] + STRUCTURED_OUTPUT_INSTRUCTION;
// Use custom system prompt for skill-based modes, standard prompts for built-in modes
const basePrompt = modeConfig.customSystemPrompt
?? SYSTEM_PROMPTS[modeConfig.systemPromptKey as AnalysisModeKey];
const systemPrompt = basePrompt + STRUCTURED_OUTPUT_INSTRUCTION;
const { model, provider, modelId } = await getModelForTenant(input.tenantId);
@@ -145,6 +148,7 @@ export async function runStructuredAnalysis(
userId: input.userId,
caseId: input.caseId ?? null,
mode: input.mode,
skillId: modeConfig.skillId ?? null,
status: 'in_progress',
title: input.title,
query: input.query,
@@ -172,7 +176,7 @@ export async function runStructuredAnalysis(
const jsonMatch = result.text.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error('No JSON object found');
structured = JSON.parse(jsonMatch[0]);
structured.mode = input.mode;
(structured as any).mode = input.mode;
} catch {
// Fallback: store raw text, mark as non-structured
await db

View File

@@ -31,6 +31,12 @@ const PERMISSIONS: Record<string, readonly Role[]> = {
'decisions:write': ['admin', 'attorney'],
'decisions:read': ['admin', 'attorney', 'paralegal', 'viewer'],
// Skills
'skills:read': ['admin', 'attorney', 'paralegal', 'viewer'],
'skills:create': ['admin'],
'skills:edit': ['admin'],
'skills:delete': ['admin'],
// Settings
'settings:manage': ['admin'],