upload
This commit is contained in:
1 parent
22b05a7a1b
commit
7058970f98
80 files changed
+6383
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# n8n-Integration
|
||||
|
||||
## Empfohlene Node-Reihenfolge
|
||||
|
||||
```text
|
||||
Schedule Trigger
|
||||
-> PostgreSQL: aktive Kunden laden
|
||||
-> Loop Over Items
|
||||
-> PostgreSQL: Monatsmetriken und Verlauf laden
|
||||
-> Code: Monatsdaten vorbereiten
|
||||
-> Code: Claude Request bauen
|
||||
-> HTTP Request: Anthropic Messages API
|
||||
-> Code: Claude JSON extrahieren
|
||||
-> Code: Render Payload bauen
|
||||
-> HTTP Request: Report Renderer /render
|
||||
-> Speicher / E-Mail / Kundenportal
|
||||
-> PostgreSQL: Reportlauf protokollieren
|
||||
```
|
||||
|
||||
## Anthropic HTTP Request
|
||||
|
||||
- Methode: `POST`
|
||||
- URL: `https://api.anthropic.com/v1/messages`
|
||||
- Header:
|
||||
- `x-api-key`: aus Credential oder Secret
|
||||
- `anthropic-version`: `2023-06-01`
|
||||
- `content-type`: `application/json`
|
||||
- Body: `={{ $json.claudeRequest }}`
|
||||
- Response: JSON
|
||||
|
||||
## Renderer HTTP Request
|
||||
|
||||
- Methode: `POST`
|
||||
- URL im gleichen Docker-Netz: `http://report-renderer:8080/render`
|
||||
- Body Content Type: JSON
|
||||
- Body: gesamtes aktuelles Item
|
||||
- Response Format: File
|
||||
- Binary Property: `data`
|
||||
|
||||
Danach kann `data` als E-Mail-Anhang, S3-Upload oder Datei verwendet werden.
|
||||
|
||||
## Datenschutz und Fehlerbehandlung
|
||||
|
||||
- Jeden Kunden im Loop als separates Item verarbeiten.
|
||||
- Kunden-ID und Berichtsmonat vor jedem Node erhalten.
|
||||
- Bei Claude- oder Renderfehlern keinen leeren Report versenden.
|
||||
- Reportlauf erst nach erfolgreicher Ablage als `completed` markieren.
|
||||
- Eindeutigen DB-Schluessel auf Kunde, Jahr und Monat setzen.
|
||||
@@ -0,0 +1,40 @@
|
||||
// n8n Code Node: "Claude Request bauen"
|
||||
// Erwartet: customer, period, metrics, trends und optional context.
|
||||
// Das Schema ist fuer die rohe Messages API vereinfacht. Die strengeren
|
||||
// Laengen- und Wertepruefungen erfolgen anschliessend im Report Renderer.
|
||||
|
||||
const input = $json;
|
||||
const schema = {"type": "object", "additionalProperties": false, "properties": {"summary": {"type": "object", "additionalProperties": false, "properties": {"headline": {"type": "string", "description": "Minimum characters: 8. Maximum characters: 110."}, "text": {"type": "string", "description": "Minimum characters: 80. Maximum characters: 900."}, "rating": {"type": "string", "enum": ["positive", "neutral", "watch"]}}, "required": ["headline", "text", "rating"]}, "developments": {"type": "array", "items": {"type": "object", "additionalProperties": false, "properties": {"text": {"type": "string", "description": "Minimum characters: 40. Maximum characters: 450."}, "impact": {"type": "string", "enum": ["positive", "neutral", "negative"]}}, "required": ["title", "text", "impact"]}, "description": "Minimum items: 2. Maximum items: 4."}, "recommendations": {"type": "array", "items": {"type": "object", "additionalProperties": false, "properties": {"priority": {"type": "integer", "description": "Minimum value: 1. Maximum value: 3."}, "category": {"type": "string", "description": "Minimum characters: 3. Maximum characters: 80."}, "reason": {"type": "string", "description": "Minimum characters: 40. Maximum characters: 450."}, "action": {"type": "string", "description": "Minimum characters: 30. Maximum characters: 450."}, "expected_effect": {"type": "string", "description": "Minimum characters: 20. Maximum characters: 300."}}, "required": ["priority", "category", "title", "reason", "action", "expected_effect"]}, "description": "Minimum items: 1. Maximum items: 3."}, "outlook": {"type": "object", "additionalProperties": false, "properties": {"headline": {"type": "string", "description": "Minimum characters: 5. Maximum characters: 100."}, "text": {"type": "string", "description": "Minimum characters: 60. Maximum characters: 700."}}, "required": ["headline", "text"]}, "data_quality_notes": {"type": "array", "items": {"type": "string", "description": "Minimum characters: 5. Maximum characters: 240."}, "description": "Maximum items: 4."}}, "required": ["summary", "developments", "recommendations", "outlook", "data_quality_notes"]};
|
||||
|
||||
const systemPrompt = `Du erstellst die redaktionellen Inhalte eines monatlichen Performance-Reports fuer Betreiber von Ferienunterkuenften.
|
||||
Nutze ausschliesslich die gelieferten Daten. Erfinde keine Zahlen oder Ursachen.
|
||||
Jede Empfehlung muss konkret und aus den Daten ableitbar sein.
|
||||
Formuliere professionell, klar und auf Deutsch. Vermeide Marketingfloskeln und Wiederholungen.
|
||||
Erzeuge 2 bis 4 Entwicklungen, 1 bis 3 Empfehlungen und hoechstens 4 Datenhinweise.
|
||||
Nutze data_quality_notes fuer fehlende oder eingeschraenkte Datengrundlagen.`;
|
||||
|
||||
const userPrompt = `Erstelle den Monatsreport fuer diese Daten:
|
||||
|
||||
${JSON.stringify({
|
||||
customer: input.customer,
|
||||
period: input.period,
|
||||
metrics: input.metrics,
|
||||
trends: input.trends,
|
||||
context: input.context ?? {}
|
||||
}, null, 2)}`;
|
||||
|
||||
return [{
|
||||
json: {
|
||||
...input,
|
||||
claudeRequest: {
|
||||
model: input.claudeModel ?? 'claude-sonnet-4-6',
|
||||
max_tokens: 3000,
|
||||
temperature: 0.2,
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
output_config: {
|
||||
format: { type: 'json_schema', schema }
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
@@ -0,0 +1,26 @@
|
||||
// n8n Code Node: "Render Payload bauen"
|
||||
// Passe die Node-Namen unten an deinen Workflow an.
|
||||
|
||||
const source = $('Monatsdaten vorbereiten').first().json;
|
||||
const claude = $('Claude JSON extrahieren').first().json;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const payload = {
|
||||
report: {
|
||||
report_version: '1.0',
|
||||
prompt_version: '1.0',
|
||||
template_version: '1.0',
|
||||
generated_at: now,
|
||||
publisher: source.publisher ?? 'Mein Reporting',
|
||||
period: source.period
|
||||
},
|
||||
customer: source.customer,
|
||||
design: source.design,
|
||||
metrics: source.metrics,
|
||||
trends: source.trends,
|
||||
content: claude.content,
|
||||
legal_note: source.legal_note ?? 'Dieser Report ist eine datenbasierte Entscheidungshilfe und keine Garantie fuer zukuenftige Ergebnisse.'
|
||||
};
|
||||
|
||||
return [{ json: payload }];
|
||||
@@ -0,0 +1,37 @@
|
||||
// n8n Code Node: "Claude JSON extrahieren"
|
||||
// Erwartet die Antwort der Anthropic Messages API.
|
||||
|
||||
const response = $json;
|
||||
|
||||
if (response.stop_reason === 'refusal') {
|
||||
throw new Error('Claude hat die Anfrage abgelehnt. Es wird kein Report erzeugt.');
|
||||
}
|
||||
if (response.stop_reason === 'max_tokens') {
|
||||
throw new Error('Claude-Ausgabe wurde wegen max_tokens abgeschnitten. max_tokens erhoehen und erneut ausfuehren.');
|
||||
}
|
||||
const textBlock = Array.isArray(response.content)
|
||||
? response.content.find(block => block.type === 'text')
|
||||
: null;
|
||||
|
||||
if (!textBlock?.text) {
|
||||
throw new Error('Anthropic-Antwort enthaelt keinen Textblock.');
|
||||
}
|
||||
|
||||
let content;
|
||||
try {
|
||||
content = JSON.parse(textBlock.text);
|
||||
} catch (error) {
|
||||
throw new Error(`Claude-Ausgabe ist kein valides JSON: ${error.message}`);
|
||||
}
|
||||
|
||||
return [{
|
||||
json: {
|
||||
content,
|
||||
claudeMeta: {
|
||||
id: response.id,
|
||||
model: response.model,
|
||||
stop_reason: response.stop_reason,
|
||||
usage: response.usage
|
||||
}
|
||||
}
|
||||
}];
|
||||
@@ -0,0 +1,49 @@
|
||||
// n8n Code Node: "Monatsdaten vorbereiten"
|
||||
// Dieses Beispiel normalisiert eine DB-Zeile. Feldnamen an deine Tabellen anpassen.
|
||||
|
||||
const row = $json;
|
||||
const year = Number(row.report_year);
|
||||
const month = Number(row.report_month);
|
||||
const monthNames = ['Januar','Februar','Maerz','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
||||
|
||||
if (!Number.isInteger(year) || month < 1 || month > 12) {
|
||||
throw new Error('report_year/report_month ungueltig');
|
||||
}
|
||||
|
||||
const previous = new Date(Date.UTC(year, month - 2, 1));
|
||||
const previousLabel = `${monthNames[previous.getUTCMonth()]} ${previous.getUTCFullYear()}`;
|
||||
|
||||
return [{
|
||||
json: {
|
||||
publisher: row.publisher_name ?? 'Mein Reporting',
|
||||
customer: {
|
||||
id: row.customer_slug,
|
||||
name: row.customer_name,
|
||||
property_name: row.property_name,
|
||||
location: row.location,
|
||||
contact_line: row.contact_line ?? '',
|
||||
logo_data_uri: row.logo_data_uri || undefined,
|
||||
logo_text: row.logo_text ?? row.customer_name.slice(0, 2).toUpperCase()
|
||||
},
|
||||
design: {
|
||||
theme: row.theme ?? 'modern',
|
||||
primary_color: row.primary_color ?? '#1F5D66',
|
||||
secondary_color: row.secondary_color ?? '#2C7C82',
|
||||
accent_color: row.accent_color ?? '#DBA25D',
|
||||
text_color: row.text_color ?? '#24343A',
|
||||
muted_color: row.muted_color ?? '#718087',
|
||||
surface_color: row.surface_color ?? '#F2F6F5',
|
||||
font_family: row.font_family ?? 'Arial'
|
||||
},
|
||||
period: {
|
||||
year,
|
||||
month,
|
||||
label: `${monthNames[month - 1]} ${year}`,
|
||||
comparison_label: previousLabel
|
||||
},
|
||||
metrics: row.metrics,
|
||||
trends: row.trends,
|
||||
context: row.context ?? {},
|
||||
legal_note: row.legal_note
|
||||
}
|
||||
}];
|
||||
@@ -0,0 +1,49 @@
|
||||
-- 1. Aktive Kunden laden
|
||||
SELECT *
|
||||
FROM report_customer_config
|
||||
WHERE active = TRUE
|
||||
ORDER BY customer_id;
|
||||
|
||||
-- 2. Noch nicht erzeugte Reports eines Monats finden
|
||||
SELECT c.*
|
||||
FROM report_customer_config c
|
||||
WHERE c.active = TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM monthly_report_runs r
|
||||
WHERE r.customer_id = c.customer_id
|
||||
AND r.report_year = $1
|
||||
AND r.report_month = $2
|
||||
AND r.report_version = '1.0'
|
||||
AND r.status = 'completed'
|
||||
);
|
||||
|
||||
-- 3. Reportlauf reservieren
|
||||
INSERT INTO monthly_report_runs (
|
||||
customer_id, report_year, report_month, status,
|
||||
report_version, prompt_version, template_version
|
||||
)
|
||||
VALUES ($1, $2, $3, 'processing', '1.0', '1.0', '1.0')
|
||||
ON CONFLICT (customer_id, report_year, report_month, report_version)
|
||||
DO UPDATE SET
|
||||
status = 'processing',
|
||||
error_message = NULL,
|
||||
started_at = NOW(),
|
||||
completed_at = NULL
|
||||
RETURNING id;
|
||||
|
||||
-- 4. Erfolgreich abschliessen
|
||||
UPDATE monthly_report_runs
|
||||
SET status = 'completed',
|
||||
storage_path = $2,
|
||||
checksum_sha256 = $3,
|
||||
claude_model = $4,
|
||||
completed_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- 5. Fehler protokollieren
|
||||
UPDATE monthly_report_runs
|
||||
SET status = 'failed',
|
||||
error_message = LEFT($2, 5000),
|
||||
completed_at = NOW()
|
||||
WHERE id = $1;
|
||||
@@ -0,0 +1,42 @@
|
||||
CREATE TABLE IF NOT EXISTS report_customer_config (
|
||||
customer_id VARCHAR(64) PRIMARY KEY,
|
||||
customer_name VARCHAR(160) NOT NULL,
|
||||
property_name VARCHAR(160) NOT NULL,
|
||||
location VARCHAR(160) NOT NULL,
|
||||
contact_line VARCHAR(180) NOT NULL DEFAULT '',
|
||||
logo_data_uri TEXT,
|
||||
logo_text VARCHAR(5) NOT NULL,
|
||||
theme VARCHAR(20) NOT NULL DEFAULT 'modern' CHECK (theme IN ('modern', 'classic')),
|
||||
primary_color CHAR(7) NOT NULL DEFAULT '#1F5D66',
|
||||
secondary_color CHAR(7) NOT NULL DEFAULT '#2C7C82',
|
||||
accent_color CHAR(7) NOT NULL DEFAULT '#DBA25D',
|
||||
text_color CHAR(7) NOT NULL DEFAULT '#24343A',
|
||||
muted_color CHAR(7) NOT NULL DEFAULT '#718087',
|
||||
surface_color CHAR(7) NOT NULL DEFAULT '#F2F6F5',
|
||||
font_family VARCHAR(100) NOT NULL DEFAULT 'Arial',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS monthly_report_runs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
customer_id VARCHAR(64) NOT NULL REFERENCES report_customer_config(customer_id),
|
||||
report_year INTEGER NOT NULL CHECK (report_year BETWEEN 2020 AND 2100),
|
||||
report_month INTEGER NOT NULL CHECK (report_month BETWEEN 1 AND 12),
|
||||
status VARCHAR(20) NOT NULL CHECK (status IN ('queued', 'processing', 'completed', 'failed')),
|
||||
report_version VARCHAR(20) NOT NULL,
|
||||
prompt_version VARCHAR(20) NOT NULL,
|
||||
template_version VARCHAR(20) NOT NULL,
|
||||
claude_model VARCHAR(80),
|
||||
storage_path TEXT,
|
||||
checksum_sha256 CHAR(64),
|
||||
error_message TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (customer_id, report_year, report_month, report_version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_monthly_report_runs_status
|
||||
ON monthly_report_runs(status, report_year, report_month);
|
||||
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"name": "Monatsreport - Startertemplate",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "bce6507d-0a40-4c72-a054-3f3860f96601",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-760,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const sample = {\"report\": {\"report_version\": \"1.0\", \"prompt_version\": \"1.0\", \"template_version\": \"1.0\", \"generated_at\": \"2026-06-24T14:30:00+02:00\", \"publisher\": \"Fewo Performance Studio\", \"period\": {\"year\": 2026, \"month\": 5, \"label\": \"Mai 2026\", \"comparison_label\": \"April 2026\"}}, \"customer\": {\"id\": \"kuestenblick-001\", \"name\": \"Ferienapartments Kuestenblick\", \"property_name\": \"Apartment Seestern\", \"location\": \"Nordseekueste\", \"contact_line\": \"reporting@kuestenblick.example\", \"logo_data_uri\": \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNjAgMTYwJz48cmVjdCB3aWR0aD0nMTYwJyBoZWlnaHQ9JzE2MCcgcng9JzM1JyBmaWxsPScjZmZmZmZmJy8+PHBhdGggZD0nTTI4IDEwNWMyOC0xMCA1NC0xMCAxMDQgMCcgc3Ryb2tlPScjMWY1ZDY2JyBzdHJva2Utd2lkdGg9JzknIGZpbGw9J25vbmUnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcvPjxjaXJjbGUgY3g9JzgwJyBjeT0nNjInIHI9JzI4JyBmaWxsPScjZGJhMjVkJy8+PHBhdGggZD0nTTQ4IDYyaDY0JyBzdHJva2U9JyMxZjVkNjYnIHN0cm9rZS13aWR0aD0nOCcgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJy8+PC9zdmc+\", \"logo_text\": \"KB\"}, \"design\": {\"theme\": \"modern\", \"primary_color\": \"#1F5D66\", \"secondary_color\": \"#2C7C82\", \"accent_color\": \"#DBA25D\", \"text_color\": \"#24343A\", \"muted_color\": \"#718087\", \"surface_color\": \"#F2F6F5\", \"font_family\": \"Arial\"}, \"metrics\": {\"currency\": \"EUR\", \"revenue\": 6840.5, \"occupancy_rate\": 78.4, \"adr\": 126.68, \"revpar\": 99.32, \"bookings\": 19, \"nights_sold\": 54, \"average_stay\": 3.6, \"cancellation_rate\": 6.2, \"comparison\": {\"revenue_delta_pct\": 12.8, \"occupancy_delta_pp\": 5.7, \"adr_delta_pct\": 4.6, \"bookings_delta\": 2}}, \"trends\": [{\"label\": \"Dez 25\", \"revenue\": 4210.0, \"occupancy_rate\": 55.2, \"adr\": 112.3}, {\"label\": \"Jan 26\", \"revenue\": 3890.0, \"occupancy_rate\": 51.4, \"adr\": 109.85}, {\"label\": \"Feb 26\", \"revenue\": 4475.0, \"occupancy_rate\": 59.7, \"adr\": 114.2}, {\"label\": \"Mär 26\", \"revenue\": 5360.0, \"occupancy_rate\": 66.1, \"adr\": 118.75}, {\"label\": \"Apr 26\", \"revenue\": 6064.0, \"occupancy_rate\": 72.7, \"adr\": 121.1}, {\"label\": \"Mai 26\", \"revenue\": 6840.5, \"occupancy_rate\": 78.4, \"adr\": 126.68}], \"content\": {\"summary\": {\"headline\": \"Starker Umsatzmonat bei weiterhin gesunder Auslastung\", \"text\": \"Der Mai verbindet eine hohe Auslastung mit einem gestiegenen durchschnittlichen Tagespreis. Dadurch liegt der Umsatz klar ueber dem Vormonat, ohne dass die Buchungszahl im gleichen Umfang wachsen musste. Die Stornoquote bleibt beherrschbar. Fuer die kommenden Wochen sollte der Fokus deshalb auf einer feineren Preissteuerung an besonders nachgefragten Tagen liegen.\", \"rating\": \"positive\"}, \"developments\": [{\"title\": \"Umsatz gewinnt ueberproportional\", \"text\": \"Der Umsatz steigt staerker als die Buchungszahl. Das spricht dafuer, dass der hoehere Tagespreis und eine bessere Belegung der verfuegbaren Naechte den Monat getragen haben.\", \"impact\": \"positive\"}, {\"title\": \"Auslastung erreicht ein solides Niveau\", \"text\": \"Mit 78,4 Prozent ist ein grosser Teil der verfuegbaren Kapazitaet verkauft. Gleichzeitig verbleibt genug Spielraum, um einzelne schwache Wochentage gezielt zu stimulieren.\", \"impact\": \"positive\"}, {\"title\": \"Stornos bleiben ein Beobachtungspunkt\", \"text\": \"Die Stornoquote ist nicht kritisch, sollte aber nach Buchungskanal und Vorlaufzeit beobachtet werden. So laesst sich erkennen, ob bestimmte Tarife oder Plattformen ueberdurchschnittlich betroffen sind.\", \"impact\": \"neutral\"}, {\"title\": \"Aufenthaltsdauer bleibt kompakt\", \"text\": \"Die durchschnittliche Aufenthaltsdauer von 3,6 Naechten sorgt fuer regelmaessige Gaestewechsel. Das kann Umsatzchancen schaffen, erhoeht jedoch zugleich den operativen Aufwand je belegter Nacht.\", \"impact\": \"neutral\"}], \"recommendations\": [{\"priority\": 1, \"category\": \"Preisgestaltung\", \"title\": \"Nachfragestarke Wochenenden differenzierter bepreisen\", \"reason\": \"Umsatz, Tagespreis und Auslastung entwickeln sich gleichzeitig positiv. Dadurch besteht an stark nachgefragten Terminen Spielraum, den Preis vorsichtig weiter auszureizen.\", \"action\": \"Fuer Freitage, Samstage und lokale Spitzentermine einen Preisaufschlag von zunaechst 5 Prozent testen und die Buchungsgeschwindigkeit wochenweise kontrollieren.\", \"expected_effect\": \"Hoeherer Umsatz pro verfuegbarer Nacht ohne pauschale Preiserhoehung fuer den gesamten Monat.\"}, {\"priority\": 2, \"category\": \"Verfuegbarkeit & Buchungssteuerung\", \"title\": \"Schwache Wochentage mit Mindestaufenthalten entlasten\", \"reason\": \"Die Auslastung ist insgesamt gut, dennoch koennen einzelne Luecken zwischen kurzen Aufenthalten entstehen und wertvolle Naechte blockieren.\", \"action\": \"Fuer ausgewaehlte Anreisetage flexible Mindestaufenthalte einsetzen und kurze Restluecken gezielt fuer Zwei-Nacht-Buchungen freigeben.\", \"expected_effect\": \"WenigLine truncated
|
||||
},
|
||||
"id": "049ac065-2b1f-4ae6-815d-29c44a34df01",
|
||||
"name": "Monatsdaten vorbereiten",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-540,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// n8n Code Node: \"Claude Request bauen\"\n// Erwartet: customer, period, metrics, trends und optional context.\n// Das Schema ist fuer die rohe Messages API vereinfacht. Die strengeren\n// Laengen- und Wertepruefungen erfolgen anschliessend im Report Renderer.\n\nconst input = $json;\nconst schema = {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"summary\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"headline\": {\"type\": \"string\", \"description\": \"Minimum characters: 8. Maximum characters: 110.\"}, \"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 80. Maximum characters: 900.\"}, \"rating\": {\"type\": \"string\", \"enum\": [\"positive\", \"neutral\", \"watch\"]}}, \"required\": [\"headline\", \"text\", \"rating\"]}, \"developments\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 40. Maximum characters: 450.\"}, \"impact\": {\"type\": \"string\", \"enum\": [\"positive\", \"neutral\", \"negative\"]}}, \"required\": [\"title\", \"text\", \"impact\"]}, \"description\": \"Minimum items: 2. Maximum items: 4.\"}, \"recommendations\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"priority\": {\"type\": \"integer\", \"description\": \"Minimum value: 1. Maximum value: 3.\"}, \"category\": {\"type\": \"string\", \"description\": \"Minimum characters: 3. Maximum characters: 80.\"}, \"reason\": {\"type\": \"string\", \"description\": \"Minimum characters: 40. Maximum characters: 450.\"}, \"action\": {\"type\": \"string\", \"description\": \"Minimum characters: 30. Maximum characters: 450.\"}, \"expected_effect\": {\"type\": \"string\", \"description\": \"Minimum characters: 20. Maximum characters: 300.\"}}, \"required\": [\"priority\", \"category\", \"title\", \"reason\", \"action\", \"expected_effect\"]}, \"description\": \"Minimum items: 1. Maximum items: 3.\"}, \"outlook\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"headline\": {\"type\": \"string\", \"description\": \"Minimum characters: 5. Maximum characters: 100.\"}, \"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 60. Maximum characters: 700.\"}}, \"required\": [\"headline\", \"text\"]}, \"data_quality_notes\": {\"type\": \"array\", \"items\": {\"type\": \"string\", \"description\": \"Minimum characters: 5. Maximum characters: 240.\"}, \"description\": \"Maximum items: 4.\"}}, \"required\": [\"summary\", \"developments\", \"recommendations\", \"outlook\", \"data_quality_notes\"]};\n\nconst systemPrompt = `Du erstellst die redaktionellen Inhalte eines monatlichen Performance-Reports fuer Betreiber von Ferienunterkuenften.\nNutze ausschliesslich die gelieferten Daten. Erfinde keine Zahlen oder Ursachen.\nJede Empfehlung muss konkret und aus den Daten ableitbar sein.\nFormuliere professionell, klar und auf Deutsch. Vermeide Marketingfloskeln und Wiederholungen.\nErzeuge 2 bis 4 Entwicklungen, 1 bis 3 Empfehlungen und hoechstens 4 Datenhinweise.\nNutze data_quality_notes fuer fehlende oder eingeschraenkte Datengrundlagen.`;\n\nconst userPrompt = `Erstelle den Monatsreport fuer diese Daten:\n\n${JSON.stringify({\n customer: input.customer,\n period: input.period,\n metrics: input.metrics,\n trends: input.trends,\n context: input.context ?? {}\n}, null, 2)}`;\n\nreturn [{\n json: {\n ...input,\n claudeRequest: {\n model: input.claudeModel ?? 'claude-sonnet-4-6',\n max_tokens: 3000,\n temperature: 0.2,\n system: systemPrompt,\n messages: [{ role: 'user', content: userPrompt }],\n output_config: {\n format: { type: 'json_schema', schema }\n }\n }\n }\n}];\n"
|
||||
},
|
||||
"id": "6a1c5321-97ac-453f-80b5-0f6782d12103",
|
||||
"name": "Claude Request bauen",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-300,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.ANTHROPIC_API_KEY }}"
|
||||
},
|
||||
{
|
||||
"name": "anthropic-version",
|
||||
"value": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"contentType": "raw",
|
||||
"rawContentType": "application/json",
|
||||
"body": "={{ JSON.stringify($json.claudeRequest) }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "8efe292a-6210-4ea7-8d4d-5d2c009e2de2",
|
||||
"name": "Claude Report erzeugen",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
-40,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// n8n Code Node: \"Claude JSON extrahieren\"\n// Erwartet die Antwort der Anthropic Messages API.\n\nconst response = $json;\n\nif (response.stop_reason === 'refusal') {\n throw new Error('Claude hat die Anfrage abgelehnt. Es wird kein Report erzeugt.');\n}\nif (response.stop_reason === 'max_tokens') {\n throw new Error('Claude-Ausgabe wurde wegen max_tokens abgeschnitten. max_tokens erhoehen und erneut ausfuehren.');\n}\nconst textBlock = Array.isArray(response.content)\n ? response.content.find(block => block.type === 'text')\n : null;\n\nif (!textBlock?.text) {\n throw new Error('Anthropic-Antwort enthaelt keinen Textblock.');\n}\n\nlet content;\ntry {\n content = JSON.parse(textBlock.text);\n} catch (error) {\n throw new Error(`Claude-Ausgabe ist kein valides JSON: ${error.message}`);\n}\n\nreturn [{\n json: {\n content,\n claudeMeta: {\n id: response.id,\n model: response.model,\n stop_reason: response.stop_reason,\n usage: response.usage\n }\n }\n}];\n"
|
||||
},
|
||||
"id": "b254c588-e563-424e-b43c-0607b48a029a",
|
||||
"name": "Claude JSON extrahieren",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
210,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// n8n Code Node: \"Render Payload bauen\"\n// Passe die Node-Namen unten an deinen Workflow an.\n\nconst source = $('Monatsdaten vorbereiten').first().json;\nconst claude = $('Claude JSON extrahieren').first().json;\n\nconst now = new Date().toISOString();\n\nconst payload = {\n report: {\n report_version: '1.0',\n prompt_version: '1.0',\n template_version: '1.0',\n generated_at: now,\n publisher: source.publisher ?? 'Mein Reporting',\n period: source.period\n },\n customer: source.customer,\n design: source.design,\n metrics: source.metrics,\n trends: source.trends,\n content: claude.content,\n legal_note: source.legal_note ?? 'Dieser Report ist eine datenbasierte Entscheidungshilfe und keine Garantie fuer zukuenftige Ergebnisse.'\n};\n\nreturn [{ json: payload }];\n"
|
||||
},
|
||||
"id": "b6b414aa-c65d-4ce4-8147-7508ab63cc04",
|
||||
"name": "Render Payload bauen",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
460,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://report-renderer:8080/render",
|
||||
"sendBody": true,
|
||||
"contentType": "raw",
|
||||
"rawContentType": "application/json",
|
||||
"body": "={{ JSON.stringify($json) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file",
|
||||
"outputPropertyName": "data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "166260b5-0b58-421c-ac87-6696c293968c",
|
||||
"name": "PDF rendern",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
710,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Monatsdaten vorbereiten",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Monatsdaten vorbereiten": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Claude Request bauen",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Claude Request bauen": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Claude Report erzeugen",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Claude Report erzeugen": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Claude JSON extrahieren",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Claude JSON extrahieren": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Render Payload bauen",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Render Payload bauen": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PDF rendern",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"active": false,
|
||||
"versionId": "d6a55fb4-d389-48de-9ba5-4e63c6e0d552",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
Reference in new issue
Block a user