fix: resolve MariaDB collation error by switching from mysql-connector to PyMySQL

- Replace mysql-connector-python with PyMySQL driver for better MariaDB compatibility
- PyMySQL handles utf8mb4_0900_ai_ci collation properly without errors
- Update Dockerfile.sql-executor to install PyMySQL and psycopg2-binary
- Refactor sql-query-executor.py to use PyMySQL API (pymysql.connect, DictCursor)
- Verified sql-executor service with SELECT, INSERT, UPDATE operations on Freescout DB
- Add n8n workflow definitions: workflow-a-http.json and workflow-b-http.json
  * Workflow A: Polls unprocessed conversations, analyzes with LiteLLM, saves suggestions
  * Workflow B: Polls approved suggestions, executes Baramundi jobs or email replies
- Update compose.yaml with sql-executor service configuration and dependencies

All SQL operations now execute successfully against MariaDB 11.3.2
This commit is contained in:
Claude Code
2026-03-17 09:31:03 +01:00
parent 3a7dfeb09a
commit 96d70d9edf
5 changed files with 854 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
{
"name": "Workflow A - Mail Processing (HTTP)",
"description": "Fetch unprocessed conversations from Freescout, analyze with AI, save suggestions",
"nodes": [
{
"id": "uuid-trigger-1",
"name": "Trigger",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [250, 200],
"parameters": {
"cronExpression": "*/5 * * * *"
}
},
{
"id": "uuid-get-conversations",
"name": "Get Unprocessed Conversations",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [450, 200],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"SELECT c.id, c.number, c.subject, c.customer_email, c.status, GROUP_CONCAT(t.body SEPARATOR '\\\\n') as threads_text FROM conversations c LEFT JOIN threads t ON c.id = t.conversation_id WHERE c.status = 1 AND c.id NOT IN (SELECT DISTINCT conversation_id FROM conversation_custom_field WHERE custom_field_id = 8) GROUP BY c.id LIMIT 20\"}"
}
},
{
"id": "uuid-split-results",
"name": "Split Results",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [650, 200],
"parameters": {
"batchSize": 1,
"options": {}
}
},
{
"id": "uuid-extract-data",
"name": "Extract Conversation Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [850, 200],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "ticket_id",
"value": "={{ $json.id }}",
"type": "number"
},
{
"name": "ticket_number",
"value": "={{ $json.number }}",
"type": "number"
},
{
"name": "subject",
"value": "={{ $json.subject }}",
"type": "string"
},
{
"name": "problem_text",
"value": "={{ ($json.threads_text || 'No description provided').substring(0, 2000) }}",
"type": "string"
},
{
"name": "customer_email",
"value": "={{ $json.customer_email }}",
"type": "string"
}
]
}
}
},
{
"id": "uuid-llm-analyze",
"name": "LiteLLM AI Analysis",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1050, 200],
"parameters": {
"url": "http://llm.eks-ai.apps.asgard.eks-lnx.fft-it.de/v1/chat/completions",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"model\":\"gpt-3.5-turbo\",\"messages\":[{\"role\":\"system\",\"content\":\"Du bist ein IT-Support-Assistent. Analysiere das folgende IT-Support-Ticket und gib eine strukturierte JSON-Antwort mit folgenden Feldern: kategorie (z.B. Hardware, Software, Netzwerk, Zugriff), lösung_typ (BARAMUNDI_JOB, AUTOMATISCHE_ANTWORT, oder ESKALATION), vertrauen (Dezimal zwischen 0.0 und 1.0 - wie sicher bist du bei dieser Lösung), baramundi_job (Name des Jobs falls BARAMUNDI_JOB), antwort_text (Die Antwort an den Nutzer), begründung (Kurze Erklärung deiner Analyse)\"},{\"role\":\"user\",\"content\":\"Ticket-Nummer: {{$json.ticket_number}}\\nBetreff: {{$json.subject}}\\nProblembeschreibung:\\n{{$json.problem_text}}\\n\\nBitte antworte NUR mit gültiger JSON in dieser Struktur: {\\\"kategorie\\\": \\\"...\\\", \\\"lösung_typ\\\": \\\"...\\\", \\\"vertrauen\\\": 0.75, \\\"baramundi_job\\\": \\\"...\\\", \\\"antwort_text\\\": \\\"...\\\", \\\"begründung\\\": \\\"...\\\"}\"}],\"temperature\":0.7,\"max_tokens\":1000}"
}
},
{
"id": "uuid-parse-response",
"name": "Parse AI Response",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [1250, 200],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "response_text",
"value": "={{ $json.choices[0].message.content }}",
"type": "string"
},
{
"name": "ai_response",
"value": "={{ JSON.parse($json.response_text) }}",
"type": "object"
}
]
}
}
},
{
"id": "uuid-check-confidence",
"name": "Check Confidence >= 0.6",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [1450, 200],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"extractValue": false
},
"combinator": "and",
"conditions": [
{
"id": "condition_1",
"leftValue": "={{ $json.ai_response.vertrauen }}",
"rightValue": 0.6,
"operator": {
"name": "filter.operator.gte",
"value": ">="
}
}
]
}
}
},
{
"id": "uuid-save-to-db",
"name": "Save Suggestion to Freescout DB",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1650, 100],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 6, '{{$json.ai_response | json.stringify}}') ON DUPLICATE KEY UPDATE value = VALUES(value); INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 7, 'PENDING') ON DUPLICATE KEY UPDATE value = VALUES(value); INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 8, '1') ON DUPLICATE KEY UPDATE value = VALUES(value);\"}"
}
},
{
"id": "uuid-no-action",
"name": "Skip - Low Confidence",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [1650, 350],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "skipped",
"value": true,
"type": "boolean"
},
{
"name": "reason",
"value": "Confidence {{$json.ai_response.vertrauen}} < 0.6",
"type": "string"
}
]
}
}
}
],
"connections": {
"Trigger": {
"main": [
[
{
"node": "Get Unprocessed Conversations",
"index": 0
}
]
]
},
"Get Unprocessed Conversations": {
"main": [
[
{
"node": "Split Results",
"index": 0
}
]
]
},
"Split Results": {
"main": [
[
{
"node": "Extract Conversation Data",
"index": 0
}
]
]
},
"Extract Conversation Data": {
"main": [
[
{
"node": "LiteLLM AI Analysis",
"index": 0
}
]
]
},
"LiteLLM AI Analysis": {
"main": [
[
{
"node": "Parse AI Response",
"index": 0
}
]
]
},
"Parse AI Response": {
"main": [
[
{
"node": "Check Confidence >= 0.6",
"index": 0
}
]
]
},
"Check Confidence >= 0.6": {
"main": [
[
{
"node": "Save Suggestion to Freescout DB",
"index": 0
}
],
[
{
"node": "Skip - Low Confidence",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"errorHandler": "continueOnError"
}
}

View File

@@ -0,0 +1,337 @@
{
"name": "Workflow B - Approval & Execution (HTTP)",
"description": "Poll for approved AI suggestions and execute them (Baramundi jobs or email replies)",
"nodes": [
{
"id": "uuid-trigger-b",
"name": "Trigger",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [250, 200],
"parameters": {
"cronExpression": "*/2 * * * *"
}
},
{
"id": "uuid-get-approved",
"name": "Get Approved Conversations",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [450, 200],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"SELECT c.id, c.number, c.subject, c.customer_email, ccf.value as ai_suggestion FROM conversations c JOIN conversation_custom_field ccf ON c.id = ccf.conversation_id WHERE ccf.custom_field_id = 7 AND ccf.value = 'APPROVED' LIMIT 10\"}"
}
},
{
"id": "uuid-split-approved",
"name": "Split Results",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [650, 200],
"parameters": {
"batchSize": 1,
"options": {}
}
},
{
"id": "uuid-extract-approved",
"name": "Extract & Parse Suggestion",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [850, 200],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "ticket_id",
"value": "={{ $json.id }}",
"type": "number"
},
{
"name": "ticket_number",
"value": "={{ $json.number }}",
"type": "number"
},
{
"name": "subject",
"value": "={{ $json.subject }}",
"type": "string"
},
{
"name": "customer_email",
"value": "={{ $json.customer_email }}",
"type": "string"
},
{
"name": "ai_suggestion_raw",
"value": "={{ typeof $json.ai_suggestion === 'string' ? $json.ai_suggestion : JSON.stringify($json.ai_suggestion) }}",
"type": "string"
},
{
"name": "ai_suggestion",
"value": "={{ typeof $json.ai_suggestion === 'string' ? JSON.parse($json.ai_suggestion) : $json.ai_suggestion }}",
"type": "object"
},
{
"name": "solution_type",
"value": "={{ $json.ai_suggestion.lösung_typ || 'UNKNOWN' }}",
"type": "string"
}
]
}
}
},
{
"id": "uuid-decide-solution",
"name": "Decide Solution Type",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [1050, 200],
"parameters": {
"options": [
{
"condition": "equal",
"value1": "={{ $json.solution_type }}",
"value2": "BARAMUNDI_JOB"
},
{
"condition": "equal",
"value1": "={{ $json.solution_type }}",
"value2": "AUTOMATISCHE_ANTWORT"
}
]
}
},
{
"id": "uuid-execute-baramundi",
"name": "Execute Baramundi Job",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1250, 50],
"parameters": {
"url": "https://baramundi-api.example.com/api/jobs",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BARAMUNDI_TOKEN"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"job_name\":\"{{$json.ai_suggestion.baramundi_job}}\",\"ticket_id\":{{$json.ticket_id}},\"target_system\":\"IT\",\"description\":\"{{$json.subject}}\"}"
}
},
{
"id": "uuid-send-email",
"name": "Send Email Reply",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1250, 150],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"INSERT INTO threads (conversation_id, customer_id, user_id, type, status, body, created_at, updated_at) VALUES ({{$json.ticket_id}}, (SELECT customer_id FROM conversations WHERE id = {{$json.ticket_id}} LIMIT 1), NULL, 'customer', 'active', '{{$json.ai_suggestion.antwort_text | replace(\\\"'\\\", \\\"''\\\")}}', NOW(), NOW())\"}"
}
},
{
"id": "uuid-mark-escalation",
"name": "Mark for Manual Review",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [1250, 270],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "action",
"value": "Manual escalation required",
"type": "string"
}
]
}
}
},
{
"id": "uuid-update-status",
"name": "Update Status to EXECUTED",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1450, 200],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"UPDATE conversation_custom_field SET value = 'EXECUTED' WHERE conversation_id = {{$json.ticket_id}} AND custom_field_id = 7\"}"
}
},
{
"id": "uuid-trigger-workflow-c",
"name": "Trigger Workflow C (KB Update)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1650, 200],
"parameters": {
"url": "https://n8n.fft-it.de/webhook/workflow-c",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"ticket_id\":{{$json.ticket_id}},\"subject\":\"{{$json.subject}}\",\"problem\":\"{{$json.subject}}\",\"solution\":\"{{$json.ai_suggestion.antwort_text}}\",\"category\":\"{{$json.ai_suggestion.kategorie}}\",\"solution_type\":\"{{$json.solution_type}}\"}"
}
},
{
"id": "uuid-log-audit",
"name": "Log to PostgreSQL",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [1850, 200],
"parameters": {
"url": "http://host.docker.internal:4000/query/audit",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"INSERT INTO workflow_executions (workflow_name, ticket_id, status, execution_time_ms, created_at) VALUES ('Workflow B - Approval Execution', {{$json.ticket_id}}, 'SUCCESS', 0, NOW())\"}"
}
}
],
"connections": {
"Trigger": {
"main": [
[
{
"node": "Get Approved Conversations",
"index": 0
}
]
]
},
"Get Approved Conversations": {
"main": [
[
{
"node": "Split Results",
"index": 0
}
]
]
},
"Split Results": {
"main": [
[
{
"node": "Extract & Parse Suggestion",
"index": 0
}
]
]
},
"Extract & Parse Suggestion": {
"main": [
[
{
"node": "Decide Solution Type",
"index": 0
}
]
]
},
"Decide Solution Type": {
"main": [
[
{
"node": "Execute Baramundi Job",
"index": 0
}
],
[
{
"node": "Send Email Reply",
"index": 0
}
],
[
{
"node": "Mark for Manual Review",
"index": 0
}
]
]
},
"Execute Baramundi Job": {
"main": [
[
{
"node": "Update Status to EXECUTED",
"index": 0
}
]
]
},
"Send Email Reply": {
"main": [
[
{
"node": "Update Status to EXECUTED",
"index": 0
}
]
]
},
"Mark for Manual Review": {
"main": [
[
{
"node": "Update Status to EXECUTED",
"index": 0
}
]
]
},
"Update Status to EXECUTED": {
"main": [
[
{
"node": "Trigger Workflow C (KB Update)",
"index": 0
}
]
]
},
"Trigger Workflow C (KB Update)": {
"main": [
[
{
"node": "Log to PostgreSQL",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"errorHandler": "continueOnError"
}
}